Skip to content

Commit bbe5008

Browse files
luke2xclaudejuliusmarminge
authored
fix(desktop): a slow PowerShell profile delays Windows startup by 5 s, not 10 (#19467)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Julius Marminge <[email protected]>
1 parent 48fcf90 commit bbe5008

2 files changed

Lines changed: 94 additions & 2 deletions

File tree

‎apps/desktop/src/shell/DesktopShellEnvironment.test.ts‎

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as NodeServices from "@effect/platform-node/NodeServices";
2+
import * as Clock from "effect/Clock";
23
import { assert, describe, it } from "@effect/vitest";
4+
import * as Deferred from "effect/Deferred";
35
import * as Effect from "effect/Effect";
6+
import * as Fiber from "effect/Fiber";
47
import * as Layer from "effect/Layer";
58
import * as Logger from "effect/Logger";
69
import * as PlatformError from "effect/PlatformError";
@@ -9,6 +12,7 @@ import * as Sink from "effect/Sink";
912
import * as Stream from "effect/Stream";
1013
import * as ChildProcess from "effect/process/ChildProcess";
1114
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
15+
import * as TestClock from "effect/testing/TestClock";
1216
import * as NodeOS from "node:os";
1317
import { vi } from "vite-plus/test";
1418

@@ -74,7 +78,10 @@ function runShellEnvironment(input: {
7478
readonly userShell?: string;
7579
readonly handler: (
7680
command: ChildProcess.Command,
77-
) => string | { output: string; exitCode: number };
81+
) =>
82+
| string
83+
| { output: string; exitCode: number }
84+
| Effect.Effect<ChildProcessSpawner.ChildProcessHandle, PlatformError.PlatformError>;
7885
readonly failure?: PlatformError.PlatformError;
7986
}) {
8087
const layerEnvironment = Layer.succeed(
@@ -88,6 +95,7 @@ function runShellEnvironment(input: {
8895
ChildProcessSpawner.make((command) => {
8996
if (input.failure !== undefined) return Effect.fail(input.failure);
9097
const result = input.handler(command);
98+
if (Effect.isEffect(result)) return result;
9199
return Effect.succeed(
92100
typeof result === "string"
93101
? makeProcess(result)
@@ -489,6 +497,85 @@ describe("DesktopShellEnvironment", () => {
489497
}),
490498
);
491499

500+
it.effect("waits at most 5 seconds in total when the PowerShell profile hangs", () =>
501+
Effect.gen(function* () {
502+
const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows\\System32" };
503+
const profileShells: string[] = [];
504+
const started = yield* Deferred.make<void>();
505+
const completed = yield* Deferred.make<void>();
506+
const baseClock = yield* Clock.Clock;
507+
let wallTime = 0;
508+
const clockWithBackwardWallJump: Clock.Clock = {
509+
...baseClock,
510+
currentTimeMillisUnsafe: () => wallTime,
511+
currentTimeMillis: Effect.sync(() => wallTime),
512+
};
513+
514+
const install = yield* Effect.forkChild(
515+
runShellEnvironment({
516+
env,
517+
platform: "win32",
518+
handler: (command) => {
519+
if (command._tag !== "StandardCommand") return "";
520+
if (command.args.includes("-NoProfile")) {
521+
return envOutput({ PATH: "C:\\NoProfile\\Bin;C:\\Windows\\System32" });
522+
}
523+
profileShells.push(command.command);
524+
return Deferred.succeed(started, undefined).pipe(
525+
Effect.as(
526+
ChildProcessSpawner.makeHandle({
527+
...makeProcess(""),
528+
stdout: Stream.never,
529+
all: Stream.never,
530+
exitCode: Effect.never,
531+
isRunning: Effect.succeed(true),
532+
}),
533+
),
534+
);
535+
},
536+
}).pipe(
537+
Effect.tap(() => Deferred.succeed(completed, undefined)),
538+
Effect.provide(Logger.layer([])),
539+
Effect.provideService(Clock.Clock, clockWithBackwardWallJump),
540+
),
541+
);
542+
yield* Deferred.await(started);
543+
wallTime = -60_000;
544+
yield* TestClock.adjust("5 seconds");
545+
assert.isTrue(yield* Deferred.isDone(completed), "finishes within the shared budget");
546+
assert.isFalse(yield* Fiber.join(install));
547+
assert.deepEqual(profileShells, ["pwsh.exe"]);
548+
assert.equal(env.PATH, "C:\\NoProfile\\Bin;C:\\Windows\\System32");
549+
}),
550+
);
551+
552+
it.effect("falls back to Windows PowerShell when pwsh.exe is not installed", () =>
553+
Effect.gen(function* () {
554+
const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows\\System32" };
555+
const missing = PlatformError.systemError({
556+
_tag: "NotFound",
557+
module: "ChildProcess",
558+
method: "spawn",
559+
pathOrDescriptor: "pwsh.exe",
560+
});
561+
562+
const prepared = yield* runShellEnvironment({
563+
env,
564+
platform: "win32",
565+
handler: (command) => {
566+
if (command._tag !== "StandardCommand") return "";
567+
if (command.command === "pwsh.exe") return Effect.fail(missing);
568+
return command.args.includes("-NoProfile")
569+
? envOutput({ PATH: "C:\\Windows\\System32" })
570+
: envOutput({ PATH: "C:\\Profile\\Node;C:\\Windows\\System32" });
571+
},
572+
}).pipe(Effect.provide(Logger.layer([])));
573+
574+
assert.isTrue(prepared);
575+
assert.equal(env.PATH, "C:\\Profile\\Node;C:\\Windows\\System32");
576+
}),
577+
);
578+
492579
it.effect.each([false, true])(
493580
"leaves Windows preparation incomplete when loadProfile=%s fails",
494581
(loadProfile) =>

‎apps/desktop/src/shell/DesktopShellEnvironment.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { listLoginShellCandidates, setWindowsPath } from "@t3tools/shared/shell";
2+
import * as Clock from "effect/Clock";
23
import * as Context from "effect/Context";
34
import * as Duration from "effect/Duration";
45
import * as Effect from "effect/Effect";
@@ -377,12 +378,16 @@ const readWindowsEnvironment = Effect.fn("desktop.shellEnvironment.readWindowsEn
377378
captureWindowsEnvironmentCommand(names),
378379
];
379380

381+
const deadline =
382+
(yield* Clock.monotonicTimeNanos) + Duration.toNanosUnsafe(LOGIN_SHELL_TIMEOUT);
380383
for (const command of WINDOWS_SHELL_CANDIDATES) {
384+
const remainingNanos = deadline - (yield* Clock.monotonicTimeNanos);
385+
if (remainingNanos <= 0n) break;
381386
const output = yield* runCommandOutput({
382387
probe: options.loadProfile ? "powershell-profile" : "powershell-no-profile",
383388
command,
384389
args,
385-
timeout: LOGIN_SHELL_TIMEOUT,
390+
timeout: Duration.nanos(remainingNanos),
386391
});
387392
const environment = extractEnvironment(output, names);
388393
if (Object.keys(environment).length > 0) {

0 commit comments

Comments
 (0)