File Coverage

blib/lib/Linux/Event/Pid.pm
Criterion Covered Total %
statement 28 128 21.8
branch 0 42 0.0
condition 0 12 0.0
subroutine 10 19 52.6
pod 0 3 0.0
total 38 204 18.6


line stmt bran cond sub pod time code
1             package Linux::Event::Pid;
2 13     13   133 use v5.36;
  13         38  
3 13     13   57 use strict;
  13         15  
  13         248  
4 13     13   41 use warnings;
  13         16  
  13         839  
5              
6             our $VERSION = '0.012';
7              
8 13     13   83 use Carp qw(croak);
  13         20  
  13         676  
9 13     13   76 use Scalar::Util qw(weaken);
  13         26  
  13         483  
10 13     13   51 use POSIX ();
  13         17  
  13         255  
11             # Linux waitid(2) flags. POSIX.pm does not always expose WEXITED.
12             # On Linux, WEXITED is 0x00000004 (from ).
13 13     13   52 use constant _WEXITED => 4;
  13         15  
  13         11036  
14              
15 0     0     sub _wait_flags ($self) {
  0            
  0            
16 0           my $wnohang = eval { POSIX::WNOHANG() };
  0            
17 0 0         $wnohang = 1 if !defined $wnohang; # WNOHANG is 1 on Linux
18              
19 0           my $wexited = eval { POSIX::WEXITED() };
  0            
20 0 0         $wexited = _WEXITED if !defined $wexited;
21              
22 0           return $wexited | $wnohang;
23             }
24              
25              
26             # pidfd-backed process exit notifications for Linux::Event.
27             #
28             # This module is a thin adaptor over Linux::FD::Pid. It opens a pidfd and
29             # registers a normal watcher with the loop; core dispatch remains unchanged.
30              
31 0     0 0   sub new ($class, %args) {
  0            
  0            
  0            
32 0           my $loop = delete $args{loop};
33 0 0         croak "loop is required" if !$loop;
34 0 0         croak "unknown args: " . join(", ", sort keys %args) if %args;
35              
36 0           weaken($loop);
37              
38 0           return bless {
39             loop => $loop,
40             by_pid => {}, # pid -> entry
41             }, $class;
42             }
43              
44 0     0 0   sub loop ($self) { return $self->{loop} }
  0            
  0            
  0            
45              
46 0     0 0   sub pid ($self, $pid, $cb, %opts) {
  0            
  0            
  0            
  0            
  0            
47 0 0         croak "pid is required" if !defined $pid;
48 0 0 0       croak "callback is required" if !$cb || ref($cb) ne 'CODE';
49              
50 0           my $data = delete $opts{data};
51 0           my $reap = delete $opts{reap};
52 0 0         $reap = 1 if !defined $reap;
53 0 0         croak "unknown opts: " . join(", ", sort keys %opts) if %opts;
54              
55 0 0 0       croak "pid must be a positive integer" if $pid !~ /\A\d+\z/ || $pid < 1;
56              
57             # Replacement semantics per PID:
58 0 0         if (my $old = $self->{by_pid}{$pid}) {
59 0           $old->{sub}->cancel;
60             }
61              
62 0           my $fh = $self->_open_pidfd($pid);
63              
64 0 0         my $entry = {
65             pid => $pid,
66             fh => $fh,
67             cb => $cb,
68             data => $data,
69             reap => $reap ? 1 : 0,
70             sub => undef,
71             w => undef,
72             };
73              
74 0           my $sub = bless { _pid => $pid, _owner => $self, _active => 1 }, 'Linux::Event::Pid::Subscription';
75              
76 0           $entry->{sub} = $sub;
77              
78             # Watch pidfd like any other fd. pidfd becomes readable when the target exits.
79 0           my $w = $self->{loop}->watch($fh,
80 0     0     read => sub ($loop, $watcher, $ud) { $self->_on_ready($pid) },
  0            
  0            
  0            
  0            
81 0     0     error => sub ($loop, $watcher, $ud) { $self->_on_ready($pid) },
  0            
  0            
  0            
  0            
  0            
82 0           data => undef,
83             );
84              
85 0           $entry->{w} = $w;
86 0           $self->{by_pid}{$pid} = $entry;
87              
88 0           return $sub;
89             }
90              
91 0     0     sub _open_pidfd ($self, $pid) {
  0            
  0            
  0            
92 0 0         eval { require Linux::FD::Pid; 1 }
  0            
  0            
93             or croak "Linux::FD::Pid is required for pid() support: $@";
94              
95             # The module accepts 'non-blocking' as a flag; we still use WNOHANG for waits
96             # to guarantee we never block in dispatch.
97 0           my $fh = Linux::FD::Pid->new($pid, 'non-blocking');
98 0           return $fh;
99             }
100              
101 0     0     sub _on_ready ($self, $pid) {
  0            
  0            
  0            
102 0 0         my $entry = $self->{by_pid}{$pid} or return;
103              
104             # If the subscription has been canceled, ignore spurious readiness.
105 0 0 0       return if !$entry->{sub} || !$entry->{sub}{_active};
106              
107 0           my $status;
108 0 0         if ($entry->{reap}) {
109             # Non-blocking: returns undef if not ready.
110 0           my $ok = eval {
111 0           $status = $entry->{fh}->wait($self->_wait_flags);
112 0           1;
113             };
114 0 0         if (!$ok) {
115             # Not our child, already reaped, or other waitid() failure.
116 0   0       my $err = $@ || "$!";
117 0           croak "pid() reap failed for pid $pid: $err";
118             }
119              
120             # If wait returned undef, the process may not be fully ready yet.
121 0 0         return if !defined $status;
122             } else {
123 0           $status = undef;
124             }
125              
126             # Dispatch (strict ABI: always 4 args).
127 0           my $cb = $entry->{cb};
128 0           $cb->($self->{loop}, $pid, $status, $entry->{data});
129              
130             # One-shot: once the process has exited (and we've observed readiness),
131             # tear down the subscription.
132 0           $entry->{sub}->cancel;
133              
134 0           return;
135             }
136              
137             package Linux::Event::Pid::Subscription;
138 13     13   181 use v5.36;
  13         37  
139 13     13   49 use strict;
  13         27  
  13         263  
140 13     13   47 use warnings;
  13         15  
  13         2074  
141              
142 0     0     sub cancel ($self) {
  0            
  0            
143 0 0         return 0 if !$self->{_active};
144 0           $self->{_active} = 0;
145              
146 0 0         my $owner = $self->{_owner} or return 1;
147 0           my $pid = $self->{_pid};
148              
149 0           my $entry = delete $owner->{by_pid}{$pid};
150 0 0         return 1 if !$entry;
151              
152             # Unwatch first (idempotent in watcher).
153 0 0         if (my $w = $entry->{w}) {
154 0           $w->cancel;
155             }
156              
157             # Drop pidfd handle (will close when refcount reaches zero).
158 0           $entry->{fh} = undef;
159              
160 0           return 1;
161             }
162              
163             1;
164              
165             __END__