No description
  • Go 98.3%
  • Shell 1.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-29 15:27:38 +02:00
cmd/dmancli refactor manager - explicit start/watch lifecycle 2026-08-29 15:27:38 +02:00
docs fix lookup with snapshot and improve doc comments 2026-08-29 06:27:45 +02:00
internal/assert add assert package 2026-08-10 21:19:42 +02:00
scripts add release script 2026-08-29 06:40:35 +02:00
.gitignore fix watch.go and updates dmancli 2026-08-28 10:32:32 +02:00
conn.go modify watch api 2026-08-12 22:25:06 +02:00
delta.go refactor manager - explicit start/watch lifecycle 2026-08-29 15:27:38 +02:00
device.go fix lookup with snapshot and improve doc comments 2026-08-29 06:27:45 +02:00
doc.go fix lookup with snapshot and improve doc comments 2026-08-29 06:27:45 +02:00
go.mod add connection and watch 2026-08-10 22:44:57 +02:00
go.sum add connection and watch 2026-08-10 22:44:57 +02:00
LICENSE add LICENSE and README 2026-08-29 06:14:22 +02:00
listen.go rework processing architecture 2026-08-28 10:17:30 +02:00
listen_test.go implement new architecture 2026-08-16 10:52:18 +02:00
log.go add logger concept 2026-08-16 13:54:38 +02:00
manager.go refactor manager - explicit start/watch lifecycle 2026-08-29 15:27:38 +02:00
query.go add devpath match 2026-08-29 06:02:25 +02:00
README.md refactor manager - explicit start/watch lifecycle 2026-08-29 15:27:38 +02:00
scan.go refactor manager - explicit start/watch lifecycle 2026-08-29 15:27:38 +02:00
store.go fix lookup with snapshot and improve doc comments 2026-08-29 06:27:45 +02:00
sysfs.go add uevent add and bind handing 2026-08-20 15:54:41 +02:00
uevent.go fix lookup with snapshot and improve doc comments 2026-08-29 06:27:45 +02:00
uevent_test.go reintroduce uevent_test.go 2026-08-12 21:11:42 +02:00
watch.go refactor manager - explicit start/watch lifecycle 2026-08-29 15:27:38 +02:00

dman

Subsystem-agnostic Linux device-monitoring core, in pure Go. No cgo.

Captures the Linux device model (/sys/devices) as a generic graph via netlink uevent transport and sysfs reads, and exposes it through a query/subscribe API. Device classification (gamepad/HID, USB block storage, etc.) is left entirely to consumers — dman has no built-in notion of what a "device" is beyond structural presence in sysfs.

Requirements

  • Go 1.25.3+
  • Linux (netlink uevent is a Linux kernel mechanism; no cross-platform support)
  • CGO_ENABLED=0 compatible — no cgo anywhere in this module

Install

go get codeberg.org/neox5/dman

Quick start

m := dman.New()

// Subscribe before Start — calling Subscribe after Start panics.
events := m.Subscribe(dman.Match{Subsystem: "block", DevType: "disk"})

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if err := m.Start(ctx); err != nil {
	log.Fatal(err)
}

// Baseline: every already-attached matching device, as of Start's
// return — reliable, not subject to any live buffer.
for _, d := range m.Query(dman.NewQuery().MatchOn(dman.Match{Subsystem: "block", DevType: "disk"})) {
	fmt.Println("already attached:", d.DevName())
}

go func() {
	if err := m.Watch(ctx); err != nil {
		log.Println("watch stopped:", err)
	}
}()

for ev := range events {
	switch ev.Action {
	case dman.EventAdd:
		fmt.Println("attached:", ev.Device.DevName())
	case dman.EventRemove:
		fmt.Println("removed:", ev.Device.DevName())
	}
}

Start opens the connection and performs a full sysfs Scan, synchronously — once it returns, Query reflects a complete, reliable baseline. Watch (run separately, typically via go) then delivers only live changes from that point on to every Subscribed listener; pre-existing devices are read via Query, not delivered as events.

Device.Parent/Device.Children are bare DevPath strings, not nested Device values — resolve them via Query:

// walk up from a disk to its usb_device ancestor
usb := m.Query(dman.NewQuery().
	Ancestors(disk.DevPath).
	MatchOn(dman.Match{Subsystem: "usb", DevType: "usb_device"}))

// resolve one already-known DevPath directly
results := m.Query(dman.NewQuery().MatchOn(dman.Match{DevPath: disk.Parent}))

One-shot scan, no live watch

store := dman.NewStore()

for d, err := range dman.Scan(ctx) {
	if err != nil {
		log.Fatal(err)
	}
	if d.Type == dman.Upsert {
		store.Add(ctx, d.Device)
	}
}

API surface

Type / Func Purpose
Manager Owns Store, the live connection, and subscribers. New, Subscribe, Start, Watch, Query.
Device One captured sysfs node. Attrs, Refs, Children, Parent + Subsystem()/DevType()/DevName() accessors.
Store The captured graph. Add/Remove (writes), Lookup/All/Query (reads).
Match Filter vocabulary — DevPath, Subsystem, DevType, Attrs (AND-combined). Used by both Subscribe and Query.
Query Match + Scope (Descendants/Ancestors, self-inclusive) against a Store.
Scan Pure, one-shot, full sysfs capture, yielded as iter.Seq2[Delta, error].
Watch Pure, live netlink uevent stream, yielded as iter.Seq2[Delta, error].
ListenRaw Low-level: raw, unfiltered UEvent channel, bypassing Manager/Store entirely. Diagnostic use.

Diagnostic tool

go build -o bin/dmancli ./cmd/dmancli

dmancli scan  [devpath...]
dmancli watch [-subsystem=X] [-devtype=Y] [devpath...]
dmancli raw

dmancli is a diagnostic/manual-validation tool, not a public product surface — see dmancli -h. watch/raw require permission to open a netlink uevent socket (typically root or CAP_NET_ADMIN); scan reads only /sys and does not.

Development

go vet ./...
go build ./...
go test ./...

License

MIT — see LICENSE.