mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-21 08:04:26 +08:00
* ava12升级 * Enable centralized package versioning Add <Project> and <PropertyGroup> with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> to Directory.Packages.props to enable centralized package version management across the repository. This allows package versions to be controlled from this single file instead of individual project files. * Migrate codebase to Avalonia 12 APIs Apply Avalonia 12 migration changes: replace SystemDecorations with WindowDecorations and remove ExtendClientAreaChromeHints/ExtendClientAreaTitleBarHeightHint usages; update BindingPlugins removal logic (no-op); switch clipboard usage to ClipboardExtensions.SetTextAsync; update Bitmap.CopyPixels calls to the new signature. Replace TextBox.Watermark with PlaceholderText, convert NumberBox styles to FANumberBox and adjust templates, change Checked/Unchecked handlers to IsCheckedChanged, and adapt FluentIcons usages (SymbolIconSource -> FASymbol/FAFont/FluentIcon equivalents). Fix MainWindow partial classes to inherit Window and correct missing variables/fields/usings. Add migration docs/specs/tasks under .trae and include a small TestFluentIcons project for icon testing. * Migrate to Avalonia 12 and Plugin SDK v5 Upgrade project to the Avalonia 12 baseline and Plugin SDK v5: centralize Avalonia packages, remove legacy WebView.Avalonia usage (use NativeWebView/WebView2 EnvironmentRequested), and update Fluent/Material icon/package usages. Bump multiple package/project versions to 5.0.0 and Avalonia 12.0.1, update plugin template and README/docs to SDK v5, and add PLUGIN_SDK_V5_MIGRATION.md. Also fix runtime/behavior bugs: make DataLocationResolver use a fixed bootstrap launcher data path and avoid recursive ResolveDataRoot; add legacy-state handling and extraction in OobeStateService; and update component settings tests to reflect migrated storage (DB/backup) and reset cache for test reloads. Various csproj, tests, and docs updated to reflect the migration and ensure build/test compatibility. * Update icon glyphs and symbol mappings Replace and refine icon sources across settings pages and controls: many FAFontIconSource glyphs were updated to specific Seagull Fluent Icons codepoints, some FASymbolIconSource usages were replaced with FAFontIconSource, and a number of symbol-to-Symbol enum mappings were adjusted (e.g. "Bell" -> AlertOn, "Shield" -> ShieldLock). Also clarified a comment in SettingsWindow and fixed a trailing newline in StudySettingsPage. Changes standardize icon visuals and bridge FluentIcons glyphs into FluentAvalonia icon sources. * fix.修复合并产生的问题。
341 lines
12 KiB
C#
341 lines
12 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Avalonia;
|
|
using LanMountainDesktop.DesktopHost;
|
|
using LanMountainDesktop.Models;
|
|
using LanMountainDesktop.Plugins;
|
|
using LanMountainDesktop.Services;
|
|
using LanMountainDesktop.Services.Launcher;
|
|
using LanMountainDesktop.Services.Settings;
|
|
using LanMountainDesktop.Shared.Contracts.Launcher;
|
|
|
|
namespace LanMountainDesktop;
|
|
|
|
public sealed class Program
|
|
{
|
|
internal static string StartupRenderMode { get; private set; } = AppRenderingModeHelper.Default;
|
|
|
|
[STAThread]
|
|
public static void Main(string[] args)
|
|
{
|
|
AppLogger.Initialize();
|
|
AppDataPathProvider.Initialize(args);
|
|
DevPluginOptions.Parse(args);
|
|
RegisterGlobalExceptionLogging();
|
|
var restartParentProcessId = LauncherRuntimeMetadata.GetRestartParentProcessId(args);
|
|
|
|
using var singleInstance = AcquireSingleInstance(restartParentProcessId);
|
|
if (!singleInstance.IsPrimaryInstance)
|
|
{
|
|
if (restartParentProcessId is not null)
|
|
{
|
|
AppLogger.Warn(
|
|
"Startup",
|
|
$"Restart relaunch could not acquire the single-instance lock. pid={restartParentProcessId.Value}. Suppressing multi-open activation prompt.");
|
|
ReportLauncherStageBeforeExit(StartupStage.ActivationFailed, "Restart relaunch could not acquire the single-instance lock.");
|
|
Environment.ExitCode = HostExitCodes.RestartLockNotAcquired;
|
|
return;
|
|
}
|
|
|
|
var activationAcknowledged = singleInstance.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2), out var failureReason);
|
|
if (activationAcknowledged)
|
|
{
|
|
AppLogger.Info(
|
|
"Startup",
|
|
$"Secondary launch forwarded to primary instance successfully. Acked={activationAcknowledged}; Pid={Environment.ProcessId}.");
|
|
ReportLauncherStageBeforeExit(StartupStage.ActivationRedirected, "Secondary launch forwarded to the primary instance.");
|
|
Environment.ExitCode = HostExitCodes.SecondaryActivationSucceeded;
|
|
}
|
|
else
|
|
{
|
|
AppLogger.Warn(
|
|
"Startup",
|
|
$"Secondary launch failed to activate the primary instance. Acked={activationAcknowledged}; Reason='{failureReason ?? "unknown"}'; Pid={Environment.ProcessId}.");
|
|
ReportLauncherStageBeforeExit(
|
|
StartupStage.ActivationFailed,
|
|
$"Secondary launch failed to activate the primary instance. Reason='{failureReason ?? "unknown"}'.");
|
|
Environment.ExitCode = HostExitCodes.SecondaryActivationFailed;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
DesktopBootstrap.InitializeStartupServices(
|
|
InitializeTelemetryIdentity,
|
|
InitializeCrashTelemetry,
|
|
InitializeUsageTelemetry,
|
|
ScheduleWhiteboardNoteStartupCleanup);
|
|
|
|
var diagnostics = StartupDiagnosticsService.Run(args);
|
|
StartupDiagnosticsService.ShowLegacyExecutableWarningIfNeeded(diagnostics);
|
|
|
|
try
|
|
{
|
|
var renderMode = LoadConfiguredRenderMode();
|
|
StartupRenderMode = renderMode;
|
|
AppLogger.Info("Startup", $"Resolved render mode '{renderMode}'.");
|
|
App.CurrentSingleInstanceService = singleInstance;
|
|
singleInstance.StartActivationListener(() =>
|
|
{
|
|
if (Avalonia.Application.Current is App app)
|
|
{
|
|
app.ActivateMainWindow();
|
|
return;
|
|
}
|
|
|
|
AppLogger.Info("SingleInstance", "Activation acknowledged before Avalonia App was ready.");
|
|
});
|
|
BuildAvaloniaApp(renderMode).StartWithClassicDesktopLifetime(args);
|
|
AppLogger.Info("Startup", "Application exited normally.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Critical("Startup", "Application terminated during startup.", ex);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
App.CurrentSingleInstanceService = null;
|
|
}
|
|
}
|
|
|
|
public static AppBuilder BuildAvaloniaApp()
|
|
{
|
|
return BuildAvaloniaApp(AppRenderingModeHelper.Default);
|
|
}
|
|
|
|
public static AppBuilder BuildAvaloniaApp(string renderMode)
|
|
{
|
|
var builder = AppBuilder.Configure<App>()
|
|
.UsePlatformDetect()
|
|
.WithInterFont()
|
|
.LogToTrace();
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
var configuredModes = AppRenderingModeHelper.GetWin32RenderingModes(renderMode);
|
|
if (configuredModes is { Length: > 0 })
|
|
{
|
|
builder = builder.With(new Win32PlatformOptions
|
|
{
|
|
RenderingMode = configuredModes
|
|
});
|
|
}
|
|
}
|
|
|
|
return builder;
|
|
}
|
|
|
|
private static void ScheduleWhiteboardNoteStartupCleanup()
|
|
{
|
|
_ = Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
var deletedCount = new WhiteboardNotePersistenceService().DeleteExpiredNotesBatch(batchSize: 512);
|
|
if (deletedCount > 0)
|
|
{
|
|
AppLogger.Info("Startup", $"Deleted {deletedCount} expired whiteboard notes during startup maintenance.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warn("Startup", "Failed to run whiteboard note startup maintenance.", ex);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static SingleInstanceService AcquireSingleInstance(int? restartParentProcessId)
|
|
{
|
|
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
|
|
{
|
|
var snapshot = HostSettingsFacadeProvider.GetOrCreate()
|
|
.Settings
|
|
.LoadSnapshot<AppSettingsSnapshot>(LanMountainDesktop.PluginSdk.SettingsScope.App);
|
|
return AppRenderingModeHelper.Normalize(snapshot.AppRenderMode);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warn("Startup", "Failed to load configured render mode. Falling back to default.", ex);
|
|
return AppRenderingModeHelper.Default;
|
|
}
|
|
}
|
|
|
|
private static void WaitForRestartParentExit(int processId, DateTime deadlineUtc)
|
|
{
|
|
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)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warn("Startup", $"Failed while waiting for restart parent pid={processId} to exit.", ex);
|
|
}
|
|
}
|
|
|
|
private static void RegisterGlobalExceptionLogging()
|
|
{
|
|
AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) =>
|
|
{
|
|
var exception = eventArgs.ExceptionObject as Exception
|
|
?? new Exception(eventArgs.ExceptionObject?.ToString() ?? "Unhandled exception.");
|
|
|
|
AppLogger.Critical(
|
|
"UnhandledException",
|
|
$"Unhandled exception. IsTerminating={eventArgs.IsTerminating}",
|
|
exception);
|
|
|
|
try
|
|
{
|
|
TelemetryServices.Crash?.CaptureUnhandledException(
|
|
exception,
|
|
"AppDomain.UnhandledException",
|
|
eventArgs.IsTerminating);
|
|
}
|
|
catch (Exception telemetryException)
|
|
{
|
|
AppLogger.Warn("UnhandledException", "Failed to forward unhandled exception to crash telemetry.", telemetryException);
|
|
}
|
|
};
|
|
|
|
TaskScheduler.UnobservedTaskException += (_, eventArgs) =>
|
|
{
|
|
AppLogger.Error("TaskScheduler", "Unobserved task exception.", eventArgs.Exception);
|
|
|
|
try
|
|
{
|
|
TelemetryServices.Crash?.CaptureTaskException(
|
|
eventArgs.Exception,
|
|
"TaskScheduler.UnobservedTaskException");
|
|
}
|
|
catch (Exception telemetryException)
|
|
{
|
|
AppLogger.Warn("TaskScheduler", "Failed to forward task exception to crash telemetry.", telemetryException);
|
|
}
|
|
|
|
eventArgs.SetObserved();
|
|
};
|
|
}
|
|
|
|
private static void ReportLauncherStageBeforeExit(StartupStage stage, string message)
|
|
{
|
|
if (!LauncherIpcClient.IsLaunchedByLauncher())
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var launcherIpcClient = new LauncherIpcClient();
|
|
var connected = launcherIpcClient.ConnectAsync().GetAwaiter().GetResult();
|
|
if (!connected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
launcherIpcClient.ReportProgressAsync(new StartupProgressMessage
|
|
{
|
|
Stage = stage,
|
|
ProgressPercent = 100,
|
|
Message = message
|
|
}).GetAwaiter().GetResult();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warn("LauncherIpc", $"Failed to report early launcher stage '{stage}'.", ex);
|
|
}
|
|
}
|
|
|
|
private static void InitializeTelemetryIdentity()
|
|
{
|
|
try
|
|
{
|
|
TelemetryIdentityService.Initialize(HostSettingsFacadeProvider.GetOrCreate());
|
|
AppLogger.Info(
|
|
"Startup",
|
|
$"Telemetry identity initialized. InstallId={TelemetryIdentityService.Instance.InstallId}; TelemetryId={TelemetryIdentityService.Instance.TelemetryId}.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warn("Startup", "Failed to initialize telemetry identity service.", ex);
|
|
}
|
|
}
|
|
|
|
private static void InitializeCrashTelemetry()
|
|
{
|
|
try
|
|
{
|
|
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
|
var crashTelemetry = new SentryCrashTelemetryService(settingsFacade);
|
|
TelemetryServices.Crash = crashTelemetry;
|
|
crashTelemetry.Initialize();
|
|
AppLogger.Info("Startup", $"Crash telemetry initialized. Enabled={crashTelemetry.IsEnabled}.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warn("Startup", "Failed to initialize crash telemetry service.", ex);
|
|
}
|
|
}
|
|
|
|
private static void InitializeUsageTelemetry()
|
|
{
|
|
try
|
|
{
|
|
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
|
var usageTelemetry = new PostHogUsageTelemetryService(settingsFacade);
|
|
TelemetryServices.Usage = usageTelemetry;
|
|
usageTelemetry.Initialize();
|
|
AppLogger.Info("Startup", $"Usage telemetry initialized. Enabled={usageTelemetry.IsUsageEnabled}.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warn("Startup", "Failed to initialize usage telemetry service.", ex);
|
|
}
|
|
}
|
|
}
|