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.
120 lines
4.1 KiB
JavaScript
120 lines
4.1 KiB
JavaScript
/**
|
|
* Holesail server tunnel management.
|
|
* Exposes a local TCP/UDP port to the Holesail P2P network (server mode).
|
|
*/
|
|
|
|
let Holesail = null;
|
|
try { Holesail = require('holesail'); } catch (e) {
|
|
if (process.stderr) process.stderr.write('[holesail-manager] Holesail not installed: ' + e.message + '\n');
|
|
}
|
|
|
|
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 servers = new Map(); // serverId -> { holesail, port, host, secure, udp, label, url, createdAt }
|
|
let nextServerId = 0;
|
|
let _saveState = null;
|
|
let _getReadyTimeoutMs = null;
|
|
|
|
function init(saveStateFn, getReadyTimeoutMsFn) {
|
|
_saveState = saveStateFn;
|
|
_getReadyTimeoutMs = getReadyTimeoutMsFn;
|
|
}
|
|
|
|
function applyLoaded(loadedNextServerId) {
|
|
if (loadedNextServerId > nextServerId) nextServerId = loadedNextServerId;
|
|
}
|
|
|
|
function parseServerIdNum(serverId) {
|
|
if (!serverId || typeof serverId !== 'string') return 0;
|
|
const m = serverId.match(/^server_(\d+)$/);
|
|
return m ? Math.max(0, parseInt(m[1], 10)) : 0;
|
|
}
|
|
|
|
function generateServerId() {
|
|
nextServerId += 1;
|
|
return 'server_' + nextServerId;
|
|
}
|
|
|
|
function useServerId(givenId) {
|
|
if (givenId && typeof givenId === 'string' && givenId.startsWith('server_')) {
|
|
const n = parseServerIdNum(givenId);
|
|
if (n >= nextServerId) nextServerId = n + 1;
|
|
return givenId;
|
|
}
|
|
return generateServerId();
|
|
}
|
|
|
|
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 startServer(payload) {
|
|
if (!Holesail) return { ok: false, error: 'Holesail module not installed' };
|
|
const port = payload.port || 3000;
|
|
const host = payload.host || '127.0.0.1';
|
|
const secure = payload.secure !== false;
|
|
const udp = payload.udp === true;
|
|
const label = (payload.label || '').trim();
|
|
const serverId = useServerId(payload.serverId);
|
|
debugLog('startServer: serverId=', serverId, 'port=', port, 'host=', host, 'secure=', secure, 'udp=', udp, 'label=', label);
|
|
if (servers.has(serverId)) {
|
|
debugLog('startServer: serverId already in use');
|
|
return { ok: false, error: 'Server id already in use' };
|
|
}
|
|
try {
|
|
const hs = new Holesail({ server: true, port, host, secure, udp });
|
|
await readyWithTimeout(hs, 'server:' + serverId);
|
|
const url = hs.info.url;
|
|
servers.set(serverId, { holesail: hs, port: hs.info.port, host: hs.info.host, secure, udp, label, url, createdAt: Date.now() });
|
|
if (_saveState) _saveState();
|
|
debugLog('startServer: ok serverId=', serverId, 'url=', url);
|
|
return { ok: true, serverId, url, port: hs.info.port, host: hs.info.host, secure, udp, label };
|
|
} catch (e) {
|
|
debugLog('startServer: error', e.message);
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}
|
|
|
|
async function stopServer(payload) {
|
|
const serverId = payload.serverId;
|
|
debugLog('stopServer: serverId=', serverId);
|
|
const entry = servers.get(serverId);
|
|
if (!entry) {
|
|
debugLog('stopServer: not found');
|
|
return { ok: false, error: 'Server not found' };
|
|
}
|
|
try { await entry.holesail.close(); } catch (_) {}
|
|
servers.delete(serverId);
|
|
if (_saveState) _saveState();
|
|
return { ok: true };
|
|
}
|
|
|
|
function getServers() {
|
|
const list = [];
|
|
for (const [id, s] of servers) {
|
|
list.push({ id, serverId: id, port: s.port, host: s.host, url: s.url, secure: s.secure, udp: s.udp || false, label: s.label || '', createdAt: s.createdAt });
|
|
}
|
|
return list;
|
|
}
|
|
|
|
function getNextServerId() { return nextServerId; }
|
|
|
|
async function cleanupServers() {
|
|
for (const [, s] of servers) {
|
|
try { await s.holesail.close(); } catch (_) {}
|
|
}
|
|
servers.clear();
|
|
}
|
|
|
|
module.exports = { init, applyLoaded, startServer, stopServer, getServers, getNextServerId, cleanupServers };
|