aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 21:21:58 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 21:21:58 +0200
commita92ce86c0dff7ce8f1de4113f20edd76c249371e (patch)
treec41a4f01c610a49cf3b60e9e9e658d6cb8b94b33 /internal
parent3890b27b5a68f5521f4f0a234cebf528744356ee (diff)
downloadkrino-a92ce86c0dff7ce8f1de4113f20edd76c249371e.tar.gz
krino-a92ce86c0dff7ce8f1de4113f20edd76c249371e.zip
plan 9: the log restores a missing final newline
Diffstat (limited to 'internal')
-rw-r--r--internal/journal/journal.go26
-rw-r--r--internal/journal/journal_test.go27
2 files changed, 52 insertions, 1 deletions
diff --git a/internal/journal/journal.go b/internal/journal/journal.go
index 465cd51..085c88a 100644
--- a/internal/journal/journal.go
+++ b/internal/journal/journal.go
@@ -54,13 +54,37 @@ func Open(path string) (*Writer, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, fmt.Errorf("journal: %w", err)
}
- f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o644)
if err != nil {
return nil, fmt.Errorf("journal: %w", err)
}
+ if err := endWithNewline(f); err != nil {
+ f.Close()
+ return nil, fmt.Errorf("journal: %w", err)
+ }
return &Writer{f: f}, nil
}
+// endWithNewline restores a missing final newline. A crash can cut the log's
+// last line short (spec ยง15.1: krino survives a truncated line); appending
+// straight after the fragment would glue the next run's first line onto it,
+// and that run could then never be undone.
+func endWithNewline(f *os.File) error {
+ fi, err := f.Stat()
+ if err != nil || fi.Size() == 0 {
+ return err
+ }
+ last := make([]byte, 1)
+ if _, err := f.ReadAt(last, fi.Size()-1); err != nil {
+ return err
+ }
+ if last[0] == '\n' {
+ return nil
+ }
+ _, err = f.Write([]byte{'\n'})
+ return err
+}
+
// Append writes e as one line and flushes it before returning. The whole
// line is written with a single Write call so that two concurrent runs
// appending to the same file cannot interleave a partial line.
diff --git a/internal/journal/journal_test.go b/internal/journal/journal_test.go
index a95bb05..e8b18ae 100644
--- a/internal/journal/journal_test.go
+++ b/internal/journal/journal_test.go
@@ -148,3 +148,30 @@ func TestAppendIsAppendOnly(t *testing.T) {
t.Errorf("%d lines after two Open/Append/Close cycles, want 2: the second Open truncated", got)
}
}
+
+// TestOpenRepairsAMissingFinalNewline: a crash can leave the log's last
+// line without its newline; the next run's first line must not be glued
+// onto it, or that run could never be undone (review M8).
+func TestOpenRepairsAMissingFinalNewline(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "krino.log")
+ if err := os.WriteFile(path, []byte("2026-09-11T10:02:03+02:00\tR0\tdl\ta.pdf\t1\tmo"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ w, err := Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ at := time.Date(2026, 9, 11, 10, 5, 0, 0, time.UTC)
+ for _, e := range []Entry{{Time: at, Run: "R1", Action: "run-start", Status: "ok"}, {Time: at, Run: "R1", Action: "run-end", Status: "ok"}} {
+ if err := w.Append(e); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := w.Close(); err != nil {
+ t.Fatal(err)
+ }
+ entries, err := Entries(path, "R1")
+ if err != nil || len(entries) != 2 {
+ t.Fatalf("Entries(R1) = %d entries, %v; want run-start and run-end", len(entries), err)
+ }
+}