Skip to content

Commit d5553c9

Browse files
committed
refactor: migrates to a new simpler executor
1 parent b93d6c5 commit d5553c9

9 files changed

Lines changed: 353 additions & 76 deletions

File tree

Runfile.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,13 @@ tasks:
1818
cmd:
1919
- echo "hello world"
2020

21-
test:old:
22-
cmd:
23-
- go test -json ./pkg/runfile | gotestfmt
24-
2521
test:
2622
env:
2723
pattern:
2824
default: ""
2925
only_failing:
3026
default: false
3127
watch:
32-
enable: true
3328
dir:
3429
- ./parser
3530
onlySuffixes:
@@ -42,8 +37,11 @@ tasks:
4237
testfmt_args=""
4338
[ "$only_failing" = "true" ] && testfmt_args="--hide successful-tests"
4439
45-
go test -json ./pkg/runfile/... $pattern_args | gotestfmt $testfmt_args
40+
go test -json ./pkg/runfile/resolver/... $pattern_args | gotestfmt $testfmt_args
41+
go test -json ./pkg/executor/... $pattern_args | gotestfmt $testfmt_args
4642
4743
test:only-failing:
4844
cmd:
49-
- go test -json ./pkg/runfile | gotestfmt --hide successful-tests
45+
- go test -json ./pkg/runfile/resolver/... | gotestfmt --hide successful-tests
46+
go test -json ./pkg/executor/... $pattern_args | gotestfmt --hide successful-tests
47+

examples/Runfile.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,8 @@ tasks:
123123
- run: first-and-second
124124
- echo "Hello World"
125125

126+
node:
127+
interactive: true
128+
cmd:
129+
- node
130+

go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ require (
1313
github.com/nxtcoder17/fwatcher v1.2.2-0.20250804201159-543ad31be162
1414
github.com/nxtcoder17/go.errors v0.0.0-20251116060059-d31bd582d4c8
1515
github.com/urfave/cli/v3 v3.0.0-beta1
16-
golang.org/x/term v0.32.0
16+
golang.org/x/term v0.39.0
1717
gopkg.in/yaml.v3 v3.0.1
1818
)
1919

