Skip to content

Commit 3586d05

Browse files
authored
Use WaitDelay to ensure system() exits quickly when context is cancelled (#270)
I don't think this is an exhaustive fix, but it fixes the main issue of system() never returning when the context is cancelled (due to stdout/stderr pipes being passed to the child process). This PR also bumps up the minimum Go version required in go.mod from 1.18 to 1.20, as `Cmd.WaitDelay` was only introduced in Go 1.20. That's fairly old by now, so this seems reasonable. Fixes #122
1 parent f0bf50b commit 3586d05

5 files changed

Lines changed: 31 additions & 12 deletions

File tree

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
go: ['1.25', '1.24', '1.23', '1.18']
16+
go: ['1.25', '1.24', '1.23', '1.20']
1717

1818
name: Go ${{ matrix.go }} on Linux
1919

@@ -59,7 +59,7 @@ jobs:
5959
strategy:
6060
fail-fast: false
6161
matrix:
62-
go: ['1.25', '1.18']
62+
go: ['1.25', '1.20']
6363

6464
name: Go ${{ matrix.go }} on Windows
6565

@@ -85,7 +85,7 @@ jobs:
8585
strategy:
8686
fail-fast: false
8787
matrix:
88-
go: ['1.25'] # TODO: '1.18' on macOS not working right now (403 when downloading)
88+
go: ['1.25', '1.20']
8989

9090
name: Go ${{ matrix.go }} on macOS
9191

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/benhoyt/goawk
22

3-
go 1.18
3+
go 1.20

interp/io.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"regexp"
1414
"strconv"
1515
"strings"
16+
"time"
1617
"unicode/utf8"
1718

1819
"github.com/benhoyt/goawk/internal/resolver"
@@ -170,11 +171,16 @@ func (p *interp) execShell(code string) *exec.Cmd {
170171
executable := p.shellCommand[0]
171172
args := p.shellCommand[1:]
172173
args = append(args, code)
174+
175+
var cmd *exec.Cmd
173176
if p.checkCtx {
174-
return exec.CommandContext(p.ctx, executable, args...)
177+
cmd = exec.CommandContext(p.ctx, executable, args...)
175178
} else {
176-
return exec.Command(executable, args...)
179+
cmd = exec.Command(executable, args...)
177180
}
181+
// Ensure stdout/stderr pipes being held open don't keep process running.
182+
cmd.WaitDelay = 250 * time.Millisecond
183+
return cmd
178184
}
179185

180186
// Get input Scanner to use for "getline" based on file name

interp/newexecute.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ func (p *Interpreter) ResetRand() {
163163
// set an execution timeout or cancel the execution. For efficiency, the
164164
// context is only tested every 1000 virtual machine instructions.
165165
//
166-
// Context handling is not preemptive: currently long-running operations like
167-
// system() won't be interrupted.
166+
// Context handling is not preemptive: some long-running operations may not
167+
// be interrupted when the context is cancelled.
168168
func (p *Interpreter) ExecuteContext(ctx context.Context, config *Config) (int, error) {
169169
p.interp.resetCore()
170170
p.interp.checkCtx = ctx != context.Background() && ctx != context.TODO()

interp/newexecute_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"errors"
99
"math"
10+
"runtime"
1011
"strings"
1112
"testing"
1213
"time"
@@ -167,13 +168,25 @@ func TestExecuteContextCancel(t *testing.T) {
167168
}
168169

169170
func TestExecuteContextSystemTimeout(t *testing.T) {
170-
t.Skip("TODO: skipping for now due to #122")
171-
interpreter := newInterp(t, `BEGIN { print system("sleep 4") }`)
171+
start := time.Now()
172+
interpreter := newInterp(t, `BEGIN { print system("sleep 1") }`)
172173
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
173174
defer cancel()
175+
174176
_, err := interpreter.ExecuteContext(ctx, nil)
175-
if !errors.Is(err, context.DeadlineExceeded) {
176-
t.Fatalf("expected DeadlineExceeded error, got: %v", err)
177+
switch runtime.GOOS {
178+
case "windows", "darwin":
179+
// For some reason on Windows and macOS the returned error is nil, rather
180+
// than DeadlineExceeded. Don't check it on those platforms.
181+
default:
182+
if !errors.Is(err, context.DeadlineExceeded) {
183+
t.Fatalf("expected DeadlineExceeded error, got: %v", err)
184+
}
185+
}
186+
187+
elapsed := time.Since(start)
188+
if elapsed > 500*time.Millisecond {
189+
t.Fatalf("should have taken ~5ms, took %v", elapsed)
177190
}
178191
}
179192

0 commit comments

Comments
 (0)