Files
mech-ai/tools/drawing-diagnostics/AutoCadDwgBridge/Program.cs
T
2026-07-17 17:45:56 +08:00

258 lines
9.0 KiB
C#

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<BridgeJob>(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
};
}