Edit File: restartsrv
#!/usr/local/cpanel/3rdparty/bin/perl # cpanel - scripts/restartsrv Copyright 2015 cPanel, Inc. # All rights Reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited package Scripts::restartsrv; use strict; use warnings; use Cpanel::Exception (); use Cpanel::Exception::Utils (); use Cpanel::RestartSrv (); use Cpanel::TimeHiRes (); use Cpanel::Services::Enabled (); use Cpanel::Services (); use Cpanel::Sys::OS::Check (); use Cpanel::Sys::Setsid (); use Cpanel::Output (); use Try::Tiny; #Names that are aliases of each other are on the same line use constant KNOWN_SERVICES => ( 'apache', 'httpd', 'apache_php_fpm', 'bind', 'chkservd', 'clamd', 'cpanel_php_fpm', 'cpanel', 'cpanellogd', 'cpdavd', 'cpipv6', 'cpgreylistd', 'cphulkd', 'cpsrvd', 'crond', 'dovecot', 'dnsadmin', 'exim', 'ftp', 'ftpd', 'ftpmagic', 'ftpserver', 'imap', 'ipaliases', 'mailman', 'mydns', 'mysql', 'mysqld', 'named', 'nscd', 'nsd', 'openssh', 'p0f', 'lmtp', 'pdns', 'powerdns', 'pop', 'postgres', 'postgresql', 'proftpd', 'pureftpd', 'queueprocd', 'spamd', 'sshd', 'tailwatchd', 'rsyslog', 'rsyslogd', ); our $DEFAULT_TIMEOUT = 1800; # mysql can take up to 1800 seconds to restart our $DEFAULT_SLEEP_INTERVAL = 0.25; our $INIT_DIR = q{/etc/init.d}; # used for testing our $STATUS_CHECK_MAX_ATTEMPTS = 10; # transition to refactored init system # if ( !caller() ) { my ( $service, @args ) = handle_service(@ARGV); if ( defined $service ) { my $cmd; # this is required for ftpd & ftpserver, which are specials if ( -e "/usr/local/cpanel/scripts/restartsrv_$service" ) { $cmd = "/usr/local/cpanel/scripts/restartsrv_$service"; if ( !-x _ ) { warn "“$cmd” is not executable ($!); falling back\n"; } } if ( !$cmd ) { $cmd = '/usr/local/cpanel/scripts/restartsrv_base'; unshift @args, $service; } exec {$cmd} $cmd, @args or die "exec($cmd, @args): $!"; } else { __PACKAGE__->script(@ARGV); } } sub handle_service { my (@args) = @_; my ( $service, @call_with ); foreach my $arg (@args) { my $arg_lc = ( $arg =~ tr<A-Z><a-z>r ); if ( grep { $arg_lc eq $_ } KNOWN_SERVICES() ) { $service = $arg_lc; next; } # preserve any other argument push @call_with, $arg; } return undef unless defined $service; # some services use alias or legacy names # $service = 'ftpserver' if $service eq 'ftp' || $service eq 'ftpmagic'; $service = 'imap' if $service eq 'pop'; $service = 'imap' if $service eq 'pop3'; $service = 'tailwatchd' if $service eq 'chkservd'; $service = 'cpsrvd' if $service eq 'cpanel'; $service = 'mysql' if $service eq 'mysqld'; $service = 'sshd' if $service eq 'openssh'; $service = 'pdns' if $service eq 'powerdns'; return ( $service, @call_with ); } sub script { my ( $class, @argv ) = @_; my $self = bless { 'timeout' => $DEFAULT_TIMEOUT, 'sleep_interval' => $DEFAULT_SLEEP_INTERVAL }, $class; local $SIG{'HUP'} = 'IGNORE'; local $| = 1; $self->{'output_obj'} = Cpanel::RestartSrv::get_formatted_output_object(@argv); $self->{'wait'} = grep ( m/--wait/, @argv ) ? 1 : 0; $self->_usage() if !@argv || !$argv[-1]; $self->{'service'} = $self->_find_internal_name_for_service( $argv[-1] ); $self->_setup_env(); my ( $service_status_system_command, $service_port_configuration, $service_restart_command, $service_check_system_command ) = $self->_lookup_service_restart_info( $self->{'service'} ); my ($err); try { if ( !Cpanel::Services::Enabled::is_enabled( $self->{'service'} ) ) { die Cpanel::Exception::create( 'Services::Disabled', [ service => $self->{'service'} ] ); } $self->_restart_service_and_wait_for_startup( $service_restart_command, $service_status_system_command ); $self->_check_service_using_system_command($service_check_system_command) if $service_check_system_command; $self->_check_service_using_test_connection($service_port_configuration) if $service_port_configuration && $service_port_configuration->{'port'}; } catch { $err = $_; }; if ($err) { $self->{'output_obj'}->error( Cpanel::Exception::get_string($err) . "\n" ); if ( UNIVERSAL::isa( $err, 'Cpanel::Exception::Services::NotInstalled' ) || UNIVERSAL::isa( $err, 'Cpanel::Exception::Services::Disabled' ) ) { return 0; } $self->output(""); $self->_show_startup_log(); $self->{'output_obj'}->error("$self->{'service'} has failed. Please contact your system administrator if the service does not automagically recover."); return 0; } $self->_show_startup_log(); $self->{'output_obj'}->success("$self->{'service'} started successfully."); return 1; } sub _restart_service_and_wait_for_startup { my ( $self, $service_restart_command, $service_status_system_command ) = @_; require Cpanel::TempFile; my $temp_obj = Cpanel::TempFile->new(); my $error_log_file = $temp_obj->file(); my $restart_pid = $self->_run_restart_command_in_background( $service_restart_command, $error_log_file ); $self->output_partial("Waiting for “$self->{'service'}” to restart …"); my ( $restart_child_completed, $exit_status ) = $self->_wait_for_restart_child_to_complete($restart_pid); if ( !$restart_child_completed ) { require Cpanel::Kill::Single; $exit_status = Cpanel::Kill::Single::safekill_single_pid( $restart_pid, 3 ); } if ($exit_status) { my $error_log_file_contents = Cpanel::LoadFile::load($error_log_file); $self->_handle_premature_restart_exit( $service_restart_command, $exit_status, $error_log_file_contents ); } my $service_status = $self->_wait_for_service_to_init_by_checking_status_command($service_status_system_command) if $service_status_system_command; $self->output("…finished."); $self->output(""); if ($service_status) { $self->{'output_obj'}->display_message_set( "Service Status", $service_status ); } return 1; } sub _check_service_using_system_command { my ( $self, $service_check_system_command ) = @_; my $test_name = "Service Check Test"; my $service_status = $self->_run_check_command_until_successful_or_max_attempts_reached($service_check_system_command); chomp($service_status); $service_status = join( "\n", grep { length $_ } split( m{\n}, $service_status ) ); chomp($service_status); # # Restartsrv scripts return an empty string # # Init scripts return 'runnning..' _is_running_message # if ( $service_status && !$self->_is_running_message($service_status) ) { die Cpanel::Exception::create( 'Services::CheckFailed', [ service => $self->{'service'}, message => $service_status ] ); } return 1; } # TODO: Breakout Cpanel::TailWatch::Chkservd's alternate implementation # into a module and use here. sub _check_service_using_test_connection { my ( $self, $service_port_configuration ) = @_; my $port = $service_port_configuration->{'port'}; my $send = $service_port_configuration->{'send'}; my $res = $service_port_configuration->{'expect'}; my $test_name = "Connection Test"; my $host = "127.0.0.1"; if ( $port eq '80' ) { require Cpanel::HttpUtils::ApRestart; require Cpanel::LoadFile; my $file_pid = Cpanel::LoadFile::loadfile( Cpanel::HttpUtils::ApRestart::DEFAULT_PID_FILE_LOCATION() ) || 0; chomp $file_pid; Cpanel::HttpUtils::ApRestart::_wait_ap_restart( 0, Cpanel::HttpUtils::ApRestart::DEFAULT_PID_FILE_LOCATION(), $file_pid ); my ( $restart_status, $our_restart_message ) = Cpanel::HttpUtils::ApRestart::_check_ap_restart( Cpanel::HttpUtils::ApRestart::DEFAULT_PID_FILE_LOCATION() ); if ( !$restart_status ) { die Cpanel::Exception::create( 'Services::CheckFailed', [ service => $self->{'service'}, message => $our_restart_message ] ); } } else { require Socket; require IO::Handle; my ( $response, $err ); try { local $SIG{'PIPE'} = sub { die "Connection unexpectedly closed."; }; local $SIG{'ALRM'} = sub { die "Timed out while trying to connect."; }; alarm( $self->{'wait'} ? 28 : 14 ); my $proto = getprotobyname('tcp'); my $socket = IO::Handle->new(); for ( my $time = 0; $time < 30; $time++ ) { socket( $socket, &Socket::AF_INET, &Socket::SOCK_STREAM, $proto ) || die "Cold not create a socket: $!"; my $sin = Socket::sockaddr_in( $port, Socket::inet_aton("127.0.0.1") ) || die "Cannot connect to 127.0.0.1:$port: $!"; connect( $socket, $sin ) and last; sleep(1); } alarm(0); local $SIG{'ALRM'} = sub { die "Timed out while waiting for a response."; }; alarm(30); if ( length $send ) { send( $socket, "$send\n\n", 0 ) || die "Failed to send data to the socket: $!"; } if ( !defined recv( $socket, $response, ( length($res) ), 0 ) ) { die "Failed to receive data from the socket: $!"; } if ( $response !~ m/^$res/ ) { $res =~ s/\n//g; $response =~ s/\n//g; die "$self->{'service'}: [$response != $res]"; } alarm(0); } catch { $err = $_; }; alarm(0); if ($err) { die Cpanel::Exception::create( 'Services::BadResponse', [ service => $self->{'service'}, host => $host, port => $port, error => Cpanel::Exception::Utils::traceback_to_error( Cpanel::Exception::get_string($err) ) ] ); } $self->{'output_obj'}->display_message_set( $test_name, "Successfully connected to $host:$port and received response: “$response”." ); } return 1; } sub _setup_env { my ($self) = @_; return Cpanel::Services::setup_env_for_starting_a_daemon(); } sub _wait_for_service_to_init_by_checking_status_command { my ( $self, $command ) = @_; $self->output_partial("…waiting for “$self->{'service'}” to initialize …"); return $self->_status_check_loop( $command, sub { my ($service_status) = @_; $self->output_partial("…"); $self->_check_if_service_is_available($service_status); return 1 if $self->_is_completed_message($service_status); return 0; } ); } sub _run_check_command_until_successful_or_max_attempts_reached { my ( $self, $command ) = @_; return $self->_status_check_loop( $command, sub { my ($service_status) = @_; $self->_check_if_service_is_available($service_status); return 1 if !$service_status || $self->_is_completed_message($service_status); return 0; } ); } sub _status_check_loop { my ( $self, $command, $check_code ) = @_; my $service_status; for ( 0 .. ( $STATUS_CHECK_MAX_ATTEMPTS * 2 ) ) { $service_status = $self->_safe_getstatus($command) || ''; last if ( $check_code->($service_status) ); Cpanel::TimeHiRes::sleep(0.5); } return $service_status; } sub _is_running_message { my ( $self, $service_status ) = @_; return 1 if ( $service_status !~ m{not\s+running}i && $service_status =~ m{running}i ); return 0; } sub _is_completed_message { my ( $self, $service_status ) = @_; return 1 if ( $service_status =~ m{(?:not installed|disabled)}i || $self->_is_running_message($service_status) ); return 0; } sub _safe_getstatus { my ( $self, $cmd ) = @_; require Cpanel::SafeRun::Object; my @args = @{$cmd}; my $program = shift @args; my $saferun; my $err; try { $saferun = Cpanel::SafeRun::Object->new( 'program' => $program, 'args' => \@args, 'timeout' => 60, 'read_timeout' => 60, ); } catch { $err = $_; }; return Cpanel::Exception::get_string($err) if $err; my @status; if ( $saferun->CHILD_ERROR() ) { push @status, $saferun->autopsy(); } if ( my $stdout = $saferun->stdout() ) { push @status, $stdout; } if ( my $stderr = $saferun->stderr() ) { push @status, $stderr; } my $service_status = join( "\n", @status ); chomp($service_status); return $service_status; } sub _wait_for_restart_child_to_complete { my ( $self, $restart_pid ) = @_; my $count = 0; while ( $count <= $self->{'timeout'} ) { local $?; my $wait_result = waitpid( $restart_pid, 1 ); if ( $wait_result > 0 ) { my $exit_status = $?; return ( 1, $exit_status ); } elsif ( $wait_result == -1 ) { return (1); } $self->output_partial("…"); eval { Cpanel::TimeHiRes::sleep( $self->{'sleep_interval'} ); }; # eval used for tight loop because try is expensive $count += $self->{'sleep_interval'}; } return (0); } sub _show_startup_log { my ($self) = @_; require Cpanel::Services::Log::Display; return Cpanel::Services::Log::Display->new( %{$self} )->show_startup_log(); } sub _check_system_service { my $service = shift; return unless $service; # systemd: do not try to check case insensitive return $service if Cpanel::RestartSrv::has_service_via_systemd($service); my $candidate; if ( opendir( my $dh, $INIT_DIR ) ) { foreach my $f ( readdir($dh) ) { my $path = $INIT_DIR . '/' . $f; next if -d $path; next unless -f $path; # if we find the service then stop here if ( $service eq $f ) { $candidate = $service; last; } # need to use case insensitive some 3rd party services # i.e. mailscanner -> MailScanner if ( lc($service) eq lc($f) ) { $candidate = $f; # do not stop, we might found the case sensitive match later } } closedir($dh); } return $candidate; } sub _lookup_service_restart_info { my ( $self, $service ) = @_; my ( $service_status_system_command, $service_port_configuration, $service_restart_command, $service_check_system_command ); my $scripts = '/usr/local/cpanel/scripts'; _check_service_name($service); # will die if service name isn't valid if ( -e "$scripts/restartsrv_$service" ) { $service_check_system_command = [ "$scripts/restartsrv_$service", '--check' ]; $service_status_system_command = [ "$scripts/restartsrv_$service", '--status' ]; $service_restart_command = [ "$scripts/restartsrv_$service", '--restart' ]; } elsif ( my $system_service = _check_system_service($service) ) { # use status action to check a system service $service_check_system_command = [ "/sbin/service", $system_service, 'status' ]; $service_status_system_command = [ "/sbin/service", $system_service, 'status' ]; $service_restart_command = [ "/sbin/service", $system_service, 'restart' ]; } else { die Cpanel::Exception::create( 'Services::Unknown', [ service => $service ] ); } return ( $service_status_system_command, $service_port_configuration, $service_restart_command, $service_check_system_command ); } sub _check_service_name { my ($service) = @_; return if Cpanel::Sys::OS::Check::has_systemd() && $service =~ m{^[A-Za-z0-9-_\@]+$}; # can be getty@tty with systemd return if $service =~ m{^[A-Za-z0-9-_]+$}; die Cpanel::Exception::create( 'InvalidParameter', '“[_1]” is not a valid service name.', [$service] ); } sub _find_internal_name_for_service { my ( $self, $service ) = @_; require Cpanel::Config::LoadCpConf; my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf(); if ( $service =~ m/rsyslog/i ) { $service = 'rsyslogd'; } elsif ( $service =~ m/syslog/i ) { $service = 'syslogd'; } elsif ( $service =~ m/inet/i ) { $service = 'inetd'; } elsif ( $service =~ m/pop/i ) { $service = 'imap'; } # pop now comes with dovecot return $service; } sub _run_restart_command_in_background { my ( $self, $service_restart_command, $error_log_file ) = @_; my $text_service_restart_command = join( ' ', @{$service_restart_command} ); my $restart_pid; unless ( $restart_pid = Cpanel::Sys::Setsid::full_daemonize( { 'keep_parent' => 1 } ) ) { $0 = "$0 - running $service_restart_command"; if ($error_log_file) { open( STDERR, '>>', $error_log_file ) || die "Could not redirect STDERR to a temp file: $!"; } exec( @{$service_restart_command} ) or exit 1; } return $restart_pid; } sub _usage { my ($self) = @_; $self->output("Usage: $0 [--wait] <service>"); $self->output(""); $self->output("Examples:"); $self->output("\t$0 imap"); $self->output("\t$0 mysql"); $self->output(""); $self->output("Options:"); $self->output("\t--wait: Increase the timeout for checking a service via connnect."); $self->output(""); exit(1); } sub _handle_premature_restart_exit { my ( $self, $restart_command, $exit_status, $error_log_file_contents ) = @_; $self->output("…failed."); $self->output(""); $self->_check_if_service_is_available($error_log_file_contents); $self->{'output_obj'}->display_message_set( "Error Output", $error_log_file_contents ); require Cpanel::ChildErrorStringifier; my $text_restart_command = join( ' ', @{$restart_command} ); die Cpanel::Exception::create( 'Services::RestartError', [ service => $self->{'service'}, error => "The execution of “$text_restart_command” failed because: " . Cpanel::ChildErrorStringifier->new($exit_status)->autopsy() ] ); } sub _check_if_service_is_available { my ( $self, $service_status ) = @_; return 1 if !$service_status; # TODO: make check scripts modulinos that can be loaded in so # we can check a return code. if ( $service_status =~ m{not installed} ) { die Cpanel::Exception::create( 'Services::NotInstalled', [ service => $self->{'service'} ] ); } # Match # cpanel-dovecot-solr is disabled # # Do not match # loaded (/etc/systemd/system/cpanel-dovecot-solr.service; enabled; vendor preset: disabled) elsif ( $service_status =~ m{disabled}i && $service_status !~ m{: disabled} ) { die Cpanel::Exception::create( 'Services::Disabled', [ service => $self->{'service'} ] ); } return 1; } sub output { my ( $self, $output ) = @_; return $self->{'output_obj'}->out( $output, $Cpanel::Output::SOURCE_LOCAL, $Cpanel::Output::COMPLETE_MESSAGE ); } sub output_partial { my ( $self, $output ) = @_; return $self->{'output_obj'}->out( $output, $Cpanel::Output::SOURCE_LOCAL, $Cpanel::Output::PARTIAL_MESSAGE ); } 1;