2020
require (
2121
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2222
github.com/charmbracelet/x/ansi v0.4.2 // indirect
23+
github.com/creack/pty v1.1.24 // indirect
2324
github.com/dlclark/regexp2 v1.11.4 // indirect
2425
github.com/fsnotify/fsnotify v1.8.0 // indirect
2526
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
@@ -32,6 +33,7 @@ require (
3233
github.com/samber/lo v1.47.0 // indirect
3334
github.com/samber/slog-common v0.18.1 // indirect
3435
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
35-
golang.org/x/sys v0.33.0 // indirect
36+
golang.org/x/sync v0.19.0 // indirect
37+
golang.org/x/sys v0.40.0 // indirect
3638
golang.org/x/text v0.16.0 // indirect
3739
)

go.sum

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOY
1111
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
1212
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
1313
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
14+
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
15+
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
1416
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1517
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1618
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
@@ -38,12 +40,6 @@ github.com/nxtcoder17/fastlog v0.0.0-20251112144402-5324a708e570 h1:uiafpAq+4R/W
3840
github.com/nxtcoder17/fastlog v0.0.0-20251112144402-5324a708e570/go.mod h1:x6o+8WEHRGaWu9XEhSdTrjmDjKhVnKNXd/XZ56bNN/o=
3941
github.com/nxtcoder17/fwatcher v1.2.2-0.20250804201159-543ad31be162 h1:7EHTiBm6MVUMzT8pdeavpXcxwzzIbDC0QJwre6OvGAk=
4042
github.com/nxtcoder17/fwatcher v1.2.2-0.20250804201159-543ad31be162/go.mod h1:SMwIdCpyi5fBygrkCX8hIIUeILzgoxJFaDSlhFBOWWQ=
41-
github.com/nxtcoder17/go.errors v0.0.0-20251113120002-a0f554f5bc7e h1:CaipKbo8QLN+xmebk7JUy+HJE6v6KjCyVHRuNSBVV8Q=
42-
github.com/nxtcoder17/go.errors v0.0.0-20251113120002-a0f554f5bc7e/go.mod h1:9gp0I4JikKZGKflgPqqXCPZlIznVzlPWmnv+CWIrdxE=
43-
github.com/nxtcoder17/go.errors v0.0.0-20251116055045-656c03e9c6a6 h1:KzDMvpFNcEtd0Pbsun6qPkUg+lHKovDmUYsldlnCFWk=
44-
github.com/nxtcoder17/go.errors v0.0.0-20251116055045-656c03e9c6a6/go.mod h1:9gp0I4JikKZGKflgPqqXCPZlIznVzlPWmnv+CWIrdxE=
45-
github.com/nxtcoder17/go.errors v0.0.0-20251116055423-b54e4f346408 h1:uiAn9JS/vVvFtnEissR0sSIduvz+W2jQ9bdvwbvlBfo=
46-
github.com/nxtcoder17/go.errors v0.0.0-20251116055423-b54e4f346408/go.mod h1:9gp0I4JikKZGKflgPqqXCPZlIznVzlPWmnv+CWIrdxE=
4743
github.com/nxtcoder17/go.errors v0.0.0-20251116060059-d31bd582d4c8 h1:C1vUEvYbbpofqK4xnbEU1htxZl66myq6ZJfHpcdA/GQ=
4844
github.com/nxtcoder17/go.errors v0.0.0-20251116060059-d31bd582d4c8/go.mod h1:9gp0I4JikKZGKflgPqqXCPZlIznVzlPWmnv+CWIrdxE=
4945
github.com/nxtcoder17/go.pkgs v0.0.0-20250216034729-39e2d2cd48da h1:Y6GILHFlrihVfDqDPQ98y2kdUeI0SQc8tnoXh2NbEIA=
@@ -67,13 +63,19 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
6763
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
6864
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
6965
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
66+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
67+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
7068
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7169
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7270
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7371
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
7472
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
73+
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
74+
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
7575
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
7676
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
77+
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
78+
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
7779
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
7880
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
7981
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

nixy.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ nixpkgs:
33

44
packages:
55
- go
6-
76
- pre-commit
87
- gotestfmt
8+
9+
onShellEnter: |+
10+
export PATH="/workspace/bin:$PATH"
11+
source $HOME/.profile

pkg/executor/command.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package executor
2+
3+
import (
4+
"context"
5+
)
6+
7+
// Command is the unit of work in a pipeline
8+
type Command interface {
9+
Run(ctx context.Context) error
10+
}
11+
12+
type command struct {
13+
run func(ctx context.Context) error
14+
preHooks []func(ctx context.Context) error
15+
postHooks []func(ctx context.Context) error
16+
}
17+
18+
func (c *command) Run(ctx context.Context) error {
19+
for _, h := range c.preHooks {
20+
if err := h(ctx); err != nil {
21+
return err
22+
}
23+
}
24+
if err := c.run(ctx); err != nil {
25+
return err
26+
}
27+
for _, h := range c.postHooks {
28+
if err := h(ctx); err != nil {
29+
return err
30+
}
31+
}
32+
return nil
33+
}
34+
35+
func (c *command) AddPreHook(h func(ctx context.Context) error) *command {
36+
c.preHooks = append(c.preHooks, h)
37+
return c
38+
}
39+
40+
func (c *command) AddPostHook(h func(ctx context.Context) error) *command {
41+
c.postHooks = append(c.postHooks, h)
42+
return c
43+
}
44+
45+
// CommandFunc is now a factory
46+
func CommandFunc(fn func(context.Context) error) *command {
47+
return &command{run: fn}
48+
}

pkg/executor/pipeline.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package executor
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"sync"
7+
8+
"golang.org/x/sync/errgroup"
9+
)
10+
11+
// Step is a pipeline Step.
12+
// It could either have substeps or commands. Not both at the same time.
13+
type Step struct {
14+
SubSteps []Step
15+
Commands []Command
16+
17+
// Parallel means all the Commands/SubSteps will be executed in parallel
18+
Parallel bool
19+
}
20+
21+
// Pipeline executes a sequence of Steps
22+
type Pipeline struct {
23+
logger *slog.Logger
24+
mu sync.Mutex
25+
cancel func()
26+
steps []Step
27+
}
28+
29+
func NewPipeline(logger *slog.Logger, steps []Step) *Pipeline {
30+
if logger == nil {
31+
logger = slog.Default()
32+
}
33+
return &Pipeline{logger: logger, steps: steps}
34+
}
35+
36+
func (p *Pipeline) Start(parent context.Context) error {
37+
p.mu.Lock()
38+
ctx, cf := context.WithCancel(parent)
39+
p.cancel = cf
40+
defer p.mu.Unlock()
41+
42+
for i := range p.steps {
43+
step := p.steps[i]
44+
if err := p.execStep(ctx, &step); err != nil {
45+
return err
46+
}
47+
}
48+
49+
return nil
50+
}
51+
52+
func (p *Pipeline) Stop() error {
53+
if p.cancel != nil {
54+
p.cancel()
55+
}
56+
return nil
57+
}
58+
59+
func (p *Pipeline) execStep(ctx context.Context, step *Step) error {
60+
if err := p.execSubSteps(ctx, step); err != nil {
61+
return err
62+
}
63+
return p.execCommands(ctx, step)
64+
}
65+
66+
func (p *Pipeline) execCommands(ctx context.Context, step *Step) error {
67+
if step.Parallel {
68+
g, gctx := errgroup.WithContext(ctx)
69+
for i := range step.Commands {
70+
cmd := step.Commands[i]
71+
g.Go(func() error {
72+
return cmd.Run(gctx)
73+
})
74+
}
75+
return g.Wait()
76+
}
77+
78+
for i := range step.Commands {
79+
if err := step.Commands[i].Run(ctx); err != nil {
80+
return err
81+
}
82+
}
83+
return nil
84+
}
85+
86+
func (p *Pipeline) execSubSteps(ctx context.Context, step *Step) error {
87+
if step.Parallel {
88+
g, gctx := errgroup.WithContext(ctx)
89+
for i := range step.SubSteps {
90+
substep := &step.SubSteps[i]
91+
g.Go(func() error {
92+
return p.execStep(gctx, substep)
93+
})
94+
}
95+
return g.Wait()
96+
}
97+
98+
for i := range step.SubSteps {
99+
if err := p.execStep(ctx, &step.SubSteps[i]); err != nil {
100+
return err
101+
}
102+
}
103+
return nil
104+
}

