Files
Wand-Enhancer-k1tbyte/WandEnhancer/Program.cs
T
kitbyte 2cac40558d feat(launcher): write startup diagnostics to launcher.log
Records spawned processes and their fuse state, exit and unhandled
exception codes, detach timing and reason, and the exit code after detach.
2026-08-29 21:49:29 +03:00

180 lines
6.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using WandEnhancer.Core;
using WandEnhancer.Models;
using WandEnhancer.Utils;
using WandEnhancer.View.MainWindow;
namespace WandEnhancer
{
public static class Program
{
/// <summary>Log lines from a failed startup auto-patch, replayed by the UI when it opens.</summary>
public static readonly List<KeyValuePair<string, ELogType>> StartupLog =
new List<KeyValuePair<string, ELogType>>();
[STAThread]
public static void Main(string[] args)
{
if (TryLaunchMode(args))
return;
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var application = new App();
application.InitializeComponent();
application.MainWindow = new MainWindow();
application.Run();
}
private static bool TryLaunchMode(string[] args)
{
string myExe = Assembly.GetExecutingAssembly().Location;
string myName = Path.GetFileNameWithoutExtension(myExe);
if (!Constants.WeModBrandNames.Any(
n => n.Equals(myName, StringComparison.OrdinalIgnoreCase)))
return false;
string myDir = Path.GetDirectoryName(myExe);
string forwardedArgs = args.Length > 0 ? QuoteArguments(args) : null;
LauncherLog.Open(myDir, $"WandEnhancer {Constants.Version} | {myExe}" +
(forwardedArgs == null ? "" : $" | args {forwardedArgs}"));
if (args.Length > 0 &&
args[0].StartsWith("--squirrel", StringComparison.OrdinalIgnoreCase))
{
string updateExe = Path.Combine(myDir, "Update.exe");
if (File.Exists(updateExe))
{
LauncherLog.Write($"Squirrel hook {args[0]} forwarded to Update.exe.", ELogType.Info);
Process.Start(updateExe, QuoteArguments(args));
}
else
{
LauncherLog.Write($"Squirrel hook {args[0]} ignored: Update.exe is missing.", ELogType.Warn);
}
return true;
}
var config = WeModInstalls.FindLatestWeMod(myDir);
if (config == null)
{
LauncherLog.Write($"No Wand install found under {myDir}; opening the UI instead.", ELogType.Error);
return false;
}
bool isPatched = Enhancer.IsPatched(config.RootDirectory);
LauncherLog.Write($"Install {config.ExecutablePath} is {(isPatched ? "patched" : "not patched")}.",
ELogType.Info);
// A fresh Wand version drops our patches; re-apply the saved selection automatically.
// On failure fall through to the UI so the user sees which patch broke.
if (!isPatched && !TryAutoPatch(config, myDir))
return false;
FuseLauncher.Launch(config.ExecutablePath, forwardedArgs, LauncherLog.Write);
return true;
}
/// <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 string QuoteArguments(IEnumerable<string> args)
{
return string.Join(" ", args.Select(QuoteArgument));
}
private static string QuoteArgument(string value)
{
if (!string.IsNullOrEmpty(value) && value.IndexOfAny(new[] { ' ', '\t', '"' }) < 0)
{
return value;
}
// Backslashes are literal unless they run into the closing quote, where they double.
var quoted = new System.Text.StringBuilder("\"");
int backslashes = 0;
foreach (char current in value ?? string.Empty)
{
if (current == '\\')
{
backslashes++;
continue;
}
if (current == '"')
{
quoted.Append('\\', backslashes * 2 + 1).Append('"');
}
else
{
quoted.Append('\\', backslashes).Append(current);
}
backslashes = 0;
}
return quoted.Append('\\', backslashes * 2).Append('"').ToString();
}
private static bool TryAutoPatch(WeModConfig config, string launcherDir)
{
var patchConfig = Enhancer.LoadAutoPatchConfig(launcherDir);
if (patchConfig == null)
return true; // nothing saved to replay; launch as-is
try
{
new Enhancer(config, RecordStartupLog, patchConfig).Patch();
return true;
}
catch (Exception e)
{
// Localization resources are not loaded yet in launcher mode (no Application),
// so these two replay into the UI log in English by design.
RecordStartupLog($"Auto-patch failed: {e.Message}", ELogType.Error);
RecordStartupLog("The new Wand version may need updated patches. Restore the backup and patch again.", ELogType.Warn);
return false;
}
}
/// <summary>Buffers for the UI and mirrors to disk: auto-patch runs headless, so the
/// file is the only copy if the user never opens the window afterwards.</summary>
private static void RecordStartupLog(string message, ELogType type)
{
StartupLog.Add(new KeyValuePair<string, ELogType>(message, type));
LauncherLog.Write(message, type);
}
// Fires on the finalizer thread for a task nobody awaited. Non-fatal since .NET 4.5:
// record it and mark it observed rather than killing a patch mid-run.
private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
e.SetObserved();
RecordStartupLog($"Background task failed: {e.Exception.GetBaseException().Message}", ELogType.Error);
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var error = e.ExceptionObject as Exception;
MessageBox.Show(
error?.Message ?? e.ExceptionObject?.ToString() ?? "Unknown error",
Constants.RepoName,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
Environment.Exit(1);
}
}
}