Skip to content

Commit f628fa3

Browse files
committed
Added Process helpers
1 parent ee34a5b commit f628fa3

File tree

7 files changed

+481
-0
lines changed

7 files changed

+481
-0
lines changed

executils/pipes.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 executils
31+
32+
import (
33+
"bytes"
34+
"io"
35+
"os/exec"
36+
)
37+
38+
// PipeCommands executes the commands received as input by feeding the output of
39+
// one to the input of the other, exactly like Unix Pipe (|).
40+
// Returns the output of the final command and the eventual error.
41+
//
42+
// code inspired by https://gist.github.com/tyndyll/89fbb2c2273f83a074dc
43+
func PipeCommands(commands ...*exec.Cmd) ([]byte, error) {
44+
var errorBuffer, outputBuffer bytes.Buffer
45+
pipeStack := make([]*io.PipeWriter, len(commands)-1)
46+
i := 0
47+
for ; i < len(commands)-1; i++ {
48+
stdinPipe, stdoutPipe := io.Pipe()
49+
commands[i].Stdout = stdoutPipe
50+
commands[i].Stderr = &errorBuffer
51+
commands[i+1].Stdin = stdinPipe
52+
pipeStack[i] = stdoutPipe
53+
}
54+
commands[i].Stdout = &outputBuffer
55+
commands[i].Stderr = &errorBuffer
56+
57+
if err := call(commands, pipeStack); err != nil {
58+
return nil, err
59+
}
60+
61+
return outputBuffer.Bytes(), nil
62+
}
63+
64+
func call(stack []*exec.Cmd, pipes []*io.PipeWriter) (err error) {
65+
if stack[0].Process == nil {
66+
if err = stack[0].Start(); err != nil {
67+
return err
68+
}
69+
}
70+
if len(stack) > 1 {
71+
if err = stack[1].Start(); err != nil {
72+
return err
73+
}
74+
defer func() {
75+
pipes[0].Close()
76+
err = call(stack[1:], pipes[1:])
77+
}()
78+
}
79+
return stack[0].Wait()
80+
}

process.go

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
}

process_others.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
//go:build !windows
31+
32+
package paths
33+
34+
import "os/exec"
35+
36+
func tellCommandNotToSpawnShell(_ *exec.Cmd) {
37+
// no op
38+
}

0 commit comments

Comments
 (0)