]> git.proxmox.com Git - pve-installer.git/blob - Proxmox/Sys/Command.pm
Merge remote-tracking branch 'cheiss/tui-installer' into tui
[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 Gtk3 qw(); # FIXME: drop once possible (when (G)UI plugin approacg is there)
8 use IO::File;
9 use IPC::Open3;
10 use IO::Select;
11 use String::ShellQuote;
12
13 use Proxmox::Install::ISOEnv;
14 use Proxmox::Log;
15
16 use base qw(Exporter);
17 our @EXPORT_OK = qw(run_command syscmd);
18
19 my sub shellquote {
20 my $str = shift;
21 return String::ShellQuote::shell_quote($str);
22 }
23
24 my sub cmd2string {
25 my ($cmd) = @_;
26
27 die "no arguments" if !$cmd;
28 return $cmd if !ref($cmd);
29
30 my $quoted_args = [ map { shellquote($_) } $cmd->@* ];
31
32 return join (' ', $quoted_args->@*);
33 }
34
35 sub syscmd {
36 my ($cmd) = @_;
37
38 return run_command($cmd, undef, undef, 1);
39 }
40
41 sub run_command {
42 my ($cmd, $func, $input, $noout) = @_;
43
44 my $cmdstr;
45 if (!ref($cmd)) {
46 $cmdstr = $cmd;
47 if ($cmd =~ m/|/) {
48 # see 'man bash' for option pipefail
49 $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmd" ];
50 } else {
51 $cmd = [ $cmd ];
52 }
53 } else {
54 $cmdstr = cmd2string($cmd);
55 }
56
57 my $cmdtxt;
58 if ($input && ($cmdstr !~ m/chpasswd/)) {
59 $cmdtxt = "# $cmdstr <<EOD\n$input";
60 chomp $cmdtxt;
61 $cmdtxt .= "\nEOD\n";
62 } else {
63 $cmdtxt = "# $cmdstr\n";
64 }
65
66 if (is_test_mode()) {
67 print $cmdtxt;
68 STDOUT->flush();
69 }
70 log_info($cmdtxt);
71
72 my ($reader, $writer, $error) = (IO::File->new(), IO::File->new(), IO::File->new());
73
74 my $orig_pid = $$;
75
76 my $pid = eval { open3($writer, $reader, $error, @$cmd) || die $!; };
77 my $err = $@;
78
79 if ($orig_pid != $$) { # catch exec errors
80 POSIX::_exit (1);
81 kill ('KILL', $$);
82 }
83 die $err if $err;
84
85 print $writer $input if defined $input;
86 close $writer;
87
88 my $select = IO::Select->new();
89 $select->add($reader);
90 $select->add($error);
91
92 my ($ostream, $logout) = ('', '', '');
93
94 while ($select->count) {
95 my @handles = $select->can_read (0.2);
96
97 Gtk3::main_iteration() while Gtk3::events_pending();
98
99 next if !scalar (@handles); # timeout
100
101 foreach my $h (@handles) {
102 my $buf = '';
103 my $count = sysread ($h, $buf, 4096);
104 if (!defined ($count)) {
105 my $err = $!;
106 kill (9, $pid);
107 waitpid ($pid, 0);
108 die "command '$cmd' failed: $err";
109 }
110 $select->remove($h) if !$count;
111 if ($h eq $reader) {
112 $ostream .= $buf if !($noout || $func);
113 $logout .= $buf;
114 while ($logout =~ s/^([^\010\r\n]*)(\r|\n|(\010)+|\r\n)//s) {
115 my $line = $1;
116 $func->($line) if $func;
117 }
118
119 } elsif ($h eq $error) {
120 $ostream .= $buf if !($noout || $func);
121 }
122 print $buf;
123 STDOUT->flush();
124 log_info($buf);
125 }
126 }
127
128 &$func($logout) if $func;
129
130 my $rv = waitpid ($pid, 0);
131
132 return $? if $noout; # behave like standard system();
133
134 if ($? == -1) {
135 croak "command '$cmdstr' failed to execute\n";
136 } elsif (my $sig = ($? & 127)) {
137 croak "command '$cmdstr' failed - got signal $sig\n";
138 } elsif (my $exitcode = ($? >> 8)) {
139 croak "command '$cmdstr' failed with exit code $exitcode";
140 }
141
142 return $ostream;
143 }
144
145 # forks and runs the provided coderef in the child
146 # do not use syscmd or run_command as both confuse the GTK mainloop if
147 # run from a child process
148 sub run_in_background {
149 my ($cmd) = @_;
150
151 my $pid = fork() // die "fork failed: $!\n";
152 if (!$pid) {
153 eval { $cmd->(); };
154 if (my $err = $@) {
155 warn "run_in_background error: $err\n";
156 POSIX::_exit(1);
157 }
158 POSIX::_exit(0);
159 }
160 }
161
162 1;