258 lines
9.0 KiB
C#
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
|
|
};
|
|
}
|