fix(launcher): co-deploy acdream-bake on Build and write a crash report (#398)
Two gaps found launching the launcher for the LA11 gate. 1. acdream-bake was co-deployed only AfterTargets=Publish with a RID, so a plain `dotnet build` left the launcher with no bake tool beside it while App.OnFrameworkInitializationCompleted resolves it at AppContext.BaseDirectory/acdream-bake[.exe]. A developer-built launcher therefore reached the first-run wizard with an installer it could never run. CoDeployBakeToolToBuildOutput does for Build what the publish target does for Publish: still NO Launcher -> Bake project reference, still a self-contained single file so exactly one file lands beside the launcher rather than scattering Content/Chorizite assemblies into its output. Staged through obj/ because publishing straight into the launcher output makes the inner publish delete what the outer build just wrote. Inputs/Outputs keep it incremental - verified: 79.6 MB bake exe present, --help exits 0, and a second build skips the republish in ~1 s. 2. #398: the top-level guard printed only ex.Message, so the crash that preceded this commit surfaced with no file, line, or frame. The full exception now goes to a crash-reports file under the resolved data root and stderr names the path. The first implementation wrote to the machine real data root when option parsing itself failed, which broke LA11 process local roots during an isolated run; the reporter now reads --data-dir positionally for that fallback. Verified: report lands inside the isolated root and the real root stays empty. The redaction comment states exactly what is guaranteed - args/environment are never serialized, while exception text may quote an option name or path, which is safe only because credentials never enter launcher state. Launcher.Core 317/317 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ef9d610459
commit
e1e94697b2
2 changed files with 144 additions and 0 deletions
|
|
@ -12,8 +12,26 @@
|
|||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<SelfContained Condition="'$(RuntimeIdentifier)' != ''">true</SelfContained>
|
||||
<PublishBakeTool Condition="'$(PublishBakeTool)' == ''">true</PublishBakeTool>
|
||||
<!-- App.OnFrameworkInitializationCompleted resolves the bake CLI at
|
||||
AppContext.BaseDirectory/acdream-bake[.exe]. Publish co-deploys it
|
||||
(PublishCoDeployedBakeTool below), but an ordinary `dotnet build`
|
||||
did not, so a developer-built launcher reached the first-run wizard
|
||||
with no bake tool beside it. This does for Build what that target
|
||||
does for Publish. Set to false to skip (CI publish lanes pass
|
||||
PublishBakeTool=false into the inner build for the same reason). -->
|
||||
<CoDeployBakeToolOnBuild Condition="'$(CoDeployBakeToolOnBuild)' == ''">true</CoDeployBakeToolOnBuild>
|
||||
<_BakeExecutableName Condition="$([MSBuild]::IsOSPlatform('Windows'))">acdream-bake.exe</_BakeExecutableName>
|
||||
<_BakeExecutableName Condition="'$(_BakeExecutableName)' == ''">acdream-bake</_BakeExecutableName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Declared outside the target so the Inputs/Outputs check below can see
|
||||
them: MSBuild evaluates a target's Inputs before the target body runs. -->
|
||||
<ItemGroup>
|
||||
<_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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="AcDream.Launcher.Tests" />
|
||||
</ItemGroup>
|
||||
|
|
@ -28,6 +46,38 @@
|
|||
<ProjectReference Include="..\AcDream.Launcher.Core\AcDream.Launcher.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Build-time counterpart of PublishCoDeployedBakeTool. Same rule applies:
|
||||
NO Launcher -> Bake project reference. The tool is published as its own
|
||||
self-contained single file so exactly one file lands beside the
|
||||
launcher, matching the shipped layout - a framework-dependent copy would
|
||||
scatter AcDream.Content/Chorizite assemblies into the launcher's output
|
||||
and risk colliding with its own. Inputs/Outputs keep incremental builds
|
||||
from re-publishing it every time. -->
|
||||
<Target Name="CoDeployBakeToolToBuildOutput"
|
||||
AfterTargets="Build"
|
||||
Condition="'$(CoDeployBakeToolOnBuild)' == 'true' and '$(PublishBakeTool)' == 'true' and '$(DesignTimeBuild)' != 'true'"
|
||||
Inputs="@(_BakeToolSource)"
|
||||
Outputs="$(OutputPath)$(_BakeExecutableName)">
|
||||
<PropertyGroup>
|
||||
<_BakeBuildRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</_BakeBuildRid>
|
||||
<_BakeBuildRid Condition="'$(_BakeBuildRid)' == ''">$(NETCoreSdkPortableRuntimeIdentifier)</_BakeBuildRid>
|
||||
<_BakeBuildStagingDirectory>$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)bake-codeploy\$(Configuration)\$(_BakeBuildRid)\</_BakeBuildStagingDirectory>
|
||||
<_BakeBuildOutputDirectory Condition="$([System.IO.Path]::IsPathRooted('$(OutputPath)'))">$(OutputPath)</_BakeBuildOutputDirectory>
|
||||
<_BakeBuildOutputDirectory Condition="'$(_BakeBuildOutputDirectory)' == ''">$(MSBuildProjectDirectory)\$(OutputPath)</_BakeBuildOutputDirectory>
|
||||
</PropertyGroup>
|
||||
<!-- Staged, then copied: publishing straight into the launcher's output
|
||||
would have the inner publish delete files the outer build just wrote. -->
|
||||
<MSBuild Projects="$(MSBuildProjectDirectory)\..\AcDream.Bake\AcDream.Bake.csproj"
|
||||
Targets="Restore;Publish"
|
||||
BuildInParallel="false"
|
||||
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(_BakeBuildRid);SelfContained=true;PublishSingleFile=true;IncludeNativeLibrariesForSelfExtract=true;EnableSingleFileAnalyzer=false;PublishDir=$(_BakeBuildStagingDirectory);PublishBakeTool=false;CoDeployBakeToolOnBuild=false" />
|
||||
<Copy SourceFiles="$(_BakeBuildStagingDirectory)$(_BakeExecutableName)"
|
||||
DestinationFolder="$(_BakeBuildOutputDirectory)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<Message Importance="high"
|
||||
Text="Co-deployed $(_BakeExecutableName) ($(_BakeBuildRid)) next to the launcher." />
|
||||
</Target>
|
||||
|
||||
<!-- Distribution composition only: do not add a Launcher -> Bake project
|
||||
reference. A per-RID launcher publish explicitly publishes the GL-free
|
||||
CLI as its own self-contained single file into the same directory. -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Issue #398: stderr alone carried only <c>ex.Message</c>, 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.
|
||||
///
|
||||
/// <para>Redaction contract, stated exactly. This method writes only the
|
||||
/// exception chain plus non-identifying host facts; it never serializes
|
||||
/// <paramref name="args"/>, 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 <c>LauncherProcessSpec</c> carries no
|
||||
/// credential member (guarded by its own test). If that ever changes, this
|
||||
/// sink needs the same credential scanning the status stream has.</para>
|
||||
///
|
||||
/// <para>Never throws: a crash reporter that can itself fail would replace
|
||||
/// the original failure with its own.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positional, validation-free read of <c>--data-dir</c> 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 — <see cref="LauncherStartupOptions"/>
|
||||
/// remains the only validated path authority.
|
||||
/// </summary>
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue