14 Commits

Author SHA1 Message Date
ec5cb9400c cmd/hpkg/test: print share directory
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m30s
This is more useful now that state is tracked here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:51:57 +09:00
ae66b3d2fb message: rename NewMsg to New
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m37s
Should have done this when relocating this from container. Now is a good time to rename it before v0.3.x.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:49:27 +09:00
149bc3671a internal/store: remove compat adapter
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m17s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m30s
This is no longer used as everything has been migrated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:26:01 +09:00
24435694a5 hst/config: make identifier omitempty
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m33s
This is an optional field. Serialise it as such.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:23:15 +09:00
1c168babf2 cmd/hakurei/print: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m40s
This removes the final uses of the compat interfaces.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:19:16 +09:00
0edcb7c1d3 test: print share directory
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 41s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m24s
Test / Hakurei (race detector) (push) Successful in 3m3s
Test / Flake checks (push) Successful in 1m29s
This is more useful now that state is tracked here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 17:00:59 +09:00
0e5ca74b98 cmd/hakurei/print: serialise array for ps
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 2m25s
Test / Hakurei (race detector) (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m13s
Test / Flake checks (push) Successful in 1m27s
Wanted to do this for a long time, since the key is redundant. This also makes it easier to migrate to the new store interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 16:37:08 +09:00
23ae7822bf cmd/hakurei/parse: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hpkg (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m28s
This greatly reduces overhead. The iterator also significantly cleans up the usage code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 16:00:41 +09:00
898b5aed3d internal/store: iterator over all entries
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m31s
This is quite convenient for searching the store or printing active instance information.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 15:54:00 +09:00
7c3c3135d8 internal/outcome: track state in TMPDIR
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m56s
Test / Flake checks (push) Successful in 1m30s
The SharePath is a more stable path than RunDirPath, since it is available all the time and should remain consistent. This also fits better into the intended use case of XDG_RUNTIME_DIR.

Closes #17.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 12:40:58 +09:00
f33aea9ff9 internal/env: cleaner runtime dir fallback
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m28s
This now places rundir inside the fallback runtime dir, so special case in internal/outcome is avoided.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 12:22:32 +09:00
e7fc311d0b internal/outcome/shim: cover reparent and exit request paths
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m31s
These test cases were missed when making the changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 11:58:09 +09:00
f5274067f6 internal/outcome/process: nil-safe unlock when failing to lock
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m26s
This also prints a debug message which might be useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 11:47:51 +09:00
e7161f8e61 internal/outcome: measure finalise time
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m8s
Test / Flake checks (push) Successful in 1m19s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m56s
This also increases precision of state time output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 05:17:33 +09:00
38 changed files with 608 additions and 589 deletions

View File

@@ -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"
)
@@ -294,7 +293,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}
{
var flagShort bool
var (
flagShort bool
flagNoStore bool
)
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) {
case 0: // system
@@ -302,10 +304,23 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
case 1: // instance
name := args[0]
config, entry := tryIdentifier(msg, name)
if config == nil {
config = tryPath(msg, name)
var (
config *hst.Config
entry *hst.State
)
if !flagNoStore {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
}
if entry == nil {
config = tryPath(msg, name)
} else {
config = entry.Config
}
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
os.Exit(1)
}
@@ -314,7 +329,9 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
log.Fatal("show requires 1 argument")
}
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information")
}).
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
}
{
@@ -322,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.RunDirPath), 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")
}

View File

@@ -77,7 +77,7 @@ Flags:
t.Parallel()
out := new(bytes.Buffer)
c := buildCommand(t.Context(), message.NewMsg(nil), new(earlyHardeningErrs), out)
c := buildCommand(t.Context(), message.New(nil), new(earlyHardeningErrs), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)

View File

