Skip to content

Commit f4d12f8

Browse files
typelesslunny
authored andcommitted
Fix run command race (#1470)
* Use exec.CommandContext to simplfy timeout handling And fixing the data races which can be identified by the added tests when -race enabled. * Use sleep commmand instead of reading from stdin * Make the error handling go-esque
1 parent e9728bf commit f4d12f8

File tree

2 files changed

+33
-22
lines changed

2 files changed

+33
-22
lines changed

modules/process/manager.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ package process
66

77
import (
88
"bytes"
9+
"context"
910
"errors"
1011
"fmt"
1112
"os/exec"
1213
"sync"
1314
"time"
14-
15-
"code.gitea.io/gitea/modules/log"
1615
)
1716

1817
// TODO: This packages still uses a singleton for the Manager.
@@ -101,7 +100,10 @@ func (pm *Manager) ExecDirEnv(timeout time.Duration, dir, desc string, env []str
101100
stdOut := new(bytes.Buffer)
102101
stdErr := new(bytes.Buffer)
103102

104-
cmd := exec.Command(cmdName, args...)
103+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
104+
defer cancel()
105+
106+
cmd := exec.CommandContext(ctx, cmdName, args...)
105107
cmd.Dir = dir
106108
cmd.Env = env
107109
cmd.Stdout = stdOut
@@ -111,30 +113,14 @@ func (pm *Manager) ExecDirEnv(timeout time.Duration, dir, desc string, env []str
111113
}
112114

113115
pid := pm.Add(desc, cmd)
114-
done := make(chan error)
115-
go func() {
116-
done <- cmd.Wait()
117-
}()
118-
119-
var err error
120-
select {
121-
case <-time.After(timeout):
122-
if errKill := pm.Kill(pid); errKill != nil {
123-
log.Error(4, "exec(%d:%s) failed to kill: %v", pid, desc, errKill)
124-
}
125-
<-done
126-
return "", "", ErrExecTimeout
127-
case err = <-done:
128-
}
129-
116+
err := cmd.Wait()
130117
pm.Remove(pid)
131118

132119
if err != nil {
133-
out := fmt.Errorf("exec(%d:%s) failed: %v stdout: %v stderr: %v", pid, desc, err, stdOut, stdErr)
134-
return stdOut.String(), stdErr.String(), out
120+
err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr)
135121
}
136122

137-
return stdOut.String(), stdErr.String(), nil
123+
return stdOut.String(), stdErr.String(), err
138124
}
139125

140126
// Kill and remove a process from list.

modules/process/manager_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package process
33
import (
44
"os/exec"
55
"testing"
6+
"time"
67

78
"github.com/stretchr/testify/assert"
89
)
@@ -31,3 +32,27 @@ func TestManager_Remove(t *testing.T) {
3132
_, exists := pm.Processes[pid2]
3233
assert.False(t, exists, "PID %d is in the list but shouldn't", pid2)
3334
}
35+
36+
func TestExecTimeoutNever(t *testing.T) {
37+
38+
// TODO Investigate how to improve the time elapsed per round.
39+
maxLoops := 10
40+
for i := 1; i < maxLoops; i++ {
41+
_, stderr, err := GetManager().ExecTimeout(5*time.Second, "ExecTimeout", "git", "--version")
42+
if err != nil {
43+
t.Fatalf("git --version: %v(%s)", err, stderr)
44+
}
45+
}
46+
}
47+
48+
func TestExecTimeoutAlways(t *testing.T) {
49+
50+
maxLoops := 100
51+
for i := 1; i < maxLoops; i++ {
52+
_, stderr, err := GetManager().ExecTimeout(100*time.Microsecond, "ExecTimeout", "sleep", "5")
53+
// TODO Simplify logging and errors to get precise error type. E.g. checking "if err != context.DeadlineExceeded".
54+
if err == nil {
55+
t.Fatalf("sleep 5 secs: %v(%s)", err, stderr)
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)