PageRenderTime 26ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/POE/Resource/Signals.pm

https://github.com/EvanCarroll/MooseyPoopoe
Perl | 667 lines | 412 code | 142 blank | 113 comment | 56 complexity | 6b8eb6d922718a7cd416747d64d044d1 MD5 | raw file
  1. package POE::Resource::Signals;
  2. use Moose::Role;
  3. use strict;
  4. use POE::Helpers::Error qw( _warn _trap );
  5. use POE::Helpers::Constants qw(
  6. :signal_types
  7. TRACE_SIGNALS
  8. USE_SIGCHLD
  9. RUNNING_IN_HELL
  10. EN_SCPOLL
  11. ET_SCPOLL
  12. EN_SIGNAL
  13. ET_SIGNAL
  14. ET_SIGCLD
  15. );
  16. use POSIX qw(:sys_wait_h);
  17. use Errno qw(ESRCH EINTR ECHILD EPERM EINVAL EEXIST EAGAIN EWOULDBLOCK);
  18. ### Map watched signal names to the sessions that are watching them
  19. ### and the events that must be delivered when they occur.
  20. has [qw/kr_signals kr_pids_to_events/] => (
  21. isa => 'HashRef'
  22. , is => 'ro'
  23. , default => sub { +{} }
  24. );
  25. #my %kr_signals;
  26. # ( $signal_name =>
  27. # { $session_reference => $event_name,
  28. # ...,
  29. # },
  30. # ...,
  31. # );
  32. my %kr_sessions_to_signals;
  33. # ( $session =>
  34. # { $signal_name => $event_name,
  35. # ...,
  36. # },
  37. # ...,
  38. # );
  39. #my %kr_pids_to_events;
  40. # { $pid =>
  41. # { $session =>
  42. # [ $blessed_session, # PID_SESSION
  43. # $event_name, # PID_EVENT
  44. # ]
  45. # }
  46. # }
  47. my %kr_sessions_to_pids;
  48. # { $session => { $pid => 1 } }
  49. sub PID_SESSION () { 0 }
  50. sub PID_EVENT () { 1 }
  51. # Bookkeeping per dispatched signal.
  52. use vars (
  53. '@kr_signaled_sessions', # The sessions touched by a signal.
  54. '$kr_signal_total_handled', # How many sessions handled a signal.
  55. '$kr_signal_type', # The type of signal being dispatched.
  56. );
  57. #my @kr_signaled_sessions; # The sessions touched by a signal.
  58. #my $kr_signal_total_handled; # How many sessions handled a signal.
  59. #my $kr_signal_type; # The type of signal being dispatched.
  60. # A flag to tell whether we're currently polling for signals.
  61. # Under POE::Kernel::USE_SIGCHLD, determines whether a SIGCHLD polling event has already been queued
  62. my $polling_for_signals = 0;
  63. # A flag determining whether there are child processes.
  64. my $kr_child_procs = exists($INC{'Apache.pm'}) ? 0 : ( USE_SIGCHLD ? 0 : 1 );
  65. # A list of special signal types. Signals that aren't listed here are
  66. # benign (they do not kill sessions at all). "Terminal" signals are
  67. # the ones that UNIX defaults to killing processes with. Thus STOP is
  68. # not terminal.
  69. my %_signal_types = (
  70. QUIT => SIGTYPE_TERMINAL,
  71. INT => SIGTYPE_TERMINAL,
  72. KILL => SIGTYPE_TERMINAL,
  73. TERM => SIGTYPE_TERMINAL,
  74. HUP => SIGTYPE_TERMINAL,
  75. IDLE => SIGTYPE_TERMINAL,
  76. DIE => SIGTYPE_TERMINAL,
  77. ZOMBIE => SIGTYPE_NONMASKABLE,
  78. UIDESTROY => SIGTYPE_NONMASKABLE,
  79. );
  80. # Build a list of useful, real signals. Nonexistent signals, and ones
  81. # which are globally unhandled, usually cause segmentation faults if
  82. # perl was poorly configured. Some signals aren't available in some
  83. # environments.
  84. my %_safe_signals;
  85. sub _data_sig_initialize {
  86. my $self = shift;
  87. # Initialize this to a true value so our waitpid() loop can run at
  88. # least once. Starts false when running in an Apache handler so our
  89. # SIGCHLD hijinks don't interfere with the web server.
  90. $kr_child_procs = exists($INC{'Apache.pm'}) ? 0 : ( USE_SIGCHLD ? 0 : 1 );
  91. # In case we're called multiple times.
  92. unless (keys %_safe_signals) {
  93. foreach my $signal (keys %SIG) {
  94. # Nonexistent signals, and ones which are globally unhandled.
  95. next if (
  96. $signal =~ /^
  97. ( NUM\d+
  98. |__[A-Z0-9]+__
  99. |ALL|CATCHALL|DEFER|HOLD|IGNORE|MAX|PAUSE
  100. |RTMIN|RTMAX|SETS
  101. |SEGV
  102. |
  103. )
  104. $/x
  105. );
  106. # Windows doesn't have a SIGBUS, but the debugger causes SIGBUS
  107. # to be entered into %SIG. It's fatal to register its handler.
  108. next if $signal eq 'BUS' and RUNNING_IN_HELL;
  109. # Apache uses SIGCHLD and/or SIGCLD itself, so we can't.
  110. next if $signal =~ /^CH?LD$/ and exists $INC{'Apache.pm'};
  111. $_safe_signals{$signal} = 1;
  112. }
  113. # Reset some important signal handlers. The rest remain
  114. # untouched.
  115. $self->loop_ignore_signal("CHLD") if exists $SIG{CHLD};
  116. $self->loop_ignore_signal("CLD") if exists $SIG{CLD};
  117. $self->loop_ignore_signal("PIPE") if exists $SIG{PIPE};
  118. }
  119. }
  120. ### Return signals that are safe to manipulate.
  121. sub _data_sig_get_safe_signals {
  122. return keys %_safe_signals;
  123. }
  124. ### End-run leak checking.
  125. sub _data_sig_finalize {
  126. my $self = shift;
  127. my $finalized_ok = 1;
  128. my $kr_signals = $self->kr_signals;
  129. while (my ($sig, $sig_rec) = each(%$kr_signals)) {
  130. $finalized_ok = 0;
  131. _warn "!!! Leaked signal $sig\n";
  132. while (my ($ses, $event) = each(%{$kr_signals->{$sig}})) {
  133. _warn "!!!\t$ses = $event\n";
  134. }
  135. }
  136. while (my ($ses, $sig_rec) = each(%kr_sessions_to_signals)) {
  137. $finalized_ok = 0;
  138. _warn "!!! Leaked signal cross-reference: $ses\n";
  139. while (my ($sig, $event) = each(%{$kr_signals->{$ses}})) {
  140. _warn "!!!\t$sig = $event\n";
  141. }
  142. }
  143. while (my ($ses, $pid_rec) = each(%kr_sessions_to_pids)) {
  144. $finalized_ok = 0;
  145. my @pids = keys %$pid_rec;
  146. _warn "!!! Leaked session to PID map: $ses -> (@pids)\n";
  147. }
  148. while (my ($pid, $ses_rec) = each( %{$self->kr_pids_to_events}) ) {
  149. $finalized_ok = 0;
  150. _warn "!!! Leaked PID to event map: $pid\n";
  151. while (my ($ses, $event_rec) = each %$ses_rec) {
  152. _warn "!!!\t$ses -> $event_rec->[PID_EVENT]\n";
  153. }
  154. }
  155. %_safe_signals = ();
  156. unless (RUNNING_IN_HELL) {
  157. local $!;
  158. local $?;
  159. until ((my $pid = waitpid( -1, 0 )) == -1) {
  160. _warn( "!!! Child process PID:$pid reaped: $!\n" ) if $pid;
  161. $finalized_ok = 0;
  162. }
  163. }
  164. return $finalized_ok;
  165. }
  166. ### Add a signal to a session.
  167. sub _data_sig_add {
  168. my ($self, $session, $signal, $event) = @_;
  169. $kr_sessions_to_signals{$session}->{$signal} = $event;
  170. $self->_data_sig_signal_watch($session, $signal);
  171. $self->kr_signals->{$signal}->{$session} = $event;
  172. }
  173. sub _data_sig_signal_watch {
  174. my ($self, $session, $signal) = @_;
  175. # First session to watch the signal.
  176. # Ask the event loop to watch the signal.
  177. if (
  178. !exists($self->kr_signals->{$signal}) and
  179. exists($_safe_signals{$signal}) and
  180. ($signal ne "CHLD" or !scalar(keys %kr_sessions_to_pids))
  181. ) {
  182. $self->loop_watch_signal($signal);
  183. }
  184. }
  185. sub _data_sig_signal_ignore {
  186. my ($self, $session, $signal) = @_;
  187. if (
  188. !exists($self->kr_signals->{$signal}) and
  189. exists($_safe_signals{$signal}) and
  190. ($signal ne "CHLD" or !scalar(keys %kr_sessions_to_pids))
  191. ) {
  192. $self->loop_ignore_signal($signal);
  193. }
  194. }
  195. ### Remove a signal from a session.
  196. sub _data_sig_remove {
  197. my ($self, $session, $signal) = @_;
  198. my $kr_signals = $self->kr_signals;
  199. delete $kr_sessions_to_signals{$session}->{$signal};
  200. delete $kr_sessions_to_signals{$session}
  201. unless keys(%{$kr_sessions_to_signals{$session}});
  202. delete $kr_signals->{$signal}->{$session};
  203. # Last watcher for that signal. Stop watching it internally.
  204. unless (keys %{$kr_signals->{$signal}}) {
  205. delete $kr_signals->{$signal};
  206. $self->_data_sig_signal_ignore($session, $signal);
  207. }
  208. }
  209. ### Clear all the signals from a session.
  210. # XXX - It's ok to clear signals from a session that doesn't exist.
  211. # Usually it means that the signals are being cleared, but it might
  212. # mean that the session really doesn't exist. Should we care?
  213. sub _data_sig_clear_session {
  214. my ($self, $session) = @_;
  215. if (exists $kr_sessions_to_signals{$session}) { # avoid autoviv
  216. foreach (keys %{$kr_sessions_to_signals{$session}}) {
  217. $self->_data_sig_remove($session, $_);
  218. }
  219. }
  220. if (exists $kr_sessions_to_pids{$session}) { # avoid autoviv
  221. foreach (keys %{$kr_sessions_to_pids{$session}}) {
  222. $self->_data_sig_pid_ignore($session, $_);
  223. }
  224. }
  225. }
  226. ### Watch and ignore PIDs.
  227. sub _data_sig_pid_watch {
  228. my ($self, $session, $pid, $event) = @_;
  229. $self->kr_pids_to_events->{$pid}{$session} = [
  230. $session, # PID_SESSION
  231. $event, # PID_EVENT
  232. ];
  233. $self->_data_sig_signal_watch($session, "CHLD");
  234. $kr_sessions_to_pids{$session}{$pid} = 1;
  235. $self->_data_ses_refcount_inc($session);
  236. }
  237. sub _data_sig_pid_ignore {
  238. my ($self, $session, $pid) = @_;
  239. my $kr_pids_to_events = $self->kr_pids_to_events;
  240. # Remove PID to event mapping.
  241. delete $kr_pids_to_events->{$pid}{$session};
  242. delete $kr_pids_to_events->{$pid} unless (
  243. keys %{$kr_pids_to_events->{$pid}}
  244. );
  245. # Remove session to PID mapping.
  246. delete $kr_sessions_to_pids{$session}{$pid};
  247. unless (keys %{$kr_sessions_to_pids{$session}}) {
  248. delete $kr_sessions_to_pids{$session};
  249. $self->_data_sig_signal_ignore($session, "CHLD");
  250. }
  251. $self->_data_ses_refcount_dec($session);
  252. }
  253. sub _data_sig_pids_ses {
  254. my ($self, $session) = @_;
  255. return 0 unless exists $kr_sessions_to_pids{$session};
  256. return scalar keys %{$kr_sessions_to_pids{$session}};
  257. }
  258. sub _data_sig_pids_is_ses_watching {
  259. my ($self, $session, $pid) = @_;
  260. return(
  261. exists($kr_sessions_to_pids{$session}) &&
  262. exists($kr_sessions_to_pids{$session}{$pid})
  263. );
  264. }
  265. ### Return a signal's type, or SIGTYPE_BENIGN if it's not special.
  266. sub _data_sig_type {
  267. my ($self, $signal) = @_;
  268. return $_signal_types{$signal} || SIGTYPE_BENIGN;
  269. }
  270. ### Flag a signal as being handled by some session.
  271. sub _data_sig_handled {
  272. my $self = shift;
  273. $kr_signal_total_handled++;
  274. }
  275. ### Clear the structures associated with a signal's "handled" status.
  276. sub _data_sig_reset_handled {
  277. my ($self, $signal) = @_;
  278. undef $kr_signal_total_handled;
  279. $kr_signal_type = $self->_data_sig_type($signal);
  280. undef @kr_signaled_sessions;
  281. }
  282. ### Is the signal explicitly watched?
  283. sub _data_sig_explicitly_watched {
  284. return exists $_[0]->kr_signals->{$_[1]};
  285. }
  286. ### Return the signals watched by a session and the events they
  287. ### generate. TODO Used mainly for testing, but may also be useful
  288. ### for introspection.
  289. sub _data_sig_watched_by_session {
  290. my ($self, $session) = @_;
  291. return %{$kr_sessions_to_signals{$session}};
  292. }
  293. ### Which sessions are watching a signal?
  294. sub _data_sig_watchers {
  295. return %{ $_[0]->kr_signals->{$_[1]} };
  296. }
  297. ### Return the current signal's handled status.
  298. ### TODO Used for testing.
  299. sub _data_sig_handled_status {
  300. return(
  301. $kr_signal_total_handled,
  302. $kr_signal_type,
  303. \@kr_signaled_sessions,
  304. );
  305. }
  306. ### Determine if a given session is watching a signal. This uses a
  307. ### two-step exists so that the longer one does not autovivify keys in
  308. ### the shorter one.
  309. sub _data_sig_is_watched_by_session {
  310. my ($self, $signal, $session) = @_;
  311. my $kr_signals = $self->kr_signals;
  312. return(
  313. exists($kr_signals->{$signal}) &&
  314. exists($kr_signals->{$signal}->{$session})
  315. );
  316. }
  317. ### Destroy sessions touched by a nonmaskable signal or by an
  318. ### unhandled terminal signal. Check for garbage-collection on
  319. ### sessions which aren't to be terminated.
  320. sub _data_sig_free_terminated_sessions {
  321. my $self = shift;
  322. if (
  323. ($kr_signal_type & SIGTYPE_NONMASKABLE) or
  324. ($kr_signal_type & SIGTYPE_TERMINAL and !$kr_signal_total_handled)
  325. ) {
  326. foreach my $dead_session (@kr_signaled_sessions) {
  327. next unless $self->_data_ses_exists($dead_session);
  328. if (TRACE_SIGNALS) {
  329. _warn(
  330. "<sg> stopping signaled session ",
  331. $self->_data_alias_loggable($dead_session)
  332. );
  333. }
  334. $self->_data_ses_stop($dead_session);
  335. }
  336. }
  337. else {
  338. # TODO Implicit signal reaping. This is deprecated behavior and
  339. # will eventually be removed. See the commented out tests in
  340. # t/res/signals.t.
  341. #
  342. # Don't reap the parent if it's the kernel. It still needs to be
  343. # a part of the system for finalization in certain cases.
  344. foreach my $touched_session (@kr_signaled_sessions) {
  345. next unless $self->_data_ses_exists($touched_session);
  346. next if $touched_session == $self;
  347. $self->_data_ses_collect_garbage($touched_session);
  348. }
  349. }
  350. # Erase @kr_signaled_sessions, or they will leak until the next
  351. # signal.
  352. undef @kr_signaled_sessions;
  353. }
  354. ### A signal has touched a session. Record this fact for later
  355. ### destruction tests.
  356. sub _data_sig_touched_session {
  357. my ($self, $session) = @_;
  358. push @kr_signaled_sessions, $session;
  359. }
  360. # only used under !POE::Kernel::USE_SIGCHLD
  361. sub _data_sig_begin_polling {
  362. my $self = shift;
  363. return if $polling_for_signals;
  364. $polling_for_signals = 1;
  365. $self->_data_sig_enqueue_poll_event();
  366. $self->_idle_queue_grow();
  367. }
  368. # only used under !POE::Kernel::USE_SIGCHLD
  369. sub _data_sig_cease_polling {
  370. $polling_for_signals = 0;
  371. }
  372. sub _data_sig_enqueue_poll_event {
  373. my $self = shift;
  374. if ( USE_SIGCHLD ) {
  375. return if $polling_for_signals;
  376. $polling_for_signals = 1;
  377. $self->_data_ev_enqueue(
  378. $self, $self, EN_SCPOLL, ET_SCPOLL, [ ],
  379. __FILE__, __LINE__, undef, time(),
  380. );
  381. } else {
  382. return if $self->_data_ses_count() < 1;
  383. return unless $polling_for_signals;
  384. $self->_data_ev_enqueue(
  385. $self, $self, EN_SCPOLL, ET_SCPOLL, [ ],
  386. __FILE__, __LINE__, undef, time() + POE::Kernel::CHILD_POLLING_INTERVAL(),
  387. );
  388. }
  389. }
  390. sub _data_sig_handle_poll_event {
  391. my $self = shift;
  392. my $kr_pids_to_events = $self->kr_pids_to_events;
  393. if ( USE_SIGCHLD ) {
  394. $polling_for_signals = undef;
  395. }
  396. if (TRACE_SIGNALS) {
  397. _warn("<sg> POE::Kernel is polling for signals at " . time() . (USE_SIGCHLD ? " due to SIGCHLD" : ""));
  398. }
  399. # Reap children for as long as waitpid(2) says something
  400. # interesting has happened.
  401. # TODO This has a possibility of an infinite loop, but so far it
  402. # hasn't hasn't happened.
  403. my $pid;
  404. while ($pid = waitpid(-1, WNOHANG)) {
  405. # waitpid(2) returned a process ID. Emit an appropriate SIGCHLD
  406. # event and loop around again.
  407. if ((RUNNING_IN_HELL and $pid < -1) or ($pid > 0)) {
  408. if (RUNNING_IN_HELL or WIFEXITED($?) or WIFSIGNALED($?)) {
  409. if (TRACE_SIGNALS) {
  410. _warn("<sg> POE::Kernel detected SIGCHLD (pid=$pid; exit=$?)");
  411. }
  412. # Check for explicit SIGCHLD watchers, and enqueue explicit
  413. # events for them.
  414. if (exists $kr_pids_to_events->{$pid}) {
  415. my @sessions_to_clear;
  416. while (my ($ses_key, $ses_rec) = each %{$kr_pids_to_events->{$pid}}) {
  417. $self->_data_ev_enqueue(
  418. $ses_rec->[PID_SESSION], $self, $ses_rec->[PID_EVENT], ET_SIGCLD,
  419. [ 'CHLD', $pid, $? ],
  420. __FILE__, __LINE__, undef, time(),
  421. );
  422. push @sessions_to_clear, $ses_rec->[PID_SESSION];
  423. }
  424. $self->_data_sig_pid_ignore($_, $pid) foreach @sessions_to_clear;
  425. }
  426. # Kick off a SIGCHLD cascade.
  427. $self->_data_ev_enqueue(
  428. $self, $self, EN_SIGNAL, ET_SIGNAL, [ 'CHLD', $pid, $? ],
  429. __FILE__, __LINE__, undef, time(),
  430. );
  431. }
  432. elsif (TRACE_SIGNALS) {
  433. _warn("<sg> POE::Kernel detected strange exit (pid=$pid; exit=$?");
  434. }
  435. if (TRACE_SIGNALS) {
  436. _warn("<sg> POE::Kernel will poll again immediately");
  437. }
  438. next;
  439. }
  440. # The only other negative value waitpid(2) should return is -1.
  441. # This is highly unlikely, but it's necessary to catch
  442. # portability problems.
  443. #
  444. # TODO - Find a way to test this.
  445. _trap "internal consistency error: waitpid returned $pid"
  446. if $pid != -1;
  447. # If the error is an interrupted syscall, poll again right away.
  448. if ($! == EINTR) {
  449. if (TRACE_SIGNALS) {
  450. _warn(
  451. "<sg> POE::Kernel's waitpid(2) was interrupted.\n",
  452. "POE::Kernel will poll again immediately.\n"
  453. );
  454. }
  455. next;
  456. }
  457. # No child processes exist. TODO This is different than
  458. # children being present but running. Maybe this condition
  459. # could halt polling entirely, and some UNIVERSAL::fork wrapper
  460. # could restart polling when processes are forked.
  461. if ($! == ECHILD) {
  462. if (TRACE_SIGNALS) {
  463. _warn("<sg> POE::Kernel has no child processes");
  464. }
  465. last;
  466. }
  467. # Some other error occurred.
  468. if (TRACE_SIGNALS) {
  469. _warn("<sg> POE::Kernel's waitpid(2) got error: $!");
  470. }
  471. last;
  472. }
  473. # If waitpid() returned 0, then we have child processes.
  474. $kr_child_procs = !$pid;
  475. unless ( USE_SIGCHLD ) {
  476. # The poll loop is over. Resume slowly polling for signals.
  477. if (TRACE_SIGNALS) {
  478. _warn("<sg> POE::Kernel will poll again after a delay");
  479. }
  480. if ($polling_for_signals) {
  481. $self->_data_sig_enqueue_poll_event();
  482. }
  483. else {
  484. $self->_idle_queue_shrink();
  485. }
  486. }
  487. }
  488. # Are there child processes worth waiting for?
  489. # We don't really care if we're not polling for signals.
  490. # TODO - Will this change?
  491. sub _data_sig_child_procs {
  492. return if !USE_SIGCHLD and !$polling_for_signals;
  493. return $kr_child_procs;
  494. }
  495. 1;
  496. __END__
  497. =head1 NAME
  498. POE::Resource::Signals - internal signal manager for POE::Kernel
  499. =head1 SYNOPSIS
  500. There is no public API.
  501. =head1 DESCRIPTION
  502. POE::Resource::Signals is a mix-in class for POE::Kernel. It provides
  503. the features needed to manage signals. It is used internally by
  504. POE::Kernel, so it has no public interface.
  505. =head1 SEE ALSO
  506. See L<POE::Kernel/Signals> for a deeper discussion about POE's signal
  507. handling.
  508. See L<POE::Kernel/Signal Watcher Methods> for POE's public signals
  509. API.
  510. See L<POE::Kernel/Resources> for for public information about POE
  511. resources.
  512. See L<POE::Resource> for general discussion about resources and the
  513. classes that manage them.
  514. =head1 BUGS
  515. None known.
  516. =head1 AUTHORS & COPYRIGHTS
  517. Please see L<POE> for more information about authors and contributors.
  518. =cut
  519. # rocco // vim: ts=2 sw=2 expandtab
  520. # TODO - Edit.