二次启动拦截,统一了生命进程API
This commit is contained in:
lincube
2026-03-11 09:40:36 +08:00
parent 2781d7e0d9
commit e7a03404ce
21 changed files with 652 additions and 62 deletions

View File

@@ -3,8 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
@@ -12,17 +11,9 @@ public static class AppRestartService
{
public static bool TryRestartApplication()
{
if (!TryRestartCurrentProcess())
{
return false;
}
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.Shutdown();
}
return true;
return App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
Source: nameof(AppRestartService),
Reason: "Legacy restart entry point invoked.")) == true;
}
public static bool TryRestartCurrentProcess()

View File

@@ -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;
}
}
}

View File

@@ -80,6 +80,19 @@ public static class AudioRecorderServiceFactory
{
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

View 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);
}
}
}
}

View File

@@ -16,6 +16,14 @@ public static class StudyAnalyticsServiceFactory
{
return SharedService.Value;
}
public static void DisposeSharedService()
{
if (SharedService.IsValueCreated)
{
SharedService.Value.Dispose();
}
}
}
public sealed class StudyAnalyticsService : IStudyAnalyticsService
@@ -446,6 +454,7 @@ public sealed class StudyAnalyticsService : IStudyAnalyticsService
_disposed = true;
StopTimerLocked();
_samplingTimer.Dispose();
_audioRecorderService.Dispose();
}
}
@@ -759,4 +768,3 @@ public sealed class StudyAnalyticsService : IStudyAnalyticsService
_lastSessionReport = null;
}
}

View File

@@ -6,9 +6,20 @@ namespace LanMountainDesktop.Services;
internal static class WindowsNativeDialogService
{
private const uint Ok = 0x00000000;
private const uint IconInformation = 0x00000040;
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)
{
Show(caption, message, Ok | IconWarning, "StartupDiagnostics");
}
private static void Show(string caption, string message, uint type, string logCategory)
{
if (!OperatingSystem.IsWindows())
{
@@ -17,11 +28,11 @@ internal static class WindowsNativeDialogService
try
{
_ = MessageBoxW(IntPtr.Zero, message, caption, Ok | IconWarning);
_ = MessageBoxW(IntPtr.Zero, message, caption, type);
}
catch (Exception ex)
{
AppLogger.Warn("StartupDiagnostics", "Failed to show legacy executable warning dialog.", ex);
AppLogger.Warn(logCategory, "Failed to show native dialog.", ex);
}
}