use MooseX::Declare; class Unicorn::Manager { use Carp; # for sane error reporting use File::Basename; # to strip the config file from the path our $VERSION = '0.03.03'; use Unicorn::Manager::Proc; has username => ( is => 'rw', isa => 'Str', required => 1 ); has group => ( is => 'rw', isa => 'Str' ); has config => ( is => 'rw', isa => 'HashRef' ); has DEBUG => ( is => 'rw', isa => 'Bool', default => 0 ); has proc => ( is => 'rw', isa => 'Unicorn::Manager::Proc' ); has uid => ( is => 'rw', isa => 'Num' ); has rails => ( is => 'rw', isa => 'Bool', default => 0 ); method start ( Str :config($config_file), ArrayRef :$args? ) { my $timeout = 20; if ( -f $config_file ){ if (my $pid = fork()){ my $spawned = 0; while ( $spawned == 0 && $timeout > 0 ){ sleep 2; $self->proc->refresh; $spawned = 1 if $self->proc->process_table->ptable->{$self->uid}; $timeout--; } croak "Failed to start unicorn. Timed out.\n" if $timeout <= 0; } else { # 0 => name # 2 => uid # 3 => gid # 7 => home dir my @passwd = getpwnam($self->username); # drop rights: # group rights first because we can not drop group rights # after user rights # set $HOME to our users home directory $ENV{'HOME'} = $passwd[7]; $( = $) = $passwd[3]; $< = $> = $passwd[2]; my $appdir = ''; my $conf_file; my $conf_dir; if ( defined $config_file && $config_file ne '' ){ $conf_dir = dirname($config_file); $conf_file = basename($config_file); if ( $self->_is_abspath($conf_dir) ){ $appdir = $conf_dir; } else { $appdir = $passwd[7] . '/' . $conf_dir; } } $self->_change_dir ( $appdir ); my $argstring; $argstring .= $_ . ' ' for @{ $args }; # dirty hack. remove this! $ENV{'RAILS_ENV'} = 'production'; # spawn the unicorn if ($self->rails){ # start unicorn_rails exec "/bin/bash --login -c \"unicorn_rails -c $conf_file $argstring\""; } else { # start unicorn exec "/bin/bash --login -c \"unicorn -c $conf_file $argstring\""; } } } else { return 0; } return 1; } method stop { my $master = ( keys %{ $self->proc->process_table->ptable->{$self->uid} } )[0]; $self->_send_signal('QUIT', $master) if $master; return 1; } method restart ( Str :$mode? = 'graceful' ) { my @signals = ( 'USR2', 'WINCH', 'QUIT'); my $master = ( keys %{ $self->proc->process_table->ptable->{$self->uid} } )[0]; my $err = 0; for (@signals){ $err += $self->_send_signal ($_, $master); sleep 5; } if ( (defined $mode && $mode eq 'hard') || $err ){ $err = 0; $err += $self->stop; sleep 3; $err += $self->start; } if ($err){ carp "error restarting unicorn! error code: $err\n"; return 0; } else { return 1; } } method reload { my $err; for my $pid (keys %{ $self->proc->process_table->ptable->{$self->uid} }){ $err = $self->_send_signal( 'HUP', $pid ); } $err > 0 ? return 0 : return 1; } method read_config ( Str $filename ) { # TODO # should return a config object # # all config related stuff should go into a seperate class anyway: Unicorn::Manager::Config return 0; } method write_config ( Str $filename ) { # TODO # this one wont be fun .. # create a unicorn.conf from config hash # this is basically ruby code, so an idea could be to build it from # heredoc snippets # # should return a string. could be written to file or screen. # # all config related stuff should go into a seperate class anyway: Unicorn::Manager::Config return 0; } method add_worker ( Num :$num? = 1 ) { # return error on non positive number return 0 unless $num > 0; my $err = 0; for ( 1 .. $num ){ my $master = ( keys %{ $self->proc->process_table->ptable->{$self->uid} } )[0]; $err += $self->_send_signal( 'TTIN', $master ); } $err > 0 ? return 0 : return 1; } method remove_worker ( Num :$num? = 1 ){ # return error on non positive number return 0 unless $num > 0; my $err = 0; my $master = ( keys %{ $self->proc->process_table->ptable->{$self->uid} } )[0]; my $count = @{ $self->proc->process_table->ptable->{$self->uid}->{$master} }; # save at least one worker $num = $count - 1 if $num >= $count; if ($self->DEBUG){ print "\$count => $count\n"; print "\$num => $num\n"; } for ( 1 .. $num ){ $err += $self->_send_signal( 'TTOU', $master ); } $err > 0 ? return 0 : return 1; } # # send a signal to a pid # method _send_signal (Str $signal!, Num $pid!) { (kill $signal => $pid) ? return 0 : return 1; } # # small piece to check if a path is starting at root # method _is_abspath ( Str $path! ) { return 0 unless $path =~ /^\//; return 1; } # # cd into the given dir # requires an absolute path # method _change_dir ( Str $dir! ) { # requires abs path return 0 unless $self->_is_abspath($dir); my $dh; opendir $dh, $dir; chdir $dh; closedir $dh; use Cwd; cwd() eq $dir ? return 1 : return 0; } method BUILD { # does username exist? if ($self->DEBUG){ print "Initializing object with username: " . $self->username . "\n"; } croak "no such username\n" unless getpwnam($self->username); $self->uid((getpwnam($self->username))[2]); $self->proc(Unicorn::Manager::Proc->new) unless $self->proc; } } =head1 NAME Unicorn::Manager - A Perl interface to the Unicorn webserver =head1 VERSION Version 0.02 =head1 SYNOPSIS The Unicorn::Manager module aimes to provide methods to start, stop and gracefully restart the server. You can add and remove workers on the fly. TODO: Unicorn::Manager::Config should provide methods to create config files and offer an OO interface to the config object. Until now basically only unicorn_rails is supported. This Lib is a quick hack to integrate management of rails apps with rvm and unicorn into perl scripts. Also some assumption are made about your environment: you use Linux (the module relies on /proc) you use the bash shell your unicorn config is located in your apps root directory every user is running one single application I will add and improve what is needed though. Requests and patches are welcome. =head1 ATTRIBUTES/CONSTRUCTION Unicorn::Manager has following attributes: =head2 username Username of the user that owns the Unicorn process that will be operated on. The username is a required attribute. =head2 group Groupname of the Unicorn process. Defaults to the users primary group. =head2 config A HashRef containing the information to create a Unicorn::Config object. See perldoc Unicon::Config for more information. =head2 DEBUG Is a Bool type attribute. Defaults to 'false' and prints additional information if set 'true'. TODO: Needs to be improved. =head2 Contruction my $unicorn = Unicorn->new( username => 'myuser', group => 'mygroup', ); =head1 METHODS =head2 start $unicorn->start( config => '/path/to/my/config', args => ['-D', '--host 127.0.0.1'], ); Parameters are the path to the config file and an optional ArrayRef with additional arguments. These will override the arguments defined in the config file. This method needs more love and will be rethought and rewritten. Now it assumes the config file is located in the rails apps root directory. It changes into this directory and drops rights to start unicorn. =head2 stop $unicorn->stop; Sends SIGQUIT to the unicorn master. This will gracefully shut down the workers and then quit the master. If graceful stop will not work SIGKILL will be send. If no master is running nothing will be happening. =head2 restart my $result = $unicorn->restart( mode => 'hard'); Mode defaults to 'graceful'. If mode is set 'hard' graceful restart will be tried first and $unicorn->stop plus $unicorn->start if that fails. returns true on success, false on error. =head2 reload my $result = $unicorn->reload; Reloads the users unicorn. Reloads the config file. Code changes are reloaded unless app_preload is set. Basically a SIGHUP will be send to the unicorn master. =head2 read_config NOT YET IMPLEMENTED $unicorn->read_config('/path/to/config'); Reads the configuration from a unicorn config file. =head2 write_config NOT YET IMPLEMENTED $unicorn->make_config('/path/to/config'); Writes the configuration into a unicorn config file. =head2 add_worker my $result = $unicorn->add_worker( num => 3 ); Adds num workers to the users unicorn. num defaults to 1. =head2 remove_worker my $result = $unicorn->remove_worker( num => 3 ); Removes num workers but maximum of workers count -1. num defaults to 1. =head1 AUTHOR Mugen Kenichi, C<< <mugen.kenichi at uninets.eu> >> =head1 BUGS Report bugs at: =over 2 =item * Unicorn::Manager issue tracker L<https://github.com/mugenken/Unicorn/issues> =item * support at uninets.eu C<< <mugen.kenichi at uninets.eu> >> =back =head1 SUPPORT =over 2 =item * Technical support C<< <mugen.kenichi at uninets.eu> >> =back =cut