refactor: resolve launcher paths and isolate batch automation
This commit is contained in:
@@ -13,7 +13,8 @@ public sealed class PilotExecutionService(
|
||||
IStartupScriptBuilder scriptBuilder,
|
||||
IExecutionPermitService permits,
|
||||
IAutoCadProcessProbe probe,
|
||||
IOperatingSystemProcessInvoker invoker) : IPilotExecutionService
|
||||
IOperatingSystemProcessInvoker invoker,
|
||||
IPathResolver? pathResolver = null) : IPilotExecutionService
|
||||
{
|
||||
private sealed record Pointer(string Version);
|
||||
private sealed record ActivationSnapshot(
|
||||
@@ -25,6 +26,7 @@ public sealed class PilotExecutionService(
|
||||
int SchemaVersion, string SessionId, string TransactionId, string State,
|
||||
string PackageId, string PackageVersion, string ManifestSha256,
|
||||
string PilotRoot, string VersionDirectory, string StartupScriptPath,
|
||||
string LaunchMode, IReadOnlyDictionary<string, string?> ResolvedPaths, IReadOnlyList<string> AcadArguments, string? ActualStartupScriptSha256,
|
||||
string ActivationSnapshotPath, int? ProcessId, DateTimeOffset? ProcessStartUtc,
|
||||
int? ProcessExitCode, DateTimeOffset CreatedAtUtc, DateTimeOffset UpdatedAtUtc,
|
||||
string? LastErrorCode, string? LastErrorMessage);
|
||||
@@ -32,28 +34,30 @@ public sealed class PilotExecutionService(
|
||||
public async Task<PilotPreparationResult> PrepareAsync(PilotPreparationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var root = rootPolicy.Validate(request.PilotRoot);
|
||||
var resolver = pathResolver ?? new PathResolver();
|
||||
var paths = resolver.Resolve(new LauncherPathVariables(request.Configuration.PathVariables), [LauncherPathVariableNames.LocalLauncherRoot, LauncherPathVariableNames.AcadExecutable]);
|
||||
if (!string.Equals(Path.GetFullPath(request.PilotRoot), paths.LocalLauncherRoot, StringComparison.OrdinalIgnoreCase))
|
||||
return new(null, new(false, ["PILOT_ROOT_PATH_VARIABLE_MISMATCH"]));
|
||||
var root = rootPolicy.Validate(paths.LocalLauncherRoot);
|
||||
var validation = validator.Validate(request.Manifest, 2026, "x64");
|
||||
var hashes = await ValidateSource(request.Manifest, request.PackageSourceRoot, cancellationToken);
|
||||
var product = request.Product with { ExecutablePath = paths.AcadExecutable };
|
||||
var profile = new AutoCadProfileCandidate(string.Empty, false, true, ["Confirmed Plant 3D launch contract has no /p argument."]);
|
||||
var safety = gate.Evaluate(new(request.Configuration, request.Product, profile, root, validation, hashes, false, probe.IsAcadRunning()), false);
|
||||
var safety = gate.Evaluate(new(request.Configuration, product, profile, root, validation, hashes, false, probe.IsAcadRunning()), false);
|
||||
if (!safety.IsAllowed) return new(null, safety);
|
||||
|
||||
var futureEntry = Path.Combine(request.PilotRoot, "packages", request.Manifest.PackageId, "versions", request.Manifest.Version, request.Manifest.EntryAssembly);
|
||||
var fixture = request.Configuration.AutomationMode is null ? null : ReadFixtureOrtho(request.Configuration.AutomationConfigurationPath);
|
||||
var content = request.Configuration.AutomationMode is null
|
||||
? $"_.NETLOAD\n\"{futureEntry.Replace("\"", "\\\"")}\"\n"
|
||||
: $"_.OPEN\n\"{fixture!.Replace("\"", "\\\"")}\"\n_.NETLOAD\n\"{futureEntry.Replace("\"", "\\\"")}\"\nPHP_REC_AUTOMATION_PREPARE\nPXP-OA\nPXP-OA\nPHP_REC_AUTOMATION_FINALIZE\n";
|
||||
var automation = request.Configuration.LaunchMode is AcadLaunchMode.NextPilotPreview or AcadLaunchMode.NextPilotApply or AcadLaunchMode.LifecycleProbe;
|
||||
var content = automation ? "AUTOMATION_SCRIPT_PENDING" : "PRODUCTION_NO_SCRIPT";
|
||||
var scriptHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content)));
|
||||
var plan = PilotPlanFactory.Create(request.Product, profile, request.Manifest, request.ManifestHash, request.PilotRoot, request.StartupScriptPath, scriptHash, safety);
|
||||
var plan = PilotPlanFactory.Create(product, profile, request.Manifest, request.ManifestHash, paths.LocalLauncherRoot, automation ? paths.Require(LauncherPathVariableNames.AutomationScriptPath) : string.Empty, scriptHash, safety, request.Configuration.LaunchMode);
|
||||
var environment = new Dictionary<string, string> { ["P_REC_ANNOT_RUNTIME_MODE"] = request.Configuration.PluginExecutionMode.ToString() };
|
||||
if (!string.IsNullOrWhiteSpace(request.Configuration.ApplyPermitPath))
|
||||
environment["P_REC_ANNOT_APPLY_PERMIT_PATH"] = request.Configuration.ApplyPermitPath;
|
||||
if (request.Configuration.AutomationMode is not null)
|
||||
if (automation)
|
||||
{
|
||||
environment["P_REC_ANNOT_LAUNCHER_MODE"] = "NextPilot";
|
||||
environment["P_REC_ANNOT_AUTOMATION"] = request.Configuration.AutomationMode;
|
||||
environment["P_REC_ANNOT_AUTOMATION_CONFIG"] = request.Configuration.AutomationConfigurationPath!;
|
||||
environment["P_REC_ANNOT_AUTOMATION"] = request.Configuration.AutomationMode!;
|
||||
environment["P_REC_ANNOT_AUTOMATION_CONFIG"] = paths.Require(LauncherPathVariableNames.AutomationConfigPath);
|
||||
}
|
||||
return new(plan with { Process = plan.Process with { EnvironmentVariables = environment } }, safety);
|
||||
}
|
||||
@@ -61,6 +65,8 @@ public sealed class PilotExecutionService(
|
||||
public async Task<PilotExecutionResult> ExecuteAsync(PilotExecutionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var plan = request.Plan;
|
||||
var resolver = pathResolver ?? new PathResolver();
|
||||
var resolvedPaths = resolver.Resolve(new LauncherPathVariables(request.Preparation.Configuration.PathVariables));
|
||||
var root = rootPolicy.Validate(request.Preparation.PilotRoot);
|
||||
var validation = validator.Validate(request.Preparation.Manifest, 2026, "x64");
|
||||
var hashes = await ValidateSource(request.Preparation.Manifest, request.Preparation.PackageSourceRoot, cancellationToken);
|
||||
@@ -68,7 +74,7 @@ public sealed class PilotExecutionService(
|
||||
new AutoCadProfileCandidate(string.Empty, false, true, ["No profile in confirmed contract."]), root, validation, hashes, false, probe.IsAcadRunning());
|
||||
var decision = gate.Evaluate(context, true);
|
||||
if (!decision.IsAllowed) return new(false, null, decision.Violations.First(), null, null);
|
||||
if (!string.Equals(plan.PlanId, PilotPlanFactory.Create(request.Preparation.Product, context.Profile!, request.Preparation.Manifest, request.Preparation.ManifestHash, request.Preparation.PilotRoot, request.Preparation.StartupScriptPath, plan.StartupScriptSha256, plan.SafetyDecision).PlanId, StringComparison.Ordinal))
|
||||
if (!string.Equals(plan.PlanId, PilotPlanFactory.Create(plan.Product, context.Profile!, request.Preparation.Manifest, request.Preparation.ManifestHash, plan.PilotRoot, plan.StartupScriptPath, plan.StartupScriptSha256, plan.SafetyDecision, request.Preparation.Configuration.LaunchMode).PlanId, StringComparison.Ordinal))
|
||||
return new(false, null, "PILOT_PLAN_CHANGED", null, null);
|
||||
|
||||
var sessionId = Guid.NewGuid().ToString("N");
|
||||
@@ -76,7 +82,7 @@ public sealed class PilotExecutionService(
|
||||
var packageRoot = Path.Combine(plan.PilotRoot, "packages", plan.PackageId);
|
||||
var versionDirectory = plan.VersionDirectory;
|
||||
var staging = Path.Combine(plan.PilotRoot, "staging", transactionId);
|
||||
var scriptPath = Path.Combine(plan.PilotRoot, "startup", sessionId + ".scr");
|
||||
var scriptPath = plan.StartupScriptPath;
|
||||
var snapshotPath = Path.Combine(plan.PilotRoot, "state", "transactions", transactionId + ".json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(snapshotPath)!);
|
||||
var currentPath = Path.Combine(packageRoot, "current.json");
|
||||
@@ -85,7 +91,7 @@ public sealed class PilotExecutionService(
|
||||
var previous = await ReadTextOrNull(previousPath, cancellationToken);
|
||||
var snapshot = new ActivationSnapshot(transactionId, plan.PackageId, plan.PackageVersion, current, Hash(current), previous, Hash(previous), versionDirectory, scriptPath, staging);
|
||||
await WriteJsonAtomic(snapshotPath, snapshot, cancellationToken);
|
||||
await WriteSession(plan.PilotRoot, new PersistedSession(1, sessionId, transactionId, "Activating", plan.PackageId, plan.PackageVersion, plan.ManifestSha256, plan.PilotRoot, versionDirectory, scriptPath, snapshotPath, null, null, null, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, null, null), cancellationToken);
|
||||
await WriteSession(plan.PilotRoot, new PersistedSession(2, sessionId, transactionId, "Activating", plan.PackageId, plan.PackageVersion, plan.ManifestSha256, plan.PilotRoot, versionDirectory, scriptPath, request.Preparation.Configuration.LaunchMode.ToString(), resolvedPaths.Values, plan.Process.Arguments, null, snapshotPath, null, null, null, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, null, null), cancellationToken);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(staging);
|
||||
@@ -103,13 +109,20 @@ public sealed class PilotExecutionService(
|
||||
if (Directory.Exists(versionDirectory)) Directory.Delete(staging, true); else Directory.Move(staging, versionDirectory);
|
||||
await WriteTextAtomic(previousPath, current ?? string.Empty, cancellationToken, current is not null);
|
||||
await WriteTextAtomic(currentPath, JsonSerializer.Serialize(new Pointer(plan.PackageVersion)), cancellationToken, true);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!);
|
||||
var script = request.Preparation.Configuration.AutomationMode is null
|
||||
? scriptBuilder.BuildNetloadScript(versionDirectory, request.Preparation.Manifest.EntryAssembly)
|
||||
: scriptBuilder.BuildAutomationScript(versionDirectory, request.Preparation.Manifest.EntryAssembly, ReadFixtureOrtho(request.Preparation.Configuration.AutomationConfigurationPath));
|
||||
await File.WriteAllTextAsync(scriptPath, script, cancellationToken);
|
||||
await WriteSession(plan.PilotRoot, (await ReadSession(plan.PilotRoot, cancellationToken))! with { State = "ProcessStarting", UpdatedAtUtc = DateTimeOffset.UtcNow }, cancellationToken);
|
||||
var result = new GuardedAutodeskProcessLauncher(gate, probe, invoker).Start(context, plan with { StartupScriptPath = scriptPath, Process = plan.Process with { Arguments = ["/product", "PLNT3D", "/language", "ru-RU", "/b", scriptPath], WorkingDirectory = plan.PilotRoot } }, request.Permit, permits);
|
||||
var automation = request.Preparation.Configuration.LaunchMode is AcadLaunchMode.NextPilotPreview or AcadLaunchMode.NextPilotApply or AcadLaunchMode.LifecycleProbe;
|
||||
if (automation)
|
||||
{
|
||||
var paths = resolver.Resolve(new LauncherPathVariables(request.Preparation.Configuration.PathVariables), [LauncherPathVariableNames.AutomationScriptPath, LauncherPathVariableNames.FixtureOrthoDwg, LauncherPathVariableNames.LocalSessionRoot]);
|
||||
scriptPath = paths.Require(LauncherPathVariableNames.AutomationScriptPath);
|
||||
if (!Path.GetFullPath(scriptPath).StartsWith(Path.GetFullPath(paths.Require(LauncherPathVariableNames.LocalSessionRoot)).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("AUTOMATION_SCRIPT_OUTSIDE_SESSION_ROOT");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!);
|
||||
var script = scriptBuilder.BuildAutomationScript(new(versionDirectory, request.Preparation.Manifest.EntryAssembly, paths.Require(LauncherPathVariableNames.FixtureOrthoDwg), scriptPath));
|
||||
await File.WriteAllTextAsync(scriptPath, script, cancellationToken);
|
||||
}
|
||||
var arguments = new AcadArgumentBuilder().Build(request.Preparation.Configuration.LaunchMode, automation ? scriptPath : null);
|
||||
var actualScriptHash = automation ? Convert.ToHexString(SHA256.HashData(await File.ReadAllBytesAsync(scriptPath, cancellationToken))) : null;
|
||||
await WriteSession(plan.PilotRoot, (await ReadSession(plan.PilotRoot, cancellationToken))! with { State = "ProcessStarting", StartupScriptPath = automation ? scriptPath : string.Empty, AcadArguments = arguments, ActualStartupScriptSha256 = actualScriptHash, UpdatedAtUtc = DateTimeOffset.UtcNow }, cancellationToken);
|
||||
var result = new GuardedAutodeskProcessLauncher(gate, probe, invoker).Start(context, plan with { StartupScriptPath = automation ? scriptPath : string.Empty, Process = plan.Process with { Arguments = arguments, WorkingDirectory = plan.PilotRoot } }, request.Permit, permits);
|
||||
if (!result.Started)
|
||||
{
|
||||
await MarkFailed(plan.PilotRoot, result.BlockCode ?? "PROCESS_START_FAILED", cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user