using System.Diagnostics; using System.Text.Json; var options = BridgeOptions.Parse(args); Directory.CreateDirectory(options.QueueDir); Directory.CreateDirectory(Path.Combine(options.QueueDir, "jobs")); Directory.CreateDirectory(Path.Combine(options.QueueDir, "results")); Console.WriteLine($"AutoCadDwgBridge queue: {options.QueueDir}"); Console.WriteLine($"Extractor: {options.ExtractorDll}"); Console.WriteLine("Press Ctrl+C to stop."); var stopping = false; Console.CancelKeyPress += (_, eventArgs) => { eventArgs.Cancel = true; stopping = true; }; while (!stopping) { WriteHeartbeat(options); ProcessJobs(options); Thread.Sleep(options.PollMs); } static void ProcessJobs(BridgeOptions options) { var jobsDir = Path.Combine(options.QueueDir, "jobs"); foreach (var requestPath in Directory.GetFiles(jobsDir, "*.request.json").OrderBy(File.GetCreationTimeUtc)) { var runningPath = Path.ChangeExtension(requestPath, ".running.json"); try { File.Move(requestPath, runningPath); } catch { continue; } BridgeJob? job = null; try { job = JsonSerializer.Deserialize(File.ReadAllText(runningPath), JsonDefaults.Options) ?? throw new InvalidOperationException($"Invalid bridge job: {runningPath}"); var result = RunJob(options, job); WriteResult(options, job.JobId, result); File.Delete(runningPath); } catch (Exception ex) { var jobId = job?.JobId ?? Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(runningPath)); WriteResult(options, jobId, new BridgeJobResult { Ok = false, JobId = jobId, ExitCode = -1, Error = ex.ToString() }); TryMove(runningPath, Path.ChangeExtension(runningPath, ".failed.json")); } } } static BridgeJobResult RunJob(BridgeOptions options, BridgeJob job) { if (string.IsNullOrWhiteSpace(job.JobId)) throw new InvalidOperationException("Bridge job missing job_id."); if (!File.Exists(options.ExtractorDll)) throw new FileNotFoundException($"AutoCadDwgExtractor.dll not found: {options.ExtractorDll}"); if (string.IsNullOrWhiteSpace(job.DrawingPath) || !File.Exists(job.DrawingPath)) throw new FileNotFoundException($"DWG not found: {job.DrawingPath}"); if (string.IsNullOrWhiteSpace(job.OutputDir)) throw new InvalidOperationException("Bridge job missing output_dir."); Directory.CreateDirectory(job.OutputDir); var startInfo = new ProcessStartInfo { FileName = "dotnet", WorkingDirectory = options.AgentRoot, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; startInfo.ArgumentList.Add(options.ExtractorDll); AddArg(startInfo, "--drawing-path", job.DrawingPath); AddArg(startInfo, "--output-dir", job.OutputDir); AddArg(startInfo, "--prog-id", string.IsNullOrWhiteSpace(job.ProgId) ? options.ProgId : job.ProgId); AddArg(startInfo, "--visible", job.Visible ? "true" : "false"); AddArg(startInfo, "--close-after-extract", job.CloseAfterExtract ? "true" : "false"); AddArg(startInfo, "--max-entities", (job.MaxEntities <= 0 ? 100_000 : job.MaxEntities).ToString()); if (!string.IsNullOrWhiteSpace(job.MainViewSeedMode)) AddArg(startInfo, "--main-view-seed-mode", job.MainViewSeedMode); if (!string.IsNullOrWhiteSpace(job.ReuseExtractionDir)) AddArg(startInfo, "--reuse-extraction-dir", job.ReuseExtractionDir); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] job {job.JobId} start: {Path.GetFileName(job.DrawingPath)}"); using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start extractor."); var stdout = process.StandardOutput.ReadToEnd(); var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] job {job.JobId} exit={process.ExitCode}"); var manifestPath = Path.Combine(job.OutputDir, "manifest.json"); return new BridgeJobResult { Ok = process.ExitCode == 0 && File.Exists(manifestPath), JobId = job.JobId, ExitCode = process.ExitCode, Stdout = stdout, Stderr = stderr, OutputDir = job.OutputDir, ManifestPath = manifestPath, Error = process.ExitCode == 0 ? "" : $"Extractor failed with exit code {process.ExitCode}." }; } static void AddArg(ProcessStartInfo startInfo, string key, string value) { startInfo.ArgumentList.Add(key); startInfo.ArgumentList.Add(value); } static void WriteHeartbeat(BridgeOptions options) { var path = Path.Combine(options.QueueDir, "heartbeat.json"); File.WriteAllText(path, JsonSerializer.Serialize(new BridgeHeartbeat { Ok = true, ProcessId = Environment.ProcessId, MachineName = Environment.MachineName, UserName = Environment.UserName, WrittenAtUtc = DateTime.UtcNow }, JsonDefaults.Options)); } static void WriteResult(BridgeOptions options, string jobId, BridgeJobResult result) { var path = Path.Combine(options.QueueDir, "results", $"{jobId}.result.json"); var tempPath = path + ".tmp"; File.WriteAllText(tempPath, JsonSerializer.Serialize(result, JsonDefaults.Options)); File.Move(tempPath, path, overwrite: true); } static void TryMove(string source, string destination) { try { if (File.Exists(source)) File.Move(source, destination, overwrite: true); } catch { // Best effort. } } sealed class BridgeOptions { public string AgentRoot { get; set; } = Directory.GetCurrentDirectory(); public string QueueDir { get; set; } = ""; public string ExtractorDll { get; set; } = ""; public string ProgId { get; set; } = "AutoCAD.Application.23.1"; public int PollMs { get; set; } = 1000; public static BridgeOptions Parse(string[] args) { var options = new BridgeOptions(); for (var i = 0; i < args.Length; i++) { var key = args[i]; var value = i + 1 < args.Length ? args[i + 1] : ""; switch (key) { case "--agent-root": options.AgentRoot = Path.GetFullPath(value.Trim().Trim('"')); i++; break; case "--queue-dir": options.QueueDir = Path.GetFullPath(value.Trim().Trim('"')); i++; break; case "--extractor-dll": options.ExtractorDll = Path.GetFullPath(value.Trim().Trim('"')); i++; break; case "--prog-id": options.ProgId = value.Trim(); i++; break; case "--poll-ms": options.PollMs = int.TryParse(value, out var pollMs) ? Math.Max(100, pollMs) : options.PollMs; i++; break; } } if (string.IsNullOrWhiteSpace(options.QueueDir)) options.QueueDir = Path.Combine(options.AgentRoot, "runtime", "dwg-draft", "bridge"); if (string.IsNullOrWhiteSpace(options.ExtractorDll)) options.ExtractorDll = Path.Combine( options.AgentRoot, "tools", "drawing-diagnostics", "AutoCadDwgExtractor", "bin", "Debug", "net10.0", "AutoCadDwgExtractor.dll"); return options; } } sealed class BridgeJob { public string JobId { get; set; } = ""; public string DrawingPath { get; set; } = ""; public string OutputDir { get; set; } = ""; public string ProgId { get; set; } = ""; public bool Visible { get; set; } = true; public bool CloseAfterExtract { get; set; } public int MaxEntities { get; set; } = 100_000; public string MainViewSeedMode { get; set; } = ""; public string ReuseExtractionDir { get; set; } = ""; } sealed class BridgeJobResult { public bool Ok { get; set; } public string JobId { get; set; } = ""; public int ExitCode { get; set; } public string Stdout { get; set; } = ""; public string Stderr { get; set; } = ""; public string OutputDir { get; set; } = ""; public string ManifestPath { get; set; } = ""; public string Error { get; set; } = ""; } sealed class BridgeHeartbeat { public bool Ok { get; set; } public int ProcessId { get; set; } public string MachineName { get; set; } = ""; public string UserName { get; set; } = ""; public DateTime WrittenAtUtc { get; set; } } static class JsonDefaults { public static JsonSerializerOptions Options { get; } = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower }; }