'use strict'; const fs = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); const { spawn } = require('child_process'); const utils = require('./spine-utils'); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); function enqueue(src) { const library = utils.findLibraryRoot(src); if (!library) return; // Standalone tests/previews do not need Eagle refresh. const key = crypto.createHash('sha256').update(__dirname + library).digest('hex'); const directory = path.join(os.tmpdir(), 'spineviewer-reconcile', key); fs.mkdirSync(directory, { recursive: true }); const job = path.join(directory, crypto.createHash('sha256').update(src).digest('hex') + '.json'); const temp = job + '.' + crypto.randomBytes(8).toString('hex') + '.tmp'; fs.writeFileSync(temp, JSON.stringify({ src, deadline: Date.now() + 120000 })); fs.renameSync(temp, job); // Electron's Node mode runs the local worker without opening another Eagle. const child = spawn(process.execPath, [__filename, directory], { detached: true, stdio: 'ignore', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } }); child.on('error', error => { try { fs.appendFileSync(path.join(__dirname, '../thumbnails.log'), '[worker] ' + String(error) + '\n'); } catch (_) {} }); child.unref(); } async function run(directory) { const lock = path.join(directory, 'worker.lock'); let fd; try { fd = fs.openSync(lock, 'wx'); } catch (error) { if (error.code !== 'EEXIST') throw error; let pid; try { pid = Number(fs.readFileSync(lock, 'utf8')); } catch (_) { return; } if (!pid) return; // Another worker is still writing its lock. try { process.kill(pid, 0); return; } catch (e) { if (e.code !== 'ESRCH') return; } try { fs.unlinkSync(lock); } catch (_) { return; } return run(directory); } fs.writeFileSync(fd, String(process.pid)); fs.closeSync(fd); const snapshots = new Map(); const nextChecks = new Map(); const thumbnail = require('../thumbnail/spine'); try { let idle = 0; while (idle < 2) { await sleep(1000); // Allow the importing callback/window to finish. const jobs = fs.readdirSync(directory).filter(name => name.endsWith('.json')); if (!jobs.length) { idle++; continue; } idle = 0; for (const name of jobs) { const file = path.join(directory, name); let raw; try { raw = fs.readFileSync(file, 'utf8'); const job = JSON.parse(raw); if (Date.now() < (nextChecks.get(name) || 0)) continue; const remaining = job.deadline - Date.now(); nextChecks.set(name, Date.now() + (remaining > 110000 ? 1000 : remaining > 80000 ? 2500 : 5000)); const result = Date.now() > job.deadline ? { complete: true } : await thumbnail.reconcileOnce(job.src, snapshots.get(name)); snapshots.set(name, result.signature); if (result.complete && fs.readFileSync(file, 'utf8') === raw) { fs.unlinkSync(file); snapshots.delete(name); nextChecks.delete(name); } } catch (error) { if (error.code !== 'ENOENT') fs.appendFileSync(path.join(__dirname, '../thumbnails.log'), '[worker] ' + String(error) + '\n'); } } } } finally { fs.unlinkSync(lock); // Cover an enqueue racing the final idle check and lock release. if (fs.readdirSync(directory).some(name => name.endsWith('.json'))) await run(directory); } } module.exports = { enqueue, run }; if (require.main === module) run(process.argv[2]).catch(error => { try { fs.appendFileSync(path.join(__dirname, '../thumbnails.log'), '[worker] ' + String(error) + '\n'); } catch (_) {} process.exitCode = 1; });