From 121fcfa406ff4057771c880e49fc51bcef25d8ce Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sun, 29 Mar 2026 19:50:20 +0900 Subject: [PATCH] internal/uevent: enumerate objects via sysfs This is not a great way to implement cold boot, but I already have the implementation lying around. Signed-off-by: Ophestra --- internal/uevent/action.go | 10 +++- internal/uevent/action_test.go | 11 +++++ internal/uevent/message_test.go | 6 +++ internal/uevent/sysfs.go | 87 +++++++++++++++++++++++++++++++++ internal/uevent/sysfs_test.go | 28 +++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 internal/uevent/sysfs.go create mode 100644 internal/uevent/sysfs_test.go diff --git a/internal/uevent/action.go b/internal/uevent/action.go index 3eeb4d7e..d3acac1f 100644 --- a/internal/uevent/action.go +++ b/internal/uevent/action.go @@ -19,6 +19,10 @@ const ( KOBJ_OFFLINE KOBJ_BIND KOBJ_UNBIND + + // Synthetic denotes a [Message] that originates from outside the kernel. It + // is not valid in the wire format and is only meaningful within this package. + Synthetic KobjectAction = 0xfeed ) // lib/kobject_uevent.c @@ -38,6 +42,10 @@ func (act KobjectAction) Valid() bool { return int(act) < len(kobject_actions) } // String returns the corresponding string sent over netlink. func (act KobjectAction) String() string { + if act == Synthetic { + return "synthetic" + } + if !act.Valid() { return "unsupported kobject_action " + strconv.Itoa(int(act)) } @@ -45,7 +53,7 @@ func (act KobjectAction) String() string { } func (act KobjectAction) AppendText(b []byte) ([]byte, error) { - if !act.Valid() { + if !act.Valid() && act != Synthetic { return b, syscall.EINVAL } return append(b, act.String()...), nil diff --git a/internal/uevent/action_test.go b/internal/uevent/action_test.go index 1a552f15..bdec6318 100644 --- a/internal/uevent/action_test.go +++ b/internal/uevent/action_test.go @@ -29,4 +29,15 @@ func TestKobjectAction(t *testing.T) { t.Errorf("String: %q, want %q", got, want) } }) + + adeT(t, "synthetic", uevent.Synthetic, "synthetic", + uevent.UnsupportedActionError("synthetic"), nil) + + t.Run("validate synthetic", func(t *testing.T) { + t.Parallel() + + if uevent.Synthetic.Valid() { + t.Errorf("Valid unexpectedly succeeded") + } + }) } diff --git a/internal/uevent/message_test.go b/internal/uevent/message_test.go index ffc7b949..97b841cf 100644 --- a/internal/uevent/message_test.go +++ b/internal/uevent/message_test.go @@ -110,6 +110,12 @@ SEQNUM=780`}, }, "move", uevent.MissingHeaderError( "move", ), syscall.EINVAL, "unsupported kobject_action 2989 event:"}, + + {"synthetic", uevent.Message{ + Action: uevent.Synthetic, + }, "synthetic@\x00", uevent.UnsupportedActionError( + "synthetic", + ), nil, "synthetic event:"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/uevent/sysfs.go b/internal/uevent/sysfs.go new file mode 100644 index 00000000..8d0f7a60 --- /dev/null +++ b/internal/uevent/sysfs.go @@ -0,0 +1,87 @@ +package uevent + +import ( + "bytes" + "errors" + "io/fs" + "log" + "path/filepath" + "unsafe" +) + +// Enumerate scans sysfs and emits [Synthetic] events. It returns the first +// error it encounters. +// +// The specified filesystem must present the sysfs root. +func Enumerate( + sysfs fs.FS, + handleWalkErr func(error) error, + events chan<- *Message, +) error { + if handleWalkErr == nil { + handleWalkErr = func(err error) error { + if errors.Is(err, fs.ErrNotExist) { + log.Println("enumerate", err) + return nil + } + return err + } + } + + return fs.WalkDir(sysfs, "devices", func( + path string, + d fs.DirEntry, + err error, + ) error { + if err != nil { + return handleWalkErr(err) + } + + if d.IsDir() || d.Name() != "uevent" { + return nil + } + + msg := Message{ + Action: Synthetic, + + // cleans path, appears to be compatible with kernel behaviour + DevPath: filepath.Dir(path), + } + + var target string + if target, err = fs.ReadLink( + sysfs, + filepath.Join(msg.DevPath, "subsystem"), + ); err != nil { + if err = handleWalkErr(err); err != nil { + return err + } + } else { + msg.Env = append(msg.Env, "SUBSYSTEM="+filepath.Base(target)) + } + + // read entire file: slicing does not copy + var env []byte + if env, err = fs.ReadFile(sysfs, path); err != nil { + return handleWalkErr(err) + } + + for _, s := range bytes.Split(env, []byte{'\n'}) { + if len(s) == 0 { + continue + } + msg.Env = append(msg.Env, unsafe.String(unsafe.SliceData(s), len(s))) + } + + if len(msg.Env) == 0 { + // this implies absent subsystem, its error is already handled + return nil + } + + if msg.DevPath != "" && msg.DevPath[0] != '/' { + msg.DevPath = "/" + msg.DevPath + } + events <- &msg + return nil + }) +} diff --git a/internal/uevent/sysfs_test.go b/internal/uevent/sysfs_test.go new file mode 100644 index 00000000..50751bef --- /dev/null +++ b/internal/uevent/sysfs_test.go @@ -0,0 +1,28 @@ +package uevent_test + +import ( + "os" + "sync" + "testing" + + "hakurei.app/internal/uevent" +) + +func TestEnumerate(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + defer wg.Wait() + + events := make(chan *uevent.Message, 1<<10) + wg.Go(func() { + for msg := range events { + t.Log(msg) + } + }) + + if err := uevent.Enumerate(os.DirFS("/sys"), nil, events); err != nil { + t.Fatalf("Enumerate: error = %v", err) + } + close(events) +}