@@ -32,7 +32,7 @@ func main() {
log.SetPrefix("hakurei: ")
log.SetFlags(0)
msg := message.NewMsg(log.Default())
msg := message.New(log.Default())
early := earlyHardeningErrs{
yamaLSM: container.SetPtracer(0),

View File

@@ -11,8 +11,6 @@ import (
"syscall"
"hakurei.app/hst"
"hakurei.app/internal/env"
"hakurei.app/internal/outcome"
"hakurei.app/internal/store"
"hakurei.app/message"
)
@@ -81,26 +79,7 @@ func shortIdentifierString(s string) string {
}
// tryIdentifier attempts to match [hst.State] from a [hex] representation of [hst.ID] or a prefix of its lower half.
func tryIdentifier(msg message.Msg, name string) (config *hst.Config, entry *hst.State) {
return tryIdentifierEntries(msg, name, func() map[hst.ID]*hst.State {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
s := store.NewMulti(msg, sc.RunDirPath)
if entries, err := store.Join(s); err != nil {
msg.GetLogger().Printf("cannot join store: %v", err) // not fatal
return nil
} else {
return entries
}
})
}
// tryIdentifierEntries implements tryIdentifier with a custom entries pair getter.
func tryIdentifierEntries(
msg message.Msg,
name string,
getEntries func() map[hst.ID]*hst.State,
) (config *hst.Config, entry *hst.State) {
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
const (
likeShort = 1 << iota
likeFull
@@ -116,7 +95,7 @@ func tryIdentifierEntries(
if c >= 'a' && c <= 'f' {
continue
}
return
return nil
}
likely |= likeShort
} else if len(name) == hex.EncodedLen(len(hst.ID{})) {
@@ -124,40 +103,58 @@ func tryIdentifierEntries(
}
if likely == 0 {
return
}
entries := getEntries()
if entries == nil {
return
return nil
}
entries, copyError := s.All()
defer func() {
if err := copyError(); err != nil {
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
}
}()
switch {
case likely&likeShort != 0:
msg.Verbose("argument looks like short identifier")
for id := range entries {
v := id.String()
if strings.HasPrefix(v[len(hst.ID{}):], name) {
// match, use config from this state entry
entry = entries[id]
config = entry.Config
break
for eh := range entries {
if eh.DecodeErr != nil {
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
continue
}
msg.Verbosef("instance %s skipped", v)
if strings.HasPrefix(eh.ID.String()[len(hst.ID{}):], name) {
var entry hst.State
if _, err := eh.Load(&entry); err != nil {
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
continue
}
return &entry
}
}
return
return nil
case likely&likeFull != 0:
var likelyID hst.ID
if likelyID.UnmarshalText([]byte(name)) != nil {
return
return nil
}
msg.Verbose("argument looks like identifier")
if ent, ok := entries[likelyID]; ok {
entry = ent
config = ent.Config
for eh := range entries {
if eh.DecodeErr != nil {
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
continue
}
if eh.ID == likelyID {
var entry hst.State
if _, err := eh.Load(&entry); err != nil {
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
continue
}
return &entry
}
}
return
return nil
default:
panic("unreachable")

View File

@@ -1,10 +1,14 @@
package main
import (
"bytes"
"reflect"
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
"hakurei.app/message"
)
@@ -23,17 +27,47 @@ func TestShortIdentifier(t *testing.T) {
func TestTryIdentifier(t *testing.T) {
t.Parallel()
msg := message.NewMsg(nil)
msg := message.New(nil)
id := hst.ID{
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
}
withBase := func(extra ...hst.State) []hst.State {
return append([]hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xab}, len(hst.ID{}))), PID: 0x1beef, ShimPID: 0x1cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xf0}, len(hst.ID{}))), PID: 0x2beef, ShimPID: 0x2cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef2)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfe}, len(hst.ID{}))), PID: 0xbed, ShimPID: 0xfff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = hst.IdentityMax
return template
}(), Time: time.Unix(0, 0xcafebabe0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfc}, len(hst.ID{}))), PID: 0x1bed, ShimPID: 0x1fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xfc
return template
}(), Time: time.Unix(0, 0xcafebabe1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xce}, len(hst.ID{}))), PID: 0x2bed, ShimPID: 0x2fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xce
return template
}(), Time: time.Unix(0, 0xcafebabe2)},
}, extra...)
}
sampleEntry := hst.State{
ID: id,
PID: 0xcafebabe,
ShimPID: 0xdeadbeef,
Config: hst.Template(),
}
testCases := []struct {
name string
s string
entries map[hst.ID]*hst.State
want *hst.State
name string
s string
data []hst.State
want *hst.State
}{
{"likely entries fault", "ffffffff", nil, nil},
@@ -41,58 +75,40 @@ func TestTryIdentifier(t *testing.T) {
{"likely short too long", "fffffffffffffffff", nil, nil},
{"likely short invalid lower", "fffffff\x00", nil, nil},
{"likely short invalid higher", "0000000\xff", nil, nil},
{"short no match", "fedcba98", map[hst.ID]*hst.State{hst.ID{}: nil}, nil},
{"short match", "fedcba98", map[hst.ID]*hst.State{
hst.ID{}: nil,
id: {
ID: id,
PID: 0xcafebabe,
ShimPID: 0xdeadbeef,
Config: hst.Template(),
},
}, &hst.State{
ID: id,
PID: 0xcafebabe,
ShimPID: 0xdeadbeef,
Config: hst.Template(),
}},
{"short match longer", "fedcba98765", map[hst.ID]*hst.State{
hst.ID{}: nil,
id: {
ID: id,
PID: 0xcafebabe,
ShimPID: 0xdeadbeef,
Config: hst.Template(),
},
}, &hst.State{
ID: id,
PID: 0xcafebabe,
ShimPID: 0xdeadbeef,
Config: hst.Template(),
}},
{"short no match", "fedcba98", withBase(), nil},
{"short match", "fedcba98", withBase(sampleEntry), &sampleEntry},
{"short match single", "fedcba98", []hst.State{sampleEntry}, &sampleEntry},
{"short match longer", "fedcba98765", withBase(sampleEntry), &sampleEntry},
{"likely long invalid", "0123456789abcdeffedcba987654321\x00", map[hst.ID]*hst.State{}, nil},
{"long no match", "0123456789abcdeffedcba9876543210", map[hst.ID]*hst.State{hst.ID{}: nil}, nil},
{"long match", "0123456789abcdeffedcba9876543210", map[hst.ID]*hst.State{
hst.ID{}: nil,
id: {
ID: id,
PID: 0xcafebabe,
ShimPID: 0xdeadbeef,
Config: hst.Template(),
},
}, &hst.State{
ID: id,
PID: 0xcafebabe,
ShimPID: 0xdeadbeef,
Config: hst.Template(),
}},
{"likely long invalid", "0123456789abcdeffedcba987654321\x00", nil, nil},
{"long no match", "0123456789abcdeffedcba9876543210", withBase(), nil},
{"long match", "0123456789abcdeffedcba9876543210", withBase(sampleEntry), &sampleEntry},
{"long match single", "0123456789abcdeffedcba9876543210", []hst.State{sampleEntry}, &sampleEntry},
}
for _, tc := range testCases {
base := check.MustAbs(t.TempDir()).Append("store")
s := store.New(base)
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()
_, got := tryIdentifierEntries(msg, tc.s, func() map[hst.ID]*hst.State { return tc.entries })
got := tryIdentifier(msg, tc.s, store.New(base))
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("tryIdentifier: %#v, want %#v", got, tc.want)
}

View File

@@ -1,6 +1,7 @@
package main
import (
"bytes"
"fmt"
"io"
"log"
@@ -168,54 +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 := make(map[string]*hst.State, len(entries))
for id, instance := range entries {
es[id.String()] = instance
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))
}
}
encodeJSON(log.Fatal, output, short, es)
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)} }
@@ -287,3 +274,11 @@ func mustPrintln(output io.Writer, a ...any) {
log.Fatalf("cannot print: %v", err)
}
}
// getMessage returns a [message.Error] message if available, or err prefixed with fallback otherwise.
func getMessage(fallback string, err error) string {
if m, ok := message.GetMessage(err); ok {
return m
}
return fmt.Sprintln(fallback, err)
}

