File Coverage

lib/Ubic.pm
Criterion Covered Total %
statement 196 297 65.9
branch 25 68 36.7
condition 6 11 54.5
subroutine 46 60 76.6
pod 33 33 100.0
total 306 469 65.2


line stmt bran cond sub pod time code
1             package Ubic;
2             $Ubic::VERSION = '1.60';
3 23     23   643557 use strict;
  23         34  
  23         539  
4 23     23   87 use warnings;
  23         32  
  23         517  
5              
6             # ABSTRACT: polymorphic service manager
7              
8              
9 23     23   9294 use POSIX qw();
  23         102448  
  23         455  
10 23     23   102 use Carp;
  23         35  
  23         919  
11 23     23   7491 use IO::Handle;
  23         74973  
  23         831  
12 23     23   5148 use Storable qw(freeze thaw);
  23         21292  
  23         1032  
13 23     23   94 use Try::Tiny;
  23         23  
  23         952  
14 23     23   88 use Scalar::Util qw(blessed);
  23         21  
  23         1349  
15 23     23   2363 use Params::Validate qw(:all);
  23         24599  
  23         3136  
16              
17 23     23   6094 use Ubic::Result qw(result);
  23         35  
  23         991  
18 23     23   6233 use Ubic::Multiservice::Dir;
  23         35  
  23         590  
19 23     23   5280 use Ubic::AccessGuard;
  23         38  
  23         558  
20 23     23   92 use Ubic::Credentials;
  23         23  
  23         67  
21 23     23   5240 use Ubic::Persistent;
  23         30  
  23         487  
22 23     23   89 use Ubic::AtomicFile;
  23         18  
  23         327  
23 23     23   5285 use Ubic::SingletonLock;
  23         37  
  23         452  
24 23     23   85 use Ubic::Settings;
  23         21  
  23         61513  
