dsh-remote-hub 0.3.0 → 0.3.1
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/lib/bridge.js +9 -2
- package/lib/index.js +1 -1
- package/lib/supervisor.js +86 -4
- package/package.json +1 -1
package/lib/bridge.js
CHANGED
|
@@ -155,6 +155,9 @@ export function startBridge({ listenPort, authority, upstream, pairing = null, l
|
|
|
155
155
|
const body = JSON.stringify({
|
|
156
156
|
ok: true,
|
|
157
157
|
service: 'dsh-remote-bridge',
|
|
158
|
+
// A bridge built before pairing existed has no such route; callers use this flag to decide
|
|
159
|
+
// whether an already-listening port can be adopted or must be bypassed.
|
|
160
|
+
pairing: pairing != null,
|
|
158
161
|
bridgePort: listenPort,
|
|
159
162
|
authority,
|
|
160
163
|
upstreamPort: upstream.port || null,
|
|
@@ -213,9 +216,13 @@ export function startBridge({ listenPort, authority, upstream, pairing = null, l
|
|
|
213
216
|
server.once('error', reject)
|
|
214
217
|
server.listen(listenPort, '0.0.0.0', () => {
|
|
215
218
|
server.removeListener('error', reject)
|
|
216
|
-
|
|
219
|
+
// `listenPort: 0` asks the OS for a free port; the real one must be read back, otherwise the
|
|
220
|
+
// status file and every printed URL would carry port 0.
|
|
221
|
+
const addr = server.address()
|
|
222
|
+
const boundPort = addr && typeof addr === 'object' ? addr.port : listenPort
|
|
223
|
+
log(`bridge listening on 0.0.0.0:${boundPort} (Host: ${authority})`)
|
|
217
224
|
resolve({
|
|
218
|
-
port:
|
|
225
|
+
port: boundPort,
|
|
219
226
|
close: () =>
|
|
220
227
|
new Promise((done) => {
|
|
221
228
|
server.close(() => done())
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { start } from './supervisor.js'
|
|
|
16
16
|
|
|
17
17
|
export const name = 'dsh-remote-hub'
|
|
18
18
|
|
|
19
|
-
export const version = '0.3.
|
|
19
|
+
export const version = '0.3.1'
|
|
20
20
|
|
|
21
21
|
/** No host services are required: the plugin only needs Node builtins. */
|
|
22
22
|
export const inject = []
|
package/lib/supervisor.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @module lib/supervisor
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import http from 'node:http'
|
|
12
13
|
import fs from 'node:fs'
|
|
13
14
|
import os from 'node:os'
|
|
14
15
|
import path from 'node:path'
|
|
@@ -101,6 +102,32 @@ function writeStatus(status) {
|
|
|
101
102
|
fs.writeFileSync(CONNECT_FILE, lines.join(os.EOL), 'utf8')
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/** Ask whatever listens on `port` whether it is a dsh-remote bridge, and what it supports. */
|
|
106
|
+
function probeHealth(port, timeoutMs = 1200) {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const req = http.request(
|
|
109
|
+
{ host: '127.0.0.1', port, path: '/_dsh-remote/health', method: 'GET', timeout: timeoutMs },
|
|
110
|
+
(res) => {
|
|
111
|
+
const chunks = []
|
|
112
|
+
res.on('data', (c) => chunks.push(c))
|
|
113
|
+
res.on('end', () => {
|
|
114
|
+
try {
|
|
115
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))
|
|
116
|
+
} catch {
|
|
117
|
+
resolve(null)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
req.on('error', () => resolve(null))
|
|
123
|
+
req.on('timeout', () => {
|
|
124
|
+
req.destroy()
|
|
125
|
+
resolve(null)
|
|
126
|
+
})
|
|
127
|
+
req.end()
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
/**
|
|
105
132
|
* Start everything.
|
|
106
133
|
*
|
|
@@ -113,6 +140,18 @@ function writeStatus(status) {
|
|
|
113
140
|
* @param {string} [config.domain='']
|
|
114
141
|
* @param {(msg: string) => void} [config.log]
|
|
115
142
|
*/
|
|
143
|
+
/**
|
|
144
|
+
* Servers started by this plugin, by port.
|
|
145
|
+
*
|
|
146
|
+
* Plugin reload (hot module replacement) mounts the new build while the previous instance may still
|
|
147
|
+
* hold its socket. Keeping the handles on globalThis lets the new instance close its predecessor in
|
|
148
|
+
* the same process instead of leaving a stale bridge — built from older code — owning the port.
|
|
149
|
+
*/
|
|
150
|
+
function instanceRegistry() {
|
|
151
|
+
if (!globalThis.__dshRemoteInstances) globalThis.__dshRemoteInstances = new Map()
|
|
152
|
+
return globalThis.__dshRemoteInstances
|
|
153
|
+
}
|
|
154
|
+
|
|
116
155
|
export async function start(config = {}) {
|
|
117
156
|
const {
|
|
118
157
|
enabled = true,
|
|
@@ -177,6 +216,21 @@ export async function start(config = {}) {
|
|
|
177
216
|
}),
|
|
178
217
|
}
|
|
179
218
|
|
|
219
|
+
/** Assigned once the tunnel exists; the registry disposer may run before/after that. */
|
|
220
|
+
let tunnelRef = null
|
|
221
|
+
const registry = instanceRegistry()
|
|
222
|
+
const wantedPort = Number(port) || 6300
|
|
223
|
+
const previous = registry.get(wantedPort)
|
|
224
|
+
if (previous) {
|
|
225
|
+
log(`dsh-remote: closing the previous bridge on ${wantedPort} (plugin reload)`)
|
|
226
|
+
try {
|
|
227
|
+
await previous.close()
|
|
228
|
+
} catch (err) {
|
|
229
|
+
log(`dsh-remote: previous bridge close failed: ${err?.message ?? err}`)
|
|
230
|
+
}
|
|
231
|
+
registry.delete(wantedPort)
|
|
232
|
+
}
|
|
233
|
+
|
|
180
234
|
let bridge
|
|
181
235
|
try {
|
|
182
236
|
bridge = await startBridge({
|
|
@@ -187,10 +241,25 @@ export async function start(config = {}) {
|
|
|
187
241
|
log,
|
|
188
242
|
})
|
|
189
243
|
} catch (err) {
|
|
190
|
-
// The usual cause is an older bridge (or a watchdog) still holding the port. Falling back to
|
|
191
|
-
// an ephemeral port keeps the pairing page reachable; the real port is reported everywhere.
|
|
192
244
|
if (!/EADDRINUSE/.test(String(err?.message ?? err))) throw err
|
|
193
|
-
|
|
245
|
+
// Port busy. Three cases, in order of preference:
|
|
246
|
+
// 1. a live bridge of ours that supports pairing -> adopt it (nothing to do);
|
|
247
|
+
// 2. a live bridge of ours from an older build (no pairing route) -> must bypass it;
|
|
248
|
+
// 3. some unrelated program -> bypass it.
|
|
249
|
+
const wanted = Number(port) || 6300
|
|
250
|
+
const existing = await probeHealth(wanted)
|
|
251
|
+
if (existing?.ok && existing.pairing) {
|
|
252
|
+
log(`dsh-remote: port ${wanted} is already served by a dsh-remote bridge; adopting it`)
|
|
253
|
+
return {
|
|
254
|
+
status: readStatus() ?? { updatedAt: Date.now(), phase: 'adopted', bridgePort: wanted, authority },
|
|
255
|
+
close: async () => {},
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (existing?.ok) {
|
|
259
|
+
log('dsh-remote: port is held by an old bridge WITHOUT pairing support; using another port')
|
|
260
|
+
} else {
|
|
261
|
+
log(`dsh-remote: port ${wanted} is busy (not a dsh-remote bridge); using another port`)
|
|
262
|
+
}
|
|
194
263
|
bridge = await startBridge({
|
|
195
264
|
listenPort: 0,
|
|
196
265
|
authority,
|
|
@@ -198,6 +267,7 @@ export async function start(config = {}) {
|
|
|
198
267
|
pairing,
|
|
199
268
|
log,
|
|
200
269
|
})
|
|
270
|
+
log(`dsh-remote: bound to ${bridge.port} instead; use the URLs below, not the old port`)
|
|
201
271
|
}
|
|
202
272
|
|
|
203
273
|
// Wait for the host's web server in the background: plugin mode runs before DSH binds its port.
|
|
@@ -220,7 +290,6 @@ export async function start(config = {}) {
|
|
|
220
290
|
}
|
|
221
291
|
const upstreamReady = waitForHost()
|
|
222
292
|
|
|
223
|
-
bridgeInstance = bridge
|
|
224
293
|
const tunnel = await startTunnel({
|
|
225
294
|
kind: tunnelKind,
|
|
226
295
|
bridgePort: bridge.port,
|
|
@@ -229,6 +298,19 @@ export async function start(config = {}) {
|
|
|
229
298
|
log,
|
|
230
299
|
})
|
|
231
300
|
|
|
301
|
+
tunnelRef = tunnel
|
|
302
|
+
bridgeInstance = bridge
|
|
303
|
+
registry.set(bridge.port, {
|
|
304
|
+
close: async () => {
|
|
305
|
+
try {
|
|
306
|
+
tunnelRef?.close?.()
|
|
307
|
+
} catch {}
|
|
308
|
+
try {
|
|
309
|
+
await bridge.close()
|
|
310
|
+
} catch {}
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
|
|
232
314
|
lastPublicUrl = tunnel.publicUrl
|
|
233
315
|
const ip = ipForPairing
|
|
234
316
|
const status = {
|
package/package.json
CHANGED