View File

@@ -1,12 +1,16 @@
package main
import (
"bytes"
"log"
"strings"
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
"hakurei.app/message"
)
var (
@@ -16,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()
)
@@ -131,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
@@ -171,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)
@@ -185,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,
@@ -513,25 +534,28 @@ 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}, false, true, `{
"8e2c76b066dabe574cf073bdb46eb5c1": {
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,
"shim_pid": 3735928559,
@@ -683,32 +707,70 @@ func TestPrintPs(t *testing.T) {
"share_tmpdir": true
},
"time": "1970-01-01T00:00:00.000000009Z"
},
{
"instance": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"pid": 48879,
"shim_pid": 51966,
"enablements": {
"wayland": true,
"pulse": true
},
"identity": 1,
"groups": null,
"container": {
"env": null,
"filesystem": null,
"shell": "/bin/sh",
"home": "/data/data/uk.gensokyo.cat",
"path": "/usr/bin/cat",
"args": [
"cat"
],
"userns": true,
"map_real_uid": false
},
"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.New(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 }

View File

@@ -24,7 +24,7 @@ var (
func main() {
log.SetPrefix("hpkg: ")
log.SetFlags(0)
msg := message.NewMsg(log.Default())
msg := message.New(log.Default())
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)

View File

@@ -58,7 +58,7 @@ def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values()))
instance = instances[0]
if len(instance['container']['args']) != 1 or not (instance['container']['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (instance['container']['args'][0]):
raise Exception(f"unexpected args {instance['container']['args']}")
@@ -102,5 +102,9 @@ machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents:
print(machine.succeed("find /run/user/1000/hakurei"))
# Print hakurei share and rundir contents:
print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print"))
print(machine.succeed("find /run/user/1000/hakurei"))

View File

@@ -404,7 +404,7 @@ func (p *Container) ProcessState() *os.ProcessState {
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context, msg message.Msg) *Container {
if msg == nil {
msg = message.NewMsg(nil)
msg = message.New(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}

View File

@@ -556,7 +556,7 @@ func testContainerCancel(
func TestContainerString(t *testing.T) {
t.Parallel()
msg := message.NewMsg(nil)
msg := message.New(nil)
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
@@ -721,7 +721,7 @@ func TestMain(m *testing.M) {
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
msg := message.NewMsg(nil)
msg := message.New(nil)
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0)

View File

@@ -11,7 +11,7 @@ import (
func TestExecutable(t *testing.T) {
t.Parallel()
for i := 0; i < 16; i++ {
if got := container.MustExecutable(message.NewMsg(nil)); got != os.Args[0] {
if got := container.MustExecutable(message.New(nil)); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q", got, os.Args[0])
}
}

View File

@@ -461,7 +461,7 @@ func TryArgv0(msg message.Msg) {
if msg == nil {
log.SetPrefix(initName + ": ")
log.SetFlags(0)
msg = message.NewMsg(log.Default())
msg = message.New(log.Default())
}
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {

View File

@@ -12,7 +12,7 @@ import (
type Config struct {
// Reverse-DNS style configured arbitrary identifier string.
// Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy.
ID string `json:"id"`
ID string `json:"id,omitempty"`
// System services to make available in the container.
Enablements *Enablements `json:"enablements,omitempty"`

5
internal/env/env.go vendored
View File

@@ -29,12 +29,11 @@ func (env *Paths) Copy(v *hst.Paths, userid int) {
if env.RuntimePath == nil {
// fall back to path in share since hakurei has no hard XDG dependency
v.RunDirPath = v.SharePath.Append("run")
v.RuntimePath = v.RunDirPath.Append("compat")
v.RuntimePath = v.SharePath.Append("compat")
} else {
v.RuntimePath = env.RuntimePath
v.RunDirPath = env.RuntimePath.Append("hakurei")
}
v.RunDirPath = v.RuntimePath.Append("hakurei")
}
// CopyPaths returns a populated [Paths].

View File

@@ -35,8 +35,8 @@ func TestPaths(t *testing.T) {
}, hst.Paths{
TempDir: fhs.AbsTmp,
SharePath: fhs.AbsTmp.Append("hakurei.3735928559"),
RuntimePath: fhs.AbsTmp.Append("hakurei.3735928559/run/compat"),
RunDirPath: fhs.AbsTmp.Append("hakurei.3735928559/run"),
RuntimePath: fhs.AbsTmp.Append("hakurei.3735928559/compat"),
RunDirPath: fhs.AbsTmp.Append("hakurei.3735928559/compat/hakurei"),
}, ""},
{"full", &env.Paths{

View File

@@ -690,6 +690,7 @@ func (panicMsgContext) Value(any) any { panic("unreachable") }
type panicDispatcher struct{}
func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
func (panicDispatcher) getppid() int { panic("unreachable") }
func (panicDispatcher) getpid() int { panic("unreachable") }
func (panicDispatcher) getuid() int { panic("unreachable") }
func (panicDispatcher) getgid() int { panic("unreachable") }

View File

@@ -3,6 +3,7 @@ package outcome
import (
"context"
"log"
"time"
"hakurei.app/hst"
"hakurei.app/message"
@@ -16,10 +17,13 @@ func Main(ctx context.Context, msg message.Msg, config *hst.Config) {
}
seal := outcome{syscallDispatcher: direct{msg}}
finaliseTime := time.Now()
if err := seal.finalise(ctx, msg, &id, config); err != nil {
printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err)
panic("unreachable")
}
msg.Verbosef("finalise took %.2f ms", float64(time.Since(finaliseTime).Nanoseconds())/1e6)
seal.main(msg)
panic("unreachable")

View File

@@ -29,7 +29,7 @@ import (
func TestOutcomeMain(t *testing.T) {
t.Parallel()
msg := message.NewMsg(nil)
msg := message.New(nil)
msg.SwapVerbose(testing.Verbose())
testCases := []struct {
@@ -40,7 +40,7 @@ func TestOutcomeMain(t *testing.T) {
wantSys *system.I
wantParams *container.Params
}{
{"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.NewMsg(nil), 1000009).
{"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.New(nil), 1000009).
// spParamsOp
Ensure(m("/tmp/hakurei.0"), 0711).
@@ -68,10 +68,10 @@ func TestOutcomeMain(t *testing.T) {
).
// ensureRuntimeDir
Ensure(m("/run/user/1971/hakurei"), 0700).
UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ensure(m("/run/user/1971"), 0700).
UpdatePermType(system.User, m("/run/user/1971"), acl.Execute).
Ensure(m("/run/user/1971/hakurei"), 0700).
UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
// runtime
Ephemeral(system.Process, m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0700).
@@ -347,8 +347,8 @@ func TestOutcomeMain(t *testing.T) {
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
MustProxyDBus(&hst.BusConfig{
@@ -499,8 +499,8 @@ func TestOutcomeMain(t *testing.T) {
Ensure(m("/tmp/hakurei.0/runtime/1"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read, acl.Write, acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read, acl.Write, acl.Execute).
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).

View File

@@ -196,10 +196,10 @@ func (state *outcomeStateSys) ensureRuntimeDir() {
return
}
state.useRuntimeDir = true
state.sys.Ensure(state.sc.RunDirPath, 0700)
state.sys.UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
state.sys.Ensure(state.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
state.sys.UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute)
state.sys.
// ensure this dir in case XDG_RUNTIME_DIR is unset
Ensure(state.sc.RuntimePath, 0700).UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute).
Ensure(state.sc.RunDirPath, 0700).UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
}
// instance returns the pathname to a process-specific directory within TMPDIR.

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/gob"
"errors"
"iter"
"math"
"os"
"os/exec"
@@ -30,6 +29,9 @@ const (
shimSetupTimeout = 5 * time.Second
)
// NewStore returns the address of a new instance of [store.Store].
func NewStore(sc *hst.Paths) *store.Store { return store.New(sc.SharePath.Append("state")) }
// main carries out outcome and terminates. main does not return.
func (k *outcome) main(msg message.Msg) {
if k.ctx == nil || k.sys == nil || k.state == nil {
@@ -110,18 +112,17 @@ func (k *outcome) main(msg message.Msg) {
)
for {
var processTimePrev time.Time
processTimePrev, processTime = processTime, time.Now()
var processStatePrev uintptr
processStatePrev, processStateCur = processStateCur, processState
if !processTimePrev.IsZero() && processStatePrev != processLifecycle {
msg.Verbosef("state %d took %d ms", processStatePrev, processTime.Sub(processTimePrev).Milliseconds())
if !processTime.IsZero() && processStatePrev != processLifecycle {
msg.Verbosef("state %d took %.2f ms", processStatePrev, float64(time.Since(processTime).Nanoseconds())/1e6)
}
processTime = time.Now()
switch processState {
case processStart:
if h, err := store.New(k.state.sc.RunDirPath.Append("state")).Handle(k.state.identity.unwrap()); err != nil {
if h, err := NewStore(&k.state.sc).Handle(k.state.identity.unwrap()); err != nil {
perrorFatal(err, "obtain store segment handle", processFinal)
continue
} else {
@@ -238,13 +239,15 @@ func (k *outcome) main(msg message.Msg) {
// this state transition to processFinal only
processState = processFinal
unlock, err := handle.Lock()
if err != nil {
unlock := func() { msg.Verbose("skipping unlock as lock was not successfully acquired") }
if f, err := handle.Lock(); err != nil {
perror(err, "acquire lock on store segment")
} else {
unlock = f
}
if entryHandle != nil {
if err = entryHandle.Destroy(); err != nil {
if err := entryHandle.Destroy(); err != nil {
perror(err, "destroy state entry")
}
}
@@ -252,8 +255,7 @@ func (k *outcome) main(msg message.Msg) {
if isBeforeRevert {
ec := system.Process
var entries iter.Seq[*store.EntryHandle]
if entries, _, err = handle.Entries(); err != nil {
if entries, _, err := handle.Entries(); err != nil {
// it is impossible to continue from this point,
// per-process state will be reverted to limit damage
perror(err, "read store segment entries")
@@ -288,7 +290,7 @@ func (k *outcome) main(msg message.Msg) {
}
}
if err = k.sys.Revert((*system.Criteria)(&ec)); err != nil {
if err := k.sys.Revert((*system.Criteria)(&ec)); err != nil {
var joinError interface {
Unwrap() []error
error

View File

@@ -78,7 +78,7 @@ const shimName = "shim"
// Shim does not return.
func Shim(msg message.Msg) {
if msg == nil {
msg = message.NewMsg(log.Default())
msg = message.New(log.Default())
}
shimEntrypoint(direct{msg})
}

View File

@@ -3,6 +3,7 @@ package outcome
import (
"bytes"
"context"
"io"
"log"
"os"
"syscall"
@@ -138,6 +139,19 @@ func TestShimEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot set SUID_DUMP_DISABLE: %v", []any{stub.UniqueError(11)}}, nil, nil),
}}, nil},
{"receive exit request", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{
call("getMsg", stub.ExpectArgs{}, nil, nil),
call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil),
call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil),
call("getppid", stub.ExpectArgs{}, 0xbad, nil),
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, io.EOF),
call("exit", stub.ExpectArgs{hst.ExitRequest}, stub.PanicExit, nil),
// deferred
call("wKeepAlive", stub.ExpectArgs{}, nil, nil),
}}, nil},
{"receive fd", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{
call("getMsg", stub.ExpectArgs{}, nil, nil),
call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil),
@@ -177,6 +191,26 @@ func TestShimEntrypoint(t *testing.T) {
call("wKeepAlive", stub.ExpectArgs{}, nil, nil),
}}, nil},
{"reparent", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{
call("getMsg", stub.ExpectArgs{}, nil, nil),
call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil),
call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil),
call("getppid", stub.ExpectArgs{}, 0xbad, nil),
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", func() outcomeState {
state := templateState
state.Shim = newShimParams()
state.Shim.PrivPID = 0xfff
return state
}(), nil}, nil, nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil),
call("fatalf", stub.ExpectArgs{"unexpectedly reparented from %d to %d", []any{0xfff, 0xbad}}, nil, nil),
// deferred
call("wKeepAlive", stub.ExpectArgs{}, nil, nil),
}}, nil},
{"invalid state", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{
call("getMsg", stub.ExpectArgs{}, nil, nil),
call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil),

View File

@@ -80,7 +80,7 @@ func TestSpDBusOp(t *testing.T) {
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
)}}, nil, nil),
}, func() *system.I {
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid)
sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
if err := sys.ProxyDBus(
dbus.NewConfig(config.ID, true, true), nil,
@@ -162,7 +162,7 @@ func TestSpDBusOp(t *testing.T) {
"--talk=org.freedesktop.UPower",
)}}, nil, nil),
}, func() *system.I {
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid)
sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
if err := sys.ProxyDBus(
config.SessionBus, config.SystemBus,

View File

@@ -127,10 +127,10 @@ func TestSpPulseOp(t *testing.T) {
call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil),
}, newI().
// state.ensureRuntimeDir
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
Ensure(m(wantRuntimePath), 0700).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
@@ -159,10 +159,10 @@ func TestSpPulseOp(t *testing.T) {
call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie[:len(sampleCookie)-0xe])}, nil),
}, newI().
// state.ensureRuntimeDir
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
Ensure(m(wantRuntimePath), 0700).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
@@ -192,10 +192,10 @@ func TestSpPulseOp(t *testing.T) {
call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil),
}, newI().
// state.ensureRuntimeDir
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
Ensure(m(wantRuntimePath), 0700).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
@@ -222,10 +222,10 @@ func TestSpPulseOp(t *testing.T) {
call("verbose", stub.ExpectArgs{[]any{"cannot locate PulseAudio cookie (tried $PULSE_COOKIE, $XDG_CONFIG_HOME/pulse/cookie, $HOME/.pulse-cookie)"}}, nil, nil),
}, newI().
// state.ensureRuntimeDir
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
Ensure(m(wantRuntimePath), 0700).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).

View File

@@ -64,10 +64,10 @@ func TestSpWaylandOp(t *testing.T) {
call("verbose", stub.ExpectArgs{[]any{"direct wayland access, PROCEED WITH CAUTION"}}, nil, nil),
}, newI().
// state.ensureRuntimeDir
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
Ensure(m(wantRuntimePath), 0700).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
Ensure(m(wantRunDirPath), 0700).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// toSystem
UpdatePermType(hst.EWayland, m("/proc/nonexistent/wayland"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
// this op configures the container state and does not make calls during toContainer

View File

@@ -1,186 +0,0 @@
package store
import (
"errors"
"maps"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/message"
)
/* this provides an implementation of Store on top of the improved state tracking to ease in the changes */
type Compat interface {
// Do calls f exactly once and ensures store exclusivity until f returns.
// Returns whether f is called and any errors during the locking process.
// Cursor provided to f becomes invalid as soon as f returns.
Do(identity int, f func(c Cursor)) (ok bool, err error)
// List queries the store and returns a list of identities known to the store.
// Note that some or all returned identities might not have any active apps.
List() (identities []int, err error)
}
// storeAdapter satisfies [Compat] via [Store].
type storeAdapter struct {
msg message.Msg
*Store
}
func (s storeAdapter) Do(identity int, f func(c Cursor)) (bool, error) {
if h, err := s.Handle(identity); err != nil {
return false, err
} else {
return handleAdapter{h}.do(f)
}
}
func (s storeAdapter) List() ([]int, error) {
segments, n, err := s.Segments()
if err != nil {
return nil, err
}
identities := make([]int, 0, n)
for si := range segments {
if si.Err != nil {
if m, ok := message.GetMessage(err); ok {
s.msg.Verbose(m)
} else {
// unreachable
return nil, err
}
continue
}
identities = append(identities, si.Identity)
}
return identities, nil
}
// NewMulti returns an instance of the multi-file store.
func NewMulti(msg message.Msg, prefix *check.Absolute) Compat {
return storeAdapter{msg, New(prefix.Append("state"))}
}
// Cursor provides access to the store of an identity.
type Cursor interface {
Save(state *hst.State) error
Destroy(id hst.ID) error
Load() (map[hst.ID]*hst.State, error)
Len() (int, error)
}
// handleAdapter satisfies [Cursor] via [Handle].
type handleAdapter struct{ *Handle }
// do implements [Compat.Do] on [Handle].
func (h handleAdapter) do(f func(c Cursor)) (bool, error) {
if unlock, err := h.Lock(); err != nil {
return false, err
} else {
defer unlock()
}
f(h)
return true, nil
}
/* these compatibility methods must only be called while fileMu is held */
func (h handleAdapter) Save(state *hst.State) error { _, err := h.Handle.Save(state); return err }
func (h handleAdapter) Destroy(id hst.ID) error {
return (&EntryHandle{nil, h.Path.Append(id.String()), id}).Destroy()
}
func (h handleAdapter) Load() (map[hst.ID]*hst.State, error) {
entries, n, err := h.Entries()
if err != nil {
return nil, err
}
r := make(map[hst.ID]*hst.State, n)
for eh := range entries {
if eh.DecodeErr != nil {
err = eh.DecodeErr
break
}
var s hst.State
if _, err = eh.Load(&s); err != nil {
break
}
r[eh.ID] = &s
}
return r, err
}
func (h handleAdapter) Len() (int, error) {
entries, _, err := h.Entries()
if err != nil {
return -1, err
}
var n int
for eh := range entries {
if eh.DecodeErr != nil {
err = eh.DecodeErr
}
n++
}
return n, err
}
var (
ErrDuplicate = errors.New("store contains duplicates")
)
// Joiner is the interface that wraps the Join method.
//
// The Join function uses Joiner if available.
type Joiner interface {
Join() (map[hst.ID]*hst.State, error)
}
// Join returns joined state entries of all active identities.
func Join(s Compat) (map[hst.ID]*hst.State, error) {
if j, ok := s.(Joiner); ok {
return j.Join()
}
var (
aids []int
entries = make(map[hst.ID]*hst.State)
el int
res map[hst.ID]*hst.State
loadErr error
)
if ln, err := s.List(); err != nil {
return nil, err
} else {
aids = ln
}
for _, aid := range aids {
if _, err := s.Do(aid, func(c Cursor) {
res, loadErr = c.Load()
}); err != nil {
return nil, err
}
if loadErr != nil {
return nil, loadErr
}
// save expected length
el = len(entries) + len(res)
maps.Copy(entries, res)
if len(entries) != el {
return nil, ErrDuplicate
}
}
return entries, nil
}

View File

@@ -1,120 +0,0 @@
package store_test
import (
"log"
"math/rand"
"reflect"
"slices"
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
"hakurei.app/message"
)
func TestMulti(t *testing.T) {
s := store.NewMulti(message.NewMsg(log.New(log.Writer(), "multi: ", 0)), check.MustAbs(t.TempDir()))
t.Run("list empty store", func(t *testing.T) {
if identities, err := s.List(); err != nil {
t.Fatalf("List: error = %v", err)
} else if len(identities) != 0 {
t.Fatalf("List: identities = %#v", identities)
}
})
const (
insertEntryChecked = iota
insertEntryNoCheck
insertEntryOtherApp
tl
)
var tc [tl]hst.State
for i := 0; i < tl; i++ {
if err := hst.NewInstanceID(&tc[i].ID); err != nil {
t.Fatalf("cannot create dummy state: %v", err)
}
tc[i].PID = rand.Int()
tc[i].Config = hst.Template()
tc[i].Time = time.Now()
}
do := func(identity int, f func(c store.Cursor)) {
if ok, err := s.Do(identity, f); err != nil {
t.Fatalf("Do: ok = %v, error = %v", ok, err)
}
}
insert := func(i, identity int) {
do(identity, func(c store.Cursor) {
if err := c.Save(&tc[i]); err != nil {
t.Fatalf("Save: error = %v", err)
}
})
}
check := func(i, identity int) {
do(identity, func(c store.Cursor) {
if entries, err := c.Load(); err != nil {
t.Fatalf("Load: error = %v", err)
} else if got, ok := entries[tc[i].ID]; !ok {
t.Fatalf("Load: entry %s missing", &tc[i].ID)
} else {
got.Time = tc[i].Time
if !reflect.DeepEqual(got, &tc[i]) {
t.Fatalf("Load: entry %s got %#v, want %#v", &tc[i].ID, got, &tc[i])
}
}
})
}
// insert entry checked
insert(insertEntryChecked, 0)
check(insertEntryChecked, 0)
// insert entry unchecked
insert(insertEntryNoCheck, 0)
// insert entry different identity
insert(insertEntryOtherApp, 1)
check(insertEntryOtherApp, 1)
// check previous insertion
check(insertEntryNoCheck, 0)
// list identities
if identities, err := s.List(); err != nil {
t.Fatalf("List: error = %v", err)
} else {
slices.Sort(identities)
want := []int{0, 1}
if !slices.Equal(identities, want) {
t.Fatalf("List() = %#v, want %#v", identities, want)
}
}
// join store
if entries, err := store.Join(s); err != nil {
t.Fatalf("Join: error = %v", err)
} else if len(entries) != 3 {
t.Fatalf("Join(s) = %#v", entries)
}
// clear identity 1
do(1, func(c store.Cursor) {
if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil {
t.Fatalf("Destroy: error = %v", err)
}
})
do(1, func(c store.Cursor) {
if l, err := c.Len(); err != nil {
t.Fatalf("Len: error = %v", err)
} else if l != 0 {
t.Fatalf("Len: %d, want 0", l)
}
})
}

View File

@@ -152,7 +152,7 @@ func TestStateEntryHandle(t *testing.T) {
})
}
func TestStoreHandle(t *testing.T) {
func TestSegmentHandle(t *testing.T) {
t.Parallel()
testCases := []struct {

View File

@@ -126,7 +126,7 @@ func (s *Store) Segments() (iter.Seq[SegmentIdentity], int, error) {
}
// this should never happen
si.Err = &hst.AppError{Step: step, Err: syscall.EISDIR,
si.Err = &hst.AppError{Step: step, Err: syscall.ENOTDIR,
Msg: "skipped non-directory entry " + strconv.Quote(ent.Name())}
goto out
}
@@ -152,6 +152,50 @@ func (s *Store) Segments() (iter.Seq[SegmentIdentity], int, error) {
}, l, nil
}
// All returns a non-reusable iterator over all [EntryHandle] known to this [Store].
// Callers must call copyError after completing iteration and handle the error accordingly.
// A non-nil error returned by copyError is of type [hst.AppError].
func (s *Store) All() (entries iter.Seq[*EntryHandle], copyError func() error) {
var savedErr error
return func(yield func(*EntryHandle) bool) {
var segments iter.Seq[SegmentIdentity]
segments, _, savedErr = s.Segments()
if savedErr != nil {
return
}
for si := range segments {
if savedErr = si.Err; savedErr != nil {
return
}
var handle *Handle
if handle, savedErr = s.Handle(si.Identity); savedErr != nil {
return // not reached
}
var unlock func()
if unlock, savedErr = handle.Lock(); savedErr != nil {
return
}
var segmentEntries iter.Seq[*EntryHandle]
if segmentEntries, _, savedErr = handle.Entries(); savedErr != nil {
unlock()
return // not reached: lock has succeeded
}
for eh := range segmentEntries {
if !yield(eh) {
unlock()
return
}
}
unlock()
}
}, func() error { return savedErr }
}
// New returns the address of a new instance of [Store].
// Multiple instances of [Store] rooted in the same directory is possible, but unsupported.
func New(base *check.Absolute) *Store {

View File

@@ -1,6 +1,7 @@
package store_test
import (
"bytes"
"cmp"
"iter"
"os"
@@ -10,6 +11,7 @@ import (
"strings"
"syscall"
"testing"
"time"
_ "unsafe"
"hakurei.app/container/check"
@@ -20,7 +22,7 @@ import (
//go:linkname bigLock hakurei.app/internal/store.(*Store).bigLock
func bigLock(s *store.Store) (unlock func(), err error)
func TestStateStoreBigLock(t *testing.T) {
func TestStoreBigLock(t *testing.T) {
t.Parallel()
{
@@ -62,7 +64,7 @@ func TestStateStoreBigLock(t *testing.T) {
})
}
func TestStateStoreHandle(t *testing.T) {
func TestStoreHandle(t *testing.T) {
t.Parallel()
t.Run("loadstore", func(t *testing.T) {
@@ -143,7 +145,7 @@ func TestStateStoreHandle(t *testing.T) {
})
}
func TestStateStoreSegments(t *testing.T) {
func TestStoreSegments(t *testing.T) {
t.Parallel()
testCases := []struct {
@@ -159,7 +161,7 @@ func TestStateStoreSegments(t *testing.T) {
"9999",
"16384",
}}, []store.SegmentIdentity{
{-1, &hst.AppError{Step: "process store segment", Err: syscall.EISDIR,
{-1, &hst.AppError{Step: "process store segment", Err: syscall.ENOTDIR,
Msg: `skipped non-directory entry "f0-invalid-file"`}},
{-1, &hst.AppError{Step: "process store segment", Err: syscall.ERANGE,
Msg: `skipped out of bounds entry 16384`}},
@@ -257,3 +259,147 @@ func TestStateStoreSegments(t *testing.T) {
}
})
}
func TestStoreAll(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data []hst.State
extra func(t *testing.T, base *check.Absolute)
err func(base *check.Absolute) error
}{
{"segment access", []hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
}, func(t *testing.T, base *check.Absolute) {
segmentPath := base.Append("0")
if err := os.Mkdir(segmentPath.String(), 0); err != nil {
t.Fatal(err.Error())
}
t.Cleanup(func() {
if err := os.Chmod(segmentPath.String(), 0755); err != nil {
t.Fatal(err.Error())
}
})
}, func(base *check.Absolute) error {
return &hst.AppError{
Step: "acquire lock on store segment 0",
Err: &os.PathError{Op: "open", Path: base.Append("0", store.MutexName).String(), Err: syscall.EACCES},
}
}},
{"bad segment", []hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
}, func(t *testing.T, base *check.Absolute) {
if f, err := os.Create(base.Append("invalid").String()); err != nil {
t.Fatal(err.Error())
} else if err = f.Close(); err != nil {
t.Fatal(err.Error())
}
}, func(base *check.Absolute) error {
return &hst.AppError{
Step: "process store segment",
Err: syscall.ENOTDIR,
Msg: `skipped non-directory entry "invalid"`,
}
}},
{"access", []hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
}, func(t *testing.T, base *check.Absolute) {
if err := os.Chmod(base.String(), 0); err != nil {
t.Fatal(err.Error())
}
t.Cleanup(func() {
if err := os.Chmod(base.String(), 0755); err != nil {
t.Fatal(err.Error())
}
})
}, func(base *check.Absolute) error {
return &hst.AppError{
Step: "acquire lock on the state store",
Err: &os.PathError{Op: "open", Path: base.Append(store.MutexName).String(), Err: syscall.EACCES},
}
}},
{"success single", []hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
}, func(t *testing.T, base *check.Absolute) {
for i := 0; i < hst.Template().Identity; i++ {
if err := os.Mkdir(base.Append(strconv.Itoa(i)).String(), 0700); err != nil {
t.Fatal(err.Error())
}
}
}, nil},
{"success", []hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xab}, len(hst.ID{}))), PID: 0x1beef, ShimPID: 0x1cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xf0}, len(hst.ID{}))), PID: 0x2beef, ShimPID: 0x2cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef2)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfe}, len(hst.ID{}))), PID: 0xbed, ShimPID: 0xfff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = hst.IdentityMax
return template
}(), Time: time.Unix(0, 0xcafebabe0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfc}, len(hst.ID{}))), PID: 0x1bed, ShimPID: 0x1fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xfc
return template
}(), Time: time.Unix(0, 0xcafebabe1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xce}, len(hst.ID{}))), PID: 0x2bed, ShimPID: 0x2fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xce
return template
}(), Time: time.Unix(0, 0xcafebabe2)},
}, nil, nil},
}
for _, tc := range testCases {
base := check.MustAbs(t.TempDir()).Append("store")
s := store.New(base)
want := make([]*store.EntryHandle, 0, len(tc.data))
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)
}
var eh *store.EntryHandle
eh, err = h.Save(&tc.data[i])
unlock()
if err != nil {
t.Fatalf("Save: error = %v", err)
}
want = append(want, eh)
}
}
slices.SortFunc(want, func(a, b *store.EntryHandle) int { return strings.Compare(a.Pathname.String(), b.Pathname.String()) })
var wantErr error
if tc.err != nil {
wantErr = tc.err(base)
}
if tc.extra != nil {
tc.extra(t, base)
}
// store must not be written to beyond this point
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
entries, copyError := s.All()
got := slices.Collect(entries)
if err := copyError(); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("All: error = %#v, want %#v", err, wantErr)
}
if wantErr == nil {
slices.SortFunc(got, func(a, b *store.EntryHandle) int { return strings.Compare(a.Pathname.String(), b.Pathname.String()) })
if !reflect.DeepEqual(got, want) {
t.Fatalf("All: %#v, want %#v", got, want)
}
}
})
}
}

View File

@@ -52,7 +52,7 @@ type Msg interface {
}
// defaultMsg is the default implementation of the [Msg] interface.
// The zero value is not safe for use. Callers should use the [NewMsg] function instead.
// The zero value is not safe for use. Callers should use the [New] function instead.
type defaultMsg struct {
verbose atomic.Bool
@@ -60,10 +60,10 @@ type defaultMsg struct {
Suspendable
}
// NewMsg initialises a downstream [log.Logger] for a new [Msg].
// The [log.Logger] should no longer be configured after NewMsg returns.
// New initialises a downstream [log.Logger] for a new [Msg].
// The [log.Logger] should no longer be configured after [New] returns.
// If downstream is nil, a new logger is initialised in its place.
func NewMsg(downstream *log.Logger) Msg {
func New(downstream *log.Logger) Msg {
if downstream == nil {
downstream = log.New(log.Writer(), "container: ", 0)
}

View File

@@ -51,7 +51,7 @@ func TestDefaultMsg(t *testing.T) {
t.Run("logger", func(t *testing.T) {
t.Run("nil", func(t *testing.T) {
got := message.NewMsg(nil).GetLogger()
got := message.New(nil).GetLogger()
if out := got.Writer().(*message.Suspendable).Downstream; out != log.Writer() {
t.Errorf("GetLogger: Downstream = %#v", out)
@@ -64,7 +64,7 @@ func TestDefaultMsg(t *testing.T) {
t.Run("takeover", func(t *testing.T) {
l := log.New(io.Discard, "\x00", 0xdeadbeef)
got := message.NewMsg(l)
got := message.New(l)
if logger := got.GetLogger(); logger != l {
t.Errorf("GetLogger: %#v, want %#v", logger, l)
@@ -169,7 +169,7 @@ func TestDefaultMsg(t *testing.T) {
}},
}
msg := message.NewMsg(log.New(&dw, "test: ", 0))
msg := message.New(log.New(&dw, "test: ", 0))
for _, step := range steps {
// these share the same writer, so cannot be subtests
t.Logf("running step %q", step.name)

View File

@@ -227,7 +227,7 @@ in
in
pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } ''
ln -vs ${file} "$out"
hakurei show ${file}
hakurei show --no-store ${file}
'';
in
pkgs.writeShellScriptBin app.name ''

View File

@@ -93,9 +93,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
t.Run("invalid start", func(t *testing.T) {
if !useSandbox {
p = dbus.NewDirect(t.Context(), message.NewMsg(nil), nil, nil)
p = dbus.NewDirect(t.Context(), message.New(nil), nil, nil)
} else {
p = dbus.New(t.Context(), message.NewMsg(nil), nil, nil)
p = dbus.New(t.Context(), message.New(nil), nil, nil)
}
if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) {
@@ -125,9 +125,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
defer cancel()
output := new(strings.Builder)
if !useSandbox {
p = dbus.NewDirect(ctx, message.NewMsg(nil), final, output)
p = dbus.NewDirect(ctx, message.New(nil), final, output)
} else {
p = dbus.New(ctx, message.NewMsg(nil), final, output)
p = dbus.New(ctx, message.New(nil), final, output)
}
{ // check invalid wait behaviour

View File

@@ -82,7 +82,7 @@ func TestNew(t *testing.T) {
t.Errorf("recover: %v, want %v", r, want)
}
}()
New(nil, message.NewMsg(nil), 0)
New(nil, message.New(nil), 0)
})
t.Run("msg", func(t *testing.T) {
@@ -102,11 +102,11 @@ func TestNew(t *testing.T) {
t.Errorf("recover: %v, want %v", r, want)
}
}()
New(t.Context(), message.NewMsg(nil), -1)
New(t.Context(), message.New(nil), -1)
})
})
sys := New(t.Context(), message.NewMsg(nil), 0xdeadbeef)
sys := New(t.Context(), message.New(nil), 0xdeadbeef)
if sys.ctx == nil {
t.Error("New: ctx = nil")
}
@@ -125,51 +125,51 @@ func TestEqual(t *testing.T) {
want bool
}{
{"simple UID",
New(t.Context(), message.NewMsg(nil), 150),
New(t.Context(), message.NewMsg(nil), 150),
New(t.Context(), message.New(nil), 150),
New(t.Context(), message.New(nil), 150),
true},
{"simple UID differ",
New(t.Context(), message.NewMsg(nil), 150),
New(t.Context(), message.NewMsg(nil), 151),
New(t.Context(), message.New(nil), 150),
New(t.Context(), message.New(nil), 151),
false},
{"simple UID nil",
New(t.Context(), message.NewMsg(nil), 150),
New(t.Context(), message.New(nil), 150),
nil,
false},
{"op length mismatch",
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos"),
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos").
Ensure(m("/run"), 0755),
false},
{"op value mismatch",
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos").
Ensure(m("/run"), 0644),
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos").
Ensure(m("/run"), 0755),
false},
{"op type mismatch",
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos").
Wayland(m("/proc/nonexistent/dst"), m("/proc/nonexistent/src"), "\x00", "\x00"),
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos").
Ensure(m("/run"), 0755),
false},
{"op equals",
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos").
Ensure(m("/run"), 0755),
New(t.Context(), message.NewMsg(nil), 150).
New(t.Context(), message.New(nil), 150).
ChangeHosts("chronos").
Ensure(m("/run"), 0755),
true},

View File

@@ -84,14 +84,14 @@
virtualisation = {
# Hopefully reduces spurious test failures:
memorySize = 4096;
memorySize = 8192;
qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
"-vga none -device virtio-gpu-pci"
# Increase Go test compiler performance:
"-smp 8"
"-smp 16"
];
};

View File

@@ -56,7 +56,7 @@ def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values()))
instance = instances[0]
command = f"{name}-start"
if not (instance['container']['path'].startswith("/nix/store/")) or not (instance['container']['path'].endswith(command)):
@@ -271,7 +271,11 @@ machine.wait_until_fails("pgrep foot", timeout=5)
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents:
# Print hakurei share and rundir contents:
print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print"))
print(machine.succeed("find /run/user/1000/hakurei"))
# Verify go test status: