Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions lib/internal/main/watch_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p));

let graceTimer;
let child;
let childExitPromise;
let exited;
let stopping;

function start() {
exited = false;
stopping = false;
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : 'inherit';
child = spawn(process.execPath, argsWithoutWatchOptions, {
stdio,
Expand All @@ -103,29 +106,36 @@ function start() {
ArrayPrototypeForEach(kOptionalEnvFiles,
(file) => watcher.filterFile(resolve(file), undefined, { allowMissing: true }));
}
child.once('exit', (code) => {
childExitPromise = once(child, 'exit').then(({ 0: code }) => {
exited = true;
if (stopping) {
return code;
}
const waitingForChanges = 'Waiting for file changes before restarting...';
if (code === 0) {
process.stdout.write(`${blue}Completed running ${kCommandStr}. ${waitingForChanges}${white}\n`);
} else {
process.stdout.write(`${red}Failed running ${kCommandStr}. ${waitingForChanges}${white}\n`);
}
return code;
});
return child;
}

async function killAndWait(signal = kKillSignal, force = false) {
child?.removeAllListeners();
if (!child) {
const processToKill = child;
const onExit = childExitPromise;
if (!processToKill) {
return;
}
if ((child.killed || exited) && !force) {
if ((processToKill.killed || exited) && !force) {
return;
}
const onExit = once(child, 'exit');
child.kill(signal);
const { 0: exitCode } = await onExit;
stopping = true;
if (!exited && processToKill.exitCode === null && processToKill.signalCode === null) {
processToKill.kill(signal);
}
const exitCode = await onExit;
return exitCode;
}

Expand Down
3 changes: 3 additions & 0 deletions lib/internal/watch_mode/files_watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ class FilesWatcher extends EventEmitter {
this.#filteredFiles.clear();
}
clear() {
clearTimeout(this.#debounceTimer);
this.#debounceTimer = null;
this.#debounceOwners.clear();
this.#watchers.forEach(this.#unwatch);
this.#watchers.clear();
this.#filteredFiles.clear();
Expand Down
58 changes: 58 additions & 0 deletions test/parallel/test-watch-mode-files_watcher-clear.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Flags: --expose-internals
import * as common from '../common/index.mjs';
import tmpdir from '../common/tmpdir.js';
import assert from 'node:assert';
import { writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { setTimeout as sleep } from 'node:timers/promises';

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

const require = createRequire(import.meta.url);
const timers = require('node:timers');
const originalSetTimeout = timers.setTimeout;
const originalClearTimeout = timers.clearTimeout;
const { promise, resolve } = Promise.withResolvers();
let debounceTimer;
let debounceTimerCleared = false;

timers.setTimeout = function(fn, delay, ...args) {
const timer = originalSetTimeout(fn, delay, ...args);
if (delay === 1000) {
debounceTimer = timer;
debounceTimer.ref();
resolve();
}
return timer;
};

timers.clearTimeout = function(timer) {
if (timer === debounceTimer) {
debounceTimerCleared = true;
}
return originalClearTimeout(timer);
};

try {
const { FilesWatcher } = require('internal/watch_mode/files_watcher');

tmpdir.refresh();
const file = tmpdir.resolve('watcher-clear.js');
writeFileSync(file, '0');

const watcher = new FilesWatcher({ debounce: 1000, mode: 'all' });
watcher.on('changed', common.mustNotCall());
watcher.watchPath(file, false);

const interval = setInterval(() => writeFileSync(file, `${Date.now()}`), 50);
await promise;
clearInterval(interval);

watcher.clear();
assert.strictEqual(debounceTimerCleared, true);
await sleep(1100);
} finally {
timers.setTimeout = originalSetTimeout;
timers.clearTimeout = originalClearTimeout;
}
29 changes: 29 additions & 0 deletions test/sequential/test-watch-mode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,35 @@ async function failWriteSucceed({ file, watchedFile }) {
tmpdir.refresh();

describe('watch mode', { concurrency: !process.env.TEST_PARALLEL, timeout: 60_000 }, () => {
it('should exit when terminated after the watched process has completed', async () => {
const file = createTmpFile();
const child = spawn(execPath, ['--watch', '--no-warnings', file], {
encoding: 'utf8',
stdio: 'pipe',
});

for await (const line of createInterface({ input: child.stdout })) {
if (line.includes('Completed running')) {
break;
}
}

child.kill();
const timedOut = Promise.withResolvers();
const timer = setTimeout(() => {
timedOut.reject(new Error('Timed out waiting for watch mode to exit'));
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGKILL');
}
}, common.platformTimeout(5000));
timer.unref();
try {
await Promise.race([once(child, 'exit'), timedOut.promise]);
} finally {
clearTimeout(timer);
}
});

it('should watch changes to a file', async () => {
const file = createTmpFile();
const { stderr, stdout } = await runWriteSucceed({ file, watchedFile: file, watchFlag: '--watch=true', options: {
Expand Down
Loading