diff --git a/lib/internal/fs/watchers.js b/lib/internal/fs/watchers.js index 2dad214a729c9c..af54f8c16c8fd8 100644 --- a/lib/internal/fs/watchers.js +++ b/lib/internal/fs/watchers.js @@ -317,6 +317,8 @@ FSWatcher.prototype[kFSWatchStart] = function(filename, encoding); if (err) { if (!throwIfNoEntry && err === UV_ENOENT) { + // FSEventWrap::Start() already started closing the native handle. + this._handle = null; return; } diff --git a/test/parallel/test-fs-watch-enoent.js b/test/parallel/test-fs-watch-enoent.js index e363b3e9325c8c..2120485a4e6cde 100644 --- a/test/parallel/test-fs-watch-enoent.js +++ b/test/parallel/test-fs-watch-enoent.js @@ -62,7 +62,11 @@ tmpdir.refresh(); ); } else { const watcher = fs.watch(nonexistentFile, { throwIfNoEntry: false }, common.mustNotCall()); - watcher.close(); + if (common.isLinux) { + setTimeout(common.mustCall(() => watcher.close()), common.platformTimeout(10)); + } else { + watcher.close(); + } } } diff --git a/test/parallel/test-watch-mode-missing-env-file-signal.mjs b/test/parallel/test-watch-mode-missing-env-file-signal.mjs new file mode 100644 index 00000000000000..502cc2ddc334b9 --- /dev/null +++ b/test/parallel/test-watch-mode-missing-env-file-signal.mjs @@ -0,0 +1,91 @@ +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; +import { writeFileSync } from 'node:fs'; +import { clearTimeout, setTimeout } from 'node:timers'; +import tmpdir from '../common/tmpdir.js'; + +if (!common.isLinux) + common.skip('This test verifies Linux fs.watch() behavior'); + +tmpdir.refresh(); +const entry = tmpdir.resolve('entry.js'); +const missingEnvFile = tmpdir.resolve('missing.env'); +writeFileSync(entry, ''); + +async function withTimeout(promise, message) { + let timeout; + const timedOut = new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error(message)), + common.platformTimeout(10_000), + ); + }); + + try { + return await Promise.race([promise, timedOut]); + } finally { + clearTimeout(timeout); + } +} + +async function testSignal(signal) { + const child = spawn(process.execPath, [ + '--watch', + `--env-file-if-exists=${missingEnvFile}`, + entry, + ], { + cwd: tmpdir.path, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const closed = once(child, 'close'); + const ready = Promise.withResolvers(); + let stdout = ''; + let stderr = ''; + + child.stdout.setEncoding('utf8'); + child.stdout.on('data', (data) => { + stdout += data; + if (stdout.includes('Completed running')) { + ready.resolve(); + } + }); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (data) => { + stderr += data; + }); + child.once('error', ready.reject); + child.once('exit', (code, exitSignal) => { + ready.reject(new Error( + `Watch mode exited before becoming ready: code=${code}, signal=${exitSignal}`, + )); + }); + + try { + await withTimeout( + ready.promise, + 'Timed out waiting for watch mode readiness', + ); + assert.strictEqual(child.kill(signal), true); + const [code, exitSignal] = await withTimeout( + closed, + `Timed out waiting for watch mode to exit after ${signal}`, + ); + + assert.strictEqual(code, 0); + assert.strictEqual(exitSignal, null); + assert.doesNotMatch( + `${stdout}\n${stderr}`, + /Assertion failed|FSEventWrap::GetInitialized/, + ); + } finally { + if (child.exitCode === null && child.signalCode === null) { + child.kill('SIGKILL'); + } + await closed.catch(() => {}); + } +} + +await testSignal('SIGINT'); +await testSignal('SIGTERM');