feat(autotrack): Run poll loop with SIGTERM/SIGINT segment flush
b44c48ebf976ecfed2cac22296e9054f24eb94a1
Kevin Kortum <kevinkortum@me.com> · 2026-07-08 12:49
parent 77789174
2 files changed
internal/autotrack/run.go +39 −0
@@ -0,0 +1,39 @@
+package autotrack
+
+import (
+ "os"
+ "time"
+
+ "ticktock/internal/config"
+)
+
+// Run executes the poll loop: sample → Classify → Step → WriteSegment →
+// sleep, until a signal arrives on sigc; then it flushes the open segment and
+// returns (parity with the Python daemon's SIGTERM/SIGINT flush). sample is
+// injected (ReadSample in production) so the loop is testable.
+func Run(dataDir string, tr config.Tracking, sample func() Sample, sigc <-chan os.Signal) {
+ var cur *Segment
+ var curKey Key
+ poll := time.Duration(tr.PollSecs) * time.Second
+ for {
+ now := time.Now()
+ s := sample()
+ hard := s.Locked || Away(s.Raw)
+ ctx := Classify(s.Idle, s.Raw, tr.IdleGraceSecs, s.Locked)
+ var emitted *Segment
+ cur, curKey, emitted = Step(cur, curKey, now, s.Idle, ctx, tr.IdleGraceSecs, hard)
+ if emitted != nil {
+ _ = WriteSegment(dataDir, emitted, tr.MinSecs) // an IO hiccup must not kill the loop
+ }
+ select {
+ case <-sigc:
+ if cur != nil {
+ closed := *cur
+ closed.End = time.Now()
+ _ = WriteSegment(dataDir, &closed, tr.MinSecs)
+ }
+ return
+ case <-time.After(poll):
+ }
+ }
+}
internal/autotrack/run_test.go +39 −0
@@ -0,0 +1,39 @@
+package autotrack
+
+import (
+ "os"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ "ticktock/internal/config"
+)
+
+func TestRunFlushesOpenSegmentOnSignal(t *testing.T) {
+ dir := t.TempDir()
+ sigc := make(chan os.Signal, 1)
+ sample := func() Sample {
+ return Sample{Idle: 0, Raw: &Ctx{App: "TestApp", Title: "doc"}}
+ }
+ done := make(chan struct{})
+ go func() {
+ Run(dir, config.Tracking{PollSecs: 1, IdleGraceSecs: 900, MinSecs: 0}, sample, sigc)
+ close(done)
+ }()
+ time.Sleep(50 * time.Millisecond) // let the first poll open a segment
+ sigc <- syscall.SIGTERM
+ select {
+ case <-done:
+ case <-time.After(2 * time.Second):
+ t.Fatal("Run did not return after signal")
+ }
+ b, err := os.ReadFile(LogPath(dir, time.Now()))
+ if err != nil {
+ t.Fatalf("flush should have written today's log: %v", err)
+ }
+ line := strings.TrimSpace(string(b))
+ if !strings.Contains(line, `"app": "TestApp"`) || !strings.Contains(line, `"idle": false`) {
+ t.Errorf("flushed segment malformed: %s", line)
+ }
+}