detached 718054e

feat(module): count script starts with Gen

commit
718054e7612e0f188e5123cccffa59c81f3e2074
author
brice · · 2026-10-09 03:59 UTC
parent
3c1ce2c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +32 -1
M internal/module/module.go +9 -1
@@ -15,6 +15,7 @@ import (
1515 "path/filepath"
1616 "strings"
1717 "sync"
18+ "sync/atomic"
1819 "syscall"
1920 "time"
2021 )
@@ -55,7 +56,8 @@ type Module struct {
5556 dirty bool
5657 failed bool // the script is not running; show an error marker
5758
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
5961
6062 wake chan<- struct{}
6163 log *slog.Logger
@@ -78,6 +80,11 @@ func New(name string, zone Zone, command string, wake chan<- struct{}, log *slog
7880 pending: make([]byte, 0, MaxFrame), input: make(chan event, 32)}
7981 }
8082
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+
8188 // Send queues line (without a newline) for the script's stdin. It never
8289 // blocks and does not allocate. It reports false when the module is not
8390 // interactive, the line is longer than 127 bytes, or 32 lines are already
@@ -181,6 +188,7 @@ func (m *Module) Run(ctx context.Context) {
181188 }
182189
183190 func (m *Module) runOnce(ctx context.Context) error {
191+ m.gen.Add(1)
184192 cmd := exec.CommandContext(ctx, "/bin/sh", "-c", m.Exec)
185193 cmd.Stderr = &logWriter{log: m.log, name: m.Name}
186194 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 bool56 dirty bool
56 failed bool // the script is not running; show an error marker57 failed bool // the script is not running; show an error marker
57 58
58- input chan event // lines for the script's stdin59+ 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.Logger63 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 never88 // 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 not89 // 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 already90 // 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) {
226226 t.Errorf("Send allocates %.1f objects; want 0", got)
227227 }
228228 }
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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 5

Tags 4

↑↓ moveenter openesc close