File Coverage

blib/lib/Doit.pm
Criterion Covered Total %
statement 981 1383 70.9
branch 482 782 61.6
condition 107 226 47.3
subroutine 152 182 83.5
pod 8 48 16.6
total 1730 2621 66.0


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