190 lines
7.8 KiB
C#
190 lines
7.8 KiB
C#
using System.Text.Json;
|
|
|
|
namespace Zcbot.WindowsNode;
|
|
|
|
internal interface IJobMonitorService
|
|
{
|
|
IReadOnlyList<JobDisplaySnapshot> ReadSnapshots(int limit = 50);
|
|
}
|
|
|
|
internal sealed class JobMonitorService(string jobsDirectory) : IJobMonitorService
|
|
{
|
|
public IReadOnlyList<JobDisplaySnapshot> ReadSnapshots(int limit = 50)
|
|
{
|
|
if (!Directory.Exists(jobsDirectory))
|
|
{
|
|
return [];
|
|
}
|
|
var snapshots = new List<JobDisplaySnapshot>();
|
|
foreach (var requestPath in Directory.EnumerateFiles(
|
|
jobsDirectory, "request.json", SearchOption.AllDirectories))
|
|
{
|
|
try
|
|
{
|
|
using var requestDocument = JsonDocument.Parse(File.ReadAllBytes(requestPath));
|
|
var root = requestDocument.RootElement;
|
|
if (!JobInboxStore.TryReadGuid(root, "job_id", out var jobId))
|
|
{
|
|
continue;
|
|
}
|
|
var jobDirectory = Directory.GetParent(
|
|
Directory.GetParent(requestPath)!.FullName)!.FullName;
|
|
var acceptedAt = ReadDate(root, "accepted_at")
|
|
?? new DateTimeOffset(File.GetCreationTimeUtc(requestPath));
|
|
var capability = ReadString(root, "capability", "unknown");
|
|
var title = ReadDisplayTitle(root);
|
|
var inputFilename = root.TryGetProperty("input_transfers", out var transfers)
|
|
&& transfers.ValueKind == JsonValueKind.Array
|
|
? string.Join(", ", transfers.EnumerateArray()
|
|
.Select(item => ReadString(item, "filename", "-")))
|
|
: "-";
|
|
var state = JobInboxStore.ReadState(Path.Combine(jobDirectory, "state.json"));
|
|
var terminal = JobInboxStore.ReadTerminal(Path.Combine(jobDirectory, "terminal.json"));
|
|
var uploadPath = Path.Combine(jobDirectory, "upload-complete.json");
|
|
var uploadComplete = File.Exists(uploadPath);
|
|
var cloudTerminalPath = Path.Combine(jobDirectory, "cloud-terminal.json");
|
|
var cloudTerminal = JobInboxStore.ReadTerminal(cloudTerminalPath);
|
|
var stage = state?.Stage ?? "accepted";
|
|
var progress = state?.Progress ?? 0;
|
|
var detail = state?.Detail ?? "任务已由本机接收";
|
|
var updatedAt = state is not null
|
|
&& state.UpdatedAt != DateTimeOffset.MinValue
|
|
&& state.UpdatedAt > acceptedAt
|
|
? state.UpdatedAt
|
|
: acceptedAt;
|
|
if (terminal is JsonElement terminalValue)
|
|
{
|
|
var terminalStatus = ReadString(terminalValue, "status", "failed");
|
|
if (terminalStatus == "succeeded" && !uploadComplete)
|
|
{
|
|
stage = "uploading_outputs";
|
|
progress = Math.Max(progress, 90);
|
|
detail = state?.Stage == "uploading_outputs"
|
|
? state.Detail
|
|
: "软件执行完成,等待上传结果";
|
|
}
|
|
else
|
|
{
|
|
stage = terminalStatus;
|
|
progress = terminalStatus == "succeeded" ? 100 : progress;
|
|
detail = TerminalDetail(terminalValue, terminalStatus, detail);
|
|
}
|
|
updatedAt = LatestWrite(updatedAt, Path.Combine(jobDirectory, "terminal.json"));
|
|
}
|
|
if (uploadComplete)
|
|
{
|
|
stage = "succeeded";
|
|
progress = 100;
|
|
detail = File.Exists(Path.Combine(jobDirectory, "workspace-result.json"))
|
|
? "预览已上传,工程保存在本机工作区"
|
|
: "结果已上传并由云端确认";
|
|
updatedAt = LatestWrite(updatedAt, uploadPath);
|
|
}
|
|
else if (cloudTerminal is JsonElement cloudValue)
|
|
{
|
|
var cloudStatus = ReadString(cloudValue, "status", "failed");
|
|
stage = "cloud_terminal";
|
|
progress = 100;
|
|
detail = cloudStatus == "cancelled"
|
|
? "云端任务已取消,本地生成的结果仍保留"
|
|
: "云端任务已终止,本地生成的结果仍保留";
|
|
updatedAt = LatestWrite(updatedAt, cloudTerminalPath);
|
|
}
|
|
snapshots.Add(new JobDisplaySnapshot(
|
|
jobId,
|
|
capability,
|
|
title,
|
|
inputFilename,
|
|
stage,
|
|
Math.Clamp(progress, 0, 100),
|
|
detail,
|
|
acceptedAt,
|
|
updatedAt,
|
|
uploadComplete));
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is JsonException or IOException or UnauthorizedAccessException)
|
|
{
|
|
}
|
|
}
|
|
return snapshots
|
|
.OrderByDescending(item => item.IsActive)
|
|
.ThenByDescending(item => item.UpdatedAt)
|
|
.Take(Math.Max(1, limit))
|
|
.ToArray();
|
|
}
|
|
|
|
private static string TerminalDetail(JsonElement terminal, string status, string fallback)
|
|
{
|
|
if (terminal.TryGetProperty("error", out var error))
|
|
{
|
|
var detail = ReadString(error, "detail", "");
|
|
if (!string.IsNullOrWhiteSpace(detail))
|
|
{
|
|
return detail;
|
|
}
|
|
}
|
|
return status switch
|
|
{
|
|
"succeeded" => "软件任务执行成功",
|
|
"cancelled" => "任务已取消",
|
|
"failed" => "任务执行失败",
|
|
_ => fallback,
|
|
};
|
|
}
|
|
|
|
private static DateTimeOffset LatestWrite(DateTimeOffset current, string path)
|
|
{
|
|
var writtenAt = new DateTimeOffset(File.GetLastWriteTimeUtc(path));
|
|
return writtenAt > current ? writtenAt : current;
|
|
}
|
|
|
|
private static string ReadString(JsonElement value, string name, string fallback) =>
|
|
value.TryGetProperty(name, out var property)
|
|
&& property.ValueKind == JsonValueKind.String
|
|
&& !string.IsNullOrWhiteSpace(property.GetString())
|
|
? property.GetString()!
|
|
: fallback;
|
|
|
|
private static string ReadDisplayTitle(JsonElement root)
|
|
{
|
|
const string fallback = "未命名任务";
|
|
if (root.TryGetProperty("request_summary", out var summary)
|
|
&& summary.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var title = ReadString(summary, "title", fallback);
|
|
if (title != fallback)
|
|
{
|
|
return title;
|
|
}
|
|
}
|
|
|
|
if (root.TryGetProperty("request", out var request)
|
|
&& request.ValueKind == JsonValueKind.Object
|
|
&& request.TryGetProperty("operation", out var operation)
|
|
&& operation.ValueKind == JsonValueKind.Object)
|
|
{
|
|
foreach (var action in operation.EnumerateObject())
|
|
{
|
|
if (action.Value.ValueKind != JsonValueKind.Object)
|
|
{
|
|
continue;
|
|
}
|
|
var title = ReadString(action.Value, "title", fallback);
|
|
if (title != fallback)
|
|
{
|
|
return title;
|
|
}
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
private static DateTimeOffset? ReadDate(JsonElement value, string name) =>
|
|
value.TryGetProperty(name, out var property)
|
|
&& property.ValueKind == JsonValueKind.String
|
|
&& DateTimeOffset.TryParse(property.GetString(), out var parsed)
|
|
? parsed
|
|
: null;
|
|
}
|