Skip to content

Commit 8b4d0b2

Browse files
os-zhuangclaude
andcommitted
fix(one): default to port 8787 and stop the double port-selection hang
Two related fixes for the "service hangs when 3000 is busy" report: 1. The shell (Rust) picked a free port and probed it, while the Node launcher independently re-ran its own free-port selection. When those diverged (a port taken in the race window), the runtime served on one port while the splash probed another — hanging on "Starting local runtime…" until the 120s deadline. The shell now sets OBJECTOS_MANAGED=1 and one.mjs honors the exact PORT it was handed (exiting for a clean supervised restart if it's no longer free) instead of re-selecting. 2. Default port moved off 3000 → 8787. 3000 collides with Next.js/Vite/CRA and most local dev servers, causing a different port every launch and widening the race window. 8787 is quiet, so the URL stays stable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bfce5f1 commit 8b4d0b2

6 files changed

Lines changed: 68 additions & 25 deletions

File tree

apps/objectos-one/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Commonly used keys:
5353

5454
| Key | Default | Notes |
5555
|-----------------|------------------|------------------------------------------------------------------|
56-
| `PORT` | auto (3000+) | Fixed port. If in use, falls back to auto with a log line. |
56+
| `PORT` | auto (8787+) | Fixed port. If in use, falls back to auto with a log line. |
5757
| `HOST` | `127.0.0.1` | `0.0.0.0` exposes the server to the LAN. **Set up auth first.** |
5858
| `OBJECTOS_HOME` | `~/.objectstack` | Data directory. Absolute path. |
5959
| `LOG_LEVEL` || Forwarded to the Node server. |

apps/objectos-one/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/objectos-one/src-tauri/src/sidecar.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ const MAX_CRASH_RESTARTS: u32 = 5;
4747
/// Sidecar considered "healthy" after this much uptime; resets crash count.
4848
const STABLE_AFTER: Duration = Duration::from_secs(60);
4949

50+
/// Default port to start scanning from when the user hasn't pinned `PORT`.
51+
/// Deliberately *not* 3000 — that range collides with Next.js, Vite, CRA and
52+
/// most other local dev servers, which both causes frequent fallback churn
53+
/// (a different port every launch) and widens the window for the
54+
/// pick-a-port / bind-a-port race the Node launcher used to have.
55+
const DEFAULT_PORT: u16 = 8787;
56+
5057
pub fn kill_current(app: &AppHandle) {
5158
let state: tauri::State<Sidecar> = app.state();
5259
let taken = state.0.lock().unwrap().take();
@@ -146,9 +153,9 @@ fn spawn_sidecar(app: &AppHandle) -> Result<Child, String> {
146153
"WARN",
147154
&format!("PORT {fixed} on {host} in use; auto-selecting"),
148155
);
149-
pick_free_port(&host, 3000)
156+
pick_free_port(&host, DEFAULT_PORT)
150157
}
151-
None => pick_free_port(&host, 3000),
158+
None => pick_free_port(&host, DEFAULT_PORT),
152159
};
153160
if port == 0 {
154161
return Err("no free port available".into());
@@ -166,7 +173,12 @@ fn spawn_sidecar(app: &AppHandle) -> Result<Child, String> {
166173
cmd.env("OBJECTOS_HOME", &data_dir)
167174
.env("PORT", port.to_string())
168175
.env("HOST", &host)
169-
.env("OBJECTOS_NO_OPEN", "1");
176+
.env("OBJECTOS_NO_OPEN", "1")
177+
// We already picked a free port above and the readiness probe below
178+
// watches *this exact* port. Tell the launcher not to re-select its
179+
// own port: if it bound a different one, the probe would wait out its
180+
// full deadline on a dead port and the UI would hang on the splash.
181+
.env("OBJECTOS_MANAGED", "1");
170182

171183
let mut child = cmd.spawn().map_err(|e| format!("spawn node: {e}"))?;
172184

apps/objectos-one/src/prefs.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ <h1>Environment variables</h1>
189189

190190
<div class="presets">
191191
Quick add:
192-
<button data-key="PORT" data-val="3000">PORT</button>
192+
<button data-key="PORT" data-val="8787">PORT</button>
193193
<button data-key="HOST" data-val="0.0.0.0">HOST=0.0.0.0 (LAN)</button>
194194
<button data-key="OBJECTOS_HOME" data-val="">OBJECTOS_HOME</button>
195195
<button data-key="LOG_LEVEL" data-val="debug">LOG_LEVEL</button>

apps/objectos/one.mjs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ import { fileURLToPath } from 'node:url';
2828
const __dirname = dirname(fileURLToPath(import.meta.url));
2929
const APP_NAME = 'ObjectOS';
3030

31+
// Default port to start from when PORT isn't pinned. Deliberately off 3000 —
32+
// that range collides with Next.js/Vite/CRA and most local dev servers, so a
33+
// quieter default means a stable, predictable URL across launches.
34+
const DEFAULT_PORT = 8787;
35+
3136
function userDataDir() {
3237
if (process.env.OBJECTOS_HOME) return process.env.OBJECTOS_HOME;
3338
return join(homedir(), '.objectstack');
@@ -50,21 +55,55 @@ function ensureDir(p) {
5055
return p;
5156
}
5257

58+
function isPortFree(port) {
59+
return new Promise((resolveTry) => {
60+
const srv = createServer();
61+
srv.once('error', () => resolveTry(false));
62+
srv.once('listening', () => srv.close(() => resolveTry(true)));
63+
srv.listen(port, '127.0.0.1');
64+
});
65+
}
66+
5367
async function findFreePort(preferred) {
54-
const tryPort = (port) =>
55-
new Promise((resolveTry) => {
56-
const srv = createServer();
57-
srv.once('error', () => resolveTry(false));
58-
srv.once('listening', () => srv.close(() => resolveTry(true)));
59-
srv.listen(port, '127.0.0.1');
60-
});
61-
if (await tryPort(preferred)) return preferred;
68+
if (await isPortFree(preferred)) return preferred;
6269
for (let p = preferred + 1; p < preferred + 50; p++) {
63-
if (await tryPort(p)) return p;
70+
if (await isPortFree(p)) return p;
6471
}
6572
return 0;
6673
}
6774

75+
/**
76+
* Resolve the port to serve on.
77+
*
78+
* Two modes:
79+
* - Managed (OBJECTOS_MANAGED=1): the ObjectOS One shell already picked a
80+
* free port, set PORT, and is probing/navigating to *that exact* port.
81+
* We must honor it verbatim — re-selecting our own would strand the UI on
82+
* a port nobody serves. If it was taken in the race window since the shell
83+
* checked it, exit so the shell's supervisor respawns us with a fresh one.
84+
* - Standalone (plain `node one.mjs`): pick a free port ourselves, falling
85+
* forward from the preferred/default if it's busy.
86+
*/
87+
async function resolvePort() {
88+
const preferred = Number(process.env.PORT ?? DEFAULT_PORT);
89+
if (process.env.OBJECTOS_MANAGED === '1') {
90+
if (await isPortFree(preferred)) return preferred;
91+
console.error(
92+
`[one] managed port ${preferred} no longer free; exiting for supervisor restart`,
93+
);
94+
process.exit(75); // EX_TEMPFAIL — supervised restart will pick a new port
95+
}
96+
const port = await findFreePort(preferred);
97+
if (!port) {
98+
console.error('[one] no free port available');
99+
process.exit(1);
100+
}
101+
if (port !== preferred) {
102+
console.log(`[one] port ${preferred} busy → using ${port}`);
103+
}
104+
return port;
105+
}
106+
68107
function openBrowser(url) {
69108
const cmd =
70109
platform() === 'darwin' ? 'open'
@@ -118,15 +157,7 @@ async function main() {
118157
}
119158
}
120159

121-
const preferredPort = Number(process.env.PORT ?? 3000);
122-
const port = await findFreePort(preferredPort);
123-
if (!port) {
124-
console.error('[one] no free port available');
125-
process.exit(1);
126-
}
127-
if (port !== preferredPort) {
128-
console.log(`[one] port ${preferredPort} busy → using ${port}`);
129-
}
160+
const port = await resolvePort();
130161
process.env.PORT = String(port);
131162

132163
const url = `http://localhost:${port}`;

apps/objectos/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"scripts": {
99
"dev": "objectstack dev",
1010
"build": "objectstack compile",
11-
"start": "objectstack serve --port ${PORT:-3000}",
11+
"start": "objectstack serve --port ${PORT:-8787}",
1212
"one": "node one.mjs",
1313
"type-check": "tsc --noEmit"
1414
},

0 commit comments

Comments
 (0)