Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+741
View File
@@ -0,0 +1,741 @@
const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron');
const { spawn, spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const projectRoot = path.resolve(__dirname, '..', '..');
const outputDir = path.join(projectRoot, 'dist-release-final');
const toolLogPath = path.join(__dirname, 'oem-tool.log');
const statePath = path.join(__dirname, 'active-run.json');
const toolAppName = 'OEM Batch Builder Tool';
app.setName(toolAppName);
app.setAppUserModelId('com.oem.batch-builder-tool');
let mainWindow = null;
let running = false;
let currentProcess = null;
let latestResults = [];
let activePollTimer = null;
let activeLogOffset = 0;
let activeSessionDir = '';
let activeResultFile = '';
let activeWorkerLogFile = '';
let activeStageStatusFile = '';
let allowQuitWhileRunning = false;
const gotSingleInstanceLock = app.requestSingleInstanceLock();
if (!gotSingleInstanceLock) {
appendToolLog(`single-instance lock denied pid=${process.pid} argv=${safeJson(process.argv)}`);
app.quit();
}
appendToolLog(`process start pid=${process.pid} ppid=${process.ppid} cwd=${process.cwd()} argv=${safeJson(process.argv)}`);
function appendToolLog(message) {
try {
fs.appendFileSync(toolLogPath, `[${new Date().toISOString()}] ${message}\n`, 'utf8');
} catch {
// ignore logging failure
}
}
function safeJson(value) {
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function sendLog(payload) {
const message = String(payload.message || '');
if (payload.level === 'stdout' && shouldSuppressWindowLog(message)) return;
const displayPayload = {
...payload,
message: message.length > 6000 ? `${message.slice(0, 6000)}\n... [full output is in build-output.log]` : message,
};
appendToolLog(`${payload.level || 'info'} ${payload.jobId ? `[${payload.jobId}] ` : ''}${displayPayload.message || ''}`);
if (!mainWindow || mainWindow.isDestroyed()) return;
mainWindow.webContents.send('oem-tool:log', {
time: new Date().toISOString(),
...displayPayload,
});
}
function shouldSuppressWindowLog(message) {
const text = String(message || '');
return text.includes('[OK] Copied qrcode')
|| text.includes('[OK] Copied logo')
|| text.includes('[OK] Copied login-bg')
|| text.includes('[SKIP] pattern not matched')
|| text.includes('Processing: F:');
}
function readState() {
try {
if (!fs.existsSync(statePath)) return null;
return JSON.parse(fs.readFileSync(statePath, 'utf8').replace(/^\uFEFF/, ''));
} catch (error) {
appendToolLog(`readState failed: ${error && error.message ? error.message : String(error)}`);
return null;
}
}
function writeState(payload) {
try {
fs.writeFileSync(statePath, JSON.stringify(payload, null, 2), 'utf8');
} catch (error) {
appendToolLog(`writeState failed: ${error && error.message ? error.message : String(error)}`);
}
}
function clearStateIfFinished(payload) {
writeState({
running: false,
finishedAt: new Date().toISOString(),
outputDir,
results: payload.results || [],
message: payload.message || '',
success: Boolean(payload.success),
sessionDir: activeSessionDir,
});
}
function createWindow() {
appendToolLog('createWindow start');
const staleFailure = repairStaleRunningState();
if (staleFailure) {
appendToolLog(`repaired stale active run before window creation: ${staleFailure.message}`);
}
mainWindow = new BrowserWindow({
width: 1320,
height: 860,
minWidth: 1120,
minHeight: 720,
title: toolAppName,
backgroundColor: '#f6f7fb',
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.webContents.on('console-message', (_event, level, message, line, sourceId) => {
appendToolLog(`renderer console level=${level} ${sourceId || ''}:${line || 0} ${message}`);
});
mainWindow.webContents.on('preload-error', (_event, preloadPath, error) => {
appendToolLog(`preload-error ${preloadPath}: ${error && error.stack ? error.stack : error}`);
});
mainWindow.webContents.on('did-crash', (_event, killed) => {
appendToolLog(`renderer did-crash killed=${killed}`);
});
mainWindow.webContents.on('destroyed', () => {
appendToolLog('webContents destroyed');
});
mainWindow.on('unresponsive', () => {
appendToolLog('window unresponsive');
});
mainWindow.on('responsive', () => {
appendToolLog('window responsive');
});
const showMainWindow = () => {
if (!mainWindow || mainWindow.isDestroyed()) return;
appendToolLog(`showMainWindow visible=${mainWindow.isVisible()} minimized=${mainWindow.isMinimized()}`);
mainWindow.setTitle(toolAppName);
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.center();
mainWindow.show();
mainWindow.focus();
mainWindow.moveTop();
appendToolLog(`showMainWindow done visible=${mainWindow.isVisible()} bounds=${JSON.stringify(mainWindow.getBounds())}`);
};
mainWindow.loadFile(path.join(__dirname, 'index.html'));
showMainWindow();
mainWindow.on('page-title-updated', event => {
event.preventDefault();
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.setTitle(toolAppName);
});
mainWindow.once('ready-to-show', showMainWindow);
mainWindow.webContents.once('did-finish-load', () => setTimeout(showMainWindow, 50));
setTimeout(showMainWindow, 2000);
mainWindow.webContents.on('render-process-gone', (_event, details) => {
appendToolLog(`render-process-gone: ${JSON.stringify(details)}`);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.reload();
}
});
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
appendToolLog(`did-fail-load: ${errorCode} ${errorDescription} ${validatedURL}`);
});
mainWindow.on('close', event => {
if (running && !allowQuitWhileRunning) {
const payload = pollActiveSession();
if (payload) {
finishActiveSession({ ...payload, outputDir });
return;
}
const pid = getKnownWorkerPid();
if (!pid || !isProcessAlive(pid)) {
markStaleRunStopped(`window close allowed because worker is not alive. pid=${pid || 'unknown'}`);
return;
}
event.preventDefault();
appendToolLog(`window close requested while worker is running; keep tool window open. pid=${pid}`);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show();
mainWindow.focus();
}
}
});
mainWindow.on('closed', () => {
appendToolLog('window closed');
mainWindow = null;
if (running) {
setTimeout(() => {
if (!mainWindow) createWindow();
}, 300);
}
});
}
function readNewWorkerLogs(workerLogFile) {
if (!fs.existsSync(workerLogFile)) return;
const stat = fs.statSync(workerLogFile);
if (stat.size <= activeLogOffset) return;
const fd = fs.openSync(workerLogFile, 'r');
const buffer = Buffer.alloc(stat.size - activeLogOffset);
fs.readSync(fd, buffer, 0, buffer.length, activeLogOffset);
fs.closeSync(fd);
activeLogOffset = stat.size;
const lines = buffer.toString('utf8').split(/\r?\n/).filter(Boolean);
for (const line of lines) {
try {
sendLog(JSON.parse(line));
} catch {
sendLog({ level: 'info', message: line });
}
}
}
function stopActivePoll() {
if (activePollTimer) {
clearInterval(activePollTimer);
activePollTimer = null;
}
}
function setActiveSession(sessionDir) {
activeSessionDir = sessionDir;
activeResultFile = path.join(sessionDir, 'result.json');
activeWorkerLogFile = path.join(sessionDir, 'worker.log');
activeStageStatusFile = path.join(sessionDir, 'stage-status.json');
activeLogOffset = 0;
}
function readJsonFile(filePath) {
try {
if (!filePath || !fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
} catch (error) {
appendToolLog(`readJsonFile failed ${filePath}: ${error && error.message ? error.message : String(error)}`);
return null;
}
}
function readWorkerPidFromSession(sessionDir) {
try {
const pidFile = path.join(sessionDir, 'worker.pid');
if (!fs.existsSync(pidFile)) return null;
const parsed = Number(fs.readFileSync(pidFile, 'utf8').replace(/^\uFEFF/, '').trim());
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
} catch (error) {
appendToolLog(`readWorkerPidFromSession failed: ${error && error.message ? error.message : String(error)}`);
return null;
}
}
function waitForWorkerPidFromSession(sessionDir, timeoutMs = 5000) {
const started = Date.now();
const sleepBuffer = new SharedArrayBuffer(4);
const sleepView = new Int32Array(sleepBuffer);
while (Date.now() - started < timeoutMs) {
const pid = readWorkerPidFromSession(sessionDir);
if (pid) return pid;
Atomics.wait(sleepView, 0, 0, 100);
}
return null;
}
function makeJobsFailure(jobs, sessionDir, message) {
const safeJobs = Array.isArray(jobs) ? jobs : [];
return {
success: false,
results: safeJobs.map(job => ({
id: job.id,
brandName: job.brandName,
version: job.version,
success: false,
outputDir,
buildOutputFile: sessionDir ? path.join(sessionDir, 'build-output.log') : '',
artifacts: [],
message,
})),
outputDir,
message,
};
}
function getKnownWorkerPid() {
const state = readState();
if (currentProcess && currentProcess.pid) return currentProcess.pid;
if (state && state.pid) return state.pid;
if (activeSessionDir) return readWorkerPidFromSession(activeSessionDir);
if (state && state.sessionDir) return readWorkerPidFromSession(state.sessionDir);
return null;
}
function repairStaleRunningState() {
const state = readState();
if (!state || !state.running || !state.sessionDir) return null;
const resultPath = state.resultFile || path.join(state.sessionDir, 'result.json');
if (fs.existsSync(resultPath)) return null;
const pid = state.pid || readWorkerPidFromSession(state.sessionDir);
if (pid && isProcessAlive(pid)) return null;
const launcherStartedAt = state.launcherStartedAt ? Date.parse(state.launcherStartedAt) : 0;
if (!pid && launcherStartedAt && Date.now() - launcherStartedAt < 15000) return null;
if (activeSessionDir !== state.sessionDir) {
setActiveSession(state.sessionDir);
}
const failed = makeMissingResultFailure({
...state,
pid,
});
finishActiveSession(failed);
return failed;
}
function markStaleRunStopped(message) {
stopActivePoll();
running = false;
currentProcess = null;
writeState({
...(readState() || {}),
running: false,
stoppedAt: new Date().toISOString(),
outputDir,
sessionDir: activeSessionDir,
message,
});
appendToolLog(message);
}
function pollActiveSession() {
if (!activeSessionDir) return null;
readNewWorkerLogs(activeWorkerLogFile);
if (!fs.existsSync(activeResultFile)) return null;
try {
return JSON.parse(fs.readFileSync(activeResultFile, 'utf8'));
} catch (error) {
return {
success: false,
results: latestResults,
outputDir,
message: error && error.message ? error.message : String(error),
};
}
}
function makeMissingResultFailure(state) {
const stage = readJsonFile(path.join(state.sessionDir, 'stage-status.json')) || readJsonFile(activeStageStatusFile);
const jobs = Array.isArray(state.jobs) ? state.jobs : [];
const firstJob = jobs[0] || {};
const stageText = stage && stage.stage ? ` Last stage: ${stage.stage} (${stage.status || 'unknown'}).` : '';
const message = `Worker process ${state.pid || 'unknown'} stopped without producing result.json.${stageText} See ${state.workerLogFile || activeWorkerLogFile}`;
return {
success: false,
results: jobs.length > 0 ? jobs.map(job => ({
id: job.id,
brandName: job.brandName,
version: job.version,
success: false,
outputDir,
buildOutputFile: path.join(state.sessionDir, 'build-output.log'),
artifacts: [],
message,
stage,
})) : [{
id: firstJob.id || 'oem-0',
brandName: firstJob.brandName || 'OEM',
success: false,
outputDir,
buildOutputFile: path.join(state.sessionDir, 'build-output.log'),
artifacts: [],
message,
stage,
}],
outputDir,
message,
};
}
function finishActiveSession(payload) {
stopActivePoll();
latestResults = payload.results || [];
running = false;
currentProcess = null;
clearStateIfFinished(payload);
sendLog({ level: payload.success ? 'info' : 'error', message: payload.message || 'Build queue finished' });
}
function finishStartupFailure(jobs, sessionDir, message) {
finishActiveSession(makeJobsFailure(jobs, sessionDir, message));
}
function findNodeExe() {
const candidates = [
process.env.npm_node_execpath,
process.env.NODE_EXE,
].filter(Boolean);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
const result = spawnSync('where.exe', ['node.exe'], {
cwd: projectRoot,
encoding: 'utf8',
windowsHide: true,
});
const found = String(result.stdout || '')
.split(/\r?\n/)
.map(line => line.trim())
.find(Boolean);
if (found && fs.existsSync(found)) return found;
return 'node.exe';
}
function launchDetachedWorker(workerPath, relativeJobsFile, relativeSessionDir) {
const sessionDir = path.resolve(projectRoot, relativeSessionDir);
fs.mkdirSync(sessionDir, { recursive: true });
const nodeExe = findNodeExe();
const stdoutFd = fs.openSync(path.join(sessionDir, 'worker-main.stdout.log'), 'a');
const stderrFd = fs.openSync(path.join(sessionDir, 'worker-main.stderr.log'), 'a');
appendToolLog(`launchDetachedWorker node=${nodeExe} worker=${workerPath} jobs=${relativeJobsFile} session=${relativeSessionDir}`);
const worker = spawn(nodeExe, [workerPath, relativeJobsFile, relativeSessionDir], {
cwd: projectRoot,
windowsHide: true,
detached: true,
stdio: ['ignore', stdoutFd, stderrFd],
});
fs.closeSync(stdoutFd);
fs.closeSync(stderrFd);
if (!worker.pid) {
throw new Error('Build worker did not return a PID.');
}
fs.writeFileSync(path.join(sessionDir, 'worker.pid'), String(worker.pid), 'utf8');
worker.unref();
return {
launcherPid: null,
workerPid: worker.pid,
};
}
function startActivePoll() {
stopActivePoll();
activePollTimer = setInterval(() => {
const payload = pollActiveSession();
if (payload) {
finishActiveSession({ ...payload, outputDir });
}
}, 700);
}
function isProcessAlive(pid) {
if (!pid) return false;
try {
process.kill(Number(pid), 0);
return true;
} catch {
return false;
}
}
ipcMain.handle('oem-tool:diagnostic', async (_event, payload) => {
const message = payload && payload.message ? payload.message : 'renderer diagnostic';
appendToolLog(`renderer diagnostic: ${message} ${safeJson(payload && payload.payload ? payload.payload : {})}`);
return { ok: true };
});
ipcMain.handle('oem-tool:pick-image', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'webp', 'ico', 'icns'] }],
});
if (result.canceled) return null;
return result.filePaths[0] || null;
});
ipcMain.handle('oem-tool:run-batch', async (_event, jobs) => {
appendToolLog(`run-batch invoked jobs=${Array.isArray(jobs) ? jobs.length : 'invalid'}`);
if (running) {
appendToolLog('run-batch rejected: already running');
return { success: false, message: 'Build queue is already running', results: latestResults, outputDir };
}
if (!Array.isArray(jobs) || jobs.length === 0) {
appendToolLog('run-batch rejected: no jobs');
return { success: false, message: 'No OEM jobs selected', results: [], outputDir };
}
const sessionDir = path.join(__dirname, 'runs', `${Date.now()}`);
const jobsFile = path.join(sessionDir, 'jobs.json');
try {
fs.mkdirSync(sessionDir, { recursive: true });
fs.writeFileSync(jobsFile, JSON.stringify(jobs, null, 2), 'utf8');
running = true;
latestResults = [];
setActiveSession(sessionDir);
const relativeSessionDir = path.relative(projectRoot, sessionDir);
const relativeJobsFile = path.relative(projectRoot, jobsFile);
const workerPath = path.join('tools', 'oem-batch-builder', 'worker.cjs');
writeState({
running: true,
startedAt: new Date().toISOString(),
pid: null,
launcherPid: null,
outputDir,
sessionDir,
jobsFile,
resultFile: activeResultFile,
workerLogFile: activeWorkerLogFile,
stageStatusFile: activeStageStatusFile,
jobs,
message: 'Starting worker',
launcherStartedAt: new Date().toISOString(),
});
appendToolLog(`run-batch state written session=${sessionDir}`);
const launchInfo = launchDetachedWorker(workerPath, relativeJobsFile, relativeSessionDir);
const workerPid = launchInfo.workerPid || readWorkerPidFromSession(sessionDir);
appendToolLog(`run-batch launchDetachedWorker returned launcherPid=${launchInfo.launcherPid} workerPid=${workerPid || ''}`);
currentProcess = workerPid ? { pid: workerPid } : null;
writeState({
...(readState() || {}),
running: true,
pid: workerPid || null,
launcherPid: launchInfo.launcherPid,
outputDir,
sessionDir,
jobsFile,
resultFile: activeResultFile,
workerLogFile: activeWorkerLogFile,
stageStatusFile: activeStageStatusFile,
jobs,
launcherStartedAt: new Date().toISOString(),
message: workerPid ? `Worker started. pid=${workerPid}` : `Worker launcher started. launcherPid=${launchInfo.launcherPid}`,
});
sendLog({ level: 'info', message: workerPid ? `Started background worker. pid=${workerPid} jobs=${jobs.length}` : `Started worker launcher. pid=${launchInfo.launcherPid} jobs=${jobs.length}` });
appendToolLog(`background worker launcherPid=${launchInfo.launcherPid} workerPid=${workerPid || ''} worker=${workerPath} jobs=${relativeJobsFile}`);
startActivePoll();
appendToolLog(`run-batch accepted session=${sessionDir} launcherPid=${launchInfo.launcherPid} workerPid=${workerPid || ''}`);
return {
success: true,
accepted: true,
running: true,
sessionDir,
outputDir,
message: `Build started. Logs: ${activeWorkerLogFile}`,
results: [],
};
} catch (error) {
const message = `Failed to start build worker: ${error && error.message ? error.message : String(error)}`;
appendToolLog(`${message}\n${error && error.stack ? error.stack : ''}`);
if (running || activeSessionDir === sessionDir) {
finishStartupFailure(jobs, sessionDir, message);
} else {
appendToolLog(message);
}
return { success: false, running: false, outputDir, message, results: latestResults };
}
});
ipcMain.handle('oem-tool:get-status', async () => {
const staleFailure = repairStaleRunningState();
if (staleFailure) {
return {
running: false,
results: staleFailure.results || [],
outputDir,
sessionDir: activeSessionDir,
message: staleFailure.message,
};
}
const state = readState();
if (state && state.running && state.sessionDir) {
if (!fs.existsSync(state.sessionDir)) {
const failed = {
success: false,
results: [],
outputDir,
message: `Previous build state points to a missing session directory: ${state.sessionDir}`,
};
finishActiveSession(failed);
return { running: false, results: [], outputDir, sessionDir: state.sessionDir, message: failed.message };
}
if (!state.pid) {
const workerPid = readWorkerPidFromSession(state.sessionDir);
if (workerPid) {
state.pid = workerPid;
writeState({ ...state, pid: workerPid });
} else {
const launcherStartedAt = state.launcherStartedAt ? Date.parse(state.launcherStartedAt) : 0;
if (launcherStartedAt && Date.now() - launcherStartedAt < 15000) {
return { running: true, results: latestResults, outputDir, sessionDir: state.sessionDir, workerLogFile: state.workerLogFile, message: 'Worker launcher is starting' };
}
const failed = makeMissingResultFailure(state);
finishActiveSession(failed);
return { running: false, results: failed.results, outputDir, sessionDir: state.sessionDir, message: failed.message };
}
}
if (activeSessionDir !== state.sessionDir) {
setActiveSession(state.sessionDir);
running = true;
startActivePoll();
}
const payload = pollActiveSession();
if (payload) {
finishActiveSession({ ...payload, outputDir });
return { running: false, results: latestResults, outputDir, sessionDir: state.sessionDir };
}
if (state.pid && !isProcessAlive(state.pid)) {
const failed = makeMissingResultFailure(state);
finishActiveSession(failed);
return { running: false, results: failed.results, outputDir, sessionDir: state.sessionDir, message: failed.message };
}
return { running: true, results: latestResults, outputDir, sessionDir: state.sessionDir, workerLogFile: state.workerLogFile, stage: readJsonFile(state.stageStatusFile) };
}
return { running, results: latestResults, outputDir, sessionDir: activeSessionDir };
});
ipcMain.handle('oem-tool:cancel', async () => {
const state = readState();
const pid = currentProcess && currentProcess.pid ? currentProcess.pid : state && state.pid;
if (!pid) return { success: false, message: 'No running build process' };
spawn('taskkill', ['/F', '/T', '/PID', String(pid)], { windowsHide: true, stdio: 'ignore' });
stopActivePoll();
currentProcess = null;
running = false;
writeState({ running: false, cancelledAt: new Date().toISOString(), outputDir, sessionDir: activeSessionDir });
sendLog({ level: 'warn', message: 'Build worker cancelled' });
return { success: true };
});
ipcMain.handle('oem-tool:open-output-dir', async () => {
fs.mkdirSync(outputDir, { recursive: true });
await shell.openPath(outputDir);
return { success: true, outputDir };
});
app.on('second-instance', () => {
appendToolLog('second-instance received');
if (!mainWindow || mainWindow.isDestroyed()) {
createWindow();
return;
}
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.center();
mainWindow.show();
mainWindow.focus();
});
if (gotSingleInstanceLock) {
app.whenReady().then(() => {
appendToolLog(`app ready pid=${process.pid}`);
createWindow();
}).catch(error => {
appendToolLog(`app ready failed: ${error && error.stack ? error.stack : error}`);
});
}
app.on('will-quit', () => {
appendToolLog(`app will-quit running=${running} activeSession=${activeSessionDir || ''}`);
});
app.on('quit', (_event, exitCode) => {
appendToolLog(`app quit exitCode=${exitCode} running=${running} activeSession=${activeSessionDir || ''}`);
});
app.on('before-quit', event => {
appendToolLog(`before-quit running=${running} allowQuitWhileRunning=${allowQuitWhileRunning}`);
if (running) {
if (!allowQuitWhileRunning) {
event.preventDefault();
appendToolLog('before-quit ignored while worker is running');
if (!mainWindow || mainWindow.isDestroyed()) {
createWindow();
} else {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
return;
}
writeState({
...(readState() || {}),
running: true,
outputDir,
sessionDir: activeSessionDir,
resultFile: activeResultFile,
workerLogFile: activeWorkerLogFile,
stageStatusFile: activeStageStatusFile,
});
}
});
app.on('window-all-closed', () => {
if (running && !allowQuitWhileRunning) {
appendToolLog('window-all-closed ignored while worker is running');
return;
}
app.quit();
});
process.on('uncaughtException', error => {
appendToolLog(`uncaughtException: ${error && error.stack ? error.stack : error}`);
sendLog({ level: 'error', message: `Tool error: ${error && error.message ? error.message : String(error)}` });
});
process.on('unhandledRejection', error => {
appendToolLog(`unhandledRejection: ${error && error.stack ? error.stack : error}`);
sendLog({ level: 'error', message: `Tool error: ${error && error.message ? error.message : String(error)}` });
});
process.on('SIGINT', () => {
appendToolLog(`process SIGINT running=${running} activeSession=${activeSessionDir || ''}`);
});
process.on('SIGTERM', () => {
appendToolLog(`process SIGTERM running=${running} activeSession=${activeSessionDir || ''}`);
});
process.on('beforeExit', code => {
appendToolLog(`process beforeExit code=${code} running=${running} activeSession=${activeSessionDir || ''}`);
});
process.on('exit', code => {
appendToolLog(`process exit code=${code} running=${running} activeSession=${activeSessionDir || ''}`);
});