File Coverage

blib/lib/Doit.pm
Criterion Covered Total %
statement 969 1377 70.3
branch 477 778 61.3
condition 107 226 47.3
subroutine 152 185 82.1
pod 8 48 16.6
total 1713 2614 65.5


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