This commit is contained in:
lincube
2026-04-16 14:17:46 +08:00
parent 2f0c178df2
commit 1aaf6cd0e9
21 changed files with 1856 additions and 611 deletions

View File

@@ -0,0 +1,8 @@
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAxPqgXsrnG8Re0kV4HBb+x61HQpjCahJoilzKvvlnXanuGtGxbjZT
B+kMzmPUwyx8gt1fcaBNoKPwpwP0UZRWjvJDZQ++5ex7LGGw0YRWtJmeeigS17YI
90vEfX3xQ5InJoBKnndsRy2a742chE6YwHGrJ4b107ZJ+zd26FmokQS47Uzay3go
msbQHdehwCdCiW1mh8YFDm0xny+PYoYZkGXiDOYY0nvg4yJ/BG2fQkkC5TNizr0l
YcE3RrMRcyJB7zU3jN1QnjHIvIvwfCOXaLdcXtxgQFRv45sYpmj9amNjuurM5iUa
20Mk1ilYBuLxqe6P9C8DakZY/akVxpzxrQIDAQAB
-----END RSA PUBLIC KEY-----

View File

@@ -19,6 +19,30 @@
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106" PrivateAssets="all" />
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
</ItemGroup>
<!-- Embed public-key.pem and copy to .launcher/update/ in output directory -->
<ItemGroup>
<None Include="Assets\public-key.pem" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Target Name="CopyPublicKeyToLauncherDir" AfterTargets="Build">
<PropertyGroup>
<PublicKeySource>$(MSBuildProjectDirectory)\Assets\public-key.pem</PublicKeySource>
<PublicKeyDestDir>$(OutDir).launcher\update</PublicKeyDestDir>
</PropertyGroup>
<MakeDir Directories="$(PublicKeyDestDir)" />
<Copy SourceFiles="$(PublicKeySource)" DestinationFolder="$(PublicKeyDestDir)" SkipUnchangedFiles="true" />
</Target>
<Target Name="CopyPublicKeyToPublishDir" AfterTargets="Publish">
<PropertyGroup>
<PublishedKeySource>$(MSBuildProjectDirectory)\Assets\public-key.pem</PublishedKeySource>
<PublishedKeyDestDir>$(PublishDir).launcher\update</PublishedKeyDestDir>
</PropertyGroup>
<MakeDir Directories="$(PublishedKeyDestDir)" />
<Copy SourceFiles="$(PublishedKeySource)" DestinationFolder="$(PublishedKeyDestDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -89,7 +89,7 @@ internal static class Commands
return context.SubCommand.ToLowerInvariant() switch
{
"check" => updateEngine.CheckPendingUpdate(),
"apply" => updateEngine.ApplyPendingUpdate(),
"apply" => await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false),
"rollback" => updateEngine.RollbackLatest(),
"download" => await updateEngine.DownloadAsync(
context.GetOption("manifest-url") ?? throw new InvalidOperationException("Missing --manifest-url."),

View File

@@ -0,0 +1,32 @@
using Avalonia.Threading;
using LanMountainDesktop.Launcher.Views;
namespace LanMountainDesktop.Launcher.Services;
internal sealed class DeferredSplashStageReporter : ISplashStageReporter
{
private ISplashStageReporter? _inner;
private readonly List<(string Stage, string Message)> _pending = [];
public void SetInner(ISplashStageReporter inner)
{
_inner = inner;
foreach (var (stage, message) in _pending)
{
_inner.Report(stage, message);
}
_pending.Clear();
}
public void Report(string stage, string message)
{
if (_inner is not null)
{
_inner.Report(stage, message);
}
else
{
_pending.Add((stage, message));
}
}
}

View File

@@ -13,7 +13,6 @@ internal sealed class LauncherFlowCoordinator
private readonly UpdateEngineService _updateEngine;
private readonly UpdateCheckService _updateCheckService;
private readonly PluginInstallerService _pluginInstallerService;
private readonly ISplashStageReporter _splashStageReporter;
private readonly IReadOnlyList<IOobeStep> _oobeSteps;
public LauncherFlowCoordinator(
@@ -30,7 +29,6 @@ internal sealed class LauncherFlowCoordinator
_updateEngine = updateEngine;
_updateCheckService = updateCheckService;
_pluginInstallerService = pluginInstallerService;
_splashStageReporter = new NullSplashStageReporter();
_oobeSteps = [new WelcomeOobeStep(_oobeStateService)];
}
@@ -41,7 +39,6 @@ internal sealed class LauncherFlowCoordinator
// 清理待删除的旧版本
_deploymentLocator.CleanupDestroyedDeployments();
_splashStageReporter.Report("bootstrap", "bootstrap");
if (_oobeStateService.IsFirstRun())
{
foreach (var step in _oobeSteps)
@@ -57,16 +54,18 @@ internal sealed class LauncherFlowCoordinator
return window;
});
var reporter = (ISplashStageReporter)splashWindow;
try
{
_splashStageReporter.Report("silentUpdate", "update");
var updateResult = _updateEngine.ApplyPendingUpdate();
reporter.Report("silentUpdate", "update");
var updateResult = await _updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
if (!updateResult.Success)
{
return updateResult;
}
_splashStageReporter.Report("pluginTasks", "plugins");
reporter.Report("pluginTasks", "plugins");
var pluginsDir = _context.GetOption("plugins-dir")
?? Path.Combine(_deploymentLocator.GetAppRoot(), "plugins");
var queueResult = new PluginUpgradeQueueService(_pluginInstallerService).ApplyPendingUpgrades(pluginsDir);
@@ -75,7 +74,7 @@ internal sealed class LauncherFlowCoordinator
return queueResult;
}
_splashStageReporter.Report("launchHost", "launch");
reporter.Report("launchHost", "launch");
var hostResult = LaunchHost();
if (!hostResult.Success)
{
@@ -192,13 +191,4 @@ internal sealed class LauncherFlowCoordinator
}
}
}
private sealed class NullSplashStageReporter : ISplashStageReporter
{
public void Report(string stage, string message)
{
_ = stage;
_ = message;
}
}
}

