package Ubic; $Ubic::VERSION = '1.60'; use strict; use warnings; # ABSTRACT: polymorphic service manager use POSIX qw(); use Carp; use IO::Handle; use Storable qw(freeze thaw); use Try::Tiny; use Scalar::Util qw(blessed); use Params::Validate qw(:all); use Ubic::Result qw(result); use Ubic::Multiservice::Dir; use Ubic::AccessGuard; use Ubic::Credentials; use Ubic::Persistent; use Ubic::AtomicFile; use Ubic::SingletonLock; use Ubic::Settings; our $SINGLETON; my $service_name_re = qr{^[\w-]+(?:\.[\w-]+)*$}; my $validate_service = { type => SCALAR, regex => $service_name_re }; # singleton constructor sub _obj { my ($param) = validate_pos(@_, 1); if (blessed($param)) { return $param; } if ($param eq 'Ubic') { # method called as a class method => singleton $SINGLETON ||= Ubic->new({}); return $SINGLETON; } die "Unknown argument '$param'"; } sub new { my $class = shift; my $options = validate(@_, { service_dir => { type => SCALAR, optional => 1 }, data_dir => { type => SCALAR, optional => 1 }, }); if (caller ne 'Ubic') { warn "Using Ubic->new constructor is discouraged. Just call methods as class methods."; } for my $key (qw/ service_dir data_dir /) { Ubic::Settings->$key($options->{ $key }) if defined $options->{$key}; } Ubic::Settings->check_settings; my $self = {}; $self->{data_dir} = Ubic::Settings->data_dir; $self->{service_dir} = Ubic::Settings->service_dir; $self->{status_dir} = "$self->{data_dir}/status"; $self->{lock_dir} = "$self->{data_dir}/lock"; $self->{tmp_dir} = "$self->{data_dir}/tmp"; $self->{service_cache} = {}; return bless $self => $class; } sub start($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); $self->enable($name); my $result = $self->do_cmd($name, 'start'); $self->set_cached_status($name, $result); return $result; } sub stop($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); $self->disable($name); # FIXME - 'stop' command can fail, in this case daemon will keep running. # This is bad. # We probably need to implement the same logic as when starting: # retry stop attempts until actual status matches desired status. my $result = $self->do_cmd($name, 'stop'); return $result; } sub restart($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); $self->enable($name); my $result = $self->do_cmd($name, 'stop'); $result = $self->do_cmd($name, 'start'); $self->set_cached_status($name, $result); return result('restarted'); # FIXME - should return original status } sub try_restart($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); unless ($self->is_enabled($name)) { return result('down'); } $self->do_cmd($name, 'stop'); $self->do_cmd($name, 'start'); return result('restarted'); } sub reload($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); unless ($self->is_enabled($name)) { return result('down'); } # if reload isn't implemented, do nothing # TODO - would it be better to execute reload as force-reload always? but it would be incompatible with LSB specification... my $result = $self->do_cmd($name, 'reload'); unless ($result->action eq 'reloaded') { die $result; } return $result; } sub force_reload($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); unless ($self->is_enabled($name)) { return result('down'); } my $result = $self->do_cmd($name, 'reload'); return $result if $result->action eq 'reloaded'; $self->try_restart($name); } sub status($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); return $self->do_cmd($name, 'status'); } sub enable($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); my $guard = $self->access_guard($name); my $status_obj = $self->status_obj($name); $status_obj->{status} = 'unknown'; $status_obj->{enabled} = 1; $status_obj->commit; return result('unknown'); } sub is_enabled($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); die "Service '$name' not found" unless $self->root_service->has_service($name); unless (-e $self->status_file($name)) { return $self->service($name)->auto_start(); } my $status_obj = $self->status_obj_ro($name); if ($status_obj->{enabled} or not exists $status_obj->{enabled}) { return 1; } return; } sub disable($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = $self->lock($name); my $guard = $self->access_guard($name); my $status_obj = $self->status_obj($name); delete $status_obj->{status}; $status_obj->{enabled} = 0; $status_obj->commit; } sub cached_status($$) { my ($self) = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $type; if (not $self->is_enabled($name)) { $type = 'disabled'; } elsif (-e $self->status_file($name)) { $type = $self->status_obj_ro($name)->{status}; } else { $type = 'autostarting'; } return Ubic::Result::Class->new({ type => $type, cached => 1 }); } sub do_custom_command($$) { my ($self) = _obj(shift); my ($name, $command) = validate_pos(@_, $validate_service, 1); # TODO - do all custom commands require locks? # they can be distinguished in future by some custom_commands_ext method which will provide hash { command => properties }, i think... my $lock = $self->lock($name); # TODO - check custom_command presence by custom_commands() method first? $self->do_sub(sub { $self->service($name)->do_custom_command($command); # can custom commands require custom arguments? }); } sub service($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); # this guarantees that : will be unambiguous separator in status filename (what??) unless ($self->{service_cache}{$name}) { # Service construction is a memory-leaking operation (because of package name randomization in Ubic::Multiservice::Dir), # so we need to cache each service which we create. $self->{service_cache}{$name} = $self->root_service->service($name); } return $self->{service_cache}{$name}; } sub has_service($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); # TODO - it would be safer to do this check without actual service construction # but it would require cron-based script which maintains list of all services return $self->root_service->has_service($name); } sub services($) { my $self = _obj(shift); return $self->root_service->services(); } sub service_names($) { my $self = _obj(shift); return $self->root_service->service_names(); } sub root_service($) { my $self = _obj(shift); unless (defined $self->{root}) { $self->{root} = Ubic::Multiservice::Dir->new($self->{service_dir}, { protected => 1 }); } return $self->{root}; } sub compl_services($$) { my $self = _obj(shift); my $line = shift; my @parts = split /\./, $line; if ($line =~ /\.$/) { push @parts, ''; } if (@parts == 0) { return $self->service_names; } my $node = $self->root_service; my $is_subservice = (@parts > 1); while (@parts > 1) { unless ($node->isa('Ubic::Multiservice')) { return; } my $part = shift @parts; return unless $node->has_service($part); # no such service $node = $node->service($part); } my @variants = $node->service_names; return map { ( $is_subservice ? $node->full_name.".".$_ : $_ ) } grep { $_ =~ m{^\Q$parts[0]\E} } @variants; } sub set_cached_status($$$) { my $self = _obj(shift); my ($name, $status) = validate_pos(@_, $validate_service, 1); my $guard = $self->access_guard($name); if (blessed $status) { croak "Wrong status param '$status'" unless $status->isa('Ubic::Result::Class'); $status = $status->status; } my $lock = $self->lock($name); if (-e $self->status_file($name) and $self->status_obj_ro($name)->{status} eq $status) { # optimization - don't update status if nothing changed return; } my $status_obj = $self->status_obj($name); $status_obj->{status} = $status; $status_obj->commit; } sub get_data_dir($) { my $self = _obj(shift); validate_pos(@_); return $self->{data_dir}; } sub set_data_dir($$) { my ($arg, $dir) = validate_pos(@_, 1, 1); my $md = sub { my $new_dir = shift; mkdir $new_dir or die "mkdir $new_dir failed: $!" unless -d $new_dir; }; $md->($dir); # FIXME - directory list is copy-pasted from Ubic::Admin::Setup for my $subdir (qw[ status simple-daemon simple-daemon/pid lock ubic-daemon tmp watchdog watchdog/lock watchdog/status ]) { $md->("$dir/$subdir"); } Ubic::Settings->data_dir($dir); if ($SINGLETON) { $SINGLETON->{lock_dir} = "$dir/lock"; $SINGLETON->{status_dir} = "$dir/status"; $SINGLETON->{tmp_dir} = "$dir/tmp"; $SINGLETON->{data_dir} = $dir; } } sub set_ubic_dir($$); *set_ubic_dir = \&set_data_dir; sub set_default_user($$) { my ($arg, $user) = validate_pos(@_, 1, 1); Ubic::Settings->default_user($user); } sub get_service_dir($) { my $self = _obj(shift); validate_pos(@_); return $self->{service_dir}; } sub set_service_dir($$) { my ($arg, $dir) = validate_pos(@_, 1, 1); Ubic::Settings->service_dir($dir); if ($SINGLETON) { $SINGLETON->{service_dir} = $dir; undef $SINGLETON->{root}; # force lazy regeneration } } sub status_file($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); return "$self->{status_dir}/".$name; } sub status_obj($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); return Ubic::Persistent->new($self->status_file($name)); } sub status_obj_ro($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); return Ubic::Persistent->load($self->status_file($name)); } sub access_guard($$) { my $self = _obj(shift); my ($name) = validate_pos(@_, $validate_service); return Ubic::AccessGuard->new( Ubic::Credentials->new(service => $self->service($name)) ); } sub lock($$) { my ($self) = _obj(shift); my ($name) = validate_pos(@_, $validate_service); my $lock = do { my $guard = $self->access_guard($name); Ubic::SingletonLock->new($self->{lock_dir}."/".$name); }; return $lock; } sub do_sub($$) { my ($self, $code) = @_; my $result = try { $code->(); } catch { die result($_); }; return result($result); } sub do_cmd($$$) { my ($self, $name, $cmd) = @_; $self->do_sub(sub { my $service = $self->service($name); my $creds = Ubic::Credentials->new( service => $service ); if ($creds->eq(Ubic::Credentials->new)) { # current credentials fit service expectations return $service->$cmd(); } # setting just effective uid is not enough, because: # - we can accidentally enter tainted mode, and service authors don't expect this # - local administrator may want to allow everyone to write their own services, and leaving root as real uid is an obvious security breach # (ubic will have to learn to compare service user with service file's owner for such policy to be safe, though - this is not implemented yet) $self->forked_call(sub { $creds->set(); return $service->$cmd(); }); }); } sub forked_call { my ($self, $callback) = @_; my $tmp_file = $self->{tmp_dir}."/".time.".$$.".rand(1000000); my $child; unless ($child = fork) { unless (defined $child) { die "fork failed"; } my $result; try { $result = { ok => $callback->() }; } catch { $result = { error => $_ }; }; try { Ubic::AtomicFile::store( freeze($result) => $tmp_file ); STDOUT->flush; STDERR->flush; POSIX::_exit(0); # don't allow to lock to be released - this process was forked from unknown environment, don't want to run unknown destructors } catch { # probably tmp_file is not writable warn $_; POSIX::_exit(1); }; } waitpid($child, 0); unless (-e $tmp_file) { die "temp file $tmp_file not found after fork"; } open my $fh, '<', $tmp_file or die "Can't read $tmp_file: $!"; my $content = do { local $/; <$fh>; }; close $fh or die "Can't close $tmp_file: $!"; unlink $tmp_file; my $result = thaw($content); if ($result->{error}) { die $result->{error}; } else { return $result->{ok}; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Ubic - polymorphic service manager =head1 VERSION version 1.60 =head1 SYNOPSIS Configure ubic: $ ubic-admin setup Write the service config: $ cat >/etc/ubic/service/foo.ini [options] bin = /usr/bin/foo.pl Start your service: $ ubic start foo Enjoy your daemonized, monitored service. =head1 DESCRIPTION This module is a perl frontend to ubic services. It is a singleton OOP class. All of its methods should be invoked as class methods: Ubic->start('foo'); Ubic->stop('foo'); my $status = Ubic->status('foo'); =head1 INTRODUCTION Ubic is a polymorphic service manager. Further directions: if you are looking for a general introduction to Ubic, see L; if you want to use ubic from the command line, see L; if you want to manage ubic services from the perl scripts, read this POD; if you want to write your own service, see L and other C modules. =head1 CONSTRUCTOR =over =item B<< Ubic->new({ ... }) >> All methods in this package can be invoked as class methods, but sometimes you may need to override some status dirs. In this case you should construct your own C instance. Note that you can't create several instances in one process and have them work independently. So, this constructor is actually just a weird way to override service_dir and data_dir. Constructor options (all of them are optional): =over =item I Name of dir with service descriptions (which will be used to construct root C object). =item I Dir into which ubic stores all of its data (locks, status files, tmp files). =back =back =head1 LSB METHODS See L for init-script method specifications. Following methods are trying to conform, except that all dashes in method names are replaced with underscores. These methods return the result objects, i.e., instances of the C class. =over =item B Start the service. =item B Stop the service. =item B Restart the service; start it if it's not running. =item B Restart the service if it is enabled. =item B Reload the service. This method will do reloading if the service implements C; it will throw an exception otherwise. =item B Reload the service if reloading is implemented, otherwise restart it. Does nothing if service is disabled. =item B Get the service status. =back =head1 OTHER METHODS =over =item B Enable the service. Enabled service means that service B be running. Watchdog will periodically check its status, attempt to restart it and mark it as I if it won't succeed. =item B Check whether the service is enabled. Returns true or false. =item B Disable the service. Disabled service means that the service is ignored by ubic. Its state will no longer be checked by the watchdog, and C will report that the service is I. =item B Get cached status of the service. Unlike other methods, it can be invoked by any user. =item B Execute the custom command C<$command> for the given service. =item B Get service object by name. =item B<< has_service($name) >> Check whether the service named C<$name> exists. =item B Get the list of all services. =item B Get the list of all service names. =item B Get the root multiservice object. Root service doesn't have a name and returns all top-level services with C method. You can use it to traverse the whole service tree. =item B Get the list of autocompletion variants for a given service prefix. =item B Write the new status into the service's status file. =item B<< get_data_dir() >> Get the data dir. =item B<< set_data_dir($dir) >> Set the data dir, creating it if necessary. Data dir is a directory with service statuses and locks. (See C for more details on how it's chosen). This setting will be propagated into subprocesses using environment, so the following code works: Ubic->set_data_dir('tfiles/ubic'); Ubic->set_service_dir('etc/ubic/service'); system('ubic start some_service'); system('ubic stop some_service'); =item B<< set_ubic_dir($dir) >> Deprecated. This method got renamed to C. =item B<< set_default_user($user) >> Set default user for all services. This is a simple proxy for C<< Ubic::Settings->default_user($user) >>. =item B<< get_service_dir() >> Get the ubic services dir. =item B<< set_service_dir($dir) >> Set the ubic services dir. =back =head1 INTERNAL METHODS You shouldn't call these from a code which doesn't belong to core Ubic distribution. These methods can be changed or removed without further notice. =over =item B Get the status file name by a service's name. =item B Get the status persistent object by a service's name. It's a bad idea to call this from any other class than C, but if you'll ever want to do this, at least don't forget to create C first. =item B Get the readonly, nonlocked status persistent object (see L) by a service's name. =item B Get an access guard (L object) for the given service. =item B Acquire lock object for given service. You can lock one object twice from the same process, but not from different processes. =item B<< do_sub($code) >> Run any code and wrap any result or exception into a result object. =item B<< do_cmd($name, $cmd) >> Run C<$cmd> method from the service named C<$name> and wrap any result or exception in a result object. =item B<< forked_call($callback) >> Run a C<$callback> in a subprocess and return its return value. Interaction happens through a temporary file in C<< $ubic->{tmp_dir} >> dir. =back =head1 CONTRIBUTORS Andrei Mishchenko Yury Zavarin Dmitry Yashin Christian Walde Ivan Bessarabov Oleg Komarov Andrew Kirkpatrick =head1 SEE ALSO Most Ubic-related links are collected on github wiki: L. L and L provide the start/stop/status style mechanisms for init scripts and apachectl-style commands. L is an apachectl-style, heavyweight subclassable module for handling network daemons. L - process supervisor, similar to Ubic in its command-line interface. There are also L, L and L. =head1 SUPPORT Our IRC channel is irc://irc.perl.org#ubic. There's also a mailing list at ubic-perl@googlegroups.com. Send an empty message to ubic-perl+subscribe@googlegroups.com to subscribe. =head1 AUTHOR Vyacheslav Matyukhin =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2016 by Yandex LLC. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut