PRA-only-demo/src/main/run/manager.ts

102 lines
3.6 KiB
TypeScript

import { app, type WebContents } from "electron";
import { overlayBroadcast, hideRunOverlay, showRunOverlay } from "../overlay";
import fs from "node:fs";
import path from "node:path";
import type { FlowDefinition, RunEvent } from "../../shared/types";
import { loadCatalog } from "../catalog";
import { runFlow } from "../engine/runtime";
import { BrowserSession } from "../browser/session";
const active = new Map<string, AbortController>();
let currentRunId: string | null = null;
let _getMainWindow: () => import("electron").BrowserWindow | null = () => null;
export function setMainWindowGetter(fn: () => import("electron").BrowserWindow | null) { _getMainWindow = fn; }
function getMainWindow() { return _getMainWindow(); }
function newRunId(): string {
return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
}
export function startRun(flow: FlowDefinition, sender: WebContents): { runId: string } {
if (currentRunId) active.get(currentRunId)?.abort();
const runId = newRunId();
const root = path.join(app.getPath("userData"), "runs", runId);
const evidenceDir = path.join(root, "evidence");
fs.mkdirSync(evidenceDir, { recursive: true });
const controller = new AbortController();
active.set(runId, controller);
currentRunId = runId;
showRunOverlay(getMainWindow());
const catalog = loadCatalog();
const components = new Map(catalog.components.map((item) => [item.id, item]));
const nodeLabels = new Map<string, string>();
for (const node of flow.nodes) {
nodeLabels.set(node.id, node.label || node.componentId);
}
const emit = (event: Omit<RunEvent, "runId">) => {
const enriched = { ...event, runId } as RunEvent;
if (event.type === "node.started" && event.nodeId) {
enriched.nodeLabel = nodeLabels.get(event.nodeId);
}
if (!sender.isDestroyed()) sender.send("run:event", enriched);
overlayBroadcast(enriched);
};
const log: (message: string, level?: "info" | "warn" | "error", nodeId?: string) => void = (
message,
level = "info",
nodeId
) => emit({ type: "node.log", nodeId, level, message });
void (async () => {
try {
const result = await runFlow(flow, {
runId,
evidenceDir,
signal: controller.signal,
emit,
log,
componentOf: (node) => components.get(node.componentId)
});
fs.writeFileSync(path.join(root, "run.json"), JSON.stringify({ ...result, flowId: flow.id }, null, 2), "utf8");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
emit({ type: "run.finished", status: "failed", durationMs: 0, error: { code: "unknown", reason: message } });
fs.writeFileSync(
path.join(root, "run.json"),
JSON.stringify({ runId, flowId: flow.id, status: "failed", error: { reason: message }, finishedAt: new Date().toISOString(), durationMs: 0 }, null, 2),
"utf8"
);
} finally {
active.delete(runId);
if (currentRunId === runId) currentRunId = null;
hideRunOverlay(getMainWindow());
}
})();
return { runId };
}
export function stopRun(runId?: string): boolean {
const id = runId || currentRunId;
if (!id) return false;
const controller = active.get(id);
if (!controller) return false;
controller.abort();
return true;
}
export async function closeBrowserSession(): Promise<void> {
if (globalThis.__rpaBrowserWindow && !globalThis.__rpaBrowserWindow.isDestroyed()) {
const session = await BrowserSession.create();
await session.close();
}
}
export function isRunning(): boolean {
return currentRunId !== null;
}