]> git.proxmox.com Git - pve-installer.git/blame - Proxmox/Sys/Command.pm
sys: command: handle EINTR in run_command()
[pve-installer.git] / Proxmox / Sys / Command.pm
CommitLineData
d9ba239d
TL
1package Proxmox::Sys::Command;
2
3use strict;
4use warnings;
5
7b0a64c1 6use Carp;
d9ba239d
TL
7use IO::File;
8use IPC::Open3;
9use IO::Select;
10use String::ShellQuote;
499af19f 11use POSIX ":sys_wait_h";
d9ba239d 12
026620be 13use Proxmox::Install::ISOEnv;
d9ba239d 14use Proxmox::Log;
499af19f 15use Proxmox::UI;
d9ba239d
TL
16
17use base qw(Exporter);
18our @EXPORT_OK = qw(run_command syscmd);
19
20my sub shellquote {
21 my $str = shift;
22 return String::ShellQuote::shell_quote($str);
23}
24
25my 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).
45my 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
68sub 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
85sub 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
202sub 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
2161;