市场插件安装机制修复,然后修复了一大堆东西
This commit is contained in:
lincube
2026-03-11 15:14:08 +08:00
parent e7a03404ce
commit 87110f1d69
9 changed files with 344 additions and 68 deletions

View File

@@ -6,6 +6,13 @@ using LanMountainDesktop.PluginSdk;
internal static class Program
{
private static readonly TimeSpan[] RetryDelays =
[
TimeSpan.FromMilliseconds(120),
TimeSpan.FromMilliseconds(250),
TimeSpan.FromMilliseconds(500)
];
private static async Task<int> Main(string[] args)
{
var result = new HelperResult();
@@ -35,10 +42,12 @@ internal static class Program
var manifest = ReadManifestFromPackage(fullSourcePath);
Directory.CreateDirectory(fullPluginsDirectory);
RemoveExistingPluginPackages(fullPluginsDirectory, manifest.Id);
var destinationPath = Path.Combine(fullPluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
File.Copy(fullSourcePath, destinationPath, overwrite: true);
var stagingPath = destinationPath + ".incoming";
DeleteFileWithRetry(stagingPath);
CopyWithRetry(fullSourcePath, stagingPath, overwrite: true);
RemoveExistingPluginPackages(fullPluginsDirectory, manifest.Id, destinationPath, stagingPath);
MoveWithOverwriteRetry(stagingPath, destinationPath);
result = new HelperResult
{
@@ -123,7 +132,7 @@ internal static class Program
return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
}
private static void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId)
private static void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
{
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
foreach (var existingPackagePath in Directory
@@ -133,13 +142,19 @@ internal static class Program
{
try
{
if (string.Equals(existingPackagePath, Path.GetFullPath(destinationPath), StringComparison.OrdinalIgnoreCase) ||
string.Equals(existingPackagePath, Path.GetFullPath(stagingPath), StringComparison.OrdinalIgnoreCase))
{
continue;
}
var existingManifest = ReadManifestFromPackage(existingPackagePath);
if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
{
continue;
}
File.Delete(existingPackagePath);
DeleteFileWithRetry(existingPackagePath);
}
catch
{
@@ -148,6 +163,56 @@ internal static class Program
}
}
private static void CopyWithRetry(string sourcePath, string destinationPath, bool overwrite)
{
Retry(() => File.Copy(sourcePath, destinationPath, overwrite));
}
private static void MoveWithOverwriteRetry(string sourcePath, string destinationPath)
{
Retry(() => File.Move(sourcePath, destinationPath, overwrite: true));
}
private static void DeleteFileWithRetry(string filePath)
{
Retry(() =>
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
});
}
private static void Retry(Action action)
{
Exception? lastException = null;
for (var attempt = 0; attempt <= RetryDelays.Length; attempt++)
{
try
{
action();
return;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
lastException = ex;
if (attempt >= RetryDelays.Length)
{
break;
}
Thread.Sleep(RetryDelays[attempt]);
}
}
if (lastException is not null)
{
throw lastException;
}
}
private static string BuildInstalledPackageFileName(string pluginId)
{
var invalidChars = Path.GetInvalidFileNameChars();

View File

@@ -271,6 +271,10 @@ public partial class App : Application
mainWindow.Activate();
mainWindow.Topmost = true;
mainWindow.Topmost = false;
if (mainWindow is MainWindow lanMountainMainWindow)
{
lanMountainMainWindow.ShowSingleInstanceNotice();
}
}
catch (Exception ex)
{

View File

@@ -743,7 +743,10 @@
"placement.fit": "Fit",
"placement.stretch": "Stretch",
"placement.center": "Center",
"placement.tile": "Tile"
}
"placement.tile": "Tile",
"single_instance.notice.title": "App already open",
"single_instance.notice.description": "LanMountainDesktop is already running. Switched back to the active desktop.",
"single_instance.notice.button": "Got it"
}

View File

@@ -743,7 +743,10 @@
"placement.fit": "适应",
"placement.stretch": "拉伸",
"placement.center": "居中",
"placement.tile": "平铺"
}
"placement.tile": "平铺",
"single_instance.notice.title": "应用已打开",
"single_instance.notice.description": "阑山桌面已经在运行,已为你切换到当前正在使用的桌面。",
"single_instance.notice.button": "知道了"
}

View File