pkg/executor/shell-command.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package executor
2+
3+
import (
4+
"context"
5+
"io"
6+
"os"
7+
"os/exec"
8+
"os/signal"
9+
"syscall"
10+
"time"
11+
12+
"github.com/creack/pty"
13+
"golang.org/x/term"
14+
)
15+
16+
// NewInteractiveShellCommand creates a Command that runs an exec.Cmd with PTY for full terminal support
17+
func NewInteractiveShellCommand(handler func(context.Context) *exec.Cmd) *command {
18+
return CommandFunc(func(ctx context.Context) error {
19+
cmd := handler(ctx)
20+
21+
// Clear these - pty.Start sets them to TTY but won't override existing values
22+
cmd.Stdout = nil
23+
cmd.Stderr = nil
24+
cmd.Stdin = nil
25+
26+
ptmx, err := pty.Start(cmd)
27+
if err != nil {
28+
return err
29+
}
30+
defer ptmx.Close()
31+
32+
// Handle terminal resize
33+
sigCh := make(chan os.Signal, 1)
34+
signal.Notify(sigCh, syscall.SIGWINCH)
35+
go func() {
36+
for range sigCh {
37+
pty.InheritSize(os.Stdin, ptmx)
38+
}
39+
}()
40+
sigCh <- syscall.SIGWINCH // Initial resize
41+
defer signal.Stop(sigCh)
42+
43+
// Set stdin to raw mode
44+
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
45+
if err != nil {
46+
return err
47+
}
48+
defer term.Restore(int(os.Stdin.Fd()), oldState)
49+
50+
// Copy I/O
51+
go io.Copy(ptmx, os.Stdin)
52+
io.Copy(os.Stdout, ptmx)
53+
54+
return cmd.Wait()
55+
})
56+
}
57+
58+
// NewShellCommand creates a Command that runs an exec.Cmd with lifecycle management
59+
func NewShellCommand(handler func(context.Context) *exec.Cmd) *command {
60+
return CommandFunc(func(ctx context.Context) error {
61+
cmd := handler(ctx)
62+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
63+
64+
if err := cmd.Start(); err != nil {
65+
return err
66+
}
67+
68+
pid := cmd.Process.Pid
69+
done := make(chan error, 1)
70+
71+
go func() {
72+
done <- cmd.Wait()
73+
}()
74+
75+
select {
76+
case err := <-done:
77+
return err
78+
case <-ctx.Done():
79+
syscall.Kill(-pid, syscall.SIGTERM)
80+
81+
select {
82+
case <-done:
83+
case <-time.After(2 * time.Second):
84+
syscall.Kill(-pid, syscall.SIGKILL)
85+
<-done
86+
}
87+
return ctx.Err()
88+
}
89+
})
90+
}

0 commit comments

Comments
 (0)