From 99fe0311e89c533c4985eedd90667dc2dd3bf65d Mon Sep 17 00:00:00 2001 From: SivanTechDev <191865266+SivanTechDev@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:39:37 +0300 Subject: [PATCH 1/3] 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..f6efde99d57 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, []); + }).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..c9523fda5ca 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 = [] 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 23ac8567b8261fadf94038ff54379a4fe1ebbb3e Mon Sep 17 00:00:00 2001 From: SivanTechDev <191865266+SivanTechDev@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:59:44 +0300 Subject: [PATCH 2/3] fix(server): discover external editors reliably --- .../src/process/externalLauncher.test.ts | 19 +++----- apps/server/src/process/externalLauncher.ts | 45 ++++++++++++------- apps/server/src/server.test.ts | 3 +- apps/server/src/ws.ts | 2 +- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts index 43ca40e9c7c..9680440bfce 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -56,10 +56,7 @@ const testLayer = (input: { return Layer.mergeAll( ExternalLauncher.layer.pipe(Layer.provide(Layer.merge(NodeServices.layer, spawnerLayer))), Layer.succeed(HostProcessPlatform, input.platform), - Layer.succeed( - SpawnExecutableResolution, - (command) => input.resolveExecutable?.(command) ?? command, - ), + Layer.succeed(SpawnExecutableResolution, (command) => input.resolveExecutable?.(command)), ConfigProvider.layer(ConfigProvider.fromEnv({ env: input.env ?? {} })), ); }; @@ -132,12 +129,6 @@ it.effect("launches an installed editor with platform-safe arguments", () => it.effect("discovers editors through the service API", () => Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" }); - yield* fileSystem.writeFileString(path.join(binDir, "code.CMD"), "@echo off\r\n"); - yield* fileSystem.writeFileString(path.join(binDir, "explorer.CMD"), "@echo off\r\n"); - const editors = yield* Effect.gen(function* () { const launcher = yield* ExternalLauncher.ExternalLauncher; return yield* launcher.resolveAvailableEditors(); @@ -145,14 +136,14 @@ it.effect("discovers editors through the service API", () => Effect.provide( testLayer({ platform: "win32", - env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" }, + resolveExecutable: (command) => + command === "code" || command === "explorer" ? command : undefined, }), ), ); - assert.equal(editors.includes("vscode"), true); - assert.equal(editors.includes("file-manager"), true); - }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + assert.deepEqual(editors, ["vscode", "file-manager"]); + }), ); it.effect("rejects unknown editors through the service API", () => diff --git a/apps/server/src/process/externalLauncher.ts b/apps/server/src/process/externalLauncher.ts index 9c2f0e417d3..67e6766c387 100644 --- a/apps/server/src/process/externalLauncher.ts +++ b/apps/server/src/process/externalLauncher.ts @@ -18,7 +18,11 @@ import { type LaunchEditorInput, } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; -import { isCommandAvailable, resolveSpawnCommand } from "@t3tools/shared/shell"; +import { + isCommandAvailable, + resolveSpawnCommand, + SpawnExecutableResolution, +} from "@t3tools/shared/shell"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -64,6 +68,7 @@ interface TargetPathAndPosition { } const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/; +const EDITOR_DISCOVERY_CONCURRENCY = 4; const POWERSHELL_ARGUMENTS_PREFIX = [ "-NoProfile", "-NonInteractive", @@ -264,24 +269,30 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" platform: NodeJS.Platform, env: NodeJS.ProcessEnv, ): Effect.fn.Return, never, FileSystem.FileSystem | Path.Path> { - const available: EditorId[] = []; - - for (const editor of EDITORS) { - if (editor.commands === null) { - const command = fileManagerCommandForPlatform(platform); - if (yield* isCommandAvailable(command, { env })) { - available.push(editor.id); - } - continue; - } - - const command = yield* resolveAvailableCommand(editor.commands, env); - if (Option.isSome(command)) { - available.push(editor.id); - } + if (platform === "win32") { + // Use the existing fast resolver: repeated asynchronous PATH scans exceeded five seconds on the reported machine. + const resolveExecutable = yield* SpawnExecutableResolution; + return EDITORS.flatMap((editor) => { + const commands = editor.commands ?? [fileManagerCommandForPlatform(platform)]; + return commands.some((command) => resolveExecutable(command, platform, env) !== undefined) + ? [editor.id] + : []; + }); } - return available; + const availability = yield* Effect.all( + EDITORS.map((editor) => { + const probe = + editor.commands === null + ? isCommandAvailable(fileManagerCommandForPlatform(platform), { env }) + : resolveAvailableCommand(editor.commands, env).pipe(Effect.map(Option.isSome)); + + return probe; + }), + { concurrency: EDITOR_DISCOVERY_CONCURRENCY }, + ); + + return EDITORS.flatMap((editor, index) => (availability[index] ? [editor.id] : [])); }); const resolveBrowserLaunch = Effect.fn("externalLauncher.resolveBrowserLaunch")(function* ( diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index f6efde99d57..bc2bde80e99 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -4225,6 +4225,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { it.effect("routes websocket rpc subscribeServerConfig streams snapshot then update", () => Effect.gen(function* () { + const path = yield* Path.Path; const providers = [ { instanceId: ProviderInstanceId.make("codex"), @@ -4278,7 +4279,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.deepEqual(first.config.keybindings, []); assert.deepEqual(first.config.issues, []); assert.deepEqual(first.config.providers, providers); - assert.equal(first.config.observability.logsDirectoryPath.endsWith("/logs"), true); + assert.equal(path.basename(first.config.observability.logsDirectoryPath), "logs"); assert.equal(first.config.observability.localTracingEnabled, true); assert.equal(first.config.observability.otlpTracesUrl, "http://localhost:4318/v1/traces"); assert.equal(first.config.observability.otlpTracesEnabled, true); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index c9523fda5ca..d214e713b92 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -117,7 +117,7 @@ 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_TIMEOUT = Duration.seconds(5); const EDITOR_DISCOVERY_FALLBACK = [] as const; function unexpectedCompatibilityError(error: never): never { From f302154ff35127e7cc5836e8abcbdc08f7f52908 Mon Sep 17 00:00:00 2001 From: SivanTechDev <191865266+SivanTechDev@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:34:25 +0300 Subject: [PATCH 3/3] address editor discovery review feedback --- .../diagnostics/ProcessDiagnostics.test.ts | 3 ++ apps/server/src/server.test.ts | 28 ++++++++----------- apps/server/src/ws.ts | 18 ++++++++---- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts index 37b42148292..46db9f462a2 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts @@ -250,6 +250,9 @@ describe("ProcessDiagnostics", () => { expect( script.match(/Get-CimInstance Win32_PerfFormattedData_PerfProc_Process/g), ).toHaveLength(1); + expect(script).toMatch( + /Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -ErrorAction SilentlyContinue/, + ); expect(script).toContain("$perfById[[int]$_.ProcessId]"); expect(script).not.toContain("-Filter"); }), diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index bc2bde80e99..f9a677c9fcf 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -45,6 +45,7 @@ import * as Deferred from "effect/Deferred"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as ManagedRuntime from "effect/ManagedRuntime"; @@ -72,6 +73,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); import * as ServerConfig from "./config.ts"; import { makeRoutesLayer } from "./server.ts"; +import { withEditorDiscoveryTimeout } from "./ws.ts"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as GitManager from "./git/GitManager.ts"; import * as Keybindings from "./keybindings.ts"; @@ -3720,23 +3722,17 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); - it.effect("does not block server.getConfig on editor discovery", () => + it.effect("does not block server config 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, []); - }).pipe(TestClock.withLive, Effect.provide(NodeHttpServer.layerTest)), + const responseFiber = yield* withEditorDiscoveryTimeout( + Effect.never, + Duration.seconds(5), + ).pipe(Effect.forkChild); + yield* TestClock.adjust(Duration.seconds(5)); + const response = yield* Fiber.join(responseFiber); + + assert.deepEqual(response, []); + }), ); it.effect( diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index d214e713b92..8967cab18d7 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -120,6 +120,15 @@ const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const EDITOR_DISCOVERY_TIMEOUT = Duration.seconds(5); const EDITOR_DISCOVERY_FALLBACK = [] as const; +export const withEditorDiscoveryTimeout = ( + discovery: Effect.Effect, E, R>, + timeout: Duration.Input = EDITOR_DISCOVERY_TIMEOUT, +) => + discovery.pipe( + Effect.timeoutOption(timeout), + Effect.map(Option.getOrElse(() => EDITOR_DISCOVERY_FALLBACK)), + ); + function unexpectedCompatibilityError(error: never): never { throw new Error(`Unhandled compatibility error: ${String(error)}`); } @@ -925,12 +934,9 @@ const makeWsRpcLayer = ( keybindings: keybindingsConfig.keybindings, issues: keybindingsConfig.issues, providers, - availableEditors: yield* externalLauncher - .resolveAvailableEditors() - .pipe( - Effect.timeoutOption(EDITOR_DISCOVERY_TIMEOUT), - Effect.map(Option.getOrElse(() => EDITOR_DISCOVERY_FALLBACK)), - ), + availableEditors: yield* withEditorDiscoveryTimeout( + externalLauncher.resolveAvailableEditors(), + ), observability: { logsDirectoryPath: config.logsDir, localTracingEnabled: true,