File Coverage

blib/lib/Doit.pm
Criterion Covered Total %
statement 1041 1395 74.6
branch 509 790 64.4
condition 117 233 50.2
subroutine 155 182 85.1
pod 9 49 18.3
total 1831 2649 69.1


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