fix: wait for the dev server rather than tunnelling a guessed port
When the dev child had not reported a bound port within 60s, the wrapper fell back to the configured port and published a tunnel to it. In the case the port fix exists for — a container whose own Fusion owns 4040 — that hands out a dev-looking URL serving a completely different instance, with only a passing "which may not be it" warning to show for it. Observed with a dev server stopped on the interactive `Run central db now? (Y/n)` prompt: it never listens, so it never reports, so the tunnel published the container's Fusion instead. A missing tunnel is a visible problem that explains itself; a tunnel to the wrong app is a silent one. The wrapper now waits, unbounded, printing a notice once a minute that names the interactive-prompt case. An explicit --tunnel=PORT still publishes immediately, since it names a target the dev child knows nothing about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/dev-tunnel-waits-for-dev-server.md
Normal file
7
.changeset/dev-tunnel-waits-for-dev-server.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: `pnpm dev --tunnel` waits for the dev server instead of publishing a tunnel to whatever holds the configured port.
|
||||||
|
category: fix
|
||||||
|
dev: When the dev child had not reported a bound port within 60s the wrapper fell back to `resolveDevTunnelPort(undefined)` (PORT, else 4040) and published that. In a container whose own Fusion owns 4040 — the case the port fix was written for — this handed out a dev-looking URL serving a different instance, and the only signal was a passing "which may not be it" warning. Observed with a dev server stopped on the interactive `Run central db now? (Y/n)` prompt, which never listens and so never reports. The wait is now unbounded (a tunnel is worthless before the server is up) with a once-a-minute notice naming the interactive-prompt case; an explicit `--tunnel=PORT` still publishes immediately, since that names a target the dev child knows nothing about.
|
||||||
@@ -136,6 +136,12 @@ Behaviour worth knowing:
|
|||||||
- **The token comes from the same place the dashboard's does** — `FUSION_DASHBOARD_TOKEN`,
|
- **The token comes from the same place the dashboard's does** — `FUSION_DASHBOARD_TOKEN`,
|
||||||
`FUSION_DAEMON_TOKEN`, then `~/.fusion/settings.json`. On a first authenticated run the token may
|
`FUSION_DAEMON_TOKEN`, then `~/.fusion/settings.json`. On a first authenticated run the token may
|
||||||
not exist yet when the tunnel comes up; the banner then points at the dashboard's own startup line.
|
not exist yet when the tunnel comes up; the banner then points at the dashboard's own startup line.
|
||||||
|
- **The tunnel waits for the dev server, and never guesses.** It publishes only the port the dev
|
||||||
|
server reports it actually bound. If startup is slow — or stopped on an interactive prompt such as
|
||||||
|
`Run central db now? (Y/n)` — no tunnel appears and the wrapper says so once a minute. It will not
|
||||||
|
fall back to the configured port: on a machine where something else already owns that port (a
|
||||||
|
container whose own Fusion holds 4040) that published a "dev server" URL serving a different
|
||||||
|
instance entirely.
|
||||||
- **A failed tunnel never takes the dev server down.** If `cloudflared` is missing or no URL is
|
- **A failed tunnel never takes the dev server down.** If `cloudflared` is missing or no URL is
|
||||||
published, it logs and carries on; losing a preview URL must not cost you your dev loop.
|
published, it logs and carries on; losing a preview URL must not cost you your dev loop.
|
||||||
- **Restarts reuse the tunnel.** In `--watch` mode a fresh quick tunnel would hand out a different
|
- **Restarts reuse the tunnel.** In `--watch` mode a fresh quick tunnel would hand out a different
|
||||||
|
|||||||
@@ -109,7 +109,13 @@ An explicit `--tunnel=PORT` names a target the operator chose (a Vite server the
|
|||||||
nothing about), so it is used immediately and never waits. If the report never arrives the tunnel
|
nothing about), so it is used immediately and never waits. If the report never arrives the tunnel
|
||||||
still comes up on the configured port, since a mis-targeted preview beats no preview at all.
|
still comes up on the configured port, since a mis-targeted preview beats no preview at all.
|
||||||
*/
|
*/
|
||||||
const DEV_SERVER_PORT_REPORT_TIMEOUT_MS = 60_000;
|
/*
|
||||||
|
FNXC:DevTunnel 2026-08-19-03:38:
|
||||||
|
How long to wait quietly before saying WHY no tunnel has appeared yet. Startup legitimately takes
|
||||||
|
minutes (workspace build, embedded Postgres) and can stop dead on an interactive prompt — a real
|
||||||
|
run sat on "Run central db now? (Y/n)" and never listened at all.
|
||||||
|
*/
|
||||||
|
const DEV_SERVER_REPORT_NOTICE_MS = 60_000;
|
||||||
let reportDevServerListening;
|
let reportDevServerListening;
|
||||||
const devServerListeningReport = new Promise((resolve) => { reportDevServerListening = resolve; });
|
const devServerListeningReport = new Promise((resolve) => { reportDevServerListening = resolve; });
|
||||||
|
|
||||||
@@ -117,19 +123,29 @@ async function resolveTunnelTarget() {
|
|||||||
const configured = resolveDevTunnelPort(undefined);
|
const configured = resolveDevTunnelPort(undefined);
|
||||||
if (tunnelPort) return { port: tunnelPort, token: null, source: "explicit" };
|
if (tunnelPort) return { port: tunnelPort, token: null, source: "explicit" };
|
||||||
|
|
||||||
const timeout = new Promise((resolve) => {
|
/*
|
||||||
setTimeout(() => resolve(null), DEV_SERVER_PORT_REPORT_TIMEOUT_MS).unref?.();
|
FNXC:DevTunnel 2026-08-19-03:38:
|
||||||
});
|
WAIT for the dev server rather than falling back to the configured port. The old fallback published
|
||||||
const reported = await Promise.race([devServerListeningReport, timeout]);
|
a tunnel to whatever held that port, which on the machine this matters for — a container whose own
|
||||||
|
Fusion owns 4040 — meant handing out a "dev server" URL that served a DIFFERENT instance entirely.
|
||||||
|
A missing tunnel is a visible, self-explaining problem; a tunnel to the wrong app is a silent one
|
||||||
|
that costs an afternoon. The wait is unbounded because a tunnel is worthless before the server is
|
||||||
|
up anyway, and it costs nothing: it holds no work, and the dev loop is already running.
|
||||||
|
*/
|
||||||
|
const notice = setInterval(() => {
|
||||||
|
console.warn(`[fusion:dev] waiting for the dev server to start before tunnelling (nothing is published yet) — if it is asking you something, answer it`);
|
||||||
|
}, DEV_SERVER_REPORT_NOTICE_MS);
|
||||||
|
notice.unref?.();
|
||||||
|
|
||||||
if (!reported) {
|
try {
|
||||||
console.warn(`[fusion:dev] dev server never reported its port — tunnelling ${configured}, which may not be it`);
|
const reported = await devServerListeningReport;
|
||||||
return { port: configured, token: null, source: "assumed" };
|
if (reported.port !== configured) {
|
||||||
|
console.log(`[fusion:dev] dev server bound ${reported.port} (not ${configured}) — tunnelling ${reported.port}`);
|
||||||
|
}
|
||||||
|
return { port: reported.port, token: reported.token, source: "reported" };
|
||||||
|
} finally {
|
||||||
|
clearInterval(notice);
|
||||||
}
|
}
|
||||||
if (reported.port !== configured) {
|
|
||||||
console.log(`[fusion:dev] dev server bound ${reported.port} (not ${configured}) — tunnelling ${reported.port}`);
|
|
||||||
}
|
|
||||||
return { port: reported.port, token: reported.token, source: "reported" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDevTunnel() {
|
async function openDevTunnel() {
|
||||||
|
|||||||
Reference in New Issue
Block a user