Skip to content

Commit d3cdf11

Browse files
committed
v1.2.52: Fix backend crash in .exe — add missing hidden imports + error logging to file
1 parent b3baeed commit d3cdf11

7 files changed

Lines changed: 127 additions & 27 deletions

File tree

.github/workflows/release.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ jobs:
5353
--collect-all uvicorn \
5454
--collect-all fastapi \
5555
--collect-all wsproto \
56+
--collect-all requests \
57+
--collect-all pydantic \
5658
--hidden-import=uvicorn.logging \
5759
--hidden-import=uvicorn.loops \
5860
--hidden-import=uvicorn.loops.auto \
@@ -63,6 +65,13 @@ jobs:
6365
--hidden-import=uvicorn.protocols.websockets.auto \
6466
--hidden-import=uvicorn.lifespan \
6567
--hidden-import=uvicorn.lifespan.on \
68+
--hidden-import=starlette \
69+
--hidden-import=starlette.routing \
70+
--hidden-import=starlette.middleware \
71+
--hidden-import=starlette.requests \
72+
--hidden-import=starlette.responses \
73+
--hidden-import=multipart \
74+
--hidden-import=python_multipart \
6675
api_server.py
6776
if [ "${{ matrix.os }}" = "windows-latest" ]; then
6877
mv dist/api_server.exe .
@@ -136,6 +145,8 @@ jobs:
136145
--collect-all uvicorn \
137146
--collect-all fastapi \
138147
--collect-all wsproto \
148+
--collect-all requests \
149+
--collect-all pydantic \
139150
--hidden-import=uvicorn.logging \
140151
--hidden-import=uvicorn.loops \
141152
--hidden-import=uvicorn.loops.auto \
@@ -146,6 +157,13 @@ jobs:
146157
--hidden-import=uvicorn.protocols.websockets.auto \
147158
--hidden-import=uvicorn.lifespan \
148159
--hidden-import=uvicorn.lifespan.on \
160+
--hidden-import=starlette \
161+
--hidden-import=starlette.routing \
162+
--hidden-import=starlette.middleware \
163+
--hidden-import=starlette.requests \
164+
--hidden-import=starlette.responses \
165+
--hidden-import=multipart \
166+
--hidden-import=python_multipart \
149167
api_server.py
150168
if [ "${{ matrix.os }}" = "windows-latest" ]; then
151169
mv dist/api_server.exe .

backend/api_server.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
# --- CRITICAL: REDIRECT STREAMS IN FROZEN EXE ---
66
# If running as a PyInstaller --noconsole EXE, stdout/stderr are None, which crashes many libraries.
77
if getattr(sys, "frozen", False) and sys.platform == "win32":
8-
null_device = open(os.devnull, "w")
9-
sys.stdout = null_device
10-
sys.stderr = null_device
11-
# Also ensure freeze_support is called as early as possible
8+
log_dir = os.path.join(os.getenv("APPDATA", ""), "MinecraftServerGUI")
9+
os.makedirs(log_dir, exist_ok=True)
10+
log_file = open(os.path.join(log_dir, "backend_crash.log"), "w", encoding="utf-8")
11+
sys.stdout = log_file
12+
sys.stderr = log_file
1213
multiprocessing.freeze_support()
1314

1415
import asyncio

backend/server/server_handler.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,9 @@ def delayed_kill():
975975
player_name = leave_match.group(1)
976976
self.tracked_players.discard(player_name)
977977

978+
# Detect common server errors and broadcast structured events
979+
self._detect_and_broadcast_errors(line_no_ansi)
980+
978981
clean_line = line_no_ansi.strip()
979982
suppress_from_console = False
980983
if clean_line:
@@ -1205,6 +1208,45 @@ def wait_for_stop(self, timeout=30):
12051208
self._log(f"Error while waiting for server to stop: {e}\n", "error")
12061209
return False
12071210

