okay,.. I think this is ready for release
[gitmo/MooseX-Daemonize.git] / lib / MooseX / Daemonize / Core.pm
index 27e0dd6..5447f63 100644 (file)
@@ -1,5 +1,6 @@
 package MooseX::Daemonize::Core;
-use strict;    # because Kwalitee is pedantic
+use strict;         # cause Perl::Critic errors are annoying
+use MooseX::Getopt; # to load the NoGetopt metaclass
 use Moose::Role;
 
 our $VERSION = 0.01;
@@ -7,88 +8,235 @@ our $VERSION = 0.01;
 use POSIX ();
 
 has is_daemon => (
-    isa     => 'Bool',
-    is      => 'rw',
-    default => sub { 0 },
+    # NOTE:
+    # this should never be accessible
+    # from the command line
+    # - SL
+    metaclass => 'NoGetopt',
+    isa       => 'Bool',
+    is        => 'rw',
+    default   => sub { 0 },
 );
 
-sub daemon_fork   { fork }
-sub daemon_detach { 
-    # ignore these signals
-    for (qw(TSTP TTIN TTOU PIPE POLL STOP CONT CHLD)) {
-        $SIG{$_} = 'IGNORE' if (exists $SIG{$_});
+sub daemon_fork {
+    my ($self, %options) = @_;
+
+    $SIG{CHLD} = 'IGNORE'
+        if $options{ignore_zombies};
+
+    if (my $pid = fork) {
+        return $pid;
+    }
+    else {
+        $self->is_daemon(1);
+        return;
+    }
+}
+
+sub daemon_detach {
+    my ($self, %options) = @_;
+
+    return unless $self->is_daemon; # return if parent ...
+
+    # now we are in the daemon ...
+
+    (POSIX::setsid)  # set session id
+        || confess "Cannot detach from controlling process";
+
+    unless ($options{no_double_fork}) {
+        $SIG{'HUP'} = 'IGNORE';
+        fork && exit;
     }
-    
-    POSIX::setsid;  # set session id
+
     chdir '/';      # change to root directory
-    umask 0;        # clear the file creation mask            
-    
+    umask 0;        # clear the file creation mask
+
     # get the max numnber of possible file descriptors
     my $openmax = POSIX::sysconf( &POSIX::_SC_OPEN_MAX );
     $openmax = 64 if !defined($openmax) || $openmax < 0;
-    
-    # close them all 
+
+    # close them all
     POSIX::close($_) foreach (0 .. $openmax);
 
-    open(STDIN,  "+>/dev/null");
-    open(STDOUT, "+>&STDIN");
-    open(STDERR, "+>&STDIN");    
+    # fixup STDIN ...
+
+    open(STDIN, "+>/dev/null")
+        or confess "Could not redirect STDOUT to /dev/null";
+
+    # fixup STDOUT ...
+
+    if (my $stdout_file = $ENV{MX_DAEMON_STDOUT}) {
+        open STDOUT, ">", $stdout_file
+            or confess "Could not redirect STDOUT to $stdout_file : $!";
+    }
+    else {
+        open(STDOUT, "+>&STDIN")
+            or confess "Could not redirect STDOUT to /dev/null";
+    }
+
+    # fixup STDERR ...
+
+    if (my $stderr_file = $ENV{MX_DAEMON_STDERR}) {
+        open STDERR, ">", "ERR.txt"
+            or confess "Could not redirect STDERR to $stderr_file : $!";
+    }
+    else {
+        open(STDERR, "+>&STDIN")
+            or confess "Could not redirect STDERR to /dev/null";        ;
+    }
+
+    # do a little house cleaning ...
+
+    # Avoid 'stdin reopened for output'
+    # warning with newer perls
+    open( NULL, '/dev/null' );
+    <NULL> if (0);
+
+    # return success
+    return 1;
 }
 
 sub daemonize {
-    my ($self) = @_;
-    return if $self->daemon_fork;
-    $self->daemon_detach;
-    $self->is_daemon(1);
+    my ($self, %options) = @_;
+    $self->daemon_fork(%options);
+    $self->daemon_detach(%options);
 }
 
 1;
+
 __END__
 
-=head1 NAME
+=pod
 
-MooseX::Daemonize::Core - provides a Role the core daemonization features
+=head1 NAME
 
-=head1 VERSION
+MooseX::Daemonize::Core - A Role with the core daemonization features
 
 =head1 SYNOPSIS
-     
+
+  package My::Daemon;
+  use Moose;
+
+  with 'MooseX::Daemonize::Core';
+
+  sub start {
+      my $self = shift;
+      # daemonize me ...
+      $self->daemonize;
+      # return from the parent,...
+      return unless $self->is_daemon;
+      # but continue on in the child (daemon)
+  }
+
 =head1 DESCRIPTION
 
+This is the basic daemonization Role, it provides a few methods (see
+below) and the minimum features needed to properly daemonize your code.
+
+=head2 Important Notes
+
+None of the methods in this role will exit the parent process for you,
+it only forks and detaches your child (daemon) process. It is your
+responsibility to exit the parent process in some way.
+
+There is no PID or PID file management in this role, that is your
+responsibility (see some of the other roles in this distro for that).
+
 =head1 ATTRIBUTES
 
 =over
 
-=item is_daemon Bool
+=item I<is_daemon (is => rw, isa => Bool)>
 
-If true, the process is the backgrounded process. This is useful for example
-in an after 'start' => sub { } block
+This attribute is used to signal if we are within the
+daemon process or not.
 
 =back
 
-=head1 METHODS 
+=head1 METHODS
 
 =over
 
-=item daemon_fork()
+=item B<daemon_fork (%options)>
+
+This forks off the child process to be daemonized. Just as with
+the built in fork, it returns the child pid to the parent process,
+0 to the child process. It will also set the is_daemon flag
+appropriately.
+
+The C<%options> available for this function are:
+
+=over 4
+
+=item I<ignore_zombies>
+
+Setting this key to a true value will result in setting the C<$SIG{CHLD}>
+handler to C<IGNORE>. This tells perl to clean up zombie processes. By
+default, and for the most part you don't I<need> it, only when you turn off
+the double fork behavior (with the I<no_double_fork> option) in C<daemon_detach>
+do you sometimes want this behavior.
+
+=back
+
+=item B<daemon_detach (%options)>
+
+This detaches the new child process from the terminal by doing
+the following things.
+
+=over 4
+
+=item Becomes a session leader
 
-=item daemon_detach()
+This detaches the program from the controlling terminal, it is
+accomplished by calling POSIX::setsid.
 
-=item daemonize()
+=item Performing the double-fork
 
-Calls C<Proc::Daemon::Init> to daemonize this process. 
+See below for information on how to change this part of the process.
 
-=item setup_signals()
+=item Changes the current working directory to "/"
 
-Setup the signal handlers, by default it only sets up handlers for SIGINT and SIGHUP
+This is standard daemon behavior, if you want a different working
+directory then simply change it later in your daemons code.
 
-=item handle_sigint()
+=item Clears the file creation mask.
 
-Handle a INT signal, by default calls C<$self->stop()>
+=item Closes all open file descriptors.
 
-=item handle_sighup()
+=item Reopen STDERR, STDOUT & STDIN to /dev/null
 
-Handle a HUP signal. By default calls C<$self->restart()>
+This behavior can be controlled slightly though the MX_DAEMON_STDERR
+and MX_DAEMON_STDOUT environment variables. It will look for a filename
+in either of these variables and redirect STDOUT and/or STDERR to those
+files. This is useful for debugging and/or testing purposes.
+
+=back
+
+The C<%options> available for this function are:
+
+=over 4
+
+=item I<no_double_fork>
+
+Setting this option to true will cause this method to not perform the
+typical double-fork, which is extra added protection from your process
+accidentally aquiring a controlling terminal. More information can be
+found above, and by Googling "double fork daemonize".
+
+If you the double-fork behavior off, you might want to enable the
+I<ignore_zombies> behavior in the C<daemon_fork> method.
+
+=back
+
+B<NOTE>
+
+If called from within the parent process (the is_daemon flag is set to
+false), this method will simply return and do nothing.
+
+=item B<daemonize (%options)>
+
+This will simply call C<daemon_fork> followed by C<daemon_detach>, it will
+pass any C<%options> onto both methods.
 
 =item meta()
 
@@ -96,38 +244,78 @@ The C<meta()> method from L<Class::MOP::Class>
 
 =back
 
-=head1 DEPENDENCIES
+=head1 STUFF YOU SHOULD READ
 
-=for author to fill in:
-    A list of all the other modules that this module relies upon,
-    including any restrictions on versions, and an indication whether
-    the module is part of the standard Perl distribution, part of the
-    module's distribution, or must be installed separately. ]
+=over 4
 
-Obviously L<Moose>, and L<Proc::Daemon>
+=item Note about double fork
 
-=head1 INCOMPATIBILITIES
+Taken from L<http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012>
+in a comment entitled I<The second fork _is_ necessary by Jonathan Bartlett>,
+it is not the definitive statement on the issue, but it's clear and well
+written enough so I decided to reproduce it here.
 
-=for author to fill in:
-    A list of any modules that this module cannot be used in conjunction
-    with. This may be due to name conflicts in the interface, or
-    competition for system or program resources, or due to internal
-    limitations of Perl (for example, many modules that use source code
-    filters are mutually incompatible).
+  The first fork accomplishes two things - allow the shell to return,
+  and allow you to do a setsid().
 
-None reported.
+  The setsid() removes yourself from your controlling terminal. You
+  see, before, you were still listed as a job of your previous process,
+  and therefore the user might accidentally send you a signal. setsid()
+  gives you a new session, and removes the existing controlling terminal.
 
+  The problem is, you are now a session leader. As a session leader, if
+  you open a file descriptor that is a terminal, it will become your
+  controlling terminal (oops!). Therefore, the second fork makes you NOT
+  be a session leader. Only session leaders can acquire a controlling
+  terminal, so you can open up any file you wish without worrying that
+  it will make you a controlling terminal.
 
-=head1 BUGS AND LIMITATIONS
+  So - first fork - allow shell to return, and permit you to call setsid()
+
+  Second fork - prevent you from accidentally reacquiring a controlling
+  terminal.
+
+That said, you don't always want this to be the behavior, so you are
+free to specify otherwise using the C<%options>.
+
+=item Note about zombies
+
+Doing the double fork (see above) tends to get rid of your zombies since
+by the time you have double forked your daemon process is then owned by
+the init process. However, sometimes the double-fork is more than you
+really need, and you want to keep your daemon processes a little closer
+to you. In this case you have to watch out for zombies, you can avoid then
+by just setting the C<ignore_zombies> option (see above).
+
+=back
+
+=head1 ENVIRONMENT VARIABLES
 
-=for author to fill in:
-    A list of known problems with the module, together with some
-    indication Whether they are likely to be fixed in an upcoming
-    release. Also a list of restrictions on the features the module
-    does provide: data types that cannot be handled, performance issues
-    and the circumstances in which they may arise, practical
-    limitations on the size of data sets, special cases that are not
-    (yet) handled, etc.
+These variables are best just used for debugging and/or testing, but
+not used for actual logging. For that, you should reopen STDOUT/ERR on
+your own.
+
+=over 4
+
+=item B<MX_DAEMON_STDOUT>
+
+A filename to redirect the daemon STDOUT to.
+
+=item B<MX_DAEMON_STDERR>
+
+A filename to redirect the daemon STDERR to.
+
+=back
+
+=head1 DEPENDENCIES
+
+L<Moose::Role>, L<POSIX>
+
+=head1 INCOMPATIBILITIES
+
+None reported.
+
+=head1 BUGS AND LIMITATIONS
 
 No bugs have been reported.
 
@@ -137,28 +325,26 @@ L<http://rt.cpan.org>.
 
 =head1 SEE ALSO
 
-L<Proc::Daemon>, L<Daemon::Generic>, L<MooseX::Getopt>
-
-=head1 AUTHOR
+L<Proc::Daemon>
 
-Chris Prather  C<< <perigrin@cpan.org> >>
+This code is based B<HEAVILY> on L<Proc::Daemon>, we originally
+depended on it, but we needed some more flexibility, so instead
+we just stole the code.
 
-=head1 THANKS
-
-Mike Boyko, Matt S. Trout, Stevan Little, Brandon Black, Ash Berlin and the 
-#moose denzians
+=head1 AUTHOR
 
-Some bug fixes sponsored by Takkle Inc.
+Stevan Little  C<< <stevan.little@iinteractive.com> >>
 
 =head1 LICENCE AND COPYRIGHT
 
-Copyright (c) 2007, Chris Prather C<< <perigrin@cpan.org> >>. All rights 
+Copyright (c) 2007, Chris Prather C<< <perigrin@cpan.org> >>. All rights
 reserved.
 
+Portions heavily borrowed from L<Proc::Daemon> which is copyright Earl Hood.
+
 This module is free software; you can redistribute it and/or
 modify it under the same terms as Perl itself. See L<perlartistic>.
 
-
 =head1 DISCLAIMER OF WARRANTY
 
 BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
@@ -181,3 +367,5 @@ RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
 FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
 SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
 SUCH DAMAGES.
+
+=cut