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.58_01'; # TRIAL
3 25     25   2860245 use strict;
  25         53  
  25         866  
4 25     25   190 use warnings;
  25         43  
  25         814  
5              
6             # ABSTRACT: polymorphic service manager
7              
8              
9 25     25   15033 use POSIX qw();
  25         176696  
  25         807  
10 25     25   219 use Carp;
  25         90  
  25         1563  
11 25     25   11735 use IO::Handle;
  25         114767  
  25         1455  
12 25     25   6974 use Storable qw(freeze thaw);
  25         30272  
  25         1738  
13 25     25   158 use Try::Tiny;
  25         35  
  25         1597  
14 25     25   127 use Scalar::Util qw(blessed);
  25         41  
  25         2271  
15 25     25   2339 use Params::Validate qw(:all);
  25         24545  
  25         5456  
16              
17 25     25   11066 use Ubic::Result qw(result);
  25         64  
  25         1819  
18 25     25   10687 use Ubic::Multiservice::Dir;
  25         71  
  25         1196  
19 25     25   10113 use Ubic::AccessGuard;
  25         61  
  25         980  
20 25     25   140 use Ubic::Credentials;
  25         35  
  25         125  
21 25     25   9206 use Ubic::Persistent;
  25         58  
  25         833  
22 25     25   189 use Ubic::AtomicFile;
  25         32  
  25         491  
23 25     25   7809 use Ubic::SingletonLock;
  25         50  
  25         772  
