diff --git a/benchmark/apps/cli/src/index.ts b/benchmark/apps/cli/src/index.ts index 8a5ef48274..d8daa34378 100644 --- a/benchmark/apps/cli/src/index.ts +++ b/benchmark/apps/cli/src/index.ts @@ -154,6 +154,7 @@ const runExercise = async ({ run, task, server }: { run: Run; task: Task; server } let isTaskFinished = false + let isTaskAborted = false client.on(IpcMessageType.Disconnect, () => { console.log(`[cli#runExercise | ${language} / ${exercise}] disconnect`) @@ -216,39 +217,59 @@ const runExercise = async ({ run, task, server }: { run: Run; task: Task; server } if (eventName === RooCodeEventName.TaskAborted) { - isTaskFinished = true + isTaskAborted = true } }) - console.log(`[cli#runExercise | ${language} / ${exercise}] StartNewTask (${taskSocketPath})`) - client.sendMessage({ - type: IpcMessageType.TaskCommand, + type: IpcMessageType.VSCodeCommand, origin: IpcOrigin.Client, clientId: client.clientId!, - data: { - commandName: TaskCommandName.StartNewTask, - data: { - configuration: { - ...rooCodeDefaults, - openRouterApiKey: process.env.OPENROUTER_API_KEY!, - ...run.settings, - }, - text: prompt, - newTab: true, - }, - }, + data: "workbench.action.closeWindow", }) + // client.sendMessage({ + // type: IpcMessageType.TaskCommand, + // origin: IpcOrigin.Client, + // clientId: client.clientId!, + // data: { + // commandName: TaskCommandName.StartNewTask, + // data: { + // configuration: { + // ...rooCodeDefaults, + // openRouterApiKey: process.env.OPENROUTER_API_KEY!, + // ...run.settings, + // }, + // text: prompt, + // newTab: true, + // }, + // }, + // }) + + console.log(`[cli#runExercise | ${language} / ${exercise}] StartNewTask`) + try { - await pWaitFor(() => isTaskFinished, { interval: 1_000, timeout: 300 * 1_000 }) - client.disconnect() - return true + await pWaitFor(() => isTaskFinished || isTaskAborted, { interval: 1_000, timeout: 300 * 1_000 }) } catch (error) { console.error(error) - client.disconnect() - return false } + + try { + client.sendMessage({ + type: IpcMessageType.VSCodeCommand, + origin: IpcOrigin.Client, + clientId: client.clientId!, + data: "workbench.action.closeWindow", + }) + + console.log(`[cli#runExercise | ${language} / ${exercise}] VSCodeCommand (workbench.action.closeWindow)`) + + client.disconnect() + } catch (error) { + console.error(error) + } + + return isTaskFinished } const runUnitTest = async ({ task }: { task: Task }) => { diff --git a/benchmark/packages/ipc/src/client.ts b/benchmark/packages/ipc/src/client.ts new file mode 100644 index 0000000000..5b5bee988e --- /dev/null +++ b/benchmark/packages/ipc/src/client.ts @@ -0,0 +1,119 @@ +import EventEmitter from "node:events" +import * as crypto from "node:crypto" + +import ipc from "node-ipc" + +import { IpcOrigin, IpcMessageType, IpcMessage, ipcMessageSchema, TaskCommand, TaskEvent } from "@benchmark/types" + +export type IpcClientEvents = { + [IpcMessageType.Connect]: [] + [IpcMessageType.Disconnect]: [] + [IpcMessageType.Ack]: [clientId: string] + [IpcMessageType.TaskCommand]: [data: TaskCommand] + [IpcMessageType.TaskEvent]: [data: TaskEvent] +} + +export class IpcClient extends EventEmitter { + private readonly _socketPath: string + private readonly _id: string + private readonly _log: (...args: unknown[]) => void + private _isConnected = false + private _clientId?: string + + constructor(socketPath: string, log = console.log) { + super() + + this._socketPath = socketPath + this._id = `benchmark-${crypto.randomBytes(6).toString("hex")}` + this._log = log + + ipc.config.silent = true + + ipc.connectTo(this._id, this.socketPath, () => { + ipc.of[this._id]?.on("connect", () => this.onConnect()) + ipc.of[this._id]?.on("disconnect", () => this.onDisconnect()) + ipc.of[this._id]?.on("message", (data) => this.onMessage(data)) + }) + } + + private onConnect() { + if (this._isConnected) { + return + } + + this.log("[client#onConnect]") + this._isConnected = true + this.emit(IpcMessageType.Connect) + } + + private onDisconnect() { + if (!this._isConnected) { + return + } + + this.log("[client#onDisconnect]") + this._isConnected = false + this.emit(IpcMessageType.Disconnect) + } + + private onMessage(data: unknown) { + if (typeof data !== "object") { + this._log("[client#onMessage] invalid data", data) + return + } + + const result = ipcMessageSchema.safeParse(data) + + if (!result.success) { + this.log("[client#onMessage] invalid payload", data) + return + } + + const payload = result.data + + if (payload.origin === IpcOrigin.Server) { + switch (payload.type) { + case IpcMessageType.Ack: + this._clientId = payload.data.clientId + this.emit(IpcMessageType.Ack, payload.data.clientId) + break + case IpcMessageType.TaskEvent: + this.emit(IpcMessageType.TaskEvent, payload.data) + break + } + } + } + + private log(...args: unknown[]) { + this._log(...args) + } + + public sendMessage(message: IpcMessage) { + ipc.of[this._id]?.emit("message", message) + } + + public disconnect() { + try { + ipc.disconnect(this._id) + // @TODO: Should we set _disconnect here? + } catch (error) { + this.log("[client#disconnect] error disconnecting", error) + } + } + + public get socketPath() { + return this._socketPath + } + + public get clientId() { + return this._clientId + } + + public get isConnected() { + return this._isConnected + } + + public get isReady() { + return this._isConnected && this._clientId !== undefined + } +} diff --git a/benchmark/packages/ipc/src/index.ts b/benchmark/packages/ipc/src/index.ts index 7a18554e28..03cf66634b 100644 --- a/benchmark/packages/ipc/src/index.ts +++ b/benchmark/packages/ipc/src/index.ts @@ -1,246 +1,2 @@ -import EventEmitter from "node:events" -import { Socket } from "node:net" -import * as crypto from "node:crypto" - -import ipc from "node-ipc" - -import { IpcOrigin, IpcMessageType, IpcMessage, ipcMessageSchema, TaskCommand, TaskEvent } from "@benchmark/types" - -/** - * IpcClient - */ - -export type IpcClientEvents = { - [IpcMessageType.Connect]: [] - [IpcMessageType.Disconnect]: [] - [IpcMessageType.Ack]: [clientId: string] - [IpcMessageType.TaskCommand]: [data: TaskCommand] - [IpcMessageType.TaskEvent]: [data: TaskEvent] -} - -export class IpcClient extends EventEmitter { - private readonly _socketPath: string - private readonly _id: string - private readonly _log: (...args: unknown[]) => void - private _isConnected = false - private _clientId?: string - - constructor(socketPath: string, log = console.log) { - super() - - this._socketPath = socketPath - this._id = `benchmark-${crypto.randomBytes(6).toString("hex")}` - this._log = log - - ipc.config.silent = true - - ipc.connectTo(this._id, this.socketPath, () => { - ipc.of[this._id]?.on("connect", () => this.onConnect()) - ipc.of[this._id]?.on("disconnect", () => this.onDisconnect()) - ipc.of[this._id]?.on("message", (data) => this.onMessage(data)) - }) - } - - private onConnect() { - if (this._isConnected) { - return - } - - this.log("[client#onConnect]") - this._isConnected = true - this.emit(IpcMessageType.Connect) - } - - private onDisconnect() { - if (!this._isConnected) { - return - } - - this.log("[client#onDisconnect]") - this._isConnected = false - this.emit(IpcMessageType.Disconnect) - } - - private onMessage(data: unknown) { - if (typeof data !== "object") { - this._log("[client#onMessage] invalid data", data) - return - } - - const result = ipcMessageSchema.safeParse(data) - - if (!result.success) { - this.log("[client#onMessage] invalid payload", data) - return - } - - const payload = result.data - - if (payload.origin === IpcOrigin.Server) { - switch (payload.type) { - case IpcMessageType.Ack: - this._clientId = payload.data.clientId - this.emit(IpcMessageType.Ack, payload.data.clientId) - break - case IpcMessageType.TaskEvent: - this.emit(IpcMessageType.TaskEvent, payload.data) - break - } - } - } - - private log(...args: unknown[]) { - this._log(...args) - } - - public sendMessage(message: IpcMessage) { - ipc.of[this._id]?.emit("message", message) - } - - public disconnect() { - try { - ipc.disconnect(this._id) - // @TODO: Should we set _disconnect here? - } catch (error) { - this.log("[client#disconnect] error disconnecting", error) - } - } - - public get socketPath() { - return this._socketPath - } - - public get clientId() { - return this._clientId - } - - public get isConnected() { - return this._isConnected - } - - public get isReady() { - return this._isConnected && this._clientId !== undefined - } -} - -/** - * IpcServer - */ - -type IpcServerEvents = { - [IpcMessageType.Connect]: [clientId: string] - [IpcMessageType.Disconnect]: [clientId: string] - [IpcMessageType.TaskCommand]: [clientId: string, data: TaskCommand] - [IpcMessageType.TaskEvent]: [relayClientId: string | undefined, data: TaskEvent] -} - -export class IpcServer extends EventEmitter { - private readonly _socketPath: string - private readonly _log: (...args: unknown[]) => void - private readonly _clients: Map - - private _isListening = false - - constructor(socketPath: string, log = console.log) { - super() - - this._socketPath = socketPath - this._log = log - this._clients = new Map() - } - - public listen() { - this._isListening = true - - ipc.config.silent = true - - ipc.serve(this.socketPath, () => { - ipc.server.on("connect", (socket) => this.onConnect(socket)) - ipc.server.on("socket.disconnected", (socket) => this.onDisconnect(socket)) - ipc.server.on("message", (data) => this.onMessage(data)) - }) - - ipc.server.start() - } - - private onConnect(socket: Socket) { - const clientId = crypto.randomBytes(6).toString("hex") - this._clients.set(clientId, socket) - this.log(`[server#onConnect] clientId = ${clientId}, # clients = ${this._clients.size}`) - this.send(socket, { type: IpcMessageType.Ack, origin: IpcOrigin.Server, data: { clientId } }) - this.emit(IpcMessageType.Connect, clientId) - } - - private onDisconnect(destroyedSocket: Socket) { - let disconnectedClientId: string | undefined - - for (const [clientId, socket] of this._clients.entries()) { - if (socket === destroyedSocket) { - disconnectedClientId = clientId - this._clients.delete(clientId) - break - } - } - - this.log(`[server#socket.disconnected] clientId = ${disconnectedClientId}, # clients = ${this._clients.size}`) - - if (disconnectedClientId) { - this.emit(IpcMessageType.Disconnect, disconnectedClientId) - } - } - - private onMessage(data: unknown) { - if (typeof data !== "object") { - this.log("[server#onMessage] invalid data", data) - return - } - - const result = ipcMessageSchema.safeParse(data) - - if (!result.success) { - this.log("[server#onMessage] invalid payload", result.error) - return - } - - const payload = result.data - - if (payload.origin === IpcOrigin.Client) { - switch (payload.type) { - case IpcMessageType.TaskCommand: - this.emit(IpcMessageType.TaskCommand, payload.clientId, payload.data) - break - } - } - } - - private log(...args: unknown[]) { - this._log(...args) - } - - public broadcast(message: IpcMessage) { - this.log("[server#broadcast] message =", message) - ipc.server.broadcast("message", message) - } - - public send(client: string | Socket, message: IpcMessage) { - this.log("[server#send] message =", message) - - if (typeof client === "string") { - const socket = this._clients.get(client) - - if (socket) { - ipc.server.emit(socket, "message", message) - } - } else { - ipc.server.emit(client, "message", message) - } - } - - public get socketPath() { - return this._socketPath - } - - public get isListening() { - return this._isListening - } -} +export * from "./client.js" +export * from "./server.js" diff --git a/benchmark/packages/ipc/src/server.ts b/benchmark/packages/ipc/src/server.ts new file mode 100644 index 0000000000..cb044f6fae --- /dev/null +++ b/benchmark/packages/ipc/src/server.ts @@ -0,0 +1,126 @@ +import EventEmitter from "node:events" +import { Socket } from "node:net" +import * as crypto from "node:crypto" + +import ipc from "node-ipc" + +import { IpcOrigin, IpcMessageType, IpcMessage, ipcMessageSchema, TaskCommand, TaskEvent } from "@benchmark/types" + +type IpcServerEvents = { + [IpcMessageType.Connect]: [clientId: string] + [IpcMessageType.Disconnect]: [clientId: string] + [IpcMessageType.TaskCommand]: [clientId: string, data: TaskCommand] + [IpcMessageType.TaskEvent]: [relayClientId: string | undefined, data: TaskEvent] + [IpcMessageType.VSCodeCommand]: [clientId: string, data: string] +} + +export class IpcServer extends EventEmitter { + private readonly _socketPath: string + private readonly _log: (...args: unknown[]) => void + private readonly _clients: Map + + private _isListening = false + + constructor(socketPath: string, log = console.log) { + super() + + this._socketPath = socketPath + this._log = log + this._clients = new Map() + } + + public listen() { + this._isListening = true + + ipc.config.silent = true + + ipc.serve(this.socketPath, () => { + ipc.server.on("connect", (socket) => this.onConnect(socket)) + ipc.server.on("socket.disconnected", (socket) => this.onDisconnect(socket)) + ipc.server.on("message", (data) => this.onMessage(data)) + }) + + ipc.server.start() + } + + private onConnect(socket: Socket) { + const clientId = crypto.randomBytes(6).toString("hex") + this._clients.set(clientId, socket) + this.log(`[server#onConnect] clientId = ${clientId}, # clients = ${this._clients.size}`) + this.send(socket, { type: IpcMessageType.Ack, origin: IpcOrigin.Server, data: { clientId } }) + this.emit(IpcMessageType.Connect, clientId) + } + + private onDisconnect(destroyedSocket: Socket) { + let disconnectedClientId: string | undefined + + for (const [clientId, socket] of this._clients.entries()) { + if (socket === destroyedSocket) { + disconnectedClientId = clientId + this._clients.delete(clientId) + break + } + } + + this.log(`[server#socket.disconnected] clientId = ${disconnectedClientId}, # clients = ${this._clients.size}`) + + if (disconnectedClientId) { + this.emit(IpcMessageType.Disconnect, disconnectedClientId) + } + } + + private onMessage(data: unknown) { + if (typeof data !== "object") { + this.log("[server#onMessage] invalid data", data) + return + } + + const result = ipcMessageSchema.safeParse(data) + + if (!result.success) { + this.log("[server#onMessage] invalid payload", result.error) + return + } + + const payload = result.data + + if (payload.origin === IpcOrigin.Client) { + switch (payload.type) { + case IpcMessageType.TaskCommand: + this.emit(IpcMessageType.TaskCommand, payload.clientId, payload.data) + break + } + } + } + + private log(...args: unknown[]) { + this._log(...args) + } + + public broadcast(message: IpcMessage) { + this.log("[server#broadcast] message =", message) + ipc.server.broadcast("message", message) + } + + public send(client: string | Socket, message: IpcMessage) { + this.log("[server#send] message =", message) + + if (typeof client === "string") { + const socket = this._clients.get(client) + + if (socket) { + ipc.server.emit(socket, "message", message) + } + } else { + ipc.server.emit(client, "message", message) + } + } + + public get socketPath() { + return this._socketPath + } + + public get isListening() { + return this._isListening + } +} diff --git a/benchmark/packages/types/src/ipc.ts b/benchmark/packages/types/src/ipc.ts index a2737021f8..cfc3c0c651 100644 --- a/benchmark/packages/types/src/ipc.ts +++ b/benchmark/packages/types/src/ipc.ts @@ -98,6 +98,7 @@ export enum IpcMessageType { Ack = "Ack", TaskCommand = "TaskCommand", TaskEvent = "TaskEvent", + VSCodeCommand = "VSCodeCommand", } export enum IpcOrigin { @@ -123,6 +124,12 @@ export const ipcMessageSchema = z.discriminatedUnion("type", [ relayClientId: z.string().optional(), data: taskEventSchema, }), + z.object({ + type: z.literal(IpcMessageType.VSCodeCommand), + origin: z.literal(IpcOrigin.Client), + clientId: z.string(), + data: z.string(), + }), ]) export type IpcMessage = z.infer diff --git a/src/exports/api.ts b/src/exports/api.ts index e5d67455a7..2d2e695b05 100644 --- a/src/exports/api.ts +++ b/src/exports/api.ts @@ -6,6 +6,7 @@ import { openClineInNewTab } from "../activate/registerCommands" import { RooCodeSettings, RooCodeEvents, RooCodeEventName } from "../schemas" import { IpcOrigin, IpcMessageType, TaskCommandName, TaskEvent } from "../schemas/ipc" +import { formatLog } from "./formatLog" import { RooCodeAPI } from "./interface" import { IpcServer } from "./ipc" @@ -26,20 +27,28 @@ export class API extends EventEmitter implements RooCodeAPI { this.registerListeners(this.sidebarProvider) if (socketPath) { - this.ipc = new IpcServer(socketPath) + this.ipc = new IpcServer(socketPath, (...args: unknown[]) => formatLog(this.outputChannel, ...args)) + this.ipc.listen() this.outputChannel.appendLine( - `IPC server started: socketPath=${socketPath}, pid=${process.pid}, ppid=${process.ppid}`, + `[API] ipc server started: socketPath=${socketPath}, pid=${process.pid}, ppid=${process.ppid}`, ) this.ipc.on(IpcMessageType.TaskCommand, async (_clientId, { commandName, data }) => { + this.outputChannel.appendLine(`[API] TaskCommand -> ${commandName}`) + switch (commandName) { case TaskCommandName.StartNewTask: this.startNewTask(data) break } }) + + this.ipc.on(IpcMessageType.VSCodeCommand, async (_clientId, command) => { + this.outputChannel.appendLine(`[API] VSCodeCommand -> ${command}`) + await vscode.commands.executeCommand(command) + }) } } @@ -66,6 +75,8 @@ export class API extends EventEmitter implements RooCodeAPI { let provider: ClineProvider if (newTab) { + await vscode.commands.executeCommand("workbench.action.closeAllEditors") + if (!this.tabProvider) { this.tabProvider = await openClineInNewTab({ context: this.context, outputChannel: this.outputChannel }) this.registerListeners(this.tabProvider) diff --git a/src/exports/formatLog.ts b/src/exports/formatLog.ts new file mode 100644 index 0000000000..30142ffee4 --- /dev/null +++ b/src/exports/formatLog.ts @@ -0,0 +1,52 @@ +/** + * @fileoverview Utility for robust object logging with special handling for various data types + */ + +import * as vscode from "vscode" + +/** + * Formats and logs values to a VSCode output channel with special handling for various data types + * + * Features: + * - Explicit handling for null and undefined values + * - Special handling for Error objects to preserve stack traces + * - Handles circular references in objects + * - Properly formats special types like BigInt, functions, and symbols + * - Pretty prints objects with indentation for better readability + * + * @param outputChannel - The VSCode output channel to log to + * @param args - The values to log + */ +export function formatLog(outputChannel: vscode.OutputChannel, ...args: unknown[]): void { + for (const arg of args) { + if (arg === null) { + outputChannel.appendLine("null") + } else if (arg === undefined) { + outputChannel.appendLine("undefined") + } else if (typeof arg === "string") { + outputChannel.appendLine(arg) + } else if (arg instanceof Error) { + // Special handling for Error objects to preserve stack traces + outputChannel.appendLine(`Error: ${arg.message}\n${arg.stack || ""}`) + } else { + try { + outputChannel.appendLine( + JSON.stringify( + arg, + (key, value) => { + // Handle special types that JSON.stringify doesn't handle well + if (typeof value === "bigint") return `BigInt(${value})` + if (typeof value === "function") return `Function: ${value.name || "anonymous"}` + if (typeof value === "symbol") return value.toString() + return value + }, + 2, + ), + ) // Pretty print with 2 spaces + } catch (error) { + // Handle circular references or other JSON.stringify errors + outputChannel.appendLine(`[Non-serializable object: ${Object.prototype.toString.call(arg)}]`) + } + } + } +} diff --git a/src/exports/ipc.ts b/src/exports/ipc.ts index 0c6aa2332b..647cc1e15f 100644 --- a/src/exports/ipc.ts +++ b/src/exports/ipc.ts @@ -15,6 +15,7 @@ type IpcServerEvents = { [IpcMessageType.Disconnect]: [clientId: string] [IpcMessageType.TaskCommand]: [clientId: string, data: TaskCommand] [IpcMessageType.TaskEvent]: [relayClientId: string | undefined, data: TaskEvent] + [IpcMessageType.VSCodeCommand]: [clientId: string, data: string] } export class IpcServer extends EventEmitter { @@ -98,6 +99,12 @@ export class IpcServer extends EventEmitter { case IpcMessageType.TaskCommand: this.emit(IpcMessageType.TaskCommand, payload.clientId, payload.data) break + case IpcMessageType.VSCodeCommand: + this.emit(IpcMessageType.VSCodeCommand, payload.clientId, payload.data) + break + default: + throw new Error(`[server#onMessage] unhandled payload: ${JSON.stringify(payload)}`) + break } } } diff --git a/src/schemas/ipc.ts b/src/schemas/ipc.ts index cae0617968..70eb93648a 100644 --- a/src/schemas/ipc.ts +++ b/src/schemas/ipc.ts @@ -83,6 +83,7 @@ export enum IpcMessageType { Ack = "Ack", TaskCommand = "TaskCommand", TaskEvent = "TaskEvent", + VSCodeCommand = "VSCodeCommand", } export enum IpcOrigin { @@ -108,6 +109,12 @@ export const ipcMessageSchema = z.discriminatedUnion("type", [ relayClientId: z.string().optional(), data: taskEventSchema, }), + z.object({ + type: z.literal(IpcMessageType.VSCodeCommand), + origin: z.literal(IpcOrigin.Client), + clientId: z.string(), + data: z.string(), + }), ]) export type IpcMessage = z.infer