using System; using System.IO; using System.Linq; using System.Diagnostics; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Launcher.Next.Application; using Launcher.Next.Autodesk; using Launcher.Next.Core; using Launcher.Next.Infrastructure; namespace Launcher.Next.App; /// /// Explicit non-production workflow for a legacy-shaped configuration whose source, /// cache, activation state, starter and evidence all live under one supplied local root. /// It never accepts UNC roots or uses the legacy compatibility synchronizer. /// internal sealed record LocalParityLaunchProfile( int SchemaVersion, string Name, string Root, string SettingsPath, string DownloadListPath, string OverlayPath, string CatalogRoot, string PatternName, string EvidenceRoot, string? NetworkSettingsEndpoint = null, LegacySettingsSnapshot? SettingsSnapshot = null); internal sealed record LocalParityPackage(PackageRequirement Requirement, PackageManifest Manifest, ImmutableLocalPackage Package, PackageActivationResult Activation); internal sealed record LocalParityPrepared(LocalParityLaunchProfile Profile, IReadOnlyList Packages, string StarterPath, string StarterSha256); internal sealed record LocalParityLaunchResult(int ProcessId, bool StartupVerified, bool PackageLoadConfirmed, string EvidencePath, string TerminalState); internal sealed class LocalParityLaunchService { private readonly LegacyConfigurationAdapter adapter = new(); public static LocalParityLaunchProfile LoadProfile(string path) { var profile = JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("LOCAL_PARITY_PROFILE_INVALID"); if (profile.SchemaVersion != 1 || string.IsNullOrWhiteSpace(profile.Name) || string.IsNullOrWhiteSpace(profile.PatternName)) throw new InvalidOperationException("LOCAL_PARITY_PROFILE_INVALID"); ValidateProfile(profile); profile = profile with { Root = Path.GetFullPath(profile.Root) }; if (!string.IsNullOrWhiteSpace(profile.NetworkSettingsEndpoint)) { var snapshot = LegacyNetworkConfiguration.AcquireApprovedSnapshot(profile.NetworkSettingsEndpoint, profile.Root); profile = profile with { SettingsPath = snapshot.LocalSnapshotPath, SettingsSnapshot = snapshot }; } return profile; } public async Task PrepareAsync(LocalParityLaunchProfile profile, CancellationToken ct) { ValidateProfile(profile); var settings = adapter.ParseSettings(await File.ReadAllTextAsync(profile.SettingsPath, ct)); var pattern = settings.Patterns.Where(x => string.Equals(x.Name, profile.PatternName, StringComparison.Ordinal)).ToArray(); if (pattern.Length != 1) throw new InvalidOperationException(pattern.Length == 0 ? "LOCAL_PARITY_PATTERN_NOT_FOUND" : "LOCAL_PARITY_PATTERN_AMBIGUOUS"); var selection = adapter.ParseDownloadList(await File.ReadAllTextAsync(profile.DownloadListPath, ct)); var overlay = LegacyCompatibilityOverlayParser.Parse(await File.ReadAllTextAsync(profile.OverlayPath, ct)); var requirements = adapter.Join(pattern[0], selection, overlay); var store = new LocalPackageStore(profile.Root, new Sha256FileHasher()); var prepared = new List(); foreach (var requirement in requirements) { var source = Under(profile.CatalogRoot, requirement.PackageId, requirement.RequiredVersion); var manifestPath = Under(source, "package.manifest.json"); var manifest = PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath, ct)); if (!ManifestMatches(manifest, requirement)) throw new InvalidOperationException("LOCAL_PARITY_MANIFEST_REQUIREMENT_MISMATCH"); var package = await store.MaterializeAsync(manifest, source, ct); await store.BindAsync(requirement, package.Version, ct); var activation = await store.ActivateImmutableAsync(package, requirement, ct); if (activation.Outcome is not ("Activated" or "AlreadyActiveExact")) throw new InvalidOperationException("LOCAL_PARITY_ACTIVATION_" + activation.Outcome); if (await store.ResolveAsync(requirement, ct) is null) throw new InvalidOperationException("LOCAL_PARITY_ACTIVE_REVALIDATION_FAILED"); prepared.Add(new(requirement, manifest, package, activation)); } var starter = await GenerateStarterAsync(profile, prepared, ct); return new(profile, prepared, starter.Path, starter.Sha256); } public async Task LaunchAsync(LocalParityPrepared prepared, CancellationToken ct) { var product = new PlantProductDiscovery(new WindowsRegistryReader()).DiscoverPlant3D(2026) ?? throw new InvalidOperationException("PLANT3D_2026_NOT_FOUND"); var operationId = Guid.NewGuid().ToString("N"); var evidenceDirectory = Under(prepared.Profile.EvidenceRoot, operationId); Directory.CreateDirectory(evidenceDirectory); var logPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Promhimproekt", "PRecAnnot2026", "Logs", "runtime.jsonl"); var beforeLogLength = File.Exists(logPath) ? new FileInfo(logPath).Length : 0L; var psi = new ProcessStartInfo { FileName = product.ExecutablePath, WorkingDirectory = Path.GetDirectoryName(product.ExecutablePath)!, UseShellExecute = false }; psi.ArgumentList.Add("/product"); psi.ArgumentList.Add("PLNT3D"); psi.ArgumentList.Add("/language"); psi.ArgumentList.Add("ru-RU"); // Controlled-local only: accepted Plant runtime bootstrap. Keep /b and the owned SCR last. psi.ArgumentList.Add("/b"); psi.ArgumentList.Add(prepared.StarterPath); psi.Environment["P_REC_ANNOT_RUNTIME_MODE"] = "Smoke"; psi.Environment["P_REC_UPDATE_MARKER_ROOT"] = Path.Combine(Path.GetDirectoryName(prepared.StarterPath)!, "markers"); using var process = Process.Start(psi) ?? throw new InvalidOperationException("PLANT3D_PROCESS_NOT_CREATED"); await Task.Delay(TimeSpan.FromSeconds(30), ct); var alive = !process.HasExited; var marker = Path.Combine(Path.GetDirectoryName(prepared.StarterPath)!, "pxp-oa-netload.marker"); var markerConfirmed = File.Exists(marker); var runtimeConfirmed = false; if (File.Exists(logPath)) { await using var stream = new FileStream(logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); stream.Seek(beforeLogLength, SeekOrigin.Begin); using var reader = new StreamReader(stream, Encoding.UTF8); var newLog = await reader.ReadToEndAsync(ct); runtimeConfirmed = newLog.Contains("AssemblyInitializeCompleted", StringComparison.Ordinal) && newLog.Contains($"\"ProcessId\":{process.Id}", StringComparison.Ordinal); } var updatePackage = prepared.Packages.SingleOrDefault(x => string.Equals(x.Package.PackageId, "P_Rec_Update", StringComparison.Ordinal)); var updateInitialized = updatePackage is null || File.Exists(Path.Combine(Path.GetDirectoryName(prepared.StarterPath)!, "markers", "AssemblyInitializeCompleted.marker")); var loaded = runtimeConfirmed && updateInitialized; var terminal = alive && loaded ? "Succeeded" : "Blocked"; var settingsSnapshot = prepared.Profile.SettingsSnapshot is null ? null : new { sourceEndpointFingerprint = prepared.Profile.SettingsSnapshot.SourceFingerprint, prepared.Profile.SettingsSnapshot.Sha256, prepared.Profile.SettingsSnapshot.Length, prepared.Profile.SettingsSnapshot.LastWriteUtc, prepared.Profile.SettingsSnapshot.AcquiredUtc, prepared.Profile.SettingsSnapshot.OfflineFallback }; var evidence = new { schemaVersion = 3, launcherCommit = ThisCommit(), autodeskCommit = "68f5006", operationId, configuration = prepared.Profile.Name, selectedPattern = prepared.Profile.PatternName, settingsSnapshot, orderedPluginIds = prepared.Packages.Select(x => x.Requirement.PackageId), exactPackages = prepared.Packages.Select(x => new { x.Package.PackageId, x.Package.Version, x.Manifest.Product, x.Manifest.ProductYear, x.Manifest.Architecture, x.Manifest.RolloutChannel, manifestSha256 = HashAsync(Path.Combine(x.Package.LocalPackageRoot, "package.manifest.json"), ct).GetAwaiter().GetResult(), payloads = x.Manifest.Files.Select(f => new { path = f.Path, f.Length, f.Sha256 }), activation = x.Activation.Outcome, pointerGeneration = x.Activation.Receipt?.PointerGeneration }), materialization = "MaterializedOrAlreadyPresentExact", bootstrapMechanism="StartupScript", commandLineSwitch="/b", scriptWasLastArgument=true, scriptSha256=prepared.StarterSha256, plant = new { product = "Plant3D", year = 2026, executableFingerprint = Fingerprint(product.ExecutablePath, operationId), interactive = true, batchArgumentUsed = true }, process = new { pid = process.Id, newProcessCreated=true, startupVerified = alive, packageLoadConfirmed = loaded, markerConfirmed, runtimeJournalConfirmed = runtimeConfirmed, updateInitialized, processInteractiveAfterScript=alive }, terminalState = terminal, sideEffects = new { corporateDfsUsed = false, corporateDfsWriteAttempted = false, directDfsExecutionAttempted = false, productionProjectAccessed = false, productionPointerMutationAttempted = false, machineWideSecurityChanged=false, productionDeploymentAttempted = false } }; var evidencePath = await WriteEvidenceAsync(evidenceDirectory, evidence, ct); return new(process.Id, alive, loaded, evidencePath, terminal); } private static async Task<(string Path, string Sha256)> GenerateStarterAsync(LocalParityLaunchProfile profile, IReadOnlyList packages, CancellationToken ct) { var starterRoot = Under(profile.Root, "bootstrap", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(starterRoot); var entries = packages.Select(x => Under(x.Package.LocalPackageRoot, x.Manifest.EntryAssembly)).ToArray(); if (entries.Any(x => !File.Exists(x))) throw new InvalidOperationException("LOCAL_PARITY_ENTRY_MISSING"); Directory.CreateDirectory(Under(starterRoot, "markers")); var content = "_.FILEDIA\r\n0\r\n_.CMDDIA\r\n0\r\n" + string.Concat(entries.Select(entry => "_.NETLOAD\r\n\"" + entry.Replace("\"", "\\\"") + "\"\r\n")); var path = Under(starterRoot, "startup.scr"); await File.WriteAllTextAsync(path, content, new UTF8Encoding(false), ct); return (path, await HashAsync(path, ct)); } private static bool ManifestMatches(PackageManifest manifest, PackageRequirement requirement) => manifest.PackageId == requirement.PackageId && manifest.Version == requirement.RequiredVersion && manifest.Product == requirement.Product && manifest.ProductYear == requirement.ProductYear && string.Equals(manifest.Architecture, requirement.Architecture, StringComparison.OrdinalIgnoreCase) && string.Equals(manifest.RolloutChannel, requirement.Channel, StringComparison.OrdinalIgnoreCase); private static void ValidateProfile(LocalParityLaunchProfile profile) { if (profile.SchemaVersion != 1 || string.IsNullOrWhiteSpace(profile.Root)) throw new InvalidOperationException("LOCAL_PARITY_PROFILE_INVALID"); var root = Path.GetFullPath(profile.Root); if (!Path.IsPathFullyQualified(root) || root.StartsWith("\\\\", StringComparison.Ordinal) || root.Contains("?", StringComparison.Ordinal) || new DirectoryInfo(root).Exists && new DirectoryInfo(root).Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("LOCAL_PARITY_ROOT_NOT_LOCAL"); foreach (var path in new[] { profile.SettingsPath, profile.DownloadListPath, profile.OverlayPath, profile.CatalogRoot, profile.EvidenceRoot }) if (!UnderRoot(root, path)) throw new InvalidOperationException("LOCAL_PARITY_PATH_OUTSIDE_ROOT"); } private static bool UnderRoot(string root, string path) { var r = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; var p = Path.GetFullPath(path); return p.StartsWith(r, StringComparison.OrdinalIgnoreCase); } private static string Under(string root, params string[] parts) { var result = Path.GetFullPath(Path.Combine(new[] { root }.Concat(parts).ToArray())); if (!UnderRoot(root, result) && !string.Equals(Path.GetFullPath(root), result, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LOCAL_PARITY_PATH_ESCAPE"); return result; } private static async Task HashAsync(string path, CancellationToken ct) { await using var stream = File.OpenRead(path); return Convert.ToHexString(await SHA256.HashDataAsync(stream, ct)); } private static string Fingerprint(string path, string salt) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(salt + "|" + path))); private static async Task WriteEvidenceAsync(string directory, object value, CancellationToken ct) { var final = Path.Combine(directory, "controlled-local-parity-launch.json"); var temp = final + ".tmp"; await using (var stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { await JsonSerializer.SerializeAsync(stream, value, cancellationToken: ct); await stream.FlushAsync(ct); } File.Move(temp, final); using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(final, ct)); if (doc.RootElement.GetProperty("schemaVersion").GetInt32() != 3) throw new InvalidOperationException("LOCAL_PARITY_EVIDENCE_INVALID"); return final; } private static string ThisCommit() { var embedded = Environment.GetEnvironmentVariable("LAUNCHER_COMMIT"); if (!string.IsNullOrWhiteSpace(embedded)) return embedded; try { var releaseManifest = Path.Combine(AppContext.BaseDirectory, "owner-review-release.json"); if (File.Exists(releaseManifest)) { using var document = JsonDocument.Parse(File.ReadAllText(releaseManifest)); if (document.RootElement.TryGetProperty("launcherCommit", out var commit) && !string.IsNullOrWhiteSpace(commit.GetString())) return commit.GetString()!; } var start = new ProcessStartInfo("git", "rev-parse HEAD") { WorkingDirectory = AppContext.BaseDirectory, UseShellExecute = false, RedirectStandardOutput = true }; using var process = Process.Start(start); var value = process is null ? "" : process.StandardOutput.ReadToEnd().Trim(); return string.IsNullOrWhiteSpace(value) ? "unknown" : value; } catch { return "unknown"; } } }