From 6973e7d60cb51ae4abbeb3ed81edada18ab26789 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 17 Jul 2026 20:38:36 -0500 Subject: [PATCH] Launcher: kill whole process tree on stop; fix Win10 install.bat icacls Two field issues from the live rollout, both launcher-side. 1) Console "Stop" did nothing. Every Tesla game runs under a supervisor that stays alive and respawns the game (Firestorm -> launcher.exe, Red Planet -> a looping .bat under cmd.exe, tesla410revival -> pod-launch.exe -> dosbox). net40's Process.Kill() terminates only the tracked supervisor PID, so the game survived (and the supervisor/loop relaunched it). New KillProcessTree uses `taskkill /PID /T /F` (whole tree; XP Pro + Win10/11), Process.Kill() as fallback. All three kill paths (KillApp/KillAllOfType/KillAllApps) now untrack under the lock and tree-kill outside it, so the RPC lock isn't held across taskkill and our auto-restart watcher won't relaunch. Proven against a real supervisor->child: old Kill orphaned the child; tree-kill takes both. 2) install.bat threw "(CI)M was unexpected at this time" at [1/7] on Windows 10. The icacls `/grant *S-1-5-32-545:(OI)(CI)M` sat inside an `else ( ... )` block; cmd read the literal ) in (OI)(CI) as the block end. XP was fine (its branch uses cacls, no parens). Quote the grant token at all three icacls sites; move the explanatory notes above the blocks (a stray ) even in a rem inside ( ) is the same trap). Verified on Win11: as-shipped reproduces the error, fixed form parses (rc=0) and applies the identical ACE. Co-Authored-By: Claude Fable 5 --- Launcher/TeslaLauncher.cs | 67 +++++++++++++++++++++++++++++++++++---- Launcher/install.bat | 11 +++++-- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/Launcher/TeslaLauncher.cs b/Launcher/TeslaLauncher.cs index c8a4d69..5e24765 100644 --- a/Launcher/TeslaLauncher.cs +++ b/Launcher/TeslaLauncher.cs @@ -502,6 +502,11 @@ namespace Tesla.Launcher private void CmdKillApp(Guid launchKey, int? pid) { var key = FindKeyString(launchKey); + var victims = new List(); + // Untrack under the lock, terminate outside it: KillProcessTree spawns + // taskkill and waits, which must not block the RPC lock. Untracking + // first also stops our own auto-restart watcher from relaunching (its + // stillTracked check fails once the key is gone). lock (_processLock) { List procs; @@ -513,38 +518,86 @@ namespace Tesla.Launcher foreach (var p in toKill) { - try { if (!p.HasExited) p.Kill(); } catch { } + victims.Add(p); procs.Remove(p); } if (procs.Count == 0) _runningProcesses.Remove(key); } + KillProcessTrees(victims); } private void CmdKillAllOfType(Guid launchKey) { var key = FindKeyString(launchKey); + List victims; lock (_processLock) { - List procs; - if (key == null || !_runningProcesses.TryGetValue(key, out procs)) return; - foreach (var p in procs) - try { if (!p.HasExited) p.Kill(); } catch { } + if (key == null || !_runningProcesses.TryGetValue(key, out victims)) return; _runningProcesses.Remove(key); } + KillProcessTrees(victims); } private void CmdKillAllApps() { + var victims = new List(); lock (_processLock) { foreach (var kvp in _runningProcesses) - foreach (var p in kvp.Value) - try { if (!p.HasExited) p.Kill(); } catch { } + victims.AddRange(kvp.Value); _runningProcesses.Clear(); } + KillProcessTrees(victims); SetTrayStatus("All apps stopped"); } + private static void KillProcessTrees(IEnumerable procs) + { + if (procs == null) return; + foreach (var p in procs) KillProcessTree(p); + } + + // Every Tesla game runs under a supervisor that stays alive and re-spawns + // the game (Firestorm -> launcher.exe, Red Planet -> a looping .bat under + // cmd.exe, tesla410revival -> pod-launch.exe -> dosbox). net40's + // Process.Kill() terminates ONLY the tracked supervisor PID, so the game + // survives (and the supervisor/loop relaunches it). taskkill /T walks the + // parent-PID tree and terminates the supervisor AND its children in one + // shot; it ships on XP Professional and Win10/11. Process.Kill() remains + // the fallback if taskkill is somehow unavailable. + private static void KillProcessTree(Process p) + { + if (p == null) return; + int pid; + try + { + if (p.HasExited) return; + pid = p.Id; + } + catch { return; } + + try + { + var psi = new ProcessStartInfo("taskkill", "/PID " + pid + " /T /F") + { + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + using (var tk = Process.Start(psi)) + { + if (tk != null) + { + tk.WaitForExit(10000); + return; + } + } + } + catch { /* taskkill missing/failed — fall back to single-process kill */ } + + try { if (!p.HasExited) p.Kill(); } catch { } + } + private void CmdShutdown(bool restart) { CmdKillAllApps(); diff --git a/Launcher/install.bat b/Launcher/install.bat index 3c41fba..42f7d4e 100644 --- a/Launcher/install.bat +++ b/Launcher/install.bat @@ -142,12 +142,15 @@ if not exist "C:\Games" mkdir "C:\Games" :: Grant Users modify access to the data and games directories so the launcher :: can write files (LaunchApps.xml, session key, game installs) from any account. +:: The icacls grant token MUST stay quoted: its (OI)(CI) inheritance parens would +:: otherwise be read as the end of the if-block -- "(CI)M was unexpected at this +:: time" on Win10. Keep this note ABOVE the block; a stray ) inside ( ) breaks it. if "%ISXP%"=="1" ( cacls "%DATA_DIR%" /T /E /G Users:C >nul 2>&1 cacls "C:\Games" /T /E /G Users:C >nul 2>&1 ) else ( - icacls "%DATA_DIR%" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1 - icacls "C:\Games" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1 + icacls "%DATA_DIR%" /grant "*S-1-5-32-545:(OI)(CI)M" /T >nul 2>&1 + icacls "C:\Games" /grant "*S-1-5-32-545:(OI)(CI)M" /T >nul 2>&1 ) echo %INSTALL_DIR% echo %DATA_DIR% (Users: modify access) @@ -203,12 +206,14 @@ if "%ISXP%"=="0" ( ) :: Create and share game-data folders (closed network - open to Everyone). +:: The icacls grant token stays quoted so its (OI)(CI) parens are not read as the +:: end of the if-block. Keep this note ABOVE the block, not inside the ( ). echo Creating network shares... if not exist "C:\mw4files" mkdir "C:\mw4files" if "%ISXP%"=="1" ( cacls "C:\mw4files" /T /E /G Everyone:C >nul 2>&1 ) else ( - icacls "C:\mw4files" /grant *S-1-1-0:(OI)(CI)M /T >nul 2>&1 + icacls "C:\mw4files" /grant "*S-1-1-0:(OI)(CI)M" /T >nul 2>&1 ) net share mw4files /delete >nul 2>&1 net share mw4files=C:\mw4files /grant:Everyone,FULL >nul 2>&1