Skip to content

Commit d402f1a

Browse files
authored
🐛 [subprocess] Improve the error reporting when a process is cancelled (#655)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> ### Description Improve the error reported when a process is cancelled ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [x] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update).
1 parent 1a7d7d7 commit d402f1a

File tree

5 files changed

+57
-37
lines changed

5 files changed

+57
-37
lines changed

changes/20250718123400.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:bug: `[subprocess]` Improve the error reporting when a process is cancelled

utils/platform/deletion.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package platform
22

33
import (
44
"context"
5-
"fmt"
5+
"errors"
66
"os"
77
"os/exec"
88
"time"
@@ -16,7 +16,7 @@ import (
1616
func RemoveWithPrivileges(ctx context.Context, path string) (err error) {
1717
fi, err := os.Stat(path)
1818
if err != nil {
19-
err = fmt.Errorf("%w: could not find path [%v]: %v", commonerrors.ErrNotFound, path, err.Error())
19+
err = commonerrors.WrapErrorf(commonerrors.ErrNotFound, err, "could not find path [%v]", path)
2020
return
2121
}
2222
if fi.IsDir() {
@@ -25,33 +25,25 @@ func RemoveWithPrivileges(ctx context.Context, path string) (err error) {
2525
err = removeFileAs(ctx, WithPrivileges(command.Me()), path)
2626
}
2727
if err != nil {
28-
err = fmt.Errorf("%w: could not remove the path [%v]: %v", commonerrors.ErrUnexpected, path, err.Error())
28+
err = commonerrors.WrapErrorf(commonerrors.ErrUnexpected, err, "could not remove the path [%v]", path)
2929
}
3030
return
3131
}
3232

3333
func executeCommandAs(ctx context.Context, as *command.CommandAsDifferentUser, args ...string) error {
3434
if as == nil {
35-
return fmt.Errorf("%w: missing command wrapper", commonerrors.ErrUndefined)
35+
return commonerrors.UndefinedVariable("command wrapper")
3636
}
3737
if len(args) == 0 {
38-
return fmt.Errorf("%w: missing command to execute", commonerrors.ErrUndefined)
38+
return commonerrors.UndefinedVariable("command to execute")
3939
}
4040
cmdName, cmdArgs := as.RedefineCommand(args...)
4141
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
4242
// setting the following to avoid having hanging subprocesses as described in https://github.com/golang/go/issues/24050
4343
cmd.WaitDelay = 5 * time.Second
44-
cmd.Cancel = func() error {
45-
if cmd.Process == nil {
46-
return nil
47-
}
48-
p, err := proc.FindProcess(context.Background(), cmd.Process.Pid)
49-
if err == nil {
50-
return p.KillWithChildren(context.Background())
51-
} else {
52-
// Default behaviour
53-
return cmd.Process.Kill()
54-
}
44+
cmd, err := proc.DefineCmdCancel(cmd)
45+
if err != nil {
46+
return commonerrors.WrapError(commonerrors.ErrUnexpected, err, "could not set the command cancel function")
5547
}
5648
return runCommand(args[0], cmd)
5749
}
@@ -71,10 +63,11 @@ func runCommand(cmdDescription string, cmd *exec.Cmd) error {
7163
return newErr
7264
default:
7365
details := "no further details"
74-
if exitError, ok := err.(*exec.ExitError); ok {
66+
var exitError *exec.ExitError
67+
if errors.As(err, &exitError) {
7568
details = string(exitError.Stderr)
7669
}
77-
newErr = fmt.Errorf("%w: the command `%v` failed: %v (%v)", commonerrors.ErrUnknown, cmdDescription, err.Error(), details)
70+
newErr = commonerrors.WrapErrorf(commonerrors.ErrUnknown, err, "the command `%v` failed (%v)", cmdDescription, details)
7871
return newErr
7972
}
8073
}

utils/proc/interrupt.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package proc
22

33
import (
44
"context"
5+
"os"
6+
"os/exec"
57
"time"
68

79
"github.com/ARM-software/golang-utils/utils/commonerrors"
@@ -12,9 +14,10 @@ import (
1214
type InterruptType int
1315

1416
const (
15-
SigInt InterruptType = 2
16-
SigKill InterruptType = 9
17-
SigTerm InterruptType = 15
17+
SigInt InterruptType = 2
18+
SigKill InterruptType = 9
19+
SigTerm InterruptType = 15
20+
SubprocessTerminationGracePeriod = 10 * time.Millisecond
1821
)
1922

2023
func InterruptProcess(ctx context.Context, pid int, signal InterruptType) (err error) {
@@ -62,3 +65,32 @@ func TerminateGracefully(ctx context.Context, pid int, gracePeriod time.Duration
6265
err = InterruptProcess(ctx, pid, SigKill)
6366
return
6467
}
68+
69+
// CancelExecCommand defines a more robust way to cancel subprocesses than what is done per default by [CommandContext](https://pkg.go.dev/os/exec#CommandContext)
70+
func CancelExecCommand(cmd *exec.Cmd) (err error) {
71+
if cmd == nil {
72+
err = commonerrors.UndefinedVariable("command")
73+
return
74+
}
75+
if cmd.Process == nil {
76+
return
77+
}
78+
err = TerminateGracefully(context.Background(), cmd.Process.Pid, SubprocessTerminationGracePeriod)
79+
err = commonerrors.Ignore(err, os.ErrProcessDone)
80+
if err != nil {
81+
// Default behaviour
82+
err = cmd.Process.Kill()
83+
}
84+
return
85+
}
86+
87+
// DefineCmdCancel sets and overwrites the cmd.Cancel function with CancelExecCommand so that it is more robust and thorough.
88+
func DefineCmdCancel(cmd *exec.Cmd) (*exec.Cmd, error) {
89+
if cmd == nil {
90+
return nil, commonerrors.UndefinedVariable("command")
91+
}
92+
cmd.Cancel = func() error {
93+
return CancelExecCommand(cmd)
94+
}
95+
return cmd, nil
96+
}

utils/subprocess/command_wrapper.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
commandUtils "github.com/ARM-software/golang-utils/utils/subprocess/command"
2121
)
2222

23-
const subprocessTerminationGracePeriod = 10 * time.Millisecond
24-
2523
// INTERNAL
2624
// wrapper over an exec cmd.
2725
type cmdWrapper struct {
@@ -73,7 +71,7 @@ func (c *cmdWrapper) interruptWithContext(ctx context.Context, interrupt proc.In
7371
stopErr := atomic.NewError(nil)
7472
if subprocess != nil {
7573
pid := subprocess.Pid
76-
parallelisation.ScheduleAfter(ctx, subprocessTerminationGracePeriod, func(time.Time) {
74+
parallelisation.ScheduleAfter(ctx, proc.SubprocessTerminationGracePeriod, func(time.Time) {
7775
process, sErr := proc.FindProcess(ctx, pid)
7876
if process == nil || sErr != nil {
7977
return
@@ -134,12 +132,9 @@ type command struct {
134132
func (c *command) createCommand(cmdCtx context.Context) *exec.Cmd {
135133
newCmd, newArgs := c.as.Redefine(c.cmd, c.args...)
136134
cmd := exec.CommandContext(cmdCtx, newCmd, newArgs...) //nolint:gosec
137-
cmd.Cancel = func() error {
138-
p := cmd.Process
139-
if p == nil {
140-
return nil
141-
}
142-
return proc.TerminateGracefully(context.Background(), p.Pid, subprocessTerminationGracePeriod)
135+
cancellableCmd, err := proc.DefineCmdCancel(cmd)
136+
if err == nil {
137+
cmd = cancellableCmd
143138
}
144139
cmd.Stdout = newOutStreamer(cmdCtx, c.loggers)
145140
cmd.Stderr = newErrLogStreamer(cmdCtx, c.loggers)
@@ -209,6 +204,6 @@ func CleanKillOfCommand(ctx context.Context, cmd *exec.Cmd) (err error) {
209204
if thisP == nil {
210205
return
211206
}
212-
err = proc.TerminateGracefully(ctx, thisP.Pid, subprocessTerminationGracePeriod)
207+
err = proc.TerminateGracefully(ctx, thisP.Pid, proc.SubprocessTerminationGracePeriod)
213208
return
214209
}

utils/subprocess/supervisor/supervisor.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package supervisor
22

33
import (
44
"context"
5-
"fmt"
65
"time"
76

87
"golang.org/x/sync/errgroup"
@@ -113,7 +112,7 @@ func (s *Supervisor) Run(ctx context.Context) (err error) {
113112
if commonerrors.Any(err, commonerrors.ErrCancelled, commonerrors.ErrTimeout) {
114113
return err
115114
}
116-
return fmt.Errorf("%w: error running pre-start hook: %v", commonerrors.ErrUnexpected, err.Error())
115+
return commonerrors.WrapError(commonerrors.ErrUnexpected, err, "error running pre-start hook")
117116
}
118117
}
119118

@@ -123,10 +122,10 @@ func (s *Supervisor) Run(ctx context.Context) (err error) {
123122
if commonerrors.Any(err, commonerrors.ErrCancelled, commonerrors.ErrTimeout) {
124123
return err
125124
}
126-
return fmt.Errorf("%w: error occurred when creating new command: %v", commonerrors.ErrUnexpected, err.Error())
125+
return commonerrors.WrapError(commonerrors.ErrUnexpected, err, "error occurred when creating new command")
127126
}
128127
if s.cmd == nil {
129-
return fmt.Errorf("%w: command was undefined", commonerrors.ErrUndefined)
128+
return commonerrors.UndefinedVariable("command to be supervised")
130129
}
131130

132131
g.Go(s.cmd.Execute)
@@ -137,7 +136,7 @@ func (s *Supervisor) Run(ctx context.Context) (err error) {
137136
if commonerrors.Any(err, commonerrors.ErrCancelled, commonerrors.ErrTimeout) {
138137
return err
139138
}
140-
return fmt.Errorf("%w: error running post-start hook: %v", commonerrors.ErrUnexpected, err.Error())
139+
return commonerrors.WrapError(commonerrors.ErrUnexpected, err, "error running post-start hook")
141140
}
142141
}
143142

@@ -149,7 +148,7 @@ func (s *Supervisor) Run(ctx context.Context) (err error) {
149148
if commonerrors.Any(err, commonerrors.ErrCancelled, commonerrors.ErrTimeout) {
150149
return err
151150
}
152-
return fmt.Errorf("%w: error running post-stop hook: %v", commonerrors.ErrUnexpected, err.Error())
151+
return commonerrors.WrapError(commonerrors.ErrUnexpected, err, "error running post-stop hook")
153152
}
154153
}
155154

0 commit comments

Comments
 (0)