View File

@@ -110,7 +110,7 @@ internal sealed class UpdateEngineService
};
}
public LauncherResult ApplyPendingUpdate()
public async Task<LauncherResult> ApplyPendingUpdateAsync()
{
Directory.CreateDirectory(_incomingRoot);
Directory.CreateDirectory(_snapshotsRoot);
@@ -136,7 +136,7 @@ internal sealed class UpdateEngineService
return Failed("update.apply", "signature_failed", verifyResult.Message);
}
var fileMapText = File.ReadAllText(fileMapPath);
var fileMapText = await File.ReadAllTextAsync(fileMapPath);
var fileMap = JsonSerializer.Deserialize<SignedFileMap>(fileMapText);
if (fileMap is null || fileMap.Files.Count == 0)
{

View File

@@ -3,15 +3,38 @@
x:Class="LanMountainDesktop.Launcher.Views.SplashWindow"
Title="阑山桌面"
Width="420"
Height="220"
Height="240"
CanResize="False"
WindowStartupLocation="CenterScreen"
SystemDecorations="None">
<Grid Margin="24">
<Grid Margin="24" RowDefinitions="*,Auto,Auto,Auto">
<TextBlock x:Name="AppNameText"
Text="阑山桌面"
FontSize="34"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
HorizontalAlignment="Center"
Grid.Row="0" />
<ProgressBar x:Name="ProgressIndicator"
Grid.Row="1"
Minimum="0"
Maximum="100"
Value="0"
Height="4"
Margin="0,12,0,0"
IsIndeterminate="True" />
<TextBlock x:Name="StageText"
Grid.Row="2"
FontSize="12"
Foreground="#999999"
HorizontalAlignment="Center"
Margin="0,8,0,0"
Text="" />
<TextBlock x:Name="DetailText"
Grid.Row="3"
FontSize="11"
Foreground="#BBBBBB"
HorizontalAlignment="Center"
Margin="0,2,0,0"
Text="" />
</Grid>
</Window>

View File

@@ -1,12 +1,48 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LanMountainDesktop.Launcher.Services;
namespace LanMountainDesktop.Launcher.Views;
internal partial class SplashWindow : Window
internal partial class SplashWindow : Window, ISplashStageReporter
{
private static readonly (string Stage, string Label, double Progress)[] StageMap =
[
("bootstrap", "正在初始化...", 10),
("silentUpdate", "正在应用更新...", 35),
("pluginTasks", "正在处理插件...", 65),
("launchHost", "正在启动...", 90),
];
public SplashWindow()
{
AvaloniaXamlLoader.Load(this);
}
public void Report(string stage, string message)
{
var (label, progress) = ResolveStageInfo(stage);
var stageText = this.GetControl<TextBlock>("StageText");
var detailText = this.GetControl<TextBlock>("DetailText");
var progressIndicator = this.GetControl<ProgressBar>("ProgressIndicator");
stageText.Text = label;
detailText.Text = message;
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = progress;
}
private static (string Label, double Progress) ResolveStageInfo(string stage)
{
foreach (var (s, label, progress) in StageMap)
{
if (string.Equals(s, stage, StringComparison.OrdinalIgnoreCase))
{
return (label, progress);
}
}
return (stage, 0);
}
}