Axosoft / nsfw

A super fast and scaleable file watcher that provides a consistent interface on Linux, OSX and Windows
MIT License
901 stars 113 forks source link

File watchers prevent exit handler on child process from firing #158

Open bduffany opened 2 years ago

bduffany commented 2 years ago

TLDR: It seems that file watchers are preventing the exit handler from properly firing on a child process that I have started, specifically when the exit handler is registered inside a SIGINT handler.

Minimal repro code, with problematic line commented out ```js const child_process = require('child_process'); const fs = require('fs'); const nsfw = require('nsfw'); const proc = child_process.spawn('bash', ['-c', ` trap "echo bash: Got SIGINT, exiting. ; exit 1" SIGINT sleep infinity `], { stdio: 'inherit' }); let watcher; process.on('SIGINT', async () => { console.log('Got SIGINT.'); if (watcher) { console.log('Stopping file watcher.'); // await watcher.stop(); watcher = null; } console.log('Forwarding SIGINT to child.'); await new Promise((resolve) => { proc.on('exit', () => { console.log('Child process exited.'); resolve(); }); proc.kill('SIGINT'); }); process.exit(1); }); (async () => { const watchPath = '/tmp/watch_path'; fs.mkdirSync(watchPath, { recursive: true }); (watcher = await nsfw(watchPath, () => {})).start(); console.log('Press Ctrl+C to stop child process'); })(); ```

Try running this program and pressing Ctrl+C -- the child exits as expected.

If the commented line is uncommented (await watcher.stop()), the child process exits (I see bash: Got SIGINT, exiting. printed), but the exit listener does not get fired, so the program hangs forever.

Interestingly, if the 'exit' listener is registered before stopping file watchers, everything works fine:

Repro modified to register exit listener before stopping file watchers ```js const child_process = require('child_process'); const fs = require('fs'); const nsfw = require('nsfw'); const proc = child_process.spawn('bash', ['-c', ` trap "echo bash: Got SIGINT, exiting. ; exit 1" SIGINT sleep infinity `], { stdio: 'inherit' }); let watcher; const onExit = new Promise((resolve) => { proc.on('exit', () => { console.log('Child process exited.'); resolve(); }); }); process.on('SIGINT', async () => { console.log('Got SIGINT.'); if (watcher) { console.log('Stopping file watcher.'); await watcher.stop(); watcher = null; } console.log('Forwarding SIGINT to child.'); proc.kill('SIGINT'); await onExit; process.exit(1); }); (async () => { const watchPath = '/tmp/watch_path'; fs.mkdirSync(watchPath, { recursive: true }); (watcher = await nsfw(watchPath, () => {})).start(); console.log('Press Ctrl+C to stop child process'); })(); ```

So it appears that starting file watchers is preventing subsequently registered exit listeners from firing. But maybe it's because I am trying to register the 'exit' listener inside a SIGINT handler? Indeed, it appears to have something to do with the SIGINT handler, because if I stop file watchers and register the exit listener in the main function, it works fine:

Repro modified to stop file watchers and register exit listener in main function ```js const child_process = require('child_process'); const fs = require('fs'); const nsfw = require('nsfw'); const proc = child_process.spawn('bash', ['-c', ` trap "echo bash: Got SIGINT, exiting. ; exit 1" SIGINT sleep infinity `], { stdio: 'inherit' }); let watcher; let onExit; process.on('SIGINT', async () => { console.log('Got SIGINT.'); console.log('Forwarding SIGINT to child.'); proc.kill('SIGINT'); await onExit; process.exit(1); }); (async () => { const watchPath = '/tmp/watch_path'; fs.mkdirSync(watchPath, { recursive: true }); (watcher = await nsfw(watchPath, () => {})).start(); await watcher.stop(); onExit = new Promise((resolve) => { proc.on('exit', () => { console.log('Child process exited.'); resolve(); }); }); console.log('Press Ctrl+C to stop child process'); })(); ```

I am also pretty sure that this is not a general issue with calling await inside a SIGINT handler, and has something specifically to do with file watchers. If I replace the await watcher.stop() with await new Promise(resolve => setTimeout(resolve, 500)), it works:

Minimal example without use of nsfw, demonstrating that the problem is likely nsfw-specific ```js const child_process = require('child_process'); const proc = child_process.spawn('bash', ['-c', ` trap "echo bash: Got SIGINT, exiting. ; exit 1" SIGINT sleep infinity `], { stdio: 'inherit' }); process.on('SIGINT', async () => { console.log('Got SIGINT.'); console.log('Waiting a bit...') await new Promise(resolve => setTimeout(resolve, 500)); console.log('Forwarding SIGINT to child.'); proc.kill('SIGINT'); await new Promise((resolve) => { proc.on('exit', () => { console.log('Child process exited.'); resolve(); }); }); process.exit(1); }); (async () => { console.log('Press Ctrl+C to stop child process'); })(); ```

Linux kernel version: 5.13.0-28-generic Distro: Ubuntu 20.04