]>
Commit | Line | Data |
---|---|---|
d9ba239d TL |
1 | package Proxmox::Sys::Command; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
7b0a64c1 | 6 | use Carp; |
d9ba239d TL |
7 | use IO::File; |
8 | use IPC::Open3; | |
9 | use IO::Select; | |
10 | use String::ShellQuote; | |
499af19f | 11 | use POSIX ":sys_wait_h"; |
d9ba239d | 12 | |
026620be | 13 | use Proxmox::Install::ISOEnv; |
d9ba239d | 14 | use Proxmox::Log; |
499af19f | 15 | use Proxmox::UI; |
d9ba239d TL |
16 | |
17 | use base qw(Exporter); | |
18 | our @EXPORT_OK = qw(run_command syscmd); | |
19 | ||
20 | my sub shellquote { | |
21 | my $str = shift; | |
22 | return String::ShellQuote::shell_quote($str); | |
23 | } | |
24 | ||
25 | my sub cmd2string { | |
26 | my ($cmd) = @_; | |
27 | ||
28 | die "no arguments" if !$cmd; | |
29 | return $cmd if !ref($cmd); | |
30 | ||
31 | my $quoted_args = [ map { shellquote($_) } $cmd->@* ]; | |
32 | ||
33 | return join (' ', $quoted_args->@*); | |
34 | } | |
35 | ||
152bbef4 CH |
36 | # Safely for the (sub-)process specified by $pid to exit, using a timeout. |
37 | # | |
38 | # When kill => 1 is set, at first a TERM-signal is sent to the process before | |
39 | # checking if it exited. | |
40 | # If that fails, KILL is sent to process and then up to timeout => $timeout | |
41 | # seconds (default: 5) are waited for the process to exit. | |
42 | # | |
43 | # On sucess, the exitcode of the process is returned, otherwise `undef` (aka. | |
44 | # the process was unkillable). | |
45 | my sub wait_for_process { | |
46 | my ($pid, %params) = @_; | |
47 | ||
48 | kill('TERM', $pid) if $params{kill}; | |
49 | ||
50 | my $terminated = waitpid($pid, WNOHANG); | |
51 | return $? if $terminated > 0; | |
52 | ||
53 | kill('KILL', $pid) if $params{kill}; | |
54 | ||
55 | my $timeout = $params{timeout} // 5; | |
56 | for (1 .. $timeout) { | |
57 | $terminated = waitpid($pid, WNOHANG); | |
58 | return $? if $terminated > 0; | |
59 | sleep(1); | |
60 | } | |
61 | ||
62 | log_warn("failed to kill child pid $pid, probably stuck in D-state?\n"); | |
63 | ||
64 | # We tried our best, better let the child hang in the back then completely | |
65 | # blocking installer progress .. it's a rather short-lived environment anyway | |
66 | } | |
67 | ||
d9ba239d TL |
68 | sub syscmd { |
69 | my ($cmd) = @_; | |
70 | ||
71 | return run_command($cmd, undef, undef, 1); | |
72 | } | |
73 | ||
152bbef4 CH |
74 | # Runs a command an a subprocess, properly handling IO via piping, cleaning up and passing back the |
75 | # exit code. | |
76 | # | |
77 | # If $cmd contains a pipe |, the command will be executed inside a bash shell. | |
78 | # If $cmd contains 'chpasswd', the input will be specially quoted for that purpose. | |
79 | # | |
80 | # Arguments: | |
81 | # * $cmd - The command to run, either a single string or array with individual arguments | |
82 | # * $func - Logging subroutine to call, receives both stdout and stderr | |
83 | # * $input - Stdin contents for the spawned subprocess | |
84 | # * $noout - Whether to append any process output to the return value | |
d9ba239d TL |
85 | sub run_command { |
86 | my ($cmd, $func, $input, $noout) = @_; | |
87 | ||
88 | my $cmdstr; | |
89 | if (!ref($cmd)) { | |
90 | $cmdstr = $cmd; | |
91 | if ($cmd =~ m/|/) { | |
92 | # see 'man bash' for option pipefail | |
93 | $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmd" ]; | |
94 | } else { | |
95 | $cmd = [ $cmd ]; | |
96 | } | |
97 | } else { | |
98 | $cmdstr = cmd2string($cmd); | |
99 | } | |
100 | ||
101 | my $cmdtxt; | |
102 | if ($input && ($cmdstr !~ m/chpasswd/)) { | |
103 | $cmdtxt = "# $cmdstr <<EOD\n$input"; | |
104 | chomp $cmdtxt; | |
105 | $cmdtxt .= "\nEOD\n"; | |
106 | } else { | |
107 | $cmdtxt = "# $cmdstr\n"; | |
108 | } | |
109 | ||
110 | if (is_test_mode()) { | |
111 | print $cmdtxt; | |
112 | STDOUT->flush(); | |
113 | } | |
114 | log_info($cmdtxt); | |
115 | ||
116 | my ($reader, $writer, $error) = (IO::File->new(), IO::File->new(), IO::File->new()); | |
117 | ||
118 | my $orig_pid = $$; | |
119 | ||
120 | my $pid = eval { open3($writer, $reader, $error, @$cmd) || die $!; }; | |
121 | my $err = $@; | |
122 | ||
123 | if ($orig_pid != $$) { # catch exec errors | |
124 | POSIX::_exit (1); | |
125 | kill ('KILL', $$); | |
126 | } | |
127 | die $err if $err; | |
128 | ||
129 | print $writer $input if defined $input; | |
130 | close $writer; | |
131 | ||
132 | my $select = IO::Select->new(); | |
133 | $select->add($reader); | |
134 | $select->add($error); | |
135 | ||
136 | my ($ostream, $logout) = ('', '', ''); | |
7a95f387 | 137 | my $caught_sig; |
d9ba239d TL |
138 | |
139 | while ($select->count) { | |
140 | my @handles = $select->can_read (0.2); | |
141 | ||
7a95f387 CH |
142 | # If we catch a signal, stop processing & clean up |
143 | if ($!{EINTR}) { | |
144 | $caught_sig = 1; | |
145 | last; | |
146 | } | |
147 | ||
499af19f | 148 | Proxmox::UI::process_events(); |
d9ba239d TL |
149 | |
150 | next if !scalar (@handles); # timeout | |
151 | ||
152 | foreach my $h (@handles) { | |
153 | my $buf = ''; | |
154 | my $count = sysread ($h, $buf, 4096); | |
155 | if (!defined ($count)) { | |
156 | my $err = $!; | |
152bbef4 | 157 | wait_for_process($pid, kill => 1); |
d9ba239d TL |
158 | die "command '$cmd' failed: $err"; |
159 | } | |
160 | $select->remove($h) if !$count; | |
161 | if ($h eq $reader) { | |
162 | $ostream .= $buf if !($noout || $func); | |
163 | $logout .= $buf; | |
164 | while ($logout =~ s/^([^\010\r\n]*)(\r|\n|(\010)+|\r\n)//s) { | |
165 | my $line = $1; | |
166 | $func->($line) if $func; | |
167 | } | |
168 | ||
169 | } elsif ($h eq $error) { | |
170 | $ostream .= $buf if !($noout || $func); | |
171 | } | |
172 | print $buf; | |
173 | STDOUT->flush(); | |
174 | log_info($buf); | |
175 | } | |
176 | } | |
177 | ||
178 | &$func($logout) if $func; | |
179 | ||
7a95f387 | 180 | my $ec = wait_for_process($pid, kill => $caught_sig); |
d9ba239d | 181 | |
152bbef4 CH |
182 | # behave like standard system(); returns -1 in case of errors too |
183 | return ($ec // -1) if $noout; | |
d9ba239d | 184 | |
152bbef4 CH |
185 | if (!defined($ec)) { |
186 | # Don't fail completely here to let the install continue | |
187 | warn "command '$cmdstr' failed to exit properly\n"; | |
188 | } elsif ($ec == -1) { | |
7b0a64c1 | 189 | croak "command '$cmdstr' failed to execute\n"; |
152bbef4 | 190 | } elsif (my $sig = ($ec & 127)) { |
7b0a64c1 | 191 | croak "command '$cmdstr' failed - got signal $sig\n"; |
152bbef4 | 192 | } elsif (my $exitcode = ($ec >> 8)) { |
7b0a64c1 | 193 | croak "command '$cmdstr' failed with exit code $exitcode"; |
d9ba239d TL |
194 | } |
195 | ||
196 | return $ostream; | |
197 | } | |
198 | ||
199 | # forks and runs the provided coderef in the child | |
200 | # do not use syscmd or run_command as both confuse the GTK mainloop if | |
201 | # run from a child process | |
202 | sub run_in_background { | |
203 | my ($cmd) = @_; | |
204 | ||
205 | my $pid = fork() // die "fork failed: $!\n"; | |
206 | if (!$pid) { | |
207 | eval { $cmd->(); }; | |
208 | if (my $err = $@) { | |
209 | warn "run_in_background error: $err\n"; | |
210 | POSIX::_exit(1); | |
211 | } | |
212 | POSIX::_exit(0); | |
213 | } | |
214 | } | |
215 | ||
216 | 1; |