Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n>` 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 |
Expand Down
28 changes: 24 additions & 4 deletions app/crates/vn-shell/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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 <n>".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!();
Expand Down
4 changes: 4 additions & 0 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ vncode key ask for the DeepSeek key and write it into <home>/.env (06

Flags: `-Port <n>`, `-DshHome <dir>`, `-DshVersion <ver>`, `-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 `<root>/home` inside it. A folder that carries
Expand Down Expand Up @@ -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 |
Expand Down
16 changes: 16 additions & 0 deletions scripts/checks/check-shell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------
Expand Down
Loading