The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Acme::Ghost - An yet another view to daemon processes

SYNOPSIS

    use Acme::Ghost

    my $g = Acme::Ghost->new(
        logfile     => '/tmp/daemon.log',
        pidfile     => '/tmp/daemon.pid',
        user        => 'nobody',
        group       => 'nogroup',
    );

    $g->daemonize;

    $g->log->info('Oops! I am Your Ghost');

DESCRIPTION

An yet another view to daemon processes

new

    my $g = Acme::Ghost->new(
        name        => 'myDaemon',
        user        => 'nobody',
        group       => 'nogroup',
        pidfile     => '/var/run/myDaemon.pid',
        logfile     => '/var/log/myDaemon.log',
        ident       => 'myDaemon',
        logopt      => 'ndelay,pid',
        facility    => 'user',
        logger      => Mojo::Log->new,
        loglevel    => 'debug',
        loghandle   => IO::Handler->new,
    );

ATTRIBUTES

This class implements the following attributes

facility

    facility    => 'user',

This attribute sets facility for logging

See "facility" in Acme::Ghost::Log

group

    group       => 'nogroup',
    group       => 65534,

This attribute sets group/gid for spawned process

ident

    ident       => 'myDaemon',

This attribute sets ident string for system log (syslog)

logfile

    logfile     => '/var/log/myDaemon.log',

This attribute sets log file path. By default all log entries will be printed to syslog

See "file" in Acme::Ghost::Log

logger

    logger      => Mojo::Log->new,

This attribute perfoms to set predefined logger, eg. Mojo::Log. If you set this attribute, the specified logger will be used as the preferred logger

loghandle

Log filehandle, defaults to opening "file" or uses syslog if file not specified

See "handle" in Acme::Ghost::Log

loglevel

    loglevel    => 'debug',

This attribute sets the log level

See "level" in Acme::Ghost::Log

logopt

    logopt      => 'ndelay,pid',

This attribute contains zero or more of the options

See "logopt" in Acme::Ghost::Log

name

    name        => 'myDaemon',

This attribute sets name of daemon. Default: script name basename($0)

pidfile

    pidfile     => '/var/run/myDaemon.pid',

This attribute sets PID file path. Default: ./<NAME>.pid

user

    user        => 'nobody',
    user        => 65534,

This attribute sets user/uid for spawned process

METHODS

This class implements the following methods

again

This method is called immediately after creating the instance and returns it

NOTE: Internal use only for subclasses!

daemonize

    $g = $g->daemonize;

Main routine for just daemonize. This routine will check on the pid file, safely fork, create the pid file (storing the pid in the file), become another user and group, close STDIN, STDOUT and STDERR, separate from the process group (become session leader), and install $SIG{INT} to remove the pid file. In otherwords - daemonize. All errors result in a die

filepid

    my $filepid = $g->filepid;

This method returns Acme::Ghost::FilePid object

flush

    $self = $self->flush;

This internal method flush (resets) process counters to defaults. Please do not use this method in your inherits

is_daemonized

    $g->is_daemonized or die "Your ghost process really is not a daemon"

This method returns status of daemon:

    True - the process is an daemon;
    False - the process is not daemon;

is_spirited

    my $is_spirited = $g->is_spirited;

This method returns status of spirit:

    True - the process is an spirit;
    False - the process is not spirit;

log

    my $log = $g->log;

This method returns Acme::Ghost::Log object

ok

    $g->ok or die "Interrupted!";

This method checks process state and returns boolean status of healthy. If this status is false, then it is immediately to shut down Your process as soon as possible, otherwise your process will be forcibly destroyed within 7 seconds from the moment your process receives the corresponding signal

pid

    print $g->pid;

This method returns PID of the daemon

set_gid

    $g = $g->set_gid('1000 10001 10002');
    $g = $g->set_gid(1000);
    $g = $g->set_gid('nogroup');
    $g = $g->set_gid;

Become another group. Arguments are groups (or group ids or space delimited list of group ids). All errors die

set_uid

    $g = $g->set_uid(1000);
    $g = $g->set_uid('nobody');
    $g = $g->set_uid;

Become another user. Argument is user (or userid). All errors die

CONTROL METHODS

List of LSB Daemon Control Methods

These methods can be used to control the daemon behavior. Every effort has been made to have these methods DWIM (Do What I Mean), so that you can focus on just writing the code for your daemon.

