File Coverage

blib/lib/Linux/Event/Signal.pm
Criterion Covered Total %
statement 151 165 91.5
branch 35 66 53.0
condition 3 12 25.0
subroutine 21 23 91.3
pod 0 3 0.0
total 210 269 78.0


line stmt bran cond sub pod time code
1             package Linux::Event::Signal;
2 13     13   128 use v5.36;
  13         52  
3 13     13   41 use strict;
  13         16  
  13         225  
4 13     13   45 use warnings;
  13         16  
  13         683  
5              
6             our $VERSION = '0.012';
7              
8 13     13   49 use Carp qw(croak);
  13         15  
  13         645  
9 13     13   48 use Scalar::Util qw(weaken);
  13         16  
  13         452  
10 13     13   5029 use POSIX ();
  13         75753  
  13         10360  
11              
12             # External dependency for signalfd integration.
13             #
14             # This is loaded lazily so that the core loop can be used even when
15             # Linux::FD::Signal is not installed.
16              
17 1     1 0 1 sub new ($class, %args) {
  1         1  
  1         6  
  1         1  
18 1         2 my $loop = delete $args{loop};
19 1 50       3 croak "loop is required" if !$loop;
20 1 50       2 croak "unknown args: " . join(", ", sort keys %args) if %args;
21              
22 1         2 weaken($loop);
23              
24 1         36 return bless {
25             loop => $loop,
26              
27             _fh => undef, # Linux::FD::Signal filehandle
28             _watcher => undef, # Linux::Event::Watcher for the signalfd
29              
30             _mask => POSIX::SigSet->new(),
31             _blocked => POSIX::SigSet->new(),
32              
33             # signum -> { cb => CODE, data => any, sub => $sub }
34             _handlers => {},
35             }, $class;
36             }
37              
38 0     0 0 0 sub loop ($self) { return $self->{loop} }
  0         0  
  0         0  
  0         0  
