Search code examples
macosperlanyeventmacos-sequoia

Mac::FSEvents Used Inside Class with AnyEvent::io no Longer Working


This code no longer works:

use strict;
use warnings;

use AnyEvent;
use Data::Dumper;

my $done = AnyEvent->condvar;

my $watcher1 = AnyEvent::Filesys::Watcher::FSEvents->new;
warn $watcher1->{__watchers};

$done->recv;

package AnyEvent::Filesys::Watcher::FSEvents;

use AnyEvent;
use Mac::FSEvents;

sub new {
    my ($class) = @_;

    my $self = {};

    my $fs = Mac::FSEvents->new(
        latency => 0.1,
        file_events => 1,
        path => [ '.' ],
    );
    my $fh = $fs->watch;
    my $watchers = [AnyEvent->io(
        fh => $fh,
        poll => 'r',
        cb => sub {
            warn "event fired";
            exit 0;
        }
    )];

    $self->{__watchers} = $watchers;

    bless $self, $class;
}

When you run the code and change a file in the current directory, no event gets fired. The debugging output in line 10 should ensure that the watcher variable has not gone out of scope which is a very common reason for failures like the current one.

This other piece of code should be doing exactly the same:

use strict;
use warnings;

use AnyEvent;
use Mac::FSEvents;
use Data::Dumper;

my $done = AnyEvent->condvar;

my $fs = Mac::FSEvents->new(
    latency => 0.1,
    file_events => 1,
    path => [ '.' ],
);
my $fh = $fs->watch;
my $watcher2 = [AnyEvent->io(
    fh => $fh,
    poll => 'r',
    cb => sub {
        warn "event fired";
    }
)];

$done->recv;

Change a file in the current directory, and the script terminates with a message "event fired".

I am using MacOS Sequoia (15.1) with Perl 5.40.0 and Mac::FSEvents 0.22, AnyEvent 7.17.

Background: AnyEvent::Filesys::Notify which uses Mac::FSEvents under the hood no longer works on my system. The test t/30-event.t fails, showing the same behaviour as my code: No event gets fired. The test also fails for CPAN Testers, see http://matrix.cpantesters.org/?dist=AnyEvent-Filesys-Notify%201.23;os=darwin;perl=5.38.2;reports=1. Although the output looks slightly different there.

Some time ago, I have written a replacement for AnyEvent::Filesys::Notify https://github.com/gflohr/AnyEvent-Filesys-Watcher. It does the same as AnyEvent::Filesys::Notify but with fewer dependencies. That module has the same problem with my current setup in the equivalent test t/30-event.t.

Obviously, the tests succeeded at one point. Therefore, I suppose an incompatible change in Mac::FSEvents, AnyEvent, perl, or macOS causes the problem.

I only have one Mac, and cannot test with other macOS versions.

I downgraded Mac::FSEvents to 0.20 with the same result. Earlier versions do not build anymore.

Instead of the file handle returned by the file system watcher, I have used standard input which works.

I have also tried Perl 5.34.0 and 5.30.0. Same result.

The tests for the two AnyEvent wrappers succeed on a Linux system. The failure seems to be specific to macOS or my specific setup.


Solution

  • The problem in the code is the assignment to $fh, the return value of $fs->watch. In the "class" version, the variable goes out of scope, gets undefined, and the call to select() that AnyEvent->io() does, fails immediately with EBADF because the file descriptor in the read set was closed.

    The solution is to prevent that by storing a reference to the file handle:

    $self->{__fh} = $fs->watch;
    my $watchers = [AnyEvent->io(
            fh => $self->{__fh},
            poll => 'r',
            cb => sub {
                warn "event fired";
                exit 0;
            }
        )];
    

    The actual problem in https://github.com/gflohr/AnyEvent-Filesys-Watcher gets solved by this fix, see the PR https://github.com/gflohr/AnyEvent-Filesys-Watcher/pull/50.

    In the comments to the question, a suggestion was made to move the condvar into the class constructor, see https://pastebin.com/zpjzidy8. This fixes the example code but not the underlying problem in AnyEvent::Filesys::Watcher. The reason why that seems to work is that the call to $done->recv now happens before the variable $fh goes out of scope.

    I still cannot say what caused this problem because the code used to work in the past on an older MacBook. I have downgraded Perl to 5.20 and AnyEvent to version 7.01 from 2012 and the issue was still reproducible. That suggests that the problem is caused by a change in macOS.

    See also these test results for AnyEvent::Filesys::Notify which uses very similar code:

    Both tests were done by the same tester, probably on the same machine with different Perl and OS versions. I can reproduce the problem with Perl 5.31.9. That leaves only the OS version as the culprit.