1211+
def _detect_and_broadcast_errors(self, line):
1212+
"""Detect common server errors and broadcast structured events."""
1213+
if not self.output_callback:
1214+
return
1215+
error_info = None
1216+
1217+
# Mod dependency missing (NoClassDefFoundError)
1218+
m = re.search(r"NoClassDefFoundError:\s*(\S+)", line)
1219+
if m:
1220+
cls = m.group(1)
1221+
# Map class to mod name
1222+
error_info = {"type": "server_error", "error": "mod_dependency", "detail": cls}
1223+
if "geckolib" in cls.lower() or "software.bernie" in cls.lower():
1224+
error_info["mod"] = "GeckoLib"
1225+
error_info["fix"] = "Install GeckoLib from the Mods browser"
1226+
else:
1227+
error_info["mod"] = cls.split(".")[-1]
1228+
error_info["fix"] = f"Search and install the mod that provides '{cls}'"
1229+
1230+
# Java version error
1231+
if not error_info and re.search(r"UnsupportedClassVersionError|javax\.net\.ssl", line):
1232+
error_info = {"type": "server_error", "error": "java_version", "fix": "Update Java to the required version (Settings > System)"}
1233+
1234+
# Port conflict
1235+
if not error_info and re.search(r"Address already in use|BindException", line):
1236+
error_info = {"type": "server_error", "error": "port_conflict", "fix": "Change the server port in Settings > Network, or close the program using this port"}
1237+
1238+
# Out of memory
1239+
if not error_info and re.search(r"OutOfMemoryError", line):
1240+
error_info = {"type": "server_error", "error": "out_of_memory", "fix": "Increase RAM allocation in Settings > System > Max RAM"}
1241+
1242+
# Mod loading failure
1243+
if not error_info and re.search(r"Failed to start|LoadingFailedException", line):
1244+
error_info = {"type": "server_error", "error": "mod_loading", "fix": "Check the console logs for which mod failed, or remove recently added mods"}
1245+
1246+
if error_info:
1247+
error_info["server_id"] = self.server_id
1248+
self.output_callback(error_info)
1249+
12081250
def send_command(self, command, silent: bool = False):
12091251
if (
12101252
self.server_process

changelog.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## Version 1.2.5 - 2026-05-28 DNS PROXY, FIXED ADDRESSES & CONSOLE
44

5+
### CRITICAL FIX:
6+
- **Fixed backend crash on startup (.exe)** — Added missing hidden imports (`requests`, `pydantic`, `starlette`, `multipart`) to PyInstaller build
7+
- **Error logging** — Backend crash logs are now saved to `%APPDATA%/MinecraftServerGUI/backend_crash.log`
8+
9+
### ERROR FEEDBACK (NEW):
10+
- **Server error detection** — Common errors (missing mods, Java version, port conflicts, OOM) are detected and shown as structured error banners with fix suggestions
11+
- **Mod dependency warnings** — When installing a mod that requires other mods, shows a confirmation dialog
12+
- **Banner in Dashboard** — Clear, actionable error messages with dismiss button
13+
514
### NEW: Fixed Server Addresses (DNS)
615
- **Permanent domain** — Each server gets a permanent address like `survival.play.tudominio.app` that never changes
716
- **Auto-generated** — Unique subdomain generated automatically from server name + ID (`vanilla-a3f2b`)

electron-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "minecraft-server-gui",
33
"private": true,
4-
"version": "1.2.51",
4+
"version": "1.2.52",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/CalaKuad1/Minecraft-Local-Server-GUI.git"

electron-app/src/components/Dashboard.jsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
435435
const [dnsSubdomain, setDnsSubdomain] = useState('');
436436
const [onlineMode, setOnlineMode] = useState(true);
437437
const [togglingMode, setTogglingMode] = useState(false);
438+
const [serverError, setServerError] = useState(null);
438439

439440
const { isConnected, subscribe, send } = useWebSocket();
440441
const logsEndRef = useRef(null);
@@ -450,6 +451,7 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
450451
isStoppingRef.current = false;
451452
if (onRefresh) onRefresh();
452453
} else if (item.status === 'online') {
454+
setServerError(null);
453455
if (onRefresh) onRefresh();
454456
}
455457
return;
@@ -487,6 +489,11 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
487489
return;
488490
}
489491

