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>
172 lines
3.6 KiB
JavaScript
172 lines
3.6 KiB
JavaScript
'use strict';
|
|
|
|
const hooks = [];
|
|
const errHooks = [];
|
|
let called = false;
|
|
let waitingFor = 0;
|
|
let asyncTimeoutMs = 10000;
|
|
|
|
const events = {};
|
|
const filters = {};
|
|
|
|
function exit(exit, code, err) {
|
|
// Helper functions
|
|
let doExitDone = false;
|
|
|
|
function doExit() {
|
|
if (doExitDone) {
|
|
return;
|
|
}
|
|
doExitDone = true;
|
|
|
|
if (exit === true) {
|
|
// All handlers should be called even if the exit-hook handler was registered first
|
|
process.nextTick(process.exit.bind(null, code));
|
|
}
|
|
}
|
|
|
|
// Async hook callback, decrements waiting counter
|
|
function stepTowardExit() {
|
|
process.nextTick(() => {
|
|
if (--waitingFor === 0) {
|
|
doExit();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Runs a single hook
|
|
function runHook(syncArgCount, err, hook) {
|
|
// Cannot perform async hooks in `exit` event
|
|
if (exit && hook.length > syncArgCount) {
|
|
// Hook is async, expects a finish callback
|
|
waitingFor++;
|
|
|
|
if (err) {
|
|
// Pass error, calling uncaught exception handlers
|
|
return hook(err, stepTowardExit);
|
|
}
|
|
return hook(stepTowardExit);
|
|
}
|
|
|
|
// Hook is synchronous
|
|
if (err) {
|
|
// Pass error, calling uncaught exception handlers
|
|
return hook(err);
|
|
}
|
|
return hook();
|
|
}
|
|
|
|
// Only execute hooks once
|
|
if (called) {
|
|
return;
|
|
}
|
|
|
|
called = true;
|
|
|
|
// Run hooks
|
|
if (err) {
|
|
// Uncaught exception, run error hooks
|
|
errHooks.map(runHook.bind(null, 1, err));
|
|
}
|
|
hooks.map(runHook.bind(null, 0, null));
|
|
|
|
if (waitingFor) {
|
|
// Force exit after x ms (10000 by default), even if async hooks in progress
|
|
setTimeout(() => {
|
|
doExit();
|
|
}, asyncTimeoutMs);
|
|
} else {
|
|
// No asynchronous hooks, exit immediately
|
|
doExit();
|
|
}
|
|
}
|
|
|
|
// Add a hook
|
|
function add(hook) {
|
|
hooks.push(hook);
|
|
|
|
if (hooks.length === 1) {
|
|
add.hookEvent('exit');
|
|
add.hookEvent('beforeExit', 0);
|
|
add.hookEvent('SIGHUP', 128 + 1);
|
|
add.hookEvent('SIGINT', 128 + 2);
|
|
add.hookEvent('SIGTERM', 128 + 15);
|
|
add.hookEvent('SIGBREAK', 128 + 21);
|
|
|
|
// PM2 Cluster shutdown message. Caught to support async handlers with pm2, needed because
|
|
// explicitly calling process.exit() doesn't trigger the beforeExit event, and the exit
|
|
// event cannot support async handlers, since the event loop is never called after it.
|
|
add.hookEvent('message', 0, function (msg) { // eslint-disable-line prefer-arrow-callback
|
|
if (msg !== 'shutdown') {
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// New signal / event to hook
|
|
add.hookEvent = function (event, code, filter) {
|
|
events[event] = function () {
|
|
const eventFilters = filters[event];
|
|
for (let i = 0; i < eventFilters.length; i++) {
|
|
if (eventFilters[i].apply(this, arguments)) {
|
|
return;
|
|
}
|
|
}
|
|
exit(code !== undefined && code !== null, code);
|
|
};
|
|
|
|
if (!filters[event]) {
|
|
filters[event] = [];
|
|
}
|
|
|
|
if (filter) {
|
|
filters[event].push(filter);
|
|
}
|
|
process.on(event, events[event]);
|
|
};
|
|
|
|
// Unhook signal / event
|
|
add.unhookEvent = function (event) {
|
|
process.removeListener(event, events[event]);
|
|
delete events[event];
|
|
delete filters[event];
|
|
};
|
|
|
|
// List hooked events
|
|
add.hookedEvents = function () {
|
|
const ret = [];
|
|
for (const name in events) {
|
|
if ({}.hasOwnProperty.call(events, name)) {
|
|
ret.push(name);
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
// Add an uncaught exception handler
|
|
add.uncaughtExceptionHandler = function (hook) {
|
|
errHooks.push(hook);
|
|
|
|
if (errHooks.length === 1) {
|
|
process.once('uncaughtException', exit.bind(null, true, 1));
|
|
}
|
|
};
|
|
|
|
// Add an unhandled rejection handler
|
|
add.unhandledRejectionHandler = function (hook) {
|
|
errHooks.push(hook);
|
|
|
|
if (errHooks.length === 1) {
|
|
process.once('unhandledRejection', exit.bind(null, true, 1));
|
|
}
|
|
};
|
|
|
|
// Configure async force exit timeout
|
|
add.forceExitTimeout = function (ms) {
|
|
asyncTimeoutMs = ms;
|
|
};
|
|
|
|
// Export
|
|
module.exports = add;
|