system: update doc commands and remove mutex
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m39s

The mutex is not really doing anything, none of these methods make sense when called concurrently anyway. The copylocks analysis is still satisfied by the noCopy struct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-09-02 04:54:34 +09:00
parent 1b5d20a39b
commit 6f719bc3c1
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
20 changed files with 210 additions and 210 deletions

View File

@ -1,6 +1,7 @@
package app_test package app_test
import ( import (
"context"
"syscall" "syscall"
"hakurei.app/container" "hakurei.app/container"
@ -74,7 +75,7 @@ var testCasesNixos = []sealTestCase{
0x4c, 0xf0, 0x73, 0xbd, 0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1, 0xb4, 0x6e, 0xb5, 0xc1,
}, },
system.New(1000001). system.New(context.TODO(), 1000001).
Ensure("/tmp/hakurei.1971", 0711). Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute).

View File

@ -1,6 +1,7 @@
package app_test package app_test
import ( import (
"context"
"os" "os"
"syscall" "syscall"
@ -23,7 +24,7 @@ var testCasesPd = []sealTestCase{
0xbd, 0x01, 0x78, 0x0e, 0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac, 0xb9, 0xa6, 0x07, 0xac,
}, },
system.New(1000000). system.New(context.TODO(), 1000000).
Ensure("/tmp/hakurei.1971", 0711). Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute).
@ -115,7 +116,7 @@ var testCasesPd = []sealTestCase{
0x82, 0xd4, 0x13, 0x36, 0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c, 0x9b, 0x64, 0xce, 0x7c,
}, },
system.New(1000009). system.New(context.TODO(), 1000009).
Ensure("/tmp/hakurei.1971", 0711). Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute).

View File

