Skip to content

Commit 48383da

Browse files
authored
executils refactoring / fix "debug" command startup (arduino#921)
* Added executils.Process helper This helper should avoid common pitfalls arising mainly after the adoption of the 'avrdude-stdin-hack'. For example see arduino#904 It also streamlines some of the most common operations (like process Kill or Signal). * Removed no more used executils.Command * Run 'board list' test anyway Even if no board are attached to the host system, it's always another smoke-test that runs automatically. Having this check enabled, for example, would have prevented: arduino#904
1 parent 9ff9308 commit 48383da

File tree

7 files changed

+150
-34
lines changed

7 files changed

+150
-34
lines changed

Diff for: arduino/cores/packagemanager/install_uninstall.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,11 @@ func (pm *PackageManager) RunPostInstallScript(platformRelease *cores.PlatformRe
5454
}
5555
postInstall := platformRelease.InstallDir.Join(postInstallFilename)
5656
if postInstall.Exist() && postInstall.IsNotDir() {
57-
cmd, err := executils.Command(postInstall.String())
57+
cmd, err := executils.NewProcessFromPath(postInstall)
5858
if err != nil {
5959
return err
6060
}
61-
cmd.Dir = platformRelease.InstallDir.String()
62-
cmd.Stdout = nil
63-
cmd.Stderr = nil
61+
cmd.SetDirFromPath(platformRelease.InstallDir)
6462
if err := cmd.Run(); err != nil {
6563
return err
6664
}

Diff for: commands/bundled_tools_serial_discovery.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,12 @@ func ListBoards(pm *packagemanager.PackageManager) ([]*BoardPort, error) {
131131
}
132132

133133
// build the command to be executed
134-
cmd, err := executils.Command(t.InstallDir.Join("serial-discovery").String())
134+
cmd, err := executils.NewProcessFromPath(t.InstallDir.Join("serial-discovery"))
135135
if err != nil {
136136
return nil, errors.Wrap(err, "creating discovery process")
137137
}
138138

139139
// attach in/out pipes to the process
140-
cmd.Stdin = nil
141140
in, err := cmd.StdinPipe()
142141
if err != nil {
143142
return nil, fmt.Errorf("creating stdin pipe for discovery: %s", err)
@@ -187,7 +186,7 @@ func ListBoards(pm *packagemanager.PackageManager) ([]*BoardPort, error) {
187186
out.Close()
188187
// kill the process if it takes too long to quit
189188
time.AfterFunc(time.Second, func() {
190-
cmd.Process.Kill()
189+
cmd.Kill()
191190
})
192191
cmd.Wait()
193192

Diff for: commands/debug/debug.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func Debug(ctx context.Context, req *dbg.DebugConfigReq, inStream io.Reader, out
6464
}
6565
entry.Debug("Executing debugger")
6666

67-
cmd, err := executils.Command(commandLine...)
67+
cmd, err := executils.NewProcess(commandLine...)
6868
if err != nil {
6969
return nil, errors.Wrap(err, "Cannot execute debug tool")
7070
}
@@ -77,8 +77,8 @@ func Debug(ctx context.Context, req *dbg.DebugConfigReq, inStream io.Reader, out
7777
defer in.Close()
7878

7979
// Merge tool StdOut and StdErr to stream them in the io.Writer passed stream
80-
cmd.Stdout = out
81-
cmd.Stderr = out
80+
cmd.RedirectStdoutTo(out)
81+
cmd.RedirectStderrTo(out)
8282

8383
// Start the debug command
8484
if err := cmd.Start(); err != nil {
@@ -91,7 +91,7 @@ func Debug(ctx context.Context, req *dbg.DebugConfigReq, inStream io.Reader, out
9191
if sig, ok := <-interrupt; !ok {
9292
break
9393
} else {
94-
cmd.Process.Signal(sig)
94+
cmd.Signal(sig)
9595
}
9696
}
9797
}()
@@ -103,7 +103,7 @@ func Debug(ctx context.Context, req *dbg.DebugConfigReq, inStream io.Reader, out
103103
// In any case, try process termination after a second to avoid leaving
104104
// zombie process.
105105
time.Sleep(time.Second)
106-
cmd.Process.Kill()
106+
cmd.Kill()
107107
}()
108108

109109
// Wait for process to finish

Diff for: commands/upload/upload.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -373,13 +373,13 @@ func runTool(recipeID string, props *properties.Map, outStream, errStream io.Wri
373373
if verbose {
374374
outStream.Write([]byte(fmt.Sprintln(cmdLine)))
375375
}
376-
cmd, err := executils.Command(cmdArgs...)
376+
cmd, err := executils.NewProcess(cmdArgs...)
377377
if err != nil {
378378
return fmt.Errorf("cannot execute upload tool: %s", err)
379379
}
380380

381-
cmd.Stdout = outStream
382-
cmd.Stderr = errStream
381+
cmd.RedirectStdoutTo(outStream)
382+
cmd.RedirectStderrTo(errStream)
383383

384384
if err := cmd.Start(); err != nil {
385385
return fmt.Errorf("cannot execute upload tool: %s", err)

Diff for: executils/executils.go

-18
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package executils
1717

1818
import (
1919
"bytes"
20-
"fmt"
2120
"io"
2221
"os/exec"
2322
)
@@ -71,20 +70,3 @@ func call(stack []*exec.Cmd, pipes []*io.PipeWriter) (err error) {
7170
func TellCommandNotToSpawnShell(cmd *exec.Cmd) {
7271
tellCommandNotToSpawnShell(cmd)
7372
}
74-
75-
// Command creates a command with the provided command line arguments.
76-
// The first argument is the path to the executable, the remainder are the
77-
// arguments to the command.
78-
func Command(args ...string) (*exec.Cmd, error) {
79-
if args == nil || len(args) == 0 {
80-
return nil, fmt.Errorf("no executable specified")
81-
}
82-
cmd := exec.Command(args[0], args[1:]...)
83-
TellCommandNotToSpawnShell(cmd)
84-
85-
// This is required because some tools detects if the program is running
86-
// from terminal by looking at the stdin/out bindings.
87-
// https://github.com/arduino/arduino-cli/issues/844
88-
cmd.Stdin = NullReader
89-
return cmd, nil
90-
}

Diff for: executils/process.go

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package executils
17+
18+
import (
19+
"io"
20+
"os"
21+
"os/exec"
22+
23+
"github.com/arduino/go-paths-helper"
24+
"github.com/pkg/errors"
25+
)
26+
27+
// Process is representation of an external process run
28+
type Process struct {
29+
cmd *exec.Cmd
30+
}
31+
32+
// NewProcess creates a command with the provided command line arguments.
33+
// The first argument is the path to the executable, the remainder are the
34+
// arguments to the command.
35+
func NewProcess(args ...string) (*Process, error) {
36+
if args == nil || len(args) == 0 {
37+
return nil, errors.New("no executable specified")
38+
}
39+
p := &Process{
40+
cmd: exec.Command(args[0], args[1:]...),
41+
}
42+
TellCommandNotToSpawnShell(p.cmd)
43+
44+
// This is required because some tools detects if the program is running
45+
// from terminal by looking at the stdin/out bindings.
46+
// https://github.com/arduino/arduino-cli/issues/844
47+
p.cmd.Stdin = NullReader
48+
return p, nil
49+
}
50+
51+
// NewProcessFromPath creates a command from the provided executable path and
52+
// command line arguments.
53+
func NewProcessFromPath(executable *paths.Path, args ...string) (*Process, error) {
54+
processArgs := []string{executable.String()}
55+
processArgs = append(processArgs, args...)
56+
return NewProcess(processArgs...)
57+
}
58+
59+
// RedirectStdoutTo will redirect the process' stdout to the specified
60+
// writer. Any previous redirection will be overwritten.
61+
func (p *Process) RedirectStdoutTo(out io.Writer) {
62+
p.cmd.Stdout = out
63+
}
64+
65+
// RedirectStderrTo will redirect the process' stdout to the specified
66+
// writer. Any previous redirection will be overwritten.
67+
func (p *Process) RedirectStderrTo(out io.Writer) {
68+
p.cmd.Stderr = out
69+
}
70+
71+
// StdinPipe returns a pipe that will be connected to the command's standard
72+
// input when the command starts. The pipe will be closed automatically after
73+
// Wait sees the command exit. A caller need only call Close to force the pipe
74+
// to close sooner. For example, if the command being run will not exit until
75+
// standard input is closed, the caller must close the pipe.
76+
func (p *Process) StdinPipe() (io.WriteCloser, error) {
77+
if p.cmd.Stdin == NullReader {
78+
p.cmd.Stdin = nil
79+
}
80+
return p.cmd.StdinPipe()
81+
}
82+
83+
// StdoutPipe returns a pipe that will be connected to the command's standard
84+
// output when the command starts.
85+
func (p *Process) StdoutPipe() (io.ReadCloser, error) {
86+
return p.cmd.StdoutPipe()
87+
}
88+
89+
// StderrPipe returns a pipe that will be connected to the command's standard
90+
// error when the command starts.
91+
func (p *Process) StderrPipe() (io.ReadCloser, error) {
92+
return p.cmd.StderrPipe()
93+
}
94+
95+
// Start will start the underliyng process.
96+
func (p *Process) Start() error {
97+
return p.cmd.Start()
98+
}
99+
100+
// Wait waits for the command to exit and waits for any copying to stdin or copying
101+
// from stdout or stderr to complete.
102+
func (p *Process) Wait() error {
103+
// TODO: make some helpers to retrieve exit codes out of *ExitError.
104+
return p.cmd.Wait()
105+
}
106+
107+
// Signal sends a signal to the Process. Sending Interrupt on Windows is not implemented.
108+
func (p *Process) Signal(sig os.Signal) error {
109+
return p.cmd.Process.Signal(sig)
110+
}
111+
112+
// Kill causes the Process to exit immediately. Kill does not wait until the Process has
113+
// actually exited. This only kills the Process itself, not any other processes it may
114+
// have started.
115+
func (p *Process) Kill() error {
116+
return p.cmd.Process.Kill()
117+
}
118+
119+
// SetDir sets the working directory of the command. If Dir is the empty string, Run
120+
// runs the command in the calling process's current directory.
121+
func (p *Process) SetDir(dir string) {
122+
p.cmd.Dir = dir
123+
}
124+
125+
// SetDirFromPath sets the working directory of the command. If path is nil, Run
126+
// runs the command in the calling process's current directory.
127+
func (p *Process) SetDirFromPath(path *paths.Path) {
128+
if path == nil {
129+
p.cmd.Dir = ""
130+
} else {
131+
p.cmd.Dir = path.String()
132+
}
133+
}
134+
135+
// Run starts the specified command and waits for it to complete.
136+
func (p *Process) Run() error {
137+
return p.cmd.Run()
138+
}

Diff for: test/test_board.py

-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,6 @@
372372
""" # noqa: E501
373373

374374

375-
@pytest.mark.skipif(running_on_ci(), reason="VMs have no serial ports")
376375
def test_board_list(run_command):
377376
result = run_command("core update-index")
378377
assert result.ok

0 commit comments

Comments
 (0)