|
| 1 | +// |
| 2 | +// This file is part of PathsHelper library. |
| 3 | +// |
| 4 | +// Copyright 2023 Arduino AG (http://www.arduino.cc/) |
| 5 | +// |
| 6 | +// PathsHelper library is free software; you can redistribute it and/or modify |
| 7 | +// it under the terms of the GNU General Public License as published by |
| 8 | +// the Free Software Foundation; either version 2 of the License, or |
| 9 | +// (at your option) any later version. |
| 10 | +// |
| 11 | +// This program is distributed in the hope that it will be useful, |
| 12 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +// GNU General Public License for more details. |
| 15 | +// |
| 16 | +// You should have received a copy of the GNU General Public License |
| 17 | +// along with this program; if not, write to the Free Software |
| 18 | +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| 19 | +// |
| 20 | +// As a special exception, you may use this file as part of a free software |
| 21 | +// library without restriction. Specifically, if other files instantiate |
| 22 | +// templates or use macros or inline functions from this file, or you compile |
| 23 | +// this file and link it with other files to produce an executable, this |
| 24 | +// file does not by itself cause the resulting executable to be covered by |
| 25 | +// the GNU General Public License. This exception does not however |
| 26 | +// invalidate any other reasons why the executable file might be covered by |
| 27 | +// the GNU General Public License. |
| 28 | +// |
| 29 | + |
| 30 | +package paths |
| 31 | + |
| 32 | +import ( |
| 33 | + "bytes" |
| 34 | + "context" |
| 35 | + "io" |
| 36 | + "os" |
| 37 | + "os/exec" |
| 38 | + |
| 39 | + "github.com/pkg/errors" |
| 40 | +) |
| 41 | + |
| 42 | +// Process is representation of an external process run |
| 43 | +type Process struct { |
| 44 | + cmd *exec.Cmd |
| 45 | +} |
| 46 | + |
| 47 | +// NewProcess creates a command with the provided command line arguments |
| 48 | +// and environment variables (that will be added to the parent os.Environ). |
| 49 | +// The argument args[0] is the path to the executable, the remainder are the |
| 50 | +// arguments to the command. |
| 51 | +func NewProcess(extraEnv []string, args ...string) (*Process, error) { |
| 52 | + if len(args) == 0 { |
| 53 | + return nil, errors.New("no executable specified") |
| 54 | + } |
| 55 | + p := &Process{ |
| 56 | + cmd: exec.Command(args[0], args[1:]...), |
| 57 | + } |
| 58 | + p.cmd.Env = append(os.Environ(), extraEnv...) |
| 59 | + p.TellCommandNotToSpawnShell() |
| 60 | + |
| 61 | + // This is required because some tools detects if the program is running |
| 62 | + // from terminal by looking at the stdin/out bindings. |
| 63 | + // https://github.com/arduino/arduino-cli/issues/844 |
| 64 | + p.cmd.Stdin = nullReaderInstance |
| 65 | + return p, nil |
| 66 | +} |
| 67 | + |
| 68 | +// TellCommandNotToSpawnShell avoids that the specified Cmd display a small |
| 69 | +// command prompt while runnning on Windows. It has no effects on other OS. |
| 70 | +func (p *Process) TellCommandNotToSpawnShell() { |
| 71 | + tellCommandNotToSpawnShell(p.cmd) |
| 72 | +} |
| 73 | + |
| 74 | +// NewProcessFromPath creates a command from the provided executable path, |
| 75 | +// additional environment vars (in addition to the system default ones) |
| 76 | +// and command line arguments. |
| 77 | +func NewProcessFromPath(extraEnv []string, executable *Path, args ...string) (*Process, error) { |
| 78 | + processArgs := []string{executable.String()} |
| 79 | + processArgs = append(processArgs, args...) |
| 80 | + return NewProcess(extraEnv, processArgs...) |
| 81 | +} |
| 82 | + |
| 83 | +// RedirectStdoutTo will redirect the process' stdout to the specified |
| 84 | +// writer. Any previous redirection will be overwritten. |
| 85 | +func (p *Process) RedirectStdoutTo(out io.Writer) { |
| 86 | + p.cmd.Stdout = out |
| 87 | +} |
| 88 | + |
| 89 | +// RedirectStderrTo will redirect the process' stdout to the specified |
| 90 | +// writer. Any previous redirection will be overwritten. |
| 91 | +func (p *Process) RedirectStderrTo(out io.Writer) { |
| 92 | + p.cmd.Stderr = out |
| 93 | +} |
| 94 | + |
| 95 | +// StdinPipe returns a pipe that will be connected to the command's standard |
| 96 | +// input when the command starts. The pipe will be closed automatically after |
| 97 | +// Wait sees the command exit. A caller need only call Close to force the pipe |
| 98 | +// to close sooner. For example, if the command being run will not exit until |
| 99 | +// standard input is closed, the caller must close the pipe. |
| 100 | +func (p *Process) StdinPipe() (io.WriteCloser, error) { |
| 101 | + if p.cmd.Stdin == nullReaderInstance { |
| 102 | + p.cmd.Stdin = nil |
| 103 | + } |
| 104 | + return p.cmd.StdinPipe() |
| 105 | +} |
| 106 | + |
| 107 | +// StdoutPipe returns a pipe that will be connected to the command's standard |
| 108 | +// output when the command starts. |
| 109 | +// |
| 110 | +// Wait will close the pipe after seeing the command exit, so most callers |
| 111 | +// don't need to close the pipe themselves. It is thus incorrect to call Wait |
| 112 | +// before all reads from the pipe have completed. |
| 113 | +// For the same reason, it is incorrect to call Run when using StdoutPipe. |
| 114 | +func (p *Process) StdoutPipe() (io.ReadCloser, error) { |
| 115 | + return p.cmd.StdoutPipe() |
| 116 | +} |
| 117 | + |
| 118 | +// StderrPipe returns a pipe that will be connected to the command's standard |
| 119 | +// error when the command starts. |
| 120 | +// |
| 121 | +// Wait will close the pipe after seeing the command exit, so most callers |
| 122 | +// don't need to close the pipe themselves. It is thus incorrect to call Wait |
| 123 | +// before all reads from the pipe have completed. |
| 124 | +// For the same reason, it is incorrect to use Run when using StderrPipe. |
| 125 | +func (p *Process) StderrPipe() (io.ReadCloser, error) { |
| 126 | + return p.cmd.StderrPipe() |
| 127 | +} |
| 128 | + |
| 129 | +// Start will start the underliyng process. |
| 130 | +func (p *Process) Start() error { |
| 131 | + return p.cmd.Start() |
| 132 | +} |
| 133 | + |
| 134 | +// Wait waits for the command to exit and waits for any copying to stdin or copying |
| 135 | +// from stdout or stderr to complete. |
| 136 | +func (p *Process) Wait() error { |
| 137 | + // TODO: make some helpers to retrieve exit codes out of *ExitError. |
| 138 | + return p.cmd.Wait() |
| 139 | +} |
| 140 | + |
| 141 | +// Signal sends a signal to the Process. Sending Interrupt on Windows is not implemented. |
| 142 | +func (p *Process) Signal(sig os.Signal) error { |
| 143 | + return p.cmd.Process.Signal(sig) |
| 144 | +} |
| 145 | + |
| 146 | +// Kill causes the Process to exit immediately. Kill does not wait until the Process has |
| 147 | +// actually exited. This only kills the Process itself, not any other processes it may |
| 148 | +// have started. |
| 149 | +func (p *Process) Kill() error { |
| 150 | + return p.cmd.Process.Kill() |
| 151 | +} |
| 152 | + |
| 153 | +// SetDir sets the working directory of the command. If Dir is the empty string, Run |
| 154 | +// runs the command in the calling process's current directory. |
| 155 | +func (p *Process) SetDir(dir string) { |
| 156 | + p.cmd.Dir = dir |
| 157 | +} |
| 158 | + |
| 159 | +// GetDir gets the working directory of the command. |
| 160 | +func (p *Process) GetDir() string { |
| 161 | + return p.cmd.Dir |
| 162 | +} |
| 163 | + |
| 164 | +// SetDirFromPath sets the working directory of the command. If path is nil, Run |
| 165 | +// runs the command in the calling process's current directory. |
| 166 | +func (p *Process) SetDirFromPath(path *Path) { |
| 167 | + if path == nil { |
| 168 | + p.cmd.Dir = "" |
| 169 | + } else { |
| 170 | + p.cmd.Dir = path.String() |
| 171 | + } |
| 172 | +} |
| 173 | + |
| 174 | +// Run starts the specified command and waits for it to complete. |
| 175 | +func (p *Process) Run() error { |
| 176 | + return p.cmd.Run() |
| 177 | +} |
| 178 | + |
| 179 | +// SetEnvironment set the environment for the running process. Each entry is of the form "key=value". |
| 180 | +// System default environments will be wiped out. |
| 181 | +func (p *Process) SetEnvironment(values []string) { |
| 182 | + p.cmd.Env = append([]string{}, values...) |
| 183 | +} |
| 184 | + |
| 185 | +// RunWithinContext starts the specified command and waits for it to complete. If the given context |
| 186 | +// is canceled before the normal process termination, the process is killed. |
| 187 | +func (p *Process) RunWithinContext(ctx context.Context) error { |
| 188 | + if err := p.Start(); err != nil { |
| 189 | + return err |
| 190 | + } |
| 191 | + completed := make(chan struct{}) |
| 192 | + defer close(completed) |
| 193 | + go func() { |
| 194 | + select { |
| 195 | + case <-ctx.Done(): |
| 196 | + p.Kill() |
| 197 | + case <-completed: |
| 198 | + } |
| 199 | + }() |
| 200 | + return p.Wait() |
| 201 | +} |
| 202 | + |
| 203 | +// RunAndCaptureOutput starts the specified command and waits for it to complete. If the given context |
| 204 | +// is canceled before the normal process termination, the process is killed. The standard output and |
| 205 | +// standard error of the process are captured and returned at process termination. |
| 206 | +func (p *Process) RunAndCaptureOutput(ctx context.Context) ([]byte, []byte, error) { |
| 207 | + stdout := &bytes.Buffer{} |
| 208 | + stderr := &bytes.Buffer{} |
| 209 | + p.RedirectStdoutTo(stdout) |
| 210 | + p.RedirectStderrTo(stderr) |
| 211 | + err := p.RunWithinContext(ctx) |
| 212 | + return stdout.Bytes(), stderr.Bytes(), err |
| 213 | +} |
| 214 | + |
| 215 | +// GetArgs returns the command arguments |
| 216 | +func (p *Process) GetArgs() []string { |
| 217 | + return p.cmd.Args |
| 218 | +} |
| 219 | + |
| 220 | +// nullReaderInstance is an io.Reader that will always return EOF |
| 221 | +var nullReaderInstance = &nullReader{} |
| 222 | + |
| 223 | +type nullReader struct{} |
| 224 | + |
| 225 | +func (r *nullReader) Read(buff []byte) (int, error) { |
| 226 | + return 0, io.EOF |
| 227 | +} |
0 commit comments