From 1c168babf2213f2b6c5bc90134a3ea74aca9934f Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sun, 2 Nov 2025 23:10:07 +0900 Subject: [PATCH] cmd/hakurei/print: use new store interface This removes the final uses of the compat interfaces. Signed-off-by: Ophestra --- cmd/hakurei/command.go | 3 +- cmd/hakurei/print.go | 103 ++++++++++++++------------------ cmd/hakurei/print_test.go | 121 ++++++++++++++++++++++---------------- 3 files changed, 116 insertions(+), 111 deletions(-) diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index b2302b3..6f72712 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -20,7 +20,6 @@ import ( "hakurei.app/internal" "hakurei.app/internal/env" "hakurei.app/internal/outcome" - "hakurei.app/internal/store" "hakurei.app/message" "hakurei.app/system/dbus" ) @@ -340,7 +339,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr c.NewCommand("ps", "List active instances", func(args []string) error { var sc hst.Paths env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil)) - printPs(os.Stdout, time.Now().UTC(), store.NewMulti(msg, sc.SharePath), flagShort, flagJSON) + printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON) return errSuccess }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") } diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index eaa8661..c6f060a 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "log" - "maps" "slices" "strconv" "strings" @@ -170,52 +169,52 @@ func printShowInstance( } // printPs writes a representation of active instances to output. -func printPs(output io.Writer, now time.Time, s store.Compat, short, flagJSON bool) { - var entries map[hst.ID]*hst.State - if e, err := store.Join(s); err != nil { - log.Fatalf("cannot join store: %v", err) - } else { - entries = e +func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) { + f := func(a func(eh *store.EntryHandle)) { + entries, copyError := s.All() + for eh := range entries { + a(eh) + } + if err := copyError(); err != nil { + msg.GetLogger().Println(getMessage("cannot iterate over store:", err)) + } } - if !short && flagJSON { - es := slices.Collect(maps.Values(entries)) - slices.SortFunc(es, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) }) - encodeJSON(log.Fatal, output, short, es) + if short { // short output requires identifier only + var identifiers []*hst.ID + f(func(eh *store.EntryHandle) { + if _, err := eh.Load(nil); err != nil { // passes through decode error + msg.GetLogger().Println(getMessage("cannot validate state entry header:", err)) + return + } + identifiers = append(identifiers, &eh.ID) + }) + slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) }) + + if flagJSON { + encodeJSON(log.Fatal, output, short, identifiers) + } else { + for _, id := range identifiers { + mustPrintln(output, shortIdentifier(id)) + } + } return } - // sort state entries by id string to ensure consistency between runs - exp := make([]*expandedStateEntry, 0, len(entries)) - for id, instance := range entries { - // gracefully skip nil states - if instance == nil { - log.Printf("got invalid state entry %s", id.String()) - continue + // long output requires full instance state + var instances []*hst.State + f(func(eh *store.EntryHandle) { + var state hst.State + if _, err := eh.Load(&state); err != nil { // passes through decode error + msg.GetLogger().Println(getMessage("cannot load state entry:", err)) + return } + instances = append(instances, &state) + }) + slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) }) - // gracefully skip inconsistent states - if id != instance.ID { - log.Printf("possible store corruption: entry %s has id %s", - id.String(), instance.ID.String()) - continue - } - exp = append(exp, &expandedStateEntry{s: id.String(), State: instance}) - } - slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) }) - - if short { - if flagJSON { - v := make([]string, len(exp)) - for i, e := range exp { - v[i] = e.s - } - encodeJSON(log.Fatal, output, short, v) - } else { - for _, e := range exp { - mustPrintln(output, shortIdentifierString(e.s)) - } - } + if flagJSON { + encodeJSON(log.Fatal, output, short, instances) return } @@ -223,33 +222,21 @@ func printPs(output io.Writer, now time.Time, s store.Compat, short, flagJSON bo defer t.MustFlush() t.Println("\tInstance\tPID\tApplication\tUptime") - for _, e := range exp { - if len(e.s) != 1<<5 { - // unreachable - log.Printf("possible store corruption: invalid instance string %s", e.s) - continue - } - + for _, instance := range instances { as := "(No configuration information)" - if e.Config != nil { - as = strconv.Itoa(e.Config.Identity) - id := e.Config.ID + if instance.Config != nil { + as = strconv.Itoa(instance.Config.Identity) + id := instance.Config.ID if id == "" { - id = "app.hakurei." + shortIdentifierString(e.s) + id = "app.hakurei." + shortIdentifier(&instance.ID) } as += " (" + id + ")" } t.Printf("\t%s\t%d\t%s\t%s\n", - shortIdentifierString(e.s), e.PID, as, now.Sub(e.Time).Round(time.Second).String()) + shortIdentifier(&instance.ID), instance.PID, as, now.Sub(instance.Time).Round(time.Second).String()) } } -// expandedStateEntry stores [hst.State] alongside a string representation of its [hst.ID]. -type expandedStateEntry struct { - s string - *hst.State -} - // newPrinter returns a configured, wrapped [tabwriter.Writer]. func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} } diff --git a/cmd/hakurei/print_test.go b/cmd/hakurei/print_test.go index 5309e37..5a7d214 100644 --- a/cmd/hakurei/print_test.go +++ b/cmd/hakurei/print_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "log" "strings" "testing" "time" @@ -9,6 +10,7 @@ import ( "hakurei.app/container/check" "hakurei.app/hst" "hakurei.app/internal/store" + "hakurei.app/message" ) var ( @@ -18,13 +20,30 @@ var ( 0x4c, 0xf0, 0x73, 0xbd, 0xb4, 0x6e, 0xb5, 0xc1, } - testState = &hst.State{ + testState = hst.State{ ID: testID, PID: 0xcafebabe, ShimPID: 0xdeadbeef, Config: hst.Template(), Time: testAppTime, } + testStateSmall = hst.State{ + ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), + PID: 0xbeef, + ShimPID: 0xcafe, + Config: &hst.Config{ + Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse), + Identity: 1, + Container: &hst.ContainerConfig{ + Shell: check.MustAbs("/bin/sh"), + Home: check.MustAbs("/data/data/uk.gensokyo.cat"), + Path: check.MustAbs("/usr/bin/cat"), + Args: []string{"cat"}, + Flags: hst.FUserns, + }, + }, + Time: time.Unix(0, 0xdeadbeef).UTC(), + } testTime = time.Unix(3752, 1).UTC() testAppTime = time.Unix(0, 9).UTC() ) @@ -133,7 +152,7 @@ Session bus `, false}, - {"instance", testState, hst.Template(), false, false, `State + {"instance", &testState, hst.Template(), false, false, `State Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3405691582 -> 3735928559) Uptime: 1h2m32s @@ -173,7 +192,7 @@ System bus Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"] `, true}, - {"instance pd", testState, new(hst.Config), false, false, `Error: configuration missing container state! + {"instance pd", &testState, new(hst.Config), false, false, `Error: configuration missing container state! State Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3405691582 -> 3735928559) @@ -187,7 +206,7 @@ App {"json nil", nil, nil, false, true, `null `, true}, - {"json instance", testState, nil, false, true, `{ + {"json instance", &testState, nil, false, true, `{ "instance": "8e2c76b066dabe574cf073bdb46eb5c1", "pid": 3405691582, "shim_pid": 3735928559, @@ -515,41 +534,27 @@ func TestPrintPs(t *testing.T) { testCases := []struct { name string - entries map[hst.ID]*hst.State + data []hst.State short, json bool - want string + want, log string }{ - {"no entries", make(map[hst.ID]*hst.State), false, false, " Instance PID Application Uptime\n"}, - {"no entries short", make(map[hst.ID]*hst.State), true, false, ""}, - {"nil instance", map[hst.ID]*hst.State{testID: nil}, false, false, " Instance PID Application Uptime\n"}, - {"state corruption", map[hst.ID]*hst.State{hst.ID{}: testState}, false, false, " Instance PID Application Uptime\n"}, + {"no entries", []hst.State{}, false, false, " Instance PID Application Uptime\n", ""}, + {"no entries short", []hst.State{}, true, false, "", ""}, - {"valid pd", map[hst.ID]*hst.State{testID: {ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime - 4cf073bd 256 0 (app.hakurei.4cf073bd) 1h2m32s -`}, + {"invalid config", []hst.State{{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, " Instance PID Application Uptime\n", "check: configuration missing container state\n"}, - {"valid", map[hst.ID]*hst.State{testID: testState}, false, false, ` Instance PID Application Uptime + {"valid", []hst.State{testStateSmall, testState}, false, false, ` Instance PID Application Uptime 4cf073bd 3405691582 9 (org.chromium.Chromium) 1h2m32s -`}, - {"valid short", map[hst.ID]*hst.State{testID: testState}, true, false, "4cf073bd\n"}, - {"valid json", map[hst.ID]*hst.State{testID: testState, (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))): { - ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), - PID: 0xbeef, - ShimPID: 0xcafe, - Config: &hst.Config{ - ID: "uk.gensokyo.cat", - Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse), - Identity: 1, - Container: &hst.ContainerConfig{ - Shell: check.MustAbs("/bin/sh"), - Home: check.MustAbs("/data/data/uk.gensokyo.cat"), - Path: check.MustAbs("/usr/bin/cat"), - Args: []string{"cat"}, - Flags: hst.FUserns, - }, - }, - Time: time.Unix(0, 0xdeadbeef).UTC(), - }}, false, true, `[ + aaaaaaaa 48879 1 (app.hakurei.aaaaaaaa) 1h2m28s +`, ""}, + {"valid single", []hst.State{testState}, false, false, ` Instance PID Application Uptime + 4cf073bd 3405691582 9 (org.chromium.Chromium) 1h2m32s +`, ""}, + + {"valid short", []hst.State{testStateSmall, testState}, true, false, "4cf073bd\naaaaaaaa\n", ""}, + {"valid short single", []hst.State{testState}, true, false, "4cf073bd\n", ""}, + + {"valid json", []hst.State{testState, testStateSmall}, false, true, `[ { "instance": "8e2c76b066dabe574cf073bdb46eb5c1", "pid": 3405691582, @@ -707,7 +712,7 @@ func TestPrintPs(t *testing.T) { "instance": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "pid": 48879, "shim_pid": 51966, - "id": "uk.gensokyo.cat", + "id": "", "enablements": { "wayland": true, "pulse": true @@ -729,30 +734,44 @@ func TestPrintPs(t *testing.T) { "time": "1970-01-01T00:00:03.735928559Z" } ] -`}, - {"valid short json", map[hst.ID]*hst.State{testID: testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1"] -`}, +`, ""}, + {"valid short json", []hst.State{testStateSmall, testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] +`, ""}, } for _, tc := range testCases { + s := store.New(check.MustAbs(t.TempDir()).Append("store")) + for i := range tc.data { + if h, err := s.Handle(tc.data[i].Identity); err != nil { + t.Fatalf("Handle: error = %v", err) + } else { + var unlock func() + if unlock, err = h.Lock(); err != nil { + t.Fatalf("Lock: error = %v", err) + } + _, err = h.Save(&tc.data[i]) + unlock() + if err != nil { + t.Fatalf("Save: error = %v", err) + } + } + } + + // store must not be written to beyond this point t.Run(tc.name, func(t *testing.T) { t.Parallel() - output := new(strings.Builder) - printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json) - if got := output.String(); got != tc.want { - t.Errorf("printPs: got\n%s\nwant\n%s", - got, tc.want) + var printBuf, logBuf bytes.Buffer + msg := message.NewMsg(log.New(&logBuf, "check: ", 0)) + msg.SwapVerbose(true) + printPs(msg, &printBuf, testTime, s, tc.short, tc.json) + if got := printBuf.String(); got != tc.want { + t.Errorf("printPs:\n%s\nwant\n%s", got, tc.want) return } + if got := logBuf.String(); got != tc.log { + t.Errorf("msg:\n%s\nwant\n%s", got, tc.log) + } }) } } - -// stubStore implements [state.Store] and returns test samples via [state.Joiner]. -type stubStore map[hst.ID]*hst.State - -func (s stubStore) Join() (map[hst.ID]*hst.State, error) { return s, nil } -func (s stubStore) Do(int, func(c store.Cursor)) (bool, error) { panic("unreachable") } -func (s stubStore) List() ([]int, error) { panic("unreachable") } -func (s stubStore) Close() error { return nil }