fix: class RpcProcessInstance as state machine
This commit is contained in:
@@ -16,187 +16,186 @@ interface PendingRequest {
|
|||||||
reject(error: Error): void;
|
reject(error: Error): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RpcProcessInstance {
|
|
||||||
process: ChildProcess;
|
|
||||||
send(command: RpcCommand): Promise<RpcResponse>;
|
|
||||||
handleUiResponse(response: RpcExtensionUIResponse): void;
|
|
||||||
setUiRequestHandler(handler?: (request: RpcExtensionUIRequest) => void): void;
|
|
||||||
onEvent(listener: (event: AgentSessionEvent) => void): () => void;
|
|
||||||
onExit(listener: (error?: Error) => void): () => void;
|
|
||||||
dispose(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
function getRpcSpawnCommand(): { command: string; args: string[] } {
|
|
||||||
if (isBunBinary) {
|
|
||||||
return {
|
|
||||||
command: join(dirname(process.execPath), process.platform === "win32" ? "pi.exe" : "pi"),
|
|
||||||
args: ["--mode", "rpc"],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
command: process.execPath,
|
|
||||||
args: [require.resolve("@earendil-works/pi-coding-agent/rpc-entry")],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toError(error: unknown): Error {
|
function toError(error: unknown): Error {
|
||||||
return error instanceof Error ? error : new Error(String(error));
|
return error instanceof Error ? error : new Error(String(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRpcProcessInstance(options: { cwd: string }): RpcProcessInstance {
|
export class RpcProcessInstance {
|
||||||
const rpcCommand = getRpcSpawnCommand();
|
readonly process: ChildProcess;
|
||||||
const child = spawn(rpcCommand.command, rpcCommand.args, {
|
|
||||||
cwd: options.cwd,
|
private exited = false;
|
||||||
env: process.env,
|
private nextRequestId = 0;
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
private stdoutBuffer = "";
|
||||||
});
|
private stderrBuffer = "";
|
||||||
if (!child.stdin || !child.stdout) {
|
private readonly pendingRequests = new Map<string, PendingRequest>();
|
||||||
throw new Error("Failed to create RPC process stdio");
|
private readonly eventListeners = new Set<(event: AgentSessionEvent) => void>();
|
||||||
|
private readonly exitListeners = new Set<(error?: Error) => void>();
|
||||||
|
private uiRequestHandler: ((request: RpcExtensionUIRequest) => void) | undefined;
|
||||||
|
|
||||||
|
constructor(options: { cwd: string }) {
|
||||||
|
const rpcCommand = this.getSpawnCommand();
|
||||||
|
this.process = spawn(rpcCommand.command, rpcCommand.args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: process.env,
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
if (!this.process.stdin || !this.process.stdout) {
|
||||||
|
throw new Error("Failed to create RPC process stdio");
|
||||||
|
}
|
||||||
|
this.attachListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
let exited = false;
|
private getSpawnCommand(): { command: string; args: string[] } {
|
||||||
let nextRequestId = 0;
|
if (isBunBinary) {
|
||||||
let stdoutBuffer = "";
|
return {
|
||||||
let stderrBuffer = "";
|
command: join(dirname(process.execPath), process.platform === "win32" ? "pi.exe" : "pi"),
|
||||||
const pendingRequests = new Map<string, PendingRequest>();
|
args: ["--mode", "rpc"],
|
||||||
const eventListeners = new Set<(event: AgentSessionEvent) => void>();
|
};
|
||||||
const exitListeners = new Set<(error?: Error) => void>();
|
|
||||||
|
|
||||||
const rejectAllPending = (error: Error) => {
|
|
||||||
for (const [id, pending] of pendingRequests) {
|
|
||||||
pendingRequests.delete(id);
|
|
||||||
pending.reject(error);
|
|
||||||
}
|
}
|
||||||
};
|
return {
|
||||||
|
command: process.execPath,
|
||||||
|
args: [require.resolve("@earendil-works/pi-coding-agent/rpc-entry")],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const notifyExit = (error?: Error) => {
|
private attachListeners(): void {
|
||||||
for (const listener of exitListeners) {
|
this.process.stdout?.setEncoding("utf8");
|
||||||
listener(error);
|
this.process.stdout?.on("data", (chunk: string) => {
|
||||||
}
|
this.stdoutBuffer += chunk;
|
||||||
};
|
while (true) {
|
||||||
|
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
||||||
|
if (newlineIndex === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
||||||
|
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
||||||
|
if (!line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.handleLine(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let uiRequestHandler: ((request: RpcExtensionUIRequest) => void) | undefined;
|
this.process.stderr?.setEncoding("utf8");
|
||||||
|
this.process.stderr?.on("data", (chunk: string) => {
|
||||||
|
this.stderrBuffer += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
const handleLine = (line: string) => {
|
this.process.once("error", (error) => {
|
||||||
|
this.exited = true;
|
||||||
|
const wrapped = new Error(`RPC process error: ${error.message}. Stderr: ${this.stderrBuffer}`);
|
||||||
|
this.rejectAllPending(wrapped);
|
||||||
|
this.notifyExit(wrapped);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process.once("exit", (code, signal) => {
|
||||||
|
this.exited = true;
|
||||||
|
const error = new Error(`RPC process exited (code=${code} signal=${signal}). Stderr: ${this.stderrBuffer}`);
|
||||||
|
this.rejectAllPending(error);
|
||||||
|
this.notifyExit(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleLine(line: string): void {
|
||||||
const parsed = JSON.parse(line) as { type?: string; id?: string };
|
const parsed = JSON.parse(line) as { type?: string; id?: string };
|
||||||
|
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case "response": {
|
case "response": {
|
||||||
if (!parsed.id) {
|
if (!parsed.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pending = pendingRequests.get(parsed.id);
|
const pending = this.pendingRequests.get(parsed.id);
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingRequests.delete(parsed.id);
|
this.pendingRequests.delete(parsed.id);
|
||||||
pending.resolve(parsed as RpcResponse);
|
pending.resolve(parsed as RpcResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "extension_ui_request": {
|
case "extension_ui_request": {
|
||||||
uiRequestHandler?.(parsed as RpcExtensionUIRequest);
|
this.uiRequestHandler?.(parsed as RpcExtensionUIRequest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
for (const listener of eventListeners) {
|
for (const listener of this.eventListeners) {
|
||||||
listener(parsed as AgentSessionEvent);
|
listener(parsed as AgentSessionEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
child.stdout.setEncoding("utf8");
|
private rejectAllPending(error: Error): void {
|
||||||
child.stdout.on("data", (chunk: string) => {
|
for (const [id, pending] of this.pendingRequests) {
|
||||||
stdoutBuffer += chunk;
|
this.pendingRequests.delete(id);
|
||||||
while (true) {
|
pending.reject(error);
|
||||||
const newlineIndex = stdoutBuffer.indexOf("\n");
|
|
||||||
if (newlineIndex === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const line = stdoutBuffer.slice(0, newlineIndex).trim();
|
|
||||||
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
||||||
if (!line) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
handleLine(line);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
child.stderr?.setEncoding("utf8");
|
private notifyExit(error?: Error): void {
|
||||||
child.stderr?.on("data", (chunk: string) => {
|
for (const listener of this.exitListeners) {
|
||||||
stderrBuffer += chunk;
|
listener(error);
|
||||||
});
|
|
||||||
|
|
||||||
child.once("error", (error) => {
|
|
||||||
exited = true;
|
|
||||||
const wrapped = new Error(`RPC process error: ${error.message}. Stderr: ${stderrBuffer}`);
|
|
||||||
rejectAllPending(wrapped);
|
|
||||||
notifyExit(wrapped);
|
|
||||||
});
|
|
||||||
|
|
||||||
child.once("exit", (code, signal) => {
|
|
||||||
exited = true;
|
|
||||||
const error = new Error(`RPC process exited (code=${code} signal=${signal}). Stderr: ${stderrBuffer}`);
|
|
||||||
rejectAllPending(error);
|
|
||||||
notifyExit(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const send = async (command: RpcCommand): Promise<RpcResponse> => {
|
|
||||||
if (exited) {
|
|
||||||
throw new Error(`RPC process is not running. Stderr: ${stderrBuffer}`);
|
|
||||||
}
|
}
|
||||||
const id = command.id ?? `orchestrator_${++nextRequestId}_${randomUUID()}`;
|
}
|
||||||
|
|
||||||
|
send(command: RpcCommand): Promise<RpcResponse> {
|
||||||
|
if (this.exited) {
|
||||||
|
throw new Error(`RPC process is not running. Stderr: ${this.stderrBuffer}`);
|
||||||
|
}
|
||||||
|
const id = command.id ?? `orchestrator_${++this.nextRequestId}_${randomUUID()}`;
|
||||||
const fullCommand = { ...command, id };
|
const fullCommand = { ...command, id };
|
||||||
return new Promise<RpcResponse>((resolve, reject) => {
|
return new Promise<RpcResponse>((resolve, reject) => {
|
||||||
pendingRequests.set(id, { resolve, reject });
|
this.pendingRequests.set(id, { resolve, reject });
|
||||||
child.stdin.write(`${JSON.stringify(fullCommand)}\n`, (error) => {
|
this.process.stdin?.write(`${JSON.stringify(fullCommand)}\n`, (error) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingRequests.delete(id);
|
this.pendingRequests.delete(id);
|
||||||
reject(toError(error));
|
reject(toError(error));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
return {
|
handleUiResponse(response: RpcExtensionUIResponse): void {
|
||||||
process: child,
|
if (this.exited) {
|
||||||
send,
|
return;
|
||||||
handleUiResponse(response) {
|
}
|
||||||
if (exited) {
|
this.process.stdin?.write(`${JSON.stringify(response)}\n`);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
child.stdin.write(`${JSON.stringify(response)}\n`);
|
setUiRequestHandler(handler?: (request: RpcExtensionUIRequest) => void): void {
|
||||||
},
|
this.uiRequestHandler = handler;
|
||||||
setUiRequestHandler(handler) {
|
}
|
||||||
uiRequestHandler = handler;
|
|
||||||
},
|
onEvent(listener: (event: AgentSessionEvent) => void): () => void {
|
||||||
onEvent(listener) {
|
this.eventListeners.add(listener);
|
||||||
eventListeners.add(listener);
|
return () => {
|
||||||
return () => {
|
this.eventListeners.delete(listener);
|
||||||
eventListeners.delete(listener);
|
};
|
||||||
};
|
}
|
||||||
},
|
|
||||||
onExit(listener) {
|
onExit(listener: (error?: Error) => void): () => void {
|
||||||
exitListeners.add(listener);
|
this.exitListeners.add(listener);
|
||||||
return () => {
|
return () => {
|
||||||
exitListeners.delete(listener);
|
this.exitListeners.delete(listener);
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
async dispose() {
|
|
||||||
uiRequestHandler = undefined;
|
async dispose(): Promise<void> {
|
||||||
rejectAllPending(new Error("RPC process disposed"));
|
this.uiRequestHandler = undefined;
|
||||||
if (exited) {
|
this.rejectAllPending(new Error("RPC process disposed"));
|
||||||
return;
|
if (this.exited) {
|
||||||
}
|
return;
|
||||||
child.kill("SIGTERM");
|
}
|
||||||
await new Promise<void>((resolve) => {
|
this.process.kill("SIGTERM");
|
||||||
child.once("exit", () => resolve());
|
await new Promise<void>((resolve) => {
|
||||||
});
|
this.process.once("exit", () => resolve());
|
||||||
},
|
});
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRpcProcessInstance(options: { cwd: string }): RpcProcessInstance {
|
||||||
|
return new RpcProcessInstance(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ import type {
|
|||||||
import { radiusPresence } from "./radius.ts";
|
import { radiusPresence } from "./radius.ts";
|
||||||
import { createRpcProcessInstance, type RpcProcessInstance } from "./rpc-process.ts";
|
import { createRpcProcessInstance, type RpcProcessInstance } from "./rpc-process.ts";
|
||||||
import { getInstance, loadInstances, removeInstance, saveInstances, upsertInstance } from "./storage.ts";
|
import { getInstance, loadInstances, removeInstance, saveInstances, upsertInstance } from "./storage.ts";
|
||||||
import type { InstanceRecord } from "./types.ts";
|
import type { InstanceRecord, InstanceStatus } from "./types.ts";
|
||||||
|
|
||||||
|
interface LiveInstanceResources {
|
||||||
|
rpcProcess?: RpcProcessInstance;
|
||||||
|
radiusPiId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface LiveInstance {
|
interface LiveInstance {
|
||||||
rpc: RpcProcessInstance;
|
|
||||||
record: InstanceRecord;
|
record: InstanceRecord;
|
||||||
|
resources: LiveInstanceResources;
|
||||||
subscribers: Set<AgentSessionEventListener>;
|
subscribers: Set<AgentSessionEventListener>;
|
||||||
onUiRequest?: (request: RpcExtensionUIRequest) => void;
|
onUiRequest?: (request: RpcExtensionUIRequest) => void;
|
||||||
unsubscribeEvents?: () => void;
|
unsubscribeEvents?: () => void;
|
||||||
@@ -57,51 +63,133 @@ function isGetStateSuccess(
|
|||||||
export class OrchestratorSupervisor {
|
export class OrchestratorSupervisor {
|
||||||
private readonly liveInstances = new Map<string, LiveInstance>();
|
private readonly liveInstances = new Map<string, LiveInstance>();
|
||||||
|
|
||||||
private async syncInstanceRecord(live: LiveInstance): Promise<void> {
|
private setStatus(live: LiveInstance, status: InstanceStatus): void {
|
||||||
const response = await live.rpc.send({ type: "get_state" });
|
|
||||||
if (!isGetStateSuccess(response)) {
|
|
||||||
live.record = {
|
|
||||||
...live.record,
|
|
||||||
lastSeenAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
upsertInstance(live.record);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
live.record = {
|
live.record = {
|
||||||
...live.record,
|
...live.record,
|
||||||
sessionId: response.data.sessionId,
|
status,
|
||||||
sessionFile: response.data.sessionFile,
|
|
||||||
lastSeenAt: new Date().toISOString(),
|
lastSeenAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
upsertInstance(live.record);
|
upsertInstance(live.record);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindLiveInstance(live: LiveInstance): void {
|
private updateRecord(live: LiveInstance, updates: Partial<InstanceRecord>): void {
|
||||||
|
live.record = {
|
||||||
|
...live.record,
|
||||||
|
...updates,
|
||||||
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
if (updates.radiusPiId !== undefined) {
|
||||||
|
live.resources.radiusPiId = updates.radiusPiId;
|
||||||
|
}
|
||||||
|
if (updates.sessionId !== undefined) {
|
||||||
|
live.resources.sessionId = updates.sessionId;
|
||||||
|
}
|
||||||
|
upsertInstance(live.record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearBindings(live: LiveInstance): void {
|
||||||
live.unsubscribeEvents?.();
|
live.unsubscribeEvents?.();
|
||||||
live.unsubscribeExit?.();
|
live.unsubscribeExit?.();
|
||||||
live.unsubscribeEvents = live.rpc.onEvent((event) => {
|
live.unsubscribeEvents = undefined;
|
||||||
|
live.unsubscribeExit = undefined;
|
||||||
|
live.onUiRequest = undefined;
|
||||||
|
live.resources.rpcProcess?.setUiRequestHandler(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindRpcProcess(live: LiveInstance, rpcProcess: RpcProcessInstance): void {
|
||||||
|
this.clearBindings(live);
|
||||||
|
live.resources.rpcProcess = rpcProcess;
|
||||||
|
live.unsubscribeEvents = rpcProcess.onEvent((event) => {
|
||||||
for (const subscriber of live.subscribers) {
|
for (const subscriber of live.subscribers) {
|
||||||
subscriber(event);
|
subscriber(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
live.unsubscribeExit = live.rpc.onExit(() => {
|
live.unsubscribeExit = rpcProcess.onExit((error) => {
|
||||||
live.record = {
|
void this.handleUnexpectedRpcExit(live, error);
|
||||||
...live.record,
|
|
||||||
status: "stopped",
|
|
||||||
lastSeenAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
upsertInstance(live.record);
|
|
||||||
this.liveInstances.delete(live.record.id);
|
|
||||||
});
|
});
|
||||||
live.rpc.setUiRequestHandler((request) => {
|
rpcProcess.setUiRequestHandler((request) => {
|
||||||
live.onUiRequest?.(request);
|
live.onUiRequest?.(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleUnexpectedRpcExit(live: LiveInstance, _error?: Error): Promise<void> {
|
||||||
|
if (this.liveInstances.get(live.record.id) !== live) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (live.record.status === "stopping" || live.record.status === "stopped") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setStatus(live, "error");
|
||||||
|
this.clearBindings(live);
|
||||||
|
live.resources.rpcProcess = undefined;
|
||||||
|
if (live.resources.radiusPiId) {
|
||||||
|
try {
|
||||||
|
await radiusPresence.disconnectPi(live.record);
|
||||||
|
this.updateRecord(live, { radiusPiId: undefined });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to disconnect Radius Pi ${live.record.id}: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.liveInstances.delete(live.record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRpcProcess(live: LiveInstance): RpcProcessInstance | undefined {
|
||||||
|
return live.resources.rpcProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncInstanceRecord(live: LiveInstance): Promise<void> {
|
||||||
|
const rpcProcess = this.getRpcProcess(live);
|
||||||
|
if (!rpcProcess) {
|
||||||
|
this.updateRecord(live, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await rpcProcess.send({ type: "get_state" });
|
||||||
|
if (!isGetStateSuccess(response)) {
|
||||||
|
this.updateRecord(live, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateRecord(live, {
|
||||||
|
sessionId: response.data.sessionId,
|
||||||
|
sessionFile: response.data.sessionFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanupAcquiredResources(live: LiveInstance): Promise<void> {
|
||||||
|
const rpcProcess = live.resources.rpcProcess;
|
||||||
|
this.clearBindings(live);
|
||||||
|
if (live.resources.radiusPiId) {
|
||||||
|
await radiusPresence.disconnectPi(live.record);
|
||||||
|
live.resources.radiusPiId = undefined;
|
||||||
|
live.record = {
|
||||||
|
...live.record,
|
||||||
|
radiusPiId: undefined,
|
||||||
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
live.resources.sessionId = undefined;
|
||||||
|
if (rpcProcess) {
|
||||||
|
live.resources.rpcProcess = undefined;
|
||||||
|
await rpcProcess.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async failSpawn(live: LiveInstance, error: unknown): Promise<never> {
|
||||||
|
this.setStatus(live, "error");
|
||||||
|
try {
|
||||||
|
await this.cleanupAcquiredResources(live);
|
||||||
|
} finally {
|
||||||
|
this.setStatus(live, "stopped");
|
||||||
|
this.liveInstances.delete(live.record.id);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
updateInstance(instance: InstanceRecord): void {
|
updateInstance(instance: InstanceRecord): void {
|
||||||
const live = this.liveInstances.get(instance.id);
|
const live = this.liveInstances.get(instance.id);
|
||||||
if (live) {
|
if (live) {
|
||||||
live.record = instance;
|
live.record = instance;
|
||||||
|
live.resources.radiusPiId = instance.radiusPiId;
|
||||||
|
live.resources.sessionId = instance.sessionId;
|
||||||
}
|
}
|
||||||
upsertInstance(instance);
|
upsertInstance(instance);
|
||||||
}
|
}
|
||||||
@@ -118,21 +206,22 @@ export class OrchestratorSupervisor {
|
|||||||
}
|
}
|
||||||
| undefined {
|
| undefined {
|
||||||
const live = this.liveInstances.get(instanceId);
|
const live = this.liveInstances.get(instanceId);
|
||||||
if (!live) {
|
const rpcProcess = live ? this.getRpcProcess(live) : undefined;
|
||||||
|
if (!live || !rpcProcess) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
live.subscribers.add(onEvent);
|
live.subscribers.add(onEvent);
|
||||||
live.onUiRequest = onUiRequest;
|
live.onUiRequest = onUiRequest;
|
||||||
return {
|
return {
|
||||||
handleRpc: async (command) => {
|
handleRpc: async (command) => {
|
||||||
const response = await live.rpc.send(command);
|
const response = await rpcProcess.send(command);
|
||||||
if (shouldRefreshSessionMetadata(command)) {
|
if (shouldRefreshSessionMetadata(command)) {
|
||||||
await this.syncInstanceRecord(live);
|
await this.syncInstanceRecord(live);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
handleUiResponse: (response) => {
|
handleUiResponse: (response) => {
|
||||||
live.rpc.handleUiResponse(response);
|
rpcProcess.handleUiResponse(response);
|
||||||
},
|
},
|
||||||
close: () => {
|
close: () => {
|
||||||
if (live.onUiRequest === onUiRequest) {
|
if (live.onUiRequest === onUiRequest) {
|
||||||
@@ -179,28 +268,33 @@ export class OrchestratorSupervisor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async spawnInstance(options: { cwd: string; label?: string }): Promise<InstanceRecord> {
|
async spawnInstance(options: { cwd: string; label?: string }): Promise<InstanceRecord> {
|
||||||
const rpc = createRpcProcessInstance({ cwd: options.cwd });
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const record: InstanceRecord = {
|
|
||||||
id: randomUUID(),
|
|
||||||
status: "online",
|
|
||||||
cwd: options.cwd,
|
|
||||||
createdAt: now,
|
|
||||||
lastSeenAt: now,
|
|
||||||
label: options.label,
|
|
||||||
};
|
|
||||||
|
|
||||||
const registeredRecord = await radiusPresence.registerPi(record);
|
|
||||||
const live: LiveInstance = {
|
const live: LiveInstance = {
|
||||||
rpc,
|
record: {
|
||||||
record: registeredRecord,
|
id: randomUUID(),
|
||||||
|
status: "starting",
|
||||||
|
cwd: options.cwd,
|
||||||
|
createdAt: now,
|
||||||
|
lastSeenAt: now,
|
||||||
|
label: options.label,
|
||||||
|
},
|
||||||
|
resources: {},
|
||||||
subscribers: new Set(),
|
subscribers: new Set(),
|
||||||
};
|
};
|
||||||
this.bindLiveInstance(live);
|
this.liveInstances.set(live.record.id, live);
|
||||||
this.liveInstances.set(registeredRecord.id, live);
|
|
||||||
await this.syncInstanceRecord(live);
|
|
||||||
upsertInstance(live.record);
|
upsertInstance(live.record);
|
||||||
return cloneInstance(live.record);
|
|
||||||
|
try {
|
||||||
|
const rpcProcess = createRpcProcessInstance({ cwd: options.cwd });
|
||||||
|
this.bindRpcProcess(live, rpcProcess);
|
||||||
|
await this.syncInstanceRecord(live);
|
||||||
|
const registeredRecord = await radiusPresence.registerPi(live.record);
|
||||||
|
this.updateRecord(live, { radiusPiId: registeredRecord.radiusPiId });
|
||||||
|
this.setStatus(live, "online");
|
||||||
|
return cloneInstance(live.record);
|
||||||
|
} catch (error) {
|
||||||
|
return await this.failSpawn(live, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopInstance(instanceId: string): Promise<InstanceRecord | undefined> {
|
async stopInstance(instanceId: string): Promise<InstanceRecord | undefined> {
|
||||||
@@ -209,23 +303,29 @@ export class OrchestratorSupervisor {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
await radiusPresence.disconnectPi(live.record);
|
this.setStatus(live, "stopping");
|
||||||
live.unsubscribeEvents?.();
|
try {
|
||||||
live.unsubscribeExit?.();
|
await this.cleanupAcquiredResources(live);
|
||||||
live.onUiRequest = undefined;
|
} finally {
|
||||||
await live.rpc.dispose();
|
live.record = {
|
||||||
this.liveInstances.delete(instanceId);
|
...live.record,
|
||||||
removeInstance(instanceId);
|
status: "stopped",
|
||||||
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
this.liveInstances.delete(instanceId);
|
||||||
|
removeInstance(instanceId);
|
||||||
|
}
|
||||||
return cloneInstance(live.record);
|
return cloneInstance(live.record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleRpc(instanceId: string, command: RpcCommand): Promise<RpcResponse | undefined> {
|
async handleRpc(instanceId: string, command: RpcCommand): Promise<RpcResponse | undefined> {
|
||||||
const live = this.liveInstances.get(instanceId);
|
const live = this.liveInstances.get(instanceId);
|
||||||
if (!live) {
|
const rpcProcess = live ? this.getRpcProcess(live) : undefined;
|
||||||
|
if (!live || !rpcProcess) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await live.rpc.send(command);
|
const response = await rpcProcess.send(command);
|
||||||
if (shouldRefreshSessionMetadata(command)) {
|
if (shouldRefreshSessionMetadata(command)) {
|
||||||
await this.syncInstanceRecord(live);
|
await this.syncInstanceRecord(live);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user