Stamp release versions and harden launcher

Add automatic release version stamping and multiple launcher reliability improvements. The Release workflow now runs scripts/Set-ReleaseVersion.ps1 in build jobs to inject tag-derived Version/AssemblyVersion into project metadata; several .csproj/Directory.Build.props and app.manifest files were changed to use a dev placeholder. Introduced AppVersionProvider (and related runtime metadata) to centralize version resolution and updated DeploymentLocator to use it and to prefer package-root/version.json. Launcher startup flow was hardened: added startup success tracking, public-activation recovery path, improved success/fallback semantics, and related IPC handling. UI/UX fixes include OOBE entrance/exit animation improvements (scaling-aware, concurrent fade+translate) and minor window lifecycle reorder in DesktopShellHost. CommandContext now recognizes restart and key=value args. New DesktopTrayService and .trae spec files (spec, checklist, tasks) document shell/tray hardening work. Miscellaneous logging, comments and housekeeping edits across launcher and shared contracts to support the above.
This commit is contained in:
lincube
2026-04-23 00:27:01 +08:00
parent e20462ac2b
commit 001d77968f
31 changed files with 1727 additions and 478 deletions

View File

@@ -59,6 +59,9 @@ public partial class App : Application
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate();
private readonly DateTimeOffset _startupAt = DateTimeOffset.UtcNow;
private readonly string _launchSource = LauncherRuntimeMetadata.GetLaunchSource(Environment.GetCommandLineArgs()) ?? "normal";
private readonly RestartPresentationMode? _requestedRestartPresentationMode =
LauncherRuntimeMetadata.GetRestartPresentationMode(Environment.GetCommandLineArgs());
private ISettingsPageRegistry? _settingsPageRegistry;
private ISettingsWindowService? _settingsWindowService;
private WeatherLocationRefreshService? _weatherLocationRefreshService;
@@ -67,12 +70,7 @@ public partial class App : Application
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
private ShutdownIntent _shutdownIntent;
private TrayIcon? _trayIcon;
private NativeMenuItem? _trayShowDesktopMenuItem;
private NativeMenuItem? _traySettingsMenuItem;
private NativeMenuItem? _trayComponentLibraryMenuItem;
private NativeMenuItem? _trayRestartMenuItem;
private NativeMenuItem? _trayExitMenuItem;
private DesktopTrayService? _desktopTrayService;
private PluginRuntimeService? _pluginRuntimeService;
private MainWindow? _mainWindow;
private TransparentOverlayWindow? _transparentOverlayWindow;
@@ -108,6 +106,15 @@ public partial class App : Application
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
internal ISettingsWindowService? SettingsWindowService => _settingsWindowService;
internal INotificationService? NotificationService => _notificationService;
internal RestartPresentationMode GetCurrentRestartPresentationMode()
{
return _desktopShellState switch
{
DesktopShellState.TrayOnly => RestartPresentationMode.Tray,
DesktopShellState.MinimizedToTaskbar => RestartPresentationMode.Minimized,
_ => RestartPresentationMode.Foreground
};
}
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
{
@@ -470,128 +477,90 @@ public partial class App : Application
private void InitializeTrayIcon()
{
try
EnsureDesktopTrayService();
_trayInitialized = _desktopTrayService?.EnsureReady("Startup") == true;
if (_trayInitialized)
{
if (_trayIcon is null)
{
_trayShowDesktopMenuItem = new NativeMenuItem();
_trayShowDesktopMenuItem.Click += OnTrayShowDesktopClick;
_traySettingsMenuItem = new NativeMenuItem();
_traySettingsMenuItem.Click += OnTraySettingsClick;
_trayComponentLibraryMenuItem = new NativeMenuItem();
_trayComponentLibraryMenuItem.Click += OnTrayComponentLibraryClick;
_trayRestartMenuItem = new NativeMenuItem();
_trayRestartMenuItem.Click += OnTrayRestartClick;
_trayExitMenuItem = new NativeMenuItem();
_trayExitMenuItem.Click += OnTrayExitClick;
var trayMenu = new NativeMenu();
trayMenu.Items.Add(_trayShowDesktopMenuItem);
trayMenu.Items.Add(_traySettingsMenuItem);
trayMenu.Items.Add(_trayComponentLibraryMenuItem);
trayMenu.Items.Add(new NativeMenuItemSeparator());
trayMenu.Items.Add(_trayRestartMenuItem);
trayMenu.Items.Add(new NativeMenuItemSeparator());
trayMenu.Items.Add(_trayExitMenuItem);
_trayIcon = new TrayIcon
{
Icon = _appLogoService.CreateTrayIcon(),
Menu = trayMenu,
IsVisible = true
};
TrayIcon.SetIcons(this, [_trayIcon]);
}
RefreshTrayIconContent();
_trayInitialized = true;
ReportStartupProgress(StartupStage.TrayReady, 75, "Tray ready.");
AppLogger.Info("TrayIcon", $"Tray initialized successfully. Pid={Environment.ProcessId}.");
return;
}
catch (Exception ex)
{
_trayInitialized = false;
AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex);
}
AppLogger.Warn("TrayIcon", "Tray initialization did not reach the ready state.");
}
private void RefreshTrayIconContent()
{
if (_trayIcon is not null)
{
_trayIcon.IsVisible = true;
if (!OperatingSystem.IsLinux())
{
_trayIcon.ToolTipText = L("tray.tooltip", "LanMountainDesktop");
}
}
if (_trayShowDesktopMenuItem is not null)
{
_trayShowDesktopMenuItem.Header = L("tray.menu.show_desktop", "Open Desktop");
}
if (_traySettingsMenuItem is not null)
{
_traySettingsMenuItem.Header = L("tray.menu.settings", "Settings");
}
RefreshFusedDesktopMenuItemVisibility();
if (_trayRestartMenuItem is not null)
{
_trayRestartMenuItem.Header = L("tray.menu.restart", "Restart App");
}
if (_trayExitMenuItem is not null)
{
_trayExitMenuItem.Header = L("tray.menu.exit", "Exit App");
}
EnsureDesktopTrayService();
_desktopTrayService?.Refresh("RefreshTrayContent");
_trayInitialized = _desktopTrayService?.IsReady == true;
}
private void RefreshFusedDesktopMenuItemVisibility()
{
if (_trayComponentLibraryMenuItem is null)
{
return;
}
if (!OperatingSystem.IsWindows())
{
_trayComponentLibraryMenuItem.IsVisible = false;
return;
}
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
_trayComponentLibraryMenuItem.IsVisible = appSnapshot.EnableFusedDesktop;
if (_trayComponentLibraryMenuItem.IsVisible)
{
_trayComponentLibraryMenuItem.Header = L("tray.menu.component_library", "Component Library");
}
RefreshTrayIconContent();
}
private void DisposeTrayIcon()
{
if (_trayIcon is null)
_desktopTrayService?.Dispose();
_trayInitialized = false;
}
private void EnsureDesktopTrayService()
{
if (_desktopTrayService is not null)
{
return;
}
try
_desktopTrayService = new DesktopTrayService(
this,
_appLogoService,
L,
ShouldShowTrayComponentLibraryMenuItem,
OnTrayShowDesktopClick,
OnTraySettingsClick,
OnTrayComponentLibraryClick,
OnTrayRestartClick,
OnTrayExitClick);
_desktopTrayService.StateChanged += OnTrayAvailabilityStateChanged;
}
private bool EnsureTrayReady(string reason)
{
EnsureDesktopTrayService();
var ready = _desktopTrayService?.EnsureReady(reason) == true;
_trayInitialized = ready;
if (ready)
{
_trayIcon.IsVisible = false;
ReportStartupProgress(StartupStage.TrayReady, 75, "Tray ready.");
}
catch (Exception ex)
return ready;
}
private void OnTrayAvailabilityStateChanged(TrayAvailabilityState state)
{
_trayInitialized = state == TrayAvailabilityState.Ready;
if (state == TrayAvailabilityState.Failed && _desktopShellState == DesktopShellState.TrayOnly)
{
AppLogger.Warn("TrayIcon", "Failed to hide tray icon during cleanup.", ex);
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayAvailabilityFailed");
}
}
private bool ShouldShowTrayComponentLibraryMenuItem()
{
if (!OperatingSystem.IsWindows())
{
return false;
}
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
return appSnapshot.EnableFusedDesktop;
}
private void EnsureSettingsWindowService()
{
_settingsPageRegistry ??= new SettingsPageRegistry(
@@ -764,6 +733,7 @@ public partial class App : Application
mainWindow.PlayEnterAnimation();
}, DispatcherPriority.Background);
_desktopTrayService?.StopWatchdog();
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
AppLogger.Info(
"DesktopShell",
@@ -872,6 +842,7 @@ public partial class App : Application
if (themeChanged)
{
ApplyThemeFromSettings();
RefreshTrayIconContent();
}
if (languageChanged)
@@ -898,7 +869,11 @@ public partial class App : Application
_ = sender;
_ = e;
Dispatcher.UIThread.Post(ApplyThemeFromSettings, DispatcherPriority.Background);
Dispatcher.UIThread.Post(() =>
{
ApplyThemeFromSettings();
RefreshTrayIconContent();
}, DispatcherPriority.Background);
}
private void ApplyAdaptiveThemeResources()
@@ -1144,18 +1119,56 @@ public partial class App : Application
{
mainWindow.Opened -= OnMainWindowOpened;
_mainWindowOpened = true;
_loadingStateManager?.CompleteItem("system.init", "System initialization completed.");
if (TryApplyStartupPresentation(mainWindow))
{
AppLogger.Info(
"App",
$"Main window opened and startup presentation was applied. LaunchSource='{_launchSource}'; RestartPresentation='{_requestedRestartPresentationMode?.ToString() ?? "<none>"}'; ShellState='{_desktopShellState}'.");
ReportStartupProgressSync(StartupStage.Ready, 100, "Ready.");
_loadingStateReporter?.Stop();
return;
}
AppLogger.Info(
"App",
$"Main window opened. Reporting DesktopVisible. TrayInitialized={_trayInitialized}; ShellState='{_desktopShellState}'.");
_loadingStateManager?.CompleteItem("system.init", "System initialization completed.");
ReportStartupProgressSync(StartupStage.DesktopVisible, 100, "Desktop visible.");
ReportStartupProgressSync(StartupStage.Ready, 100, "Ready.");
_loadingStateReporter?.Stop();
}
}
private bool TryApplyStartupPresentation(MainWindow mainWindow)
{
if (!string.Equals(_launchSource, "restart", StringComparison.OrdinalIgnoreCase) ||
_requestedRestartPresentationMode is null ||
_requestedRestartPresentationMode == RestartPresentationMode.Foreground)
{
return false;
}
switch (_requestedRestartPresentationMode)
{
case RestartPresentationMode.Minimized:
mainWindow.ShowInTaskbar = true;
mainWindow.WindowState = WindowState.Minimized;
_desktopTrayService?.StopWatchdog();
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, "StartupRestartPresentation");
ReportStartupProgressSync(StartupStage.BackgroundReady, 95, "Background ready.");
return true;
case RestartPresentationMode.Tray:
HideMainWindowToTray(mainWindow, "StartupRestartPresentation");
return true;
default:
return false;
}
}
private MainWindow GetOrCreateMainWindow(
IClassicDesktopStyleApplicationLifetime desktop,
string reason)
@@ -1242,7 +1255,15 @@ public partial class App : Application
if (_shutdownIntent == ShutdownIntent.None)
{
SetDesktopShellState(DesktopShellState.TrayOnly, "MainWindowClosedUnexpected");
if (EnsureTrayReady("MainWindowClosedUnexpected"))
{
_desktopTrayService?.StartWatchdog();
SetDesktopShellState(DesktopShellState.TrayOnly, "MainWindowClosedUnexpected");
}
else
{
SetDesktopShellState(DesktopShellState.ForegroundDesktop, "MainWindowClosedUnexpectedWithoutTray");
}
}
}
@@ -1276,9 +1297,17 @@ public partial class App : Application
{
try
{
if (!EnsureTrayReady($"HideToTray:{source}"))
{
RecoverFromTrayUnavailable(mainWindow, source);
return;
}
mainWindow.ShowInTaskbar = false;
mainWindow.Hide();
_desktopTrayService?.StartWatchdog();
SetDesktopShellState(DesktopShellState.TrayOnly, source);
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready.");
AppLogger.Info(
"DesktopShell",
$"Main window hidden to tray. Source='{source}'; WindowState='{mainWindow.WindowState}'.");
@@ -1293,9 +1322,56 @@ public partial class App : Application
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Failed to hide main window to tray. Source='{source}'.", ex);
RecoverFromTrayUnavailable(mainWindow, source);
}
}
private void RecoverFromTrayUnavailable(MainWindow mainWindow, string source)
{
AppLogger.Warn(
"DesktopShell",
$"Tray was unavailable. Recovering to a visible or taskbar-backed state instead of TrayOnly. Source='{source}'.");
var showInTaskbar = ShouldShowMainWindowInTaskbar();
if (showInTaskbar)
{
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
mainWindow.WindowState = WindowState.Minimized;
_desktopTrayService?.StopWatchdog();
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, $"TrayFallbackTaskbar:{source}");
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready via taskbar fallback.");
return;
}
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
if (mainWindow.WindowState == WindowState.Minimized)
{
mainWindow.WindowState = WindowState.Normal;
}
if (mainWindow.WindowState != WindowState.FullScreen)
{
mainWindow.WindowState = WindowState.FullScreen;
}
mainWindow.Activate();
mainWindow.Topmost = true;
mainWindow.Topmost = false;
_desktopTrayService?.StopWatchdog();
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"TrayFallbackForeground:{source}");
ReportStartupProgress(StartupStage.DesktopVisible, 100, "Desktop restored because tray was unavailable.");
}
private bool ShouldShowMainWindowInTaskbar()
{
return _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ShowInTaskbar;
@@ -1364,17 +1440,19 @@ public partial class App : Application
try
{
var version = typeof(App).Assembly.GetName().Version?.ToString() ?? "1.0.0";
var versionInfo = AppVersionProvider.ResolveForCurrentProcess();
_publicIpcHostService = new PublicIpcHostService();
_publicIpcHostService.PluginDescriptorProvider = BuildPublicPluginDescriptors;
_publicIpcHostService.RegisterPublicService<IPublicAppInfoService>(
new PublicAppInfoService(version, "Administrate", _startupAt));
new PublicAppInfoService(_startupAt));
_publicIpcHostService.RegisterPublicService<IPublicShellControlService>(
new PublicShellControlService());
_publicIpcHostService.RegisterPublicService<IPublicPluginCatalogService>(
new PublicPluginCatalogService(_publicIpcHostService));
_publicIpcHostService.Start();
AppLogger.Info("PublicIpc", $"Public IPC host started. PipeName='{IpcConstants.DefaultPipeName}'.");
AppLogger.Info(
"PublicIpc",
$"Public IPC host started. PipeName='{IpcConstants.DefaultPipeName}'; Version='{versionInfo.Version}'; Codename='{versionInfo.Codename}'.");
}
catch (Exception ex)
{