CI / Build & Test (push) Successful in 2m54s
- 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
138 lines
5.1 KiB
JavaScript
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 };
|