galleria_photo_face_rknn_op.../tools/runFaceScanDaemon.js
2026-10-03 12:16:58 +02:00

428 lines
7.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const db = require('../db/knex');
const {
processNextJob,
resetStaleProcessingJobs,
} = require('../api_v1/scanner/faceScanWorker');
const PROJECT_ROOT =
path.resolve(__dirname, '..');
const DEFAULT_LOCK_PATH =
path.join(PROJECT_ROOT, 'face-scan-daemon.lock');
function parsePositiveInteger(
value,
optionName,
) {
const number = Number(value);
if (
!Number.isInteger(number)
|| number <= 0
) {
throw new Error(
`${optionName} deve essere un intero positivo`,
);
}
return number;
}
function parseArguments(argv) {
const options = {
pollMs: 2000,
resetStaleMinutes: 30,
staleCheckMs: 60000,
lockPath:
process.env.FACE_SCAN_DAEMON_LOCK
|| DEFAULT_LOCK_PATH,
};
for (
let index = 0;
index < argv.length;
index += 1
) {
const argument = argv[index];
const value = argv[index + 1];
if (argument === '--poll-ms') {
if (value == null) {
throw new Error(
'Valore mancante dopo --poll-ms',
);
}
options.pollMs = parsePositiveInteger(
value,
'--poll-ms',
);
index += 1;
continue;
}
if (
argument
=== '--reset-stale-minutes'
) {
if (value == null) {
throw new Error(
'Valore mancante dopo '
+ '--reset-stale-minutes',
);
}
options.resetStaleMinutes =
parsePositiveInteger(
value,
'--reset-stale-minutes',
);
index += 1;
continue;
}
if (argument === '--stale-check-ms') {
if (value == null) {
throw new Error(
'Valore mancante dopo '
+ '--stale-check-ms',
);
}
options.staleCheckMs =
parsePositiveInteger(
value,
'--stale-check-ms',
);
index += 1;
continue;
}
if (argument === '--lock-file') {
if (!value) {
throw new Error(
'Valore mancante dopo --lock-file',
);
}
options.lockPath = path.resolve(value);
index += 1;
continue;
}
throw new Error(
`Argomento sconosciuto: ${argument}`,
);
}
return options;
}
function wait(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
function isProcessAlive(pid) {
if (
!Number.isInteger(pid)
|| pid <= 0
) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error.code === 'EPERM';
}
}
function readLock(lockPath) {
try {
const text = fs.readFileSync(
lockPath,
'utf8',
);
return JSON.parse(text);
} catch {
return null;
}
}
function acquireLock(lockPath) {
const lockDirectory =
path.dirname(lockPath);
fs.mkdirSync(
lockDirectory,
{ recursive: true },
);
try {
const descriptor = fs.openSync(
lockPath,
'wx',
);
const lockData = {
pid: process.pid,
started_at:
new Date().toISOString(),
project_root: PROJECT_ROOT,
};
fs.writeFileSync(
descriptor,
JSON.stringify(lockData, null, 2)
+ '\n',
'utf8',
);
fs.closeSync(descriptor);
return;
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
}
const existingLock = readLock(lockPath);
const existingPid =
Number(existingLock?.pid);
if (isProcessAlive(existingPid)) {
throw new Error(
`Daemon già attivo con PID `
+ `${existingPid}. Lock: ${lockPath}`,
);
}
console.warn(
'[faceScanDaemon] rimuovo lock obsoleto:',
{
lock_path: lockPath,
previous_pid:
existingLock?.pid ?? null,
},
);
fs.unlinkSync(lockPath);
acquireLock(lockPath);
}
function releaseLock(lockPath) {
const lock = readLock(lockPath);
if (
lock
&& Number(lock.pid) !== process.pid
) {
console.warn(
'[faceScanDaemon] lock appartenente '
+ 'a un altro processo, non rimosso:',
lockPath,
);
return;
}
try {
fs.unlinkSync(lockPath);
} catch (error) {
if (error.code !== 'ENOENT') {
console.warn(
'[faceScanDaemon] impossibile '
+ 'rimuovere il lock:',
error.message,
);
}
}
}
async function getQueueStatus() {
const rows = await db('face_scan_jobs')
.select('status')
.count({ count: '*' })
.groupBy('status');
const status = {
pending: 0,
processing: 0,
done: 0,
error: 0,
};
for (const row of rows) {
status[row.status] =
Number(row.count || 0);
}
return status;
}
async function main() {
const options = parseArguments(
process.argv.slice(2),
);
let stopRequested = false;
let lastStaleCheck = 0;
acquireLock(options.lockPath);
const requestStop = (signal) => {
if (stopRequested) {
console.log(
`[faceScanDaemon] ${signal}: `
+ 'arresto già richiesto',
);
return;
}
stopRequested = true;
console.log(
`[faceScanDaemon] ${signal}: `
+ 'arresto dopo il job corrente',
);
};
process.on(
'SIGINT',
() => requestStop('SIGINT'),
);
process.on(
'SIGTERM',
() => requestStop('SIGTERM'),
);
const initialReset =
await resetStaleProcessingJobs(
options.resetStaleMinutes,
);
lastStaleCheck = Date.now();
console.log(
'[faceScanDaemon] avviato',
{
pid: process.pid,
project_root: PROJECT_ROOT,
poll_ms: options.pollMs,
stale_check_ms:
options.staleCheckMs,
reset_stale_minutes:
options.resetStaleMinutes,
stale_jobs_reset: initialReset,
lock_path: options.lockPath,
queue: await getQueueStatus(),
},
);
let processed = 0;
let succeeded = 0;
let failed = 0;
let rescheduled = 0;
while (!stopRequested) {
const now = Date.now();
if (
now - lastStaleCheck
>= options.staleCheckMs
) {
const resetJobs =
await resetStaleProcessingJobs(
options.resetStaleMinutes,
);
lastStaleCheck = now;
if (resetJobs > 0) {
console.log(
'[faceScanDaemon] job obsoleti '
+ 'ripristinati:',
resetJobs,
);
}
}
const result = await processNextJob();
if (result) {
processed += 1;
if (result.status === 'done') {
succeeded += 1;
} else if (
result.status === 'error'
) {
failed += 1;
} else if (
result.status === 'pending'
) {
rescheduled += 1;
}
continue;
}
await wait(options.pollMs);
}
console.log(
'[faceScanDaemon] terminato',
{
pid: process.pid,
jobs_processed: processed,
jobs_succeeded: succeeded,
jobs_failed: failed,
jobs_rescheduled: rescheduled,
queue: await getQueueStatus(),
},
);
}
let lockPath = null;
(async () => {
const options = parseArguments(
process.argv.slice(2),
);
lockPath = options.lockPath;
await main();
})()
.catch((error) => {
console.error(
'[faceScanDaemon] errore fatale:',
error.stack
|| error.message
|| error,
);
process.exitCode = 1;
})
.finally(async () => {
if (lockPath) {
releaseLock(lockPath);
}
await db.destroy();
});