24 25     25   145 use Ubic::Settings;
  25         46  
  25         104652  
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 474     474   3577 my ($param) = validate_pos(@_, 1);
34 474 100       2185 if (blessed($param)) {
35 405         985 return $param;
36             }
37 69 50       269 if ($param eq 'Ubic') {
38             # method called as a class method => singleton
39 69   66     402 $SINGLETON ||= Ubic->new({});
40 69         257 return $SINGLETON;
41             }
42 0         0 die "Unknown argument '$param'";
43             }
44              
45             sub new {
46 10     10 1 23 my $class = shift;
47 10         254 my $options = validate(@_, {
48             service_dir => { type => SCALAR, optional => 1 },
49             data_dir => { type => SCALAR, optional => 1 },
50             });
51              
52 10 50       85 if (caller ne 'Ubic') {
53 0         0 warn "Using Ubic->new constructor is discouraged. Just call methods as class methods.";
54             }
55              
56 10         57 for my $key (qw/ service_dir data_dir /) {
57 20 50       70 Ubic::Settings->$key($options->{ $key }) if defined $options->{$key};
58             }
59              
60 10         99 Ubic::Settings->check_settings;
61              
62 10         15 my $self = {};
63 10         54 $self->{data_dir} = Ubic::Settings->data_dir;
64 10         43 $self->{service_dir} = Ubic::Settings->service_dir;
65              
66 10         47 $self->{status_dir} = "$self->{data_dir}/status";
67 10         43 $self->{lock_dir} = "$self->{data_dir}/lock";
68 10         37 $self->{tmp_dir} = "$self->{data_dir}/tmp";
69              
70 10         39 $self->{service_cache} = {};
71 10         131 return bless $self => $class;
72             }
73              
74             sub start($$) {
75 11     11 1 8960 my $self = _obj(shift);
76 11         214 my ($name) = validate_pos(@_, $validate_service);
77 11         271 my $lock = $self->lock($name);
78              
79 11         67 $self->enable($name);
80 11         111 my $result = $self->do_cmd($name, 'start');
81 7         226 $self->set_cached_status($name, $result);
82 7         100 return $result;
83             }
84              
85             sub stop($$) {
86 4     4 1 18 my $self = _obj(shift);
87 4         56 my ($name) = validate_pos(@_, $validate_service);
88 4         83 my $lock = $self->lock($name);
89              
90 4         33 $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         57 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 1166 my $self = _obj(shift);
161 2         42 my ($name) = validate_pos(@_, $validate_service);
162 2         62 my $lock = $self->lock($name);
163              
164 2         21 return $self->do_cmd($name, 'status');
165             }
166              
167             sub enable($$) {
168 13     13 1 46 my $self = _obj(shift);
169 13         180 my ($name) = validate_pos(@_, $validate_service);
170 13         250 my $lock = $self->lock($name);
171 13         38 my $guard = $self->access_guard($name);
172              
173 13         537 my $status_obj = $self->status_obj($name);
174 13         70 $status_obj->{status} = 'unknown';
175 13         41 $status_obj->{enabled} = 1;
176 13         60 $status_obj->commit;
177 13         187 return result('unknown');
178             }
179              
180             sub is_enabled($$) {
181 13     13 1 3222 my $self = _obj(shift);
182 13         219 my ($name) = validate_pos(@_, $validate_service);
183              
184 13 50       357 die "Service '$name' not found" unless $self->root_service->has_service($name);
185 13 100       70 unless (-e $self->status_file($name)) {
186 6         47 return $self->service($name)->auto_start();
187             }
188              
189 7         36 my $status_obj = $self->status_obj_ro($name);
190 7 100 66     57 if ($status_obj->{enabled} or not exists $status_obj->{enabled}) {
191 3         192 return 1;
192             }
193 4         39 return;
194             }
195              
196             sub disable($$) {
197 7     7 1 25 my $self = _obj(shift);
198 7         125 my ($name) = validate_pos(@_, $validate_service);
199 7         152 my $lock = $self->lock($name);
200 7         24 my $guard = $self->access_guard($name);
201              
202 7         37 my $status_obj = $self->status_obj($name);
203 7         20 delete $status_obj->{status};
204 7         17 $status_obj->{enabled} = 0;
205 7         38 $status_obj->commit;
206             }
207              
208              
209             sub cached_status($$) {
210 1     1 1 1115 my ($self) = _obj(shift);
211 1         23 my ($name) = validate_pos(@_, $validate_service);
212              
213 1         19 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         13 return Ubic::Result::Class->new({ type => $type, cached => 1 });
223             }
224              
225             sub do_custom_command($$) {
226 3     3 1 5395 my ($self) = _obj(shift);
227 3         63 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 3         76 my $lock = $self->lock($name);
232              
233             # TODO - check custom_command presence by custom_commands() method first?
234             $self->do_sub(sub {
235 3     3   27 $self->service($name)->do_custom_command($command); # can custom commands require custom arguments?
236 3         47 });
237             }
238              
239             sub service($$) {
240 129     129 1 11229 my $self = _obj(shift);
241 129         1487 my ($name) = validate_pos(@_, $validate_service);
242             # this guarantees that : will be unambiguous separator in status filename (what??)
243 129 100       2177 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 24         99 $self->{service_cache}{$name} = $self->root_service->service($name);
247             }
248 127         1265 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 1217 my $self = _obj(shift);
261 1         7 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 39     39 1 116 my $self = _obj(shift);
271 39 100       160 unless (defined $self->{root}) {
272 14         261 $self->{root} = Ubic::Multiservice::Dir->new($self->{service_dir}, { protected => 1 });
273             }
274 39         452 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 66 my $self = _obj(shift);
311 7         258 my ($name, $status) = validate_pos(@_, $validate_service, 1);
312 7         229 my $guard = $self->access_guard($name);
313              
314 7 50       56 if (blessed $status) {
315 7 50       54 croak "Wrong status param '$status'" unless $status->isa('Ubic::Result::Class');
316 7         27 $status = $status->status;
317             }
318 7         38 my $lock = $self->lock($name);
319              
320 7 50 33     46 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         54 my $status_obj = $self->status_obj($name);
326 7         23 $status_obj->{status} = $status;
327 7         45 $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 60     60 1 64673434 my ($arg, $dir) = validate_pos(@_, 1, 1);
338              
339             my $md = sub {
340 600     600   922 my $new_dir = shift;
341 600 50 50     2125439 mkdir $new_dir or die "mkdir $new_dir failed: $!" unless -d $new_dir;
342 60         1159 };
343              
344 60         401 $md->($dir);
345             # FIXME - directory list is copy-pasted from Ubic::Admin::Setup
346 60         340 for my $subdir (qw[
347             status simple-daemon simple-daemon/pid lock ubic-daemon tmp watchdog watchdog/lock watchdog/status
348             ]) {
349 540         2456 $md->("$dir/$subdir");
350             }
351              
352 60         1548 Ubic::Settings->data_dir($dir);
353 60 100       596 if ($SINGLETON) {
354 17         155 $SINGLETON->{lock_dir} = "$dir/lock";
355 17         85 $SINGLETON->{status_dir} = "$dir/status";
356 17         79 $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 60     60 1 888 my ($arg, $user) = validate_pos(@_, 1, 1);
366              
367 60         348 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 60     60 1 1825 my ($arg, $dir) = validate_pos(@_, 1, 1);
378 60         475 Ubic::Settings->service_dir($dir);
379 60 100       371 if ($SINGLETON) {
380 17         53 $SINGLETON->{service_dir} = $dir;
381 17         263 undef $SINGLETON->{root}; # force lazy regeneration
382             }
383             }
384              
385             sub status_file($$) {
386 62     62 1 149 my $self = _obj(shift);
387 62         647 my ($name) = validate_pos(@_, $validate_service);
388 62         1876 return "$self->{status_dir}/".$name;
389             }
390              
391             sub status_obj($$) {
392 27     27 1 84 my $self = _obj(shift);
393 27         329 my ($name) = validate_pos(@_, $validate_service);
394 27         432 return Ubic::Persistent->new($self->status_file($name));
395             }
396              
397             sub status_obj_ro($$) {
398 14     14 1 49 my $self = _obj(shift);
399 14         179 my ($name) = validate_pos(@_, $validate_service);
400 14         202 return Ubic::Persistent->load($self->status_file($name));
401             }
402              
403             sub access_guard($$) {
404 84     84 1 191 my $self = _obj(shift);
405 84         808 my ($name) = validate_pos(@_, $validate_service);
406 84         1134 return Ubic::AccessGuard->new(
407             Ubic::Credentials->new(service => $self->service($name))
408             );
409             }
410              
411             sub lock($$) {
412 57     57 1 104261 my ($self) = _obj(shift);
413 57         672 my ($name) = validate_pos(@_, $validate_service);
414              
415 57         765 my $lock = do {
416 57         250 my $guard = $self->access_guard($name);
417 57         685 Ubic::SingletonLock->new($self->{lock_dir}."/".$name);
418             };
419 57         187 return $lock;
420             }
421              
422             sub do_sub($$) {
423 20     20 1 47 my ($self, $code) = @_;
424             my $result = try {
425 20     20   1023 $code->();
426             } catch {
427 1     1   17 die result($_);
428 20         217 };
429 16         1005 return result($result);
430             }
431              
432             sub do_cmd($$$) {
433 17     17 1 90 my ($self, $name, $cmd) = @_;
434             $self->do_sub(sub {
435 17     17   79 my $service = $self->service($name);
436              
437 17         171 my $creds = Ubic::Credentials->new( service => $service );
438              
439 17 50       70 if ($creds->eq(Ubic::Credentials->new)) {
440             # current credentials fit service expectations
441 17         312 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 17         272 });
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__