Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/server/src/diagnostics/ProcessDiagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> }> =
[];
const spawnerLayer = Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) => {
const childProcess = command as unknown as {
readonly command: string;
readonly args: ReadonlyArray<string>;
};
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(
Expand Down
14 changes: 10 additions & 4 deletions apps/server/src/diagnostics/ProcessDiagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -340,6 +341,7 @@ interface ProcessOutput {
const runProcess = Effect.fn("runProcess")(function* (input: {
readonly command: string;
readonly args: ReadonlyArray<string>;
readonly timeoutMillis: number;
}) {
const cwd = process.cwd();
return yield* Effect.gen(function* () {
Expand Down Expand Up @@ -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: () =>
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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",
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
() =>
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`);
Expand Down Expand Up @@ -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,
Expand Down
Loading