diff --git a/Launcher.Next.App/App.xaml.cs b/Launcher.Next.App/App.xaml.cs index 6225945..5b1086d 100644 --- a/Launcher.Next.App/App.xaml.cs +++ b/Launcher.Next.App/App.xaml.cs @@ -2,5 +2,14 @@ namespace Launcher.Next.App; public partial class App : System.Windows.Application { protected override async void OnStartup(System.Windows.StartupEventArgs e) - { base.OnStartup(e); if (e.Args.FirstOrDefault()?.Equals("pilot", StringComparison.OrdinalIgnoreCase) == true) { Shutdown(await PilotCli.RunAsync(e.Args.Skip(1).ToArray())); return; } new MainWindow().Show(); } + { + base.OnStartup(e); + if (e.Args.FirstOrDefault()?.Equals("legacy-configuration-probe", StringComparison.OrdinalIgnoreCase) == true) + { + Shutdown(await LegacyConfigurationProbeCli.RunAsync(e.Args.Skip(1).ToArray())); + return; + } + if (e.Args.FirstOrDefault()?.Equals("pilot", StringComparison.OrdinalIgnoreCase) == true) { Shutdown(await PilotCli.RunAsync(e.Args.Skip(1).ToArray())); return; } + new MainWindow().Show(); + } } diff --git a/Launcher.Next.App/LegacyConfigurationProbeCli.cs b/Launcher.Next.App/LegacyConfigurationProbeCli.cs new file mode 100644 index 0000000..75154dd --- /dev/null +++ b/Launcher.Next.App/LegacyConfigurationProbeCli.cs @@ -0,0 +1,47 @@ +using Launcher.Next.Application; +using Launcher.Next.Core; +using System.IO; + +namespace Launcher.Next.App; + +/// Dedicated, non-Autodesk command route. It deliberately constructs +/// only the read-only legacy configuration probe dependency graph. +internal static class LegacyConfigurationProbeCli +{ + internal static async Task RunAsync(string[] args) + { + if (args.Length != 4 || !string.Equals(args[0], "--mode", StringComparison.Ordinal) || + !string.Equals(args[1], "LegacyConfigurationProbe", StringComparison.Ordinal) || + !string.Equals(args[2], "--config", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(args[3])) + return Fail("LEGACY_PROBE_MODE_CONFLICT"); + + try + { + if (!Path.IsPathFullyQualified(args[3]) || !File.Exists(args[3]) || Directory.Exists(args[3])) + return Fail("LEGACY_PROBE_OVERRIDE_MISSING"); + var overrideDocument = LegacyProbeOverrideParser.Parse(await File.ReadAllTextAsync(args[3])); + var variables = new LauncherPathVariables(new Dictionary + { + ["LegacySettingsPath"] = overrideDocument.SettingsXml, + ["LegacyDownloadListPath"] = overrideDocument.DownloadListXml, + ["LegacyCompatibilityOverlayPath"] = overrideDocument.CompatibilityOverlay, + ["LegacyProbeEvidenceRoot"] = overrideDocument.EvidenceOutputRoot + }); + var resolver = new PathResolver(); + var request = new LegacyConfigurationProbeRequest( + resolver.ResolveLegacySettingsPath(variables), + resolver.ResolveLegacyDownloadListPath(variables), + resolver.ResolveLegacyCompatibilityOverlayPath(variables), + resolver.ResolveLegacyProbeEvidenceRoot(variables), + overrideDocument.SelectedPattern, + Guid.NewGuid().ToString("N")); + var result = await new LegacyConfigurationProbe(new LegacyConfigurationAdapter()).RunAsync(request); + Console.WriteLine(result.TerminalState); + return result.TerminalState == "Succeeded" ? 0 : 2; + } + catch (InvalidOperationException ex) { return Fail(ex.Message); } + catch { return Fail("LEGACY_PROBE_INTERNAL_FAILURE"); } + } + + private static int Fail(string code) { Console.Error.WriteLine(code); return 2; } +} diff --git a/Launcher.Next.Application/LegacyConfigurationProbe.cs b/Launcher.Next.Application/LegacyConfigurationProbe.cs index 3fa2750..cb24247 100644 --- a/Launcher.Next.Application/LegacyConfigurationProbe.cs +++ b/Launcher.Next.Application/LegacyConfigurationProbe.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using System.Text; using System.Text.Json; using Launcher.Next.Core; @@ -17,31 +18,34 @@ public sealed class LegacyConfigurationProbe(LegacyConfigurationAdapter adapter) var selectionPath = request.DownloadList.Value; var overlayPath = request.Overlay.Value; var evidenceRoot = request.EvidenceRoot.Value; - if (!File.Exists(settingsPath) || !File.Exists(selectionPath)) throw new InvalidOperationException("LEGACY_PROBE_INPUT_PATH_INVALID"); - if (Path.GetDirectoryName(settingsPath)!.Equals(evidenceRoot, StringComparison.OrdinalIgnoreCase) || Path.GetDirectoryName(selectionPath)!.Equals(evidenceRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("PATH_ROLE_COLLISION"); + if (!File.Exists(settingsPath) || !File.Exists(selectionPath) || !File.Exists(overlayPath)) throw new InvalidOperationException("LEGACY_PROBE_INPUT_PATH_INVALID"); + if (evidenceRoot.StartsWith(@"\\", StringComparison.Ordinal) || SameOrNested(evidenceRoot, Path.GetDirectoryName(settingsPath)!) || SameOrNested(evidenceRoot, Path.GetDirectoryName(selectionPath)!) || SameOrNested(evidenceRoot, Path.GetDirectoryName(overlayPath)!)) throw new InvalidOperationException("LEGACY_PROBE_PATH_COLLISION"); Directory.CreateDirectory(evidenceRoot); var before = await Snapshot(settingsPath, selectionPath, overlayPath, cancellationToken); try { - var settings = adapter.ParseSettings(await File.ReadAllTextAsync(settingsPath, cancellationToken)); + var settings = adapter.ParseSettings(await ReadOnlyTextAsync(settingsPath, cancellationToken)); var pattern = settings.Patterns.Where(x => string.Equals(x.Name, request.PatternName, StringComparison.Ordinal)).ToArray(); if (pattern.Length != 1) throw new InvalidOperationException(pattern.Length == 0 ? "LEGACY_PATTERN_NOT_FOUND" : "LEGACY_PATTERN_AMBIGUOUS"); - var overlay = LegacyCompatibilityOverlayParser.Parse(await File.ReadAllTextAsync(overlayPath, cancellationToken)); - var requirements = adapter.Join(pattern[0], adapter.ParseDownloadList(await File.ReadAllTextAsync(selectionPath, cancellationToken)), overlay); + var overlay = LegacyCompatibilityOverlayParser.Parse(await ReadOnlyTextAsync(overlayPath, cancellationToken)); + var requirements = adapter.Join(pattern[0], adapter.ParseDownloadList(await ReadOnlyTextAsync(selectionPath, cancellationToken)), overlay); var after = await Snapshot(settingsPath, selectionPath, overlayPath, cancellationToken); if (!before.SequenceEqual(after)) throw new InvalidOperationException("LEGACY_PROBE_SOURCE_CHANGED_DURING_READ"); - var evidence = new { mode="LegacyConfigurationProbe", operationId=request.OperationId, terminalState="LEGACY_CONFIGURATION_VERIFIED", selectedPattern=pattern[0].Name, selectedIdCount=requirements.Count, sourceWrites=false, payloadCopied=false, stagingCreated=false, activationAttempted=false, autoCadLaunchAttempted=false, dfsWriteAttempted=false, inputSnapshots=before.Select(x=>Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(x)))) }; - var output=Path.Combine(evidenceRoot, request.OperationId + ".json"); - await File.WriteAllTextAsync(output, JsonSerializer.Serialize(evidence), cancellationToken); - return new("LEGACY_CONFIGURATION_VERIFIED",requirements,output,[]); + var output = await WriteEvidenceAsync(evidenceRoot, request.OperationId, new { evidenceSchemaVersion=1, evidenceKind="legacy-configuration-probe", mode="LegacyConfigurationProbe", operationId=request.OperationId, terminalState="Succeeded", exitClassification="Success", selectedPattern=pattern[0].Name, selectedIdCount=requirements.Count, requirements, inputs=before.Select(x=>Redact(x,request.OperationId)), sideEffects=SideEffects() }, cancellationToken); + return new("Succeeded",requirements,output,[]); } catch(Exception ex) { - var output=Path.Combine(evidenceRoot, request.OperationId + ".json"); - await File.WriteAllTextAsync(output, JsonSerializer.Serialize(new { mode="LegacyConfigurationProbe", operationId=request.OperationId, terminalState="PILOT_CONFIGURATION_BLOCKED", diagnostic=ex.Message, sourceWrites=false, payloadCopied=false, stagingCreated=false, activationAttempted=false, autoCadLaunchAttempted=false, dfsWriteAttempted=false }), cancellationToken); - return new("PILOT_CONFIGURATION_BLOCKED",[],output,[ex.Message]); + var code=ex.Message.StartsWith("LEGACY_",StringComparison.Ordinal)||ex.Message.StartsWith("AMBIGUOUS_",StringComparison.Ordinal)?ex.Message:"LEGACY_PROBE_INTERNAL_FAILURE"; + var output=await WriteEvidenceAsync(evidenceRoot,request.OperationId,new { evidenceSchemaVersion=1,evidenceKind="legacy-configuration-probe",mode="LegacyConfigurationProbe",operationId=request.OperationId,terminalState="Blocked",exitClassification=code,diagnostics=new[]{code},inputs=before.Select(x=>Redact(x,request.OperationId)),sideEffects=SideEffects() },cancellationToken); + return new("Blocked",[],output,[code]); } } - private static async Task Snapshot(string a,string b,string c,CancellationToken ct) => [await One(a,ct),await One(b,ct),await One(c,ct)]; - private static async Task One(string path,CancellationToken ct) { var info=new FileInfo(path); return path+"|"+info.Length+"|"+info.LastWriteTimeUtc.Ticks+"|"+Convert.ToHexString(await SHA256.HashDataAsync(File.OpenRead(path),ct)); } + private static object SideEffects()=>new { sourceWrites=false,payloadCopied=false,stagingCreated=false,packageCommitted=false,activationAttempted=false,pointerMutationAttempted=false,autoCadLaunchAttempted=false,dfsWriteAttempted=false }; + private static bool SameOrNested(string child,string parent) { var c=Path.GetFullPath(child).TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar; var p=Path.GetFullPath(parent).TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar; return c.StartsWith(p,StringComparison.OrdinalIgnoreCase); } + private static async Task Snapshot(string a,string b,string c,CancellationToken ct) => [await One("LegacySettingsPath",a,ct),await One("LegacyDownloadListPath",b,ct),await One("LegacyCompatibilityOverlayPath",c,ct)]; + private static async Task One(string role,string path,CancellationToken ct) { var info=new FileInfo(path); if(info.Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("LEGACY_PROBE_REPARSE_SOURCE_REJECTED"); await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.Read); return string.Join('|',role,Path.GetFileName(path),info.Length,info.LastWriteTimeUtc.Ticks,Convert.ToHexString(await SHA256.HashDataAsync(stream,ct)),path); } + private static async Task ReadOnlyTextAsync(string path,CancellationToken ct) { await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.Read); using var reader=new StreamReader(stream,Encoding.UTF8,true); return await reader.ReadToEndAsync(ct); } + private static object Redact(string value,string operationId) { var parts=value.Split('|'); return new { role=parts[0],basename=parts[1],length=parts[2],lastWriteUtcTicks=parts[3],sha256=parts[4],pathFingerprint=Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(operationId+"|"+parts[5]))) }; } + private static async Task WriteEvidenceAsync(string root,string operationId,object evidence,CancellationToken ct) { var operation=Path.Combine(root,operationId); Directory.CreateDirectory(operation); var final=Path.Combine(operation,"legacy-configuration-probe.json"); var temp=final+".tmp"; await File.WriteAllTextAsync(temp,JsonSerializer.Serialize(evidence),ct); File.Move(temp,final,true); return final; } }