Implements Phases 1-8 of the TFTSR implementation plan. Rust backend (Tauri 2.x, src-tauri/): - Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama - PII detection engine: 11 regex patterns with overlap resolution - SQLCipher AES-256 encrypted database with 10 versioned migrations - 28 Tauri IPC commands for triage, analysis, document, and system ops - Ollama: hardware probe, model recommendations, pull/delete with events - RCA and blameless post-mortem Markdown document generators - PDF export via printpdf - Audit log: SHA-256 hash of every external data send - Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2) Frontend (React 18 + TypeScript + Vite, src/): - 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings - 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives - 3 Zustand stores: session, settings (persisted), history - Type-safe tauriCommands.ts matching Rust backend types exactly - 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs) DevOps: - .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push - .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload Verified: - cargo check: zero errors - tsc --noEmit: zero errors - vitest run: 13/13 unit tests passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
186 lines
4.3 KiB
JavaScript
186 lines
4.3 KiB
JavaScript
import net from 'node:net';
|
|
import os from 'node:os';
|
|
|
|
class Locked extends Error {
|
|
constructor(port) {
|
|
super(`${port} is locked`);
|
|
}
|
|
}
|
|
|
|
const lockedPorts = {
|
|
old: new Set(),
|
|
young: new Set(),
|
|
};
|
|
|
|
// On this interval, the old locked ports are discarded,
|
|
// the young locked ports are moved to old locked ports,
|
|
// and a new young set for locked ports are created.
|
|
const releaseOldLockedPortsIntervalMs = 1000 * 15;
|
|
|
|
const minPort = 1024;
|
|
const maxPort = 65_535;
|
|
|
|
// Lazily create timeout on first use
|
|
let timeout;
|
|
|
|
const getLocalHosts = () => {
|
|
const interfaces = os.networkInterfaces();
|
|
|
|
// Add undefined value for createServer function to use default host,
|
|
// and default IPv4 host in case createServer defaults to IPv6.
|
|
const results = new Set([undefined, '0.0.0.0']);
|
|
|
|
for (const _interface of Object.values(interfaces)) {
|
|
for (const config of _interface) {
|
|
results.add(config.address);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
const checkAvailablePort = options =>
|
|
new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.unref();
|
|
server.on('error', reject);
|
|
|
|
server.listen(options, () => {
|
|
const {port} = server.address();
|
|
server.close(() => {
|
|
resolve(port);
|
|
});
|
|
});
|
|
});
|
|
|
|
const getAvailablePort = async (options, hosts) => {
|
|
if (options.host || options.port === 0) {
|
|
return checkAvailablePort(options);
|
|
}
|
|
|
|
for (const host of hosts) {
|
|
try {
|
|
await checkAvailablePort({port: options.port, host}); // eslint-disable-line no-await-in-loop
|
|
} catch (error) {
|
|
if (!['EADDRNOTAVAIL', 'EINVAL'].includes(error.code)) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
return options.port;
|
|
};
|
|
|
|
const portCheckSequence = function * (ports) {
|
|
if (ports) {
|
|
yield * ports;
|
|
}
|
|
|
|
yield 0; // Fall back to 0 if anything else failed
|
|
};
|
|
|
|
export default async function getPorts(options) {
|
|
let ports;
|
|
let exclude = new Set();
|
|
|
|
if (options) {
|
|
if (options.port) {
|
|
ports = typeof options.port === 'number' ? [options.port] : options.port;
|
|
}
|
|
|
|
if (options.exclude) {
|
|
const excludeIterable = options.exclude;
|
|
|
|
if (typeof excludeIterable[Symbol.iterator] !== 'function') {
|
|
throw new TypeError('The `exclude` option must be an iterable.');
|
|
}
|
|
|
|
for (const element of excludeIterable) {
|
|
if (typeof element !== 'number') {
|
|
throw new TypeError('Each item in the `exclude` option must be a number corresponding to the port you want excluded.');
|
|
}
|
|
|
|
if (!Number.isSafeInteger(element)) {
|
|
throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`);
|
|
}
|
|
}
|
|
|
|
exclude = new Set(excludeIterable);
|
|
}
|
|
}
|
|
|
|
if (timeout === undefined) {
|
|
timeout = setTimeout(() => {
|
|
timeout = undefined;
|
|
|
|
lockedPorts.old = lockedPorts.young;
|
|
lockedPorts.young = new Set();
|
|
}, releaseOldLockedPortsIntervalMs);
|
|
|
|
// Does not exist in some environments (Electron, Jest jsdom env, browser, etc).
|
|
if (timeout.unref) {
|
|
timeout.unref();
|
|
}
|
|
}
|
|
|
|
const hosts = getLocalHosts();
|
|
|
|
for (const port of portCheckSequence(ports)) {
|
|
try {
|
|
if (exclude.has(port)) {
|
|
continue;
|
|
}
|
|
|
|
let availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop
|
|
while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
|
|
if (port !== 0) {
|
|
throw new Locked(port);
|
|
}
|
|
|
|
availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop
|
|
}
|
|
|
|
lockedPorts.young.add(availablePort);
|
|
|
|
return availablePort;
|
|
} catch (error) {
|
|
if (!['EADDRINUSE', 'EACCES'].includes(error.code) && !(error instanceof Locked)) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error('No available ports found');
|
|
}
|
|
|
|
export function portNumbers(from, to) {
|
|
if (!Number.isInteger(from) || !Number.isInteger(to)) {
|
|
throw new TypeError('`from` and `to` must be integer numbers');
|
|
}
|
|
|
|
if (from < minPort || from > maxPort) {
|
|
throw new RangeError(`'from' must be between ${minPort} and ${maxPort}`);
|
|
}
|
|
|
|
if (to < minPort || to > maxPort) {
|
|
throw new RangeError(`'to' must be between ${minPort} and ${maxPort}`);
|
|
}
|
|
|
|
if (from > to) {
|
|
throw new RangeError('`to` must be greater than or equal to `from`');
|
|
}
|
|
|
|
const generator = function * (from, to) {
|
|
for (let port = from; port <= to; port++) {
|
|
yield port;
|
|
}
|
|
};
|
|
|
|
return generator(from, to);
|
|
}
|
|
|
|
export function clearLockedPorts() {
|
|
lockedPorts.old.clear();
|
|
lockedPorts.young.clear();
|
|
}
|