@@ -1,8 +1,10 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.WebView.Desktop;
using LanMountainDesktop.Services;
using System;
using System.Threading.Tasks;
namespace LanMountainDesktop;
@@ -17,12 +19,11 @@ sealed class Program
AppLogger.Initialize();
RegisterGlobalExceptionLogging();
using var singleInstance = SingleInstanceService.CreateDefault();
using var singleInstance = AcquireSingleInstance(args);
if (!singleInstance.IsPrimaryInstance)
{
AppLogger.Warn("Startup", "A secondary launch was blocked because another instance is already running.");
var notified = singleInstance.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2));
ShowAlreadyRunningNotice(notified);
_ = singleInstance.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2));
return;
}
@@ -72,6 +73,42 @@ sealed class Program
return builder;
}
private static SingleInstanceService AcquireSingleInstance(string[] args)
{
var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args);
var singleInstance = SingleInstanceService.CreateDefault();
if (singleInstance.IsPrimaryInstance || restartParentProcessId is null)
{
return singleInstance;
}
AppLogger.Info(
"Startup",
$"Restart relaunch detected. Waiting for previous instance pid={restartParentProcessId.Value} to exit before re-acquiring the single-instance lock.");
singleInstance.Dispose();
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(12);
WaitForRestartParentExit(restartParentProcessId.Value, deadline);
while (DateTime.UtcNow < deadline)
{
var retryInstance = SingleInstanceService.CreateDefault();
if (retryInstance.IsPrimaryInstance)
{
AppLogger.Info("Startup", "Restart relaunch acquired the single-instance lock.");
return retryInstance;
}
retryInstance.Dispose();
Thread.Sleep(150);
}
AppLogger.Warn(
"Startup",
$"Restart relaunch timed out while waiting for the single-instance lock. pid={restartParentProcessId.Value}.");
return SingleInstanceService.CreateDefault();
}
private static string LoadConfiguredRenderMode()
{
try
@@ -85,14 +122,25 @@ sealed class Program
}
}
private static void ShowAlreadyRunningNotice(bool notifiedPrimaryInstance)
private static void WaitForRestartParentExit(int processId, DateTime deadlineUtc)
{
const string caption = "LanMountainDesktop";
var message = notifiedPrimaryInstance
? "应用已打开,不需要多开了。\r\n\r\n已为你切换到正在运行的阑山桌面。"
: "应用已打开,不需要多开了。\r\n\r\n请切换到正在运行的阑山桌面。";
WindowsNativeDialogService.ShowInformation(caption, message);
try
{
using var process = Process.GetProcessById(processId);
var remaining = deadlineUtc - DateTime.UtcNow;
if (remaining > TimeSpan.Zero)
{
process.WaitForExit((int)Math.Ceiling(remaining.TotalMilliseconds));
}
}
catch (ArgumentException)
{
// The previous process already exited before we started waiting.
}
catch (Exception ex)
{
AppLogger.Warn("Startup", $"Failed while waiting for restart parent pid={processId} to exit.", ex);
}
}
private static void RegisterGlobalExceptionLogging()

View File

