742 lines
25 KiB
JavaScript
742 lines
25 KiB
JavaScript
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 || ''}`);
|
|
});
|