Skip to content

fix: keep routes for commands that self-daemonize - #444

Merged
Railly merged 9 commits into
mainfrom
railly/issue-411-self-daemon-routes
Nov 4, 2026
Merged

Railly merged 9 commits into
mainfrom
railly/issue-411-self-daemon-routes

Conversation

@Railly

@Railly Railly commented Sep 29, 2026

Copy link
Copy Markdown
Member

Fixes #411. Supersedes #413.

Problem

When a wrapped command self-daemonizes (starts a server, prints its address, and exits), portless removed the route. The proxy returned 404 for a healthy server, and the orphan kept holding the port. Astro 7 does this in agentic environments.

Change

  • On exit, portless waits up to EXIT_TIMEOUT_MS for a listener on the assigned port, but only while processes from the command's process group are still alive. A plain foreground exit leaves the group empty and returns at once. Windows has no process group to watch, so it checks once.
  • If a listener is found, the route moves to that listener's pid (RouteStore.transferRoute). The proxy keeps serving it, portless list shows it, and it drops on its own once the server stops. The warning prints the PID to stop.
  • Multi-app mode does the same per app.

This also fixes a race in the original report: ./fake-dev.sh exits before the backgrounded server binds, so checking the port only at exit misses it.

Based on #413 by @Tiancheng-Xu, credited as co-author, including the self-daemon e2e fixture.

When a wrapped command exits while its server keeps listening on the assigned port, portless removed the route, so the proxy returned 404 for a healthy server and the orphan held the port.

On exit, wait up to EXIT_TIMEOUT_MS while processes from the command's process group are alive for a listener on the assigned port, then move the route to that listener's pid. The proxy keeps serving it, list shows it, and it drops once the server stops. Foreground exits still remove their routes right away.

Fixes #411

Co-Authored-By: tiancheng-Xu <[email protected]>
@vercel

vercel Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
portless Ready Ready Preview, v0 Nov 4, 2026 3:28am UTC

…-daemon-routes

# Conflicts:
#	packages/portless/src/cli-utils.ts
#	packages/portless/src/cli.ts
@vercel-internal-security-reviewer

Copy link
Copy Markdown

Security review details

Comment thread packages/portless/src/routes.ts Resolved
Comment thread packages/portless/src/cli.ts Resolved
Comment thread packages/portless/src/cli-utils.ts Resolved
…ansfer, wait for detached daemons

The proxy reloads routes when a cached owner has exited, so a reused port does not answer for the old hostname. A transferred route drops its ngrok and Tailscale URLs, which stop with portless. After a clean exit, portless waits up to EXIT_TIMEOUT_MS for a listener even when the process group is empty, since detached daemons leave it.
Comment thread tests/e2e/src/self-daemon.test.ts Resolved
…-daemon-routes

# Conflicts:
#	packages/portless/src/cli.ts
#	skills/portless/SKILL.md
The proxy reloads routes.json on a debounce, so a request sent right after the
command exits can arrive before the reload on fast Linux runners.
Comment on lines +398 to +403
// Tunnels and Serve config stop with the previous owner.
delete route.ngrokUrl;
delete route.ngrokPid;
delete route.tailscaleUrl;
delete route.tailscaleHttpsPort;
delete route.tailscaleFunnel;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a self-daemonizing command runs with PORTLESS_CLOUDFLARE=1, the exit cleanup stops cloudflared and suppresses its exit handler, but this transfer leaves cloudflareUrl and cloudflarePid on the surviving route. portless list then continues advertising a dead public URL and retains the stopped tunnel PID.

… daemon

Exit cleanup stops cloudflared, so the transferred route no longer advertises the dead URL or keeps the stopped tunnel PID.

This branch was successfully deployed

1 active deployment
Preview — 88a83aa7 Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self-daemonizing commands leave an orphaned server and a dropped route that prune cannot find

2 participants