authorgravatar for gbarros1095@gmail.comGuilherme Barros <gbarros1095@gmail.com> 2026-08-15 12:43:42+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:43:42+02:00
log672216d7e152241213a8757892f281e1f4434e8a
treedf2cc35225fb4c0b54a1496be721cf5c85b08904
parent75472802bc5ddaba860dc652000223600e529937
signature Signed by PGP key B5690EEEBB952194

fix(ssh): let cold remote servers finish starting (#6168)


2 files changed, 37 insertions(+), 1 deletions(-)

packages/ssh/src/tunnel.test.ts+34
......@@ -45,6 +45,16 @@ const makeSuccessfulProcess = (stdout: string) => {
4545 });
4646};
4747
48const makeDelayedSuccessfulProcess = (stdout: string, delayMs: number) => {
49 const process = makeSuccessfulProcess(stdout);
50 return {
51 ...process,
52 exitCode: Effect.sleep(Duration.millis(delayMs)).pipe(
53 Effect.as(ChildProcessSpawner.ExitCode(0)),
54 ),
55 };
56};
57
4858const makeRunningProcess = (onKill: () => void) => {
4959 let finish: ((exitCode: ChildProcessSpawner.ExitCode) => void) | null = null;
5060 return ChildProcessSpawner.makeHandle({
......@@ -174,6 +184,7 @@ describe("ssh tunnel scripts", () => {
174184 assert.include(buildRemoteLaunchScript(), '--base-dir "$DEFAULT_SERVER_HOME"');
175185 assert.notInclude(buildRemoteLaunchScript(), "server-home");
176186 assert.include(buildRemoteLaunchScript(), "Remote T3 server did not become ready");
187 assert.include(buildRemoteLaunchScript(), 'wait_ready "60000"');
177188 assert.include(buildRemoteLaunchScript({ packageSpec: "t3@nightly" }), "t3@nightly");
178189 assert.include(
179190 buildRemotePairingScript(target),
......@@ -235,6 +246,29 @@ describe("ssh tunnel scripts", () => {
235246 }).pipe(Effect.provide(processLayer));
236247 });
237248
249 it.effect("allows cold remote launches to exceed the default SSH command timeout", () => {
250 const target = {
251 alias: "devbox",
252 hostname: "devbox.example.com",
253 username: "julius",
254 port: 2222,
255 } as const;
256 const spawner = ChildProcessSpawner.make(() =>
257 Effect.succeed(makeDelayedSuccessfulProcess('{"remotePort":3774}\n', 75_000)),
258 );
259 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
260 const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer, TestClock.layer());
261
262 return Effect.gen(function* () {
263 const fiber = yield* Effect.forkChild(launchOrReuseRemoteServer(target));
264 yield* Effect.yieldNow;
265 yield* TestClock.adjust(Duration.seconds(75));
266
267 const result = yield* Fiber.join(fiber);
268 assert.equal(result.remotePort, 3774);
269 }).pipe(Effect.provide(processLayer));
270 });
271
238272 it("allows the remote port picker to run without a state file path", () => {
239273 assert.include(REMOTE_PICK_PORT_SCRIPT, 'const filePath = process.argv[2] ?? "";');
240274 });
packages/ssh/src/tunnel.ts+3-1
......@@ -54,7 +54,8 @@ const REMOTE_PORT_SCAN_WINDOW = 200;
5454const SSH_READY_TIMEOUT_MS = 20_000;
5555const SSH_READY_PROBE_TIMEOUT_MS = 1_000;
5656const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000;
57const REMOTE_READY_TIMEOUT_MS = 15_000;
57const REMOTE_READY_TIMEOUT_MS = 60_000;
58const REMOTE_LAUNCH_TIMEOUT_MS = 90_000;
5859const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000;
5960
6061export interface RemoteT3RunnerOptions {
......@@ -705,6 +706,7 @@ export const launchOrReuseRemoteServer = Effect.fn("ssh/tunnel.launchOrReuseRemo
705706 const result = yield* runSshCommand(target, {
706707 remoteCommandArgs: ["sh", "-s", "--", remoteStateKey(target)],
707708 stdin: buildRemoteLaunchScript(runner),
709 timeoutMs: REMOTE_LAUNCH_TIMEOUT_MS,
708710 ...(input?.authSecret === undefined ? {} : { authSecret: input.authSecret }),
709711 ...(input?.batchMode === undefined ? {} : { batchMode: input.batchMode }),
710712 ...(input?.interactiveAuth === undefined ? {} : { interactiveAuth: input.interactiveAuth }),