File Coverage

blib/lib/Doit.pm
Criterion Covered Total %
statement 959 1358 70.6
branch 471 760 61.9
condition 104 217 47.9
subroutine 152 185 82.1
pod 8 48 16.6
total 1694 2568 65.9


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