@ -30,7 +30,7 @@ func TestApp(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os) a := app.NewWithID(t.Context(), tc.id, tc.os)
var ( var (
gotSys *system.I gotSys *system.I
gotContainer *container.Params gotContainer *container.Params

View File

@ -1,17 +1,16 @@
package app package app
import ( import (
"context"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
"hakurei.app/system" "hakurei.app/system"
) )
func NewWithID(id state.ID, os sys.State) *App { func NewWithID(ctx context.Context, id state.ID, os sys.State) *App {
a := new(App) return &App{id: newID(&id), sys: os, ctx: ctx}
a.id = newID(&id)
a.sys = os
return a
} }
func AppIParams(a *App, seal *Outcome) (*system.I, *container.Params) { func AppIParams(a *App, seal *Outcome) (*system.I, *container.Params) {

View File

@ -61,7 +61,7 @@ func (seal *Outcome) Run(rs *RunState) error {
// read comp value early to allow for early failure // read comp value early to allow for early failure
hsuPath := internal.MustHsuPath() hsuPath := internal.MustHsuPath()
if err := seal.sys.Commit(seal.ctx); err != nil { if err := seal.sys.Commit(); err != nil {
return err return err
} }
store := state.NewMulti(seal.runDirPath.String()) store := state.NewMulti(seal.runDirPath.String())

View File

@ -146,8 +146,11 @@ type hsuUser struct {
} }
func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error { func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error {
if ctx == nil {
panic("invalid call to finalise")
}
if seal.ctx != nil { if seal.ctx != nil {
panic("finalise called twice") panic("attempting to finalise twice")
} }
seal.ctx = ctx seal.ctx = ctx
@ -306,7 +309,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
share := &shareHost{seal: seal, sc: sys.Paths()} share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap()) seal.sys = system.New(seal.ctx, seal.user.uid.unwrap())
seal.sys.Ensure(share.sc.SharePath.String(), 0711) seal.sys.Ensure(share.sc.SharePath.String(), 0711)
{ {

View File

@ -9,37 +9,33 @@ import (
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
// UpdatePerm appends an ephemeral acl update Op. // UpdatePerm appends [ACLUpdateOp] to [I] with the [Process] criteria.
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I { func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
sys.UpdatePermType(Process, path, perms...) sys.UpdatePermType(Process, path, perms...)
return sys return sys
} }
// UpdatePermType appends an acl update Op. // UpdatePermType appends [ACLUpdateOp] to [I].
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I { func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &ACLUpdateOp{et, path, perms})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms})
return sys return sys
} }
type ACL struct { // ACLUpdateOp maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
type ACLUpdateOp struct {
et Enablement et Enablement
path string path string
perms acl.Perms perms acl.Perms
} }
func (a *ACL) Type() Enablement { return a.et } func (a *ACLUpdateOp) Type() Enablement { return a.et }
func (a *ACL) apply(sys *I) error { func (a *ACLUpdateOp) apply(sys *I) error {
msg.Verbose("applying ACL", a) msg.Verbose("applying ACL", a)
return newOpError("acl", acl.Update(a.path, sys.uid, a.perms...), false) return newOpError("acl", acl.Update(a.path, sys.uid, a.perms...), false)
} }
func (a *ACL) revert(sys *I, ec *Criteria) error { func (a *ACLUpdateOp) revert(sys *I, ec *Criteria) error {
if ec.hasType(a) { if ec.hasType(a) {
msg.Verbose("stripping ACL", a) msg.Verbose("stripping ACL", a)
err := acl.Update(a.path, sys.uid) err := acl.Update(a.path, sys.uid)
@ -55,17 +51,17 @@ func (a *ACL) revert(sys *I, ec *Criteria) error {
} }
} }
func (a *ACL) Is(o Op) bool { func (a *ACLUpdateOp) Is(o Op) bool {
a0, ok := o.(*ACL) target, ok := o.(*ACLUpdateOp)
return ok && a0 != nil && return ok && a != nil && target != nil &&
a.et == a0.et && a.et == target.et &&
a.path == a0.path && a.path == target.path &&
slices.Equal(a.perms, a0.perms) slices.Equal(a.perms, target.perms)
} }
func (a *ACL) Path() string { return a.path } func (a *ACLUpdateOp) Path() string { return a.path }
func (a *ACL) String() string { func (a *ACLUpdateOp) String() string {
return fmt.Sprintf("%s type: %s path: %q", return fmt.Sprintf("%s type: %s path: %q",
a.perms, TypeString(a.et), a.path) a.perms, TypeString(a.et), a.path)
} }

View File

@ -18,9 +18,9 @@ func TestUpdatePerm(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) { t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.UpdatePerm(tc.path, tc.perms...) sys.UpdatePerm(tc.path, tc.perms...)
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms}}, "UpdatePerm") (&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACLUpdateOp{Process, tc.path, tc.perms}}, "UpdatePerm")
}) })
} }
} }
@ -40,9 +40,9 @@ func TestUpdatePermType(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.path+"_"+TypeString(tc.et)+permSubTestSuffix(tc.perms), func(t *testing.T) { t.Run(tc.path+"_"+TypeString(tc.et)+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.UpdatePermType(tc.et, tc.path, tc.perms...) sys.UpdatePermType(tc.et, tc.path, tc.perms...)
tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms}}, "UpdatePermType") tc.test(t, sys.ops, []Op{&ACLUpdateOp{tc.et, tc.path, tc.perms}}, "UpdatePermType")
}) })
} }
} }
@ -65,7 +65,7 @@ func TestACLString(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
a := &ACL{et: tc.et, perms: tc.perms, path: container.Nonexistent} a := &ACLUpdateOp{et: tc.et, perms: tc.perms, path: container.Nonexistent}
if got := a.String(); got != tc.want { if got := a.String(); got != tc.want {
t.Errorf("String() = %v, want %v", t.Errorf("String() = %v, want %v",
got, tc.want) got, tc.want)

View File

@ -17,6 +17,7 @@ var (
ErrDBusConfig = errors.New("dbus config not supplied") ErrDBusConfig = errors.New("dbus config not supplied")
) )
// MustProxyDBus calls ProxyDBus and panics if an error is returned.
func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I { func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I {
if _, err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil { if _, err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil {
panic(err.Error()) panic(err.Error())
@ -25,8 +26,9 @@ func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath
} }
} }
// ProxyDBus finalises configuration and appends [DBusProxyOp] to [I].
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) { func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) {
d := new(DBus) d := new(DBusProxyOp)
// session bus is required as otherwise this is effectively a very expensive noop // session bus is required as otherwise this is effectively a very expensive noop
if session == nil { if session == nil {
@ -39,7 +41,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
d.sessionBus[0], d.systemBus[0] = dbus.Address() d.sessionBus[0], d.systemBus[0] = dbus.Address()
d.sessionBus[1], d.systemBus[1] = sessionPath, systemPath d.sessionBus[1], d.systemBus[1] = sessionPath, systemPath
d.out = &scanToFmsg{msg: new(strings.Builder)} d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", msg: new(strings.Builder)}
if final, err := dbus.Finalise(d.sessionBus, d.systemBus, session, system); err != nil { if final, err := dbus.Finalise(d.sessionBus, d.systemBus, session, system); err != nil {
if errors.Is(err, syscall.EINVAL) { if errors.Is(err, syscall.EINVAL) {
return nil, newOpErrorMessage("dbus", err, return nil, newOpErrorMessage("dbus", err,
@ -65,20 +67,22 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
return d.out.Dump, nil return d.out.Dump, nil
} }
type DBus struct { // DBusProxyOp starts xdg-dbus-proxy via [dbus] and terminates it on revert.
// This [Op] is always [Process] scoped.
type DBusProxyOp struct {
proxy *dbus.Proxy // populated during apply proxy *dbus.Proxy // populated during apply
final *dbus.Final final *dbus.Final
out *scanToFmsg out *linePrefixWriter
// whether system bus proxy is enabled // whether system bus proxy is enabled
system bool system bool
sessionBus, systemBus dbus.ProxyPair sessionBus, systemBus dbus.ProxyPair
} }
func (d *DBus) Type() Enablement { return Process } func (d *DBusProxyOp) Type() Enablement { return Process }
func (d *DBus) apply(sys *I) error { func (d *DBusProxyOp) apply(sys *I) error {
msg.Verbosef("session bus proxy on %q for upstream %q", d.sessionBus[1], d.sessionBus[0]) msg.Verbosef("session bus proxy on %q for upstream %q", d.sessionBus[1], d.sessionBus[0])
if d.system { if d.system {
msg.Verbosef("system bus proxy on %q for upstream %q", d.systemBus[1], d.systemBus[0]) msg.Verbosef("system bus proxy on %q for upstream %q", d.systemBus[1], d.systemBus[0])
@ -94,7 +98,7 @@ func (d *DBus) apply(sys *I) error {
return nil return nil
} }
func (d *DBus) revert(*I, *Criteria) error { func (d *DBusProxyOp) revert(*I, *Criteria) error {
// criteria ignored here since dbus is always process-scoped // criteria ignored here since dbus is always process-scoped
msg.Verbose("terminating message bus proxy") msg.Verbose("terminating message bus proxy")
d.proxy.Close() d.proxy.Close()
@ -108,30 +112,34 @@ func (d *DBus) revert(*I, *Criteria) error {
fmt.Sprintf("message bus proxy error: %v", err), true) fmt.Sprintf("message bus proxy error: %v", err), true)
} }
func (d *DBus) Is(o Op) bool { func (d *DBusProxyOp) Is(o Op) bool {
d0, ok := o.(*DBus) target, ok := o.(*DBusProxyOp)
return ok && d0 != nil && return ok && d != nil && target != nil &&
((d.proxy == nil && d0.proxy == nil) || ((d.proxy == nil && target.proxy == nil) ||
(d.proxy != nil && d0.proxy != nil && d.proxy.String() == d0.proxy.String())) (d.proxy != nil && target.proxy != nil &&
d.proxy.String() == target.proxy.String()))
} }
func (d *DBus) Path() string { return "(dbus proxy)" } func (d *DBusProxyOp) Path() string { return "(dbus proxy)" }
func (d *DBus) String() string { return d.proxy.String() } func (d *DBusProxyOp) String() string { return d.proxy.String() }
type scanToFmsg struct { // linePrefixWriter calls println with a prefix for every line written.
msg *strings.Builder type linePrefixWriter struct {
msgbuf []string prefix string
println func(v ...any)
msg *strings.Builder
msgbuf []string
mu sync.RWMutex mu sync.RWMutex
} }
func (s *scanToFmsg) Write(p []byte) (n int, err error) { func (s *linePrefixWriter) Write(p []byte) (n int, err error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
return s.write(p, 0) return s.write(p, 0)
} }
func (s *scanToFmsg) write(p []byte, a int) (int, error) { func (s *linePrefixWriter) write(p []byte, a int) (int, error) {
if i := bytes.IndexByte(p, '\n'); i == -1 { if i := bytes.IndexByte(p, '\n'); i == -1 {
n, _ := s.msg.Write(p) n, _ := s.msg.Write(p)
return a + n, nil return a + n, nil
@ -141,7 +149,7 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
// allow container init messages through // allow container init messages through
v := s.msg.String() v := s.msg.String()
if strings.HasPrefix(v, "init: ") { if strings.HasPrefix(v, "init: ") {
log.Println("(dbus) " + v) s.println(s.prefix + v)
} else { } else {
s.msgbuf = append(s.msgbuf, v) s.msgbuf = append(s.msgbuf, v)
} }
@ -151,10 +159,10 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
} }
} }
func (s *scanToFmsg) Dump() { func (s *linePrefixWriter) Dump() {
s.mu.RLock() s.mu.RLock()
for _, m := range s.msgbuf { for _, m := range s.msgbuf {
log.Println("(dbus) " + m) s.println(s.prefix + m)
} }
s.mu.RUnlock() s.mu.RUnlock()
} }

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
) )
// Enablement represents optional system resources. // Enablement represents an optional host service to export to the target user.
type Enablement byte type Enablement byte
const ( const (

View File

@ -5,32 +5,29 @@ import (
"os" "os"
) )
// Link registers an Op that links dst to src. // Link appends [HardlinkOp] to [I] the [Process] criteria.
func (sys *I) Link(oldname, newname string) *I { return sys.LinkFileType(Process, oldname, newname) } func (sys *I) Link(oldname, newname string) *I { return sys.LinkFileType(Process, oldname, newname) }
// LinkFileType registers a file linking Op labelled with type et. // LinkFileType appends [HardlinkOp] to [I].
func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I { func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &HardlinkOp{et, newname, oldname})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Hardlink{et, newname, oldname})
return sys return sys
} }
type Hardlink struct { // HardlinkOp maintains a hardlink until its [Enablement] is no longer satisfied.
type HardlinkOp struct {
et Enablement et Enablement
dst, src string dst, src string
} }
func (l *Hardlink) Type() Enablement { return l.et } func (l *HardlinkOp) Type() Enablement { return l.et }
func (l *Hardlink) apply(*I) error { func (l *HardlinkOp) apply(*I) error {
msg.Verbose("linking", l) msg.Verbose("linking", l)
return newOpError("hardlink", os.Link(l.src, l.dst), false) return newOpError("hardlink", os.Link(l.src, l.dst), false)
} }
func (l *Hardlink) revert(_ *I, ec *Criteria) error { func (l *HardlinkOp) revert(_ *I, ec *Criteria) error {
if ec.hasType(l) { if ec.hasType(l) {
msg.Verbosef("removing hard link %q", l.dst) msg.Verbosef("removing hard link %q", l.dst)
return newOpError("hardlink", os.Remove(l.dst), true) return newOpError("hardlink", os.Remove(l.dst), true)
@ -40,6 +37,10 @@ func (l *Hardlink) revert(_ *I, ec *Criteria) error {
} }
} }
func (l *Hardlink) Is(o Op) bool { l0, ok := o.(*Hardlink); return ok && l0 != nil && *l == *l0 } func (l *HardlinkOp) Is(o Op) bool {
func (l *Hardlink) Path() string { return l.src } target, ok := o.(*HardlinkOp)
func (l *Hardlink) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) } return ok && l != nil && target != nil && *l == *target
}
func (l *HardlinkOp) Path() string { return l.src }
func (l *HardlinkOp) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) }

View File

@ -6,38 +6,30 @@ import (
"os" "os"
) )
// Ensure the existence and mode of a directory. // Ensure appends [MkdirOp] to [I] with its [Enablement] ignored.
func (sys *I) Ensure(name string, perm os.FileMode) *I { func (sys *I) Ensure(name string, perm os.FileMode) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &MkdirOp{User, name, perm, false})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{User, name, perm, false})
return sys return sys
} }
// Ephemeral ensures the temporary existence and mode of a directory through the life of et. // Ephemeral appends an ephemeral [MkdirOp] to [I].
func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I { func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &MkdirOp{et, name, perm, true})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{et, name, perm, true})
return sys return sys
} }
type Mkdir struct { // MkdirOp ensures the existence of a directory.
// For ephemeral, the directory is destroyed once [Enablement] is no longer satisfied.
type MkdirOp struct {
et Enablement et Enablement
path string path string
perm os.FileMode perm os.FileMode
ephemeral bool ephemeral bool
} }
func (m *Mkdir) Type() Enablement { func (m *MkdirOp) Type() Enablement { return m.et }
return m.et
}
func (m *Mkdir) apply(*I) error { func (m *MkdirOp) apply(*I) error {
msg.Verbose("ensuring directory", m) msg.Verbose("ensuring directory", m)
// create directory // create directory
@ -52,7 +44,7 @@ func (m *Mkdir) apply(*I) error {
} }
} }
func (m *Mkdir) revert(_ *I, ec *Criteria) error { func (m *MkdirOp) revert(_ *I, ec *Criteria) error {
if !m.ephemeral { if !m.ephemeral {
// skip non-ephemeral dir and do not log anything // skip non-ephemeral dir and do not log anything
return nil return nil
@ -67,16 +59,14 @@ func (m *Mkdir) revert(_ *I, ec *Criteria) error {
} }
} }
func (m *Mkdir) Is(o Op) bool { func (m *MkdirOp) Is(o Op) bool {
m0, ok := o.(*Mkdir) target, ok := o.(*MkdirOp)
return ok && m0 != nil && *m == *m0 return ok && m != nil && target != nil && *m == *target
} }
func (m *Mkdir) Path() string { func (m *MkdirOp) Path() string { return m.path }
return m.path
}
func (m *Mkdir) String() string { func (m *MkdirOp) String() string {
t := "ensure" t := "ensure"
if m.ephemeral { if m.ephemeral {
t = TypeString(m.Type()) t = TypeString(m.Type())

View File

@ -19,9 +19,9 @@ func TestEnsure(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) { t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.Ensure(tc.name, tc.perm) sys.Ensure(tc.name, tc.perm)
(&tcOp{User, tc.name}).test(t, sys.ops, []Op{&Mkdir{User, tc.name, tc.perm, false}}, "Ensure") (&tcOp{User, tc.name}).test(t, sys.ops, []Op{&MkdirOp{User, tc.name, tc.perm, false}}, "Ensure")
}) })
} }
} }
@ -36,9 +36,9 @@ func TestEphemeral(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) { t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.Ephemeral(tc.et, tc.path, tc.perm) sys.Ephemeral(tc.et, tc.path, tc.perm)
tc.test(t, sys.ops, []Op{&Mkdir{tc.et, tc.path, tc.perm, true}}, "Ephemeral") tc.test(t, sys.ops, []Op{&MkdirOp{tc.et, tc.path, tc.perm, true}}, "Ephemeral")
}) })
} }
} }
@ -60,7 +60,7 @@ func TestMkdirString(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
m := &Mkdir{ m := &MkdirOp{
et: tc.et, et: tc.et,
path: container.Nonexistent, path: container.Nonexistent,
perm: 0701, perm: 0701,

View File

@ -1,4 +1,4 @@
// Package system provides tools for safely interacting with the operating system. // Package system provides helpers to apply and revert groups of operations to the system.
package system package system
import ( import (
@ -6,11 +6,10 @@ import (
"errors" "errors"
"log" "log"
"strings" "strings"
"sync"
) )
const ( const (
// User type is reverted at final launcher exit. // User type is reverted at final instance exit.
User = EM << iota User = EM << iota
// Process type is unconditionally reverted on exit. // Process type is unconditionally reverted on exit.
Process Process
@ -32,12 +31,10 @@ func (ec *Criteria) hasType(o Op) bool {
// Op is a reversible system operation. // Op is a reversible system operation.
type Op interface { type Op interface {
// Type returns Op's enablement type. // Type returns [Op]'s enablement type, for matching a revert criteria.
Type() Enablement Type() Enablement
// apply the Op
apply(sys *I) error apply(sys *I) error
// revert reverses the Op if criteria is met
revert(sys *I, ec *Criteria) error revert(sys *I, ec *Criteria) error
Is(o Op) bool Is(o Op) bool
@ -45,7 +42,7 @@ type Op interface {
String() string String() string
} }
// TypeString returns the string representation of a type stored as an [Enablement]. // TypeString extends [Enablement.String] to support [User] and [Process].
func TypeString(e Enablement) string { func TypeString(e Enablement) string {
switch e { switch e {
case User: case User:
@ -68,35 +65,39 @@ func TypeString(e Enablement) string {
} }
} }
// New initialises sys with no-op verbose functions. // New returns the address of a new [I] targeting uid.
func New(uid int) (sys *I) { func New(ctx context.Context, uid int) (sys *I) {
sys = new(I) if ctx == nil || uid < 0 {
sys.uid = uid panic("invalid call to New")
return }
return &I{ctx: ctx, uid: uid}
} }
// An I provides indirect bulk operating system interaction. I must not be copied. // An I provides deferred operating system interaction. [I] must not be copied.
// Methods of [I] must not be used concurrently.
type I struct { type I struct {
_ noCopy
uid int uid int
ops []Op ops []Op
ctx context.Context ctx context.Context
// whether sys has been reverted // the behaviour of Commit is only defined for up to one call
state bool committed bool
// the behaviour of Revert is only defined for up to one call
lock sync.Mutex reverted bool
} }
func (sys *I) UID() int { return sys.uid } func (sys *I) UID() int { return sys.uid }
// Equal returns whether all [Op] instances held by v is identical to that of sys. // Equal returns whether all [Op] instances held by sys matches that of target.
func (sys *I) Equal(v *I) bool { func (sys *I) Equal(target *I) bool {
if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) { if target == nil || sys.uid != target.uid || len(sys.ops) != len(target.ops) {
return false return false
} }
for i, o := range sys.ops { for i, o := range sys.ops {
if !o.Is(v.ops[i]) { if !o.Is(target.ops[i]) {
return false return false
} }
} }
@ -104,18 +105,15 @@ func (sys *I) Equal(v *I) bool {
return true return true
} }
// Commit applies all [Op] held by [I] and reverts successful [Op] on first error encountered. // Commit applies all [Op] held by [I] and reverts all successful [Op] on first error encountered.
// Commit must not be called more than once. // Commit must not be called more than once.
func (sys *I) Commit(ctx context.Context) error { func (sys *I) Commit() error {
sys.lock.Lock() if sys.committed {
defer sys.lock.Unlock() panic("attempting to commit twice")
if sys.ctx != nil {
panic("sys instance committed twice")
} }
sys.ctx = ctx sys.committed = true
sp := New(sys.uid) sp := New(sys.ctx, sys.uid)
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
defer func() { defer func() {
// sp is set to nil when all ops are applied // sp is set to nil when all ops are applied
@ -144,13 +142,10 @@ func (sys *I) Commit(ctx context.Context) error {
// Revert reverts all [Op] meeting [Criteria] held by [I]. // Revert reverts all [Op] meeting [Criteria] held by [I].
func (sys *I) Revert(ec *Criteria) error { func (sys *I) Revert(ec *Criteria) error {
sys.lock.Lock() if sys.reverted {
defer sys.lock.Unlock() panic("attempting to revert twice")
if sys.state {
panic("sys instance reverted twice")
} }
sys.state = true sys.reverted = true
// collect errors // collect errors
errs := make([]error, len(sys.ops)) errs := make([]error, len(sys.ops))
@ -162,3 +157,16 @@ func (sys *I) Revert(ec *Criteria) error {
// errors.Join filters nils // errors.Join filters nils
return errors.Join(errs...) return errors.Join(errs...)
} }
// noCopy may be added to structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
//
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}

View File

@ -19,7 +19,7 @@ func TestNew(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run("sys initialised with uid "+strconv.Itoa(tc.uid), func(t *testing.T) { t.Run("sys initialised with uid "+strconv.Itoa(tc.uid), func(t *testing.T) {
if got := system.New(tc.uid); got.UID() != tc.uid { if got := system.New(t.Context(), tc.uid); got.UID() != tc.uid {
t.Errorf("New(%d) uid = %d, want %d", t.Errorf("New(%d) uid = %d, want %d",
tc.uid, tc.uid,
got.UID(), tc.uid) got.UID(), tc.uid)
@ -63,57 +63,57 @@ func TestI_Equal(t *testing.T) {
}{ }{
{ {
"simple UID", "simple UID",
system.New(150), system.New(t.Context(), 150),
system.New(150), system.New(t.Context(), 150),
true, true,
}, },
{ {
"simple UID differ", "simple UID differ",
system.New(150), system.New(t.Context(), 150),
system.New(151), system.New(t.Context(), 151),
false, false,
}, },
{ {
"simple UID nil", "simple UID nil",
system.New(150), system.New(t.Context(), 150),
nil, nil,
false, false,
}, },
{ {
"op length mismatch", "op length mismatch",
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"), ChangeHosts("chronos"),
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"). ChangeHosts("chronos").
Ensure("/run", 0755), Ensure("/run", 0755),
false, false,
}, },
{ {
"op value mismatch", "op value mismatch",
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"). ChangeHosts("chronos").
Ensure("/run", 0644), Ensure("/run", 0644),
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"). ChangeHosts("chronos").
Ensure("/run", 0755), Ensure("/run", 0755),
false, false,
}, },
{ {
"op type mismatch", "op type mismatch",
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"). ChangeHosts("chronos").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256), CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256),
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"). ChangeHosts("chronos").
Ensure("/run", 0755), Ensure("/run", 0755),
false, false,
}, },
{ {
"op equals", "op equals",
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"). ChangeHosts("chronos").
Ensure("/run", 0755), Ensure("/run", 0755),
system.New(150). system.New(t.Context(), 150).
ChangeHosts("chronos"). ChangeHosts("chronos").
Ensure("/run", 0755), Ensure("/run", 0755),
true, true,

View File

@ -9,20 +9,16 @@ import (
"syscall" "syscall"
) )
// CopyFile registers an Op that copies from src. // CopyFile appends [TmpfileOp] to [I].
// A buffer is initialised with size cap and the Op faults if bytes read exceed n.
func (sys *I) CopyFile(payload *[]byte, src string, cap int, n int64) *I { func (sys *I) CopyFile(payload *[]byte, src string, cap int, n int64) *I {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.Grow(cap) buf.Grow(cap)
sys.ops = append(sys.ops, &TmpfileOp{payload, src, n, buf})
sys.lock.Lock()
sys.ops = append(sys.ops, &Tmpfile{payload, src, n, buf})
sys.lock.Unlock()
return sys return sys
} }
type Tmpfile struct { // TmpfileOp reads up to n bytes from src and writes the resulting byte slice to payload.
type TmpfileOp struct {
payload *[]byte payload *[]byte
src string src string
@ -30,8 +26,8 @@ type Tmpfile struct {
buf *bytes.Buffer buf *bytes.Buffer
} }
func (t *Tmpfile) Type() Enablement { return Process } func (t *TmpfileOp) Type() Enablement { return Process }
func (t *Tmpfile) apply(*I) error { func (t *TmpfileOp) apply(*I) error {
msg.Verbose("copying", t) msg.Verbose("copying", t)
if t.payload == nil { if t.payload == nil {
@ -59,12 +55,12 @@ func (t *Tmpfile) apply(*I) error {
*t.payload = t.buf.Bytes() *t.payload = t.buf.Bytes()
return nil return nil
} }
func (t *Tmpfile) revert(*I, *Criteria) error { t.buf.Reset(); return nil } func (t *TmpfileOp) revert(*I, *Criteria) error { t.buf.Reset(); return nil }
func (t *Tmpfile) Is(o Op) bool { func (t *TmpfileOp) Is(o Op) bool {
t0, ok := o.(*Tmpfile) target, ok := o.(*TmpfileOp)
return ok && t0 != nil && return ok && t != nil && target != nil &&
t.src == t0.src && t.n == t0.n t.src == target.src && t.n == target.n
} }
func (t *Tmpfile) Path() string { return t.src } func (t *TmpfileOp) Path() string { return t.src }
func (t *Tmpfile) String() string { return fmt.Sprintf("up to %d bytes from %q", t.n, t.src) } func (t *TmpfileOp) String() string { return fmt.Sprintf("up to %d bytes from %q", t.n, t.src) }

View File

@ -15,10 +15,10 @@ func TestCopyFile(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) { t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n) sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n)
tc.test(t, sys.ops, []Op{ tc.test(t, sys.ops, []Op{
&Tmpfile{nil, tc.path, tc.n, nil}, &TmpfileOp{nil, tc.path, tc.n, nil},
}, "CopyFile") }, "CopyFile")
}) })
} }
@ -33,10 +33,10 @@ func TestLink(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) { t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.Link(tc.src, tc.dst) sys.Link(tc.src, tc.dst)
(&tcOp{Process, tc.src}).test(t, sys.ops, []Op{ (&tcOp{Process, tc.src}).test(t, sys.ops, []Op{
&Hardlink{Process, tc.dst, tc.src}, &HardlinkOp{Process, tc.dst, tc.src},
}, "Link") }, "Link")
}) })
} }
@ -52,10 +52,10 @@ func TestLinkFileType(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) { t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.LinkFileType(tc.et, tc.path, tc.dst) sys.LinkFileType(tc.et, tc.path, tc.dst)
tc.test(t, sys.ops, []Op{ tc.test(t, sys.ops, []Op{
&Hardlink{tc.et, tc.dst, tc.path}, &HardlinkOp{tc.et, tc.dst, tc.path},
}, "LinkFileType") }, "LinkFileType")
}) })
} }
@ -73,7 +73,7 @@ func TestTmpfile_String(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
if got := (&Tmpfile{src: tc.src, n: tc.n}).String(); got != tc.want { if got := (&TmpfileOp{src: tc.src, n: tc.n}).String(); got != tc.want {
t.Errorf("String() = %v, want %v", got, tc.want) t.Errorf("String() = %v, want %v", got, tc.want)
} }
}) })

View File

@ -9,17 +9,16 @@ import (
"hakurei.app/system/wayland" "hakurei.app/system/wayland"
) )
// Wayland sets up a wayland socket with a security context attached. // Wayland appends [WaylandOp] to [I].
func (sys *I) Wayland(syncFd **os.File, dst, src, appID, instanceID string) *I { func (sys *I) Wayland(syncFd **os.File, dst, src, appID, instanceID string) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &WaylandOp{syncFd, dst, src, appID, instanceID, wayland.Conn{}})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Wayland{syncFd, dst, src, appID, instanceID, wayland.Conn{}})
return sys return sys
} }
type Wayland struct { // WaylandOp maintains a wayland socket with security-context-v1 attached via [wayland].
// The socket stops accepting connections once the pipe referred to by sync is closed.
// The socket is pathname only and is destroyed on revert.
type WaylandOp struct {
sync **os.File sync **os.File
dst, src string dst, src string
appID, instanceID string appID, instanceID string
@ -27,9 +26,9 @@ type Wayland struct {
conn wayland.Conn conn wayland.Conn
} }
func (w *Wayland) Type() Enablement { return Process } func (w *WaylandOp) Type() Enablement { return Process }
func (w *Wayland) apply(sys *I) error { func (w *WaylandOp) apply(sys *I) error {
if w.sync == nil { if w.sync == nil {
// this is a misuse of the API; do not return a wrapped error // this is a misuse of the API; do not return a wrapped error
return errors.New("invalid sync") return errors.New("invalid sync")
@ -59,7 +58,7 @@ func (w *Wayland) apply(sys *I) error {
} }
} }
func (w *Wayland) revert(_ *I, ec *Criteria) error { func (w *WaylandOp) revert(_ *I, ec *Criteria) error {
if ec.hasType(w) { if ec.hasType(w) {
msg.Verbosef("removing wayland socket on %q", w.dst) msg.Verbosef("removing wayland socket on %q", w.dst)
if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) { if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
@ -74,11 +73,12 @@ func (w *Wayland) revert(_ *I, ec *Criteria) error {
} }
} }
func (w *Wayland) Is(o Op) bool { func (w *WaylandOp) Is(o Op) bool {
w0, ok := o.(*Wayland) target, ok := o.(*WaylandOp)
return ok && w.dst == w0.dst && w.src == w0.src && return ok && w != nil && target != nil &&
w.appID == w0.appID && w.instanceID == w0.instanceID w.dst == target.dst && w.src == target.src &&
w.appID == target.appID && w.instanceID == target.instanceID
} }
func (w *Wayland) Path() string { return w.dst } func (w *WaylandOp) Path() string { return w.dst }
func (w *Wayland) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) } func (w *WaylandOp) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) }

View File

@ -4,27 +4,24 @@ import (
"hakurei.app/system/internal/xcb" "hakurei.app/system/internal/xcb"
) )
// ChangeHosts appends an X11 ChangeHosts command Op. // ChangeHosts appends [XHostOp] to [I].
func (sys *I) ChangeHosts(username string) *I { func (sys *I) ChangeHosts(username string) *I {
sys.lock.Lock() sys.ops = append(sys.ops, XHostOp(username))
defer sys.lock.Unlock()
sys.ops = append(sys.ops, XHost(username))
return sys return sys
} }
type XHost string // XHostOp inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied.
type XHostOp string
func (x XHost) Type() Enablement { return EX11 } func (x XHostOp) Type() Enablement { return EX11 }
func (x XHost) apply(*I) error { func (x XHostOp) apply(*I) error {
msg.Verbosef("inserting entry %s to X11", x) msg.Verbosef("inserting entry %s to X11", x)
return newOpError("xhost", return newOpError("xhost",
xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false) xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false)
} }
func (x XHost) revert(_ *I, ec *Criteria) error { func (x XHostOp) revert(_ *I, ec *Criteria) error {
if ec.hasType(x) { if ec.hasType(x) {
msg.Verbosef("deleting entry %s from X11", x) msg.Verbosef("deleting entry %s from X11", x)
return newOpError("xhost", return newOpError("xhost",
@ -35,6 +32,6 @@ func (x XHost) revert(_ *I, ec *Criteria) error {
} }
} }
func (x XHost) Is(o Op) bool { x0, ok := o.(XHost); return ok && x == x0 } func (x XHostOp) Is(o Op) bool { target, ok := o.(XHostOp); return ok && x == target }
func (x XHost) Path() string { return string(x) } func (x XHostOp) Path() string { return string(x) }
func (x XHost) String() string { return string("SI:localuser:" + x) } func (x XHostOp) String() string { return string("SI:localuser:" + x) }

View File

@ -8,10 +8,10 @@ func TestChangeHosts(t *testing.T) {
testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"} testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"}
for _, tc := range testCases { for _, tc := range testCases {
t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) { t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.ChangeHosts(tc) sys.ChangeHosts(tc)
(&tcOp{EX11, tc}).test(t, sys.ops, []Op{ (&tcOp{EX11, tc}).test(t, sys.ops, []Op{
XHost(tc), XHostOp(tc),
}, "ChangeHosts") }, "ChangeHosts")
}) })
} }
@ -26,7 +26,7 @@ func TestXHost_String(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
if got := XHost(tc.username).String(); got != tc.want { if got := XHostOp(tc.username).String(); got != tc.want {
t.Errorf("String() = %v, want %v", got, tc.want) t.Errorf("String() = %v, want %v", got, tc.want)
} }
}) })