feat(launcher): complete Campaign LA11 pre-gate support
This commit is contained in:
parent
f881e5b467
commit
134edabed2
19 changed files with 433 additions and 55 deletions
|
|
@ -11,8 +11,9 @@ public interface IReleaseManifestClient
|
|||
|
||||
/// <summary>
|
||||
/// Strict, bounded reader for the pinned GitHub Releases manifest. Production
|
||||
/// construction is HTTPS-only. The loopback HTTP allowance is available only
|
||||
/// through an internal fixture factory and is never inferred from a URI.
|
||||
/// construction is pinned and HTTPS-only. The explicitly named process-local
|
||||
/// feed factory independently revalidates its URI and can admit HTTP only for
|
||||
/// the loopback operator fixture; it cannot change the production constructor.
|
||||
/// Redirects are followed manually so every hop is checked before any bytes
|
||||
/// cross that hop.
|
||||
/// </summary>
|
||||
|
|
@ -74,6 +75,49 @@ public sealed class ReleaseManifestClient : IReleaseManifestClient, IDisposable
|
|||
CreateRedirectDisabledHandler(),
|
||||
timeout);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the explicit process-local feed seam used by the Campaign LA
|
||||
/// isolated operator fixture. HTTPS stays HTTPS-only. HTTP is admitted
|
||||
/// only for a loopback manifest, and never by the pinned production
|
||||
/// constructor. Credential-bearing or mutable URI suffixes are rejected.
|
||||
/// </summary>
|
||||
public static ReleaseManifestClient CreateLocalUpdateFeedOverride(
|
||||
Uri manifestUri,
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifestUri);
|
||||
if (!string.IsNullOrEmpty(manifestUri.UserInfo)
|
||||
|| !string.IsNullOrEmpty(manifestUri.Query)
|
||||
|| !string.IsNullOrEmpty(manifestUri.Fragment))
|
||||
{
|
||||
throw new LauncherUpdateException(
|
||||
"A process-local manifest URI cannot contain user information, "
|
||||
+ "a query, or a fragment.");
|
||||
}
|
||||
|
||||
bool allowLoopbackHttp = string.Equals(
|
||||
manifestUri.Scheme,
|
||||
Uri.UriSchemeHttp,
|
||||
StringComparison.Ordinal)
|
||||
&& manifestUri.IsLoopback;
|
||||
if (!string.Equals(
|
||||
manifestUri.Scheme,
|
||||
Uri.UriSchemeHttps,
|
||||
StringComparison.Ordinal)
|
||||
&& !allowLoopbackHttp)
|
||||
{
|
||||
throw new LauncherUpdateException(
|
||||
"A process-local manifest URI must use HTTPS "
|
||||
+ "(loopback HTTP is fixture-only).");
|
||||
}
|
||||
|
||||
return new ReleaseManifestClient(
|
||||
manifestUri,
|
||||
allowLoopbackHttp,
|
||||
CreateRedirectDisabledHandler(),
|
||||
timeout);
|
||||
}
|
||||
|
||||
internal static ReleaseManifestClient CreateForTransportTest(
|
||||
Uri manifestUri,
|
||||
bool allowLoopbackHttp,
|
||||
|
|
|
|||
|
|
@ -28,10 +28,6 @@
|
|||
<ProjectReference Include="..\AcDream.Launcher.Core\AcDream.Launcher.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="AcDream.Launcher.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 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. -->
|
||||
|
|
|
|||
|
|
@ -14,17 +14,33 @@ namespace AcDream.Launcher;
|
|||
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
private readonly LauncherStartupOptions? _startupOptions;
|
||||
private LauncherOrchestrator? _orchestrator;
|
||||
private LauncherWindowViewModel? _viewModel;
|
||||
private LauncherUpdateComposition? _updateComposition;
|
||||
|
||||
public App()
|
||||
{
|
||||
}
|
||||
|
||||
internal App(LauncherStartupOptions startupOptions)
|
||||
{
|
||||
_startupOptions = startupOptions
|
||||
?? throw new ArgumentNullException(nameof(startupOptions));
|
||||
}
|
||||
|
||||
internal LauncherStartupOptions StartupOptions => _startupOptions
|
||||
?? throw new InvalidOperationException(
|
||||
"Launcher startup options were not supplied by the composition root.");
|
||||
|
||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
ApplicationPathSet paths = ApplicationPathSet.Resolve();
|
||||
LauncherStartupOptions startupOptions = StartupOptions;
|
||||
ApplicationPathSet paths = startupOptions.Paths;
|
||||
LauncherProfileStore profiles = LauncherProfileStore.ForApplicationPaths(paths);
|
||||
string rid = LauncherRuntimeIdentity.DetectRid();
|
||||
string executableSuffix = OperatingSystem.IsWindows() ? ".exe" : string.Empty;
|
||||
|
|
@ -57,7 +73,8 @@ public sealed partial class App : Application
|
|||
GetLauncherVersion(),
|
||||
AppContext.BaseDirectory,
|
||||
() => _orchestrator?.GetSnapshot().Sessions.Any(session => session.IsActive)
|
||||
== true);
|
||||
== true,
|
||||
updateManifestUri: startupOptions.UpdateManifestUri);
|
||||
_updateComposition = updates;
|
||||
|
||||
_orchestrator = new LauncherOrchestrator(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ internal enum LauncherStartupMode
|
|||
VerifyPublish,
|
||||
SelfUpdateHelper,
|
||||
SelfUpdateConfirmation,
|
||||
SelfUpdateDeferred,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -18,6 +19,12 @@ internal enum LauncherStartupMode
|
|||
/// </summary>
|
||||
internal sealed class LauncherStartupOptions
|
||||
{
|
||||
// This prefix is consumed only after LauncherSelfUpdateBootstrap has
|
||||
// already decided to continue after a recovered rollback. Keep it local
|
||||
// so the process-level bootstrap can remain internal to Launcher.Core.
|
||||
private const string DeferredSelfUpdateArgument =
|
||||
"--acdream-self-update-deferred-v1";
|
||||
|
||||
private readonly IReadOnlyList<string> _publicArguments;
|
||||
|
||||
private LauncherStartupOptions(
|
||||
|
|
@ -138,6 +145,13 @@ internal sealed class LauncherStartupOptions
|
|||
"The update manifest URI cannot contain user information.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(parsed.Query)
|
||||
|| !string.IsNullOrEmpty(parsed.Fragment))
|
||||
{
|
||||
throw new LauncherStartupOptionsException(
|
||||
"The update manifest URI cannot contain a query or fragment.");
|
||||
}
|
||||
|
||||
updateManifestUri = parsed;
|
||||
break;
|
||||
default:
|
||||
|
|
@ -199,6 +213,14 @@ internal sealed class LauncherStartupOptions
|
|||
arguments.Count >= 2 ? 2 : arguments.Count);
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
arguments[0],
|
||||
DeferredSelfUpdateArgument,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return (LauncherStartupMode.SelfUpdateDeferred, 1);
|
||||
}
|
||||
|
||||
return (LauncherStartupMode.Desktop, 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ internal sealed class LauncherUpdateComposition : IDisposable
|
|||
ClientVersionStore versions,
|
||||
LauncherExecutableSet executables,
|
||||
ILauncherUpdater updater,
|
||||
Uri updateManifestUri,
|
||||
HttpClient? artifactClient,
|
||||
ReleaseManifestClient? manifestClient)
|
||||
{
|
||||
Versions = versions;
|
||||
Executables = executables;
|
||||
Updater = updater;
|
||||
UpdateManifestUri = updateManifestUri;
|
||||
_artifactClient = artifactClient;
|
||||
_manifestClient = manifestClient;
|
||||
}
|
||||
|
|
@ -38,17 +40,22 @@ internal sealed class LauncherUpdateComposition : IDisposable
|
|||
|
||||
public ILauncherUpdater Updater { get; }
|
||||
|
||||
internal Uri UpdateManifestUri { get; }
|
||||
|
||||
public static LauncherUpdateComposition Create(
|
||||
ApplicationPathSet paths,
|
||||
string rid,
|
||||
LauncherVersion launcherVersion,
|
||||
string launcherTargetDirectory,
|
||||
Func<bool> hasRunningSessions,
|
||||
Func<ClientVersionStore, string, ClientVersionResolution>? initialize = null)
|
||||
Func<ClientVersionStore, string, ClientVersionResolution>? initialize = null,
|
||||
Uri? updateManifestUri = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(paths);
|
||||
ArgumentNullException.ThrowIfNull(launcherVersion);
|
||||
ArgumentNullException.ThrowIfNull(hasRunningSessions);
|
||||
Uri manifestUri = updateManifestUri
|
||||
?? ReleaseManifestClient.ProductionManifestUri;
|
||||
var versions = new ClientVersionStore(paths);
|
||||
HttpClient? artifactClient = null;
|
||||
ReleaseManifestClient? manifestClient = null;
|
||||
|
|
@ -69,7 +76,7 @@ internal sealed class LauncherUpdateComposition : IDisposable
|
|||
Timeout = TimeSpan.FromSeconds(15),
|
||||
};
|
||||
artifactClient.DefaultRequestHeaders.UserAgent.ParseAdd("acdream-launcher/1");
|
||||
manifestClient = new ReleaseManifestClient(TimeSpan.FromSeconds(15));
|
||||
manifestClient = CreateManifestClient(manifestUri);
|
||||
var selfUpdates = new LauncherSelfUpdateManager(paths, artifactClient);
|
||||
var updater = new LauncherUpdater(
|
||||
manifestClient,
|
||||
|
|
@ -84,6 +91,7 @@ internal sealed class LauncherUpdateComposition : IDisposable
|
|||
versions,
|
||||
LauncherExecutableSet.FromCurrentVersionStore(versions),
|
||||
updater,
|
||||
manifestUri,
|
||||
artifactClient,
|
||||
manifestClient);
|
||||
}
|
||||
|
|
@ -106,6 +114,7 @@ internal sealed class LauncherUpdateComposition : IDisposable
|
|||
versions,
|
||||
LauncherExecutableSet.Unavailable(status),
|
||||
new UnavailableLauncherUpdater(status, resolution),
|
||||
manifestUri,
|
||||
artifactClient: null,
|
||||
manifestClient: null);
|
||||
}
|
||||
|
|
@ -117,6 +126,16 @@ internal sealed class LauncherUpdateComposition : IDisposable
|
|||
_artifactClient?.Dispose();
|
||||
}
|
||||
|
||||
private static ReleaseManifestClient CreateManifestClient(Uri manifestUri)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifestUri);
|
||||
return manifestUri == ReleaseManifestClient.ProductionManifestUri
|
||||
? new ReleaseManifestClient(TimeSpan.FromSeconds(15))
|
||||
: ReleaseManifestClient.CreateLocalUpdateFeedOverride(
|
||||
manifestUri,
|
||||
TimeSpan.FromSeconds(15));
|
||||
}
|
||||
|
||||
private static bool IsStorageFailure(Exception exception) => exception is
|
||||
IOException
|
||||
or UnauthorizedAccessException
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using AcDream.Launcher.Core.Updates;
|
||||
using AcDream.Platform;
|
||||
using Avalonia;
|
||||
|
||||
namespace AcDream.Launcher;
|
||||
|
|
@ -9,19 +8,18 @@ internal static class Program
|
|||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
if (args is ["--verify-publish"])
|
||||
{
|
||||
// A display-free execution probe for the packaged artifact. CI
|
||||
// runs this with DOTNET_ROOT pointing at a missing directory; a
|
||||
// framework-dependent publish cannot reach this return statement.
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ApplicationPathSet paths = ApplicationPathSet.Resolve();
|
||||
LauncherStartupOptions options = LauncherStartupOptions.Parse(args);
|
||||
if (options.Mode == LauncherStartupMode.VerifyPublish)
|
||||
{
|
||||
// A display-free execution probe for the packaged artifact.
|
||||
// Parsing above deliberately never resolves user paths.
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
var selfUpdates = new LauncherSelfUpdateManager(paths, httpClient);
|
||||
var selfUpdates = new LauncherSelfUpdateManager(options.Paths, httpClient);
|
||||
string executable = Environment.ProcessPath
|
||||
?? throw new InvalidOperationException(
|
||||
"The launcher executable path is unavailable.");
|
||||
|
|
@ -37,8 +35,9 @@ internal static class Program
|
|||
return startup.ExitCode;
|
||||
}
|
||||
|
||||
return BuildAvaloniaApp().StartWithClassicDesktopLifetime(
|
||||
startup.RemainingArguments);
|
||||
RequireUnchangedPublicArguments(options, startup);
|
||||
|
||||
return BuildAvaloniaApp(options).StartWithClassicDesktopLifetime([]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -47,7 +46,25 @@ internal static class Program
|
|||
}
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp() =>
|
||||
AppBuilder.Configure<App>()
|
||||
internal static AppBuilder BuildAvaloniaApp(LauncherStartupOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
return AppBuilder.Configure(() => new App(options))
|
||||
.UsePlatformDetect();
|
||||
}
|
||||
|
||||
internal static void RequireUnchangedPublicArguments(
|
||||
LauncherStartupOptions options,
|
||||
SelfUpdateStartupResult startup)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(startup);
|
||||
if (!startup.RemainingArguments.SequenceEqual(
|
||||
options.PublicArguments,
|
||||
StringComparer.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The self-update bootstrap changed validated launcher arguments.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue