-
Notifications
You must be signed in to change notification settings - Fork 1
/
main.go
158 lines (137 loc) · 3.3 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package main
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"slices"
"strings"
"sync"
"syscall"
"time"
"github.com/fsnotify/fsnotify"
)
const name = "reload"
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <command>\n", name)
os.Exit(1)
}
input := os.Args[1:]
// Split the command into parts if the full command is quoted
if len(input) == 1 && strings.Contains(input[0], " ") {
input = strings.Split(input[0], " ")
}
command := strings.Join(input, " ")
// Find files in command that we will watch
toWatch := make([]string, 0)
for _, part := range input {
// Check if there's a file to watch
info, err := os.Stat(part)
if os.IsNotExist(err) {
continue
}
check(err)
if !slices.Contains(toWatch, info.Name()) {
toWatch = append(toWatch, info.Name())
}
}
// Fall back to watching the working directory
if len(toWatch) == 0 {
wd, err := os.Getwd()
check(err)
toWatch = append(toWatch, wd)
}
// Handle SIGTERM (CMD-C and the like)
s := make(chan os.Signal, 1)
signal.Notify(s, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
// Create a file watcher
fileChanges := make(chan string, 2)
watcher, err := fsnotify.NewWatcher()
check(err)
// Use this to synchronize the goroutines
var wg sync.WaitGroup
// Shut down when signal is received
wg.Add(1)
go func() {
defer wg.Done()
<-s
cancel()
close(s)
close(fileChanges)
_ = watcher.Close()
}()
// Add files to watch
for _, file := range toWatch {
err = watcher.Add(file)
check(err)
}
// Start the file watcher goroutine
wg.Add(1)
go func() {
defer wg.Done()
lastChange := time.Now()
dedupWindow := 100 * time.Millisecond
for {
select {
case <-ctx.Done():
return
case event := <-watcher.Events:
if event.Has(fsnotify.Write) {
// Treat multiple events at same time as one
if time.Since(lastChange) < dedupWindow {
continue
}
lastChange = time.Now()
fileChanges <- event.Name
}
}
}
}()
// First run the command
runCommand(ctx, command, fileChanges)
// Then rerun it on file changes
for name := range fileChanges {
fmt.Fprintf(os.Stderr, "--- Changed: %s\n", name)
fmt.Fprintf(os.Stderr, "--- Running: %s\n", command)
runCommand(ctx, command, fileChanges)
}
// Wait until all goroutines are done
wg.Wait()
}
func runCommand(ctx context.Context, command string, fileChanges chan string) {
// Create child context so we can cancel this command
// without cancelling the entire program
commandCtx, commandCancel := context.WithCancel(ctx)
defer commandCancel()
// Cancel and rerun the command if the file changes
// while we run the command
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
name, ok := <-fileChanges
// The channel was closed, shut down
if !ok {
return
}
commandCancel()
// Send the file change back on the channel
// to trigger `runCommand` again
fileChanges <- name
}()
// Run the command using `sh -c <command>` to allow for
// shell syntax such as pipes and boolean operators
cmd := exec.CommandContext(commandCtx, "sh", []string{"-c", command}...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run() // It's fine if the command fails!
wg.Wait()
}
func check(err error) {
if err != nil {
panic(err)
}
}