refactor: resolve launcher paths and isolate batch automation
This commit is contained in:
@@ -28,14 +28,15 @@ internal static class PilotCli
|
||||
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 (!TryResolvePaths(config, out var paths, out error)) return Fail(Blocked, error!);
|
||||
if (!string.Equals(paths.LocalLauncherRoot, root, StringComparison.OrdinalIgnoreCase) || !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 (!File.Exists(paths.AcadExecutable)) return Fail(DiscoveryFailed, "PLANT3D_2026_NOT_FOUND");
|
||||
if (command == "run" && File.Exists(SessionPath(root)) && await HasActiveSession(root)) return await HandleExistingSession(root);
|
||||
var service = CreateService();
|
||||
var product = new ProductInstallation(@"C:\Program Files\Autodesk\AutoCAD 2026\acad.exe", "2026", "x64", []);
|
||||
var service = CreateService(paths);
|
||||
var product = new ProductInstallation(paths.AcadExecutable, "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));
|
||||
@@ -86,6 +87,7 @@ internal static class PilotCli
|
||||
{
|
||||
if (!TryParseAutomation(args, out var options, out var error)) return Fail(Blocked, error!);
|
||||
if (!TryReadConfiguration(options.PilotRoot, out var baseConfig, out error)) return Fail(Blocked, error!);
|
||||
if (!TryResolvePaths(baseConfig, out var basePaths, out error)) return Fail(Blocked, error!);
|
||||
if (baseConfig.LauncherMode != LauncherMode.NextPilot || !baseConfig.PilotEnabled || !baseConfig.AllowProcessStart || !baseConfig.RequireExplicitExecute || !baseConfig.ForceAfterEvidence) return Fail(Blocked, "AUTOMATION_SAFETY_GATE_REJECTED");
|
||||
if (!TryBuildManifest(options.PackageSource, options.EntryAssembly, out var manifest, out var manifestHash, out error)) return Fail(Blocked, error!);
|
||||
if (Process.GetProcessesByName("acad").Any()) return Fail(AcadRunning, "ACAD_ALREADY_RUNNING");
|
||||
@@ -95,7 +97,7 @@ internal static class PilotCli
|
||||
var previewEvidence = Path.Combine(options.EvidenceRoot, "preview-result.json");
|
||||
await File.WriteAllTextAsync(previewConfigPath, JsonSerializer.Serialize(new { phase = "Preview", orthoDrawingPath = options.OrthoDrawing, modelDrawingPath = options.ModelDrawing, evidencePath = previewEvidence, fixtureMarkerPath = options.FixtureMarker }));
|
||||
await File.WriteAllTextAsync(previewEvidence, "{\"completed\":false}");
|
||||
var previewConfig = baseConfig with { PluginExecutionMode = PluginExecutionMode.DryRun, AllowApply = false, ApplyPermitPath = null, AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = previewConfigPath };
|
||||
var previewConfig = WithAutomationPaths(baseConfig, options, previewConfigPath, previewEvidence, null, "preview") with { PluginExecutionMode = PluginExecutionMode.DryRun, AllowApply = false, ApplyPermitPath = null, AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = previewConfigPath, FixtureMarkerPath = options.FixtureMarker, LaunchMode = AcadLaunchMode.NextPilotPreview };
|
||||
var previewSession = await RunAutomationSession(previewConfig, manifest!, manifestHash!, options, "preview", previewEvidence);
|
||||
if (!previewSession.Started || !previewSession.RollbackVerified || !previewSession.EvidenceCompleted) return Fail(ProcessFailed, "AUTOMATION_PREVIEW_SESSION_FAILED");
|
||||
var preview = JsonSerializer.Deserialize<PreviewEvidence>(await File.ReadAllTextAsync(previewEvidence), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
@@ -107,7 +109,7 @@ internal static class PilotCli
|
||||
var applyEvidence = Path.Combine(options.EvidenceRoot, "apply-result.json");
|
||||
await File.WriteAllTextAsync(applyConfigPath, JsonSerializer.Serialize(new { phase = "Apply", orthoDrawingPath = options.OrthoDrawing, modelDrawingPath = options.ModelDrawing, evidencePath = applyEvidence, fixtureMarkerPath = options.FixtureMarker }));
|
||||
await File.WriteAllTextAsync(applyEvidence, "{\"completed\":false}");
|
||||
var applyConfig = baseConfig with { PluginExecutionMode = PluginExecutionMode.Apply, AllowApply = true, ApplyPermitPath = permitPath, AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = applyConfigPath };
|
||||
var applyConfig = WithAutomationPaths(baseConfig, options, applyConfigPath, applyEvidence, permitPath, "apply") with { PluginExecutionMode = PluginExecutionMode.Apply, AllowApply = true, ApplyPermitPath = permitPath, AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = applyConfigPath, FixtureMarkerPath = options.FixtureMarker, LaunchMode = AcadLaunchMode.NextPilotApply };
|
||||
var applySession = await RunAutomationSession(applyConfig, manifest!, manifestHash!, options, "apply", applyEvidence);
|
||||
var hashesEqual = HashesEqual(before, ContentHashes(options.FixtureMarker));
|
||||
var apply = File.Exists(applyEvidence) ? JsonSerializer.Deserialize<ApplyEvidence>(await File.ReadAllTextAsync(applyEvidence), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) : null;
|
||||
@@ -118,7 +120,8 @@ internal static class PilotCli
|
||||
|
||||
private static async Task<AutomationSessionResult> RunAutomationSession(PilotLocalConfiguration config, PackageManifest manifest, string manifestHash, AutomationOptions options, string phase, string evidencePath)
|
||||
{
|
||||
var service = CreateService(); var product = new ProductInstallation(@"C:\Program Files\Autodesk\AutoCAD 2026\acad.exe", "2026", "x64", []);
|
||||
if (!TryResolvePaths(config, out var paths, out _)) return new(false, false, false, false, false);
|
||||
var service = CreateService(paths); var product = new ProductInstallation(paths.AcadExecutable, "2026", "x64", []);
|
||||
var request = new PilotPreparationRequest(config, product, manifest, manifestHash, options.PackageSource, options.PilotRoot, Path.Combine(options.PilotRoot, "startup", phase + ".scr"));
|
||||
var prepared = await service.PrepareAsync(request, CancellationToken.None); if (prepared.Plan is null) return new(false, false, false, false, false);
|
||||
var execution = await service.ExecuteAsync(new(request, prepared.Plan, PermitService.Issue(prepared.Plan, config.ExecutionPermitLifetimeSeconds)), CancellationToken.None); if (!execution.Started || execution.ProcessId is null) return new(false, false, false, false, false);
|
||||
@@ -166,7 +169,7 @@ internal static class PilotCli
|
||||
}
|
||||
private static Dictionary<string,string> ContentHashes(string marker) { var root = Path.GetDirectoryName(marker)!; return Directory.GetFiles(root, "*", SearchOption.AllDirectories).Where(x => x.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".dcf", StringComparison.OrdinalIgnoreCase) || Path.GetFileName(x).Equals("Project.xml", StringComparison.OrdinalIgnoreCase)).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToDictionary(x => Path.GetRelativePath(root,x), x => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(x))), StringComparer.OrdinalIgnoreCase); }
|
||||
private static bool HashesEqual(IReadOnlyDictionary<string,string> x, IReadOnlyDictionary<string,string> y) => x.Count == y.Count && x.All(p => y.TryGetValue(p.Key, out var value) && value == p.Value);
|
||||
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 PilotExecutionService CreateService(ResolvedLauncherPaths paths) => new(new PackageManifestValidator(), new PilotRootPolicy(paths.LocalLauncherRoot), new NextPilotSafetyGate(), new Sha256FileHasher(), new StartupScriptBuilder(), PermitService, new Probe(), new OperatingSystemProcessInvoker(), new PathResolver());
|
||||
private static async Task WaitForExitAsync(int processId) { using var process = Process.GetProcessById(processId); await process.WaitForExitAsync(); }
|
||||
private static bool HasCompletedEvidence(string path)
|
||||
{
|
||||
@@ -175,7 +178,8 @@ internal static class PilotCli
|
||||
}
|
||||
private static async Task<int> RollbackAsync(string root)
|
||||
{
|
||||
var result = await CreateService().RollbackAsync(new(root, "P_Rec_Annot", null, string.Empty, null), CancellationToken.None);
|
||||
if (!TryReadConfiguration(root, out var config, out _) || !TryResolvePaths(config, out var paths, out _)) return Fail(ExplicitRollbackFailed, "PATH_CONFIGURATION_INVALID");
|
||||
var result = await CreateService(paths).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<int> StatusAsync(string root)
|
||||
@@ -207,12 +211,38 @@ internal static class PilotCli
|
||||
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;
|
||||
config = PilotLocalConfiguration.SafeDefault; error = null; var directory = Path.Combine(root, "config"); var defaults = Path.Combine(directory, "launcher.defaults.json"); var machine = Path.Combine(directory, "launcher.machine.json"); var session = Path.Combine(directory, "launcher.session.json");
|
||||
if (!File.Exists(machine)) { error = "PILOT_CONFIGURATION_MISSING"; return false; }
|
||||
var parsed = LauncherConfigurationLayers.LoadFromFiles(defaults, machine, session); config = parsed.Configuration;
|
||||
if (!parsed.IsValid) { error = "PILOT_CONFIGURATION_INVALID"; return false; }
|
||||
return true;
|
||||
}
|
||||
private static bool TryResolvePaths(PilotLocalConfiguration configuration, out ResolvedLauncherPaths paths, out string? error)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolver = new PathResolver(); var variables = new LauncherPathVariables(configuration.PathVariables); var raw = resolver.Resolve(variables, [LauncherPathVariableNames.LocalLauncherRoot, LauncherPathVariableNames.AcadExecutable]);
|
||||
var resolved = new Dictionary<string, string?>(raw.Values, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in new[] { LauncherPathVariableNames.LocalLauncherRoot, LauncherPathVariableNames.LocalPackageCache, LauncherPathVariableNames.LocalActiveRoot, LauncherPathVariableNames.LocalSessionRoot, LauncherPathVariableNames.LocalEvidenceRoot, LauncherPathVariableNames.LocalLogsRoot, LauncherPathVariableNames.LocalRollbackRoot, LauncherPathVariableNames.AcadExecutable, LauncherPathVariableNames.PRecEntryAssembly, LauncherPathVariableNames.FixtureOrthoDwg, LauncherPathVariableNames.AutomationScriptPath, LauncherPathVariableNames.AutomationConfigPath, LauncherPathVariableNames.AutomationEvidencePath, LauncherPathVariableNames.AutomationPermitPath })
|
||||
if (raw.Get(name) is { Length: > 0 } value) resolved[name] = resolver.ResolvePath(value, variables);
|
||||
paths = new(resolved); error = null; return true;
|
||||
}
|
||||
catch (Exception ex) { paths = null!; error = ex.Message; return false; }
|
||||
}
|
||||
private static PilotLocalConfiguration WithAutomationPaths(PilotLocalConfiguration config, AutomationOptions options, string configPath, string evidencePath, string? permitPath, string phase)
|
||||
{
|
||||
var variables = new Dictionary<string, string?>(config.PathVariables, StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[LauncherPathVariableNames.LocalSessionRoot] = options.EvidenceRoot,
|
||||
[LauncherPathVariableNames.AutomationScriptPath] = Path.Combine(options.EvidenceRoot, phase + ".scr"),
|
||||
[LauncherPathVariableNames.AutomationConfigPath] = configPath,
|
||||
[LauncherPathVariableNames.AutomationEvidencePath] = evidencePath,
|
||||
[LauncherPathVariableNames.AutomationPermitPath] = permitPath,
|
||||
[LauncherPathVariableNames.FixtureOrthoDwg] = options.OrthoDrawing,
|
||||
[LauncherPathVariableNames.PRecEntryAssembly] = options.EntryAssembly
|
||||
};
|
||||
return config with { PathVariables = variables };
|
||||
}
|
||||
private static bool TryBuildManifest(string source, string entry, out PackageManifest? manifest, out string? hash, out string? error)
|
||||
{
|
||||
manifest = null; hash = null; error = null;
|
||||
|
||||
Reference in New Issue
Block a user