using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Launcher.Next.Autodesk; using Launcher.Next.Core; using Launcher.Next.Infrastructure; namespace Launcher.Next.App; internal static class PilotCli { private const int Success = 0, Blocked = 2, DiscoveryFailed = 3, AcadRunning = 4, SessionConflict = 5, ProcessFailed = 6, RollbackAfterExitFailed = 7, ExplicitRollbackFailed = 8, Unexpected = 10; public static async Task RunAsync(string[] args) { try { if (args.Length == 0 || args[0] is not ("prepare" or "run" or "status" or "rollback")) return Blocked; var command = args[0]; if (!TryParse(command, args.Skip(1).ToArray(), out var options, out var error)) return Fail(Blocked, error!); var root = options.PilotRoot!; if (command == "status") return await StatusAsync(root); if (command == "rollback") return await RollbackAsync(root); if (!TryReadConfiguration(root, out var config, out error)) return Fail(Blocked, error!); if (!string.Equals(config.PilotRoot, root, StringComparison.OrdinalIgnoreCase)) return Fail(Blocked, "PILOT_ROOT_CONFIGURATION_MISMATCH"); if (!TryBuildManifest(options.PackageSource!, options.EntryAssembly!, out var manifest, out var manifestHash, out error)) return Fail(Blocked, error!); Directory.CreateDirectory(Path.Combine(root, "catalog")); await File.WriteAllTextAsync(Path.Combine(root, "catalog", "P_Rec_Annot.manifest.json"), JsonSerializer.Serialize(manifest)); if (!File.Exists(@"C:\Program Files\Autodesk\AutoCAD 2026\acad.exe")) return Fail(DiscoveryFailed, "PLANT3D_2026_NOT_FOUND"); if (command == "run" && File.Exists(SessionPath(root))) return await HandleExistingSession(root); var service = CreateService(); var product = new ProductInstallation(@"C:\Program Files\Autodesk\AutoCAD 2026\acad.exe", "2026", "x64", []); var request = new PilotPreparationRequest(config, product, manifest!, manifestHash!, options.PackageSource!, root, Path.Combine(root, "startup", "prepare-only.scr")); var prepared = await service.PrepareAsync(request, CancellationToken.None); if (prepared.Plan is null) return Fail(prepared.SafetyDecision.Violations.Contains("ACAD_ALREADY_RUNNING") ? AcadRunning : Blocked, string.Join(',', prepared.SafetyDecision.Violations)); PrintPlan(prepared.Plan); if (command == "prepare") return Success; var sessionScript = Path.Combine(root, "startup", Guid.NewGuid().ToString("N") + ".scr"); request = request with { StartupScriptPath = sessionScript }; prepared = await service.PrepareAsync(request, CancellationToken.None); if (prepared.Plan is null) return Fail(prepared.SafetyDecision.Violations.Contains("ACAD_ALREADY_RUNNING") ? AcadRunning : Blocked, string.Join(',', prepared.SafetyDecision.Violations)); var permits = PermitService; var permit = permits.Issue(prepared.Plan, config.ExecutionPermitLifetimeSeconds); var execution = await service.ExecuteAsync(new(request, prepared.Plan, permit), CancellationToken.None); if (!execution.Started) return Fail(execution.BlockCode == "ACAD_ALREADY_RUNNING" ? AcadRunning : ProcessFailed, execution.BlockCode ?? "PROCESS_START_FAILED"); Console.WriteLine($"Plant 3D запущен. PID: {execution.ProcessId}. P_Rec_Annot загружается через startup script."); if (!options.WaitForExit) return Success; try { await WaitForExitAsync(execution.ProcessId!.Value); Console.WriteLine("Plant 3D завершён штатно."); } catch (Exception ex) { Console.Error.WriteLine($"PROCESS_WAIT_FAILED: {ex.Message}"); return ProcessFailed; } finally { if (options.RollbackAfterExit) { var rollback = await service.RollbackAsync(new(root, manifest!.PackageId, manifest.Version, execution.StartupScriptPath!, null, execution.SessionId, execution.ActivationSnapshotPath), CancellationToken.None); if (!rollback.Completed) Console.Error.WriteLine(rollback.ErrorCode ?? "ROLLBACK_FAILED"); } } return await IsRolledBack(root) ? Success : RollbackAfterExitFailed; } catch (Exception ex) { return Fail(Unexpected, ex.Message); } } private static readonly IExecutionPermitService PermitService = new ExecutionPermitService(); private static PilotExecutionService CreateService() => new(new PackageManifestValidator(), new PilotRootPolicy(@"C:\AutoCAD User Support"), new NextPilotSafetyGate(), new Sha256FileHasher(), new StartupScriptBuilder(), PermitService, new Probe(), new OperatingSystemProcessInvoker()); private static async Task WaitForExitAsync(int processId) { using var process = Process.GetProcessById(processId); await process.WaitForExitAsync(); } private static async Task RollbackAsync(string root) { var result = await CreateService().RollbackAsync(new(root, "P_Rec_Annot", null, string.Empty, null), CancellationToken.None); return result.Completed ? Success : Fail(ExplicitRollbackFailed, result.ErrorCode ?? "ROLLBACK_FAILED"); } private static async Task StatusAsync(string root) { var session = SessionPath(root); if (!File.Exists(session)) { Console.WriteLine("State: Clean"); return Success; } using var document = JsonDocument.Parse(await File.ReadAllTextAsync(session)); var state = document.RootElement.GetProperty("State").GetString() ?? "Unknown"; Console.WriteLine($"State: {state}"); return state is "RolledBack" ? Success : SessionConflict; } private static async Task HandleExistingSession(string root) { using var document = JsonDocument.Parse(await File.ReadAllTextAsync(SessionPath(root))); var state = document.RootElement.GetProperty("State").GetString(); return state == "RolledBack" ? Success : Fail(SessionConflict, "PILOT_SESSION_ALREADY_ACTIVE"); } private static async Task IsRolledBack(string root) { if (!File.Exists(SessionPath(root))) return false; using var document = JsonDocument.Parse(await File.ReadAllTextAsync(SessionPath(root))); return document.RootElement.GetProperty("State").GetString() == "RolledBack"; } private static string SessionPath(string root) => Path.Combine(root, "state", "pilot-session.json"); private static bool TryReadConfiguration(string root, out PilotLocalConfiguration config, out string? error) { config = PilotLocalConfiguration.SafeDefault; error = null; var path = Path.Combine(root, "config", "pilot.local.json"); if (!File.Exists(path)) { error = "PILOT_CONFIGURATION_MISSING"; return false; } var parsed = PilotConfigurationParser.Parse(File.ReadAllText(path)); config = parsed.Configuration; if (!parsed.IsValid) { error = "PILOT_CONFIGURATION_INVALID"; return false; } return true; } private static bool TryBuildManifest(string source, string entry, out PackageManifest? manifest, out string? hash, out string? error) { manifest = null; hash = null; error = null; if (!Directory.Exists(source) || !File.Exists(Path.Combine(source, entry))) { error = "PACKAGE_SOURCE_INVALID"; return false; } var list = new List(); foreach (var path in Directory.GetFiles(source, "*", SearchOption.AllDirectories)) { var relative = Path.GetRelativePath(source, path); var name = Path.GetFileName(relative); if (relative.StartsWith("..") || IsForbidden(name)) { error = "PACKAGE_FILE_FORBIDDEN"; return false; } using var stream = File.OpenRead(path); var bytes = SHA256.HashData(stream); list.Add(new(relative, new FileInfo(path).Length, Convert.ToHexString(bytes))); } var version = "0.1.0-pilot-ea3a9e1"; try { version = System.Reflection.AssemblyName.GetAssemblyName(Path.Combine(source, entry)).Version?.ToString() ?? version; } catch { } manifest = new PackageManifest(1, "P_Rec_Annot", version, "Plant3D", 2026, "x64", entry, list.OrderBy(x => x.Path, StringComparer.OrdinalIgnoreCase).ToArray(), new("netload", "PHP_REC_ANNOT"), "0.1.0", ["R25.1"], "net8.0-windows", null, null, "pilot", false, true); var json = JsonSerializer.Serialize(manifest); hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json))); return true; } private static bool IsForbidden(string name) => name.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase) || name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) || name.Contains("test", StringComparison.OrdinalIgnoreCase) || name.StartsWith("Ac", StringComparison.OrdinalIgnoreCase) || name.StartsWith("PnP", StringComparison.OrdinalIgnoreCase) || name.StartsWith("Autodesk.ProcessPower", StringComparison.OrdinalIgnoreCase); private static void PrintPlan(PilotLaunchPlan plan) => Console.WriteLine($"Plan={plan.PlanId}; Package={plan.PackageId}/{plan.PackageVersion}; NETLOAD=1; Args={string.Join(' ', plan.Process.Arguments)}; /p=false"); private static int Fail(int code, string error) { Console.Error.WriteLine(error); return code; } private sealed record Options(string? PackageSource, string? EntryAssembly, string? PilotRoot, bool WaitForExit, bool RollbackAfterExit); private static bool TryParse(string command, string[] args, out Options options, out string? error) { options = new(null, null, null, false, false); error = null; var values = new Dictionary(StringComparer.OrdinalIgnoreCase); var wait = false; var rollback = false; for (var i = 0; i < args.Length; i++) { var token = args[i]; if (token is "--wait-for-exit" or "--rollback-after-exit") { if (command != "run" || (token == "--wait-for-exit" ? wait : rollback)) { error = "PILOT_ARGUMENT_INVALID"; return false; } if (token == "--wait-for-exit") wait = true; else rollback = true; continue; } if (token is not ("--package-source" or "--entry-assembly" or "--pilot-root") || i + 1 >= args.Length || args[i + 1].StartsWith("--") || !values.TryAdd(token, args[++i])) { error = "PILOT_ARGUMENT_INVALID"; return false; } } if (rollback && !wait) { error = "ROLLBACK_AFTER_EXIT_REQUIRES_WAIT"; return false; } if (!values.TryGetValue("--pilot-root", out var root) || !Path.IsPathFullyQualified(root) || root.StartsWith("\\\\")) { error = "PILOT_ROOT_INVALID"; return false; } if (command is "prepare" or "run") { if (!values.TryGetValue("--package-source", out var source) || source is null || !Path.IsPathFullyQualified(source) || source.StartsWith("\\\\") || Path.GetFullPath(source).StartsWith(Path.GetFullPath(root) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) { error = "PACKAGE_SOURCE_INVALID"; return false; } if (!values.TryGetValue("--entry-assembly", out var entry) || entry is null || !string.Equals(Path.GetFileName(entry), entry, StringComparison.Ordinal) || entry.Contains("..", StringComparison.Ordinal)) { error = "ENTRY_ASSEMBLY_INVALID"; return false; } } else if (values.Keys.Any(x => x != "--pilot-root")) { error = "PILOT_ARGUMENT_INVALID"; return false; } options = new(values.GetValueOrDefault("--package-source"), values.GetValueOrDefault("--entry-assembly"), root, wait, rollback); return true; } private sealed class Probe : IAutoCadProcessProbe { public bool IsAcadRunning() => Process.GetProcessesByName("acad").Any(); } }