diff --git a/app/README.md b/app/README.md index 3beff09..06565dd 100644 --- a/app/README.md +++ b/app/README.md @@ -49,6 +49,15 @@ The app never starts node, never binds a port, and never touches the launch toke layer may relaunch the run into a Windows Terminal window this process would not wait for, and the app would report an instant, silent success. +**The port is the app's own.** `-Port ` when a person names one, otherwise a free +loopback port - **never the shared 3080**, which is what every `dsh web` on the machine +reaches for. That is a measured rule, not a preference: the first real run of this binary +asked for 3080 on a machine whose harness was already answering there and died with +`EADDRINUSE`; on a machine where 3080 is free, the same request would share an origin with +a process whose launch token this run does not have. A free port costs nothing because the +launcher opens the tab itself with the token in it. `-Port 3080` is how to ask for the +shared origin on purpose. + ## The crates | crate | what it is | diff --git a/app/crates/vn-shell/src/main.rs b/app/crates/vn-shell/src/main.rs index 8f82eb4..36e6060 100644 --- a/app/crates/vn-shell/src/main.rs +++ b/app/crates/vn-shell/src/main.rs @@ -29,7 +29,7 @@ use std::process::{Command as Process, Stdio}; use vn_shell_core::keystate::{self, KeyState}; use vn_shell_core::options::{self, Command, Options}; -use vn_shell_core::{launch, profile}; +use vn_shell_core::{launch, ports, profile}; use vn_shell_runtime as runtime; use vn_shell_runtime::setup::{self, SetupKind}; use vn_shell_runtime::Plan; @@ -129,6 +129,12 @@ fn status(plan: &Plan) { Ok(entry) => println!("[vncode] launcher: {}", entry.display()), Err(message) => println!("[vncode] launcher: {message}"), } + match plan.port_hint { + Some(port) => { + println!("[vncode] port: {port} (free now; start chooses again, -Port overrides)") + } + None => println!("[vncode] port: none free right now"), + } match plan.root.as_deref() { Some(root) => println!("[vncode] root: {}", root.display()), None => println!("[vncode] root: none found above this binary"), @@ -170,13 +176,27 @@ fn start( // 3. THE LAUNCHER. The same file a person double-clicks, in the foreground: it runs // the harness, watches for the URL line, opens the tab and takes Ctrl+C. + // + // **THE PORT IS THIS APP'S OWN, NEVER THE SHARED 3080**, and that is a measured rule + // rather than a preference: 3080 is what every `dsh web` on the machine reaches for, + // so a run that asks for it either collides (`EADDRINUSE` - measured by the first real + // run of this binary, on a machine whose harness was already answering there) or, worse, + // shares it with a process whose launch token this run does not have. A free port costs + // nothing because this app opens the tab itself with the token in it: nobody types an + // address and nothing is remembered per origin. `-Port 3080` is how to ask for the + // shared origin on purpose. + let port = match options.port { + 0 => ports::free_port().ok_or_else(|| { + "this machine would not hand out a free loopback port - pass -Port ".to_string() + })?, + asked => asked, + }; let entry = launcher(plan)?; let mut command = launcher_command(&entry); command.args(home_arguments); - if options.port > 0 { - command.arg("-Port").arg(options.port.to_string()); - } + command.arg("-Port").arg(port.to_string()); environment.apply(&mut command); + println!("[vncode] port: {port} (chosen by this app, never the shared 3080)"); println!("[vncode] running {}", entry.display()); println!("[vncode] keep this window open - the harness runs in it. Ctrl+C stops it."); println!(); diff --git a/docs/INSTALL.md b/docs/INSTALL.md index d176f70..80100ee 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -46,6 +46,9 @@ vncode key ask for the DeepSeek key and write it into /.env (06 Flags: `-Port `, `-DshHome `, `-DshVersion `, `-Help`, `-Version`. The key is asked for only when there is none and only at a terminal; a piped run never blocks on it. +`start` gives the launcher a free loopback port **of its own** - never the shared `3080`, +which is what every `dsh web` on the machine reaches for - and `-Port 3080` is how to ask +for the shared origin on purpose. **The app owns a root** (`$VNCODE_HOME`, default the platform's per-user data directory plus `vncode`) and the harness home is `/home` inside it. A folder that carries @@ -113,6 +116,7 @@ pack added; both installers prune the retired names `dsh-files` and `dsh-focus` | Symptom | What it means | |---|---| | the run launcher says the port is in use | 3080 is taken; use that instance or pass `-Port 3099` | +| the harness dies with `EADDRINUSE 127.0.0.1:3080` | something already answers on 3080 - usually the harness this very session is open in. `vncode` picks a free port of its own, so this only happens when `-Port 3080` was asked for on purpose: drop the flag, or pass another port | | `scripts\run-web.bat` flashes and closes, or no browser opens | run it from the repo root (the worker reads `.dsh-version.json` there) with `node`/`npx` on `PATH`; the URL is in the app's own output | | `vncode` says the web launcher is missing | it starts `scripts/run-web.*`, so it must run from its folder: the git archive, or a folder that carries `scripts/` and `.dsh-version.json` beside the binary | | the app came up in a home you did not expect | that is the ladder in [PATHS.md](PATHS.md): a `-DshHome` you typed is used or refused by name, a writable inherited `DSH_HOME` is joined, and otherwise the app creates `$VNCODE_HOME/home`. `vncode status` names the path, the reason, and every candidate it refused | diff --git a/scripts/checks/check-shell.mjs b/scripts/checks/check-shell.mjs index 3bf615a..1896e45 100644 --- a/scripts/checks/check-shell.mjs +++ b/scripts/checks/check-shell.mjs @@ -233,6 +233,22 @@ function bridge() { if (!runweb.includes('VNCODE_CONSOLE=[]')) { fail(`the console marker leaked into the launcher: ${runweb}`) } + // THE PORT IS THIS APP'S OWN. Asking the launcher for its default 3080 is the measured + // bug this assertion exists for: on a machine whose harness already answers on 3080 the + // child dies with `EADDRINUSE` (or, worse, shares an origin with a process whose launch + // token this run does not have). + const givenPort = /-Port\s+(\d+)/.exec(runweb) + if (!givenPort) { + fail(`the launcher was not handed a port of this app's own: ${runweb}`) + } else { + const port = Number(givenPort[1]) + if (port === 3080) { + fail('the app asked for the shared 3080 instead of a free port of its own') + } + if (port < 1 || port > 65535) { + fail(`the port handed to the launcher makes no sense: ${port}`) + } + } } // --- the key is not asked for at a pipe, and nothing is written ---------