Skip to content

Commit f4e8bb1

Browse files
authored
Merge pull request #8 from nxtcoder17/refactor/watch-and-execute
Refactor/watch and execute
2 parents 092ea9c + 0901da9 commit f4e8bb1

9 files changed

Lines changed: 833 additions & 172 deletions

File tree

Taskfile.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,19 @@ tasks:
3030
upx ./bin/{{.binary}}
3131
fi
3232
33+
test:executor:
34+
cmds:
35+
- go test ./pkg/executor/...
36+
37+
test:watcher:
38+
env:
39+
DEBUG: false
40+
cmds:
41+
- go test -json ./pkg/watcher/... | gotestfmt
42+
3343
build:dev:
3444
cmds:
35-
- go build -o ./bin/fwatcher-dev ./cmd
45+
- go build -o ./bin/fwatcher ./cmd
3646

3747
example:http-server:
3848
cmds:

cmd/main.go

Lines changed: 21 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@ package main
22

33
import (
44
"context"
5-
"fmt"
6-
"log/slog"
75
"os"
86
"os/exec"
97
"os/signal"
10-
"path/filepath"
118
"strings"
12-
"sync"
139
"syscall"
1410
"time"
1511

1612
"github.com/nxtcoder17/fwatcher/pkg/executor"
17-
fn "github.com/nxtcoder17/fwatcher/pkg/functions"
1813
"github.com/nxtcoder17/fwatcher/pkg/logging"
1914
"github.com/nxtcoder17/fwatcher/pkg/watcher"
2015
"github.com/urfave/cli/v3"
@@ -25,21 +20,11 @@ var (
2520
Version string
2621
)
2722

28-
// DefaultIgnoreList is list of directories that are mostly ignored
29-
var DefaultIgnoreList = []string{
30-
".git", ".svn", ".hg", // version control
31-
".idea", ".vscode", // IDEs
32-
".direnv", // direnv nix
33-
"node_modules", // node
34-
".DS_Store", // macOS
35-
".log", // logs
36-
}
37-
3823
func main() {
3924
cmd := &cli.Command{
4025
Name: ProgramName,
4126
UseShortOptionHandling: true,
42-
Usage: "simple tool to run commands on filesystem change events",
27+
Usage: "a simple tool to run things on filesystem change events",
4328
ArgsUsage: "<Command To Run>",
4429
Version: Version,
4530
Flags: []cli.Flag{
@@ -77,7 +62,7 @@ func main() {
7762
&cli.StringSliceFlag{
7863
Name: "ignore-list",
7964
Usage: "disables ignoring from default ignore list",
80-
Value: DefaultIgnoreList,
65+
Value: watcher.DefaultIgnoreList,
8166
Aliases: []string{"I"},
8267
},
8368

@@ -92,17 +77,10 @@ func main() {
9277
Usage: "interactive mode, with stdin",
9378
},
9479

95-
&cli.BoolFlag{
96-
Name: "sse",
97-
Usage: "run watcher in sse mode",
98-
},
99-
10080
&cli.StringFlag{
10181
Name: "sse-addr",
10282
HideDefault: false,
103-
Usage: "run watcher in sse mode",
104-
Sources: cli.ValueSourceChain{},
105-
Value: ":12345",
83+
Usage: "run watcher with Server Side Events (SSE) enabled",
10684
},
10785
},
10886
Action: func(ctx context.Context, c *cli.Command) error {
@@ -161,86 +139,39 @@ func main() {
161139
panic(err)
162140
}
163141

164-
var ex executor.Executor
142+
var executors []executor.Executor
165143

166-
switch {
167-
case c.Bool("sse"):
168-
{
169-
sseAddr := c.String("sse-addr")
170-
ex = executor.NewSSEExecutor(executor.SSEExecutorArgs{Addr: sseAddr})
171-
logger.Info("HELLo world")
172-
}
173-
default:
174-
{
175-
execCmd := c.Args().First()
176-
execArgs := c.Args().Tail()
177-
ex = executor.NewCmdExecutor(ctx, executor.CmdExecutorArgs{
178-
Logger: logger,
179-
Interactive: c.Bool("interactive"),
180-
Command: func(context.Context) *exec.Cmd {
144+
if sseAddr := c.String("sse-addr"); sseAddr != "" {
145+
executors = append(executors, executor.NewSSEExecutor(executor.SSEExecutorArgs{Addr: sseAddr}))
146+
}
147+
148+
if c.NArg() > 0 {
149+
execCmd := c.Args().First()
150+
execArgs := c.Args().Tail()
151+
executors = append(executors, executor.NewCmdExecutor(ctx, executor.CmdExecutorArgs{
152+
Logger: logger,
153+
Interactive: c.Bool("interactive"),
154+
Commands: []func(context.Context) *exec.Cmd{
155+
func(c context.Context) *exec.Cmd {
181156
cmd := exec.CommandContext(ctx, execCmd, execArgs...)
182157
cmd.Stdout = os.Stdout
183158
cmd.Stderr = os.Stderr
184159
cmd.Stdin = os.Stdin
185160
return cmd
186161
},
187-
// IsInteractive: true,
188-
})
189-
}
162+
},
163+
}))
190164
}
191165

192-
var wg sync.WaitGroup
193-
wg.Add(1)
194-
go func() {
195-
defer wg.Done()
196-
if err := ex.Start(); err != nil {
197-
slog.Error("got", "err", err)
198-
}
199-
logger.Debug("1. start-job finished")
200-
}()
201-
202-
counter := 0
203-
pwd := fn.Must(os.Getwd())
204-
205-
wg.Add(1)
206-
go func() {
207-
defer wg.Done()
208-
w.Watch(ctx)
209-
logger.Debug("2. watch context closed")
210-
}()
211-
212-
wg.Add(1)
213-
go func() {
214-
defer wg.Done()
215-
<-ctx.Done()
216-
ex.Stop()
217-
logger.Debug("3. killed signal processed")
218-
}()
219-
220-
for event := range w.GetEvents() {
221-
logger.Debug("received", "event", event)
222-
relPath, err := filepath.Rel(pwd, event.Name)
223-
if err != nil {
224-
return err
225-
}
226-
counter += 1
227-
logger.Info(fmt.Sprintf("[RELOADING (%d)] due changes in %s", counter, relPath))
228-
229-
ex.OnWatchEvent(executor.Event{Source: event.Name})
166+
if err := w.WatchAndExecute(ctx, executors); err != nil {
167+
return err
230168
}
231169

232-
// logger.Debug("stopping executor")
233-
// if err := ex.Stop(); err != nil {
234-
// return err
235-
// }
236-
// logger.Info("stopped executor")
237-
238-
wg.Wait()
239170
return nil
240171
},
241172
}
242173

243-
ctx, stop := signal.NotifyContext(context.TODO(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
174+
ctx, stop := signal.NotifyContext(context.TODO(), syscall.SIGINT, syscall.SIGTERM)
244175
defer stop()
245176

246177
if err := cmd.Run(ctx, os.Args); err != nil {

flake.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
pre-commit
2121

2222
go_1_22
23+
gotestfmt
24+
2325
upx
2426
go-task
2527
];

pkg/executor/cmd-executor.go

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ import (
1212
type CmdExecutor struct {
1313
logger *slog.Logger
1414
parentCtx context.Context
15-
newCmd func(context.Context) *exec.Cmd
15+
commands []func(context.Context) *exec.Cmd
1616

1717
interactive bool
1818

19-
mu sync.Mutex
20-
abort func()
19+
mu sync.Mutex
20+
21+
kill func() error
2122
}
2223

2324
type CmdExecutorArgs struct {
2425
Logger *slog.Logger
25-
Command func(context.Context) *exec.Cmd
26+
Commands []func(context.Context) *exec.Cmd
2627
Interactive bool
2728
}
2829

@@ -33,8 +34,8 @@ func NewCmdExecutor(ctx context.Context, args CmdExecutorArgs) *CmdExecutor {
3334

3435
return &CmdExecutor{
3536
parentCtx: ctx,
36-
logger: args.Logger.With("component", "cmd-executor"),
37-
newCmd: args.Command,
37+
logger: args.Logger,
38+
commands: args.Commands,
3839
mu: sync.Mutex{},
3940
interactive: args.Interactive,
4041
}
@@ -47,73 +48,101 @@ func (ex *CmdExecutor) OnWatchEvent(ev Event) error {
4748
return nil
4849
}
4950

50-
// Start implements Executor.
51-
func (ex *CmdExecutor) Start() error {
52-
ex.mu.Lock()
53-
ctx, cf := context.WithCancel(ex.parentCtx)
54-
ex.abort = cf
55-
ex.mu.Unlock()
56-
57-
cmd := ex.newCmd(ctx)
58-
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
59-
if ex.interactive {
60-
cmd.Stdin = os.Stdin
61-
cmd.SysProcAttr.Foreground = true
51+
func killPID(pid int, logger ...*slog.Logger) error {
52+
var l *slog.Logger
53+
if len(logger) > 0 {
54+
l = logger[0]
55+
} else {
56+
l = slog.Default()
6257
}
6358

64-
if err := cmd.Start(); err != nil {
59+
l.Debug("about to kill", "process", pid)
60+
if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil {
61+
if err == syscall.ESRCH {
62+
return nil
63+
}
64+
l.Error("failed to kill, got", "err", err)
6565
return err
6666
}
67+
return nil
68+
}
6769

68-
done := make(chan error)
69-
go func() {
70-
done <- cmd.Wait()
71-
}()
70+
// Start implements Executor.
71+
func (ex *CmdExecutor) Start() error {
72+
ex.mu.Lock()
73+
defer ex.mu.Unlock()
74+
for i := range ex.commands {
75+
if err := ex.parentCtx.Err(); err != nil {
76+
return err
77+
}
7278

73-
select {
74-
case <-ctx.Done():
75-
ex.logger.Debug("process context done")
76-
case err := <-done:
77-
ex.logger.Debug("process wait completed, got", "err", err)
78-
}
79+
ctx, cf := context.WithCancel(ex.parentCtx)
80+
defer cf()
7981

80-
ex.logger.Debug("process", "pid", cmd.Process.Pid)
82+
cmd := ex.commands[i](ctx)
83+
84+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
85+
if ex.interactive {
86+
cmd.Stdin = os.Stdin
87+
cmd.SysProcAttr.Foreground = true
88+
}
8189

82-
if ex.interactive {
83-
// Send SIGTERM to the interactive process, as user will see it on his screen
84-
proc, err := os.FindProcess(os.Getpid())
85-
if err != nil {
90+
if err := cmd.Start(); err != nil {
8691
return err
8792
}
8893

89-
err = proc.Signal(syscall.SIGTERM)
90-
if err != nil {
91-
if err != syscall.ESRCH {
92-
ex.logger.Error("failed to kill, got", "err", err)
94+
logger := ex.logger.With("pid", cmd.Process.Pid, "command", i+1)
95+
96+
ex.kill = func() error {
97+
return killPID(cmd.Process.Pid, logger)
98+
}
99+
100+
go func() {
101+
if err := cmd.Wait(); err != nil {
102+
logger.Debug("process finished (wait completed), got", "err", err)
103+
}
104+
cf()
105+
}()
106+
107+
select {
108+
case <-ctx.Done():
109+
logger.Debug("process finished (context cancelled)")
110+
case <-ex.parentCtx.Done():
111+
logger.Debug("process finished (parent context cancelled)")
112+
}
113+
114+
if ex.interactive {
115+
// Send SIGTERM to the interactive process, as user will see it on his screen
116+
proc, err := os.FindProcess(os.Getpid())
117+
if err != nil {
118+
return err
119+
}
120+
121+
err = proc.Signal(syscall.SIGTERM)
122+
if err != nil {
123+
if err != syscall.ESRCH {
124+
logger.Error("failed to kill, got", "err", err)
125+
return err
126+
}
93127
return err
94128
}
95-
return err
96129
}
97-
}
98130

99-
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil {
100-
if err == syscall.ESRCH {
101-
return nil
131+
if err := ex.kill(); err != nil {
132+
return err
102133
}
103-
ex.logger.Error("failed to kill, got", "err", err)
104-
return err
134+
135+
logger.Debug("command fully executed and processed")
105136
}
106137

107138
return nil
108139
}
109140

110141
// Stop implements Executor.
111142
func (ex *CmdExecutor) Stop() error {
112-
ex.mu.Lock()
113-
if ex.abort != nil {
114-
ex.abort()
143+
if ex.kill != nil {
144+
return ex.kill()
115145
}
116-
ex.mu.Unlock()
117146
return nil
118147
}
119148

0 commit comments

Comments
 (0)