Files
holesail-browser/native-host/holesail-manager/index.js
T
Raven Scott 0f4b82681d
CI / Build & Test (push) Successful in 4m21s
Apply sync incrementally so unchanged tunnels stay running
- Add getStateSnapshot, applySnapshotData, setStateSaveSuppressed to holesail-manager
- Sync diffs snapshot vs current state and only removes/adds/updates changed tunnels
- Suppress saves during apply and write state.json once at the end
- Fall back to full cleanup + restore when incremental APIs are unavailable
2026-03-15 03:28:47 -04:00

241 lines
9.9 KiB
JavaScript

/**
* Holesail manager — assembles all sub-modules and exposes the same API
* as the original monolithic holesail-manager.js.
*/
const stateModule = require('./state.js');
const settingsModule = require('./settings.js');
const connectionsModule = require('./connections.js');
const serversModule = require('./servers.js');
const vhostsModule = require('./virtual-hosts.js');
const svcModule = require('./service-tunnels.js');
// ── Event emitter ─────────────────────────────────────────────────────────────
let eventEmit = null;
/**
* Register the callback used to push tunnel lifecycle events to the extension.
* Must be called before any tunnels are started so events are not lost.
* @param {Function} emit - Called as `emit(eventName, payloadObject)`.
*/
function setEventEmitter(emit) { eventEmit = emit; }
function emit(event, payload) { if (eventEmit) eventEmit(event, payload); }
// ── Shared saveState snapshot ─────────────────────────────────────────────────
// Collects current state from all sub-modules and persists it.
let onStateSaved = null;
let stateSaveSuppressed = false;
/**
* When true, saveState() is a no-op. Used during sync apply so only the final
* state is written once by the sync-manager.
* @param {boolean} v
*/
function setStateSaveSuppressed(v) {
stateSaveSuppressed = !!v;
}
/**
* Register a callback invoked after each saveState() with the snapshot object.
* Used by sync-manager to push state to autopass.
* @param {Function} fn - (snapshot: object) => void
*/
function setOnStateSaved(fn) {
onStateSaved = typeof fn === 'function' ? fn : null;
}
function saveState() {
if (stateSaveSuppressed) return;
const serversList = serversModule.getServers().map(s => ({
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
}));
const virtualHostsList = vhostsModule.getVirtualHosts()
.map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true }));
const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({
id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort
}));
const snapshot = {
version: 2,
settings: settingsModule.getSettings(),
nextServerId: serversModule.getNextServerId(),
nextServiceTunnelId: svcModule.getNextServiceTunnelId(),
servers: serversList,
virtualHosts: virtualHostsList,
serviceTunnels: serviceTunnelsList,
sshConnections: connectionsModule.getSshConnections(),
rdpConnections: connectionsModule.getRdpConnections()
};
stateModule.saveStateSync(snapshot);
if (onStateSaved) onStateSaved(snapshot);
}
/**
* Return current in-memory state in the same shape as saveState() snapshot.
* Used by sync-manager to diff and apply only changes.
* @returns {{ version: number, settings: object, nextServerId: number, nextServiceTunnelId: number, servers: Array, virtualHosts: Array, serviceTunnels: Array, sshConnections: Array, rdpConnections: Array }}
*/
function getStateSnapshot() {
const serversList = serversModule.getServers().map(s => ({
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
}));
const virtualHostsList = vhostsModule.getVirtualHosts()
.map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true }));
const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({
id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort
}));
return {
version: 2,
settings: settingsModule.getSettings(),
nextServerId: serversModule.getNextServerId(),
nextServiceTunnelId: svcModule.getNextServiceTunnelId(),
servers: serversList,
virtualHosts: virtualHostsList,
serviceTunnels: serviceTunnelsList,
sshConnections: connectionsModule.getSshConnections(),
rdpConnections: connectionsModule.getRdpConnections()
};
}
/**
* Apply non-tunnel state from a snapshot (settings, connection lists, ID counters).
* Does not start/stop tunnels; used by sync-manager before applying tunnel diffs.
* @param {object} snapshot - Snapshot with settings, sshConnections, rdpConnections, nextServerId, nextServiceTunnelId
*/
function applySnapshotData(snapshot) {
if (!snapshot || typeof snapshot !== 'object') return;
if (snapshot.settings != null) settingsModule.applyLoaded(snapshot.settings);
const ssh = Array.isArray(snapshot.sshConnections) ? snapshot.sshConnections : [];
const rdp = Array.isArray(snapshot.rdpConnections) ? snapshot.rdpConnections : [];
connectionsModule.applyLoaded(ssh, rdp);
if (typeof snapshot.nextServerId === 'number') serversModule.applyLoaded(snapshot.nextServerId);
if (typeof snapshot.nextServiceTunnelId === 'number') svcModule.applyLoaded(snapshot.nextServiceTunnelId);
}
// Inject the shared saveState and emit callbacks into each sub-module
settingsModule.init(saveState);
connectionsModule.init(saveState);
serversModule.init(saveState, settingsModule.getReadyTimeoutMs);
vhostsModule.init(saveState, emit, settingsModule.getReadyTimeoutMs, settingsModule.getTunnelAutoReconnect);
svcModule.init(saveState, emit, settingsModule.getReadyTimeoutMs, settingsModule.getTunnelAutoReconnect);
// ── Storage path ──────────────────────────────────────────────────────────────
/**
* Set the base directory used for state.json persistence.
* Must be called before `restorePersistedState`.
* @param {string} baseDir - Absolute path to the storage directory.
*/
function setStoragePath(baseDir) {
stateModule.setStoragePath(baseDir);
}
function ensureStorageDir() {
stateModule.ensureStorageDir();
}
// ── Restore persisted state ───────────────────────────────────────────────────
/**
* Load state.json and apply the persisted settings, connection lists, and ID counters
* to all sub-modules. Returns the raw persisted data so the caller can re-start tunnels.
* @returns {{settings: object, servers: Array, virtualHosts: Array, serviceTunnels: Array, sshConnections: Array, rdpConnections: Array}}
*/
function restorePersistedState() {
const loaded = stateModule.loadState();
settingsModule.applyLoaded(loaded.settings);
connectionsModule.applyLoaded(loaded.sshConnections, loaded.rdpConnections);
serversModule.applyLoaded(loaded.nextServerId);
svcModule.applyLoaded(loaded.nextServiceTunnelId);
return {
settings: settingsModule.getSettings(),
servers: loaded.servers,
virtualHosts: loaded.virtualHosts,
serviceTunnels: loaded.serviceTunnels || [],
sshConnections: connectionsModule.getSshConnections(),
rdpConnections: connectionsModule.getRdpConnections()
};
}
// ── Cleanup ───────────────────────────────────────────────────────────────────
/**
* Close all active tunnels across all sub-modules.
* Called during native host shutdown to release resources cleanly.
* @returns {Promise<void>}
*/
async function cleanup() {
await serversModule.cleanupServers();
await vhostsModule.cleanupVirtualHosts();
await svcModule.cleanupServiceTunnels();
}
// ── Re-export full original API ───────────────────────────────────────────────
module.exports = {
// Event emitter
setEventEmitter,
// Storage
setStoragePath,
ensureStorageDir,
setOnStateSaved,
setStateSaveSuppressed,
getStateSnapshot,
applySnapshotData,
// Settings
getSettings: settingsModule.getSettings,
updateSettings: settingsModule.updateSettings,
getProxyPort: settingsModule.getProxyPort,
setProxyPort: settingsModule.setProxyPort,
// SSH/RDP connection lists
getSshConnections: connectionsModule.getSshConnections,
setSshConnections: connectionsModule.setSshConnections,
getRdpConnections: connectionsModule.getRdpConnections,
setRdpConnections: connectionsModule.setRdpConnections,
// Servers
startServer: serversModule.startServer,
stopServer: serversModule.stopServer,
getServers: serversModule.getServers,
// Virtual hosts
setVirtualHost: vhostsModule.setVirtualHost,
removeVirtualHost: vhostsModule.removeVirtualHost,
getVirtualHosts: vhostsModule.getVirtualHosts,
getLocalPortForHostname: vhostsModule.getLocalPortForHostname,
getLocalBackend: vhostsModule.getLocalBackend,
getVirtualHostMap: vhostsModule.getVirtualHostMap,
// Service tunnels
startServiceTunnel: svcModule.startServiceTunnel,
stopServiceTunnel: svcModule.stopServiceTunnel,
getServiceTunnels: svcModule.getServiceTunnels,
// Lookup
lookup,
// Lifecycle
restorePersistedState,
cleanup
};
// ── Lookup (standalone, only needs Holesail) ──────────────────────────────────
let Holesail = null;
try { Holesail = require('holesail'); } catch (_) {}
/**
* Resolve an hs:// key via the DHT and return connection metadata without
* establishing a full tunnel.
* @param {object} payload
* @param {string} payload.hsUrl - The hs:// key to look up.
* @returns {Promise<{ok: boolean, host?: string, port?: number, protocol?: string, secure?: boolean, error?: string}>}
*/
async function lookup(payload) {
if (!Holesail) return { ok: false, error: 'Holesail module not installed' };
const url = payload.url || payload.hsUrl;
if (!url) return { ok: false, error: 'url required' };
try {
const result = await Holesail.lookup(url);
return { ok: true, ...result };
} catch (e) {
return { ok: false, error: e.message };
}
}