File Coverage

blib/lib/Doit.pm
Criterion Covered Total %
statement 937 1330 70.4
branch 462 732 63.1
condition 101 179 56.4
subroutine 145 178 81.4
pod 4 46 8.7
total 1649 2465 66.9


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             # -*- perl -*-
3              
4             #
5             # Author: Slaven Rezic
6             #
7             # Copyright (C) 2017,2018,2019,2020,2022 Slaven Rezic. All rights reserved.
8             # This program is free software; you can redistribute it and/or
9             # modify it under the same terms as Perl itself.
10             #
11             # Mail: slaven@rezic.de
12             # WWW: http://www.rezic.de/eserte/
13             #
14              
15 43     43   2274400 use strict;
  43         401  
  43         1083  
16 43     43   183 use warnings;
  43         82  
  43         3107  
17              
18             {
19             package Doit;
20             our $VERSION = '0.025_56';
21             $VERSION =~ s{_}{};
22              
23 43     43   245 use constant IS_WIN => $^O eq 'MSWin32';
  43         75  
  43         4702  
24             }
25              
26             {
27             package Doit::Log;
28              
29             sub _use_coloring {
30 43     43   264 no warnings 'redefine';
  43         77  
  43         5648  
31 95     95   410 *colored_error = sub ($) { Term::ANSIColor::colored($_[0], 'red on_black')};
  43     43   262  
32 43     420   1498 *colored_info = sub ($) { Term::ANSIColor::colored($_[0], 'green on_black')};
  420         3854  
33             }
34             sub _no_coloring {
35 43     43   276 no warnings 'redefine';
  43         91  
  43         7448  
36 1     1   31 *colored_error = *colored_info = sub ($) { $_[0] };
  1     1   1240  
37             }
38             {
39             my $can_coloring;
40             sub _can_coloring {
41 45 100   45   84189 return $can_coloring if defined $can_coloring;
42             # XXX What needs to be done to get coloring on Windows?
43             # XXX Probably should also check if the terminal is ANSI-capable at all
44             # XXX Probably should not use coloring on non-terminals (but
45             # there could be a --color option like in git to force it)
46 43 50 33     492 $can_coloring = !Doit::IS_WIN && ($ENV{TERM}||'') !~ m{^(|dumb)$} && eval { require Term::ANSIColor; 1 } ? 1 : 0;
47             }
48             }
49              
50             BEGIN {
51 43 50   43   208 if (_can_coloring()) {
52 43         137 _use_coloring();
53             } else {
54 0         0 _no_coloring();
55             }
56             }
57              
58 43     43   292 use Exporter 'import';
  43         59  
  43         1955  
59 43     43   1030 our @EXPORT; BEGIN { @EXPORT = qw(info warning error) }
60              
61 43     43   9675 BEGIN { $INC{'Doit/Log.pm'} = __FILE__ } # XXX hack
62              
63             my $current_label = '';
64              
65 421     421 1 8731 sub info ($) { print STDERR colored_info("INFO$current_label:"), " ", $_[0], "\n" }
66 3     3 1 1485 sub warning ($) { print STDERR colored_error("WARN$current_label:"), " ", $_[0], "\n" }
67 92     92 1 3174 sub error ($) { require Carp; Carp::croak(colored_error("ERROR$current_label:"), " ", $_[0]) }
  92         494  
68              
69             sub set_label ($) {
70 2     2 1 2028 my $label = shift;
71 2 100       6 if (defined $label) {
72 1         3 $current_label = " $label";
73             } else {
74 1         3 $current_label = '';
75             }
76             }
77             }
78              
79             {
80             package Doit::Exception;
81 43     43   34325 use overload '""' => 'stringify';
  43         27562  
  43         227  
82 43     43   2721 use Exporter 'import';
  43         83  
  43         14524  
83             our @EXPORT_OK = qw(throw);
84             $INC{'Doit/Exception.pm'} = __FILE__; # XXX hack
85              
86             sub new {
87 54     54 0 82110 my($class, $msg, %opts) = @_;
88 54   50     629 my $level = delete $opts{__level} || 'auto';
89 54 50       205 if ($level eq 'auto') {
90 54         142 my $_level = 0;
91 54         76 while() {
92 310         3750 my @stackinfo = caller($_level);
93 310 50       803 if (!@stackinfo) {
94 0         0 $level = $_level - 1;
95 0         0 last;
96             }
97 310 100       1640 if ($stackinfo[1] !~ m{([/\\]|^)Doit\.pm$}) {
98 54         100 $level = $_level;
99 54         168 last;
100             }
101 256         507 $_level++;
102             }
103             }
104 54         514 ($opts{__package}, $opts{__filename}, $opts{__line}) = caller($level);
105 54         3551 bless {
106             __msg => $msg,
107             %opts,
108             }, $class;
109             }
110             sub stringify {
111 31     31 0 14975 my $self = shift;
112 31         153 my $msg = $self->{__msg};
113 31 100       137 $msg = 'Died' if !defined $msg;
114 31 100       207 if ($msg !~ /\n\z/) {
115 29         167 $msg .= ' at ' . $self->{__filename} . ' line ' . $self->{__line} . ".\n";
116             }
117 31         407 $msg;
118             }
119              
120 50     50 0 2650 sub throw { die Doit::Exception->new(@_) }
121             }
122              
123             {
124             package Doit::ScopeCleanups;
125             $INC{'Doit/ScopeCleanups.pm'} = __FILE__; # XXX hack
126 43     43   289 use Doit::Log;
  43         91  
  43         9513  
127              
128             sub new {
129 127     127 0 375 my($class) = @_;
130 127         422 bless [], $class;
131             }
132              
133             sub add_scope_cleanup {
134 131     131 0 319 my($self, $code) = @_;
135 131         560 push @$self, { code => $code };
136             }
137              
138             sub DESTROY {
139 127     127   43840 my $self = shift;
140 127         810 for my $scope_cleanup (@$self) {
141 129         420 my($code) = $scope_cleanup->{code};
142 129 50       409 if ($] >= 5.014) {
143 129         316 eval {
144 129         482 $code->();
145             };
146 129 100       5774 if ($@) {
147             # error() will give visual feedback about the problem,
148             # die() would be left unnoticed. Note that
149             # an exception in a DESTROY block is not fatal,
150             # and can be only detected by inspecting $@.
151 4         16 error "Scope cleanup failed: $@";
152             }
153             } else {
154             # And eval {} in older perl versions would
155             # clobber an outside $@. See
156             # perldoc perl5140delta, "Exception Handling"
157 0         0 $code->();
158             }
159             }
160             }
161             }
162              
163             {
164             package Doit::Util;
165 43     43   305 use Exporter 'import';
  43         82  
  43         1862  
166 43     43   1233 our @EXPORT; BEGIN { @EXPORT = qw(in_directory new_scope_cleanup copy_stat get_sudo_cmd) }
167             $INC{'Doit/Util.pm'} = __FILE__; # XXX hack
168 43     43   244 use Doit::Log;
  43         81  
  43         45047  
169              
170             sub new_scope_cleanup (&) {
171 127     127 0 14173 my($code) = @_;
172 127         1540 my $sc = Doit::ScopeCleanups->new;
173 127         609 $sc->add_scope_cleanup($code);
174 127         290 $sc;
175             }
176              
177             sub in_directory (&$) {
178 302     302 0 308801 my($code, $dir) = @_;
179 302         517 my $scope_cleanup;
180 302 100       815 if (defined $dir) {
181 102         1056 require Cwd;
182 102         1333 my $pwd = Cwd::getcwd();
183 102 100 66     1089 if (!defined $pwd || $pwd eq '') { # XS variant returns undef, PP variant returns '' --- see https://rt.perl.org/Ticket/Display.html?id=132648
184 2         8 warning "No known current working directory";
185             } else {
186             $scope_cleanup = new_scope_cleanup
187             (sub {
188 100 100   100   1474 chdir $pwd or error "Can't chdir to $pwd: $!";
189 100         1063 });
190             }
191 102 100       1766 chdir $dir
192             or error "Can't chdir to $dir: $!";
193             }
194 299         847 $code->();
195             }
196              
197             # $src may be a source file or an arrayref with stat information
198             sub copy_stat ($$;@) {
199 65     65 0 6527 my($src, $dest, %preserve) = @_;
200 65 100       823 my @stat = ref $src eq 'ARRAY' ? @$src : stat($src);
201 65 50       255 error "Can't stat $src: $!" if !@stat;
202              
203 65         186 my $preserve_default = !%preserve;
204 65 100       228 my $preserve_ownership = exists $preserve{ownership} ? delete $preserve{ownership} : $preserve_default;
205 65 100       249 my $preserve_mode = exists $preserve{mode} ? delete $preserve{mode} : $preserve_default;
206 65 100       1268 my $preserve_time = exists $preserve{time} ? delete $preserve{time} : $preserve_default;
207              
208 65 50       181 error "Unhandled preserve values: " . join(" ", %preserve) if %preserve;
209              
210 65 100       237 if ($preserve_mode) {
211 62 50       904 chmod $stat[2], $dest
212             or warning "Can't chmod $dest to " . sprintf("0%o", $stat[2]) . ": $!";
213             }
214 65 100       215 if ($preserve_ownership) {
215             chown $stat[4], $stat[5], $dest
216 62 50       1015 or do {
217 0         0 my $save_err = $!; # otherwise it's lost in the get... calls
218 0         0 warning "Can't chown $dest to " .
219             (getpwuid($stat[4]))[0] . "/" .
220             (getgrgid($stat[5]))[0] . ": $save_err";
221             };
222             }
223 65 100       277 if ($preserve_time) {
224 41 50       508 utime $stat[8], $stat[9], $dest
225             or warning "Can't utime $dest to " .
226             scalar(localtime $stat[8]) . "/" .
227             scalar(localtime $stat[9]) .
228             ": $!";
229             }
230             }
231              
232             sub get_sudo_cmd () {
233 0 0   0 0 0 return () if $> == 0;
234 0         0 return ('sudo');
235             }
236              
237             }
238              
239             {
240             package Doit::Win32Util;
241              
242             # Taken from http://blogs.perl.org/users/graham_knop/2011/12/using-system-or-exec-safely-on-windows.html
243             sub win32_quote_list {
244 0     0   0 my (@args) = @_;
245              
246 0         0 my $args = join ' ', map { _quote_literal($_) } @args;
  0         0  
247              
248 0 0       0 if (_has_shell_metachars($args)) {
249             # cmd.exe treats quotes differently from standard
250             # argument parsing. just escape everything using ^.
251 0         0 $args =~ s/([()%!^"<>&|])/^$1/g;
252             }
253 0         0 return $args;
254             }
255              
256             sub _quote_literal {
257 0     0   0 my ($text) = @_;
258              
259             # basic argument quoting. uses backslashes and quotes to escape
260             # everything.
261             #
262             # The original code had a \v here, but this is not supported
263             # in perl5.8. Also, \v probably matches too many characters here
264             # --- restrict to the ones < 0x100
265 0 0 0     0 if ($text ne '' && $text !~ /[ \t\n\x0a\x0b\x0c\x0d\x85"]/) {
266             # no quoting needed
267             } else {
268 0         0 my @text = split '', $text;
269 0         0 $text = q{"};
270 0         0 for (my $i = 0; ; $i++) {
271 0         0 my $bs_count = 0;
272 0   0     0 while ( $i < @text && $text[$i] eq "\\" ) {
273 0         0 $i++;
274 0         0 $bs_count++;
275             }
276 0 0       0 if ($i > $#text) {
    0          
277 0         0 $text .= "\\" x ($bs_count * 2);
278 0         0 last;
279             } elsif ($text[$i] eq q{"}) {
280 0         0 $text .= "\\" x ($bs_count * 2 + 1);
281             } else {
282 0         0 $text .= "\\" x $bs_count;
283             }
284 0         0 $text .= $text[$i];
285             }
286 0         0 $text .= q{"};
287             }
288              
289 0         0 return $text;
290             }
291              
292             # direct port of code from win32.c
293             sub _has_shell_metachars {
294 0     0   0 my $string = shift;
295 0         0 my $inquote = 0;
296 0         0 my $quote = '';
297              
298 0         0 my @string = split '', $string;
299 0         0 for my $char (@string) {
300 0 0 0     0 if ($char eq q{%}) {
    0 0        
    0 0        
301 0         0 return 1;
302             } elsif ($char eq q{'} || $char eq q{"}) {
303 0 0       0 if ($inquote) {
304 0 0       0 if ($char eq $quote) {
305 0         0 $inquote = 0;
306 0         0 $quote = '';
307             }
308             } else {
309 0         0 $quote = $char;
310 0         0 $inquote++;
311             }
312             } elsif ($char eq q{<} || $char eq q{>} || $char eq q{|}) {
313 0 0       0 if ( ! $inquote) {
314 0         0 return 1;
315             }
316             }
317             }
318 0         0 return;
319             }
320             }
321              
322             {
323             package Doit;
324              
325             sub import {
326 48     48   3833 warnings->import;
327 48         19817 strict->import;
328             }
329              
330             sub unimport {
331 2     2   41 warnings->unimport;
332 2         47 strict->unimport;
333             }
334              
335 43     43   300 use Doit::Log;
  43         74  
  43         18795  
336              
337             my $diff_error_shown;
338              
339             sub _new {
340 38     38   134 my $class = shift;
341 38         154 my $self = bless { }, $class;
342 38         99 $self;
343             }
344             sub runner {
345 31     31 0 82 my($self) = @_;
346             # XXX hmmm, creating now self-refential data structures ...
347 31   33     448 $self->{runner} ||= Doit::Runner->new($self);
348             }
349            
350             sub dryrunner {
351 7     7 0 45 my($self) = @_;
352             # XXX hmmm, creating now self-refential data structures ...
353 7   33     200 $self->{dryrunner} ||= Doit::Runner->new($self, dryrun => 1);
354             }
355              
356             sub init {
357 38     38 0 4011207 my($class) = @_;
358 38         19524 require Getopt::Long;
359 38         271699 my $getopt = Getopt::Long::Parser->new;
360 38         1154 $getopt->configure(qw(pass_through noauto_abbrev));
361 38         3196 $getopt->getoptions(
362             'dry-run|n' => \my $dry_run,
363             );
364 38         8330 my $doit = $class->_new;
365 38 100       165 if ($dry_run) {
366 7         74 $doit->dryrunner;
367             } else {
368 31         103 $doit->runner;
369             }
370             }
371              
372             sub install_generic_cmd {
373 2     2 0 12 my($self, $name, $check, $code, $msg) = @_;
374 2 50       15 if (!$msg) {
375 2 50   2   20 $msg = sub { my($self, $args) = @_; $name . ($args ? " @$args" : '') };
  2         11  
  2         62  
376             }
377             my $cmd = sub {
378 3     3   8 my($self, @args) = @_;
379 3         4 my @commands;
380 3         11 my $addinfo = {};
381 3 100       15 if ($check->($self, \@args, $addinfo)) {
382             push @commands, {
383 2     2   12 code => sub { $code->($self, \@args, $addinfo) },
384 2         26 msg => $msg->($self, \@args, $addinfo),
385             };
386             }
387 3         23 Doit::Commands->new(@commands);
388 2         29 };
389 43     43   279 no strict 'refs';
  43         72  
  43         331294  
390 2         10 *{"cmd_$name"} = $cmd;
  2         35  
391             }
392              
393             sub cmd_chmod {
394 33     33 0 127 my($self, @args) = @_;
395 33 100 66     65 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  33         265  
  17         48  
  17         89  
396 33         117 my $quiet = delete $options{quiet};
397 33 50       120 error "Unhandled options: " . join(" ", %options) if %options;
398 33         90 my($mode, @files) = @args;
399 33         47 my @files_to_change;
400 33         113 for my $file (@files) {
401 36         494 my @s = stat($file);
402 36 100       124 if (@s) {
403 32 100       135 if (($s[2] & 07777) != $mode) {
404 29         126 push @files_to_change, $file;
405             }
406             } else {
407 4         13 push @files_to_change, $file;
408             }
409             }
410 33 100       87 if (@files_to_change) {
411             my @commands = {
412             code => sub {
413 27     27   463 my $changed_files = chmod $mode, @files_to_change;
414 27 100       192 if ($changed_files != @files_to_change) {
415 3 100       13 if (@files_to_change == 1) {
    100          
416 1         17 error "chmod failed: $!";
417             } elsif ($changed_files == 0) {
418 1         12 error "chmod failed on all files: $!";
419             } else {
420 1         10 error "chmod failed on some files (" . (@files_to_change-$changed_files) . "/" . scalar(@files_to_change) . "): $!";
421             }
422             }
423             },
424 30 100       424 ($quiet ? () : (msg => sprintf("chmod 0%o %s", $mode, join(" ", @files_to_change)))), # shellquote?
425             rv => scalar @files_to_change,
426             };
427 30         217 Doit::Commands->new(@commands);
428             } else {
429 3         17 Doit::Commands->return_zero;
430             }
431             }
432              
433             sub cmd_chown {
434 12     12 0 24 my($self, @args) = @_;
435 12 100 66     16 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  12         57  
  2         13  
  2         8  
436 12         23 my $quiet = delete $options{quiet};
437 12 50       25 error "Unhandled options: " . join(" ", %options) if %options;
438 12         26 my($uid, $gid, @files) = @args;
439              
440 12 100       65 if (!defined $uid) {
    100          
441 3         6 $uid = -1;
442             } elsif ($uid !~ /^-?\d+$/) {
443 2         805 my $_uid = (getpwnam $uid)[2];
444 2 100       11 if (!defined $_uid) {
445             # XXX problem: in dry-run mode the user/group could be
446             # created in _this_ pass, so this error would happen
447             # while in wet-run everything would be fine. Good solution?
448             # * do uid/gid resolution _again_ in the command if it failed here?
449             # * maintain a virtual list of created users/groups while this run, and
450             # use this list as a fallback?
451 1         6 error "User '$uid' does not exist";
452             }
453 1         9 $uid = $_uid;
454             }
455 11 100       39 if (!defined $gid) {
    100          
456 6         10 $gid = -1;
457             } elsif ($gid !~ /^-?\d+$/) {
458 1         122 my $_gid = (getgrnam $gid)[2];
459 1 50       6 if (!defined $_gid) {
460 1         6 error "Group '$gid' does not exist";
461             }
462 0         0 $gid = $_gid;
463             }
464              
465 10         16 my @files_to_change;
466 10 100 100     30 if ($uid != -1 || $gid != -1) {
467 9         16 for my $file (@files) {
468 10         112 my @s = stat($file);
469 10 100       31 if (@s) {
470 7 50 66     65 if ($uid != -1 && $s[4] != $uid) {
    50 66        
471 0         0 push @files_to_change, $file;
472             } elsif ($gid != -1 && $s[5] != $gid) {
473 0         0 push @files_to_change, $file;
474             }
475             } else {
476 3         10 push @files_to_change, $file;
477             }
478             }
479             }
480              
481 10 100       22 if (@files_to_change) {
482             my @commands = {
483             code => sub {
484 2     2   21 my $changed_files = chown $uid, $gid, @files_to_change;
485 2 50       9 if ($changed_files != @files_to_change) {
486 2 100       7 if (@files_to_change == 1) {
    50          
487 1         9 error "chown failed: $!";
488             } elsif ($changed_files == 0) {
489 1         7 error "chown failed on all files: $!";
490             } else {
491 0         0 error "chown failed on some files (" . (@files_to_change-$changed_files) . "/" . scalar(@files_to_change) . "): $!";
492             }
493             }
494             },
495 2 50       25 ($quiet ? () : (msg => "chown $uid, $gid, @files_to_change")), # shellquote?
496             rv => scalar @files_to_change,
497             };
498 2         7 Doit::Commands->new(@commands);
499             } else {
500 8         23 Doit::Commands->return_zero;
501             }
502             }
503              
504             sub cmd_cond_run {
505 12     12 0 101 my($self, %opts) = @_;
506 12         36 my $if = delete $opts{if};
507 12         29 my $unless = delete $opts{unless};
508 12         23 my $creates = delete $opts{creates};
509 12         24 my $cmd = delete $opts{cmd};
510 12 100       67 error "Unhandled options: " . join(" ", %opts) if %opts;
511              
512 11 100       41 if (!$cmd) {
513 1         21 error "cmd is a mandatory option for cond_run";
514             }
515 10 100       32 if (ref $cmd ne 'ARRAY') {
516 1         8 error "cmd must be an array reference";
517             }
518              
519 9         11 my $doit = 1;
520 9 100 100     54 if ($if && !$if->()) {
521 2         20 $doit = 0;
522             }
523 9 100 100     58 if ($doit && $unless && $unless->()) {
      100        
524 1         12 $doit = 0;
525             }
526 9 100 100     75 if ($doit && $creates && -e $creates) {
      100        
527 1         11 $doit = 0;
528             }
529              
530 9 100       25 if ($doit) {
531 5         7 my $doit_commands;
532 5 50       20 if (ref $cmd->[0] eq 'ARRAY') {
533 0         0 $doit_commands = $self->cmd_run(@$cmd);
534             } else {
535 5         34 $doit_commands = $self->cmd_system(@$cmd);
536             }
537 5         39 $doit_commands->set_last_rv(1);
538 5         19 $doit_commands;
539             } else {
540 4         26 Doit::Commands->return_zero;
541             }
542             }
543              
544             sub cmd_ln_nsf {
545 6     6 0 12 my($self, $oldfile, $newfile) = @_;
546              
547 6         8 my $doit = 1;
548 6 100       130 if (!defined $oldfile) {
    100          
    100          
    100          
549 1         3 error "oldfile was not specified for ln_nsf";
550             } elsif (!defined $newfile) {
551 1         3 error "newfile was not specified for ln_nsf";
552             } elsif (-l $newfile) {
553 2 50       27 my $points_to = readlink $newfile
554             or error "Unexpected: readlink $newfile failed (race condition?)";
555 2 100       15 if ($points_to eq $oldfile) {
556 1         8 $doit = 0;
557             }
558             } elsif (-d $newfile) {
559             # Theoretically "ln -nsf destination directory" works (not always,
560             # e.g. fails with destination=/), but results are not very useful,
561             # so fail here.
562 1         47 error qq{"$newfile" already exists as a directory};
563             } else {
564             # probably a file, keep $doit=1
565             }
566              
567 3 100       10 if ($doit) {
568             my @commands = {
569             code => sub {
570 2     2   5623 system 'ln', '-nsf', $oldfile, $newfile;
571 2 50       112 error "ln -nsf $oldfile $newfile failed" if $? != 0;
572             },
573 2         22 msg => "ln -nsf $oldfile $newfile",
574             rv => 1,
575             };
576 2         9 Doit::Commands->new(@commands);
577             } else {
578 1         11 Doit::Commands->return_zero;
579             }
580             }
581              
582             sub cmd_make_path {
583 3     3 0 7 my($self, @directories) = @_;
584 3 100       8 my $options = {}; if (ref $directories[-1] eq 'HASH') { $options = pop @directories }
  3         10  
  1         2  
585 3         7 my @directories_to_create = grep { !-d $_ } @directories;
  5         68  
586 3 100       15 if (@directories_to_create) {
587             my @commands = {
588             code => sub {
589 2     2   14 require File::Path;
590 2 50       506 File::Path::make_path(@directories_to_create, $options)
591             or error $!;
592             },
593 2         23 msg => "make_path @directories",
594             rv => scalar @directories_to_create,
595             };
596 2         7 Doit::Commands->new(@commands);
597             } else {
598 1         7 Doit::Commands->return_zero;
599             }
600             }
601              
602             sub cmd_mkdir {
603 20     20 0 107 my($self, $directory, $mode) = @_;
604 20 100       431 if (!-d $directory) {
605 18         78 my @commands;
606 18 100       77 if (defined $mode) {
607             push @commands, {
608 2 100   2   71 code => sub { mkdir $directory, $mode or error "$!" },
609 2         19 msg => "mkdir $directory with mask $mode",
610             rv => 1,
611             };
612             } else {
613             push @commands, {
614 16 100   16   1008 code => sub { mkdir $directory or error "$!" },
615 16         271 msg => "mkdir $directory",
616             rv => 1,
617             };
618             }
619 18         267 Doit::Commands->new(@commands);
620             } else {
621 2         16 Doit::Commands->return_zero;
622             }
623             }
624              
625             sub cmd_remove_tree {
626 4     4 0 9 my($self, @directories) = @_;
627 4 100       7 my $options = {}; if (ref $directories[-1] eq 'HASH') { $options = pop @directories }
  4         12  
  1         2  
628 4         8 my @directories_to_remove = grep { -d $_ } @directories;
  6         78  
629 4 100       15 if (@directories_to_remove) {
630             my @commands = {
631             code => sub {
632 3     3   14 require File::Path;
633 3 50       966 File::Path::remove_tree(@directories_to_remove, $options)
634             or error "$!";
635             },
636 3         46 msg => "remove_tree @directories_to_remove",
637             rv => scalar @directories_to_remove,
638             };
639 3         13 Doit::Commands->new(@commands);
640             } else {
641 1         5 Doit::Commands->return_zero;
642             }
643             }
644              
645             sub cmd_rename {
646 15     15 0 51 my($self, $from, $to) = @_;
647 15         25 my @commands;
648             push @commands, {
649 13 100   13   1342 code => sub { rename $from, $to or error "$!" },
650 15         170 msg => "rename $from, $to",
651             rv => 1,
652             };
653 15         71 Doit::Commands->new(@commands);
654             }
655              
656             sub cmd_copy {
657 17     17 0 61 my($self, @args) = @_;
658 17 100 100     23 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  17         117  
  3         5  
  3         10  
659 17         35 my $quiet = delete $options{quiet};
660 17 100       40 error "Unhandled options: " . join(" ", %options) if %options;
661 16 100       31 if (@args != 2) {
662 1         3 error "Expecting two arguments: from and to filenames";
663             }
664 15         45 my($from, $to) = @args;
665              
666 15         17 my $real_to;
667 15 100       218 if (-d $to) {
668 4         26 require File::Basename;
669 4         198 $real_to = "$to/" . File::Basename::basename($from);
670             } else {
671 11         25 $real_to = $to;
672             }
673 15 100 100     181 if (!-e $real_to || do { require File::Compare; File::Compare::compare($from, $real_to) != 0 }) {
  8         1087  
  8         2581  
674             my @commands = {
675             code => sub {
676 11     11   882 require File::Copy;
677 11 100       4087 File::Copy::copy($from, $to)
678             or error "Copy failed: $!";
679             },
680 11         500 msg => do {
681 11 100       114 if (!-e $real_to) {
682 7         57 "copy $from $real_to (destination does not exist)";
683             } else {
684 4 100       30 if ($quiet) {
685 1         13 "copy $from $real_to";
686             } else {
687 3 50       21 if (eval { require IPC::Run; 1 }) {
  3         338  
  0         0  
688 0         0 my $diff;
689 0 0       0 if (eval { IPC::Run::run(['diff', '-u', $real_to, $from], '>', \$diff); 1 }) {
  0         0  
  0         0  
690 0         0 "copy $from $real_to\ndiff:\n$diff";
691             } else {
692 0 0       0 "copy $from $real_to\n(diff not available" . (!$diff_error_shown++ ? ", error: $@" : "") . ")";
693             }
694             } else {
695 3         34 my $diffref = _qx('diff', '-u', $real_to, $from);
696 3         155 "copy $from $real_to\ndiff:\n$$diffref";
697             }
698             }
699             }
700             },
701             rv => 1,
702             };
703 11         116 Doit::Commands->new(@commands);
704             } else {
705 4         649 Doit::Commands->return_zero;
706             }
707             }
708              
709             sub cmd_move {
710 3     3 0 9 my($self, $from, $to) = @_;
711             my @commands = {
712             code => sub {
713 2     2   511 require File::Copy;
714 2 100       2250 File::Copy::move($from, $to)
715             or error "Move failed: $!";
716             },
717 3         51 msg => "move $from $to",
718             rv => 1,
719             };
720 3         27 Doit::Commands->new(@commands);
721             }
722              
723             sub _analyze_dollar_questionmark () {
724 50 100   50   396 if ($? == -1) {
    100          
725             (
726 1         91 msg => sprintf("Could not execute command: %s", $!),
727             errno => $!,
728             exitcode => $?,
729             );
730             } elsif ($? & 127) {
731 8         54 my $signalnum = $? & 127;
732 8 50       106 my $coredump = ($? & 128) ? 'with' : 'without';
733             (
734 8         232 msg => sprintf("Command died with signal %d, %s coredump", $signalnum, $coredump),
735             signalnum => $signalnum,
736             coredump => $coredump,
737             );
738             } else {
739 41         315 my $exitcode = $?>>8;
740             (
741 41         1192 msg => "Command exited with exit code " . $exitcode,
742             exitcode => $exitcode,
743             );
744             }
745             }
746              
747             sub _handle_dollar_questionmark (@) {
748 46     46   433 my(%opts) = @_;
749 46         268 my $prefix_msg = delete $opts{prefix_msg};
750 46 50       245 error "Unhandled options: " . join(" ", %opts) if %opts;
751              
752 46         299 my %res = _analyze_dollar_questionmark;
753 46         225 my $msg = delete $res{msg};
754 46 100       387 if (defined $prefix_msg) {
755 22         83 $msg = $prefix_msg.$msg;
756             }
757 46         694 Doit::Exception::throw($msg, %res);
758             }
759              
760             sub _show_cwd ($) {
761 136     136   446 my $flag = shift;
762 136 100       354 if ($flag) {
763 42         711 require Cwd;
764 42         1421 " (in " . Cwd::getcwd() . ")";
765             } else {
766 94         1293 "";
767             }
768             }
769              
770             sub cmd_open2 {
771 11     11 0 88 my($self, @args) = @_;
772 11 100 66     60 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  11         141  
  8         86  
  8         56  
773 11         35 my $quiet = delete $options{quiet};
774 11         19 my $info = delete $options{info};
775 11 100       20 my $instr = delete $options{instr}; $instr = '' if !defined $instr;
  11         37  
776 11 50       33 error "Unhandled options: " . join(" ", %options) if %options;
777              
778 11         14 @args = Doit::Win32Util::win32_quote_list(@args) if Doit::IS_WIN;
779              
780 11         625 require IPC::Open2;
781              
782             my $code = sub {
783 10     10   18 my($chld_out, $chld_in);
784 10         38 my $pid = IPC::Open2::open2($chld_out, $chld_in, @args);
785 10         29461 print $chld_in $instr;
786 10         82 close $chld_in;
787 10         106 local $/;
788 10         13591 my $buf = <$chld_out>;
789 10         196 close $chld_out;
790 10         313 waitpid $pid, 0;
791 10 100 66     245 $? == 0
    100          
792             or _handle_dollar_questionmark($quiet||$info ? (prefix_msg => "open2 command '@args' failed: ") : ());
793 7         305 $buf;
794 11         3028 };
795              
796 11         52 my @commands;
797             push @commands, {
798 11 100   1   69 ($info ? (rv => $code->(), code => sub {}) : (code => $code)),
    100          
799             ($quiet ? () : (msg => "@args")),
800             };
801 10         266 Doit::Commands->new(@commands);
802             }
803              
804             sub cmd_info_open2 {
805 3     3 0 22 my($self, @args) = @_;
806 3 100 66     10 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  3         40  
  1         6  
  1         9  
807 3         16 $options{info} = 1;
808 3         13 $self->cmd_open2(\%options, @args);
809             }
810              
811             sub cmd_open3 {
812 38     38 0 332 my($self, @args) = @_;
813 38 100 66     142 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  38         564  
  36         77  
  36         414  
814 38         186 my $quiet = delete $options{quiet};
815 38         131 my $info = delete $options{info};
816 38         70 my $instr = delete $options{instr};
817 38         85 my $errref = delete $options{errref};
818 38         73 my $statusref = delete $options{statusref};
819 38 50       242 error "Unhandled options: " . join(" ", %options) if %options;
820              
821 38         120 @args = Doit::Win32Util::win32_quote_list(@args) if Doit::IS_WIN;
822              
823 38         376 require IO::Select;
824 38         914 require IPC::Open3;
825 38         4803 require Symbol;
826              
827             my $code = sub {
828 37     37   57 my($chld_out, $chld_in, $chld_err);
829 37         337 $chld_err = Symbol::gensym();
830 37 100       1246 my $pid = IPC::Open3::open3((defined $instr ? $chld_in : undef), $chld_out, $chld_err, @args);
831 36 100       114754 if (defined $instr) {
832 26         173 print $chld_in $instr;
833 26         192 close $chld_in;
834             }
835              
836 36         1015 my $sel = IO::Select->new;
837 36         900 $sel->add($chld_out);
838 36         2683 $sel->add($chld_err);
839              
840 36         1618 my %buf = ($chld_out => '', $chld_err => '');
841 36         194 while(my @ready_fhs = $sel->can_read()) {
842 68         1085595 for my $ready_fh (@ready_fhs) {
843 105         467 my $buf = '';
844 105         17767 while (sysread $ready_fh, $buf, 1024, length $buf) { }
845 105 100       566 if ($buf eq '') { # eof
846 72         275 $sel->remove($ready_fh);
847 72         3627 $ready_fh->close;
848 72 100       1625 last if $sel->count == 0;
849             } else {
850 33         495 $buf{$ready_fh} .= $buf;
851             }
852             }
853             }
854              
855 36         1417 waitpid $pid, 0;
856              
857 36 100       163 if ($errref) {
858 29         308 $$errref = $buf{$chld_err};
859             }
860              
861 36 100       129 if ($statusref) {
862 2         38 %$statusref = ( _analyze_dollar_questionmark );
863             } else {
864 34 100       310 if ($? != 0) {
865 4 100 66     144 _handle_dollar_questionmark($quiet||$info ? (prefix_msg => "open3 command '@args' failed: ") : ());
866             }
867             }
868              
869 32         997 $buf{$chld_out};
870 38         439 };
871              
872 38         165 my @commands;
873             push @commands, {
874 38 100   1   209 ($info ? (rv => $code->(), code => sub {}) : (code => $code)),
    100          
875             ($quiet ? () : (msg => "@args")),
876             };
877 37         542 Doit::Commands->new(@commands);
878             }
879              
880             sub cmd_info_open3 {
881 3     3 0 18 my($self, @args) = @_;
882 3 100 66     7 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  3         52  
  1         2  
  1         12  
883 3         17 $options{info} = 1;
884 3         20 $self->cmd_open3(\%options, @args);
885             }
886              
887             sub _qx {
888 154     154   590 my(@args) = @_;
889 154         199 @args = Doit::Win32Util::win32_quote_list(@args) if Doit::IS_WIN;
890              
891 154 50       347769 open my $fh, '-|', @args
892             or error "Error running '@args': $!";
893 154         4823 local $/;
894 154         261415 my $buf = <$fh>;
895 154         7136 close $fh;
896 154         8503 \$buf;
897             }
898              
899             sub cmd_qx {
900 144     144 0 621 my($self, @args) = @_;
901 144 100 66     232 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  144         1643  
  138         315  
  138         1237  
902 144         471 my $quiet = delete $options{quiet};
903 144         228 my $info = delete $options{info};
904 144         223 my $statusref = delete $options{statusref};
905 144 50       346 error "Unhandled options: " . join(" ", %options) if %options;
906              
907             my $code = sub {
908 143     143   691 my $bufref = _qx(@args);
909 143 100       1109 if ($statusref) {
910 2         33 %$statusref = ( _analyze_dollar_questionmark );
911             } else {
912 141 100       2293 if ($? != 0) {
913 22 100 100     571 _handle_dollar_questionmark($quiet||$info ? (prefix_msg => "qx command '@args' failed: ") : ());
914             }
915             }
916 121         5625 $$bufref;
917 144         1392 };
918              
919 144         281 my @commands;
920             push @commands, {
921 144 100   113   552 ($info ? (rv => $code->(), code => sub {}) : (code => $code)),
    100          
922             ($quiet ? () : (msg => "@args")),
923             };
924 124         4057 Doit::Commands->new(@commands);
925             }
926              
927             sub cmd_info_qx {
928 134     134 0 595 my($self, @args) = @_;
929 134 100 66     243 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  134         1920  
  131         314  
  131         637  
930 134         621 $options{info} = 1;
931 134         681 $self->cmd_qx(\%options, @args);
932             }
933              
934             sub cmd_rmdir {
935 6     6 0 19 my($self, $directory) = @_;
936 6 100       78 if (-d $directory) {
937             my @commands = {
938 5 100   5   253 code => sub { rmdir $directory or error "$!" },
939 5         44 msg => "rmdir $directory",
940             };
941 5         34 Doit::Commands->new(@commands);
942             } else {
943 1         10 Doit::Commands->return_zero;
944             }
945             }
946              
947             sub cmd_run {
948 0     0 0 0 my($self, @args) = @_;
949 0         0 my @commands;
950             push @commands, {
951             code => sub {
952 0     0   0 require IPC::Run;
953 0         0 my $success = IPC::Run::run(@args);
954 0 0       0 if (!$success) {
955 0         0 _handle_dollar_questionmark;
956             }
957             },
958 0         0 msg => do {
959 0         0 my @print_cmd;
960 0         0 for my $arg (@args) {
961 0 0       0 if (ref $arg eq 'ARRAY') {
962 0         0 push @print_cmd, @$arg;
963             } else {
964 0         0 push @print_cmd, $arg;
965             }
966             }
967 0         0 join " ", @print_cmd;
968             },
969             rv => 1,
970             };
971 0         0 Doit::Commands->new(@commands);
972             }
973              
974             sub cmd_setenv {
975 3     3 0 7 my($self, $key, $val) = @_;
976 3 100 100     14 if (!defined $ENV{$key} || $ENV{$key} ne $val) {
977             my @commands = {
978 2     2   11 code => sub { $ENV{$key} = $val },
979 2 100       21 msg => qq{set \$ENV{$key} to "$val", previous value was } . (defined $ENV{$key} ? qq{"$ENV{$key}"} : qq{unset}),
980             rv => 1,
981             };
982 2         7 Doit::Commands->new(@commands);
983             } else {
984 1         5 Doit::Commands->return_zero;
985             }
986             }
987              
988             sub cmd_symlink {
989 2     2 0 5 my($self, $oldfile, $newfile) = @_;
990 2         4 my $doit;
991 2 100       37 if (-l $newfile) {
    50          
992 1 50       14 my $points_to = readlink $newfile
993             or error "Unexpected: readlink $newfile failed (race condition?)";
994 1 50       5 if ($points_to ne $oldfile) {
995 0         0 $doit = 1;
996             }
997             } elsif (!-e $newfile) {
998 1         5 $doit = 1;
999             } else {
1000 0         0 warning "$newfile exists but is not a symlink, will fail later...";
1001             }
1002 2 100       5 if ($doit) {
1003             my @commands = {
1004 1 50   1   43 code => sub { symlink $oldfile, $newfile or error "$!" },
1005 1         13 msg => "symlink $oldfile $newfile",
1006             rv => 1,
1007             };
1008 1         5 Doit::Commands->new(@commands);
1009             } else {
1010 1         4 Doit::Commands->return_zero;
1011             }
1012             }
1013              
1014             sub cmd_system {
1015 138     138 0 987 my($self, @args) = @_;
1016 138 100 66     595 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  138         2234  
  46         106  
  46         517  
1017 138         692 my $quiet = delete $options{quiet};
1018 138         782 my $info = delete $options{info};
1019 138         297 my $show_cwd = delete $options{show_cwd};
1020 138 50       396 error "Unhandled options: " . join(" ", %options) if %options;
1021              
1022 138         207 @args = Doit::Win32Util::win32_quote_list(@args) if Doit::IS_WIN;
1023              
1024             my $code = sub {
1025 137     137   2123660 system @args;
1026 137 100       9471 if ($? != 0) {
1027 17         620 _handle_dollar_questionmark;
1028             }
1029 138         1602 };
1030              
1031 138         306 my @commands;
1032             push @commands, {
1033             ($info
1034             ? (
1035 2         24 rv => do { $code->(); 1 },
  2         202  
1036       0     code => sub {},
1037             )
1038 138 100       2012 : (
    100          
1039             rv => 1,
1040             code => $code,
1041             )
1042             ),
1043             ($quiet ? () : (msg => "@args" . _show_cwd($show_cwd))),
1044             };
1045 138         1595 Doit::Commands->new(@commands);
1046             }
1047              
1048             sub cmd_info_system {
1049 1     1 0 3 my($self, @args) = @_;
1050 1 50 33     2 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  1         7  
  0         0  
  0         0  
1051 1         8 $options{info} = 1;
1052 1         8 $self->cmd_system(\%options, @args);
1053             }
1054              
1055             sub cmd_touch {
1056 23     23 0 194 my($self, @files) = @_;
1057 23         56 my @commands;
1058 23         80 for my $file (@files) {
1059 29 100       643 if (!-e $file) {
1060             push @commands, {
1061 28 50   28   2766 code => sub { open my $fh, '>>', $file or error "$!" },
1062 28         543 msg => "touch non-existent file $file",
1063             }
1064             } else {
1065             push @commands, {
1066 1 50   1   34 code => sub { utime time, time, $file or error "$!" },
1067 1         12 msg => "touch existent file $file",
1068             };
1069             }
1070             }
1071 23         261 my $doit_commands = Doit::Commands->new(@commands);
1072 23         117 $doit_commands->set_last_rv(scalar @files);
1073 23         112 $doit_commands;
1074             }
1075              
1076             sub cmd_create_file_if_nonexisting {
1077 15     15 0 80 my($self, @files) = @_;
1078 15         43 my @commands;
1079 15         67 for my $file (@files) {
1080 17 100       364 if (!-e $file) {
1081             push @commands, {
1082 15 50   15   1508 code => sub { open my $fh, '>>', $file or error "$!" },
1083 15         229 msg => "create empty file $file",
1084             };
1085             }
1086             }
1087 15 100       86 if (@commands) {
1088 14         107 my $doit_commands = Doit::Commands->new(@commands);
1089 14         80 $doit_commands->set_last_rv(scalar @commands);
1090 14         63 $doit_commands;
1091             } else {
1092 1         4 Doit::Commands->return_zero;
1093             }
1094             }
1095              
1096             sub cmd_unlink {
1097 20     20 0 113 my($self, @files) = @_;
1098 20         53 my @files_to_remove;
1099 20         90 for my $file (@files) {
1100 28 100 100     550 if (-e $file || -l $file) {
1101 25         117 push @files_to_remove, $file;
1102             }
1103             }
1104 20 100       99 if (@files_to_remove) {
1105             my @commands = {
1106 18 50   18   1551 code => sub { unlink @files_to_remove or error "$!" },
1107 18         286 msg => "unlink @files_to_remove", # shellquote?
1108             };
1109 18         159 Doit::Commands->new(@commands);
1110             } else {
1111 2         11 Doit::Commands->return_zero;
1112             }
1113             }
1114              
1115             sub cmd_unsetenv {
1116 2     2 0 5 my($self, $key) = @_;
1117 2 100       7 if (defined $ENV{$key}) {
1118             my @commands = {
1119 1     1   6 code => sub { delete $ENV{$key} },
1120 1         10 msg => qq{unset \$ENV{$key}, previous value was "$ENV{$key}"},
1121             rv => 1,
1122             };
1123 1         4 Doit::Commands->new(@commands);
1124             } else {
1125 1         4 Doit::Commands->return_zero;
1126             }
1127             }
1128              
1129             sub cmd_utime {
1130 10     10 0 48 my($self, $atime, $mtime, @files) = @_;
1131              
1132 10         14 my $now;
1133 10 100       25 if (!defined $atime) {
1134 1   33     7 $atime = ($now ||= time);
1135             }
1136 10 100       29 if (!defined $mtime) {
1137 1   33     3 $mtime = ($now ||= time);
1138             }
1139              
1140 10         12 my @files_to_change;
1141 10         27 for my $file (@files) {
1142 14         173 my @s = stat $file;
1143 14 100       46 if (@s) {
1144 10 100 100     34 if ($s[8] != $atime || $s[9] != $mtime) {
1145 9         40 push @files_to_change, $file;
1146             }
1147             } else {
1148 4         10 push @files_to_change, $file; # will fail later
1149             }
1150             }
1151              
1152 10 100       29 if (@files_to_change) {
1153             my @commands = {
1154             code => sub {
1155 9     9   151 my $changed_files = utime $atime, $mtime, @files;
1156 9 100       51 if ($changed_files != @files_to_change) {
1157 3 100       9 if (@files_to_change == 1) {
    100          
1158 1         19 error "utime failed: $!";
1159             } elsif ($changed_files == 0) {
1160 1         8 error "utime failed on all files: $!";
1161             } else {
1162 1         10 error "utime failed on some files (" . (@files_to_change-$changed_files) . "/" . scalar(@files_to_change) . "): $!";
1163             }
1164             }
1165             },
1166 9         112 msg => "utime $atime, $mtime, @files",
1167             rv => scalar @files_to_change,
1168             };
1169 9         38 Doit::Commands->new(@commands);
1170             } else {
1171 1         6 Doit::Commands->return_zero;
1172             }
1173             }
1174              
1175             sub cmd_write_binary {
1176 32     32 0 145 my($self, @args) = @_;
1177 32 100 100     86 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  32         333  
  12         28  
  12         63  
1178 32   100     309 my $quiet = delete $options{quiet} || 0;
1179 32 100       148 my $atomic = exists $options{atomic} ? delete $options{atomic} : 1;
1180 32 100       109 error "Unhandled options: " . join(" ", %options) if %options;
1181 31 100       127 if (@args != 2) {
1182 1         5 error "Expecting two arguments: filename and contents";
1183             }
1184 30         93 my($filename, $content) = @args;
1185              
1186 30         74 my $doit;
1187             my $need_diff;
1188 30 100       786 if (!-e $filename) {
    100          
1189 13         61 $doit = 1;
1190             } elsif (-s $filename != length($content)) {
1191 12         68 $doit = 1;
1192 12         37 $need_diff = 1;
1193             } else {
1194 5 50       155 open my $fh, '<', $filename
1195             or error "Can't open $filename: $!";
1196 5         28 binmode $fh;
1197 5         25 local $/;
1198 5         120 my $file_content = <$fh>;
1199 5 100       4246 if ($file_content ne $content) {
1200 1         4 $doit = 1;
1201 1         21 $need_diff = 1;
1202             }
1203             }
1204              
1205 30 100       111 if ($doit) {
1206             my @commands = {
1207             code => sub {
1208             # XXX consider to reuse code for atomic writes:
1209             # either from Doit::File::file_atomic_write (problematic, different component)
1210             # or share code with change_file
1211 26 100   26   469 my $outfile = $atomic ? "$filename.$$.".time.".tmp" : $filename;
1212 26 100       2088 open my $ofh, '>', $outfile
1213             or error "Can't write to $outfile: $!";
1214 25 100       349 if (-e $filename) {
1215 13         177 Doit::Util::copy_stat($filename, $outfile, ownership => 1, mode => 1);
1216             }
1217 25         108 binmode $ofh;
1218 25         264 print $ofh $content;
1219 25 50       1058 close $ofh
1220             or error "While closing $outfile: $!";
1221 25 100       189 if ($atomic) {
1222 24 50       2011 rename $outfile, $filename
1223             or error "Error while renaming $outfile to $filename: $!";
1224             }
1225             },
1226             rv => 1,
1227             ($quiet >= 2
1228             ? ()
1229 26 100       400 : (msg => do {
1230 22 100       73 if ($quiet) {
1231 5 100       34 if ($need_diff) {
1232 2         26 "Replace existing file $filename";
1233             } else {
1234 3         26 "Create new file $filename";
1235             }
1236             } else {
1237 17 100       82 if ($need_diff) {
1238 8 50       28 if (eval { require IPC::Run; 1 }) { # no temporary file required
  8         1415  
  0         0  
1239 0         0 my $diff;
1240 0 0       0 if (eval { IPC::Run::run(['diff', '-u', $filename, '-'], '<', \$content, '>', \$diff); 1 }) {
  0         0  
  0         0  
1241 0         0 "Replace existing file $filename with diff:\n$diff";
1242             } else {
1243 0 0       0 "(diff not available" . (!$diff_error_shown++ ? ", error: $@" : "") . ")";
1244             }
1245             } else {
1246 8         48 my $diff;
1247 8 50       63 if (eval { require File::Temp; 1 }) {
  8         72  
  8         77  
1248 8         122 my($tempfh,$tempfile) = File::Temp::tempfile(UNLINK => 1);
1249 8         5429 print $tempfh $content;
1250 8 50       456 if (close $tempfh) {
1251 8         115 my $diffref = _qx('diff', '-u', $filename, $tempfile);
1252 8         102 $diff = $$diffref;
1253 8         780 unlink $tempfile;
1254 8 50       108 if (length $diff) {
1255 8         114 $diff = "Replace existing file $filename with diff:\n$diff";
1256             } else {
1257 0         0 $diff = "(diff not available, probably no diff utility installed)";
1258             }
1259             } else {
1260 0         0 $diff = "(diff not available, error in tempfile creation ($!))";
1261             }
1262             } else {
1263 0         0 $diff = "(diff not available, neither IPC::Run nor File::Temp available)";
1264             }
1265 8         322 $diff;
1266             }
1267             } else {
1268 9         75 "Create new file $filename with content:\n$content";
1269             }
1270             }
1271             }
1272             )),
1273             };
1274 26         406 Doit::Commands->new(@commands);
1275             } else {
1276 4         35 Doit::Commands->return_zero;
1277             }
1278             }
1279              
1280             sub cmd_change_file {
1281 47     47 0 165 my($self, @args) = @_;
1282 47 100 100     126 my %options; if (@args && ref $args[0] eq 'HASH') { %options = %{ shift @args } }
  47         438  
  3         9  
  3         14  
1283 47         106 my $check = delete $options{check};
1284 47         135 my $debug = delete $options{debug};
1285 47 50 66     201 if ($check && ref $check ne 'CODE') { error "check parameter should be a CODE reference" }
  0         0  
1286 47 100       101 error "Unhandled options: " . join(" ", %options) if %options;
1287              
1288 46 100       91 if (@args < 1) {
1289 1         4 error "Expecting at least a filename and one or more changes";
1290             }
1291              
1292 45         88 my($file, @changes) = @args;
1293 45 100       670 if (!-e $file) {
1294 2         14 error "$file does not exist";
1295             }
1296 43 100       382 if (!-f $file) {
1297 2         12 error "$file is not a file";
1298             }
1299              
1300 41         86 my @commands;
1301              
1302 41         81 for (@changes) {
1303 46 100       187 if ($_->{add_if_missing}) {
1304 26         90 my $line = delete $_->{add_if_missing};
1305 26         88 $_->{unless_match} = $line;
1306 26 100 100     336 if (defined $_->{add_after} ||
      100        
      100        
1307             defined $_->{add_after_first} ||
1308             defined $_->{add_before} ||
1309             defined $_->{add_before_last}
1310             ) {
1311             my $defines =
1312             (defined $_->{add_after} || 0) +
1313             (defined $_->{add_after_first} || 0) +
1314             (defined $_->{add_before} || 0) +
1315 10   100     162 (defined $_->{add_before_last} || 0)
      100        
      100        
      100        
1316             ;
1317 10 50       32 if ($defines != 1) {
1318 0         0 error "Can specify only one of the following: 'add_after', 'add_after_first', 'add_before', 'add_before_last' (change for $file)\n";
1319             }
1320 10         21 my $add;
1321             my $do_after;
1322 10         0 my $reverse;
1323 10 100       53 if (defined $_->{add_after}) {
    100          
    100          
    50          
1324 4         13 $add = delete $_->{add_after};
1325 4         9 $reverse = 1;
1326 4         7 $do_after = 1;
1327             } elsif (defined $_->{add_after_first}) {
1328 2         9 $add = delete $_->{add_after_first};
1329 2         9 $reverse = 0;
1330 2         7 $do_after = 1;
1331             } elsif (defined $_->{add_before}) {
1332 2         9 $add = delete $_->{add_before};
1333 2         8 $reverse = 0;
1334 2         7 $do_after = 0;
1335             } elsif (defined $_->{add_before_last}) {
1336 2         7 $add = delete $_->{add_before_last};
1337 2         9 $reverse = 1;
1338 2         5 $do_after = 0;
1339             } else {
1340 0         0 error "Can never happen";
1341             }
1342 10         57 qr{$add}; # must be a regexp
1343             $_->{action} = sub {
1344 6     6   21 my $arrayref = $_[0];
1345 6         7 my $found = 0;
1346 6 100       31 my $from = $reverse ? $#$arrayref : 0;
1347 6 100       58 my $to = $reverse ? 0 : $#$arrayref;
1348 6 100       33 my $inc = $reverse ? -1 : +1;
1349 6 100       19 for(my $i=$from; ($reverse ? $i>=$to : $i<=$to); $i+=$inc) {
1350 12 100       311 if ($arrayref->[$i] =~ $add) {
1351 5 100       368 if ($do_after) {
1352 3         21 splice @$arrayref, $i+1, 0, $line;
1353             } else {
1354 2         11 splice @$arrayref, $i, 0, $line;
1355             }
1356 5         1620 $found = 1;
1357 5         12 last;
1358             }
1359             }
1360 6 100       95 if (!$found) {
1361 1         18 error "Cannot find '$add' in file";
1362             }
1363 10         114 };
1364             } else {
1365 16     12   134 $_->{action} = sub { my $arrayref = $_[0]; push @$arrayref, $line };
  12         20  
  12         31  
1366             }
1367             }
1368             }
1369              
1370 41         120 my @match_actions;
1371             my @unless_match_actions;
1372 41         72 for (@changes) {
1373 46 100       3946 if ($_->{unless_match}) {
    100          
1374 29 100       70 if (ref $_->{unless_match} ne 'Regexp') {
1375 26         105 my $rx = '^' . quotemeta($_->{unless_match}) . '$';
1376 26         440 $_->{unless_match} = qr{$rx};
1377             }
1378 29 100       79 if (!$_->{action}) {
1379 1         10 error "action is missing";
1380             }
1381 28 100       109 if (ref $_->{action} ne 'CODE') {
1382 1         17 error "action must be a sub reference";
1383             }
1384 27         67 push @unless_match_actions, $_;
1385             } elsif ($_->{match}) {
1386 16 100       60 if (ref $_->{match} ne 'Regexp') {
1387 3         31 my $rx = '^' . quotemeta($_->{match}) . '$';
1388 3         87 $_->{match} = qr{$rx};
1389             }
1390 16 100       74 my $consequences = ($_->{action}?1:0) + (defined $_->{replace}?1:0) + (defined $_->{delete}?1:0);
    100          
    100          
1391 16 100       46 if ($consequences != 1) {
1392 1         16 error "Exactly one of the following is missing: action, replace, or delete";
1393             }
1394 15 100       76 if ($_->{action}) {
    100          
    50          
1395 3 100       53 if (ref $_->{action} ne 'CODE') {
1396 1         11 error "action must be a sub reference";
1397             }
1398             } elsif (defined $_->{replace}) {
1399             # accept
1400             } elsif (defined $_->{delete}) {
1401             # accept
1402             } else {
1403 0         0 error "FATAL: should never happen";
1404             }
1405 14         37 push @match_actions, $_;
1406             } else {
1407 1         8 error "match or unless_match is missing";
1408             }
1409             }
1410              
1411 36         259 require File::Temp;
1412 36         125 require File::Basename;
1413 36         1707 require File::Copy;
1414 36         8966 my($tmpfh,$tmpfile) = File::Temp::tempfile('doittemp_XXXXXXXX', UNLINK => 1, DIR => File::Basename::dirname($file));
1415 36 50       15448 File::Copy::copy($file, $tmpfile)
1416             or error "failed to copy $file to temporary file $tmpfile: $!";
1417 36         10761 Doit::Util::copy_stat($file, $tmpfile);
1418              
1419 36         4663 require Tie::File;
1420 36 50       74461 tie my @lines, 'Tie::File', $tmpfile
1421             or error "cannot tie file $file: $!";
1422              
1423 36         6091 my $no_of_changes = 0;
1424 36         77 for my $match_action (@match_actions) {
1425 14         210 my $match = $match_action->{match};
1426 14         76 for(my $line_i=0; $line_i<=$#lines; $line_i++) {
1427 44 50       3745 if ($debug) { info "change_file check '$lines[$line_i]' =~ '$match'" }
  0         0  
1428 44 100       129 if ($lines[$line_i] =~ $match) {
1429 18 100       1834 if (exists $match_action->{replace}) {
    100          
1430 9         41 my $replace = $match_action->{replace};
1431 9 50       44 if ($lines[$line_i] ne $replace) {
1432 9         385 push @commands, { msg => "replace '$lines[$line_i]' with '$replace' in '$file'" };
1433 9         335 $lines[$line_i] = $replace;
1434 9         2404 $no_of_changes++;
1435             }
1436             } elsif (exists $match_action->{delete}) {
1437 6 100       46 if ($match_action->{delete}) {
1438 5         19 push @commands, { msg => "delete '$lines[$line_i]' in '$file'" };
1439 5         253 splice @lines, $line_i, 1;
1440 5         2017 $line_i--;
1441 5         21 $no_of_changes++;
1442             }
1443             } else {
1444 3         34 push @commands, { msg => "matched '$match' on line '$lines[$line_i]' in '$file' -> execute action" };
1445 3         127 my $action = $match_action->{action};
1446 3         24 $action->($lines[$line_i]);
1447 3         836 $no_of_changes++;
1448             }
1449             }
1450             }
1451             }
1452 36         796 ITER: for my $unless_match_action (@unless_match_actions) {
1453 27         53 my $match = $unless_match_action->{unless_match};
1454 27         127 for my $line (@lines) {
1455 70 100       7816 if ($line =~ $match) {
1456 8         967 next ITER;
1457             }
1458             }
1459 19         2293 push @commands, { msg => "did not find '$match' in '$file' -> execute action" };
1460 19         30 my $action = $unless_match_action->{action};
1461 19         55 $action->(\@lines);
1462 18         3934 $no_of_changes++;
1463             }
1464              
1465 35         268 untie @lines;
1466 35         1489 close $tmpfh;
1467              
1468 35 100       165 if ($no_of_changes) {
1469             push @commands, {
1470             code => sub {
1471 24 100   24   112 if ($check) {
1472             # XXX maybe it would be good to pass the Doit::Runner object,
1473             # but unfortunately it's not available at this point ---
1474             # maybe the code sub should generally get it as first argument?
1475 2 50       21 $check->($tmpfile)
1476             or error "Check on file $file failed";
1477             }
1478 23 50       1723 rename $tmpfile, $file
1479             or error "Can't rename $tmpfile to $file: $!";
1480             },
1481 25         173 msg => do {
1482 25         40 my $diff;
1483 25 50       38 if (eval { require IPC::Run; 1 }) {
  25         2861  
  0         0  
1484 0 0       0 if (!eval { IPC::Run::run(['diff', '-u', $file, $tmpfile], '>', \$diff); 1 }) {
  0         0  
  0         0  
1485 0 0       0 $diff = "(diff not available" . (!$diff_error_shown++ ? ", error: $@" : "") . ")";
1486             }
1487             } else {
1488 25         89870 $diff = `diff -u '$file' '$tmpfile'`;
1489             }
1490 25         1300 "Final changes as diff:\n$diff";
1491             },
1492             rv => $no_of_changes,
1493             };
1494             }
1495              
1496 35 100       347 if ($no_of_changes) {
1497 25         799 Doit::Commands->new(@commands);
1498             } else {
1499 10         48 Doit::Commands->return_zero;
1500             }
1501             }
1502              
1503             }
1504              
1505             {
1506             package Doit::Commands;
1507             sub new {
1508 568     568   3472 my($class, @commands) = @_;
1509 568         2578 my $self = bless \@commands, $class;
1510 568         8960 $self;
1511             }
1512             sub return_zero {
1513 46     46   120 my $class = shift;
1514 46     45   423 $class->new({ code => sub {}, rv => 0 });
1515             }
1516 610     610   993 sub commands { @{$_[0]} }
  610         2934  
1517             sub set_last_rv {
1518 42     42   131 my($self, $rv) = @_;
1519 42         140 my @commands = $self->commands;
1520 42 50       157 if (@commands) {
1521 42         208 $commands[-1]->{rv} = $rv;
1522             }
1523             }
1524             sub doit {
1525 548     548   1578 my($self) = @_;
1526 548         1305 my $rv;
1527 548         2067 for my $command ($self->commands) {
1528 588 100       1820 if (exists $command->{msg}) {
1529 387         2389 Doit::Log::info($command->{msg});
1530             }
1531 588 100       71018 if (exists $command->{code}) {
1532 554         1651 my $this_rv = $command->{code}->();
1533 512 100       6583 if (exists $command->{rv}) {
1534 442         3007 $rv = $command->{rv};
1535             } else {
1536 70         389 $rv = $this_rv;
1537             }
1538             }
1539             }
1540 506         17798 $rv;
1541             }
1542             sub show {
1543 20     20   71 my($self) = @_;
1544 20         56 my $rv;
1545 20         110 for my $command ($self->commands) {
1546 21 100       168 if (exists $command->{msg}) {
1547 17         244 Doit::Log::info($command->{msg} . " (dry-run)");
1548             }
1549 21 100       4259 if (exists $command->{code}) {
1550 20 100       81 if (exists $command->{rv}) {
1551 17         86 $rv = $command->{rv};
1552             } else {
1553             # Well, in dry-run mode we have no real return value...
1554             }
1555             }
1556             }
1557 20         506 $rv;
1558             }
1559             }
1560              
1561             {
1562             package Doit::Runner;
1563             sub new {
1564 38     38   190 my($class, $Doit, %options) = @_;
1565 38         101 my $dryrun = delete $options{dryrun};
1566 38 50       130 die "Unhandled options: " . join(" ", %options) if %options;
1567 38         668 bless { Doit => $Doit, dryrun => $dryrun, components => [] }, $class;
1568             }
1569 15     15   95 sub is_dry_run { shift->{dryrun} }
1570              
1571 1     1   6 sub can_ipc_run { eval { require IPC::Run; 1 } }
  1         129  
  0         0  
1572              
1573             sub install_generic_cmd {
1574 2     2   280 my($self, $name, @args) = @_;
1575 2         22 $self->{Doit}->install_generic_cmd($name, @args);
1576 2         27 __PACKAGE__->install_cmd($name);
1577             }
1578              
1579             sub install_cmd {
1580 1249     1249   1301 shift; # $class unused
1581 1249         1243 my $cmd = shift;
1582 1249         1723 my $meth = 'cmd_' . $cmd;
1583             my $code = sub {
1584 614     614   289949 my($self, @args) = @_;
1585 614 100       3157 if ($self->{dryrun}) {
1586 22         149 $self->{Doit}->$meth(@args)->show;
1587             } else {
1588 592         5309 $self->{Doit}->$meth(@args)->doit;
1589             }
1590 1249         3402 };
1591 43     43   428 no strict 'refs';
  43         89  
  43         10002  
1592 1249         1414 *{$cmd} = $code;
  1249         3987  
1593             }
1594              
1595             sub add_component {
1596 14     14   1474 my($self, $component_or_module) = @_;
1597 14         22 my $module;
1598 14 100       53 if ($component_or_module =~ /::/) {
1599 3         7 $module = $component_or_module;
1600             } else {
1601 11         49 $module = 'Doit::' . ucfirst($component_or_module);
1602             }
1603              
1604 14         23 for (@{ $self->{components} }) {
  14         45  
1605 6 100       17 return if $_->{module} eq $module;
1606             }
1607              
1608 12 100       702 if (!eval qq{ require $module; 1 }) {
1609 1         9 Doit::Log::error("Cannot load $module: $@");
1610             }
1611 11 50       88 my $o = $module->new
1612             or Doit::Log::error("Error while calling $module->new");
1613 11         47 for my $function ($o->functions) {
1614 35         109 my $fullqual = $module.'::'.$function;
1615             my $code = sub {
1616 322     322   62027 my($self, @args) = @_;
1617 322         4547 $self->$fullqual(@args);
1618 35         110 };
1619 43     43   290 no strict 'refs';
  43         124  
  43         12940  
1620 35         49 *{$function} = $code;
  35         122  
1621             }
1622 11         31 my $mod_file = do {
1623 11         60 (my $relpath = $module) =~ s{::}{/};
1624 11         37 $relpath .= '.pm';
1625             };
1626 11         20 push @{ $self->{components} }, { module => $module, path => $INC{$mod_file}, relpath => $mod_file };
  11         81  
1627              
1628 11 50       135 if ($o->can('add_components')) {
1629 0         0 for my $sub_component ($o->add_components) {
1630 0         0 $self->add_component($sub_component);
1631             }
1632             }
1633             }
1634              
1635             for my $cmd (
1636             qw(chmod chown mkdir rename rmdir symlink unlink utime),
1637             qw(make_path remove_tree), # File::Path
1638             qw(copy move), # File::Copy
1639             qw(run), # IPC::Run
1640             qw(qx info_qx), # qx// and variant which even runs in dry-run mode, both using list syntax
1641             qw(open2 info_open2), # IPC::Open2
1642             qw(open3 info_open3), # IPC::Open3
1643             qw(system info_system), # builtin system with variant
1644             qw(cond_run), # conditional run
1645             qw(touch), # like unix touch
1646             qw(ln_nsf), # like unix ln -nsf
1647             qw(create_file_if_nonexisting), # does the half of touch
1648             qw(write_binary), # like File::Slurper
1649             qw(change_file), # own invention
1650             qw(setenv unsetenv), # $ENV manipulation
1651             ) {
1652             __PACKAGE__->install_cmd($cmd);
1653             }
1654              
1655             sub call_wrapped_method {
1656 6     6   19 my($self, $context, $method, @args) = @_;
1657 6         10 my @ret;
1658 6 100       12 if ($context eq 'a') {
1659 2         4 @ret = eval { $self->$method(@args) };
  2         5  
1660             } else {
1661 4         9 $ret[0] = eval { $self->$method(@args) };
  4         11  
1662             }
1663 6 100       137 if ($@) {
1664 2         19 ('e', $@);
1665             } else {
1666 4         22 ('r', @ret);
1667             }
1668             }
1669              
1670             # XXX call vs. call_with_runner ???
1671             sub call {
1672 6     6   12 my($self, $sub, @args) = @_;
1673 6 50       25 $sub = 'main::' . $sub if $sub !~ /::/;
1674 43     43   306 no strict 'refs';
  43         95  
  43         3851  
1675 6         81 &$sub(@args);
1676             }
1677              
1678             sub call_with_runner {
1679 2     2   1086 my($self, $sub, @args) = @_;
1680 2 50       21 $sub = 'main::' . $sub if $sub !~ /::/;
1681 43     43   325 no strict 'refs';
  43         79  
  43         7025  
1682 2         11 &$sub($self, @args);
1683             }
1684              
1685             # XXX does this belong here?
1686             sub do_ssh_connect {
1687 5     5   2520 my($self, $host, %opts) = @_;
1688 5         16 my $remote = Doit::SSH->do_connect($host, dry_run => $self->is_dry_run, components => $self->{components}, %opts);
1689 0         0 $remote;
1690             }
1691              
1692             # XXX does this belong here?
1693             sub do_sudo {
1694 0     0   0 my($self, %opts) = @_;
1695 0         0 my $sudo = Doit::Sudo->do_connect(dry_run => $self->is_dry_run, components => $self->{components}, %opts);
1696 0         0 $sudo;
1697             }
1698             }
1699              
1700             {
1701             package Doit::RPC;
1702              
1703             require Storable;
1704             require IO::Handle;
1705              
1706 43     43   278 use Doit::Log;
  43         81  
  43         47935  
1707              
1708             sub new {
1709 0     0   0 die "Please use either Doit::RPC::Client, Doit::RPC::Server or Doit::RPC::SimpleServer";
1710             }
1711              
1712 6     6   25 sub runner { shift->{runner} }
1713              
1714             sub receive_data {
1715 14     14   23 my($self) = @_;
1716 14         31 my $fh = $self->{infh};
1717 14         16 my $buf;
1718 14         17075 my $ret = read $fh, $buf, 4;
1719 14 50       103 if (!defined $ret) {
    50          
1720 0         0 die "receive_data failed (getting length): $!";
1721             } elsif (!$ret) {
1722 0         0 return; # eof
1723             }
1724 14         52 my $length = unpack("N", $buf);
1725 14 50       73 read $fh, $buf, $length or die "receive_data failed (getting data): $!";
1726 14         23 @{ Storable::thaw($buf) };
  14         50  
1727             }
1728              
1729             sub send_data {
1730 14     14   41 my($self, @cmd) = @_;
1731 14         27 my $fh = $self->{outfh};
1732 14         51 my $data = Storable::nfreeze(\@cmd);
1733 14         1027 print $fh pack("N", length($data)) . $data;
1734             }
1735              
1736             {
1737             my $done_POSIX_warning;
1738             sub _reap_process {
1739 0     0   0 my($self, $pid) = @_;
1740 0 0       0 return if !defined $pid;
1741 0 0       0 if (eval { require POSIX; defined &POSIX::WNOHANG }) {
  0         0  
  0         0  
1742 0 0       0 if ($self->{debug}) {
1743 0         0 info "Reaping process $pid...";
1744             }
1745 0         0 my $start_time = time;
1746             my $got_pid = Doit::RPC::gentle_retry(
1747             code => sub {
1748 0     0   0 waitpid $pid, &POSIX::WNOHANG;
1749             },
1750             retry_msg_code => sub {
1751 0     0   0 my($seconds) = @_;
1752 0 0       0 if (time - $start_time >= 2) {
1753 0         0 info "can't reap process $pid, sleep for $seconds seconds";
1754             }
1755             },
1756 0         0 fast_sleep => 0.01,
1757             );
1758 0 0       0 if (!$got_pid) {
1759 0         0 warning "Could not reap process $pid...";
1760             }
1761             } else {
1762 0 0       0 if (!$done_POSIX_warning++) {
1763 0         0 warning "Can't require POSIX, cannot reap zombies..."
1764             }
1765             }
1766             }
1767             }
1768              
1769             sub gentle_retry {
1770 0     0   0 my(%opts) = @_;
1771 0   0     0 my $code = delete $opts{code} || die "code is mandatory";
1772 0   0     0 my $tries = delete $opts{tries} || 20;
1773 0   0     0 my $fast_tries = delete $opts{fast_tries} || int($tries/2);
1774 0   0     0 my $slow_sleep = delete $opts{slow_sleep} || 1;
1775 0   0     0 my $fast_sleep = delete $opts{fast_sleep} || 0.1;
1776 0         0 my $retry_msg_code = delete $opts{retry_msg_code};
1777 0         0 my $fail_info_ref = delete $opts{fail_info_ref};
1778 0 0       0 die "Unhandled options: " . join(" ", %opts) if %opts;
1779              
1780 0         0 for my $try (1..$tries) {
1781 0         0 my $ret = $code->(fail_info_ref => $fail_info_ref, try => $try);
1782 0 0       0 return $ret if $ret;
1783 0         0 my $sleep_sub;
1784 0 0 0     0 if ($fast_tries && eval { require Time::HiRes; 1 }) {
  0         0  
  0         0  
1785 0         0 $sleep_sub = \&Time::HiRes::sleep;
1786             } else {
1787 0     0   0 $sleep_sub = sub { sleep $_[0] };
  0         0  
1788             }
1789 0 0 0     0 my $seconds = $try <= $fast_tries && defined &Time::HiRes::sleep ? $fast_sleep : $slow_sleep;
1790 0 0       0 $retry_msg_code->($seconds) if $retry_msg_code;
1791 0         0 $sleep_sub->($seconds);
1792             }
1793              
1794 0         0 undef;
1795             }
1796              
1797             }
1798              
1799             {
1800             package Doit::RPC::Client;
1801             our @ISA = ('Doit::RPC');
1802              
1803             sub new {
1804 1     1   77123 my($class, $infh, $outfh, %options) = @_;
1805              
1806 1         23 my $debug = delete $options{debug};
1807 1         19 my $label = delete $options{label};
1808 1 50       39 die "Unhandled options: " . join(" ", %options) if %options;
1809              
1810 1         63 $outfh->autoflush(1);
1811 1         168 bless {
1812             infh => $infh,
1813             outfh => $outfh,
1814             label => $label,
1815             debug => $debug,
1816             }, $class;
1817             }
1818              
1819             # Call for every command on client
1820             sub call_remote {
1821 7     7   5545 my($self, @args) = @_;
1822 7 100       41 my $context = wantarray ? 'a' : 's'; # XXX more possible context (void...)?
1823 7         26 $self->send_data($context, @args);
1824 7         40 my($rettype, @ret) = $self->receive_data(@args);
1825 7 100 66     201 if (defined $rettype && $rettype eq 'e') {
    50 33        
1826 2         18 die $ret[0];
1827             } elsif (defined $rettype && $rettype eq 'r') {
1828 5 100       10 if ($context eq 'a') {
1829 2         19 return @ret;
1830             } else {
1831 3         14 return $ret[0];
1832             }
1833             } else {
1834 0 0       0 die "Unexpected return type " . (defined $self->{label} ? "in connection '$self->{label}' " : "") . (defined $rettype ? "'$rettype'" : "") . " (should be 'e' or 'r')";
    0          
1835             }
1836             }
1837             }
1838              
1839             {
1840             package Doit::RPC::Server;
1841             our @ISA = ('Doit::RPC');
1842              
1843             sub new {
1844 0     0   0 my($class, $runner, $sockpath, %options) = @_;
1845              
1846 0         0 my $debug = delete $options{debug};
1847 0         0 my $excl = delete $options{excl};
1848 0 0       0 die "Unhandled options: " . join(" ", %options) if %options;
1849              
1850 0         0 bless {
1851             runner => $runner,
1852             sockpath => $sockpath,
1853             debug => $debug,
1854             excl => $excl,
1855             }, $class;
1856             }
1857              
1858             sub run {
1859 0     0   0 my($self) = @_;
1860              
1861 0         0 require IO::Socket::UNIX;
1862 0         0 IO::Socket::UNIX->VERSION('1.18'); # autoflush
1863 0         0 IO::Socket::UNIX->import(qw(SOCK_STREAM));
1864 43     43   19995 use IO::Select;
  43         60888  
  43         42447  
1865              
1866 0         0 my $d;
1867 0 0       0 if ($self->{debug}) {
1868             $d = sub ($) {
1869 0     0   0 Doit::Log::info("WORKER: $_[0]");
1870 0         0 };
1871             } else {
1872 0     0   0 $d = sub ($) { };
1873             }
1874              
1875 0         0 $d->("Start worker ($$)...");
1876 0         0 my $sockpath = $self->{sockpath};
1877 0 0 0     0 if (!$self->{excl} && -e $sockpath) {
1878 0         0 $d->("unlink socket $sockpath");
1879 0         0 unlink $sockpath;
1880             }
1881 0 0       0 my $sock = IO::Socket::UNIX->new(
1882             Type => SOCK_STREAM(),
1883             Local => $sockpath,
1884             Listen => 1,
1885             ) or die "WORKER: Can't create socket: $!";
1886 0         0 $d->("socket was created");
1887              
1888 0         0 my $sel = IO::Select->new($sock);
1889 0         0 $d->("waiting for client");
1890 0         0 my @ready = $sel->can_read();
1891 0 0       0 die "WORKER: unexpected filehandle @ready" if $ready[0] != $sock;
1892 0         0 $d->("accept socket");
1893 0         0 my $fh = $sock->accept;
1894 0         0 $self->{infh} = $self->{outfh} = $fh;
1895 0         0 while () {
1896 0         0 $d->(" waiting for line from comm");
1897 0         0 my($context, @data) = $self->receive_data;
1898 0 0       0 if (!defined $context) {
    0          
1899 0         0 $d->(" got eof");
1900 0         0 $fh->close;
1901 0         0 return;
1902             } elsif ($data[0] =~ m{^exit$}) {
1903 0         0 $d->(" got exit command");
1904 0         0 $self->send_data('r', 'bye-bye');
1905 0         0 $fh->close;
1906 0         0 return;
1907             }
1908 0         0 $d->(" calling method $data[0]");
1909 0         0 my($rettype, @ret) = $self->runner->call_wrapped_method($context, @data);
1910 0         0 $d->(" sending result back");
1911 0         0 $self->send_data($rettype, @ret);
1912             }
1913             }
1914              
1915             }
1916              
1917             {
1918             package Doit::RPC::SimpleServer;
1919             our @ISA = ('Doit::RPC');
1920            
1921             sub new {
1922 1     1   6 my($class, $runner, $infh, $outfh, %options) = @_;
1923 1         2 my $debug = delete $options{debug};
1924 1 50       4 die "Unhandled options: " . join(" ", %options) if %options;
1925              
1926 1 50       10 $infh = \*STDIN if !$infh;
1927 1 50       3 $outfh = \*STDOUT if !$outfh;
1928 1         17 $outfh->autoflush(1);
1929 1         85 bless {
1930             runner => $runner,
1931             infh => $infh,
1932             outfh => $outfh,
1933             debug => $debug,
1934             }, $class;
1935             }
1936              
1937             sub run {
1938 1     1   4 my $self = shift;
1939 1         2 while() {
1940 7         43 my($context, @data) = $self->receive_data;
1941 7 50       208 if (!defined $context) {
    100          
1942 0         0 return;
1943             } elsif ($data[0] =~ m{^exit$}) {
1944 1         6 $self->send_data('r', 'bye-bye');
1945 1         5 return;
1946             }
1947 6 50       134 open my $oldout, ">&STDOUT" or die $!;
1948 6         12 if (Doit::IS_WIN) {
1949             open STDOUT, '>', 'CON:' or die $!; # XXX????
1950             } else {
1951 6 50       305 open STDOUT, '>', "/dev/stderr" or die $!; # XXX????
1952             }
1953 6         32 my($rettype, @ret) = $self->runner->call_wrapped_method($context, @data);
1954 6 50       209 open STDOUT, ">&", $oldout or die $!;
1955 6         31 $self->send_data($rettype, @ret);
1956             }
1957             }
1958             }
1959              
1960             {
1961             package Doit::_AnyRPCImpl;
1962             sub call_remote {
1963 0     0   0 my($self, @args) = @_;
1964 0         0 $self->{rpc}->call_remote(@args);
1965             }
1966              
1967             our $AUTOLOAD;
1968             sub AUTOLOAD {
1969 0     0   0 (my $method = $AUTOLOAD) =~ s{.*::}{};
1970 0         0 my $self = shift;
1971 0         0 $self->call_remote($method, @_); # XXX or use goto?
1972             }
1973              
1974             sub _can_LANS {
1975 0     0   0 require POSIX;
1976 0 0       0 $^O eq 'linux' && (POSIX::uname())[2] !~ m{^([01]\.|2\.[01]\.)} # osvers >= 2.2, earlier versions did not have LANS
1977             }
1978              
1979             }
1980              
1981             {
1982             package Doit::_ScriptTools;
1983              
1984             sub add_components {
1985 0     0   0 my(@components) = @_;
1986 0         0 q|for my $component_module (qw(| . join(" ", map { qq{$_->{module}} } @components) . q|)) { $d->add_component($component_module) } |;
  0         0  
1987             }
1988              
1989             sub self_require (;$) {
1990 0     0   0 my $realscript = shift;
1991 0 0       0 if (!defined $realscript) { $realscript = $0 }
  0         0  
1992 0 0       0 if ($realscript ne '-e') { # not a oneliner
1993 0         0 q{$ENV{DOIT_IN_REMOTE} = 1; } .
1994             q{require "} . File::Basename::basename($realscript) . q{"; };
1995             } else {
1996 0         0 q{use Doit; };
1997             }
1998             }
1999             }
2000              
2001             {
2002             package Doit::Sudo;
2003              
2004             our @ISA = ('Doit::_AnyRPCImpl');
2005              
2006 43     43   357 use Doit::Log;
  43         111  
  43         22465  
2007              
2008             my $socket_count = 0;
2009              
2010             sub do_connect {
2011 0     0   0 my($class, %opts) = @_;
2012 0 0       0 my @sudo_opts = @{ delete $opts{sudo_opts} || [] };
  0         0  
2013 0         0 my $dry_run = delete $opts{dry_run};
2014 0         0 my $debug = delete $opts{debug};
2015 0 0       0 my @components = @{ delete $opts{components} || [] };
  0         0  
2016 0   0     0 my $perl = delete $opts{perl} || $^X;
2017 0 0       0 die "Unhandled options: " . join(" ", %opts) if %opts;
2018              
2019 0         0 my $self = bless { }, $class;
2020              
2021 0         0 require File::Basename;
2022 0         0 require IPC::Open2;
2023 0         0 require POSIX;
2024 0         0 require Symbol;
2025              
2026             # Socket pathname, make it possible to find out
2027             # old outdated sockets easily by including a
2028             # timestamp. Also need to maintain a $socket_count,
2029             # if the same script opens multiple sockets quickly.
2030 0         0 my $sock_path = "/tmp/." . join(".", "doit", "sudo", POSIX::strftime("%Y%m%d_%H%M%S", gmtime), $<, $$, (++$socket_count)) . ".sock";
2031              
2032             # Make sure password has to be entered only once (if at all)
2033             # Using 'sudo --validate' would be more correct, however,
2034             # mysterious "sudo: ignoring time stamp from the future"
2035             # errors may happen every now and then. Seen on a
2036             # debian/jessie system, possibly related to
2037             # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=762465
2038             {
2039 0         0 my @cmd = ('sudo', @sudo_opts, 'true');
  0         0  
2040 0         0 system @cmd;
2041 0 0       0 if ($? != 0) {
2042             # Possible cases:
2043             # - sudo is not installed
2044             # - sudo authentication is not possible or user entered wrong password
2045             # - true is not installed (hopefully this never happens on Unix systems)
2046 0         0 error "Command '@cmd' failed";
2047             }
2048             }
2049              
2050             # On linux use Linux Abstract Namespace Sockets ---
2051             # invisible and automatically cleaned up. See man 7 unix.
2052 0 0       0 my $LANS_PREFIX = $class->_can_LANS ? '\0' : '';
2053              
2054             # Run the server
2055 0 0       0 my @cmd_worker =
    0          
    0          
2056             (
2057             'sudo', @sudo_opts, $perl, "-I".File::Basename::dirname(__FILE__), "-I".File::Basename::dirname($0), "-e",
2058             Doit::_ScriptTools::self_require() .
2059             q{my $d = Doit->init; } .
2060             Doit::_ScriptTools::add_components(@components) .
2061             q{Doit::RPC::Server->new($d, "} . $LANS_PREFIX . $sock_path . q{", excl => 1, debug => } . ($debug?1:0) . q{)->run();} .
2062             ($LANS_PREFIX ? '' : q . $sock_path . q<" }>), # cleanup socket file, except if Linux Abstract Namespace Sockets are used
2063             "--", ($dry_run? "--dry-run" : ())
2064             );
2065 0         0 my $worker_pid = fork;
2066 0 0       0 if (!defined $worker_pid) {
    0          
2067 0         0 die "fork failed: $!";
2068             } elsif ($worker_pid == 0) {
2069 0 0       0 warn "worker perl cmd: @cmd_worker\n" if $debug;
2070 0         0 exec @cmd_worker;
2071 0         0 die "Failed to run '@cmd_worker': $!";
2072             }
2073              
2074             # Run the client --- must also run under root for socket
2075             # access.
2076 0         0 my($in, $out);
2077 0         0 my @cmd_comm = ('sudo', @sudo_opts, $perl, "-I".File::Basename::dirname(__FILE__), "-MDoit", "-e",
2078             q{Doit::Comm->comm_to_sock("} . $LANS_PREFIX . $sock_path . q{", debug => shift)}, !!$debug);
2079 0 0       0 warn "comm perl cmd: @cmd_comm\n" if $debug;
2080 0         0 my $comm_pid = IPC::Open2::open2($out, $in, @cmd_comm);
2081 0         0 $self->{rpc} = Doit::RPC::Client->new($out, $in, label => "sudo:", debug => $debug);
2082              
2083 0         0 $self;
2084             }
2085              
2086       0     sub DESTROY { }
2087              
2088             }
2089              
2090             {
2091             package Doit::SSH;
2092              
2093             our @ISA = ('Doit::_AnyRPCImpl');
2094              
2095 43     43   304 use Doit::Log;
  43         123  
  43         19891  
2096              
2097             sub do_connect {
2098 5     5   29 require File::Basename;
2099 5         709 require Net::OpenSSH;
2100 0           require FindBin;
2101 0           my($class, $ssh_or_host, %opts) = @_;
2102 0           my $dry_run = delete $opts{dry_run};
2103 0 0         my @components = @{ delete $opts{components} || [] };
  0            
2104 0           my $debug = delete $opts{debug};
2105 0           my $as = delete $opts{as};
2106 0           my $forward_agent = delete $opts{forward_agent};
2107 0           my $tty = delete $opts{tty};
2108 0           my $port = delete $opts{port};
2109 0           my $master_opts = delete $opts{master_opts};
2110 0           my $dest_os = delete $opts{dest_os};
2111 0 0         $dest_os = 'unix' if !defined $dest_os;
2112 0   0       my $put_to_remote = delete $opts{put_to_remote} || 'rsync_put'; # XXX ideally this should be determined automatically
2113 0 0         $put_to_remote =~ m{^(rsync_put|scp_put)$}
2114             or error "Valid values for put_to_remote: rsync_put or scp_put";
2115 0   0       my $perl = delete $opts{perl} || 'perl';
2116 0           my $umask = delete $opts{umask};
2117 0 0 0       if (defined $umask && $umask !~ m{^\d+$}) {
2118 0           error "The umask '$umask' does not look correct, it should be a (possibly octal) number";
2119             }
2120 0           my $bootstrap = delete $opts{bootstrap};
2121 0 0         error "Unhandled options: " . join(" ", %opts) if %opts;
2122              
2123 0           my $self = bless { debug => $debug }, $class;
2124 0 0         my %ssh_run_opts = (
    0          
2125             ($forward_agent ? (forward_agent => $forward_agent) : ()),
2126             ($tty ? (tty => $tty) : ()),
2127             );
2128 0 0         my %ssh_new_opts = (
    0          
2129             ($forward_agent ? (forward_agent => $forward_agent) : ()),
2130             ($master_opts ? (master_opts => $master_opts) : ()),
2131             );
2132              
2133 0           my($host, $ssh);
2134 0 0         if (UNIVERSAL::isa($ssh_or_host, 'Net::OpenSSH')) {
2135 0           $ssh = $ssh_or_host;
2136 0           $host = $ssh->get_host; # XXX what about username/port/...?
2137             } else {
2138 0           $host = $ssh_or_host;
2139 0           $ssh = Net::OpenSSH->new($host, %ssh_new_opts);
2140 0 0         $ssh->error
2141             and error "Connection error to $host: " . $ssh->error;
2142             }
2143 0           $self->{ssh} = $ssh;
2144              
2145 0 0 0       if (($bootstrap||'') eq 'perl') {
2146 0           require Doit::Bootstrap;
2147 0           Doit::Bootstrap::_bootstrap_perl($self, dry_run => $dry_run);
2148             }
2149              
2150             {
2151 0           my $remote_cmd;
  0            
2152 0 0         if ($dest_os eq 'MSWin32') {
2153 0           $remote_cmd = 'if not exist .doit\lib\ mkdir .doit\lib';
2154             } else {
2155 0           $remote_cmd = "[ ! -d .doit/lib ] && mkdir -p .doit/lib";
2156             }
2157 0 0         if ($debug) {
2158 0           info "Running '$remote_cmd' on remote";
2159             }
2160 0           $ssh->system(\%ssh_run_opts, $remote_cmd);
2161             }
2162 0 0         if ($FindBin::RealScript ne '-e') {
2163 43     43   322 no warnings 'once';
  43         72  
  43         60918  
2164 0           $ssh->$put_to_remote({verbose => $debug}, "$FindBin::RealBin/$FindBin::RealScript", ".doit/"); # XXX verbose?
2165             }
2166 0           $ssh->$put_to_remote({verbose => $debug}, __FILE__, ".doit/lib/");
2167             {
2168 0           my %seen_dir;
  0            
2169 0 0         for my $component (
2170             @components,
2171             ( # add additional RPC components
2172             $dest_os ne 'MSWin32' ? () :
2173             do {
2174 0           (my $srcpath = __FILE__) =~ s{\.pm}{/WinRPC.pm};
2175 0           {relpath => "Doit/WinRPC.pm", path => $srcpath},
2176             }
2177             )
2178             ) {
2179 0           my $from = $component->{path};
2180 0           my $to = $component->{relpath};
2181 0           my $full_target = ".doit/lib/$to";
2182 0           my $target_dir = File::Basename::dirname($full_target);
2183 0 0         if (!$seen_dir{$target_dir}) {
2184 0           my $remote_cmd;
2185 0 0         if ($dest_os eq 'MSWin32') {
2186 0           (my $win_target_dir = $target_dir) =~ s{/}{\\}g;
2187 0           $remote_cmd = "if not exist $win_target_dir mkdir $win_target_dir"; # XXX is this equivalent to mkdir -p?
2188             } else {
2189 0           $remote_cmd = "[ ! -d $target_dir ] && mkdir -p $target_dir";
2190             }
2191 0           $ssh->system(\%ssh_run_opts, $remote_cmd);
2192 0           $seen_dir{$target_dir} = 1;
2193             }
2194 0           $ssh->$put_to_remote({verbose => $debug}, $from, $full_target);
2195             }
2196             }
2197              
2198             my $sock_path = (
2199             $dest_os eq 'MSWin32'
2200             ? join("-", "doit", "ssh", POSIX::strftime("%Y%m%d_%H%M%S", gmtime), int(rand(99999999)))
2201 0 0         : do {
2202 0           require POSIX;
2203 0           "/tmp/." . join(".", "doit", "ssh", POSIX::strftime("%Y%m%d_%H%M%S", gmtime), $<, $$, int(rand(99999999))) . ".sock";
2204             }
2205             );
2206              
2207 0           my @cmd;
2208 0 0         if (defined $as) {
2209 0 0         if ($as eq 'root') {
2210 0           @cmd = ('sudo');
2211             } else {
2212 0           @cmd = ('sudo', '-u', $as);
2213             }
2214             } # XXX add ssh option -t? for password input?
2215              
2216 0           my @cmd_worker;
2217 0 0         if ($dest_os eq 'MSWin32') {
2218 0 0         @cmd_worker =
    0          
2219             (
2220             # @cmd not used here (no sudo)
2221             $perl, "-I.doit", "-I.doit\\lib", "-e",
2222             Doit::_ScriptTools::self_require($FindBin::RealScript) .
2223             q{use Doit::WinRPC; } .
2224             q{my $d = Doit->init; } .
2225             Doit::_ScriptTools::add_components(@components) .
2226             # XXX server cleanup? on signals? on END?
2227             q{Doit::WinRPC::Server->new($d, "} . $sock_path . q{", debug => } . ($debug?1:0).q{)->run();},
2228             "--", ($dry_run? "--dry-run" : ())
2229             );
2230 0           @cmd_worker = Doit::Win32Util::win32_quote_list(@cmd_worker);
2231             } else {
2232 0 0         @cmd_worker =
    0          
    0          
2233             (
2234             @cmd, $perl, "-I.doit", "-I.doit/lib", "-e",
2235             (defined $umask ? qq{umask $umask; } : q{}) .
2236             Doit::_ScriptTools::self_require($FindBin::RealScript) .
2237             q{my $d = Doit->init; } .
2238             Doit::_ScriptTools::add_components(@components) .
2239             q . $sock_path . q<" }> .
2240             q<$SIG{PIPE} = \&_server_cleanup; > .
2241             q .
2242             q{Doit::RPC::Server->new($d, "} . $sock_path . q{", excl => 1, debug => } . ($debug?1:0).q{)->run();},
2243             "--", ($dry_run? "--dry-run" : ())
2244             );
2245             }
2246 0 0         warn "remote perl cmd: @cmd_worker\n" if $debug;
2247 0           my $worker_pid = $ssh->spawn(\%ssh_run_opts, @cmd_worker); # XXX what to do with worker pid?
2248 0           $self->{worker_pid} = $worker_pid;
2249              
2250 0           my @cmd_comm;
2251 0 0         if ($dest_os eq 'MSWin32') {
2252 0           @cmd_comm =
2253             ($perl, "-I.doit\\lib", "-MDoit", "-MDoit::WinRPC", "-e",
2254             q{Doit::WinRPC::Comm->new("} . $sock_path . q{", debug => shift)->run},
2255             !!$debug,
2256             );
2257 0           @cmd_comm = Doit::Win32Util::win32_quote_list(@cmd_comm);
2258             } else {
2259 0           @cmd_comm =
2260             (
2261             @cmd, $perl, "-I.doit/lib", "-MDoit", "-e",
2262             q{Doit::Comm->comm_to_sock("} . $sock_path . q{", debug => shift);},
2263             !!$debug,
2264             );
2265             }
2266 0 0         warn "comm perl cmd: @cmd_comm\n" if $debug;
2267 0           my($out, $in, $comm_pid) = $ssh->open2(@cmd_comm);
2268 0           $self->{comm_pid} = $comm_pid;
2269 0           $self->{rpc} = Doit::RPC::Client->new($in, $out, label => "ssh:$host", debug => $debug);
2270              
2271 0           $self;
2272             }
2273              
2274 0     0     sub ssh { $_[0]->{ssh} }
2275              
2276             sub DESTROY {
2277 0     0     my $self = shift;
2278 0           local $?; # XXX Net::OpenSSH::_waitpid sets $?=0
2279 0 0         if ($self->{ssh}) {
2280 0 0         $self->{ssh}->disconnect if $self->{ssh}->can('disconnect');
2281 0           delete $self->{ssh};
2282             }
2283 0 0         if ($self->{rpc}) {
2284 0           $self->{rpc}->_reap_process($self->{comm_pid});
2285 0           $self->{rpc}->_reap_process($self->{worker_pid});
2286             }
2287             }
2288              
2289             }
2290              
2291             {
2292             package Doit::Comm;
2293              
2294             sub comm_to_sock {
2295 0     0     my(undef, $peer, %options) = @_;
2296 0 0         die "Please specify path to unix domain socket" if !defined $peer;
2297 0           my $debug = delete $options{debug};
2298 0 0         die "Unhandled options: " . join(" ", %options) if %options;
2299              
2300 0           my $infh = \*STDIN;
2301 0           my $outfh = \*STDOUT;
2302              
2303 0           require IO::Socket::UNIX;
2304 0           IO::Socket::UNIX->VERSION('1.18'); # autoflush
2305 0           IO::Socket::UNIX->import(qw(SOCK_STREAM));
2306              
2307 0           my $d;
2308 0 0         if ($debug) {
2309             $d = sub ($) {
2310 0     0     Doit::Log::info("COMM: $_[0]");
2311 0           };
2312             } else {
2313 0     0     $d = sub ($) { };
2314             }
2315              
2316 0           $d->("Start communication process (pid $$)...");
2317              
2318 0           my $tries = 20;
2319 0           my $sock_err;
2320             my $sock = Doit::RPC::gentle_retry(
2321             code => sub {
2322 0     0     my(%opts) = @_;
2323 0           my $sock = IO::Socket::UNIX->new(
2324             Type => SOCK_STREAM(),
2325             Peer => $peer,
2326             );
2327 0 0         return $sock if $sock;
2328 0           ${$opts{fail_info_ref}} = "(peer=$peer, errno=$!)";
  0            
2329 0           undef;
2330             },
2331             retry_msg_code => sub {
2332 0     0     my($seconds) = @_;
2333 0           $d->("can't connect, sleep for $seconds seconds");
2334             },
2335 0           fail_info_ref => \$sock_err,
2336             );
2337 0 0         if (!$sock) {
2338 0           die "COMM: Can't connect to socket (after $tries retries) $sock_err";
2339             }
2340 0           $d->("socket to worker was created");
2341              
2342             my $get_and_send = sub ($$$$) {
2343 0     0     my($infh, $outfh, $inname, $outname) = @_;
2344              
2345 0           my $length_buf;
2346 0 0         read $infh, $length_buf, 4 or die "COMM: reading data from $inname failed (getting length): $!";
2347 0           my $length = unpack("N", $length_buf);
2348 0           $d->("starting getting data from $inname, length is $length");
2349 0           my $buf = '';
2350 0           while (1) {
2351 0           my $got = read($infh, $buf, $length, length($buf));
2352 0 0         last if $got == $length;
2353 0 0         die "COMM: Unexpected error $got > $length" if $got > $length;
2354 0           $length -= $got;
2355             }
2356 0           $d->("finished reading data from $inname");
2357              
2358 0           print $outfh $length_buf;
2359 0           print $outfh $buf;
2360 0           $d->("finished sending data to $outname");
2361 0           };
2362              
2363 0           $outfh->autoflush(1);
2364 0           $d->("about to enter loop");
2365 0           while () {
2366 0 0         $d->("seen eof from local"), last if eof($infh);
2367 0           $get_and_send->($infh, $sock, "local", "worker");
2368 0           $get_and_send->($sock, $outfh, "worker", "local");
2369             }
2370 0           $d->("exited loop");
2371             }
2372              
2373             }
2374              
2375             1;
2376              
2377             __END__