492+
if (item.type === 'server_error') {
493+
setServerError(item);
494+
return;
495+
}
496+
490497
if (item.message !== undefined || item.level) {
491498
const msgText = typeof item.message === 'string' ? item.message : JSON.stringify(item.message || '');
492499

@@ -731,6 +738,37 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
731738
</div>
732739
</div>
733740

741+
{/* Error Banner */}
742+
{serverError && (
743+
<div className="rounded-sm border border-red-500/20 bg-red-500/5 p-4 relative overflow-hidden">
744+
<div className="flex items-start gap-3">
745+
<div className="p-1.5 rounded-sm bg-red-500/10 text-red-400 shrink-0 mt-0.5">
746+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
747+
</div>
748+
<div className="flex-1 min-w-0">
749+
<div className="text-xs font-minecraft tracking-wider uppercase text-red-400 mb-0.5">
750+
{serverError.error === 'mod_dependency' ? 'Missing Mod Dependency' :
751+
serverError.error === 'java_version' ? 'Java Version Error' :
752+
serverError.error === 'port_conflict' ? 'Port Conflict' :
753+
serverError.error === 'out_of_memory' ? 'Out of Memory' :
754+
serverError.error === 'mod_loading' ? 'Mod Loading Error' :
755+
'Server Error'}
756+
</div>
757+
<div className="text-[11px] text-zinc-400 leading-relaxed">
758+
{serverError.fix || 'Unknown error. Check the console for details.'}
759+
{serverError.mod && <span className="text-zinc-500 ml-1">{serverError.mod}</span>}
760+
</div>
761+
<div className="text-[9px] text-zinc-600 font-mono mt-1 truncate">{serverError.detail}</div>
762+
</div>
763+
<button onClick={() => setServerError(null)} className="p-1 rounded-sm text-zinc-500 hover:text-white hover:bg-white/5 transition-colors shrink-0">
764+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
765+
</button>
766+
</div>
767+
{/* Dismiss hint */}
768+
<div className="absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-red-500/[0.02] to-transparent pointer-events-none"></div>
769+
</div>
770+
)}
771+
734772
{/* Tunnel Section */}
735773
<div className="p-4 bg-[#18181b]/60 border border-white/5 rounded-sm relative z-50 backdrop-blur-2xl">
736774
<div className="flex items-center gap-4">

electron-app/src/components/Mods.jsx

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -87,37 +87,29 @@ export default function Mods({ status, onOpenWizard }) {
8787
};
8888

8989
const handleInstall = async (mod) => {
90-
// Find best version if we can, or just install what we have?
91-
// NOTE: 'mod' here is a search result. It has 'project_id' or 'slug'.
92-
// We actually need a SPECIFIC FILE VERSION ID.
93-
// The search result usually contains `latest_version` string or we should hit `getModVersions`.
94-
//
95-
// FOR NOW: Let's assume we fetch versions first to be safe, OR if the search result allows direct install (unlikely).
96-
// Modrinth Search Result -> slug.
97-
98-
// 1. Fetch versions
99-
// 2. Install latest compatible
100-
10190
try {
102-
setInstalling(prev => ({ ...prev, [mod.slug]: true })); // Use slug as key
91+
setInstalling(prev => ({ ...prev, [mod.slug]: true }));
10392

104-
// Fetch versions
10593
const versions = await api.getModVersions(mod.slug, activeLoader, activeVersion);
10694
if (!versions || versions.length === 0) {
107-
// Try fetching "any" version just in case filters were too strict
108-
// Or just error
10995
throw new Error("No compatible versions found for this server.");
11096
}
11197

112-
const targetVersion = versions[0]; // First one is usually latest
98+
const targetVersion = versions[0];
11399

114-
// Trigger install
115-
// The backend now runs async and sends WS progress
116-
await api.installMod(targetVersion.id);
100+
// Check for required dependencies
101+
const reqDeps = targetVersion.dependencies?.filter(d => d.dependency_type === 'required') || [];
102+
if (reqDeps.length > 0) {
103+
const depNames = reqDeps.map(d => d.project_id).join(', ');
104+
const proceed = await dialog.confirm(
105+
`${mod.title || mod.slug} requires ${reqDeps.length} mod${reqDeps.length > 1 ? 's' : ''}.\n\nRequired: ${reqDeps.map(d => d.file_name || d.project_id).join(', ')}\n\nInstall ${mod.title || mod.slug} anyway? (Dependencies must be installed separately)`,
106+
"Required Dependencies",
107+
{ variant: "warning", confirmLabel: "Install Anyway", cancelLabel: "Cancel" }
108+
);
109+
if (!proceed) { setInstalling(prev => ({ ...prev, [mod.slug]: false })); return; }
110+
}
117111

118-
// We don't await completion here anymore, we watch WS.
119-
// But we can keep state 'installing' until we get a completion event?
120-
// For now, let's rely on standard WS logs or optimistically say "Started".
112+
await api.installMod(targetVersion.id);
121113

122114
} catch (err) {
123115
console.error(err);

0 commit comments

Comments
 (0)