Skill Tracer
Skills › HyperFrames › media-use › config-lock.mjs

config-lock.mjs

45 lines · skills/media-use/scripts/lib/config-lock.mjs at a595755

1// The one lock on ~/.hyperframes/config.json, for the CLI, its auto-update step and media-use. The auto-update step
2// embeds this function's source, so it must use only its arguments and globals.
3export function withFileLock(lockPath, fs, task) {
4 const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
5 const started = Date.now();
6 for (;;) {
7 let fd;
8 try {
9 fd = fs.openSync(lockPath, "wx");
10 } catch (error) {
11 if (error.code !== "EEXIST") throw error;
12 // Never taken over: a lock older than any hold was left by a process that stopped, and a person removes it.
13 let leftover = false;
14 try {
15 leftover = Date.now() - fs.statSync(lockPath).mtimeMs > 5000;
16 } catch {}
17 if (leftover || Date.now() - started > 10000)
18 throw Object.assign(
19 new Error(
20 `Settings are locked by another hyperframes process. If none is running, delete ${lockPath}`,
21 ),
22 { code: "HF_SETTINGS_LOCKED" },
23 );
24 Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
25 continue;
26 }
27 try {
28 fs.writeSync(fd, token);
29 } catch (error) {
30 fs.rmSync(lockPath, { force: true });
31 throw error;
32 } finally {
33 fs.closeSync(fd);
34 }
35 break;
36 }
37 try {
38 return task();
39 } finally {
40 // No code removes another's lock, so release needs no check (only a person deleting a live one defeats it).
41 try {
42 fs.rmSync(lockPath);
43 } catch {}
44 }