centralu 0.1.0-beta.13 → 0.1.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/serve.mjs +77 -2
- package/package.json +5 -5
package/bin/serve.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { spawn } from 'node:child_process'
|
|
19
19
|
import { randomBytes } from 'node:crypto'
|
|
20
20
|
import { chmodSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync, closeSync } from 'node:fs'
|
|
21
|
-
import { connect } from 'node:net'
|
|
21
|
+
import { connect, createServer } from 'node:net'
|
|
22
22
|
import { userInfo } from 'node:os'
|
|
23
23
|
import { dirname, join } from 'node:path'
|
|
24
24
|
import { createInterface } from 'node:readline'
|
|
@@ -34,6 +34,70 @@ import { createInterface } from 'node:readline'
|
|
|
34
34
|
*/
|
|
35
35
|
export const DEFAULT_SERVE_PORT = 17175
|
|
36
36
|
|
|
37
|
+
/** How many ports from the recorded one `serve` tries when it is taken (#482); none at or above 20000 */
|
|
38
|
+
export const SERVE_PORT_TRIES = 20
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The port `serve` starts the host on (#482): `{ port, from }`, `from` being the port it moved away
|
|
42
|
+
* from (null when it did not), or null when every candidate is taken.
|
|
43
|
+
*
|
|
44
|
+
* A port the person gave (`--port`) is used as given: they asked for that one. Otherwise the recorded
|
|
45
|
+
* port (else the default) when it is free, and when another program listens there, the next free
|
|
46
|
+
* one above it, below 20000 (the ranges `DEFAULT_SERVE_PORT` keeps out of). The host's ready line
|
|
47
|
+
* then records the port it got, so `--connection` reports it and the hub forwards to it.
|
|
48
|
+
*
|
|
49
|
+
* The usual other program is the other half of one Windows laptop: WSL2 relays a distro's
|
|
50
|
+
* 127.0.0.1 ports to Windows (`wslrelay.exe`), so a Windows install and a WSL install linked as two
|
|
51
|
+
* machines both wanted 17175, and whichever started second could not start at all.
|
|
52
|
+
* `taken(port)` answers whether something listens there.
|
|
53
|
+
*/
|
|
54
|
+
export async function chooseServePort({ asked, recorded, taken }) {
|
|
55
|
+
if (asked !== null && asked !== undefined) return { port: asked, from: null }
|
|
56
|
+
const first = recorded ?? DEFAULT_SERVE_PORT
|
|
57
|
+
for (let p = first; p < first + SERVE_PORT_TRIES && p <= 65535; p++) {
|
|
58
|
+
if (p !== first && p >= 20000) break
|
|
59
|
+
if (!(await taken(p))) return { port: p, from: p === first ? null : first }
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether something listens on 127.0.0.1:`port` here: a listen that fails with EADDRINUSE (or EACCES) */
|
|
65
|
+
export function listensHere(port) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const srv = createServer()
|
|
68
|
+
srv.once('error', (e) => resolve(e?.code === 'EADDRINUSE' || e?.code === 'EACCES'))
|
|
69
|
+
srv.listen({ host: '127.0.0.1', port, exclusive: true }, () => srv.close(() => resolve(false)))
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The TCP ports Windows listens on, from `netstat.exe -an -p TCP` output: a listening socket is the
|
|
75
|
+
* row whose foreign address is `0.0.0.0:0` (or `[::]:0`). Read that way rather than by the state
|
|
76
|
+
* column, which Windows translates (a Korean Windows does not print LISTENING everywhere).
|
|
77
|
+
*/
|
|
78
|
+
export function parseNetstatListening(text) {
|
|
79
|
+
const ports = new Set()
|
|
80
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
81
|
+
const m = /^\s*TCP\s+(?:127\.0\.0\.1|0\.0\.0\.0|\[::1?\]):(\d+)\s+(?:0\.0\.0\.0:0|\[::\]:0)\s/i.exec(line)
|
|
82
|
+
if (m) ports.add(Number(m[1]))
|
|
83
|
+
}
|
|
84
|
+
return ports
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The ports the Windows side listens on, seen from inside a WSL distro (empty elsewhere, or when
|
|
89
|
+
* Windows cannot be asked). Inside the distro a port Windows holds looks free, yet a host there
|
|
90
|
+
* would be unreachable from the hub: the hub's forward ends on Windows' 127.0.0.1, where the
|
|
91
|
+
* Windows program answers instead (#482).
|
|
92
|
+
*/
|
|
93
|
+
async function windowsListening(env, platform) {
|
|
94
|
+
if (detachHow(platform, env) !== 'wsl') return new Set()
|
|
95
|
+
const netstat = windowsTool('wsl', env, ['System32', 'netstat.exe'])
|
|
96
|
+
if (!netstat) return new Set()
|
|
97
|
+
const r = await runCaptured(netstat, ['-an', '-p', 'TCP'], env)
|
|
98
|
+
return r.code === 0 ? parseNetstatListening(r.stdout) : new Set()
|
|
99
|
+
}
|
|
100
|
+
|
|
37
101
|
/**
|
|
38
102
|
* The file in the data folder that holds the token and the port, mode 0600.
|
|
39
103
|
*
|
|
@@ -669,7 +733,6 @@ export async function runServe({ env, home, entry, version, cliPath, port: asked
|
|
|
669
733
|
const dataDir = serveDataDir(env, home)
|
|
670
734
|
const state = ensureServeState(dataDir, platform)
|
|
671
735
|
keepLauncher(dataDir, cliPath, env)
|
|
672
|
-
const port = askedPort ?? state.port ?? DEFAULT_SERVE_PORT
|
|
673
736
|
const protocolVersion = protocolVersionOf(entry)
|
|
674
737
|
|
|
675
738
|
// A serve that is already up answers on the port it recorded; say so before starting a host that
|
|
@@ -682,6 +745,18 @@ export async function runServe({ env, home, entry, version, cliPath, port: asked
|
|
|
682
745
|
return 1
|
|
683
746
|
}
|
|
684
747
|
|
|
748
|
+
const onWindows = await windowsListening(env, platform)
|
|
749
|
+
const chosen = await chooseServePort({ asked: askedPort, recorded: state.port, taken: async (p) => onWindows.has(p) || (await listensHere(p)) })
|
|
750
|
+
if (chosen === null) {
|
|
751
|
+
say(`another program listens on 127.0.0.1:${recorded} and on the ${SERVE_PORT_TRIES - 1} ports above it. Give a free one with --port <n>.`)
|
|
752
|
+
return 1
|
|
753
|
+
}
|
|
754
|
+
const port = chosen.port
|
|
755
|
+
if (chosen.from !== null) {
|
|
756
|
+
const who = onWindows.has(chosen.from) ? 'Windows' : 'another program'
|
|
757
|
+
say(`127.0.0.1:${chosen.from} is in use by ${who}; starting on ${port} instead (kept for the next start)`)
|
|
758
|
+
}
|
|
759
|
+
|
|
685
760
|
const plan = hostCommand(execPath, entry, { port, dataDir, platform })
|
|
686
761
|
const child = spawn(plan.command, plan.args, {
|
|
687
762
|
stdio: ['pipe', 'pipe', logFd === null ? 'inherit' : 'pipe'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "centralu",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.14",
|
|
4
4
|
"description": "Run, watch and steer several Claude Code and Codex CLI sessions from one window",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ijun17",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"win32"
|
|
38
38
|
],
|
|
39
39
|
"optionalDependencies": {
|
|
40
|
-
"@centralu/darwin-arm64": "0.1.0-beta.
|
|
41
|
-
"@centralu/linux-arm64": "0.1.0-beta.
|
|
42
|
-
"@centralu/linux-x64": "0.1.0-beta.
|
|
43
|
-
"@centralu/win32-x64": "0.1.0-beta.
|
|
40
|
+
"@centralu/darwin-arm64": "0.1.0-beta.14",
|
|
41
|
+
"@centralu/linux-arm64": "0.1.0-beta.14",
|
|
42
|
+
"@centralu/linux-x64": "0.1.0-beta.14",
|
|
43
|
+
"@centralu/win32-x64": "0.1.0-beta.14"
|
|
44
44
|
}
|
|
45
45
|
}
|