mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87110f1d69 |
@@ -6,6 +6,13 @@ using LanMountainDesktop.PluginSdk;
|
|||||||
|
|
||||||
internal static class Program
|
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)
|
private static async Task<int> Main(string[] args)
|
||||||
{
|
{
|
||||||
var result = new HelperResult();
|
var result = new HelperResult();
|
||||||
@@ -35,10 +42,12 @@ internal static class Program
|
|||||||
|
|
||||||
var manifest = ReadManifestFromPackage(fullSourcePath);
|
var manifest = ReadManifestFromPackage(fullSourcePath);
|
||||||
Directory.CreateDirectory(fullPluginsDirectory);
|
Directory.CreateDirectory(fullPluginsDirectory);
|
||||||
RemoveExistingPluginPackages(fullPluginsDirectory, manifest.Id);
|
|
||||||
|
|
||||||
var destinationPath = Path.Combine(fullPluginsDirectory, BuildInstalledPackageFileName(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
|
result = new HelperResult
|
||||||
{
|
{
|
||||||
@@ -123,7 +132,7 @@ internal static class Program
|
|||||||
return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
|
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));
|
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
|
||||||
foreach (var existingPackagePath in Directory
|
foreach (var existingPackagePath in Directory
|
||||||
@@ -133,13 +142,19 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (string.Equals(existingPackagePath, Path.GetFullPath(destinationPath), StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(existingPackagePath, Path.GetFullPath(stagingPath), StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var existingManifest = ReadManifestFromPackage(existingPackagePath);
|
var existingManifest = ReadManifestFromPackage(existingPackagePath);
|
||||||
if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Delete(existingPackagePath);
|
DeleteFileWithRetry(existingPackagePath);
|
||||||
}
|
}
|
||||||
catch
|
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)
|
private static string BuildInstalledPackageFileName(string pluginId)
|
||||||
{
|
{
|
||||||
var invalidChars = Path.GetInvalidFileNameChars();
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
|||||||
@@ -271,6 +271,10 @@ public partial class App : Application
|
|||||||
mainWindow.Activate();
|
mainWindow.Activate();
|
||||||
mainWindow.Topmost = true;
|
mainWindow.Topmost = true;
|
||||||
mainWindow.Topmost = false;
|
mainWindow.Topmost = false;
|
||||||
|
if (mainWindow is MainWindow lanMountainMainWindow)
|
||||||
|
{
|
||||||
|
lanMountainMainWindow.ShowSingleInstanceNotice();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -743,7 +743,10 @@
|
|||||||
"placement.fit": "Fit",
|
"placement.fit": "Fit",
|
||||||
"placement.stretch": "Stretch",
|
"placement.stretch": "Stretch",
|
||||||
"placement.center": "Center",
|
"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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -743,7 +743,10 @@
|
|||||||
"placement.fit": "适应",
|
"placement.fit": "适应",
|
||||||
"placement.stretch": "拉伸",
|
"placement.stretch": "拉伸",
|
||||||
"placement.center": "居中",
|
"placement.center": "居中",
|
||||||
"placement.tile": "平铺"
|
"placement.tile": "平铺",
|
||||||
}
|
"single_instance.notice.title": "应用已打开",
|
||||||
|
"single_instance.notice.description": "阑山桌面已经在运行,已为你切换到当前正在使用的桌面。",
|
||||||
|
"single_instance.notice.button": "知道了"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.WebView.Desktop;
|
using Avalonia.WebView.Desktop;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop;
|
namespace LanMountainDesktop;
|
||||||
|
|
||||||
@@ -17,12 +19,11 @@ sealed class Program
|
|||||||
AppLogger.Initialize();
|
AppLogger.Initialize();
|
||||||
RegisterGlobalExceptionLogging();
|
RegisterGlobalExceptionLogging();
|
||||||
|
|
||||||
using var singleInstance = SingleInstanceService.CreateDefault();
|
using var singleInstance = AcquireSingleInstance(args);
|
||||||
if (!singleInstance.IsPrimaryInstance)
|
if (!singleInstance.IsPrimaryInstance)
|
||||||
{
|
{
|
||||||
AppLogger.Warn("Startup", "A secondary launch was blocked because another instance is already running.");
|
AppLogger.Warn("Startup", "A secondary launch was blocked because another instance is already running.");
|
||||||
var notified = singleInstance.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2));
|
_ = singleInstance.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2));
|
||||||
ShowAlreadyRunningNotice(notified);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,42 @@ sealed class Program
|
|||||||
return builder;
|
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()
|
private static string LoadConfiguredRenderMode()
|
||||||
{
|
{
|
||||||
try
|
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";
|
try
|
||||||
var message = notifiedPrimaryInstance
|
{
|
||||||
? "应用已打开,不需要多开了。\r\n\r\n已为你切换到正在运行的阑山桌面。"
|
using var process = Process.GetProcessById(processId);
|
||||||
: "应用已打开,不需要多开了。\r\n\r\n请切换到正在运行的阑山桌面。";
|
var remaining = deadlineUtc - DateTime.UtcNow;
|
||||||
|
if (remaining > TimeSpan.Zero)
|
||||||
WindowsNativeDialogService.ShowInformation(caption, message);
|
{
|
||||||
|
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()
|
private static void RegisterGlobalExceptionLogging()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace LanMountainDesktop.Services;
|
|||||||
|
|
||||||
public static class AppRestartService
|
public static class AppRestartService
|
||||||
{
|
{
|
||||||
|
private const string RestartParentPidArgumentPrefix = "--restart-parent-pid=";
|
||||||
|
|
||||||
public static bool TryRestartApplication()
|
public static bool TryRestartApplication()
|
||||||
{
|
{
|
||||||
return App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
return App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
||||||
@@ -75,6 +77,21 @@ public static class AppRestartService
|
|||||||
return null;
|
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(
|
private static ProcessStartInfo CreateExecutableStartInfo(
|
||||||
string executablePath,
|
string executablePath,
|
||||||
string? entryAssemblyPath,
|
string? entryAssemblyPath,
|
||||||
@@ -88,6 +105,7 @@ public static class AppRestartService
|
|||||||
};
|
};
|
||||||
|
|
||||||
AppendArguments(startInfo, commandLineArgs);
|
AppendArguments(startInfo, commandLineArgs);
|
||||||
|
AppendRestartParentProcessArgument(startInfo);
|
||||||
return startInfo;
|
return startInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +128,7 @@ public static class AppRestartService
|
|||||||
|
|
||||||
startInfo.ArgumentList.Add(entryAssemblyPath);
|
startInfo.ArgumentList.Add(entryAssemblyPath);
|
||||||
AppendArguments(startInfo, commandLineArgs);
|
AppendArguments(startInfo, commandLineArgs);
|
||||||
|
AppendRestartParentProcessArgument(startInfo);
|
||||||
return startInfo;
|
return startInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +136,34 @@ public static class AppRestartService
|
|||||||
{
|
{
|
||||||
for (var i = 1; i < commandLineArgs.Count; i++)
|
for (var i = 1; i < commandLineArgs.Count; i++)
|
||||||
{
|
{
|
||||||
|
if (TryParseRestartParentProcessId(commandLineArgs[i], out _))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
startInfo.ArgumentList.Add(commandLineArgs[i]);
|
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)
|
private static string? NormalizeExistingPath(string? path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
|||||||
59
LanMountainDesktop/Views/MainWindow.SingleInstanceNotice.cs
Normal file
59
LanMountainDesktop/Views/MainWindow.SingleInstanceNotice.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -469,51 +469,98 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ui:NavigationView>
|
</ui:NavigationView>
|
||||||
|
|
||||||
<Border x:Name="PendingRestartDock"
|
<StackPanel Grid.Row="1"
|
||||||
Grid.Row="1"
|
Spacing="12">
|
||||||
IsVisible="False"
|
<Border x:Name="SingleInstanceNoticeDock"
|
||||||
Classes="glass-panel"
|
IsVisible="False"
|
||||||
CornerRadius="18"
|
Classes="glass-panel"
|
||||||
Padding="14,12">
|
CornerRadius="18"
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
Padding="14,12">
|
||||||
ColumnSpacing="12">
|
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||||
<Border Width="34"
|
ColumnSpacing="12">
|
||||||
Height="34"
|
<Border Width="34"
|
||||||
CornerRadius="17"
|
Height="34"
|
||||||
Background="{DynamicResource AdaptiveAccentBrush}">
|
CornerRadius="17"
|
||||||
<fi:FluentIcon Icon="ArrowSync"
|
Background="{DynamicResource AdaptiveAccentBrush}">
|
||||||
IconVariant="Regular"
|
<fi:FluentIcon Icon="Alert"
|
||||||
FontSize="16"
|
IconVariant="Regular"
|
||||||
Foreground="White"
|
FontSize="16"
|
||||||
HorizontalAlignment="Center"
|
Foreground="White"
|
||||||
VerticalAlignment="Center" />
|
HorizontalAlignment="Center"
|
||||||
</Border>
|
VerticalAlignment="Center" />
|
||||||
<StackPanel Grid.Column="1"
|
</Border>
|
||||||
Spacing="2"
|
<StackPanel Grid.Column="1"
|
||||||
VerticalAlignment="Center">
|
Spacing="2"
|
||||||
<TextBlock x:Name="PendingRestartDockTitleTextBlock"
|
VerticalAlignment="Center">
|
||||||
FontSize="13"
|
<TextBlock x:Name="SingleInstanceNoticeTitleTextBlock"
|
||||||
FontWeight="SemiBold"
|
FontSize="13"
|
||||||
Text="Restart required" />
|
FontWeight="SemiBold"
|
||||||
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
|
Text="App already open" />
|
||||||
TextWrapping="Wrap"
|
<TextBlock x:Name="SingleInstanceNoticeDescriptionTextBlock"
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
TextWrapping="Wrap"
|
||||||
Text="Your changes will apply after restarting the app." />
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||||
</StackPanel>
|
Text="LanMountainDesktop is already running. Switched back to the active desktop." />
|
||||||
<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>
|
</StackPanel>
|
||||||
</Button>
|
<Button x:Name="SingleInstanceNoticeButton"
|
||||||
</Grid>
|
Grid.Column="2"
|
||||||
</Border>
|
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>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -78,9 +78,13 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var hashStream = File.OpenRead(downloadPath);
|
string actualHash;
|
||||||
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken);
|
await using (var hashStream = File.OpenRead(downloadPath))
|
||||||
var actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
{
|
||||||
|
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken);
|
||||||
|
actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
File.Delete(downloadPath);
|
File.Delete(downloadPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user