using System.Diagnostics; using Avalonia.Threading; using LanMountainDesktop.Launcher.Models; using LanMountainDesktop.Launcher.Resources; using LanMountainDesktop.Launcher.Startup; using LanMountainDesktop.Launcher.Views; using LanMountainDesktop.Shared.Contracts.Launcher; using LanMountainDesktop.Shared.IPC; using LanMountainDesktop.Shared.IPC.Abstractions.Services; namespace LanMountainDesktop.Launcher.Services; internal sealed partial class LauncherFlowCoordinator { private async Task LaunchHostWithIpcAsync(bool forceDirectMode = false, string? retryTag = null) { var resolution = _deploymentLocator.ResolveHostExecutable(_context); if (!resolution.Success || string.IsNullOrWhiteSpace(resolution.ResolvedHostPath)) { var (errorResult, selectedPath) = await ShowHostNotFoundErrorAsync().ConfigureAwait(false); if (errorResult == ErrorWindowResult.Retry) { if (!string.IsNullOrWhiteSpace(selectedPath) && File.Exists(selectedPath)) { return await LaunchHostWithExplicitPathAsync(selectedPath, forceDirectMode, retryTag).ConfigureAwait(false); } return await LaunchHostWithIpcAsync(forceDirectMode, retryTag).ConfigureAwait(false); } return HostLaunchOutcome.FromResult(BuildResult( success: false, stage: "launchHost", code: "host_not_found", message: "LanMountainDesktop host executable was not found.", details: BuildResolutionDetails(resolution, null, null, "resolve"))); } return await LaunchHostWithResolvedPathAsync(resolution, forceDirectMode, retryTag).ConfigureAwait(false); } private Task LaunchHostWithExplicitPathAsync(string hostPath, bool forceDirectMode, string? retryTag) { var resolution = new HostResolutionResult { Success = true, ResolvedHostPath = Path.GetFullPath(hostPath), ResolutionSource = "user_selected_path", AppRoot = _deploymentLocator.GetAppRoot(), ExplicitAppRoot = Path.GetDirectoryName(hostPath), SearchedPaths = [Path.GetFullPath(hostPath)] }; return LaunchHostWithResolvedPathAsync(resolution, forceDirectMode, retryTag); } internal static LauncherResult? ValidateDotNetRuntimePrerequisite( HostLaunchPlan plan, HostResolutionResult resolution, DotNetRuntimeProbeOptions? probeOptions = null) { ArgumentNullException.ThrowIfNull(plan); ArgumentNullException.ThrowIfNull(resolution); if (!DotNetRuntimeProbe.IsFrameworkDependentWindowsApp(plan.HostPath)) { return null; } var runtime = DotNetRuntimeProbe.Probe(probeOptions); Logger.Info( $"Runtime prerequisite check completed. Available={runtime.IsAvailable}; " + $"Architecture={runtime.Architecture}; Message='{runtime.Message}'."); if (runtime.IsAvailable) { return null; } var details = BuildResolutionDetails(resolution, null, null, "runtime"); foreach (var pair in runtime.ToDetails()) { details[pair.Key] = pair.Value; } return BuildResult( success: false, stage: "launchHost", code: "dotnet_runtime_missing", message: ".NET 10 Desktop Runtime is required before LanMountainDesktop can start.", details: details, errorMessage: runtime.Message); } private async Task LaunchHostWithResolvedPathAsync( HostResolutionResult resolution, bool forceDirectMode, string? retryTag) { var dataRoot = _dataLocationResolver.ResolveDataRoot(); var plan = HostLaunchPlanBuilder.Build(_context, _deploymentLocator, resolution, dataRoot); var prerequisiteFailure = ValidateDotNetRuntimePrerequisite(plan, resolution); if (prerequisiteFailure is not null) { return HostLaunchOutcome.FromResult(prerequisiteFailure); } var hostPath = plan.HostPath; if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { EnsureExecutable(hostPath); } var primaryMode = HostStartMode.Direct; var fallbackMode = !forceDirectMode && OperatingSystem.IsWindows() ? HostStartMode.ShellExecute : (HostStartMode?)null; var firstAttempt = await StartHostProcessAsync(plan, primaryMode, retryTag).ConfigureAwait(false); if (firstAttempt.ProcessCreated && firstAttempt.Process is not null) { var firstDetails = BuildResolutionDetails(resolution, firstAttempt, null, null); return HostLaunchOutcome.FromProcess( firstAttempt.Process, BuildResult(true, "launchHost", "ok", "Host launched.", firstDetails), firstDetails); } if (fallbackMode is null) { return BuildOutcomeFromAttempt(resolution, firstAttempt, null); } Logger.Warn( $"Primary host start attempt failed. Retrying with fallback mode '{fallbackMode}'. " + $"FailureReason='{firstAttempt.FailureReason ?? "unknown"}'; ExitCode='{firstAttempt.ExitCode?.ToString() ?? ""}'."); var secondAttempt = await StartHostProcessAsync(plan, fallbackMode.Value, retryTag).ConfigureAwait(false); if (secondAttempt.ProcessCreated && secondAttempt.Process is not null) { var details = BuildResolutionDetails(resolution, firstAttempt, secondAttempt, null); return HostLaunchOutcome.FromProcess( secondAttempt.Process, BuildResult(true, "launchHost", "ok", "Host launched.", details), details); } return BuildOutcomeFromAttempt(resolution, secondAttempt, firstAttempt); } private static HostLaunchOutcome BuildOutcomeFromAttempt( HostResolutionResult resolution, HostStartAttempt finalAttempt, HostStartAttempt? previousAttempt) { var details = BuildResolutionDetails( resolution, previousAttempt ?? finalAttempt, previousAttempt is null ? null : finalAttempt, !finalAttempt.ProcessCreated ? "start" : finalAttempt.ExitCode is int finalExitCode && HostActivationPolicy.IsFailedActivationExitCode(finalExitCode) ? "activation" : "early-exit"); if (!finalAttempt.ProcessCreated) { return HostLaunchOutcome.FromResult(BuildResult( false, "launchHost", "host_start_failed", $"Failed to start host using start mode '{finalAttempt.StartMode}'.", details)); } if (finalAttempt.ExitCode is not null && HostActivationPolicy.IsSuccessfulActivationExitCode(finalAttempt.ExitCode.Value)) { return HostLaunchOutcome.FromImmediateResult(BuildResult( true, "launch", "activation_redirected", "Launcher activation was redirected to the existing desktop instance.", details)); } if (finalAttempt.ExitCode is not null && HostActivationPolicy.IsFailedActivationExitCode(finalAttempt.ExitCode.Value)) { return HostLaunchOutcome.FromResult(BuildResult( false, "launch", "activation_failed", $"Host activation handshake failed using start mode '{finalAttempt.StartMode}'.", details)); } return HostLaunchOutcome.FromResult(BuildResult( false, "launchHost", "host_exited_early", $"Host exited early using start mode '{finalAttempt.StartMode}'.", details)); } private async Task StartHostProcessAsync( HostLaunchPlan plan, HostStartMode startMode, string? retryTag) { var startInfo = new ProcessStartInfo { FileName = plan.HostPath, WorkingDirectory = plan.WorkingDirectory, UseShellExecute = startMode == HostStartMode.ShellExecute }; if (startMode == HostStartMode.Direct) { foreach (var argument in plan.Arguments) { startInfo.ArgumentList.Add(argument); } foreach (var pair in plan.EnvironmentVariables) { startInfo.EnvironmentVariables[pair.Key] = pair.Value; } } else { startInfo.Arguments = HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments); } try { var process = Process.Start(startInfo); Logger.Info( $"Host launch requested. Mode='{startMode}'; RetryTag='{retryTag ?? ""}'; Path='{plan.HostPath}'; " + $"PackageRoot='{plan.PackageRoot}'; WorkingDir='{plan.WorkingDirectory}'; Pid={(process is null ? -1 : process.Id)}; " + $"Args='{HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments)}'."); if (process is null) { return HostStartAttempt.StartFailed(startMode, "process_start_returned_null", plan); } await Task.Yield(); return HostStartAttempt.Started(startMode, process, plan); } catch (Exception ex) { Logger.Error($"Host start failed. Mode='{startMode}'.", ex); return HostStartAttempt.StartFailed(startMode, ex.GetType().Name, plan); } } private static Dictionary BuildResolutionDetails( HostResolutionResult resolution, HostStartAttempt? firstAttempt, HostStartAttempt? secondAttempt, string? failureStage) { var details = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["resolvedAppRoot"] = resolution.AppRoot, ["explicitAppRoot"] = resolution.ExplicitAppRoot ?? string.Empty, ["resolvedHostPath"] = resolution.ResolvedHostPath ?? string.Empty, ["resolutionSource"] = resolution.ResolutionSource ?? string.Empty, ["devModeConfigIgnored"] = resolution.DevModeConfigIgnored.ToString(), ["searchedPaths"] = string.Join(" | ", resolution.SearchedPaths), ["failureStage"] = failureStage ?? string.Empty }; if (firstAttempt is not null) { details["startMode"] = firstAttempt.StartMode.ToString(); details["processCreated"] = firstAttempt.ProcessCreated.ToString(); details["hostPid"] = firstAttempt.ProcessId?.ToString() ?? string.Empty; details["packageRoot"] = firstAttempt.PackageRoot ?? string.Empty; details["workingDirectory"] = firstAttempt.WorkingDirectory ?? string.Empty; details["arguments"] = firstAttempt.Arguments ?? string.Empty; details["firstAttemptFailureReason"] = firstAttempt.FailureReason ?? string.Empty; details["firstAttemptExitCode"] = firstAttempt.ExitCode?.ToString() ?? string.Empty; } if (secondAttempt is not null) { details["fallbackStartMode"] = secondAttempt.StartMode.ToString(); details["fallbackProcessCreated"] = secondAttempt.ProcessCreated.ToString(); details["fallbackHostPid"] = secondAttempt.ProcessId?.ToString() ?? string.Empty; details["fallbackPackageRoot"] = secondAttempt.PackageRoot ?? string.Empty; details["fallbackWorkingDirectory"] = secondAttempt.WorkingDirectory ?? string.Empty; details["fallbackArguments"] = secondAttempt.Arguments ?? string.Empty; details["fallbackFailureReason"] = secondAttempt.FailureReason ?? string.Empty; details["fallbackExitCode"] = secondAttempt.ExitCode?.ToString() ?? string.Empty; } return details; } private static void EnsureExecutable(string path) { if (OperatingSystem.IsWindows()) { return; } try { var mode = File.GetUnixFileMode(path); mode |= UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; File.SetUnixFileMode(path, mode); } catch { } } }