tftsr-devops_investigation/node_modules/async-exit-hook/index.js
Shaun Arman 8839075805 feat: initial implementation of TFTSR IT Triage & RCA application
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>
2026-03-14 22:36:25 -05:00

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;