ctrl

    exit $g->ctrl( shift @ARGV, 'USR2' );
      # start, stop, restart, reload, status

Daemon Control Dispatcher with using USR2 to reloading

    exit $g->ctrl( shift @ARGV, 0 );

This example shows how to forced suppress reloading (disable send users signals to daemon)

reload

    $exit_code = $g->reload; # SIGHUP (by default)
    $exit_code = $g->reload('USR2'); # SIGUSR2
    $exit_code = $g->reload(12); # SIGUSR2 too
    say "Reloading ". $g->pid;

This method performs sending signal to Your daemon and return 0 as exit code. This method is primarily intended to perform a daemon reload

restart

    $exit_code = $g->restart;
    if ($exit_code) {
        say STDERR "Restart failed " . $g->pid;
    } else {
        say "Restart successful";
    }

This method performs restarting the daemon and returns 0 as successfully exit code or 1 in otherwise

start

    my $exit_code = $g->start;
    say "Running ". $g->pid;
    exit $exit_code;

This method performs starting the daemon and returns 0 as exit code. The spawned process calls the startup handler and exits with status 0 as exit code without anything return

status

    if (my $runned = $g->status) {
        say "Running $runned";
    } else {
        say "Not running";
    }

This method checks the status of running daemon and returns its PID (alive). The method returns 0 if it is not running (dead).

stop

    if (my $runned = $g->stop) {
        if ($runned < 0) {
            die "Daemon " . $g->pid ." is still running";
        } else {
            say "Stopped $runned";
        }
    } else {
        say "Not running";
    }

This method performs stopping the daemon and returns:

    +PID -- daemon stopped successfully
    0    -- daemon is not running
    -PID -- daemon is still running, stop failed

HOOKS

This class implements the following user-methods (hooks). Each of the following methods may be implemented (overwriting) in a your class

preinit

    sub preinit {
        my $self = shift;
        # . . .
    }

The preinit() method is called before spawning (forking)

init

    sub init {
        my $self = shift;
        # . . .
    }

The init() method is called after spawning (forking) and after daemonizing

startup

    sub startup {
        my $self = shift;
        # . . .
    }

The startup() method is called after daemonizing in service mode

This is your main hook into the service, it will be called at service startup. Meant to be overloaded in a subclass.

cleanup

    sub cleanup {
        my $self = shift;
        my $scope = shift; # 0 or 1
        # . . .
    }

The cleanup() method is called at before exit This method passes one argument:

    0 -- called at normal DESTROY;
    1 -- called at interrupt

NOTE! On DESTROY phase logging is unpossible. We not recommended to use logging in this method

hangup

    sub hangup {
        my $self = shift;
        # . . .
    }

The hangup() method is called on HUP or USR2 signals

For example (on Your inherit subclass):

    sub init {
        my $self = shift;

        # Listen USR2 (reload)
        $SIG{HUP} = sub { $self->hangup };
    }
    sub hangup {
        my $self = shift;
        $self->log->debug(">> Hang up!");
    }

EXAMPLES

ghost_simple.pl

This is traditional way to start daemons

    use Acme::Ghost;

    my $g = Acme::Ghost->new(
        logfile => 'daemon.log',
        pidfile => 'daemon.pid',
    );

    my $cmd = shift(@ARGV) // 'start';
    if ($cmd eq 'status') {
        if (my $runned = $g->status) {
            print "Running $runned\n";
        } else {
            print "Not running\n";
        }
        exit 0; # Ok
    } elsif ($cmd eq 'stop') {
        if (my $runned = $g->stop) {
            if ($runned < 0) {
                print STDERR "Failed to stop " . $g->pid . "\n";
                exit 1; # Error
            }
            print "Stopped $runned\n";
        } else {
            print "Not running\n";
        }
        exit 0; # Ok
    } elsif ($cmd ne 'start') {
        print STDERR "Command incorrect\n";
        exit 1; # Error
    }

    # Daemonize
    $g->daemonize;

    my $max = 10;
    my $i = 0;
    while (1) {
        $i++;
        sleep 3;
        $g->log->debug(sprintf("> %d/%d", $i, $max));
        last if $i >= $max;
    }
ghost_acme.pl

