Initial clean project import
This commit is contained in:
@@ -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 || ''}`);
|
||||
});
|
||||
Reference in New Issue
Block a user