More progress

This commit is contained in:
cte 2025-03-30 22:12:41 -07:00
parent 85aa329ef0
commit ac0c172655
9 changed files with 375 additions and 269 deletions

View file

@ -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 }) => {

View file

@ -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<IpcClientEvents> {
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
}
}

View file

@ -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<IpcClientEvents> {
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<IpcServerEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private readonly _clients: Map<string, Socket>
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"

View file

@ -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<IpcServerEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private readonly _clients: Map<string, Socket>
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
}
}

View file

@ -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<typeof ipcMessageSchema>

View file

@ -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<RooCodeEvents> 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<RooCodeEvents> 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)

52
src/exports/formatLog.ts Normal file
View file

@ -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)}]`)
}
}
}
}

View file

@ -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<IpcServerEvents> {
@ -98,6 +99,12 @@ export class IpcServer extends EventEmitter<IpcServerEvents> {
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
}
}
}

View file

@ -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<typeof ipcMessageSchema>