Simple acme example of daemon with reloading demonstration

    my $g = MyGhost->new(
        logfile => 'daemon.log',
        pidfile => 'daemon.pid',
    );

    exit $g->ctrl(shift(@ARGV) // 'start'); # start, stop, restart, reload, status

    1;

    package MyGhost;

    use parent 'Acme::Ghost';

    sub init {
        my $self = shift;
        $SIG{HUP} = sub { $self->hangup }; # Listen USR2 (reload)
    }
    sub hangup {
        my $self = shift;
        $self->log->debug("Hang up!");
    }
    sub startup {
        my $self = shift;
        my $max = 100;
        my $i = 0;
        while ($self->ok) {
            $i++;
            sleep 3;
            $self->log->debug(sprintf("> %d/%d", $i, $max));
            last if $i >= $max;
        }
    }

    1;
ghost_ioloop.pl

Mojo::IOLoop example

    my $g = MyGhost->new(
        logfile => 'daemon.log',
        pidfile => 'daemon.pid',
    );

    exit $g->ctrl(shift(@ARGV) // 'start', 0); # start, stop, restart, reload, status

    1;

    package MyGhost;

    use parent 'Acme::Ghost';
    use Mojo::IOLoop;

    sub init {
        my $self = shift;
        $self->{loop} = Mojo::IOLoop->new;
    }
    sub startup {
        my $self = shift;
        my $loop = $self->{loop};
        my $i = 0;

        # Add a timers
        my $timer = $loop->timer(5 => sub {
            my $l = shift; # loop
            $self->log->info("Timer!");
        });
        my $recur = $loop->recurring(1 => sub {
            my $l = shift; # loop
            $l->stop unless $self->ok;
            $self->log->info("Tick! " . ++$i);
            $l->stop if $i >= 10;
        });

        $self->log->debug("Start IOLoop");

        # Start event loop if necessary
        $loop->start unless $loop->is_running;

        $self->log->debug("Finish IOLoop");
    }

    1;
ghost_ae.pl

AnyEvent example

    my $g = MyGhost->new(
        logfile => 'daemon.log',
        pidfile => 'daemon.pid',
    );

    exit $g->ctrl(shift(@ARGV) // 'start', 0); # start, stop, restart, reload, status

    1;

    package MyGhost;

    use parent 'Acme::Ghost';
    use AnyEvent;

    sub startup {
        my $self = shift;
        my $quit = AnyEvent->condvar;
        my $i = 0;

        # Create watcher timer
        my $watcher = AnyEvent->timer (after => 1, interval => 1, cb => sub {
            $quit->send unless $self->ok;
        });

        # Create process timer
        my $timer = AnyEvent->timer(after => 3, interval => 3, cb => sub {
            $self->log->info("Tick! " . ++$i);
            $quit->send if $i >= 10;
        });

        $self->log->debug("Start AnyEvent");
        $quit->recv; # Run!
        $self->log->debug("Finish AnyEvent");
    }

    1;
ghost_nobody.pl

This example shows how to start daemons over nobody user and logging to syslog (default)

    my $g = MyGhost->new(
        pidfile => '/tmp/daemon.pid',
        user    => 'nobody',
        group   => 'nogroup',
    );

    exit $g->ctrl(shift(@ARGV) // 'start', 0); # start, stop, restart, status

    1;

    package MyGhost;

    use parent 'Acme::Ghost';

    sub startup {
        my $self = shift;
        my $max = 100;
        my $i = 0;
        while ($self->ok) {
            $i++;
            sleep 3;
            $self->log->debug(sprintf("> %d/%d", $i, $max));
            last if $i >= $max;
        }
    }

    1;

DEBUGGING

You can set the ACME_GHOST_DEBUG environment variable to get some advanced diagnostics information printed to STDERR.

    ACME_GHOST_DEBUG=1

TO DO

See TODO file

SEE ALSO

CTK::Daemon, Net::Server::Daemonize, Mojo::Server, Mojo::Server::Prefork, Daemon::Daemonize, MooseX::Daemonize, Proc::Daemon

AUTHOR

Serż Minus (Sergey Lepenkov) https://www.serzik.com <abalama@cpan.org>

COPYRIGHT

Copyright (C) 1998-2024 D&D Corporation. All Rights Reserved

LICENSE

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

See LICENSE file and https://dev.perl.org/licenses/