feat(module): count script starts with Gen
- commit
- 718054e7612e0f188e5123cccffa59c81f3e2074
- author
- brice · · 2026-10-09 03:59 UTC
- parent
- 3c1ce2c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +32 -1
M internal/module/module.go +9 -1
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | "path/filepath" | |
| 16 | 16 | "strings" | |
| 17 | 17 | "sync" | |
| 18 | + | "sync/atomic" | |
| 18 | 19 | "syscall" | |
| 19 | 20 | "time" | |
| 20 | 21 | ) | |
| @@ -55,7 +56,8 @@ type Module struct { | |||
| 55 | 56 | dirty bool | |
| 56 | 57 | failed bool // the script is not running; show an error marker | |
| 57 | 58 | ||
| 58 | - | input chan event // lines for the script's stdin | |
| 59 | + | input chan event // lines for the script's stdin | |
| 60 | + | gen atomic.Uint64 // counts script starts; see Gen | |
| 59 | 61 | ||
| 60 | 62 | wake chan<- struct{} | |
| 61 | 63 | log *slog.Logger | |
| @@ -78,6 +80,11 @@ func New(name string, zone Zone, command string, wake chan<- struct{}, log *slog | |||
| 78 | 80 | pending: make([]byte, 0, MaxFrame), input: make(chan event, 32)} | |
| 79 | 81 | } | |
| 80 | 82 | ||
| 83 | + | // Gen counts the times the script was started. A value that changed means a | |
| 84 | + | // new process, which knows nothing of the pointer state sent to the old one. | |
| 85 | + | // It does not allocate. | |
| 86 | + | func (m *Module) Gen() uint64 { return m.gen.Load() } | |
| 87 | + | ||
| 81 | 88 | // Send queues line (without a newline) for the script's stdin. It never | |
| 82 | 89 | // blocks and does not allocate. It reports false when the module is not | |
| 83 | 90 | // interactive, the line is longer than 127 bytes, or 32 lines are already | |
| @@ -181,6 +188,7 @@ func (m *Module) Run(ctx context.Context) { | |||
| 181 | 188 | } | |
| 182 | 189 | ||
| 183 | 190 | func (m *Module) runOnce(ctx context.Context) error { | |
| 191 | + | m.gen.Add(1) | |
| 184 | 192 | cmd := exec.CommandContext(ctx, "/bin/sh", "-c", m.Exec) | |
| 185 | 193 | cmd.Stderr = &logWriter{log: m.log, name: m.Name} | |
| 186 | 194 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGTERM} | |
| @@ -15,6 +15,7 @@ import ( | |||||
| 15 | "path/filepath" | 15 | "path/filepath" | ||
| 16 | "strings" | 16 | "strings" | ||
| 17 | "sync" | 17 | "sync" | ||
| 18 | + | "sync/atomic" | |||
| 18 | "syscall" | 19 | "syscall" | ||
| 19 | "time" | 20 | "time" | ||
| 20 | ) | 21 | ) | ||
| @@ -55,7 +56,8 @@ type Module struct { | |||||
| 55 | dirty bool | 56 | dirty bool | ||
| 56 | failed bool // the script is not running; show an error marker | 57 | failed bool // the script is not running; show an error marker | ||
| 57 | 58 | ||||
| 58 | - | input chan event // lines for the script's stdin | 59 | + | input chan event // lines for the script's stdin |
| 60 | + | gen atomic.Uint64 // counts script starts; see Gen | |||
| 59 | 61 | ||||
| 60 | wake chan<- struct{} | 62 | wake chan<- struct{} | ||
| 61 | log *slog.Logger | 63 | log *slog.Logger | ||
| @@ -78,6 +80,11 @@ func New(name string, zone Zone, command string, wake chan<- struct{}, log *slog | |||||
| 78 | pending: make([]byte, 0, MaxFrame), input: make(chan event, 32)} | 80 | pending: make([]byte, 0, MaxFrame), input: make(chan event, 32)} | ||
| 79 | } | 81 | } | ||
| 80 | 82 | ||||
| 83 | + | // Gen counts the times the script was started. A value that changed means a | |||
| 84 | + | // new process, which knows nothing of the pointer state sent to the old one. | |||
| 85 | + | // It does not allocate. | |||
| 86 | + | func (m *Module) Gen() uint64 { return m.gen.Load() } | |||
| 87 | + | ||||
| 81 | // Send queues line (without a newline) for the script's stdin. It never | 88 | // Send queues line (without a newline) for the script's stdin. It never | ||
| 82 | // blocks and does not allocate. It reports false when the module is not | 89 | // blocks and does not allocate. It reports false when the module is not | ||
| 83 | // interactive, the line is longer than 127 bytes, or 32 lines are already | 90 | // interactive, the line is longer than 127 bytes, or 32 lines are already | ||
| @@ -181,6 +188,7 @@ func (m *Module) Run(ctx context.Context) { | |||||
| 181 | } | 188 | } | ||
| 182 | 189 | ||||
| 183 | func (m *Module) runOnce(ctx context.Context) error { | 190 | func (m *Module) runOnce(ctx context.Context) error { | ||
| 191 | + | m.gen.Add(1) | |||
| 184 | cmd := exec.CommandContext(ctx, "/bin/sh", "-c", m.Exec) | 192 | cmd := exec.CommandContext(ctx, "/bin/sh", "-c", m.Exec) | ||
| 185 | cmd.Stderr = &logWriter{log: m.log, name: m.Name} | 193 | cmd.Stderr = &logWriter{log: m.log, name: m.Name} | ||
| 186 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGTERM} | 194 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGTERM} | ||
M internal/module/module_test.go +23 -0
| @@ -226,3 +226,26 @@ func TestSendAllocs(t *testing.T) { | |||
| 226 | 226 | t.Errorf("Send allocates %.1f objects; want 0", got) | |
| 227 | 227 | } | |
| 228 | 228 | } | |
| 229 | + | ||
| 230 | + | func TestGenCountsStarts(t *testing.T) { | |
| 231 | + | wake := make(chan struct{}, 1) | |
| 232 | + | m := New("t", Left, "printf 'hi\\n'", wake, slog.New(slog.DiscardHandler)) | |
| 233 | + | if m.Gen() != 0 { | |
| 234 | + | t.Fatalf("Gen before start = %d", m.Gen()) | |
| 235 | + | } | |
| 236 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| 237 | + | defer cancel() | |
| 238 | + | go m.Run(ctx) | |
| 239 | + | deadline := time.After(4 * time.Second) | |
| 240 | + | for m.Gen() < 2 { // the script exits at once and is restarted | |
| 241 | + | select { | |
| 242 | + | case <-wake: | |
| 243 | + | case <-time.After(20 * time.Millisecond): | |
| 244 | + | case <-deadline: | |
| 245 | + | t.Fatalf("Gen = %d, want at least 2 after a restart", m.Gen()) | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | if allocs := testing.AllocsPerRun(100, func() { m.Gen() }); allocs != 0 { | |
| 249 | + | t.Errorf("Gen allocates %v", allocs) | |
| 250 | + | } | |
| 251 | + | } | |
| @@ -226,3 +226,26 @@ func TestSendAllocs(t *testing.T) { | |||||
| 226 | t.Errorf("Send allocates %.1f objects; want 0", got) | 226 | t.Errorf("Send allocates %.1f objects; want 0", got) | ||
| 227 | } | 227 | } | ||
| 228 | } | 228 | } | ||
| 229 | + | ||||
| 230 | + | func TestGenCountsStarts(t *testing.T) { | |||
| 231 | + | wake := make(chan struct{}, 1) | |||
| 232 | + | m := New("t", Left, "printf 'hi\\n'", wake, slog.New(slog.DiscardHandler)) | |||
| 233 | + | if m.Gen() != 0 { | |||
| 234 | + | t.Fatalf("Gen before start = %d", m.Gen()) | |||
| 235 | + | } | |||
| 236 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |||
| 237 | + | defer cancel() | |||
| 238 | + | go m.Run(ctx) | |||
| 239 | + | deadline := time.After(4 * time.Second) | |||
| 240 | + | for m.Gen() < 2 { // the script exits at once and is restarted | |||
| 241 | + | select { | |||
| 242 | + | case <-wake: | |||
| 243 | + | case <-time.After(20 * time.Millisecond): | |||
| 244 | + | case <-deadline: | |||
| 245 | + | t.Fatalf("Gen = %d, want at least 2 after a restart", m.Gen()) | |||
| 246 | + | } | |||
| 247 | + | } | |||
| 248 | + | if allocs := testing.AllocsPerRun(100, func() { m.Gen() }); allocs != 0 { | |||
| 249 | + | t.Errorf("Gen allocates %v", allocs) | |||
| 250 | + | } | |||
| 251 | + | } | |||