File Coverage

blib/lib/Doit.pm
Criterion Covered Total %
statement 1086 1519 71.4
branch 525 844 62.2
condition 121 239 50.6
subroutine 164 196 83.6
pod 9 50 18.0
total 1905 2848 66.8


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