atexit(3) in Perl: `END {}` or `$SIG{__DIE__}`?

124 Views Asked by At

I'm wondering: When you detect you need to clean up at the end of the program (excluding "end of block"), you want to "queue an exit handler" that does the cleanup at some later time. In most cases you want the cleanup to happen even if the user presses ^C (SIGINT), so I wonder:

When installing possibly multiple exit handlers, should it be done with END blocks, or should it be done using $SIG{__DIE__} handlers (if so, how to "queue" them?)?

The manual says processes terminated by signals would skip the END blocks, to is that a no-go for them? Likewise, when a process is terminated by a signal, is the $SIG{__DIE__} handler called at all ("man perlvar" is silent on that), or do I have to install a handler for each possible signal, too?

3

There are 3 best solutions below

0
On

The manual says processes terminated by signals would skip the END block ...

This is because unless the application itself installs a signal handler, the default action for most signals is to forcibly kill the process. This means there is no way for the application to run their own cleanups in this case. The process cleanup of the OS (closing files, sockets, ...) is still done though.

If you want to have your own cleanup you need to catch the signal using %SIG. If you then call exit from inside the signal handler it will also run END functions in your program. Note that not all signals can be catched.

... when a process is terminated by a signal, is the $SIG{__DIE__} handler called at all

$SIG{__DIE__} is not relevant here. It will not be called on signals, but if the program throws a fatal exception (i.e. die).

7
On

To "queue an exit handler", ala atexit(3), in Perl use END blocks; that's precisely what they are for. There may be multiple ones, executed in order reverse of how they come in code (they run from the back one by one). Forks inherit parent's END blocks so they'll run on child's exit. If that is not desired one way out is to exit a child process using POSIX::_exit, which bypasses END blocks. Please see docs to accustom yourself with the library.

The END blocks run on exit and die, but not on termination from signals (that terminate the program -- not all do), as you noticed, so also install signal handlers. Set up a suitable one for each signal. To have handlers run END blocks as well use exit in them.

The $SIG{__DIE__} is called when an exception (die) is thrown, and while that can be triggered by a variety of situations this is not a catch-all. It's not really a signal handler at all either, since die isn't a signal, but is rather a hook into it. Signals that terminate the program do not use die so this hook won't run.

Handlers can be chain-assigned if there is no reason to distinguish their behavior:

$SIG{INT} = $SIG{TERM} = ... = sub { say "Caught $_[0]"; .... };

Note also the core sigtrap for signal handling. Apart from other functionality the above is

use sigtrap 'handler' => \&sig_handler, qw(INT TERM);

Along with much else, as it closes the process "immediately." It's a bit radical measure. See man pages for C's _exit, what this is "identical to," both in sections 2 (Linux) and 3 (POSIX).

0
On

After having read https://stackoverflow.com/a/77302322/6607497 and https://stackoverflow.com/a/77303688/6607497, and after some extra thinking, I decided to write my own mechanism, ignoring END blocks:

I'll handle an array of closures and a cleanup routine that will call the queue of closures, then flush it:

my @cleanup_queue;                      # cleanup queue

# process the cleanup queue, then clear the queue
sub cleanup()
{
    foreach (@cleanup_queue) {
        $_->();
    }
}

Then I use cleanup in three places:

  1. Signal handler(s)
  2. Die handler
  3. Normal exit

The handlers I needed thus were:

$SIG{'__DIE__'} = sub {
    my $msg = $_[0];

    cleanup();
    die $msg;                           # actually die
};

$SIG{'INT'} = $SIG{'TERM'} = sub {
    cleanup();
    $SIG{$_[0]} = 'DEFAULT';
    kill($_[0], $$);                    # execute default action
};

The cleanup closures are added in reverse order, like this:

{
#...
    unshift(
        @cleanup_queue,
        sub () {
            while (my ($pid, $peername) = each %connections) {
                child_delete($pid);
            }
        });
#...
    cleanup();
}

Tested with clean exits, signals and forced die.

Maybe I duplicated some of the Perl internals, but now I have (almost) full control of the code being involved.