Acme-Ghost

 view release on metacpan or  search on metacpan

META.json  view on Meta::CPAN

{
   "abstract" : "An yet another view to daemon processes",
   "author" : [
      "Serz Minus (Sergey Lepenkov) <abalama@cpan.org>"
   ],
   "dynamic_config" : 1,
   "generated_by" : "ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010",
   "license" : [
      "perl_5"
   ],
   "meta-spec" : {
      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",

META.yml  view on Meta::CPAN

---
abstract: 'An yet another view to daemon processes'
author:
  - 'Serz Minus (Sergey Lepenkov) <abalama@cpan.org>'
build_requires:
  ExtUtils::MakeMaker: '6.6'
  Test::More: '0.94'
configure_requires:
  ExtUtils::MakeMaker: '0'
dynamic_config: 1
generated_by: 'ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010'
license: perl

README.md  view on Meta::CPAN

[//]: # ( README.md Sun 03 Dec 2023 13:54:32 MSK )

# Acme::Ghost

An yet another view to daemon processes

eg/ghost_ae.pl  view on Meta::CPAN

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");
}

lib/Acme/Ghost.pm  view on Meta::CPAN

package Acme::Ghost;
use warnings;
use strict;
use utf8;

=encoding utf-8

=head1 NAME

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

=head1 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');

=head1 DESCRIPTION

An yet another view to daemon processes

=head2 new

    my $g = Acme::Ghost->new(
        name        => 'myDaemon',
        user        => 'nobody',
        group       => 'nogroup',
        pidfile     => '/var/run/myDaemon.pid',
        logfile     => '/var/log/myDaemon.log',
        ident       => 'myDaemon',

lib/Acme/Ghost.pm  view on Meta::CPAN


This attribute sets facility for logging

See L<Acme::Ghost::Log/facility>

=head2 group

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

This attribute sets group/gid for spawned process

=head2 ident

    ident       => 'myDaemon',

This attribute sets ident string for system log (syslog)

=head2 logfile

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

lib/Acme/Ghost.pm  view on Meta::CPAN


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

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

=head2 user

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

This attribute sets user/uid for spawned process

=head1 METHODS

This class implements the following methods

=head2 again

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

B<NOTE:> Internal use only for subclasses!

=head2 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

=head2 filepid

    my $filepid = $g->filepid;

This method returns L<Acme::Ghost::FilePid> object

=head2 flush

    $self = $self->flush;

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

=head2 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;

=head2 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;

=head2 log

    my $log = $g->log;

This method returns L<Acme::Ghost::Log> object

=head2 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

=head2 pid

    print $g->pid;

This method returns PID of the daemon

=head2 set_gid

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

lib/Acme/Ghost.pm  view on Meta::CPAN

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

=head2 start

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

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

=head2 status

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

lib/Acme/Ghost.pm  view on Meta::CPAN

    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");
    }

lib/Acme/Ghost.pm  view on Meta::CPAN

        facility    => $args->{facility},
        logfile     => $args->{logfile},
        ident       => $args->{ident} || $name,
        logopt      => $args->{logopt},
        logger      => $args->{logger},
        loglevel    => $args->{loglevel},
        loghandle   => $args->{loghandle},
        _log        => undef,

        # Runtime
        initpid     => $$,  # PID of root process
        ppid        => 0,   # PID before daemonize
        pid         => 0,   # PID daemonized process
        daemonized  => 0,   # 0 - no daemonized; 1 - daemonized
        spirited    => 0,   # 0 - is not spirit; 1 - is spirit (See ::Prefork)

        # Manage
        ok          => 0,   # 1 - Ok. Process is healthy (ok)
        signo       => 0,   # The caught signal number
        interrupt   => 0,   # The interrupt counter

    }, $class;
    return $self->again(%$args);

lib/Acme/Ghost.pm  view on Meta::CPAN

    POSIX::setgid($gid) || die "Setgid $gid failed - $!\n"; # Set first GID
    if (! grep {$gid == $_} split /\s+/, $() { # look for any valid id in the list
        die "Detected strange GID. Couldn't become GID \"$gid\": $!\n";
    }

    return $self;
}
sub daemonize {
    my $self = shift;
    my $safe = shift;
    croak "This process is already daemonized (PID=$$)\n" if $self->{daemonized};

    # Check PID
    my $pid_file = $self->filepid->file; # PID File
    if ( my $runned = $self->filepid->running ) {
        die "Already running $runned\n";
    }

    # Store current PID to instance as Parent PID
    $self->{ppid} = $$;

lib/Acme/Ghost.pm  view on Meta::CPAN

    $self->{_log} = undef; # Close log handlers before spawn

    # Spawn
    my $pid = _fork();
    if ($pid) {
        _debug("!! Spawned (PID=%s)", $pid);
        if ($safe) { # For internal use only
            $self->{pid} = $pid; # Store child PID to instance
            return $self;
        }
        exit 0; # exit parent process
    }

    # Child
    $self->{daemonized} = 1; # Set daemonized flag
    $self->filepid->pid($$)->save; # Set new PID and Write PID file
    chown($uid, $gid, $pid_file) if IS_ROOT && -e $pid_file;

    # Set GID and UID
    $self->set_gid->set_uid;

    # Turn process into session leader, and ensure no controlling terminal
    unless (DEBUG) {
        die "Can't start a new session: $!" if POSIX::setsid() < 0;
    }

    # Init logger!
    my $log = $self->log;

    # Close all standart filehandles
    unless (DEBUG) {
        my $devnull = File::Spec->devnull;

lib/Acme/Ghost.pm  view on Meta::CPAN

sub pid { shift->{pid} }

# Hooks
sub preinit { }
sub init { }
sub cleanup { } # 0 -- at destroy; 1 -- at interrupt
sub startup { }
sub hangup { }

# Process
sub flush { # Flush process counters
    my $self = shift;
    $self->{interrupt} = 0;
    $self->{signo} = 0;
    $self->{ok} = 1;
    return $self;
}
sub ok {
    my $self = shift;
    return 0 unless defined $self->{ppid}; # No parent pid found (it is not a daemon?)
    return $self->{ok} ? 1 : 0;

lib/Acme/Ghost.pm  view on Meta::CPAN


# 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
sub _term {
    my $self = shift;
    my $signo = shift || 0;
    $self->{ok} = 0; # Not Ok!
    $self->{signo} = $signo;
    $self->log->debug(sprintf("Request for terminate of ghost process %s received on signal %s", $self->pid, $signo));
    if ($self->{interrupt} >= INT_TRIES) { # Forced terminate
        POSIX::_exit(1) if $self->is_spirited;
        $self->cleanup(1);
        $self->log->fatal(sprintf("Ghost process %s forcefully terminated on signal %s", $self->pid, $signo));
        $self->filepid->remove;
        POSIX::_exit(1);
    }
    $self->{interrupt}++;
}
sub start {
    my $self = shift;
    $self->daemonize(1); # First daemonize and switch to child process
    return 0 unless $self->is_daemonized; # Exit from parent process

    # Signals Trapping for interruption
    local $SIG{INT}  = sub { $self->_term(SIGINT) };  # 2
    local $SIG{TERM} = sub { $self->_term(SIGTERM) }; # 15
    local $SIG{QUIT} = sub { $self->_term(SIGQUIT) }; # 3

    $self->flush; # Flush process counters
    $self->log->info(sprintf("Ghost process %s started", $self->pid));
    $self->startup(); # Master hook
    $self->log->info(sprintf("Ghost process %s stopped", $self->pid));
    exit 0; # Exit code for child: ok
}
sub stop {
    my $self = shift;
    my $pid = $self->filepid->running;
       $self->{pid} = $pid;
    return 0 unless $pid; # Not running

    # Try SIGQUIT ... 2s ... SIGTERM ... 4s ... SIGINT ... 3s ... SIGKILL ... 3s ... UNDEAD!
    my $tsig = 0;
    for ([SIGQUIT, 2], [SIGTERM, 2], [SIGTERM, 2], [SIGINT, 3], [SIGKILL, 3]) {
        my ($signo, $timeout) = @$_;
        kill $signo, $pid;
        for (1 .. $timeout) { # abort early if the process is now stopped
            unless ($self->filepid->running) {
                $tsig = $signo;
                last;
            }
            sleep 1;
        }
        last if $tsig;
    }
    if ($tsig) {
        if( $tsig == SIGKILL ) {
            $self->filepid->remove;
            warn "Had to resort to 'kill -9' and it worked, wiping pidfile\n";
        }
        return $pid;
    }

    # The ghost process doesn't seem to want to die. It is still running...;
    return -1 * $pid;
}
sub status {
    my $self = shift;
    return $self->{pid} = $self->filepid->running || 0;
}
sub restart {
    my $self = shift;
    my $runned = $self->stop;
    return 1 if $runned && $runned < 0; # It is still running

lib/Acme/Ghost.pm  view on Meta::CPAN

        printf "Running %s\n", $self->pid;
    } elsif ($cmd eq 'status') {
        if (my $runned = $self->status) {
            printf "Running %s\n", $runned;
        } else {
            print "Not running\n";
        }
    } elsif ($cmd eq 'stop') {
        if (my $runned = $self->stop) {
            if ($runned < 0) {
                printf STDERR "The ghost process %s doesn't seem to want to die. It is still running...\n", $self->pid;
                $exit_code = 1;
            } else {
                printf "Stopped %s\n", $runned;
            }
        } else {
            print "Not running\n";
        }
    } elsif ($cmd eq 'restart') {
        $exit_code = $self->restart;
        if ($exit_code) {

lib/Acme/Ghost/FilePid.pm  view on Meta::CPAN

    my $fp = Acme::Ghost::FilePid->new (
        file => '/some/file.pid',
        auto => 1,
    );
    die "Already running" if $fp->running;
    # . . .

=head1 DESCRIPTION

This software manages a pid file for you. It will create a pid file,
query the process within to discover if it's still running, and remove
the pid file.

=head2 new

    my $fp = Acme::Ghost::FilePid->new;

    my $fp = Acme::Ghost::FilePid->new(
        file => '/var/run/daemon.pid',
    );

lib/Acme/Ghost/FilePid.pm  view on Meta::CPAN


Removes the pid file from disk. Returns true on success, false on
failure.

=head2 running

    my $pid = $fp->running;
    die "Service already running: $pid" if $pid;

Checks to see if the pricess identified in the pid file is still
running. If the process is still running, the pid is returned. Otherwise
C<undef> is returned.

=head2 save

    $fp->save;

Writes the pid file to disk, inserting the pid inside the file.
On success, the object is returned. On failure, C<undef> is
returned.

lib/Acme/Ghost/Prefork.pm  view on Meta::CPAN


This allows for new spirits to be started while old ones are still shutting down gracefully,
drastically reducing the performance cost of spirit restarts.

Defaults to C<2>

=head2 spirits, workers

    spirits => 4

Number of spirit processes.

A good rule of thumb is two spirit processes per CPU core for applications that perform mostly
non-blocking operations.
Blocking operations often require more amount of spirits and benefit from decreasing concurrency
(often as low as C<1>)

Defaults to C<4>

=head1 METHODS

This class inherits all methods from L<Acme::Ghost> and implements the following new ones

=head2 again

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

B<NOTE:> Internal use only!

=head2 healthy

    my $healthy = $g->healthy;

This method returns the number of currently active live spirit processes (with a heartbeat)

=head2 startup

    $prefork->startup;

This method starts preforked process (manager and spirits) and wait for L</"MANAGER SIGNALS">

=head2 tick

    my $ok = $g->tick;
    my $ok = $g->tick(1); # marks the finished status

This is B<required> method of spirit main process that sends heartbeat message to
process manager and returns the status of the running server via the 'ok' attribute

=head1 MANAGER SIGNALS

The manager process can be controlled at runtime with the following signals

=head2 INT, TERM

Shut down server immediately

=head2 QUIT

Shut down server gracefully

=head2 TTIN

Increase spirit pool by one

=head2 TTOU

Decrease spirit pool by one

=head1 SPIRIT SIGNALS

The spirit processes can be controlled at runtime with the following signals

=head2 QUIT

Stop spirit gracefully

=head1 HOOKS

This class inherits all hooks from L<Acme::Ghost> and implements the following new ones

Any of the following methods may be implemented (overwriting) in your class

lib/Acme/Ghost/Prefork.pm  view on Meta::CPAN

    }

=head2 reap

    sub reap {
        my $self = shift;
        my $pid = shift;
        # . . .
    }

Is called when a child process (spirit) finished

    sub reap {
        my $self = shift;
        my $pid = shift;
        $self->log->debug("Spirit $pid stopped");
    }

=head2 spawn

    sub spawn {
        my $self = shift;
        my $pid = shift;
        # . . .
    }

Is called when a spirit process is spawned

    sub spawn {
        my $self = shift;
        my $pid = shift;
        $self->log->debug("Spirit $pid started");
    }

=head2 waitup

    sub waitup {

lib/Acme/Ghost/Prefork.pm  view on Meta::CPAN

    sub waitup {
        my $self = shift;
        my $spirits = $prefork->{spirits};
        $self->log->debug("Waiting for heartbeat messages from $spirits spirits");
    }

=head2 spirit

B<The spirit body>

This hook is called when the spirit process has started and is ready to run in isolation.
This is main hook that MUST BE implement to in user subclass

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

=head1 EXAMPLES

=over 4

lib/Acme/Ghost/Prefork.pm  view on Meta::CPAN

sub tick { # Spirit level
    my $self = shift;
    my $finished = shift || 0; # 0 - no finished; 1 - finished
    $self->_heartbeat($finished);
    return $self->ok;
}

# User hooks
sub finish { }      # Emitted when the server shuts down
sub heartbeat { }   # Emitted when a heartbeat message has been received from a spirit
sub reap { }        # Emitted when a child process exited
sub spawn { }       # Emitted when a spirit process is spawned
sub waitup { }      # Emitted when the manager starts waiting for new heartbeat messages
sub spirit {
    my $self = shift;
    my $cb = $self->{spirit_cb};
    return unless $cb;
    return $self->$cb if ref($cb) eq 'CODE';
    $self->log->error("Callback `spirit` is incorrect");
    $self->tick(1);
}

lib/Acme/Ghost/Prefork.pm  view on Meta::CPAN

    }
}
sub _stop { # Manager level
    my ($self, $graceful) = @_;
    $self->log->debug(sprintf("> Received stop signal/command: %s",
        $graceful ? 'graceful shutdown' : 'forced shutdown')) if DEBUG;
    $self->finish($graceful);
    $self->{finished} = 1;
    $self->{gracefully_stop} = $graceful ? 1 : 0;
}
sub _stopped { # Manager level (Calls when a child process exited)
    my $self = shift;
    my $pid = shift;
    $self->log->debug(sprintf("> Reap %s", $pid)) if DEBUG;
    $self->reap($pid);

    return unless my $w = delete $self->{pool}{$pid};
    $self->log->info("Spirit $pid stopped");
    unless ($w->{healthy}) {
        $self->log->error("Spirit $pid stopped too early, shutting down");
        $self->_stop;

t/02-daemon.t  view on Meta::CPAN

# Set debug mode
$ENV{ACME_GHOST_DEBUG} //= 0;

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

ok !$g->is_daemonized, "Is not daemonized";
is $g->pid, 0, "No PID in ghost process";
#note $g->pid;

done_testing;

__END__

ACME_GHOST_DEBUG=1 prove -lv t/02-daemon.t
tail -f daemon.log | bell -s mush -v 36000 | ccze -A -p syslog

t/03-filepid.t  view on Meta::CPAN

#
#########################################################################
use strict;
use Test::More;

use_ok qw/Acme::Ghost::FilePid/;

# Regular mode
{
    my $fp = Acme::Ghost::FilePid->new(file => "test03.pid");
    is $fp->pid, $$, 'current process by default';
    ok $fp->save, 'writing file';
    is $fp->running, $$, 'we are running';
    ok $fp->remove, 'deleted file';
    #note explain $fp;
}

# Autoremove mode
{
    my $fp = Acme::Ghost::FilePid->new(file => "test03.tmp", autoremove => 1);
    ok $fp->save, 'writing file';

t/03-filepid.t  view on Meta::CPAN

}

# Fork mode
my $file = 'child03.tmp';
unlink $file if -e $file;
if (my $child = fork) { # Parent PID
    sleep 1;
    my $p = Acme::Ghost::FilePid->new(file => $file, autoremove => 1);
    note sprintf "Parent PID: %s; Parent Owner: %s", $p->pid, $p->owner;
    $p->save unless $p->running;
    ok $p->running, 'child process is running';
    #note explain $p;
    waitpid $child, 0;
    done_testing;
} else { # child process
    my $p = Acme::Ghost::FilePid->new(file => $file, autoremove => 1); # hope for the best
    unless ($p->running) {
       $p->save;
       note sprintf "Start child process (Child PID: %s; Child Owner: %s)", $p->pid, $p->owner;
       sleep 3;
       note sprintf "Finish child process (Child PID: %s; Child Owner: %s)", $p->pid, $p->owner;
    }
    #note 'parent is running' if $p->running;
}

__END__

prove -lv t/03-filepid.t



( run in 0.357 second using v1.01-cache-2.11-cpan-8d75d55dd25 )