| 550 | } = require('./src/adapters/scripts/_lockPaths'); |
| 551 | |
| 552 | function _writeLockAtomic(lockFile, payload) { |
| 553 | // Round-6 (§19.8): the previous implementation used tmp + rename, which |
| 554 | // makes the WRITE atomic but not the OWNERSHIP claim. Two processes |
| 555 | // could both rename their own tmp file over the same lockFile (rename |
| 556 | // is atomic per call but successive renames overwrite each other), then |
| 557 | // each read it back and -- if the second rename happened between the |
| 558 | // first process's rename and its read-back -- see the OTHER process's |
| 559 | // PID. Each then concludes "I lost the race" and exits, leaving the |
| 560 | // lockFile owned by no live process. Symmetrically, two processes can |
| 561 | // each see their own PID if the reads happen between their respective |
| 562 | // renames, and both conclude they won. |
| 563 | // |
| 564 | // The proper primitive is link(2): given a unique tmp file, link to the |
| 565 | // target path fails atomically with EEXIST if the target already |
| 566 | // exists. Only one of N concurrent linkers succeeds. |
| 567 | // NOTE(windows): mode 0o700 / 0o600 are silently ignored on Windows. |
| 568 | // The lock directory and tmp file will NOT be owner-only on Windows. |
| 569 | // Isolation relies solely on the user-profile directory ACLs. |
| 570 | const dir = path.dirname(lockFile); |
| 571 | try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch (_) {} |
| 572 | const tmp = lockFile + '.' + process.pid + '.tmp'; |
| 573 | fs.writeFileSync(tmp, payload, { encoding: 'utf8', mode: 0o600 }); |
| 574 | // link() requires the target NOT to exist. The caller in the takeover |
| 575 | // path has already unlinked the stale lockFile via fs.unlinkSync |
| 576 | // (ignoring ENOENT). If a concurrent process beat us to the link, our |
| 577 | // linkSync below throws EEXIST -- we surface that to the caller and |
| 578 | // clean up our tmp. |
| 579 | // |
| 580 | // EXDEV: fs.link() fails with EXDEV when tmp and lockFile are on different |
| 581 | // volumes (can happen on Windows when EVOLVER_LOCK_DIR points to a drive |
| 582 | // other than the tmp dir). Fall back to renameSync, which Node.js handles |
| 583 | // cross-device by copying + deleting. rename is not atomic in this path, |
| 584 | // so the EEXIST guard is lost, but this is an unusual configuration and |
| 585 | // the result is still safe (worst case: two daemons both think they won, |
| 586 | // the second write wins, the first will exit on its next tick when it |
| 587 | // reads back a foreign PID via the heartbeat). |
| 588 | try { |
| 589 | fs.linkSync(tmp, lockFile); |
| 590 | } catch (err) { |
| 591 | if (err && err.code === 'EXDEV') { |
| 592 | // Cross-device: rename falls back to copy+delete inside Node.js; this |
| 593 | // loses the atomic-EEXIST guarantee but is better than hard-failing. |
| 594 | try { |
| 595 | fs.renameSync(tmp, lockFile); |
| 596 | } catch (renameErr) { |
| 597 | try { fs.unlinkSync(tmp); } catch (_) {} |
| 598 | throw renameErr; |
| 599 | } |
| 600 | return; // tmp has been consumed by renameSync, skip unlinkSync below |
| 601 | } |
| 602 | try { fs.unlinkSync(tmp); } catch (_) {} |
| 603 | throw err; |
| 604 | } |
| 605 | try { fs.unlinkSync(tmp); } catch (_) {} |
| 606 | } |
| 607 | |
| 608 | function _readLockPayload(lockFile) { |
| 609 | try { |