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.59';
3 23     23   700259 use strict;
  23         49  
  23         653  
4 23     23   114 use warnings;
  23         28  
  23         663  
5              
6             # ABSTRACT: polymorphic service manager
7              
8              
9 23     23   10020 use POSIX qw();
  23         112584  
  23         529  
10 23     23   117 use Carp;
  23         45  
  23         1045  
11 23     23   8084 use IO::Handle;
  23         78907  
  23         1082  
12 23     23   5487 use Storable qw(freeze thaw);
  23         23389  
  23         1134  
13 23     23   101 use Try::Tiny;
  23         31  
  23         1092  
14 23     23   91 use Scalar::Util qw(blessed);
  23         32  
  23         1501  
15 23     23   2153 use Params::Validate qw(:all);
  23         22552  
  23         3819  
16              
17 23     23   6779 use Ubic::Result qw(result);
  23         37  
  23         1111  
18 23     23   6730 use Ubic::Multiservice::Dir;
  23         42  
  23         738  
19 23     23   6092 use Ubic::AccessGuard;
  23         54  
  23         706  
20 23     23   103 use Ubic::Credentials;
  23         23  
  23         80  
21 23     23   5874 use Ubic::Persistent;
  23         37  
  23         569  
22 23     23   98 use Ubic::AtomicFile;
  23         25  
  23         354  
23 23     23   5486 use Ubic::SingletonLock;
  23         39  
  23         524  
