This commit is contained in:
tianmo committed 2026-09-10 14:04:53 +08:00
1 parent 9635428f78
commit ef01cdd6ee
6 files changed
+172 -66

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
'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;
});
+3 -3
View File
@@ -359,14 +359,14 @@ function collectSpineFiles(roots, validFolders) {
* 很短的缓存,下一轮轮询仍会重新扫描,因此既降低大文件夹导入开销,也不会漏掉
* 正在落盘的新文件。
*/
function collectSpineFilesCached(filePath, roots) {
function collectSpineFilesCached(filePath, roots, refresh) {
const libRoot = findLibraryRoot(filePath);
// Outside an Eagle library, sibling asset folders are independent search
// scopes. Sharing their parent as a cache key can return another skeleton.
const key = libRoot || JSON.stringify([...roots].sort());
const now = Date.now();
const cached = fileIndexCache.get(key);
if (cached && now - cached.time < 1500) return cached.files;
if (!refresh && cached && now - cached.time < 1500) return cached.files;
const files = collectSpineFiles(roots, null);
fileIndexCache.set(key, { time: now, files });
return files;
@@ -434,7 +434,7 @@ function discoverAssets(filePath, options) {
const allowedImportDelta = allowCrossBatch ? maxCrossBatchDeltaMs : 30000;
let files = collectSpineFiles(roots, validFolders);
if (strictStem) {
const allFiles = collectSpineFilesCached(filePath, roots);
const allFiles = collectSpineFilesCached(filePath, roots, options.refreshIndex);
if (allowCrossBatch && validFolders && validFolders.size) {
files = allFiles.filter((f) => foldersOfItem(f.fullPath).some((id) => validFolders.has(id)));
} else {