diff --git a/src/AcDream.Launcher/AcDream.Launcher.csproj b/src/AcDream.Launcher/AcDream.Launcher.csproj index 350260b2..01a0dfbc 100644 --- a/src/AcDream.Launcher/AcDream.Launcher.csproj +++ b/src/AcDream.Launcher/AcDream.Launcher.csproj @@ -12,8 +12,26 @@ true true true + + true + <_BakeExecutableName Condition="$([MSBuild]::IsOSPlatform('Windows'))">acdream-bake.exe + <_BakeExecutableName Condition="'$(_BakeExecutableName)' == ''">acdream-bake + + + <_BakeToolSource Include="$(MSBuildProjectDirectory)\..\AcDream.Bake\**\*.cs" + Exclude="$(MSBuildProjectDirectory)\..\AcDream.Bake\bin\**\*.cs;$(MSBuildProjectDirectory)\..\AcDream.Bake\obj\**\*.cs" /> + <_BakeToolSource Include="$(MSBuildProjectDirectory)\..\AcDream.Bake\AcDream.Bake.csproj" /> + + @@ -28,6 +46,38 @@ + + + + <_BakeBuildRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier) + <_BakeBuildRid Condition="'$(_BakeBuildRid)' == ''">$(NETCoreSdkPortableRuntimeIdentifier) + <_BakeBuildStagingDirectory>$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)bake-codeploy\$(Configuration)\$(_BakeBuildRid)\ + <_BakeBuildOutputDirectory Condition="$([System.IO.Path]::IsPathRooted('$(OutputPath)'))">$(OutputPath) + <_BakeBuildOutputDirectory Condition="'$(_BakeBuildOutputDirectory)' == ''">$(MSBuildProjectDirectory)\$(OutputPath) + + + + + + + diff --git a/src/AcDream.Launcher/Program.cs b/src/AcDream.Launcher/Program.cs index bb6b7c1f..982aad60 100644 --- a/src/AcDream.Launcher/Program.cs +++ b/src/AcDream.Launcher/Program.cs @@ -1,4 +1,5 @@ using AcDream.Launcher.Core.Updates; +using AcDream.Platform; using Avalonia; namespace AcDream.Launcher; @@ -42,10 +43,103 @@ internal static class Program catch (Exception ex) { Console.Error.WriteLine($"Launcher startup failed safely: {ex.Message}"); + string? report = TryWriteCrashReport(args, ex); + Console.Error.WriteLine(report is null + ? "No crash report could be written." + : $"Crash report: {report}"); return 74; } } + /// + /// Issue #398: stderr alone carried only ex.Message, so a fatal + /// dispatcher exception reached the operator with no file, line, or frame + /// and diagnosis required editing this guard and rebuilding. The full + /// exception goes to a file under the resolved data root instead of to + /// stderr, and the path is printed. + /// + /// Redaction contract, stated exactly. This method writes only the + /// exception chain plus non-identifying host facts; it never serializes + /// , the environment, or process state. It does NOT + /// claim the text is value-free: an exception message may quote whatever + /// the thrower put in it, including an offending option name or a path + /// (observed: "Launcher option '--x' requires a value"). That is acceptable + /// because a credential cannot reach this text by construction — the + /// launcher never holds a password in any field, credentials go straight to + /// a child process's stdin, and LauncherProcessSpec carries no + /// credential member (guarded by its own test). If that ever changes, this + /// sink needs the same credential scanning the status stream has. + /// + /// Never throws: a crash reporter that can itself fail would replace + /// the original failure with its own. + /// + private static string? TryWriteCrashReport(string[] args, Exception failure) + { + try + { + string dataDirectory; + try + { + dataDirectory = LauncherStartupOptions.Parse(args).Paths.DataDirectory; + } + catch + { + // Parsing is one of the things that can fail here, and the + // caller's --data-dir must still be honored: LA11's roots are + // process-local, so a crash report written to the machine's + // real data root during an isolated run would break that + // isolation (observed doing exactly that before this branch + // existed). Read the root positionally without validating it, + // and only fall back to the defaults when it is absent. + dataDirectory = TryReadRequestedDataDirectory(args) + ?? ApplicationPathSet.Resolve().DataDirectory; + } + + string directory = Path.Combine(dataDirectory, "crash-reports"); + Directory.CreateDirectory(directory); + string path = Path.Combine( + directory, + $"launcher-crash-{DateTime.UtcNow:yyyyMMdd-HHmmssfff}.log"); + File.WriteAllText( + path, + $""" + acdream launcher crash report + utc: {DateTime.UtcNow:O} + os: {Environment.OSVersion} + rid: {System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier} + version: {typeof(Program).Assembly.GetName().Version} + + {failure} + """); + return path; + } + catch + { + return null; + } + } + + /// + /// Positional, validation-free read of --data-dir for the crash + /// reporter only, so an isolated run keeps its evidence inside its own + /// roots even when option parsing is what failed. Never used for anything + /// the launcher actually runs on — + /// remains the only validated path authority. + /// + private static string? TryReadRequestedDataDirectory(string[] args) + { + for (int index = 0; index + 1 < args.Length; index++) + { + if (string.Equals(args[index], "--data-dir", StringComparison.Ordinal) + && !string.IsNullOrWhiteSpace(args[index + 1])) + { + return args[index + 1]; + } + } + + return null; + } + internal static AppBuilder BuildAvaloniaApp(LauncherStartupOptions options) { ArgumentNullException.ThrowIfNull(options);