mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
Retry fs.watch triggers until the event lands: the 1751 flake dies
- Node-on-macOS backs fs.watch with an FSEvents stream that starts asynchronously, so both fixtures' single delayed touch could slip into the startup window and vanish, the persistent watcher holding Node's loop until the 300s hang detector fired - 1751 rewrites the target on a 50ms interval until the first delivery closes everything; the options-record pins (three-arg form with undocumented keys, options-only form, readline crlfDelay) and the exact stdout bytes are unchanged - 1564 gets the same treatment in both phases: interval appends for the change phase, name flipped out of and back into existence for the rename phase - a 600-kick bound turns a genuinely dead watcher into a loud nonzero exit instead of a silent hang - the compiled side never raced (kqueue/inotify register synchronously at the fs.watch call), so only the Node oracle behavior changes
This commit is contained in:
@@ -3,7 +3,12 @@
|
||||
// idempotence, and loop liveness (an open watcher holds the process; the
|
||||
// program exits once every watcher closes). Each phase closes on its FIRST
|
||||
// event — kqueue and FSEvents coalesce differently, so exact event counts
|
||||
// are deliberately unobserved.
|
||||
// are deliberately unobserved. Triggers RETRY on an interval until the
|
||||
// event lands: Node-on-macOS's FSEvents stream starts asynchronously, so a
|
||||
// single delayed touch can slip into the startup window and be lost, the
|
||||
// persistent watcher holding the loop forever (1751's documented flake
|
||||
// shape). The bound turns a genuinely dead watcher into a loud nonzero
|
||||
// exit instead of a silent hang.
|
||||
import * as fs from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -24,12 +29,18 @@ try {
|
||||
|
||||
// Phase 2 (started from phase 1's close): the name disappearing delivers
|
||||
// "rename" to a zero-parameter listener; the FSWatcher | null local is
|
||||
// the portless watcher shape.
|
||||
// the portless watcher shape. The retried trigger alternates the name out
|
||||
// of and back into existence — every flip is a name event, so the phase
|
||||
// completes on whichever delivery arrives first.
|
||||
let renameWatcher: fs.FSWatcher | null = null;
|
||||
function startRenamePhase(): void {
|
||||
const mover = join(dir, "mover.txt");
|
||||
fs.writeFileSync(mover, "m");
|
||||
let renameFired = false;
|
||||
renameWatcher = fs.watch(mover, () => {
|
||||
if (renameFired) return; // a delivery already queued when the first one closed us
|
||||
renameFired = true;
|
||||
clearInterval(renameKick);
|
||||
console.log("rename phase fired");
|
||||
if (renameWatcher) {
|
||||
renameWatcher.close();
|
||||
@@ -39,22 +50,45 @@ function startRenamePhase(): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
console.log("all closed");
|
||||
});
|
||||
setTimeout(() => {
|
||||
fs.rmSync(mover);
|
||||
}, 30);
|
||||
let renameKicks = 0;
|
||||
const renameKick = setInterval(() => {
|
||||
if (renameFired) return;
|
||||
renameKicks += 1;
|
||||
if (renameKicks > 600) {
|
||||
console.error("rename event never delivered");
|
||||
process.exit(1);
|
||||
}
|
||||
if (fs.existsSync(mover)) {
|
||||
fs.rmSync(mover);
|
||||
} else {
|
||||
fs.writeFileSync(mover, "m");
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Phase 1: a rewrite-in-place delivers "change" with the event type.
|
||||
// Phase 1: a rewrite-in-place delivers "change" with the event type; the
|
||||
// retried trigger appends to the live inode until the event lands.
|
||||
let changeWatcher: fs.FSWatcher | null = null;
|
||||
let changeFired = false;
|
||||
changeWatcher = fs.watch(target, (eventType) => {
|
||||
if (changeFired) return; // a delivery already queued when the first one closed us
|
||||
changeFired = true;
|
||||
clearInterval(changeKick);
|
||||
console.log("event:", eventType);
|
||||
if (changeWatcher) {
|
||||
changeWatcher.close();
|
||||
changeWatcher = null;
|
||||
startRenamePhase();
|
||||
}
|
||||
startRenamePhase();
|
||||
});
|
||||
console.log("watching");
|
||||
setTimeout(() => {
|
||||
let changeKicks = 0;
|
||||
const changeKick = setInterval(() => {
|
||||
if (changeFired) return;
|
||||
changeKicks += 1;
|
||||
if (changeKicks > 600) {
|
||||
console.error("change event never delivered");
|
||||
process.exit(1);
|
||||
}
|
||||
fs.appendFileSync(target, "-more");
|
||||
}, 30);
|
||||
}, 50);
|
||||
|
||||
@@ -15,10 +15,14 @@ import { tmpdir } from "node:os";
|
||||
const dir = fs.mkdtempSync(join(tmpdir(), "scr-watch-opt-"));
|
||||
const target = join(dir, "watched.txt");
|
||||
fs.writeFileSync(target, "seed");
|
||||
let fired = false;
|
||||
const watcher = fs.watch(
|
||||
target,
|
||||
{ persistent: true, recursive: false, encoding: "utf8", zorp: 1, blep: () => "never called" },
|
||||
(eventType) => {
|
||||
if (fired) return; // a delivery already queued when the first one closed us
|
||||
fired = true;
|
||||
clearInterval(kick);
|
||||
console.log("event:", eventType);
|
||||
watcher.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
@@ -26,9 +30,24 @@ const watcher = fs.watch(
|
||||
afterWatch();
|
||||
},
|
||||
);
|
||||
setTimeout(() => {
|
||||
fs.writeFileSync(target, "rewrite");
|
||||
}, 20);
|
||||
// The trigger RETRIES until the event lands. Node-on-macOS backs fs.watch
|
||||
// with an FSEvents stream that starts asynchronously, so a single delayed
|
||||
// write can slip into the startup window and be lost — the persistent
|
||||
// watcher then holds the loop and the program hangs (this fixture's
|
||||
// documented flake under machine load). Every kick is a rewrite-in-place
|
||||
// of the live inode, "change" on every backend; the first delivered event
|
||||
// closes everything, and the bound turns a genuinely dead watcher into a
|
||||
// loud nonzero exit instead of a silent hang.
|
||||
let kicks = 0;
|
||||
const kick = setInterval(() => {
|
||||
if (fired) return;
|
||||
kicks += 1;
|
||||
if (kicks > 600) {
|
||||
console.error("watch event never delivered");
|
||||
process.exit(1);
|
||||
}
|
||||
fs.writeFileSync(target, "rewrite-" + String(kicks));
|
||||
}, 50);
|
||||
|
||||
// The options-only two-argument form: opens, holds the loop, closes.
|
||||
const dir2 = fs.mkdtempSync(join(tmpdir(), "scr-watch-opt2-"));
|
||||
|
||||
Reference in New Issue
Block a user