24 23     23   89 use Ubic::Settings;
  23         29  
  23         65829  
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   2276 my ($param) = validate_pos(@_, 1);
34 412 100       1589 if (blessed($param)) {
35 355         691 return $param;
36             }
37 57 50       210 if ($param eq 'Ubic') {
38             # method called as a class method => singleton
39 57   66     237 $SINGLETON ||= Ubic->new({});
40 57         180 return $SINGLETON;
41             }
42 0         0 die "Unknown argument '$param'";
43             }
44              
45             sub new {
46 9     9 1 21 my $class = shift;
47 9         196 my $options = validate(@_, {
48             service_dir => { type => SCALAR, optional => 1 },
49             data_dir => { type => SCALAR, optional => 1 },
50             });
51              
52 9 50       68 if (caller ne 'Ubic') {
53 0         0 warn "Using Ubic->new constructor is discouraged. Just call methods as class methods.";
54             }
55              
56 9         35 for my $key (qw/ service_dir data_dir /) {
57 18 50       61 Ubic::Settings->$key($options->{ $key }) if defined $options->{$key};
58             }
59              
60 9         78 Ubic::Settings->check_settings;
61              
62 9         13 my $self = {};
63 9         36 $self->{data_dir} = Ubic::Settings->data_dir;
64 9         36 $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         25 $self->{tmp_dir} = "$self->{data_dir}/tmp";
69              
70 9         30 $self->{service_cache} = {};
71 9         91 return bless $self => $class;
72             }
73              
74             sub start($$) {
75 10     10 1 5957 my $self = _obj(shift);
76 10         166 my ($name) = validate_pos(@_, $validate_service);
77 10         197 my $lock = $self->lock($name);
78              
79 10         50 $self->enable($name);
80 10         69 my $result = $self->do_cmd($name, 'start');
81 7         201 $self->set_cached_status($name, $result);
82 7         73 return $result;
83             }
84              
85             sub stop($$) {
86 4     4 1 24 my $self = _obj(shift);
87 4         49 my ($name) = validate_pos(@_, $validate_service);
88 4         71 my $lock = $self->lock($name);
89              
90 4         31 $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         49 my $result = $self->do_cmd($name, 'stop');
97 4         49 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 1096 my $self = _obj(shift);
161 2         29 my ($name) = validate_pos(@_, $validate_service);
162 2         41 my $lock = $self->lock($name);
163              
164 2         10 return $self->do_cmd($name, 'status');
165             }
166              
167             sub enable($$) {
168 11     11 1 30 my $self = _obj(shift);
169 11         118 my ($name) = validate_pos(@_, $validate_service);
170 11         165 my $lock = $self->lock($name);
171 11         28 my $guard = $self->access_guard($name);
172              
173 11         251 my $status_obj = $self->status_obj($name);
174 11         40 $status_obj->{status} = 'unknown';
175 11         25 $status_obj->{enabled} = 1;
176 11         35 $status_obj->commit;
177 11         140 return result('unknown');
178             }
179              
180             sub is_enabled($$) {
181 9     9 1 1184 my $self = _obj(shift);
182 9         151 my ($name) = validate_pos(@_, $validate_service);
183              
184 9 50       206 die "Service '$name' not found" unless $self->root_service->has_service($name);
185 9 100       38 unless (-e $self->status_file($name)) {
186 4         17 return $self->service($name)->auto_start();
187             }
188              
189 5         26 my $status_obj = $self->status_obj_ro($name);
190 5 100 66     49 if ($status_obj->{enabled} or not exists $status_obj->{enabled}) {
191 2         120 return 1;
192             }
193 3         25 return;
194             }
195              
196             sub disable($$) {
197 6     6 1 21 my $self = _obj(shift);
198 6         64 my ($name) = validate_pos(@_, $validate_service);
199 6         96 my $lock = $self->lock($name);
200 6         26 my $guard = $self->access_guard($name);
201              
202 6         31 my $status_obj = $self->status_obj($name);
203 6         13 delete $status_obj->{status};
204 6         14 $status_obj->{enabled} = 0;
205 6         21 $status_obj->commit;
206             }
207              
208              
209             sub cached_status($$) {
210 1     1 1 979 my ($self) = _obj(shift);
211 1         22 my ($name) = validate_pos(@_, $validate_service);
212              
213 1         15 my $type;
214 1 50       5 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         3 $type = 'autostarting';
221             }
222 1         14 return Ubic::Result::Class->new({ type => $type, cached => 1 });
223             }
224              
225             sub do_custom_command($$) {
226 2     2 1 2179 my ($self) = _obj(shift);
227 2         43 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         48 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   9 $self->service($name)->do_custom_command($command); # can custom commands require custom arguments?
236 2         19 });
237             }
238              
239             sub service($$) {
240 113     113 1 4295 my $self = _obj(shift);
241 113         962 my ($name) = validate_pos(@_, $validate_service);
242             # this guarantees that : will be unambiguous separator in status filename (what??)
243 113 100       1397 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         63 $self->{service_cache}{$name} = $self->root_service->service($name);
247             }
248 112         787 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 829 my $self = _obj(shift);
261 1         5 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 78 my $self = _obj(shift);
271 29 100       111 unless (defined $self->{root}) {
272 13         204 $self->{root} = Ubic::Multiservice::Dir->new($self->{service_dir}, { protected => 1 });
273             }
274 29         292 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 65 my $self = _obj(shift);
311 7         185 my ($name, $status) = validate_pos(@_, $validate_service, 1);
312 7         195 my $guard = $self->access_guard($name);
313              
314 7 50       42 if (blessed $status) {
315 7 50       60 croak "Wrong status param '$status'" unless $status->isa('Ubic::Result::Class');
316 7         21 $status = $status->status;
317             }
318 7         38 my $lock = $self->lock($name);
319              
320 7 50 33     43 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         35 my $status_obj = $self->status_obj($name);
326 7         43 $status_obj->{status} = $status;
327 7         23 $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 14425007 my ($arg, $dir) = validate_pos(@_, 1, 1);
338              
339             my $md = sub {
340 510     510   591 my $new_dir = shift;
341 510 50 50     21530 mkdir $new_dir or die "mkdir $new_dir failed: $!" unless -d $new_dir;
342 51         1008 };
343              
344 51         253 $md->($dir);
345             # FIXME - directory list is copy-pasted from Ubic::Admin::Setup
346 51         241 for my $subdir (qw[
347             status simple-daemon simple-daemon/pid lock ubic-daemon tmp watchdog watchdog/lock watchdog/status
348             ]) {
349 459         1319 $md->("$dir/$subdir");
350             }
351              
352 51         869 Ubic::Settings->data_dir($dir);
353 51 100       408 if ($SINGLETON) {
354 17         128 $SINGLETON->{lock_dir} = "$dir/lock";
355 17         80 $SINGLETON->{status_dir} = "$dir/status";
356 17         63 $SINGLETON->{tmp_dir} = "$dir/tmp";
357 17         128 $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 548 my ($arg, $user) = validate_pos(@_, 1, 1);
366              
367 51         273 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 870 my ($arg, $dir) = validate_pos(@_, 1, 1);
378 51         388 Ubic::Settings->service_dir($dir);
379 51 100       259 if ($SINGLETON) {
380 17         43 $SINGLETON->{service_dir} = $dir;
381 17         202 undef $SINGLETON->{root}; # force lazy regeneration
382             }
383             }
384              
385             sub status_file($$) {
386 53     53 1 132 my $self = _obj(shift);
387 53         447 my ($name) = validate_pos(@_, $validate_service);
388 53         1242 return "$self->{status_dir}/".$name;
389             }
390              
391             sub status_obj($$) {
392 24     24 1 56 my $self = _obj(shift);
393 24         223 my ($name) = validate_pos(@_, $validate_service);
394 24         316 return Ubic::Persistent->new($self->status_file($name));
395             }
396              
397             sub status_obj_ro($$) {
398 12     12 1 33 my $self = _obj(shift);
399 12         110 my ($name) = validate_pos(@_, $validate_service);
400 12         142 return Ubic::Persistent->load($self->status_file($name));
401             }
402              
403             sub access_guard($$) {
404 76     76 1 138 my $self = _obj(shift);
405 76         551 my ($name) = validate_pos(@_, $validate_service);
406 76         748 return Ubic::AccessGuard->new(
407             Ubic::Credentials->new(service => $self->service($name))
408             );
409             }
410              
411             sub lock($$) {
412 52     52 1 104081 my ($self) = _obj(shift);
413 52         487 my ($name) = validate_pos(@_, $validate_service);
414              
415 52         555 my $lock = do {
416 52         151 my $guard = $self->access_guard($name);
417 52         474 Ubic::SingletonLock->new($self->{lock_dir}."/".$name);
418             };
419 52         147 return $lock;
420             }
421              
422             sub do_sub($$) {
423 18     18 1 33 my ($self, $code) = @_;
424             my $result = try {
425 18     18   787 $code->();
426             } catch {
427 1     1   15 die result($_);
428 18         156 };
429 15         758 return result($result);
430             }
431              
432             sub do_cmd($$$) {
433 16     16 1 71 my ($self, $name, $cmd) = @_;
434             $self->do_sub(sub {
435 16     16   58 my $service = $self->service($name);
436              
437 16         143 my $creds = Ubic::Credentials->new( service => $service );
438              
439 16 50       51 if ($creds->eq(Ubic::Credentials->new)) {
440             # current credentials fit service expectations
441 16         211 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         178 });
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__