25              
26             our $SINGLETON;
27              
28             my $service_name_re = qr{^[\w-]+(?:\.[\w-]+)*$};
29             my $validate_service = { type => SCALAR, regex => $service_name_re };
30              
31             # singleton constructor
32             sub _obj {
33 412     412   2115 my ($param) = validate_pos(@_, 1);
34 412 100       1466 if (blessed($param)) {
35 355         654 return $param;
36             }
37 57 50       191 if ($param eq 'Ubic') {
38             # method called as a class method => singleton
39 57   66     191 $SINGLETON ||= Ubic->new({});
40 57         189 return $SINGLETON;
41             }
42 0         0 die "Unknown argument '$param'";
43             }
44              
45             sub new {
46 9     9 1 13 my $class = shift;
47 9         150 my $options = validate(@_, {
48             service_dir => { type => SCALAR, optional => 1 },
49             data_dir => { type => SCALAR, optional => 1 },
50             });
51              
52 9 50       46 if (caller ne 'Ubic') {
53 0         0 warn "Using Ubic->new constructor is discouraged. Just call methods as class methods.";
54             }
55              
56 9         24 for my $key (qw/ service_dir data_dir /) {
57 18 50       48 Ubic::Settings->$key($options->{ $key }) if defined $options->{$key};
58             }
59              
60 9         42 Ubic::Settings->check_settings;
61              
62 9         14 my $self = {};
63 9         25 $self->{data_dir} = Ubic::Settings->data_dir;
64 9         21 $self->{service_dir} = Ubic::Settings->service_dir;
65              
66 9         32 $self->{status_dir} = "$self->{data_dir}/status";
67 9         32 $self->{lock_dir} = "$self->{data_dir}/lock";
68 9         18 $self->{tmp_dir} = "$self->{data_dir}/tmp";
69              
70 9         27 $self->{service_cache} = {};
71 9         80 return bless $self => $class;
72             }
73              
74             sub start($$) {
75 10     10 1 5961 my $self = _obj(shift);
76 10         149 my ($name) = validate_pos(@_, $validate_service);
77 10         207 my $lock = $self->lock($name);
78              
79 10         55 $self->enable($name);
80 10         60 my $result = $self->do_cmd($name, 'start');
81 7         152 $self->set_cached_status($name, $result);
82 7         76 return $result;
83             }
84              
85             sub stop($$) {
86 4     4 1 13 my $self = _obj(shift);
87 4         39 my ($name) = validate_pos(@_, $validate_service);
88 4         60 my $lock = $self->lock($name);
89              
90 4         18 $self->disable($name);
91              
92             # FIXME - 'stop' command can fail, in this case daemon will keep running.
93             # This is bad.
94             # We probably need to implement the same logic as when starting:
95             # retry stop attempts until actual status matches desired status.
96 4         26 my $result = $self->do_cmd($name, 'stop');
97 4         47 return $result;
98             }
99              
100             sub restart($$) {
101 0     0 1 0 my $self = _obj(shift);
102 0         0 my ($name) = validate_pos(@_, $validate_service);
103 0         0 my $lock = $self->lock($name);
104              
105 0         0 $self->enable($name);
106 0         0 my $result = $self->do_cmd($name, 'stop');
107 0         0 $result = $self->do_cmd($name, 'start');
108              
109 0         0 $self->set_cached_status($name, $result);
110 0         0 return result('restarted'); # FIXME - should return original status
111             }
112              
113             sub try_restart($$) {
114 0     0 1 0 my $self = _obj(shift);
115 0         0 my ($name) = validate_pos(@_, $validate_service);
116 0         0 my $lock = $self->lock($name);
117              
118 0 0       0 unless ($self->is_enabled($name)) {
119 0         0 return result('down');
120             }
121 0         0 $self->do_cmd($name, 'stop');
122 0         0 $self->do_cmd($name, 'start');
123 0         0 return result('restarted');
124             }
125              
126             sub reload($$) {
127 0     0 1 0 my $self = _obj(shift);
128 0         0 my ($name) = validate_pos(@_, $validate_service);
129 0         0 my $lock = $self->lock($name);
130              
131 0 0       0 unless ($self->is_enabled($name)) {
132 0         0 return result('down');
133             }
134              
135             # if reload isn't implemented, do nothing
136             # TODO - would it be better to execute reload as force-reload always? but it would be incompatible with LSB specification...
137 0         0 my $result = $self->do_cmd($name, 'reload');
138 0 0       0 unless ($result->action eq 'reloaded') {
139 0         0 die $result;
140             }
141 0         0 return $result;
142             }
143              
144             sub force_reload($$) {
145 0     0 1 0 my $self = _obj(shift);
146 0         0 my ($name) = validate_pos(@_, $validate_service);
147 0         0 my $lock = $self->lock($name);
148              
149 0 0       0 unless ($self->is_enabled($name)) {
150 0         0 return result('down');
151             }
152              
153 0         0 my $result = $self->do_cmd($name, 'reload');
154 0 0       0 return $result if $result->action eq 'reloaded';
155              
156 0         0 $self->try_restart($name);
157             }
158              
159             sub status($$) {
160 2     2 1 1003 my $self = _obj(shift);
161 2         23 my ($name) = validate_pos(@_, $validate_service);
162 2         32 my $lock = $self->lock($name);
163              
164 2         5 return $self->do_cmd($name, 'status');
165             }
166              
167             sub enable($$) {
168 11     11 1 29 my $self = _obj(shift);
169 11         100 my ($name) = validate_pos(@_, $validate_service);
170 11         138 my $lock = $self->lock($name);
171 11         23 my $guard = $self->access_guard($name);
172              
173 11         138 my $status_obj = $self->status_obj($name);
174 11         41 $status_obj->{status} = 'unknown';
175 11         27 $status_obj->{enabled} = 1;
176 11         34 $status_obj->commit;
177 11         138 return result('unknown');
178             }
179              
180             sub is_enabled($$) {
181 9     9 1 1192 my $self = _obj(shift);
182 9         123 my ($name) = validate_pos(@_, $validate_service);
183              
184 9 50       163 die "Service '$name' not found" unless $self->root_service->has_service($name);
185 9 100       40 unless (-e $self->status_file($name)) {
186 4         15 return $self->service($name)->auto_start();
187             }
188              
189 5         19 my $status_obj = $self->status_obj_ro($name);
190 5 100 66     40 if ($status_obj->{enabled} or not exists $status_obj->{enabled}) {
191 2         88 return 1;
192             }
193 3         21 return;
194             }
195              
196             sub disable($$) {
197 6     6 1 20 my $self = _obj(shift);
198 6         62 my ($name) = validate_pos(@_, $validate_service);
199 6         81 my $lock = $self->lock($name);
200 6         19 my $guard = $self->access_guard($name);
201              
202 6         22 my $status_obj = $self->status_obj($name);
203 6         12 delete $status_obj->{status};
204 6         13 $status_obj->{enabled} = 0;
205 6         21 $status_obj->commit;
206             }
207              
208              
209             sub cached_status($$) {
210 1     1 1 1164 my ($self) = _obj(shift);
211 1         24 my ($name) = validate_pos(@_, $validate_service);
212              
213 1         17 my $type;
214 1 50       6 if (not $self->is_enabled($name)) {
    50          
215 0         0 $type = 'disabled';
216             }
217             elsif (-e $self->status_file($name)) {
218 0         0 $type = $self->status_obj_ro($name)->{status};
219             } else {
220 1         4 $type = 'autostarting';
221             }
222 1         16 return Ubic::Result::Class->new({ type => $type, cached => 1 });
223             }
224              
225             sub do_custom_command($$) {
226 2     2 1 1455 my ($self) = _obj(shift);
227 2         22 my ($name, $command) = validate_pos(@_, $validate_service, 1);
228              
229             # TODO - do all custom commands require locks?
230             # they can be distinguished in future by some custom_commands_ext method which will provide hash { command => properties }, i think...
231 2         29 my $lock = $self->lock($name);
232              
233             # TODO - check custom_command presence by custom_commands() method first?
234             $self->do_sub(sub {
235 2     2   6 $self->service($name)->do_custom_command($command); # can custom commands require custom arguments?
236 2         14 });
237             }
238              
239             sub service($$) {
240 113     113 1 2951 my $self = _obj(shift);
241 113         859 my ($name) = validate_pos(@_, $validate_service);
242             # this guarantees that : will be unambiguous separator in status filename (what??)
243 113 100       1282 unless ($self->{service_cache}{$name}) {
244             # Service construction is a memory-leaking operation (because of package name randomization in Ubic::Multiservice::Dir),
245             # so we need to cache each service which we create.
246 18         46 $self->{service_cache}{$name} = $self->root_service->service($name);
247             }
248 112         760 return $self->{service_cache}{$name};
249             }
250              
251             sub has_service($$) {
252 0     0 1 0 my $self = _obj(shift);
253 0         0 my ($name) = validate_pos(@_, $validate_service);
254             # TODO - it would be safer to do this check without actual service construction
255             # but it would require cron-based script which maintains list of all services
256 0         0 return $self->root_service->has_service($name);
257             }
258              
259             sub services($) {
260 1     1 1 763 my $self = _obj(shift);
261 1         4 return $self->root_service->services();
262             }
263              
264             sub service_names($) {
265 0     0 1 0 my $self = _obj(shift);
266 0         0 return $self->root_service->service_names();
267             }
268              
269             sub root_service($) {
270 29     29 1 62 my $self = _obj(shift);
271 29 100       91 unless (defined $self->{root}) {
272 13         211 $self->{root} = Ubic::Multiservice::Dir->new($self->{service_dir}, { protected => 1 });
273             }
274 29         294 return $self->{root};
275             }
276              
277             sub compl_services($$) {
278 0     0 1 0 my $self = _obj(shift);
279 0         0 my $line = shift;
280 0         0 my @parts = split /\./, $line;
281 0 0       0 if ($line =~ /\.$/) {
282 0         0 push @parts, '';
283             }
284 0 0       0 if (@parts == 0) {
285 0         0 return $self->service_names;
286             }
287 0         0 my $node = $self->root_service;
288 0         0 my $is_subservice = (@parts > 1);
289 0         0 while (@parts > 1) {
290 0 0       0 unless ($node->isa('Ubic::Multiservice')) {
291 0         0 return;
292             }
293 0         0 my $part = shift @parts;
294 0 0       0 return unless $node->has_service($part); # no such service
295 0         0 $node = $node->service($part);
296             }
297              
298 0         0 my @variants = $node->service_names;
299             return
300             map {
301 0 0       0 ( $is_subservice ? $node->full_name.".".$_ : $_ )
302             }
303             grep {
304 0         0 $_ =~ m{^\Q$parts[0]\E}
  0         0  
305             }
306             @variants;
307             }
308              
309             sub set_cached_status($$$) {
310 7     7 1 77 my $self = _obj(shift);
311 7         151 my ($name, $status) = validate_pos(@_, $validate_service, 1);
312 7         169 my $guard = $self->access_guard($name);
313              
314 7 50       31 if (blessed $status) {
315 7 50       37 croak "Wrong status param '$status'" unless $status->isa('Ubic::Result::Class');
316 7         29 $status = $status->status;
317             }
318 7         30 my $lock = $self->lock($name);
319              
320 7 50 33     40 if (-e $self->status_file($name) and $self->status_obj_ro($name)->{status} eq $status) {
321             # optimization - don't update status if nothing changed
322 0         0 return;
323             }
324              
325 7         26 my $status_obj = $self->status_obj($name);
326 7         16 $status_obj->{status} = $status;
327 7         30 $status_obj->commit;
328             }
329              
330             sub get_data_dir($) {
331 0     0 1 0 my $self = _obj(shift);
332 0         0 validate_pos(@_);
333 0         0 return $self->{data_dir};
334             }
335              
336             sub set_data_dir($$) {
337 51     51 1 14093290 my ($arg, $dir) = validate_pos(@_, 1, 1);
338              
339             my $md = sub {
340 510     510   550 my $new_dir = shift;
341 510 50 50     23212 mkdir $new_dir or die "mkdir $new_dir failed: $!" unless -d $new_dir;
342 51         750 };
343              
344 51         228 $md->($dir);
345             # FIXME - directory list is copy-pasted from Ubic::Admin::Setup
346 51         257 for my $subdir (qw[
347             status simple-daemon simple-daemon/pid lock ubic-daemon tmp watchdog watchdog/lock watchdog/status
348             ]) {
349 459         1364 $md->("$dir/$subdir");
350             }
351              
352 51         913 Ubic::Settings->data_dir($dir);
353 51 100       372 if ($SINGLETON) {
354 17         137 $SINGLETON->{lock_dir} = "$dir/lock";
355 17         93 $SINGLETON->{status_dir} = "$dir/status";
356 17         108 $SINGLETON->{tmp_dir} = "$dir/tmp";
357 17         170 $SINGLETON->{data_dir} = $dir;
358             }
359             }
360              
361             sub set_ubic_dir($$);
362             *set_ubic_dir = \&set_data_dir;
363              
364             sub set_default_user($$) {
365 51     51 1 489 my ($arg, $user) = validate_pos(@_, 1, 1);
366              
367 51         257 Ubic::Settings->default_user($user);
368             }
369              
370             sub get_service_dir($) {
371 0     0 1 0 my $self = _obj(shift);
372 0         0 validate_pos(@_);
373 0         0 return $self->{service_dir};
374             }
375              
376             sub set_service_dir($$) {
377 51     51 1 755 my ($arg, $dir) = validate_pos(@_, 1, 1);
378 51         294 Ubic::Settings->service_dir($dir);
379 51 100       239 if ($SINGLETON) {
380 17         50 $SINGLETON->{service_dir} = $dir;
381 17         263 undef $SINGLETON->{root}; # force lazy regeneration
382             }
383             }
384              
385             sub status_file($$) {
386 53     53 1 86 my $self = _obj(shift);
387 53         377 my ($name) = validate_pos(@_, $validate_service);
388 53         1149 return "$self->{status_dir}/".$name;
389             }
390              
391             sub status_obj($$) {
392 24     24 1 50 my $self = _obj(shift);
393 24         202 my ($name) = validate_pos(@_, $validate_service);
394 24         275 return Ubic::Persistent->new($self->status_file($name));
395             }
396              
397             sub status_obj_ro($$) {
398 12     12 1 67 my $self = _obj(shift);
399 12         105 my ($name) = validate_pos(@_, $validate_service);
400 12         124 return Ubic::Persistent->load($self->status_file($name));
401             }
402              
403             sub access_guard($$) {
404 76     76 1 117 my $self = _obj(shift);
405 76         509 my ($name) = validate_pos(@_, $validate_service);
406 76         719 return Ubic::AccessGuard->new(
407             Ubic::Credentials->new(service => $self->service($name))
408             );
409             }
410              
411             sub lock($$) {
412 52     52 1 103886 my ($self) = _obj(shift);
413 52         455 my ($name) = validate_pos(@_, $validate_service);
414              
415 52         583 my $lock = do {
416 52         159 my $guard = $self->access_guard($name);
417 52         416 Ubic::SingletonLock->new($self->{lock_dir}."/".$name);
418             };
419 52         140 return $lock;
420             }
421              
422             sub do_sub($$) {
423 18     18 1 24 my ($self, $code) = @_;
424             my $result = try {
425 18     18   631 $code->();
426             } catch {
427 1     1   12 die result($_);
428 18         143 };
429 15         670 return result($result);
430             }
431              
432             sub do_cmd($$$) {
433 16     16 1 67 my ($self, $name, $cmd) = @_;
434             $self->do_sub(sub {
435 16     16   48 my $service = $self->service($name);
436              
437 16         107 my $creds = Ubic::Credentials->new( service => $service );
438              
439 16 50       54 if ($creds->eq(Ubic::Credentials->new)) {
440             # current credentials fit service expectations
441 16         214 return $service->$cmd();
442             }
443              
444             # setting just effective uid is not enough, because:
445             # - we can accidentally enter tainted mode, and service authors don't expect this
446             # - local administrator may want to allow everyone to write their own services, and leaving root as real uid is an obvious security breach
447             # (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)
448             $self->forked_call(sub {
449 0           $creds->set();
450 0           return $service->$cmd();
451 0           });
452 16         143 });
453             }
454              
455             sub forked_call {
456 0     0 1   my ($self, $callback) = @_;
457 0           my $tmp_file = $self->{tmp_dir}."/".time.".$$.".rand(1000000);
458 0           my $child;
459 0 0         unless ($child = fork) {
460 0 0         unless (defined $child) {
461 0           die "fork failed";
462             }
463 0           my $result;
464             try {
465 0     0     $result = { ok => $callback->() };
466             }
467             catch {
468 0     0     $result = { error => $_ };
469 0           };
470              
471             try {
472 0     0     Ubic::AtomicFile::store( freeze($result) => $tmp_file );
473 0           STDOUT->flush;
474 0           STDERR->flush;
475 0           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
476             }
477             catch {
478             # probably tmp_file is not writable
479 0     0     warn $_;
480 0           POSIX::_exit(1);
481 0           };
482             }
483 0           waitpid($child, 0);
484 0 0         unless (-e $tmp_file) {
485 0           die "temp file $tmp_file not found after fork";
486             }
487 0 0         open my $fh, '<', $tmp_file or die "Can't read $tmp_file: $!";
488 0           my $content = do { local $/; <$fh>; };
  0            
  0            
489 0 0         close $fh or die "Can't close $tmp_file: $!";
490 0           unlink $tmp_file;
491 0           my $result = thaw($content);
492 0 0         if ($result->{error}) {
493 0           die $result->{error};
494             }
495             else {
496 0           return $result->{ok};
497             }
498             }
499              
500              
501             1;
502              
503             __END__