39              
40 3     3 0 4 sub signal ($self, $sig_or_list, $cb, %opt) {
  3         3  
  3         4  
  3         3  
  3         4  
  3         3  
41 3 50       7 croak "signal is required" if !defined $sig_or_list;
42 3 50       4 croak "cb is required" if !$cb;
43 3 50       14 croak "cb must be a coderef" if ref($cb) ne 'CODE';
44              
45 3         4 my $data = delete $opt{data};
46 3 50       7 croak "unknown args: " . join(", ", sort keys %opt) if %opt;
47              
48 3         4 my @sigs;
49 3 50       7 if (ref($sig_or_list) eq 'ARRAY') {
    50          
50 0         0 @sigs = @$sig_or_list;
51             }
52             elsif (!ref($sig_or_list)) {
53 3         7 @sigs = ($sig_or_list);
54             }
55             else {
56 0         0 croak "signal must be a number, string, or arrayref";
57             }
58              
59 3         4 @sigs = map { _sig_to_num($_) } @sigs;
  3         5  
60 3 50       5 croak "no signals provided" if !@sigs;
61              
62 3         7 $self->_ensure_fd;
63              
64 3         10 my $sub = Linux::Event::Signal::Subscription->_new($self, \@sigs);
65              
66             # Replacement semantics: one handler per signal, last registration wins.
67 3         5 for my $sig (@sigs) {
68 3         14 $self->{_handlers}{$sig} = {
69             cb => $cb,
70             data => $data,
71             sub => $sub,
72             };
73              
74             # Our semantics freeze: the mask and blocked-set only grow for the lifetime
75             # of the loop. We do not attempt to restore legacy signal state.
76 3 100       21 if (!$self->{_mask}->ismember($sig)) {
77 2         12 $self->{_mask}->addset($sig);
78 2         5 $self->_block_signal($sig);
79 2         11 $self->{_fh}->set_mask($self->{_mask});
80             }
81             }
82              
83 3         9 return $sub;
84             }
85              
86 3     3   3 sub _sig_to_num ($sig) {
  3         3  
  3         4  
87 3 50       9 croak "signal is undef" if !defined $sig;
88              
89 3 50 33     22 if (!ref($sig) && $sig =~ /\A\d+\z/) {
90 0         0 return int($sig);
91             }
92              
93 3 50       14 croak "signal must be a string or integer" if ref($sig);
94              
95 3         5 my $name = uc($sig);
96 3         10 $name =~ s/\A\s+|\s+\z//g;
97 3         5 $name =~ s/\A(SIG)//;
98              
99 3         3 my $const = "SIG$name";
100 3         19 my $sub = POSIX->can($const);
101 3 50       4 croak "unknown signal '$sig'" if !$sub;
102 3         11 return int($sub->());
103             }
104              
105 3     3   2 sub _ensure_fd ($self) {
  3         3  
  3         4  
106 3 100       6 return if $self->{_fh};
107              
108 1 50       2 eval { require Linux::FD::Signal; 1 }
  1         6  
  1         2  
109             or croak "Linux::FD::Signal is required for signal() support: $@";
110              
111             # Non-blocking is critical: epoll read readiness may be spuriously invoked
112             # when multiple records are pending; we drain to EAGAIN.
113 1         40 my $fh = Linux::FD::Signal->new($self->{_mask}, 'non-blocking');
114 1         3 $self->{_fh} = $fh;
115              
116 1 50       2 my $loop = $self->{loop} or croak "loop has been destroyed";
117 3         3 $self->{_watcher} = $loop->watch(
118             $fh,
119 3     3   6 read => sub ($loop, $fh2, $w) {
  3         2  
  3         3  
  3         3  
120 3         7 $self->_drain_and_dispatch;
121             },
122 1         6 );
123              
124 1         2 return;
125             }
126              
127 2     2   3 sub _block_signal ($self, $sig) {
  2         2  
  2         3  
  2         10  
128 2 50       9 return if $self->{_blocked}->ismember($sig);
129 2         5 $self->{_blocked}->addset($sig);
130 2         16 POSIX::sigprocmask(POSIX::SIG_BLOCK(), $self->{_blocked});
131 2         2 return;
132             }
133              
134 3     3   4 sub _drain_and_dispatch ($self) {
  3         3  
  3         3  
135 3         4 my $loop = $self->{loop};
136 3 50       6 return if !$loop;
137              
138 3 50       5 my $fh = $self->{_fh} or return;
139              
140 3         3 my %count;
141              
142 3         4 while (1) {
143 7         11 my $info = eval { $fh->receive };
  7         50  
144 7 100       16 if (!$info) {
145             # Linux::FD::Signal returns undef on EAGAIN (non-blocking), and sets $!.
146 3 50 33 11   18 last if $!{EAGAIN} || $!{EWOULDBLOCK};
  11         4645  
  11         13891  
  11         88  
147 0 0 0     0 last if $@ && ($@ =~ /EAGAIN/);
148 0 0       0 die $@ if $@;
149 0         0 last;
150             }
151              
152 4         4 my $sig = $info->{signo};
153 4 50       10 $count{$sig}++ if defined $sig;
154             }
155              
156 3 50       44 return if !%count;
157              
158             # Dispatch: per-signal callback, once per dispatch cycle.
159 3         11 for my $sig (sort { $a <=> $b } keys %count) {
  1         4  
160 4 100       25 my $h = $self->{_handlers}{$sig} or next;
161 3 50       4 my $cb = $h->{cb} or next;
162 3         8 $cb->($loop, $sig, $count{$sig}, $h->{data});
163             }
164              
165 3         28 return;
166             }
167              
168 1     1   1 sub _cancel_subscription ($self, $sub) {
  1         2  
  1         2  
  1         1  
169             # Remove mappings only if they still point at this subscription.
170 1         1 for my $sig (@{ $sub->{_sigs} }) {
  1         3  
171 1 50       3 my $h = $self->{_handlers}{$sig} or next;
172 1 50 33     6 next if !$h->{sub} || $h->{sub} != $sub;
173 1         7 delete $self->{_handlers}{$sig};
174             }
175 1         1 return;
176             }
177              
178              
179             package Linux::Event::Signal::Subscription;
180 13     13   3753 use v5.36;
  13         43  
181 13     13   52 use strict;
  13         33  
  13         354  
182 13     13   49 use warnings;
  13         28  
  13         649  
183              
184 13     13   59 use Scalar::Util qw(weaken);
  13         21  
  13         2673  
185              
186 3     3   3 sub _new ($class, $signal, $sigs) {
  3         4  
  3         3  
  3         3  
  3         3  
187 3         4 weaken($signal);
188 3         11 return bless {
189             _signal => $signal,
190             _sigs => [@$sigs],
191             _active => 1,
192             }, $class;
193             }
194              
195 2     2   651 sub cancel ($self) {
  2         3  
  2         2  
196 2 100       8 return 0 if !$self->{_active};
197 1         1 $self->{_active} = 0;
198 1         2 my $signal = $self->{_signal};
199 1 50       4 $signal->_cancel_subscription($self) if $signal;
200 1         3 return 1;
201             }
202              
203 0 0   0   0 sub is_active ($self) { return $self->{_active} ? 1 : 0 }
  0         0  
  0         0  
  0         0  
204              
205             1;
206              
207             __END__