mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.5.14
二次启动拦截,统一了生命进程API
This commit is contained in:
@@ -67,6 +67,11 @@
|
|||||||
"capability.message_bus.detail": "This sample plugin uses IPluginMessageBus to push clock ticks and state change notifications into plugin UI surfaces.",
|
"capability.message_bus.detail": "This sample plugin uses IPluginMessageBus to push clock ticks and state change notifications into plugin UI surfaces.",
|
||||||
"capability.widget_context.title": "PluginDesktopComponentContext",
|
"capability.widget_context.title": "PluginDesktopComponentContext",
|
||||||
"capability.widget_context.detail": "Widgets can read ComponentId, PlacementId, CellSize, and call GetService<T>() against the same plugin service container.",
|
"capability.widget_context.detail": "Widgets can read ComponentId, PlacementId, CellSize, and call GetService<T>() against the same plugin service container.",
|
||||||
|
"widget.close_desktop.display_name": "Close Desktop",
|
||||||
|
"widget.close_desktop.text": "Close Desktop",
|
||||||
|
"widget.close_desktop.hint": "Exit LanMountainDesktop on click",
|
||||||
|
"widget.close_desktop.unavailable": "Host lifecycle API is unavailable",
|
||||||
|
"widget.close_desktop.failed": "Host rejected the exit request",
|
||||||
"widget.subtitle.preview": "Preview surface | placed: {0}",
|
"widget.subtitle.preview": "Preview surface | placed: {0}",
|
||||||
"widget.subtitle.placement": "Placement {0} | placed: {1}",
|
"widget.subtitle.placement": "Placement {0} | placed: {1}",
|
||||||
"common.dev": "dev",
|
"common.dev": "dev",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public sealed class SamplePlugin : PluginBase, IDisposable
|
|||||||
var hostName = GetHostProperty(context, PluginHostPropertyKeys.HostApplicationName, "UnknownHost");
|
var hostName = GetHostProperty(context, PluginHostPropertyKeys.HostApplicationName, "UnknownHost");
|
||||||
var hostVersion = GetHostProperty(context, PluginHostPropertyKeys.HostVersion, "UnknownVersion");
|
var hostVersion = GetHostProperty(context, PluginHostPropertyKeys.HostVersion, "UnknownVersion");
|
||||||
var sdkApiVersion = GetHostProperty(context, PluginHostPropertyKeys.PluginSdkApiVersion, "UnknownApiVersion");
|
var sdkApiVersion = GetHostProperty(context, PluginHostPropertyKeys.PluginSdkApiVersion, "UnknownApiVersion");
|
||||||
|
var hostApplicationLifecycle = context.GetService<IHostApplicationLifecycle>();
|
||||||
var messageBus = context.GetService<IPluginMessageBus>()
|
var messageBus = context.GetService<IPluginMessageBus>()
|
||||||
?? throw new InvalidOperationException("Plugin message bus is not available.");
|
?? throw new InvalidOperationException("Plugin message bus is not available.");
|
||||||
|
|
||||||
@@ -74,6 +75,19 @@ public sealed class SamplePlugin : PluginBase, IDisposable
|
|||||||
allowStatusBarPlacement: false,
|
allowStatusBarPlacement: false,
|
||||||
resizeMode: PluginDesktopComponentResizeMode.Proportional,
|
resizeMode: PluginDesktopComponentResizeMode.Proportional,
|
||||||
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.34, 18, 34)));
|
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.34, 18, 34)));
|
||||||
|
|
||||||
|
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
|
||||||
|
"LanMountainDesktop.SamplePlugin.CloseDesktop",
|
||||||
|
localizer.GetString("widget.close_desktop.display_name", "关闭桌面"),
|
||||||
|
widgetContext => new SamplePluginCloseDesktopWidget(widgetContext),
|
||||||
|
iconKey: "DismissCircle",
|
||||||
|
category: localizer.GetString("widget.category", "鎻掍欢"),
|
||||||
|
minWidthCells: 2,
|
||||||
|
minHeightCells: 1,
|
||||||
|
allowDesktopPlacement: true,
|
||||||
|
allowStatusBarPlacement: false,
|
||||||
|
resizeMode: PluginDesktopComponentResizeMode.Free,
|
||||||
|
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.28, 14, 22)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.SamplePlugin;
|
||||||
|
|
||||||
|
internal sealed class SamplePluginCloseDesktopWidget : Border
|
||||||
|
{
|
||||||
|
private readonly PluginLocalizer _localizer;
|
||||||
|
private readonly IHostApplicationLifecycle? _hostApplicationLifecycle;
|
||||||
|
private readonly TextBlock _titleTextBlock;
|
||||||
|
private readonly TextBlock _statusTextBlock;
|
||||||
|
|
||||||
|
public SamplePluginCloseDesktopWidget(PluginDesktopComponentContext context)
|
||||||
|
{
|
||||||
|
_localizer = PluginLocalizer.Create(context);
|
||||||
|
_hostApplicationLifecycle = context.GetService<IHostApplicationLifecycle>();
|
||||||
|
|
||||||
|
_titleTextBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = T("widget.close_desktop.text", "关闭桌面"),
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
FontWeight = FontWeight.SemiBold,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
_statusTextBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = _hostApplicationLifecycle is null
|
||||||
|
? T("widget.close_desktop.unavailable", "宿主未提供退出接口")
|
||||||
|
: T("widget.close_desktop.hint", "点击后退出阑山桌面"),
|
||||||
|
Foreground = new SolidColorBrush(Color.Parse("#FFD4E7F6")),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var contentGrid = new Grid
|
||||||
|
{
|
||||||
|
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
|
||||||
|
ColumnSpacing = 14,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
CreateIconShell(),
|
||||||
|
new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 2,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
_titleTextBlock,
|
||||||
|
_statusTextBlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(contentGrid.Children[1], 1);
|
||||||
|
|
||||||
|
var actionButton = new Button
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalContentAlignment = VerticalAlignment.Stretch,
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
BorderThickness = new Thickness(0),
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
IsEnabled = _hostApplicationLifecycle is not null,
|
||||||
|
Content = contentGrid
|
||||||
|
};
|
||||||
|
actionButton.Click += OnButtonClick;
|
||||||
|
|
||||||
|
Background = new LinearGradientBrush
|
||||||
|
{
|
||||||
|
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||||
|
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||||
|
GradientStops =
|
||||||
|
[
|
||||||
|
new GradientStop(Color.Parse("#FF0B1220"), 0),
|
||||||
|
new GradientStop(Color.Parse("#FF172554"), 0.55),
|
||||||
|
new GradientStop(Color.Parse("#FF7F1D1D"), 1)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
BorderBrush = new SolidColorBrush(Color.Parse("#66FB7185"));
|
||||||
|
BorderThickness = new Thickness(1);
|
||||||
|
CornerRadius = new CornerRadius(18);
|
||||||
|
Padding = new Thickness(14, 10);
|
||||||
|
Child = actionButton;
|
||||||
|
|
||||||
|
SizeChanged += OnSizeChanged;
|
||||||
|
ApplyScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreateIconShell()
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
Width = 36,
|
||||||
|
Height = 36,
|
||||||
|
CornerRadius = new CornerRadius(999),
|
||||||
|
Background = new SolidColorBrush(Color.Parse("#33F87171")),
|
||||||
|
BorderBrush = new SolidColorBrush(Color.Parse("#88FCA5A5")),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "⏻",
|
||||||
|
FontSize = 18,
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
TextAlignment = TextAlignment.Center
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnButtonClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_hostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
|
||||||
|
Source: "SamplePlugin.CloseDesktopWidget",
|
||||||
|
Reason: "User invoked the sample plugin close-desktop widget.")) == true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_statusTextBlock.Text = T("widget.close_desktop.failed", "宿主未接受退出请求");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
ApplyScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyScale()
|
||||||
|
{
|
||||||
|
var basis = Bounds.Height > 1 ? Bounds.Height : 72;
|
||||||
|
Padding = new Thickness(Math.Clamp(basis * 0.18, 12, 18), Math.Clamp(basis * 0.14, 8, 14));
|
||||||
|
CornerRadius = new CornerRadius(Math.Clamp(basis * 0.32, 16, 24));
|
||||||
|
|
||||||
|
if (Child is not Button actionButton || actionButton.Content is not Grid contentGrid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentGrid.Children[0] is Border iconShell)
|
||||||
|
{
|
||||||
|
var iconSize = Math.Clamp(basis * 0.58, 28, 40);
|
||||||
|
iconShell.Width = iconSize;
|
||||||
|
iconShell.Height = iconSize;
|
||||||
|
if (iconShell.Child is TextBlock iconText)
|
||||||
|
{
|
||||||
|
iconText.FontSize = Math.Clamp(iconSize * 0.5, 14, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_titleTextBlock.FontSize = Math.Clamp(basis * 0.28, 14, 20);
|
||||||
|
_statusTextBlock.FontSize = Math.Clamp(basis * 0.18, 10, 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string T(string key, string fallback)
|
||||||
|
{
|
||||||
|
return _localizer.GetString(key, fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
LanMountainDesktop.PluginSdk/IHostApplicationLifecycle.cs
Normal file
12
LanMountainDesktop.PluginSdk/IHostApplicationLifecycle.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LanMountainDesktop.PluginSdk;
|
||||||
|
|
||||||
|
public sealed record HostApplicationLifecycleRequest(
|
||||||
|
string? Source = null,
|
||||||
|
string? Reason = null);
|
||||||
|
|
||||||
|
public interface IHostApplicationLifecycle
|
||||||
|
{
|
||||||
|
bool TryExit(HostApplicationLifecycleRequest? request = null);
|
||||||
|
|
||||||
|
bool TryRestart(HostApplicationLifecycleRequest? request = null);
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk" TreatAsLocalProperty="Version;PackageVersion;InformationalVersion;AssemblyVersion;FileVersion">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<PackageVersion>$(Version)</PackageVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using LanMountainDesktop.Services;
|
|||||||
using LanMountainDesktop.ViewModels;
|
using LanMountainDesktop.ViewModels;
|
||||||
using LanMountainDesktop.Views;
|
using LanMountainDesktop.Views;
|
||||||
using AvaloniaWebView;
|
using AvaloniaWebView;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
|
||||||
namespace LanMountainDesktop;
|
namespace LanMountainDesktop;
|
||||||
|
|
||||||
@@ -19,12 +20,19 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
private readonly AppSettingsService _appSettingsService = new();
|
private readonly AppSettingsService _appSettingsService = new();
|
||||||
private readonly LocalizationService _localizationService = new();
|
private readonly LocalizationService _localizationService = new();
|
||||||
|
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
||||||
|
private bool _exitCleanupCompleted;
|
||||||
|
|
||||||
private SettingsWindow? _traySettingsWindow;
|
private SettingsWindow? _traySettingsWindow;
|
||||||
private TrayIcons? _trayIcons;
|
private TrayIcons? _trayIcons;
|
||||||
private PluginRuntimeService? _pluginRuntimeService;
|
private PluginRuntimeService? _pluginRuntimeService;
|
||||||
|
|
||||||
|
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
|
||||||
|
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
|
||||||
|
(Current as App)?._hostApplicationLifecycle;
|
||||||
|
|
||||||
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
||||||
|
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -51,14 +59,14 @@ public partial class App : Application
|
|||||||
desktop.Exit += (_, _) =>
|
desktop.Exit += (_, _) =>
|
||||||
{
|
{
|
||||||
AppLogger.Info("App", "Desktop lifetime exit triggered.");
|
AppLogger.Info("App", "Desktop lifetime exit triggered.");
|
||||||
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
|
PerformExitCleanup();
|
||||||
DisposeTrayIcon();
|
|
||||||
};
|
};
|
||||||
desktop.MainWindow = new MainWindow
|
desktop.MainWindow = new MainWindow
|
||||||
{
|
{
|
||||||
DataContext = new MainWindowViewModel(),
|
DataContext = new MainWindowViewModel(),
|
||||||
};
|
};
|
||||||
AppLogger.Info("App", $"Main window created. LogFile={AppLogger.LogFilePath}");
|
AppLogger.Info("App", $"Main window created. LogFile={AppLogger.LogFilePath}");
|
||||||
|
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
@@ -66,12 +74,9 @@ public partial class App : Application
|
|||||||
|
|
||||||
private void OnTrayExitClick(object? sender, EventArgs e)
|
private void OnTrayExitClick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
DisposeTrayIcon();
|
_ = _hostApplicationLifecycle.TryExit(new HostApplicationLifecycleRequest(
|
||||||
|
Source: "TrayMenu",
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
Reason: "User selected Exit App from the tray menu."));
|
||||||
{
|
|
||||||
desktop.Shutdown();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTraySettingsClick(object? sender, EventArgs e)
|
private void OnTraySettingsClick(object? sender, EventArgs e)
|
||||||
@@ -114,18 +119,9 @@ public partial class App : Application
|
|||||||
|
|
||||||
private void OnTrayRestartClick(object? sender, EventArgs e)
|
private void OnTrayRestartClick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
AppRestartService.TryRestartApplication();
|
_ = _hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
|
||||||
}
|
Source: "TrayMenu",
|
||||||
|
Reason: "User selected Restart App from the tray menu."));
|
||||||
private void OnAppSettingsSaved(string _)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
if (_trayIcons is not null)
|
|
||||||
{
|
|
||||||
InitializeTrayIcon();
|
|
||||||
}
|
|
||||||
}, DispatcherPriority.Background);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisableAvaloniaDataAnnotationValidation()
|
private void DisableAvaloniaDataAnnotationValidation()
|
||||||
@@ -246,6 +242,95 @@ public partial class App : Application
|
|||||||
_trayIcons = null;
|
_trayIcons = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ActivateMainWindow()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desktop.MainWindow is not Window mainWindow)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!mainWindow.IsVisible)
|
||||||
|
{
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainWindow.WindowState == WindowState.Minimized)
|
||||||
|
{
|
||||||
|
mainWindow.WindowState = WindowState.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.Activate();
|
||||||
|
mainWindow.Topmost = true;
|
||||||
|
mainWindow.Topmost = false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("SingleInstance", "Failed to activate the existing main window.", ex);
|
||||||
|
}
|
||||||
|
}, DispatcherPriority.Send);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAppSettingsSaved(string _)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (_trayIcons is not null)
|
||||||
|
{
|
||||||
|
InitializeTrayIcon();
|
||||||
|
}
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PerformExitCleanup()
|
||||||
|
{
|
||||||
|
if (_exitCleanupCompleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_exitCleanupCompleted = true;
|
||||||
|
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_traySettingsWindow?.Close();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("App", "Failed to close tray-opened settings window during shutdown.", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_traySettingsWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pluginRuntimeService?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("PluginRuntime", "Failed to dispose plugin runtime during shutdown.", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pluginRuntimeService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioRecorderServiceFactory.DisposeSharedServices();
|
||||||
|
StudyAnalyticsServiceFactory.DisposeSharedService();
|
||||||
|
DisposeTrayIcon();
|
||||||
|
}
|
||||||
|
|
||||||
private string L(string key, string fallback)
|
private string L(string key, string fallback)
|
||||||
{
|
{
|
||||||
var snapshot = _appSettingsService.Load();
|
var snapshot = _appSettingsService.Load();
|
||||||
|
|||||||
@@ -251,6 +251,7 @@
|
|||||||
"settings.update.status_launching_installer": "Download complete. Launching installer...",
|
"settings.update.status_launching_installer": "Download complete. Launching installer...",
|
||||||
"settings.update.status_installer_missing": "Installer file was not found after download.",
|
"settings.update.status_installer_missing": "Installer file was not found after download.",
|
||||||
"settings.update.status_installer_started": "Installer started. The app will close for update.",
|
"settings.update.status_installer_started": "Installer started. The app will close for update.",
|
||||||
|
"settings.update.status_elevation_cancelled": "Administrator permission was not granted. Update was cancelled.",
|
||||||
"settings.update.status_launch_failed_format": "Failed to start installer: {0}",
|
"settings.update.status_launch_failed_format": "Failed to start installer: {0}",
|
||||||
"settings.about.title": "About",
|
"settings.about.title": "About",
|
||||||
"settings.about.version_format": "Version: {0}",
|
"settings.about.version_format": "Version: {0}",
|
||||||
|
|||||||
@@ -251,6 +251,7 @@
|
|||||||
"settings.update.status_launching_installer": "下载完成,正在启动安装程序...",
|
"settings.update.status_launching_installer": "下载完成,正在启动安装程序...",
|
||||||
"settings.update.status_installer_missing": "下载后未找到安装包文件。",
|
"settings.update.status_installer_missing": "下载后未找到安装包文件。",
|
||||||
"settings.update.status_installer_started": "安装程序已启动,应用将关闭进行更新。",
|
"settings.update.status_installer_started": "安装程序已启动,应用将关闭进行更新。",
|
||||||
|
"settings.update.status_elevation_cancelled": "未授予管理员权限,更新已取消。",
|
||||||
"settings.update.status_launch_failed_format": "启动安装程序失败:{0}",
|
"settings.update.status_launch_failed_format": "启动安装程序失败:{0}",
|
||||||
"settings.about.title": "关于",
|
"settings.about.title": "关于",
|
||||||
"settings.about.version_format": "版本号: {0}",
|
"settings.about.version_format": "版本号: {0}",
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ sealed class Program
|
|||||||
AppLogger.Initialize();
|
AppLogger.Initialize();
|
||||||
RegisterGlobalExceptionLogging();
|
RegisterGlobalExceptionLogging();
|
||||||
|
|
||||||
|
using var singleInstance = SingleInstanceService.CreateDefault();
|
||||||
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var diagnostics = StartupDiagnosticsService.Run(args);
|
var diagnostics = StartupDiagnosticsService.Run(args);
|
||||||
StartupDiagnosticsService.ShowLegacyExecutableWarningIfNeeded(diagnostics);
|
StartupDiagnosticsService.ShowLegacyExecutableWarningIfNeeded(diagnostics);
|
||||||
|
|
||||||
@@ -24,6 +33,7 @@ sealed class Program
|
|||||||
{
|
{
|
||||||
var renderMode = LoadConfiguredRenderMode();
|
var renderMode = LoadConfiguredRenderMode();
|
||||||
AppLogger.Info("Startup", $"Resolved render mode '{renderMode}'.");
|
AppLogger.Info("Startup", $"Resolved render mode '{renderMode}'.");
|
||||||
|
App.CurrentSingleInstanceService = singleInstance;
|
||||||
BuildAvaloniaApp(renderMode).StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp(renderMode).StartWithClassicDesktopLifetime(args);
|
||||||
AppLogger.Info("Startup", "Application exited normally.");
|
AppLogger.Info("Startup", "Application exited normally.");
|
||||||
}
|
}
|
||||||
@@ -32,6 +42,10 @@ sealed class Program
|
|||||||
AppLogger.Critical("Startup", "Application terminated during startup.", ex);
|
AppLogger.Critical("Startup", "Application terminated during startup.", ex);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
App.CurrentSingleInstanceService = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
@@ -71,6 +85,16 @@ sealed class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ShowAlreadyRunningNotice(bool notifiedPrimaryInstance)
|
||||||
|
{
|
||||||
|
const string caption = "LanMountainDesktop";
|
||||||
|
var message = notifiedPrimaryInstance
|
||||||
|
? "应用已打开,不需要多开了。\r\n\r\n已为你切换到正在运行的阑山桌面。"
|
||||||
|
: "应用已打开,不需要多开了。\r\n\r\n请切换到正在运行的阑山桌面。";
|
||||||
|
|
||||||
|
WindowsNativeDialogService.ShowInformation(caption, message);
|
||||||
|
}
|
||||||
|
|
||||||
private static void RegisterGlobalExceptionLogging()
|
private static void RegisterGlobalExceptionLogging()
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) =>
|
AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) =>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Avalonia;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
@@ -12,17 +11,9 @@ public static class AppRestartService
|
|||||||
{
|
{
|
||||||
public static bool TryRestartApplication()
|
public static bool TryRestartApplication()
|
||||||
{
|
{
|
||||||
if (!TryRestartCurrentProcess())
|
return App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
||||||
{
|
Source: nameof(AppRestartService),
|
||||||
return false;
|
Reason: "Legacy restart entry point invoked.")) == true;
|
||||||
}
|
|
||||||
|
|
||||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
||||||
{
|
|
||||||
desktop.Shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryRestartCurrentProcess()
|
public static bool TryRestartCurrentProcess()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
|
||||||
|
{
|
||||||
|
public bool TryExit(HostApplicationLifecycleRequest? request = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AppLogger.Info(
|
||||||
|
"HostLifecycle",
|
||||||
|
$"Exit requested. Source='{request?.Source ?? "Unknown"}'; Reason='{request?.Reason ?? string.Empty}'.");
|
||||||
|
|
||||||
|
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("HostLifecycle", "Exit request ignored because desktop lifetime is unavailable.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Dispatcher.UIThread.CheckAccess())
|
||||||
|
{
|
||||||
|
desktop.Shutdown();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => desktop.Shutdown(), DispatcherPriority.Send);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("HostLifecycle", "Failed to exit the application.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRestart(HostApplicationLifecycleRequest? request = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var startInfo = AppRestartService.CreateRestartStartInfo();
|
||||||
|
if (startInfo is null)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(
|
||||||
|
"HostLifecycle",
|
||||||
|
$"Restart request rejected because restart start info could not be resolved. Source='{request?.Source ?? "Unknown"}'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start(startInfo);
|
||||||
|
var exitRequest = request is null
|
||||||
|
? new HostApplicationLifecycleRequest(Reason: "Restart accepted.")
|
||||||
|
: request with
|
||||||
|
{
|
||||||
|
Reason = string.IsNullOrWhiteSpace(request.Reason)
|
||||||
|
? "Restart accepted."
|
||||||
|
: request.Reason
|
||||||
|
};
|
||||||
|
|
||||||
|
return TryExit(exitRequest);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("HostLifecycle", "Failed to restart the application.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,19 @@ public static class AudioRecorderServiceFactory
|
|||||||
{
|
{
|
||||||
return CreateRecorder();
|
return CreateRecorder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void DisposeSharedServices()
|
||||||
|
{
|
||||||
|
if (SharedRecorderService.IsValueCreated)
|
||||||
|
{
|
||||||
|
SharedRecorderService.Value.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SharedStudyMonitoringService.IsValueCreated)
|
||||||
|
{
|
||||||
|
SharedStudyMonitoringService.Value.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class NoOpAudioRecorderService(string reason) : IAudioRecorderService
|
internal sealed class NoOpAudioRecorderService(string reason) : IAudioRecorderService
|
||||||
|
|||||||
151
LanMountainDesktop/Services/SingleInstanceService.cs
Normal file
151
LanMountainDesktop/Services/SingleInstanceService.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public sealed class SingleInstanceService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Mutex _mutex;
|
||||||
|
private readonly string _pipeName;
|
||||||
|
private readonly CancellationTokenSource _listenCts = new();
|
||||||
|
private bool _ownsMutex;
|
||||||
|
private bool _disposed;
|
||||||
|
private Task? _listenTask;
|
||||||
|
|
||||||
|
private SingleInstanceService(string mutexName, string pipeName)
|
||||||
|
{
|
||||||
|
_mutex = new Mutex(initiallyOwned: false, mutexName);
|
||||||
|
_pipeName = pipeName;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ownsMutex = _mutex.WaitOne(TimeSpan.Zero, exitContext: false);
|
||||||
|
}
|
||||||
|
catch (AbandonedMutexException)
|
||||||
|
{
|
||||||
|
_ownsMutex = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPrimaryInstance => _ownsMutex;
|
||||||
|
|
||||||
|
public static SingleInstanceService CreateDefault()
|
||||||
|
{
|
||||||
|
const string appId = "LanMountainDesktop";
|
||||||
|
var userName = Environment.UserName;
|
||||||
|
var scopeSeed = $"{appId}:{userName}";
|
||||||
|
var scopeHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(scopeSeed)));
|
||||||
|
var suffix = scopeHash[..16];
|
||||||
|
var mutexName = OperatingSystem.IsWindows()
|
||||||
|
? $"Local\\{appId}.SingleInstance.{suffix}"
|
||||||
|
: $"{appId}.SingleInstance.{suffix}";
|
||||||
|
return new SingleInstanceService(
|
||||||
|
mutexName,
|
||||||
|
$"{appId}.Activate.{suffix}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartActivationListener(Action onActivationRequested)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(onActivationRequested);
|
||||||
|
|
||||||
|
if (!_ownsMutex || _disposed || _listenTask is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_listenTask = Task.Run(() => ListenForActivationAsync(onActivationRequested, _listenCts.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryNotifyPrimaryInstance(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
if (_ownsMutex || _disposed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new NamedPipeClientStream(
|
||||||
|
serverName: ".",
|
||||||
|
pipeName: _pipeName,
|
||||||
|
direction: PipeDirection.Out,
|
||||||
|
options: PipeOptions.Asynchronous);
|
||||||
|
|
||||||
|
client.Connect((int)Math.Max(1, timeout.TotalMilliseconds));
|
||||||
|
client.WriteByte(1);
|
||||||
|
client.Flush();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("SingleInstance", "Failed to notify the primary instance.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
_listenCts.Cancel();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_listenTask?.Wait(TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore listener shutdown races during process exit.
|
||||||
|
}
|
||||||
|
|
||||||
|
_listenCts.Dispose();
|
||||||
|
if (_ownsMutex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_mutex.ReleaseMutex();
|
||||||
|
}
|
||||||
|
catch (ApplicationException)
|
||||||
|
{
|
||||||
|
// Ownership may already be lost during shutdown.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutex.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ListenForActivationAsync(Action onActivationRequested, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var server = new NamedPipeServerStream(
|
||||||
|
_pipeName,
|
||||||
|
PipeDirection.In,
|
||||||
|
1,
|
||||||
|
PipeTransmissionMode.Byte,
|
||||||
|
PipeOptions.Asynchronous);
|
||||||
|
|
||||||
|
await server.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await server.ReadAsync(new byte[1], cancellationToken).ConfigureAwait(false);
|
||||||
|
onActivationRequested();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("SingleInstance", "Activation listener failed.", ex);
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,14 @@ public static class StudyAnalyticsServiceFactory
|
|||||||
{
|
{
|
||||||
return SharedService.Value;
|
return SharedService.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void DisposeSharedService()
|
||||||
|
{
|
||||||
|
if (SharedService.IsValueCreated)
|
||||||
|
{
|
||||||
|
SharedService.Value.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class StudyAnalyticsService : IStudyAnalyticsService
|
public sealed class StudyAnalyticsService : IStudyAnalyticsService
|
||||||
@@ -446,6 +454,7 @@ public sealed class StudyAnalyticsService : IStudyAnalyticsService
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
StopTimerLocked();
|
StopTimerLocked();
|
||||||
_samplingTimer.Dispose();
|
_samplingTimer.Dispose();
|
||||||
|
_audioRecorderService.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,4 +768,3 @@ public sealed class StudyAnalyticsService : IStudyAnalyticsService
|
|||||||
_lastSessionReport = null;
|
_lastSessionReport = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,20 @@ namespace LanMountainDesktop.Services;
|
|||||||
internal static class WindowsNativeDialogService
|
internal static class WindowsNativeDialogService
|
||||||
{
|
{
|
||||||
private const uint Ok = 0x00000000;
|
private const uint Ok = 0x00000000;
|
||||||
|
private const uint IconInformation = 0x00000040;
|
||||||
private const uint IconWarning = 0x00000030;
|
private const uint IconWarning = 0x00000030;
|
||||||
|
|
||||||
|
public static void ShowInformation(string caption, string message)
|
||||||
|
{
|
||||||
|
Show(caption, message, Ok | IconInformation, "NativeDialog");
|
||||||
|
}
|
||||||
|
|
||||||
public static void ShowWarning(string caption, string message)
|
public static void ShowWarning(string caption, string message)
|
||||||
|
{
|
||||||
|
Show(caption, message, Ok | IconWarning, "StartupDiagnostics");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Show(string caption, string message, uint type, string logCategory)
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsWindows())
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
@@ -17,11 +28,11 @@ internal static class WindowsNativeDialogService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ = MessageBoxW(IntPtr.Zero, message, caption, Ok | IconWarning);
|
_ = MessageBoxW(IntPtr.Zero, message, caption, type);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
AppLogger.Warn("StartupDiagnostics", "Failed to show legacy executable warning dialog.", ex);
|
AppLogger.Warn(logCategory, "Failed to show native dialog.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Threading.Tasks;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
@@ -77,7 +78,9 @@ public partial class MainWindow
|
|||||||
var result = await dialog.ShowAsync(this);
|
var result = await dialog.ShowAsync(this);
|
||||||
if (result == ContentDialogResult.Primary)
|
if (result == ContentDialogResult.Primary)
|
||||||
{
|
{
|
||||||
if (!AppRestartService.TryRestartApplication())
|
if (App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
||||||
|
Source: nameof(MainWindow),
|
||||||
|
Reason: "User confirmed a pending restart prompt.")) != true)
|
||||||
{
|
{
|
||||||
UpdatePendingRestartDock();
|
UpdatePendingRestartDock();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
@@ -357,7 +358,8 @@ public partial class MainWindow
|
|||||||
{
|
{
|
||||||
FileName = installerPath,
|
FileName = installerPath,
|
||||||
WorkingDirectory = Path.GetDirectoryName(installerPath) ?? Environment.CurrentDirectory,
|
WorkingDirectory = Path.GetDirectoryName(installerPath) ?? Environment.CurrentDirectory,
|
||||||
UseShellExecute = true
|
UseShellExecute = true,
|
||||||
|
Verb = "runas"
|
||||||
});
|
});
|
||||||
|
|
||||||
_updateStatusText = L(
|
_updateStatusText = L(
|
||||||
@@ -365,7 +367,16 @@ public partial class MainWindow
|
|||||||
"Installer started. The app will close for update.");
|
"Installer started. The app will close for update.");
|
||||||
UpdateUpdatePanelState();
|
UpdateUpdatePanelState();
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(Close, DispatcherPriority.Background);
|
_ = App.CurrentHostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
|
||||||
|
Source: nameof(MainWindow),
|
||||||
|
Reason: "Update installer started successfully."));
|
||||||
|
}
|
||||||
|
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
||||||
|
{
|
||||||
|
_updateStatusText = L(
|
||||||
|
"settings.update.status_elevation_cancelled",
|
||||||
|
"Administrator permission was not granted. Update was cancelled.");
|
||||||
|
UpdateUpdatePanelState();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Threading.Tasks;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
@@ -77,7 +78,9 @@ public partial class SettingsWindow
|
|||||||
var result = await dialog.ShowAsync(this);
|
var result = await dialog.ShowAsync(this);
|
||||||
if (result == ContentDialogResult.Primary)
|
if (result == ContentDialogResult.Primary)
|
||||||
{
|
{
|
||||||
if (!AppRestartService.TryRestartApplication())
|
if (App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
||||||
|
Source: nameof(SettingsWindow),
|
||||||
|
Reason: "User confirmed a pending restart prompt from settings.")) != true)
|
||||||
{
|
{
|
||||||
UpdatePendingRestartDock();
|
UpdatePendingRestartDock();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
@@ -250,23 +249,23 @@ public partial class SettingsWindow
|
|||||||
{
|
{
|
||||||
FileName = installerPath,
|
FileName = installerPath,
|
||||||
WorkingDirectory = Path.GetDirectoryName(installerPath) ?? Environment.CurrentDirectory,
|
WorkingDirectory = Path.GetDirectoryName(installerPath) ?? Environment.CurrentDirectory,
|
||||||
UseShellExecute = true
|
UseShellExecute = true,
|
||||||
|
Verb = "runas"
|
||||||
});
|
});
|
||||||
|
|
||||||
_updateStatusText = L("settings.update.status_installer_started", "Installer started. The app will close for update.");
|
_updateStatusText = L("settings.update.status_installer_started", "Installer started. The app will close for update.");
|
||||||
UpdateUpdatePanelState();
|
UpdateUpdatePanelState();
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
_ = App.CurrentHostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
|
||||||
{
|
Source: nameof(SettingsWindow),
|
||||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
Reason: "Update installer started successfully from settings."));
|
||||||
{
|
}
|
||||||
desktop.Shutdown();
|
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
||||||
}
|
{
|
||||||
else
|
_updateStatusText = L(
|
||||||
{
|
"settings.update.status_elevation_cancelled",
|
||||||
Close();
|
"Administrator permission was not granted. Update was cancelled.");
|
||||||
}
|
UpdateUpdatePanelState();
|
||||||
}, DispatcherPriority.Background);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ OutputBaseFilename={#MyAppName}-Setup-{#MyAppVersion}-{#MyAppArch}
|
|||||||
Compression=lzma2/ultra64
|
Compression=lzma2/ultra64
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
|
; Leave PrivilegesRequiredOverridesAllowed unset so users cannot downgrade
|
||||||
|
; installation mode via dialog or /ALLUSERS /CURRENTUSER command-line switches.
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=admin
|
||||||
CloseApplications=yes
|
CloseApplications=yes
|
||||||
CloseApplicationsFilter={#MyAppExeName}
|
CloseApplicationsFilter={#MyAppExeName}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
|
|
||||||
private readonly PluginLoader _loader;
|
private readonly PluginLoader _loader;
|
||||||
private readonly AppSettingsService _appSettingsService = new();
|
private readonly AppSettingsService _appSettingsService = new();
|
||||||
|
private readonly IHostApplicationLifecycle _applicationLifecycle = new HostApplicationLifecycleService();
|
||||||
private readonly IServiceProvider _hostServices;
|
private readonly IServiceProvider _hostServices;
|
||||||
private readonly IPluginPackageManager _packageManager;
|
private readonly IPluginPackageManager _packageManager;
|
||||||
private readonly List<LoadedPlugin> _loadedPlugins = [];
|
private readonly List<LoadedPlugin> _loadedPlugins = [];
|
||||||
@@ -34,7 +35,7 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
{
|
{
|
||||||
PluginsDirectory = Path.Combine(AppContext.BaseDirectory, "Extensions", "Plugins");
|
PluginsDirectory = Path.Combine(AppContext.BaseDirectory, "Extensions", "Plugins");
|
||||||
_packageManager = new PluginRuntimePackageManager(this);
|
_packageManager = new PluginRuntimePackageManager(this);
|
||||||
_hostServices = new PluginHostServiceProvider(_packageManager);
|
_hostServices = new PluginHostServiceProvider(_packageManager, _applicationLifecycle);
|
||||||
_loader = new PluginLoader(CreateOptions());
|
_loader = new PluginLoader(CreateOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,17 +680,29 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
private sealed class PluginHostServiceProvider : IServiceProvider
|
private sealed class PluginHostServiceProvider : IServiceProvider
|
||||||
{
|
{
|
||||||
private readonly IPluginPackageManager _packageManager;
|
private readonly IPluginPackageManager _packageManager;
|
||||||
|
private readonly IHostApplicationLifecycle _applicationLifecycle;
|
||||||
|
|
||||||
public PluginHostServiceProvider(IPluginPackageManager packageManager)
|
public PluginHostServiceProvider(
|
||||||
|
IPluginPackageManager packageManager,
|
||||||
|
IHostApplicationLifecycle applicationLifecycle)
|
||||||
{
|
{
|
||||||
_packageManager = packageManager;
|
_packageManager = packageManager;
|
||||||
|
_applicationLifecycle = applicationLifecycle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object? GetService(Type serviceType)
|
public object? GetService(Type serviceType)
|
||||||
{
|
{
|
||||||
return serviceType == typeof(IPluginPackageManager)
|
if (serviceType == typeof(IPluginPackageManager))
|
||||||
? _packageManager
|
{
|
||||||
: null;
|
return _packageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceType == typeof(IHostApplicationLifecycle))
|
||||||
|
{
|
||||||
|
return _applicationLifecycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user