diff --git a/LanMountainDesktop.Launcher/App.axaml.cs b/LanMountainDesktop.Launcher/App.axaml.cs index 96bde41..eb9d0cc 100644 --- a/LanMountainDesktop.Launcher/App.axaml.cs +++ b/LanMountainDesktop.Launcher/App.axaml.cs @@ -120,7 +120,23 @@ public partial class App : Application private static SplashWindow CreateSplashWindow() { var preferences = StartupVisualPreferencesResolver.Resolve(); - return new SplashWindow(preferences.Mode); + var window = new SplashWindow(preferences.Mode); + TrySetSplashVersionInfo(window, LauncherRuntimeContext.Current); + return window; + } + + private static void TrySetSplashVersionInfo(SplashWindow window, CommandContext context) + { + try + { + var appRoot = Commands.ResolveAppRoot(context); + var versionInfo = new DeploymentLocator(appRoot).GetVersionInfo(); + window.SetVersionInfo(versionInfo.Version, versionInfo.Codename); + } + catch (Exception ex) + { + Logger.Warn($"Failed to set splash version info before coordinator start: {ex.Message}"); + } } private async Task SimulateSplashPreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, SplashWindow window) @@ -318,12 +334,16 @@ public partial class App : Application { reporter?.Report("activation", response.Message); await DismissSplashIfNeededAsync(splashWindow).ConfigureAwait(false); + var success = response.Accepted || + IsRecoverableActivationFailure(response.ActivationResult, response.Status); return new LauncherResult { - Success = response.Accepted, + Success = success, Stage = "launch", - Code = response.Code, - Message = response.Message, + Code = success && !response.Accepted ? "attached_to_launcher_coordinator" : response.Code, + Message = success && !response.Accepted + ? "Attached to the active Launcher coordinator; desktop startup is still in progress." + : response.Message, Details = BuildCoordinatorResultDetails(response.Status, response.ActivationResult) }; } @@ -334,12 +354,19 @@ public partial class App : Application { reporter?.Report("activation", activation.Message); await DismissSplashIfNeededAsync(splashWindow).ConfigureAwait(false); + var success = activation.Accepted || IsRecoverableActivationFailure(activation, null); return new LauncherResult { - Success = activation.Accepted, + Success = success, Stage = "launch", - Code = activation.Accepted ? "existing_host_activated" : "existing_host_activation_failed", - Message = activation.Message, + Code = activation.Accepted + ? "existing_host_activated" + : success + ? "existing_host_startup_pending" + : "existing_host_activation_failed", + Message = success && !activation.Accepted + ? "Existing desktop process is still starting; Launcher attached without starting another process." + : activation.Message, Details = BuildCoordinatorResultDetails(null, activation) }; } @@ -370,6 +397,18 @@ public partial class App : Application var activation = await TryActivateExistingInstanceWithStatusAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); if (activation is not null) { + if (!activation.Accepted && IsRecoverableActivationFailure(activation, status)) + { + return new LauncherCoordinatorResponse + { + Accepted = true, + Code = "attached_to_launcher_coordinator", + Message = "Attached to the active Launcher coordinator; desktop startup is still in progress.", + Status = status, + ActivationResult = activation + }; + } + return new LauncherCoordinatorResponse { Accepted = activation.Accepted, @@ -419,6 +458,32 @@ public partial class App : Application }; } + private static bool IsRecoverableActivationFailure( + PublicShellActivationResult? activation, + LauncherCoordinatorStatus? status) + { + if (activation is { Accepted: true }) + { + return false; + } + + if (status is { Completed: false, HostProcessAlive: true }) + { + return true; + } + + var shellStatus = activation?.Status; + if (shellStatus is null || !shellStatus.PublicIpcReady) + { + return false; + } + + return !shellStatus.MainWindowOpened || + !shellStatus.DesktopVisible || + string.Equals(activation?.Code, "shell_not_ready", StringComparison.OrdinalIgnoreCase) || + string.Equals(activation?.Code, "startup_pending", StringComparison.OrdinalIgnoreCase); + } + private static Dictionary BuildCoordinatorResultDetails( LauncherCoordinatorStatus? status, PublicShellActivationResult? activation) diff --git a/LanMountainDesktop.Launcher/Models/StartupAttemptRecord.cs b/LanMountainDesktop.Launcher/Models/StartupAttemptRecord.cs index 059184a..51dc624 100644 --- a/LanMountainDesktop.Launcher/Models/StartupAttemptRecord.cs +++ b/LanMountainDesktop.Launcher/Models/StartupAttemptRecord.cs @@ -9,7 +9,8 @@ internal enum StartupAttemptState SoftTimeout, DetachedWaiting, Succeeded, - Failed + Failed, + WaitingForShell } internal sealed class StartupAttemptRecord diff --git a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs index 981c70b..81e041b 100644 --- a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs +++ b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs @@ -204,12 +204,16 @@ internal sealed class DeploymentLocator var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath(); if (!string.IsNullOrWhiteSpace(savedCustomPath)) { - var fullSavedPath = Path.GetFullPath(savedCustomPath); - searchedPaths.Add(fullSavedPath); - if (File.Exists(fullSavedPath)) + if (TryNormalizeSavedDebugPath(savedCustomPath, out var fullSavedPath)) { - source = "debug_saved_custom_path"; - return fullSavedPath; + searchedPaths.Add(fullSavedPath); + if (File.Exists(fullSavedPath)) + { + source = "debug_saved_custom_path"; + return fullSavedPath; + } + + Logger.Warn($"Saved launcher debug host path is invalid; falling back to development paths. Path='{fullSavedPath}'."); } } } @@ -229,6 +233,21 @@ internal sealed class DeploymentLocator return null; } + private static bool TryNormalizeSavedDebugPath(string savedPath, out string fullSavedPath) + { + try + { + fullSavedPath = Path.GetFullPath(savedPath); + return true; + } + catch (Exception ex) + { + fullSavedPath = string.Empty; + Logger.Warn($"Saved launcher debug host path is invalid and cannot be normalized; falling back to development paths. Path='{savedPath}'; Error='{ex.Message}'."); + return false; + } + } + private static string? FindBestDeploymentHost( string root, string executable, @@ -303,9 +322,17 @@ internal sealed class DeploymentLocator if (Views.ErrorWindow.CheckDevModeEnabled()) { var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath(); - if (!string.IsNullOrWhiteSpace(savedCustomPath) && File.Exists(savedCustomPath)) + if (!string.IsNullOrWhiteSpace(savedCustomPath)) { - return savedCustomPath; + if (TryNormalizeSavedDebugPath(savedCustomPath, out var fullSavedPath) && + File.Exists(fullSavedPath)) + { + return fullSavedPath; + } + else if (!string.IsNullOrWhiteSpace(fullSavedPath)) + { + Logger.Warn($"Saved launcher debug host path is invalid; falling back to development paths. Path='{fullSavedPath}'."); + } } var devPath = ScanDevelopmentPaths(executable); diff --git a/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs b/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs index 1d4d0ce..51c5672 100644 --- a/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs +++ b/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs @@ -560,6 +560,11 @@ namespace LanMountainDesktop.Launcher.Services; } } + if (string.Equals(source, "saved dev mode path", StringComparison.OrdinalIgnoreCase)) + { + Logger.Warn($"Saved launcher debug host path is invalid; continuing host discovery. Path='{path}'."); + } + return null; } diff --git a/LanMountainDesktop.Launcher/Services/LauncherDebugSettingsStore.cs b/LanMountainDesktop.Launcher/Services/LauncherDebugSettingsStore.cs new file mode 100644 index 0000000..5cb0bed --- /dev/null +++ b/LanMountainDesktop.Launcher/Services/LauncherDebugSettingsStore.cs @@ -0,0 +1,124 @@ +namespace LanMountainDesktop.Launcher.Services; + +internal sealed record LauncherDebugSettings(bool DevModeEnabled, string? CustomHostPath); + +internal static class LauncherDebugSettingsStore +{ + private const string DevModeFileName = "dev-mode.flag"; + private const string CustomHostPathFileName = "custom-host-path.txt"; + private const string LegacyDevModeFileName = "devmode.config"; + private const string LegacyCustomHostPathFileName = "custom-host-path.config"; + + internal static string? ConfigBaseDirectoryOverride { get; set; } + + public static string ConfigBaseDirectory => ConfigBaseDirectoryOverride ?? ResolveConfigBaseDirectory(); + + public static LauncherDebugSettings Load() + { + return new LauncherDebugSettings( + LoadDevModeState(), + LoadCustomHostPath()); + } + + public static bool IsDevModeEnabled() => Load().DevModeEnabled; + + public static string? GetSavedCustomHostPath() => Load().CustomHostPath; + + public static void Save(LauncherDebugSettings settings) + { + try + { + Directory.CreateDirectory(ConfigBaseDirectory); + File.WriteAllText(GetPath(DevModeFileName), settings.DevModeEnabled.ToString()); + File.WriteAllText(GetPath(CustomHostPathFileName), settings.CustomHostPath ?? string.Empty); + } + catch (Exception ex) + { + Logger.Warn($"Failed to save launcher debug settings: {ex.Message}"); + } + } + + public static void SaveDevModeState(bool enabled) + { + var current = Load(); + Save(current with { DevModeEnabled = enabled }); + } + + public static void SaveCustomHostPath(string? customHostPath) + { + var current = Load(); + Save(current with { CustomHostPath = customHostPath }); + } + + private static bool LoadDevModeState() + { + var newValue = TryReadText(GetPath(DevModeFileName)); + if (!string.IsNullOrWhiteSpace(newValue)) + { + return TryParseDevMode(newValue); + } + + var legacyValue = TryReadText(GetPath(LegacyDevModeFileName)); + return !string.IsNullOrWhiteSpace(legacyValue) && TryParseDevMode(legacyValue); + } + + private static string? LoadCustomHostPath() + { + var newValue = TryReadText(GetPath(CustomHostPathFileName)); + if (!string.IsNullOrWhiteSpace(newValue)) + { + return newValue.Trim(); + } + + var legacyValue = TryReadText(GetPath(LegacyCustomHostPathFileName)); + return string.IsNullOrWhiteSpace(legacyValue) ? null : legacyValue.Trim(); + } + + private static bool TryParseDevMode(string value) + { + var normalized = value.Trim(); + return normalized == "1" || + normalized.Equals("true", StringComparison.OrdinalIgnoreCase) || + normalized.Equals("yes", StringComparison.OrdinalIgnoreCase) || + normalized.Equals("on", StringComparison.OrdinalIgnoreCase); + } + + private static string? TryReadText(string path) + { + try + { + return File.Exists(path) ? File.ReadAllText(path) : null; + } + catch (Exception ex) + { + Logger.Warn($"Failed to read launcher debug setting '{path}': {ex.Message}"); + return null; + } + } + + private static string GetPath(string fileName) => Path.Combine(ConfigBaseDirectory, fileName); + + private static string ResolveConfigBaseDirectory() + { + try + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (!string.IsNullOrWhiteSpace(appData)) + { + return Path.Combine(appData, "LanMountainDesktop", ".launcher"); + } + } + catch + { + } + + try + { + return Path.Combine(AppContext.BaseDirectory, ".launcher"); + } + catch + { + return Path.Combine(Directory.GetCurrentDirectory(), ".launcher"); + } + } +} diff --git a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs index f5e9932..e8f2591 100644 --- a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs +++ b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs @@ -228,6 +228,7 @@ internal sealed class LauncherFlowCoordinator { ipcConnected = true; shellStatus = existingActivation.Status; + var recoverableActivationFailure = IsRecoverableActivationFailure(existingActivation); lastStage = existingActivation.Accepted ? StartupStage.ActivationRedirected : StartupStage.ActivationFailed; @@ -236,6 +237,10 @@ internal sealed class LauncherFlowCoordinator { _startupAttemptRegistry.MarkOwnedSucceeded(lastStage, lastStageMessage); } + else if (recoverableActivationFailure) + { + _startupAttemptRegistry.MarkOwnedWaitingForShell(lastStageMessage); + } else { _startupAttemptRegistry.MarkOwnedFailed(lastStage, lastStageMessage); @@ -244,14 +249,20 @@ internal sealed class LauncherFlowCoordinator PublishCoordinatorStatus( hostProcessAliveOverride: true, completed: true, - succeeded: existingActivation.Accepted); + succeeded: existingActivation.Accepted || recoverableActivationFailure); windowsClosingByCoordinator = true; await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); return BuildResult( - success: existingActivation.Accepted, + success: existingActivation.Accepted || recoverableActivationFailure, stage: "launch", - code: existingActivation.Accepted ? "existing_host_activated" : "existing_host_activation_failed", - message: existingActivation.Message, + code: existingActivation.Accepted + ? "existing_host_activated" + : recoverableActivationFailure + ? "existing_host_startup_pending" + : "existing_host_activation_failed", + message: recoverableActivationFailure + ? "Existing desktop process is still starting; Launcher will not start another process." + : existingActivation.Message, details: MergeDetails( launcherContextDetails, new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -438,6 +449,11 @@ internal sealed class LauncherFlowCoordinator ipcConnected = true; _startupAttemptRegistry.MarkOwnedIpcConnected(); shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false); + if (shellStatus is { DesktopVisible: false }) + { + _startupAttemptRegistry.MarkOwnedWaitingForShell("Host public IPC is ready; waiting for desktop shell."); + } + PublishCoordinatorStatus(hostProcessAliveOverride: true); } @@ -464,6 +480,7 @@ internal sealed class LauncherFlowCoordinator var softTimeoutAt = startedAt + StartupSoftTimeout; var hardTimeoutAt = startedAt + StartupHardTimeout; var nextReconnectAttemptAt = DateTimeOffset.UtcNow.AddSeconds(5); + var activationRetryAttempted = false; while (true) { @@ -482,10 +499,35 @@ internal sealed class LauncherFlowCoordinator details: ComposeLaunchDetails(!launchOutcome.Process.HasExited)); } - if (activationFailedTcs.Task.IsCompleted && string.IsNullOrWhiteSpace(activationFailureReason)) + if (activationFailedTcs.Task.IsCompleted && !activationRetryAttempted) { + activationRetryAttempted = true; activationFailureReason = await activationFailedTcs.Task.ConfigureAwait(false); Logger.Warn($"Activation failure received before startup success. Reason='{activationFailureReason}'."); + var retryOutcome = await RetryActivationAfterEarlyFailureAsync().ConfigureAwait(false); + if (retryOutcome is not null) + { + windowsClosingByCoordinator = true; + if (retryOutcome.Success) + { + _startupAttemptRegistry.MarkOwnedSucceeded(lastStage, retryOutcome.Message); + PublishCoordinatorStatus( + hostProcessAliveOverride: !launchOutcome.Process.HasExited, + completed: true, + succeeded: true); + } + else + { + _startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason); + PublishCoordinatorStatus( + hostProcessAliveOverride: !launchOutcome.Process.HasExited, + completed: true, + succeeded: false); + } + + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return WithAdditionalDetails(retryOutcome, ComposeLaunchDetails(!launchOutcome.Process.HasExited, recoveryActivationAttempted: true)); + } } if (processExitTask.IsCompleted) @@ -512,6 +554,36 @@ internal sealed class LauncherFlowCoordinator })); } + if (!activationRetryAttempted && + exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired) + { + activationRetryAttempted = true; + var retryOutcome = await RetryActivationAfterEarlyFailureAsync().ConfigureAwait(false); + if (retryOutcome is not null) + { + if (retryOutcome.Success) + { + _startupAttemptRegistry.MarkOwnedSucceeded(lastStage, retryOutcome.Message); + PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: true); + } + else + { + _startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason); + PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: false); + } + + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return WithAdditionalDetails( + retryOutcome, + MergeDetails( + ComposeLaunchDetails(hostProcessAlive: false, recoveryActivationAttempted: true), + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["exitCode"] = exitCode.ToString() + })); + } + } + _startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason); PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: false); await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); @@ -543,6 +615,11 @@ internal sealed class LauncherFlowCoordinator ipcConnected = true; _startupAttemptRegistry.MarkOwnedIpcConnected(); shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false); + if (shellStatus is { DesktopVisible: false }) + { + _startupAttemptRegistry.MarkOwnedWaitingForShell("Host public IPC reconnected; waiting for desktop shell."); + } + PublishCoordinatorStatus(hostProcessAliveOverride: true); } @@ -602,6 +679,11 @@ internal sealed class LauncherFlowCoordinator ipcConnected = true; _startupAttemptRegistry.MarkOwnedIpcConnected(); shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false); + if (shellStatus is { DesktopVisible: false }) + { + _startupAttemptRegistry.MarkOwnedWaitingForShell("Host public IPC is ready; waiting for desktop shell."); + } + PublishCoordinatorStatus(hostProcessAliveOverride: true); } } @@ -632,6 +714,23 @@ internal sealed class LauncherFlowCoordinator } } + if (connected && !launchOutcome.Process.HasExited) + { + windowsClosingByCoordinator = true; + _startupAttemptRegistry.MarkOwnedWaitingForShell("Host process is still running after the launcher wait window."); + shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false); + PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: false, succeeded: false); + await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false); + return BuildResult( + success: true, + stage: "launch", + code: "startup_pending", + message: "Host process is still running; Launcher will not start another process while the desktop shell finishes startup.", + details: ComposeLaunchDetails( + hostProcessAlive: true, + recoveryActivationAttempted)); + } + windowsClosingByCoordinator = true; _startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason); PublishCoordinatorStatus(!launchOutcome.Process.HasExited, completed: true, succeeded: false); @@ -1369,6 +1468,25 @@ internal sealed class LauncherFlowCoordinator } } + private static bool IsRecoverableActivationFailure(PublicShellActivationResult activation) + { + if (activation.Accepted) + { + return false; + } + + if (string.Equals(activation.Code, "shutdown_in_progress", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return activation.Status.PublicIpcReady && + (!activation.Status.MainWindowOpened || + !activation.Status.DesktopVisible || + string.Equals(activation.Code, "shell_not_ready", StringComparison.OrdinalIgnoreCase) || + string.Equals(activation.Code, "startup_pending", StringComparison.OrdinalIgnoreCase)); + } + private static async Task TryGetPublicShellStatusAsync( LanMountainDesktopIpcClient ipcClient) { diff --git a/LanMountainDesktop.Launcher/Services/StartupAttemptRegistry.cs b/LanMountainDesktop.Launcher/Services/StartupAttemptRegistry.cs index 57d21b3..8037cd4 100644 --- a/LanMountainDesktop.Launcher/Services/StartupAttemptRegistry.cs +++ b/LanMountainDesktop.Launcher/Services/StartupAttemptRegistry.cs @@ -309,6 +309,19 @@ internal sealed class StartupAttemptRegistry }); } + public void MarkOwnedWaitingForShell(string? message) + { + UpdateOwned(record => + { + if (record.State is StartupAttemptState.Pending or StartupAttemptState.SoftTimeout or StartupAttemptState.DetachedWaiting) + { + record.State = StartupAttemptState.WaitingForShell; + } + + record.LastObservedMessage = message ?? record.LastObservedMessage; + }); + } + public void MarkOwnedDetachedWaiting() { UpdateOwned(record => @@ -423,7 +436,11 @@ internal sealed class StartupAttemptRegistry private static bool IsAttachable(StartupAttemptRecord record) { - if (record.State is not (StartupAttemptState.Pending or StartupAttemptState.SoftTimeout or StartupAttemptState.DetachedWaiting)) + if (record.State is not ( + StartupAttemptState.Pending or + StartupAttemptState.SoftTimeout or + StartupAttemptState.DetachedWaiting or + StartupAttemptState.WaitingForShell)) { return false; } @@ -433,7 +450,11 @@ internal sealed class StartupAttemptRegistry private static bool IsRecoverableCoordinatorAttempt(StartupAttemptRecord record) { - if (record.State is not (StartupAttemptState.Pending or StartupAttemptState.SoftTimeout or StartupAttemptState.DetachedWaiting)) + if (record.State is not ( + StartupAttemptState.Pending or + StartupAttemptState.SoftTimeout or + StartupAttemptState.DetachedWaiting or + StartupAttemptState.WaitingForShell)) { return false; } @@ -448,7 +469,11 @@ internal sealed class StartupAttemptRegistry private static bool IsCoordinatorLive(StartupAttemptRecord record) { - if (record.State is not (StartupAttemptState.Pending or StartupAttemptState.SoftTimeout or StartupAttemptState.DetachedWaiting)) + if (record.State is not ( + StartupAttemptState.Pending or + StartupAttemptState.SoftTimeout or + StartupAttemptState.DetachedWaiting or + StartupAttemptState.WaitingForShell)) { return false; } diff --git a/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs b/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs index c7f51e2..db302df 100644 --- a/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs +++ b/LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs @@ -5,52 +5,41 @@ using Avalonia.Platform.Storage; namespace LanMountainDesktop.Launcher.Views; -/// -/// 错误调试窗口 - 开发人员专用调试设置 -/// public partial class ErrorDebugWindow : Window { private string? _selectedHostPath; - private bool _isInitialized = false; + private bool _isInitialized; - /// - /// 是否启用了开发模式 - /// public bool IsDevModeEnabled { get; private set; } - /// - /// 选择的主程序路径 - /// + public bool WasAccepted { get; private set; } + public string? SelectedHostPath => _selectedHostPath; public ErrorDebugWindow() { AvaloniaXamlLoader.Load(this); - - // 延迟到窗口加载完成后再初始化组件 - this.Loaded += OnWindowLoaded; + Loaded += OnWindowLoaded; } - public ErrorDebugWindow(bool devModeEnabled, string? initialPath) : this() + public ErrorDebugWindow(bool devModeEnabled, string? initialPath) + : this() { IsDevModeEnabled = devModeEnabled; _selectedHostPath = initialPath; } - /// - /// 窗口加载完成事件 - /// private void OnWindowLoaded(object? sender, RoutedEventArgs e) { - if (_isInitialized) return; + if (_isInitialized) + { + return; + } + _isInitialized = true; - - Console.WriteLine("[ErrorDebugWindow] Window loaded, initializing components..."); InitializeComponents(); - - // 设置初始值(在视觉树准备好后) - var devModeToggle = this.FindControl("DevModeToggle"); - if (devModeToggle is not null) + + if (this.FindControl("DevModeToggle") is { } devModeToggle) { devModeToggle.IsChecked = IsDevModeEnabled; } @@ -60,113 +49,72 @@ public partial class ErrorDebugWindow : Window private void InitializeComponents() { - // 开发模式开关 - var devModeToggle = this.FindControl("DevModeToggle"); - if (devModeToggle is not null) + if (this.FindControl("DevModeToggle") is { } devModeToggle) { - devModeToggle.IsCheckedChanged += (s, e) => + devModeToggle.IsCheckedChanged += (_, _) => { IsDevModeEnabled = devModeToggle.IsChecked ?? false; - Console.WriteLine($"[ErrorDebugWindow] DevMode changed to: {IsDevModeEnabled}"); }; - Console.WriteLine("[ErrorDebugWindow] DevModeToggle event bound"); - } - else - { - Console.Error.WriteLine("[ErrorDebugWindow] Failed to find DevModeToggle!"); } - // 浏览按钮 - var browseButton = this.FindControl