refactor(native-host): modularize host.js and holesail-manager.js
CI / Build & Test (push) Successful in 2m46s
CI / Build & Test (push) Successful in 2m46s
Split host.js (435 lines) into host/{paths,logger,startup,message-router}.js.
Split holesail-manager.js (698 lines) into holesail-manager/{state,settings,
connections,port-allocator,servers,virtual-hosts,service-tunnels,index}.js.
Top-level host.js and holesail-manager.js become thin shims so index.mjs
requires no changes. Deleted dev scratch file test-cp.mjs.
Updated CI with 12 new node --check lines for all sub-modules.
Updated docs/ARCHITECTURE.md with per-file tables for host/ and
holesail-manager/ sub-modules.
No functionality changed. No new dependencies.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Virtual host tunnel management.
|
||||
* Connects to a remote Holesail peer and binds it to a local port for HTTP proxying.
|
||||
*/
|
||||
|
||||
let Holesail = null;
|
||||
try { Holesail = require('holesail'); } catch (e) {
|
||||
if (process.stderr) process.stderr.write('[holesail-manager] Holesail not installed: ' + e.message + '\n');
|
||||
}
|
||||
|
||||
const { allocateTunnelPort, releaseTunnelPort } = require('./port-allocator.js');
|
||||
|
||||
const TUNNEL_HOST = '127.0.0.1';
|
||||
|
||||
const DEBUG = process.env.HOLESAIL_DEBUG === '1' || process.env.HOLESAIL_DEBUG === 'true';
|
||||
function debugLog(...args) {
|
||||
if (!DEBUG) return;
|
||||
const msg = '[holesail-manager:debug] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
||||
if (process.stderr) process.stderr.write(msg + '\n');
|
||||
}
|
||||
|
||||
const virtualHosts = new Map(); // hostname -> { hsUrl, holesail, localHost, localPort, state, createdAt }
|
||||
let _saveState = null;
|
||||
let _emit = null;
|
||||
let _getReadyTimeoutMs = null;
|
||||
|
||||
function init(saveStateFn, emitFn, getReadyTimeoutMsFn) {
|
||||
_saveState = saveStateFn;
|
||||
_emit = emitFn;
|
||||
_getReadyTimeoutMs = getReadyTimeoutMsFn;
|
||||
}
|
||||
|
||||
function readyWithTimeout(hs, label) {
|
||||
const ms = _getReadyTimeoutMs ? _getReadyTimeoutMs() : 0;
|
||||
if (!ms || ms <= 0) return hs.ready();
|
||||
return new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error('Tunnel ready timeout after ' + ms + 'ms for ' + label)), ms);
|
||||
hs.ready().then(() => { clearTimeout(t); resolve(); }, (e) => { clearTimeout(t); reject(e); });
|
||||
});
|
||||
}
|
||||
|
||||
async function setVirtualHost(payload) {
|
||||
let hostname = (payload.hostname || payload.hostName || '').trim();
|
||||
hostname = hostname.replace(/^https?:\/\//i, '').replace(/[/:?#].*$/, '').toLowerCase().trim();
|
||||
const hsUrl = payload.hsUrl || payload.url;
|
||||
debugLog('setVirtualHost: hostname=', hostname, 'hsUrl=', hsUrl);
|
||||
if (!Holesail) return { ok: false, error: 'Holesail module not installed' };
|
||||
if (!hostname || !hsUrl) return { ok: false, error: 'hostname and hsUrl required' };
|
||||
|
||||
const existing = virtualHosts.get(hostname);
|
||||
if (existing) {
|
||||
debugLog('setVirtualHost: replacing existing', hostname);
|
||||
if (existing.localPort) releaseTunnelPort(existing.localPort);
|
||||
if (existing.holesail) { try { await existing.holesail.close(); } catch (_) {} }
|
||||
}
|
||||
|
||||
const localPort = allocateTunnelPort();
|
||||
try {
|
||||
const hs = new Holesail({ client: true, key: hsUrl, host: TUNNEL_HOST, port: localPort });
|
||||
if (typeof hs.on === 'function') {
|
||||
hs.on('error', (err) => {
|
||||
if (process.stderr) process.stderr.write('[holesail-manager] tunnel error ' + hostname + ': ' + (err && err.message) + '\n');
|
||||
const v = virtualHosts.get(hostname);
|
||||
if (v && v.holesail === hs) v.state = 'error';
|
||||
if (_emit) _emit('tunnelError', { hostname, error: err && err.message });
|
||||
});
|
||||
hs.on('close', () => {
|
||||
const v = virtualHosts.get(hostname);
|
||||
if (v && v.holesail === hs) v.state = 'closed';
|
||||
if (_emit) _emit('tunnelClosed', { hostname });
|
||||
});
|
||||
}
|
||||
await readyWithTimeout(hs, 'vhost:' + hostname);
|
||||
virtualHosts.set(hostname, { hsUrl, holesail: hs, localHost: TUNNEL_HOST, localPort, state: 'ready', createdAt: (existing && existing.createdAt) || Date.now() });
|
||||
if (_emit) _emit('tunnelReady', { hostname, hsUrl, localHost: TUNNEL_HOST, localPort });
|
||||
if (_saveState) _saveState();
|
||||
debugLog('setVirtualHost: ok hostname=', hostname, 'localPort=', localPort);
|
||||
return { ok: true, hostname, localHost: TUNNEL_HOST, localPort, state: 'ready' };
|
||||
} catch (e) {
|
||||
debugLog('setVirtualHost: error hostname=', hostname, 'error=', e.message);
|
||||
releaseTunnelPort(localPort);
|
||||
const existingCreatedAt = existing ? existing.createdAt : undefined;
|
||||
virtualHosts.set(hostname, { hsUrl, holesail: null, localHost: null, localPort: null, state: 'error', createdAt: existingCreatedAt || Date.now() });
|
||||
if (_emit) _emit('tunnelError', { hostname, error: e.message });
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function removeVirtualHost(payload) {
|
||||
const hostname = payload.hostname || payload.hostName;
|
||||
debugLog('removeVirtualHost: hostname=', hostname);
|
||||
const entry = virtualHosts.get(hostname);
|
||||
if (!entry) return { ok: false, error: 'Virtual host not found' };
|
||||
if (entry.localPort) releaseTunnelPort(entry.localPort);
|
||||
if (entry.holesail) { try { await entry.holesail.close(); } catch (_) {} }
|
||||
virtualHosts.delete(hostname);
|
||||
if (_saveState) _saveState();
|
||||
if (_emit) _emit('tunnelClosed', { hostname });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function getVirtualHosts() {
|
||||
const list = [];
|
||||
for (const [hostname, v] of virtualHosts) {
|
||||
list.push({ hostname, hsUrl: v.hsUrl, localHost: v.localHost ?? null, localPort: v.localPort ?? null, state: v.state || 'unknown', createdAt: v.createdAt });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function getLocalPortForHostname(hostname) {
|
||||
const v = virtualHosts.get(hostname);
|
||||
return v && v.localPort != null ? v.localPort : null;
|
||||
}
|
||||
|
||||
function getLocalBackend(hostname) {
|
||||
const v = virtualHosts.get(hostname);
|
||||
const out = (!v || v.localPort == null) ? null : { host: v.localHost ?? '127.0.0.1', port: v.localPort };
|
||||
debugLog('getLocalBackend: hostname=', hostname, 'result=', out, 'virtualHostsKeys=', Array.from(virtualHosts.keys()));
|
||||
return out;
|
||||
}
|
||||
|
||||
function getVirtualHostMap() {
|
||||
const m = new Map();
|
||||
for (const [hostname, v] of virtualHosts) {
|
||||
if (v.localPort != null) m.set(hostname, v.localPort);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
async function cleanupVirtualHosts() {
|
||||
for (const [, v] of virtualHosts) {
|
||||
if (v.holesail) { try { await v.holesail.close(); } catch (_) {} }
|
||||
}
|
||||
virtualHosts.clear();
|
||||
}
|
||||
|
||||
module.exports = { init, setVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts };
|
||||
Reference in New Issue
Block a user