@@ -9,6 +9,8 @@ namespace LanMountainDesktop.Services;
public static class AppRestartService
{
private const string RestartParentPidArgumentPrefix = "--restart-parent-pid=";
public static bool TryRestartApplication()
{
return App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
@@ -75,6 +77,21 @@ public static class AppRestartService
return null;
}
public static int? TryGetRestartParentProcessId(IReadOnlyList<string> commandLineArgs)
{
ArgumentNullException.ThrowIfNull(commandLineArgs);
foreach (var argument in commandLineArgs)
{
if (TryParseRestartParentProcessId(argument, out var processId))
{
return processId;
}
}
return null;
}
private static ProcessStartInfo CreateExecutableStartInfo(
string executablePath,
string? entryAssemblyPath,
@@ -88,6 +105,7 @@ public static class AppRestartService
};
AppendArguments(startInfo, commandLineArgs);
AppendRestartParentProcessArgument(startInfo);
return startInfo;
}
@@ -110,6 +128,7 @@ public static class AppRestartService
startInfo.ArgumentList.Add(entryAssemblyPath);
AppendArguments(startInfo, commandLineArgs);
AppendRestartParentProcessArgument(startInfo);
return startInfo;
}
@@ -117,10 +136,34 @@ public static class AppRestartService
{
for (var i = 1; i < commandLineArgs.Count; i++)
{
if (TryParseRestartParentProcessId(commandLineArgs[i], out _))
{
continue;
}
startInfo.ArgumentList.Add(commandLineArgs[i]);
}
}
private static void AppendRestartParentProcessArgument(ProcessStartInfo startInfo)
{
startInfo.ArgumentList.Add($"{RestartParentPidArgumentPrefix}{Environment.ProcessId}");
}
private static bool TryParseRestartParentProcessId(string? argument, out int processId)
{
processId = 0;
if (string.IsNullOrWhiteSpace(argument) ||
!argument.StartsWith(RestartParentPidArgumentPrefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return int.TryParse(
argument[RestartParentPidArgumentPrefix.Length..],
out processId) && processId > 0;
}
private static string? NormalizeExistingPath(string? path)
{
if (string.IsNullOrWhiteSpace(path))

View File

@@ -0,0 +1,59 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private readonly DispatcherTimer _singleInstanceNoticeTimer = new()
{
Interval = TimeSpan.FromSeconds(6)
};
internal void ShowSingleInstanceNotice()
{
if (Dispatcher.UIThread.CheckAccess())
{
ShowSingleInstanceNoticeCore();
return;
}
Dispatcher.UIThread.Post(ShowSingleInstanceNoticeCore, DispatcherPriority.Send);
}
private void ShowSingleInstanceNoticeCore()
{
SingleInstanceNoticeTitleTextBlock.Text = L(
"single_instance.notice.title",
"App already open");
SingleInstanceNoticeDescriptionTextBlock.Text = L(
"single_instance.notice.description",
"LanMountainDesktop is already running. Switched back to the active desktop.");
SingleInstanceNoticeButtonTextBlock.Text = L(
"single_instance.notice.button",
"Got it");
SingleInstanceNoticeDock.IsVisible = true;
_singleInstanceNoticeTimer.Stop();
_singleInstanceNoticeTimer.Tick -= OnSingleInstanceNoticeTimerTick;
_singleInstanceNoticeTimer.Tick += OnSingleInstanceNoticeTimerTick;
_singleInstanceNoticeTimer.Start();
}
private void OnSingleInstanceNoticeButtonClick(object? sender, RoutedEventArgs e)
{
HideSingleInstanceNotice();
}
private void OnSingleInstanceNoticeTimerTick(object? sender, EventArgs e)
{
HideSingleInstanceNotice();
}
private void HideSingleInstanceNotice()
{
_singleInstanceNoticeTimer.Stop();
SingleInstanceNoticeDock.IsVisible = false;
}
}

View File

@@ -469,51 +469,98 @@
</ScrollViewer>
</ui:NavigationView>
<Border x:Name="PendingRestartDock"
Grid.Row="1"
IsVisible="False"
Classes="glass-panel"
CornerRadius="18"
Padding="14,12">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="12">
<Border Width="34"
Height="34"
CornerRadius="17"
Background="{DynamicResource AdaptiveAccentBrush}">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="16"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="PendingRestartDockTitleTextBlock"
FontSize="13"
FontWeight="SemiBold"
Text="Restart required" />
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Your changes will apply after restarting the app." />
</StackPanel>
<Button x:Name="PendingRestartDockButton"
Grid.Column="2"
Padding="14,8"
Click="OnPendingRestartDockButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular" />
<TextBlock x:Name="PendingRestartDockButtonTextBlock"
VerticalAlignment="Center"
Text="Restart app" />
<StackPanel Grid.Row="1"
Spacing="12">
<Border x:Name="SingleInstanceNoticeDock"
IsVisible="False"
Classes="glass-panel"
CornerRadius="18"
Padding="14,12">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="12">
<Border Width="34"
Height="34"
CornerRadius="17"
Background="{DynamicResource AdaptiveAccentBrush}">
<fi:FluentIcon Icon="Alert"
IconVariant="Regular"
FontSize="16"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="SingleInstanceNoticeTitleTextBlock"
FontSize="13"
FontWeight="SemiBold"
Text="App already open" />
<TextBlock x:Name="SingleInstanceNoticeDescriptionTextBlock"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="LanMountainDesktop is already running. Switched back to the active desktop." />
</StackPanel>
</Button>
</Grid>
</Border>
<Button x:Name="SingleInstanceNoticeButton"
Grid.Column="2"
Padding="14,8"
Click="OnSingleInstanceNoticeButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="Checkmark"
IconVariant="Regular" />
<TextBlock x:Name="SingleInstanceNoticeButtonTextBlock"
VerticalAlignment="Center"
Text="Got it" />
</StackPanel>
</Button>
</Grid>
</Border>
<Border x:Name="PendingRestartDock"
IsVisible="False"
Classes="glass-panel"
CornerRadius="18"
Padding="14,12">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="12">
<Border Width="34"
Height="34"
CornerRadius="17"
Background="{DynamicResource AdaptiveAccentBrush}">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="16"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="PendingRestartDockTitleTextBlock"
FontSize="13"
FontWeight="SemiBold"
Text="Restart required" />
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Your changes will apply after restarting the app." />
</StackPanel>
<Button x:Name="PendingRestartDockButton"
Grid.Column="2"
Padding="14,8"
Click="OnPendingRestartDockButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular" />
<TextBlock x:Name="PendingRestartDockButtonTextBlock"
VerticalAlignment="Center"
Text="Restart app" />
</StackPanel>
</Button>
</Grid>
</Border>
</StackPanel>
</Grid>
</Border>
</Border>

View File

@@ -78,9 +78,13 @@ internal sealed class AirAppMarketInstallService : IDisposable
}
}
await using var hashStream = File.OpenRead(downloadPath);
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken);
var actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
string actualHash;
await using (var hashStream = File.OpenRead(downloadPath))
{
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken);
actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
}
if (!string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
{
File.Delete(downloadPath);