Prevent child process from messing with parent's inotify watch

24 views Asked by At

I have a job system built on top of Linux inotify, which watches a directory and starts a new process when a file is inserted. The problem with that is that the started child process then messes with the parent's inotify filedescriptor, redering the parent process defunct.

Here is an example, extracted from the job system:

<?php

declare(strict_types=1);

if (!extension_loaded('inotify')) {
    throw new Exception('inotify extension is not loaded');
}
if (!extension_loaded('pcntl')) {
    throw new Exception('pcntl extension is not loaded');
}
if (!extension_loaded('posix')) {
    throw new Exception('posix extension is not loaded');
}

function watch(): Generator
{
    $inotify = inotify_init();
    if ($inotify === false) {
        throw new Exception('inotify_init() failed');
    }
    try {
        $watch = inotify_add_watch(
            $inotify,
            __DIR__,
            IN_CLOSE_WRITE
        );
        try {
            while (true) {
                echo posix_getpid() . ': waiting for inotify events' . PHP_EOL;
                $events = inotify_read($inotify);
                if ($events === false) {
                    throw new Exception('inotify_read() failed');
                }
                foreach ($events as $event) {
                    echo posix_getpid() . ': inotify event: ' . json_encode($event, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL;
                    yield $event;
                }
            }
        } finally {
            echo posix_getpid() . ': removing watch' . PHP_EOL;
            inotify_rm_watch($inotify, $watch);
        }
    } finally {
        echo posix_getpid() . ': closing inotify stream' . PHP_EOL;
        fclose($inotify);
    }
}

foreach (watch() as $event) {
    $pid = pcntl_fork();
    if ($pid == -1) {
        throw new Exception('pcntl_fork() failed');
    } elseif ($pid) {
        echo posix_getpid() . ': child process ' . $pid . ' forked' . PHP_EOL;
    } else {
        echo posix_getpid() . ': child process running' . PHP_EOL;
        // do something here
        sleep(1);
        echo posix_getpid() . ': child process finished' . PHP_EOL;
        exit;
    }
}

You should be able to just run this code from the CLI. If you now e.g. make a change to the file, it should detect that change and react to it. The output it generates then looks for example like this:

54119: waiting for inotify events
54119: inotify event: {"wd":1,"mask":8,"cookie":0,"name":"inotify-fork-generator-issue.php"}
54119: child process 54121 forked
54119: waiting for inotify events
54121: child process running
54121: child process finished
54121: removing watch
54121: closing inotify stream
54119: inotify event: {"wd":1,"mask":32768,"cookie":0,"name":""}
54119: child process 54122 forked
54119: waiting for inotify events
54122: child process running
54122: child process finished
54122: removing watch
54122: closing inotify stream
PHP Warning:  inotify_rm_watch(): The file descriptor is not an inotify instance or the watch descriptor is invalid in /home/uli/simplequeue/inotify-fork-generator-issue.php on line 41
PHP Stack trace:
PHP   1. watch() /home/uli/simplequeue/inotify-fork-generator-issue.php:0
PHP   2. inotify_rm_watch($inotify_instance = resource(4) of type (stream), $mask = 1) /home/uli/simplequeue/inotify-fork-generator-issue.php:41

At this point, the application is hanging. The parent process doesn't respond to any changes in the watched directory.

Notes:

  • The use of a child process is relevant. Without one, this works without any problems.
  • I know, the child processes are not properly cleaned up (pcntl_wait()) but I believe it's irrelevant here. The actual code does it.
  • The use of the Generator is relevant. As I understand it, exit will call shutdown functions and destructors, which doesn't include catch clauses. However, if you're running inside of a Generator, that seems to change slightly or become part of the destructor.
  • I could image that if I bound the inotify filedescriptor to an object with a destructor, I could reproduce the same without a generator.
  • It is relevant that the child process uses the cloned PHP engine. If I use pcntl_exec('/bin/true') instead of exit in above code, this works as expected, so at least I have a workaround.
0

There are 0 answers