Files
holesail-browser/native-host/holesail-manager/state.js
T
Raven Scott 32cd108480
CI / Build & Test (push) Successful in 2m54s
fix(settings): fix latency ping and tunnel auto-reconnect defaults
- Default latencyPingEnabled to false so badges are hidden until opted in
- Fix toggleLatencyPing sync condition from !== false to === true to match
  the new default correctly
- Default tunnelAutoReconnect to true so tunnels reconnect automatically
  out of the box
- Hide latency badges in virtual hosts and service tunnels tables when
  latency ping is disabled
2026-03-01 01:10:59 -05:00

138 lines
5.1 KiB
JavaScript

/**
* State persistence for the native host.
* Loads and saves all persistent data (tunnels, settings, connections) to state.json.
* Handles migration from the legacy holesail-persist.json format.
*/
const path = require('bare-path');
const fs = require('bare-fs');
const STATE_FILENAME = 'state.json';
const LEGACY_PERSIST_FILENAME = 'holesail-persist.json';
const SETTINGS_DEFAULTS = {
proxyPort: 8443,
connectProxyPort: 8442,
readyTimeoutMs: 30000,
notifyOnDisconnect: true,
notifyOnTunnelError: true,
debug: false,
disableOnFileUrls: false,
backupRetention: 5,
backupIntervalHours: 0,
tunnelAutoReconnect: true,
latencyPingEnabled: false,
latencyPingIntervalMs: 5000
};
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');
}
let stateFilePath = null;
function setStoragePath(baseDir) {
if (baseDir && typeof baseDir === 'string') {
stateFilePath = path.join(baseDir, STATE_FILENAME);
}
}
function getStatePath() {
if (stateFilePath) return stateFilePath;
const base = process.env.HOLESAIL_BROWSER_STORAGE || path.join(__dirname, '..', 'holesail-browser-storage');
stateFilePath = path.join(base, STATE_FILENAME);
return stateFilePath;
}
function getLegacyPersistPath() {
const dir = path.dirname(getStatePath());
return path.join(dir, LEGACY_PERSIST_FILENAME);
}
function buildDefaultState() {
return {
settings: { ...SETTINGS_DEFAULTS },
servers: [],
virtualHosts: [],
serviceTunnels: [],
sshConnections: [],
rdpConnections: [],
nextServerId: 0,
nextServiceTunnelId: 0
};
}
function saveStateSync(data) {
const file = getStatePath();
const json = JSON.stringify(data, null, 2);
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, json, 'utf8');
debugLog('state saved path=', file);
} catch (e) {
if (process.stderr) process.stderr.write('[holesail-manager] state save failed: ' + e.message + ' (path: ' + file + ')\n');
}
}
function loadState() {
const file = getStatePath();
try {
const raw = fs.readFileSync(file, 'utf8');
const data = JSON.parse(raw);
if (!data || typeof data !== 'object') return buildDefaultState();
const out = {
settings: (data.settings && typeof data.settings === 'object') ? { ...SETTINGS_DEFAULTS, ...data.settings } : { ...SETTINGS_DEFAULTS },
servers: Array.isArray(data.servers) ? data.servers : [],
virtualHosts: Array.isArray(data.virtualHosts) ? data.virtualHosts : [],
serviceTunnels: Array.isArray(data.serviceTunnels) ? data.serviceTunnels : [],
sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [],
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0
};
debugLog('state loaded path=', file, 'servers=', out.servers.length, 'vhosts=', out.virtualHosts.length);
if (process.stderr && (out.servers.length || out.virtualHosts.length)) {
process.stderr.write('[holesail-manager] state loaded from ' + file + ' (servers=' + out.servers.length + ' vhosts=' + out.virtualHosts.length + ')\n');
}
return out;
} catch (e) {
if (e && e.code !== 'ENOENT') {
if (process.stderr) process.stderr.write('[holesail-manager] state load failed: ' + e.message + '\n');
return buildDefaultState();
}
}
// state.json not found — try migrating from legacy holesail-persist.json
const legacyFile = getLegacyPersistPath();
try {
const raw = fs.readFileSync(legacyFile, 'utf8');
const data = JSON.parse(raw);
if (process.stderr) process.stderr.write('[holesail-manager] migrating from ' + legacyFile + ' to ' + file + '\n');
const out = {
settings: { ...SETTINGS_DEFAULTS },
servers: Array.isArray(data.servers) ? data.servers : [],
virtualHosts: Array.isArray(data.virtualHosts) ? data.virtualHosts : [],
serviceTunnels: Array.isArray(data.serviceTunnels) ? data.serviceTunnels : [],
sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [],
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0
};
try { fs.unlinkSync(legacyFile); } catch (_) {}
if (process.stderr) process.stderr.write('[holesail-manager] migration complete, legacy file removed\n');
return out;
} catch (e) {
if (e && e.code !== 'ENOENT') {
if (process.stderr) process.stderr.write('[holesail-manager] legacy persist load failed: ' + e.message + '\n');
}
}
return buildDefaultState();
}
module.exports = { SETTINGS_DEFAULTS, setStoragePath, loadState, saveStateSync, buildDefaultState };