using LanMountainDesktop.Shared.Contracts.Launcher; namespace LanMountainDesktop.Launcher.Services; internal sealed record HostLaunchPlan( string HostPath, string PackageRoot, string WorkingDirectory, IReadOnlyList Arguments, IReadOnlyDictionary EnvironmentVariables, AppVersionInfo VersionInfo); internal static class HostLaunchPlanBuilder { public const string DataRootOptionName = "data-root"; private static readonly string[] LauncherOnlyOptions = [ "debug", "show-loading-details", "plugins-dir", "source", "result", "app-root", DataRootOptionName, LauncherIpcConstants.LauncherPidEnvVar, LauncherIpcConstants.PackageRootEnvVar, LauncherIpcConstants.VersionEnvVar, LauncherIpcConstants.CodenameEnvVar ]; public static HostLaunchPlan Build( CommandContext context, DeploymentLocator deploymentLocator, HostResolutionResult resolution, string? dataRoot = null) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(deploymentLocator); ArgumentNullException.ThrowIfNull(resolution); if (string.IsNullOrWhiteSpace(resolution.ResolvedHostPath)) { throw new InvalidOperationException("Host path must be resolved before building a launch plan."); } var hostPath = Path.GetFullPath(resolution.ResolvedHostPath); var packageRoot = ResolvePackageRoot(hostPath, resolution.AppRoot, resolution.ResolutionSource); var versionInfo = deploymentLocator.GetVersionInfo(); var arguments = BuildForwardedArguments(context, packageRoot, versionInfo, dataRoot); var environment = new Dictionary(StringComparer.OrdinalIgnoreCase) { [LauncherIpcConstants.LauncherPidEnvVar] = Environment.ProcessId.ToString(), [LauncherIpcConstants.PackageRootEnvVar] = packageRoot, [LauncherIpcConstants.VersionEnvVar] = versionInfo.Version, [LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename }; if (!string.IsNullOrWhiteSpace(dataRoot)) { environment["LMD_DATA_ROOT"] = dataRoot; } return new HostLaunchPlan( hostPath, packageRoot, Directory.Exists(packageRoot) ? packageRoot : Path.GetDirectoryName(hostPath) ?? AppContext.BaseDirectory, arguments, environment, versionInfo); } public static string FormatArgumentsForLog(IReadOnlyList arguments) { return string.Join(" ", arguments.Select(QuoteArgument)); } private static string ResolvePackageRoot(string hostPath, string appRoot, string? resolutionSource) { var fullAppRoot = string.IsNullOrWhiteSpace(appRoot) ? AppContext.BaseDirectory : Path.GetFullPath(appRoot); var hostDirectory = Path.GetDirectoryName(hostPath); if (hostDirectory is not null && Directory.Exists(fullAppRoot) && IsAppDeploymentDirectory(hostDirectory) && IsParentOf(fullAppRoot, hostDirectory)) { return fullAppRoot; } if (string.Equals(resolutionSource, "published_deployment", StringComparison.OrdinalIgnoreCase) || string.Equals(resolutionSource, "explicit_app_root_deployment", StringComparison.OrdinalIgnoreCase) || string.Equals(resolutionSource, "legacy_fallback", StringComparison.OrdinalIgnoreCase)) { return fullAppRoot; } return hostDirectory ?? fullAppRoot; } private static IReadOnlyList BuildForwardedArguments( CommandContext context, string packageRoot, AppVersionInfo versionInfo, string? dataRoot = null) { var arguments = new List(); for (var index = 0; index < context.RawArgs.Count; index++) { var arg = context.RawArgs[index]; if (index == 0 && !arg.StartsWith("--", StringComparison.Ordinal) && string.Equals(arg, context.Command, StringComparison.OrdinalIgnoreCase)) { continue; } if (index == 1 && !arg.StartsWith("--", StringComparison.Ordinal) && string.Equals(arg, context.SubCommand, StringComparison.OrdinalIgnoreCase)) { continue; } if (arg.StartsWith("--", StringComparison.Ordinal)) { var key = arg[2..]; var equalsIndex = key.IndexOf('='); if (equalsIndex >= 0) { key = key[..equalsIndex]; } if (LauncherOnlyOptions.Contains(key, StringComparer.OrdinalIgnoreCase)) { if (equalsIndex < 0 && index + 1 < context.RawArgs.Count && !context.RawArgs[index + 1].StartsWith("--", StringComparison.Ordinal)) { index++; } continue; } } arguments.Add(arg); } arguments.Add($"--{LauncherIpcConstants.LauncherPidEnvVar}={Environment.ProcessId}"); arguments.Add($"--{LauncherIpcConstants.PackageRootEnvVar}={packageRoot}"); arguments.Add($"--{LauncherIpcConstants.VersionEnvVar}={versionInfo.Version}"); arguments.Add($"--{LauncherIpcConstants.CodenameEnvVar}={versionInfo.Codename}"); if (!string.IsNullOrWhiteSpace(dataRoot)) { arguments.Add($"--{DataRootOptionName}={dataRoot}"); } return arguments; } private static bool IsAppDeploymentDirectory(string path) { var fileName = Path.GetFileName(Path.TrimEndingDirectorySeparator(path)); return fileName.StartsWith("app-", StringComparison.OrdinalIgnoreCase); } private static bool IsParentOf(string parent, string child) { var parentPath = Path.GetFullPath(parent).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var childPath = Path.GetFullPath(child).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (string.Equals(parentPath, childPath, StringComparison.OrdinalIgnoreCase)) { return true; } return childPath.StartsWith( parentPath + Path.DirectorySeparatorChar, OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } private static string QuoteArgument(string value) { if (string.IsNullOrEmpty(value)) { return "\"\""; } if (!value.Contains('"') && !value.Contains(' ') && !value.Contains('\t')) { return value; } var builder = new System.Text.StringBuilder(); builder.Append('"'); foreach (var ch in value) { if (ch == '"') { builder.Append("\\\""); } else { builder.Append(ch); } } builder.Append('"'); return builder.ToString(); } }