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(); 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(); for (const node of flow.nodes) { nodeLabels.set(node.id, node.label || node.componentId); } const emit = (event: Omit) => { 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 { if (globalThis.__rpaBrowserWindow && !globalThis.__rpaBrowserWindow.isDestroyed()) { const session = await BrowserSession.create(); await session.close(); } } export function isRunning(): boolean { return currentRunId !== null; }