diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index be17b8ffaf3..461509ea0ad 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -108,6 +108,9 @@ describe("ssh tunnel scripts", () => { assert.include(script, "exec npx --yes 't3@latest' \"$@\""); assert.include(script, "exec npm exec --yes 't3@latest' -- \"$@\""); assert.include(script, "could not install 't3@latest'"); + assert.include(script, "require_installed_t3_cli npx --yes --package 't3@latest'"); + assert.include(script, "require_installed_t3_cli npm exec --yes --package 't3@latest'"); + assert.include(script, "npm produced no t3 executable"); assert.include(script, 'prepend_path_if_dir "$HOME/.local/bin"'); assert.include(script, `T3_NODE_ENGINE_RANGE='${TEST_NODE_ENGINE_RANGE}'`); assert.include(script, "remote_node_satisfies_engine()"); @@ -140,6 +143,10 @@ describe("ssh tunnel scripts", () => { assert.include(script, "exec npx --yes 't3@nightly; touch /tmp/t3-owned' \"$@\""); assert.include(script, "exec npm exec --yes 't3@nightly; touch /tmp/t3-owned' -- \"$@\""); + assert.include( + script, + "require_installed_t3_cli npx --yes --package 't3@nightly; touch /tmp/t3-owned'", + ); assert.notInclude(script, "exec npx --yes t3@nightly; touch /tmp/t3-owned"); }); @@ -185,6 +192,8 @@ describe("ssh tunnel scripts", () => { assert.notInclude(buildRemoteLaunchScript(), "server-home"); assert.include(buildRemoteLaunchScript(), "Remote T3 server did not become ready"); assert.include(buildRemoteLaunchScript(), 'wait_ready "60000"'); + assert.include(buildRemoteLaunchScript(), 'if [ -s "$LOG_FILE" ]; then'); + assert.include(buildRemoteLaunchScript(), "It wrote nothing to %s"); assert.include(buildRemoteLaunchScript({ packageSpec: "t3@nightly" }), "t3@nightly"); assert.include( buildRemotePairingScript(target), diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index a1611c5770f..12ab0027803 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -426,10 +426,26 @@ fi if command -v t3 >/dev/null 2>&1; then exec t3 "$@" fi +# npm extracts a package before it runs the native builds of its dependencies, +# so a failed build (t3 depends on node-pty, which needs a C toolchain) leaves +# the npx cache without a t3 executable. \`npx --yes\` then exits 0 without +# running anything at all, which the caller only ever sees as a server that +# never becomes ready. Resolve the CLI once up front so that install failure is +# reported here, with npm's own output on stderr. +require_installed_t3_cli() { + T3_CLI_PATH="$("$@" -- sh -c 'command -v t3' || true)" + if [ -n "$T3_CLI_PATH" ]; then + return 0 + fi + printf 'Remote host installed %s but npm produced no t3 executable, which usually means a native dependency (node-pty) failed to build. Install a C toolchain on the remote host (Debian/Ubuntu: build-essential, Fedora/RHEL: gcc-c++ make, macOS: xcode-select --install) and try again.\\n' @@T3_PACKAGE_SPEC@@ >&2 + return 1 +} if command -v npx >/dev/null 2>&1; then + require_installed_t3_cli npx --yes --package @@T3_PACKAGE_SPEC@@ || exit 1 exec npx --yes @@T3_PACKAGE_SPEC@@ "$@" fi if command -v npm >/dev/null 2>&1; then + require_installed_t3_cli npm exec --yes --package @@T3_PACKAGE_SPEC@@ || exit 1 exec npm exec --yes @@T3_PACKAGE_SPEC@@ -- "$@" fi printf 'Remote host is missing the t3 CLI and could not install @@T3_PACKAGE_SPEC@@ because node/npm/npx are unavailable on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2 @@ -581,7 +597,11 @@ if [ -z "$REMOTE_PORT" ]; then printf 'managed\\n' >"$MANAGED_FILE" if ! wait_ready "@@T3_READY_TIMEOUT_MS@@"; then printf 'Remote T3 server did not become ready on 127.0.0.1:%s.\\n' "$REMOTE_PORT" >&2 - tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true + if [ -s "$LOG_FILE" ]; then + tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true + else + printf 'It wrote nothing to %s, so it exited before producing any output.\\n' "$LOG_FILE" >&2 + fi kill "$REMOTE_PID" 2>/dev/null || true wait_for_pid_exit "$REMOTE_PID" rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"