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
114 changes: 114 additions & 0 deletions packages/ssh/src/tunnel.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { assert, describe, it } from "@effect/vitest";
import { afterAll } from "vite-plus/test";
// @effect-diagnostics-next-line nodeBuiltinImport:off - the executed suite runs the generated launch script through a real POSIX shell.
import * as NodeChildProcess from "node:child_process";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as NetService from "@t3tools/shared/Net";
import * as Duration from "effect/Duration";
Expand Down Expand Up @@ -447,3 +450,114 @@ describe("ssh tunnel scripts", () => {
}).pipe(Effect.provide(layer), Effect.scoped);
});
});

// The launch script's failure diagnostic only misbehaves when a real shell runs
// the failure branch against a log left over from a previous run, so the suite
// below executes the real generated script. Find a shell that has the tools it
// needs; anywhere else the executed suite skips.
const REQUIRED_SHELL_TOOLS = ["nohup", "mktemp", "cmp", "tail"] as const;

const posixShellRunner = (() => {
// Candidates rather than a platform switch: wsl.exe simply fails to spawn
// where it does not exist, which is the same answer as a missing tool.
const candidates = [
{ file: "bash", args: [] as ReadonlyArray<string> },
{ file: "wsl.exe", args: ["-e", "bash"] as ReadonlyArray<string> },
];
const probe = REQUIRED_SHELL_TOOLS.map((tool) => `command -v ${tool} >/dev/null || exit 1`).join(
"\n",
);
return (
candidates.find((candidate) => {
const result = NodeChildProcess.spawnSync(candidate.file, [...candidate.args, "-c", probe], {
encoding: "utf8",
});
return result.status === 0;
}) ?? null
);
})();

const runShell = (script: string, args: ReadonlyArray<string> = []) => {
if (posixShellRunner === null) throw new Error("no POSIX shell runner available");
// The launch script arrives on stdin with the state key as $1 in production too.
const result = NodeChildProcess.spawnSync(
posixShellRunner.file,
[...posixShellRunner.args, "-s", "--", ...args],
{ input: script, encoding: "utf8" },
);
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
};

const sh = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;

// Reading the generated script proves what it says, not what it does. An
// append-mode launch satisfied every text assertion and still blamed a silent
// server's failure on the previous run's log, because [ -s "$LOG_FILE" ] stayed
// true forever once any run had logged. So run the real script in a throwaway
// HOME seeded with a stale log, with a fake `node` that picks a port, fails
// readiness immediately, and lets the runner exit without writing a byte.
describe.skipIf(posixShellRunner === null)("remote launch script (executed)", () => {
const fixtures: Array<string> = [];

afterAll(() => {
for (const work of fixtures) runShell(`set -eu\nrm -rf ${sh(work)}`);
fixtures.length = 0;
});

it("reports a silent launch instead of tailing the previous run's log", () => {
const setup = runShell(
[
"set -eu",
"work=$(mktemp -d)",
'mkdir -p "$work/bin" "$work/home/.t3/ssh-launch/launch-test"',
// One fake node serves the launch script's three contracts, told apart
// by argv: `node - <state>/port ...` picks a port, `node - <runtime
// file>` finds no external server, `node - <ms> ...` probes readiness,
// and `node <script> serve ...` is the runner, which must exit silently.
'cat > "$work/bin/node" <<\'T3_FAKE_NODE\'',
"#!/bin/sh",
'case "${2-}" in',
" */server-runtime.json) exit 1 ;;",
" */port) printf '43117'; exit 0 ;;",
"esac",
'[ "${1-}" = "-" ] && exit 1',
"exit 0",
"T3_FAKE_NODE",
'chmod 700 "$work/bin/node"',
"printf 'STALE_PREVIOUS_RUN\\n' > \"$work/home/.t3/ssh-launch/launch-test/server.log\"",
"printf 'work:%s\\n' \"$work\"",
].join("\n"),
);
assert.strictEqual(setup.status, 0, setup.stderr);
const workLine = setup.stdout.split("\n").find((line) => line.startsWith("work:"));
if (workLine === undefined) throw new Error(`missing work dir in setup output: ${setup.stdout}`);
const work = workLine.slice("work:".length).trim();
fixtures.push(work);

// The script reads $HOME and finds node on $PATH, so the overrides ride in
// the script itself, ahead of the generated text.
const launch = runShell(
[
`HOME=${sh(`${work}/home`)}`,
"export HOME",
`PATH=${sh(`${work}/bin`)}":$PATH"`,
"export PATH",
buildRemoteLaunchScript({ nodeScriptPath: `${work}/serve.mjs` }),
].join("\n"),
["launch-test"],
);

assert.strictEqual(launch.status, 1, launch.stderr);
assert.include(launch.stderr, "Remote T3 server did not become ready");
// The whole point: a launch that wrote nothing must say so, not replay
// whatever the previous server left in the log.
assert.include(launch.stderr, "It wrote nothing to");
assert.notInclude(launch.stderr, "STALE_PREVIOUS_RUN");

const logBytes = runShell(
`set -eu\nwc -c < ${sh(`${work}/home/.t3/ssh-launch/launch-test/server.log`)}`,
);
assert.strictEqual(logBytes.status, 0, logBytes.stderr);
assert.strictEqual(logBytes.stdout.trim(), "0");
});
});
1 change: 1 addition & 0 deletions packages/ssh/src/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ if [ -z "$REMOTE_PORT" ]; then
printf 'Failed to find an available port on the remote host. Ensure node is available on PATH.\\n' >&2
exit 1
fi
rm -f "$LOG_FILE"
nohup env T3CODE_NO_BROWSER=1 "$RUNNER_FILE" serve --host 127.0.0.1 --port "$REMOTE_PORT" --base-dir "$DEFAULT_SERVER_HOME" >>"$LOG_FILE" 2>&1 < /dev/null &
REMOTE_PID="$!"
printf '%s\\n' "$REMOTE_PID" >"$PID_FILE"
Expand Down
Loading