File Coverage

blib/lib/Doit.pm
Criterion Covered Total %
statement 1129 1567 72.0
branch 550 880 62.5
condition 132 257 51.3
subroutine 166 198 83.8
pod 9 51 17.6
total 1986 2953 67.2


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