From e6ba592f956abddc8ad4d5cd115dedf342244d4d Mon Sep 17 00:00:00 2001 From: Sivan Niv Date: Sat, 11 Jul 2026 00:32:23 +0300 Subject: [PATCH 1/2] fix(server): bound editor discovery and optimize Windows diagnostics --- .../diagnostics/ProcessDiagnostics.test.ts | 35 +++++++++++++++++++ .../src/diagnostics/ProcessDiagnostics.ts | 14 +++++--- apps/server/src/server.test.ts | 19 ++++++++++ apps/server/src/ws.ts | 9 ++++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts index 7d16a11c829..37b42148292 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts @@ -220,6 +220,41 @@ describe("ProcessDiagnostics", () => { }), ); + it.effect("queries each Windows CIM class once", () => + Effect.gen(function* () { + const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = + []; + const spawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => { + const childProcess = command as unknown as { + readonly command: string; + readonly args: ReadonlyArray; + }; + commands.push({ command: childProcess.command, args: childProcess.args }); + return Effect.succeed(mockHandle({ stdout: "[]" })); + }), + ); + + yield* ProcessDiagnostics.readProcessRows.pipe( + Effect.provide(spawnerLayer), + Effect.provideService(HostProcessPlatform, "win32"), + ); + + expect(commands).toHaveLength(1); + const [command] = commands; + expect(command?.command).toBe("powershell.exe"); + expect(command?.args.slice(0, 3)).toEqual(["-NoProfile", "-NonInteractive", "-Command"]); + const script = command?.args[3] ?? ""; + expect(script.match(/Get-CimInstance Win32_Process/g)).toHaveLength(1); + expect( + script.match(/Get-CimInstance Win32_PerfFormattedData_PerfProc_Process/g), + ).toHaveLength(1); + expect(script).toContain("$perfById[[int]$_.ProcessId]"); + expect(script).not.toContain("-Filter"); + }), + ); + it.effect("keeps bounded command diagnostics when the process query exits unsuccessfully", () => Effect.gen(function* () { const spawnerLayer = Layer.succeed( diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts index b39d560a228..3315fa23581 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts @@ -28,7 +28,8 @@ export interface ProcessRow { readonly command: string; } -const PROCESS_QUERY_TIMEOUT_MS = 1_000; +const POSIX_PROCESS_QUERY_TIMEOUT_MS = 1_000; +const WINDOWS_PROCESS_QUERY_TIMEOUT_MS = 5_000; const POSIX_PROCESS_QUERY_COMMAND = "pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command="; const PROCESS_QUERY_MAX_OUTPUT_BYTES = 2 * 1024 * 1024; @@ -340,6 +341,7 @@ interface ProcessOutput { const runProcess = Effect.fn("runProcess")(function* (input: { readonly command: string; readonly args: ReadonlyArray; + readonly timeoutMillis: number; }) { const cwd = process.cwd(); return yield* Effect.gen(function* () { @@ -381,7 +383,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { } satisfies ProcessOutput; }).pipe( Effect.scoped, - Effect.timeoutOption(Duration.millis(PROCESS_QUERY_TIMEOUT_MS)), + Effect.timeoutOption(Duration.millis(input.timeoutMillis)), Effect.flatMap((result) => Option.match(result, { onNone: () => @@ -390,7 +392,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { command: input.command, argCount: input.args.length, cwd, - timeoutMillis: PROCESS_QUERY_TIMEOUT_MS, + timeoutMillis: input.timeoutMillis, }), ), onSome: Effect.succeed, @@ -417,6 +419,7 @@ function readPosixProcessRows(): Effect.Effect< return runProcess({ command: "ps", args: ["-axo", POSIX_PROCESS_QUERY_COMMAND], + timeoutMillis: POSIX_PROCESS_QUERY_TIMEOUT_MS, }).pipe( Effect.flatMap((result) => result.exitCode !== 0 @@ -443,8 +446,10 @@ function readWindowsProcessRows(): Effect.Effect< ChildProcessSpawner.ChildProcessSpawner > { const command = [ + "$perfById = @{};", + "Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -ErrorAction SilentlyContinue | ForEach-Object { $perfById[[int]$_.IDProcess] = $_ };", "$processes = Get-CimInstance Win32_Process | ForEach-Object {", - '$perf = Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -Filter "IDProcess = $($_.ProcessId)" -ErrorAction SilentlyContinue;', + "$perf = $perfById[[int]$_.ProcessId];", "[pscustomobject]@{ ProcessId = $_.ProcessId; ParentProcessId = $_.ParentProcessId; Name = $_.Name; CommandLine = $_.CommandLine; Status = $_.Status; WorkingSetSize = $_.WorkingSetSize; PercentProcessorTime = if ($perf) { $perf.PercentProcessorTime } else { 0 } }", "};", "$processes | ConvertTo-Json -Compress -Depth 3", @@ -453,6 +458,7 @@ function readWindowsProcessRows(): Effect.Effect< return runProcess({ command: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-Command", command], + timeoutMillis: WINDOWS_PROCESS_QUERY_TIMEOUT_MS, }).pipe( Effect.flatMap((result) => result.exitCode !== 0 diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 347c2920792..abaa986651c 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -3720,6 +3720,25 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + it.effect("does not block server.getConfig on editor discovery", () => + Effect.gen(function* () { + yield* buildAppUnderTest({ + layers: { + externalLauncher: { + resolveAvailableEditors: () => Effect.never, + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const response = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({})), + ); + + assert.deepEqual(response.availableEditors, ["file-manager"]); + }).pipe(TestClock.withLive, Effect.provide(NodeHttpServer.layerTest)), + ); + it.effect( "rejects websocket rpc handshake when a session token is only provided via query string", () => diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index fafdbfa6814..ceb0c10600d 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -117,6 +117,8 @@ import * as RelayClient from "@t3tools/shared/relayClient"; const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); +const EDITOR_DISCOVERY_TIMEOUT = Duration.millis(500); +const EDITOR_DISCOVERY_FALLBACK = ["file-manager"] as const; function unexpectedCompatibilityError(error: never): never { throw new Error(`Unhandled compatibility error: ${String(error)}`); @@ -923,7 +925,12 @@ const makeWsRpcLayer = ( keybindings: keybindingsConfig.keybindings, issues: keybindingsConfig.issues, providers, - availableEditors: yield* externalLauncher.resolveAvailableEditors(), + availableEditors: yield* externalLauncher + .resolveAvailableEditors() + .pipe( + Effect.timeoutOption(EDITOR_DISCOVERY_TIMEOUT), + Effect.map(Option.getOrElse(() => EDITOR_DISCOVERY_FALLBACK)), + ), observability: { logsDirectoryPath: config.logsDir, localTracingEnabled: true, From fb9096ee7532daf230ada775f510f253ec7aa3bc Mon Sep 17 00:00:00 2001 From: Sivan Niv Date: Sat, 11 Jul 2026 00:53:19 +0300 Subject: [PATCH 2/2] fix(server): avoid advertising unverified file manager --- apps/server/src/server.test.ts | 2 +- apps/server/src/ws.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index abaa986651c..f6efde99d57 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -3735,7 +3735,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({})), ); - assert.deepEqual(response.availableEditors, ["file-manager"]); + assert.deepEqual(response.availableEditors, []); }).pipe(TestClock.withLive, Effect.provide(NodeHttpServer.layerTest)), ); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index ceb0c10600d..c9523fda5ca 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -118,7 +118,7 @@ const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchComma const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const EDITOR_DISCOVERY_TIMEOUT = Duration.millis(500); -const EDITOR_DISCOVERY_FALLBACK = ["file-manager"] as const; +const EDITOR_DISCOVERY_FALLBACK = [] as const; function unexpectedCompatibilityError(error: never): never { throw new Error(`Unhandled compatibility error: ${String(error)}`);