mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-31 01:01:22 +00:00
feat(launcher): show the window when Wand fails to start
Launch mode pushes its log into the UI and opens it on failure instead of leaving only launcher.log. Exit codes of Wand child processes are read again by keeping their handles, which the job object does not carry.
This commit is contained in:
+1
-1
@@ -42,7 +42,7 @@ The newest entry must match the version in `WandEnhancer/Properties/AssemblyInfo
|
||||
|
||||
### Improvements
|
||||
|
||||
- The launcher now writes a `launcher.log` next to itself, recording every process Electron starts and whether its fuse was cleared, exit and crash codes, and how long it stayed attached. Starting Wand happens without a window, so until now a client that refused to open left nothing to go on.
|
||||
- When Wand fails to start, the Enhancer window now opens by itself with the reason already in the log, instead of leaving you to find a log file. The same lines are still written to a `launcher.log` next to the launcher: every process Electron starts and whether the patch reached it, plus exit and crash codes.
|
||||
- Log messages in the desktop app are now translated into all 12 supported languages.
|
||||
- The remote panel is now usable with a keyboard and a screen reader: dialogs trap focus and close on Escape, and controls have accessible names. Pinning a mod previously required a swipe and had no keyboard path at all, so mod rows now have a pin button.
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WandEnhancer.View.MainWindow;
|
||||
@@ -20,7 +21,8 @@ namespace WandEnhancer.Core
|
||||
{
|
||||
private const int AsarIntegrityExitCode = -36861;
|
||||
|
||||
public static void Launch(string exePath, string args, Action<string, ELogType> log = null)
|
||||
/// <returns>False when the session ended badly enough to be worth showing the user.</returns>
|
||||
public static bool Launch(string exePath, string args, Action<string, ELogType> log = null)
|
||||
{
|
||||
long stateRva = ElectronFuse.FindStateRva(exePath);
|
||||
|
||||
@@ -34,12 +36,12 @@ namespace WandEnhancer.Core
|
||||
IntPtr.Zero, System.IO.Path.GetDirectoryName(exePath), ref startupInfo, out var info))
|
||||
{
|
||||
log?.Invoke($"Could not start Wand (win32 error {Marshal.GetLastWin32Error()}).", ELogType.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr job = IntPtr.Zero;
|
||||
IntPtr port = IntPtr.Zero;
|
||||
bool mainCleared = false;
|
||||
bool resumed = false;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -49,10 +51,10 @@ namespace WandEnhancer.Core
|
||||
{
|
||||
log?.Invoke($"No Electron fuse block in {exePath}. A patched Wand will exit " +
|
||||
$"with {AsarIntegrityExitCode}; an unpatched one is unaffected.", ELogType.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
mainCleared = ElectronFuse.ClearIn(info.hProcess, stateRva);
|
||||
bool mainCleared = ElectronFuse.ClearIn(info.hProcess, stateRva);
|
||||
log?.Invoke(mainCleared
|
||||
? $"pid {info.dwProcessId} started - fuse cleared."
|
||||
: $"Fuse not cleared in pid {info.dwProcessId}; it may exit with {AsarIntegrityExitCode}.",
|
||||
@@ -62,28 +64,42 @@ namespace WandEnhancer.Core
|
||||
{
|
||||
log?.Invoke($"Could not watch Wand for new processes (win32 error {Marshal.GetLastWin32Error()}). " +
|
||||
"Wand will run, but the in-game overlay will not.", ELogType.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ResumeThread(info.hThread);
|
||||
CloseHandle(info.hThread);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ResumeThread(info.hThread);
|
||||
resumed = true;
|
||||
|
||||
ClearFuseInNewProcesses(port, exePath, stateRva, info.dwProcessId, mainCleared, log);
|
||||
log?.Invoke(GetExitCodeProcess(info.hProcess, out int exitCode)
|
||||
? $"Wand exited with code {DescribeCode(exitCode)}."
|
||||
: "Wand exited, code unreadable.",
|
||||
ELogType.Info);
|
||||
|
||||
if (!GetExitCodeProcess(info.hProcess, out int exitCode))
|
||||
{
|
||||
log?.Invoke("Wand exited, and its exit code could not be read.", ELogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
log?.Invoke($"Wand exited with code {DescribeCode(exitCode)}.",
|
||||
exitCode == 0 ? ELogType.Info : ELogType.Error);
|
||||
return exitCode == 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(port);
|
||||
CloseHandle(job);
|
||||
if (!resumed)
|
||||
{
|
||||
ResumeThread(info.hThread);
|
||||
}
|
||||
|
||||
CloseHandle(info.hThread);
|
||||
CloseHandle(info.hProcess);
|
||||
if (port != IntPtr.Zero)
|
||||
{
|
||||
CloseHandle(port);
|
||||
}
|
||||
|
||||
if (job != IntPtr.Zero)
|
||||
{
|
||||
CloseHandle(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,35 +132,60 @@ namespace WandEnhancer.Core
|
||||
private static void ClearFuseInNewProcesses(IntPtr port, string exePath, long stateRva,
|
||||
int mainProcessId, bool mainCleared, Action<string, ELogType> log)
|
||||
{
|
||||
var tracked = new Dictionary<int, IntPtr>();
|
||||
int cleared = mainCleared ? 1 : 0;
|
||||
int missed = mainCleared ? 0 : 1;
|
||||
|
||||
while (GetQueuedCompletionStatus(port, out uint message, out _, out IntPtr value, INFINITE))
|
||||
try
|
||||
{
|
||||
if (message == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
|
||||
while (GetQueuedCompletionStatus(port, out uint message, out _, out IntPtr value, INFINITE))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (message == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int processId = value.ToInt32();
|
||||
// The main process is announced here too, having been patched while it was still
|
||||
// suspended, and a game started from Wand joins the job like any other child.
|
||||
if (message != JOB_OBJECT_MSG_NEW_PROCESS || processId == mainProcessId ||
|
||||
!IsImage(processId, exePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int processId = value.ToInt32();
|
||||
if (message == JOB_OBJECT_MSG_EXIT_PROCESS || message == JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS)
|
||||
{
|
||||
ReportExit(tracked, processId, log);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Clear(processId, stateRva))
|
||||
{
|
||||
cleared++;
|
||||
log?.Invoke($"pid {processId} started - fuse cleared.", ELogType.Info);
|
||||
// The main process is announced here too, having been patched while it was
|
||||
// still suspended, and a game started from Wand joins the job like any child.
|
||||
if (message != JOB_OBJECT_MSG_NEW_PROCESS || processId == mainProcessId ||
|
||||
!IsImage(processId, exePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The handle is kept open: it is what makes the exit code readable later, and
|
||||
// it also stops Windows handing the pid to someone else in the meantime.
|
||||
IntPtr process = OpenProcess(ProcessAccess, false, processId);
|
||||
if (process != IntPtr.Zero)
|
||||
{
|
||||
tracked[processId] = process;
|
||||
}
|
||||
|
||||
if (process != IntPtr.Zero && ElectronFuse.ClearIn(process, stateRva))
|
||||
{
|
||||
cleared++;
|
||||
log?.Invoke($"pid {processId} started - fuse cleared.", ELogType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
missed++;
|
||||
log?.Invoke($"Fuse not cleared in pid {processId}; it may exit with {AsarIntegrityExitCode}.",
|
||||
ELogType.Warn);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var handle in tracked.Values)
|
||||
{
|
||||
missed++;
|
||||
log?.Invoke($"Fuse not cleared in pid {processId}; it may exit with {AsarIntegrityExitCode}.",
|
||||
ELogType.Warn);
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +193,26 @@ namespace WandEnhancer.Core
|
||||
missed == 0 ? ELogType.Info : ELogType.Warn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only anomalies are reported: on a normal shutdown every process exits with 0, and a
|
||||
/// line each would bury the one death that matters.
|
||||
/// </summary>
|
||||
private static void ReportExit(Dictionary<int, IntPtr> tracked, int processId, Action<string, ELogType> log)
|
||||
{
|
||||
if (!tracked.TryGetValue(processId, out IntPtr process))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tracked.Remove(processId);
|
||||
if (GetExitCodeProcess(process, out int exitCode) && exitCode != 0)
|
||||
{
|
||||
log?.Invoke($"pid {processId} exited with code {DescribeCode(exitCode)}.", ELogType.Error);
|
||||
}
|
||||
|
||||
CloseHandle(process);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identity check before anything heavier: a game started from Wand is in the job as well,
|
||||
/// and is opened for nothing beyond the right the task manager uses to read a path.
|
||||
@@ -177,24 +238,6 @@ namespace WandEnhancer.Core
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Clear(int processId, long stateRva)
|
||||
{
|
||||
IntPtr process = OpenProcess(ProcessAccess, false, processId);
|
||||
if (process == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return ElectronFuse.ClearIn(process, stateRva);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(process);
|
||||
}
|
||||
}
|
||||
|
||||
private static string DescribeCode(int code)
|
||||
{
|
||||
switch (code)
|
||||
@@ -202,6 +245,8 @@ namespace WandEnhancer.Core
|
||||
case 0: return "0";
|
||||
case AsarIntegrityExitCode:
|
||||
return $"{code} (ASAR integrity check failed - the fuse was not cleared in time)";
|
||||
// Chromium breaks into a debugger that is not there when it hits a fatal error.
|
||||
case unchecked((int)0x80000003): return $"0x{code:X8} (Wand aborted itself during startup)";
|
||||
case unchecked((int)0xC0000005): return $"0x{code:X8} (access violation)";
|
||||
case unchecked((int)0xC0000135): return $"0x{code:X8} (a required DLL is missing)";
|
||||
case unchecked((int)0xC0000142): return $"0x{code:X8} (a DLL failed to initialise)";
|
||||
@@ -219,6 +264,8 @@ namespace WandEnhancer.Core
|
||||
private const int JobObjectAssociateCompletionPortInformation = 7;
|
||||
private const uint JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4;
|
||||
private const uint JOB_OBJECT_MSG_NEW_PROCESS = 6;
|
||||
private const uint JOB_OBJECT_MSG_EXIT_PROCESS = 7;
|
||||
private const uint JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8;
|
||||
private const uint INFINITE = 0xFFFFFFFF;
|
||||
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
|
||||
|
||||
|
||||
+23
-7
@@ -28,9 +28,18 @@ namespace WandEnhancer
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
|
||||
bool startupFailed = StartupLog.Exists(entry => entry.Value == ELogType.Error);
|
||||
|
||||
var application = new App();
|
||||
application.InitializeComponent();
|
||||
application.MainWindow = new MainWindow();
|
||||
var window = new MainWindow();
|
||||
|
||||
// Launch mode has no window at all, so a user whose Wand never opened would have to
|
||||
// be told where launcher.log lives. Put the same lines in front of them instead.
|
||||
if (startupFailed)
|
||||
window.Loaded += (sender, e) => BringToFront(window);
|
||||
|
||||
application.MainWindow = window;
|
||||
application.Run();
|
||||
}
|
||||
|
||||
@@ -82,14 +91,21 @@ namespace WandEnhancer
|
||||
if (!isPatched && !TryAutoPatch(config, myDir))
|
||||
return false;
|
||||
|
||||
FuseLauncher.Launch(config.ExecutablePath, forwardedArgs, LauncherLog.Write);
|
||||
return true;
|
||||
// RecordStartupLog, not LauncherLog.Write: whatever the launcher says has to survive
|
||||
// into the window on the failure path below.
|
||||
return FuseLauncher.Launch(config.ExecutablePath, forwardedArgs, RecordStartupLog);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-quotes argv for a command line. Squirrel hands us paths with spaces
|
||||
/// (`--squirrel-install "C:\Users\Some Name\..."`); re-joining on spaces splits them.
|
||||
/// </summary>
|
||||
|
||||
private static void BringToFront(System.Windows.Window window)
|
||||
{
|
||||
window.WindowState = System.Windows.WindowState.Normal;
|
||||
window.Topmost = true;
|
||||
window.Activate();
|
||||
window.Topmost = false;
|
||||
}
|
||||
|
||||
|
||||
private static string QuoteArguments(IEnumerable<string> args)
|
||||
{
|
||||
return string.Join(" ", args.Select(QuoteArgument));
|
||||
|
||||
Reference in New Issue
Block a user