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