package devices import ( "bufio" "bytes" "errors" "fmt" "io" "os" "path/filepath" "strings" "golang.org/x/sys/unix" "edge-infra.dev/pkg/lib/filesystem" "edge-infra.dev/pkg/lib/kernel/udev" ) // Device interface type Device interface { Path() string Node() (Node, error) Attribute(attributeKey string) (string, bool, error) Property(propertyKey string) (string, bool, error) } // Device node implementation type Node interface { Path() string GroupID() (int64, error) UserID() (int64, error) FileMode() (os.FileMode, error) Type() (string, error) } // Device represent a unix device type device struct { path string properties map[string]string } // device node type node struct { path string } var ErrNotADevice = errors.New("not a device node") var ( SysPath = "/sys" DevPath = "/dev" sysDevicePath = filepath.Join(SysPath, "devices") sysClassPath = filepath.Join(SysPath, "class") sysBusPath = filepath.Join(SysPath, "bus") subSystemSymlinkFn = filepath.EvalSymlinks deviceTypeFn = deviceType ) // Returns a new device from sys path including child devices func New(sysPath string) (Device, error) { dev := device{ sysPath, map[string]string{}, } if err := dev.readProps(); err != nil { return &dev, err } return &dev, nil } // FromUEvent creates device object from the uevent func FromUEvent(ue *udev.UEvent) (d Device, err error) { if !strings.HasPrefix(ue.SysPath, SysPath) { return New(filepath.Join(SysPath, ue.SysPath)) } return New(ue.SysPath) } // Returns the sys path of device func (d *device) Path() string { return d.path } // Returns a devices property func (d *device) Property(prop string) (string, bool, error) { propVal, hasProp := d.properties[prop] return propVal, hasProp, nil } // Attempts to get attribute for a device if the value is not already present func (d *device) Attribute(attribute string) (string, bool, error) { readAttr, err := d.readAttribute(attribute) if err != nil { return "", false, err } return readAttr, true, nil } // Returns the device node path func (n *node) Path() string { return n.path } func (d *device) Node() (Node, error) { devNodePath, exists, err := d.Property("DEVNAME") if err != nil || !exists { return nil, fmt.Errorf("device node %s does not exist", devNodePath) } if !strings.HasPrefix(DevPath, devNodePath) { devNodePath = filepath.Join(DevPath, devNodePath) } return &node{path: devNodePath}, nil } // FileMode returns the FileMode func (n *node) FileMode() (os.FileMode, error) { fileInfo, err := os.Stat(n.Path()) if err != nil { return 0, fmt.Errorf("could not stat device node: %s : %w", n.Path(), err) } return fileInfo.Mode(), nil } // GroupID returns the group owner id of device node func (n *node) GroupID() (int64, error) { return filesystem.GroupID(n.Path()) } // UserID returns the user id of device node func (n *node) UserID() (int64, error) { return filesystem.UserID(n.Path()) } // Returns the device type i.e. char/block func (n *node) Type() (string, error) { devType, err := deviceTypeFn(n.path) if err != nil { return "", fmt.Errorf("error fetching device type for path %s: %w", n.path, err) } return devType, nil } // Reads a devices attribute from the sys path func (d *device) readAttribute(attribute string) (string, error) { file, err := os.Open(filepath.Join(d.path, attribute)) if err != nil { return "", err } defer file.Close() s := bufio.NewScanner(file) for s.Scan() { return s.Text(), nil } return "", fmt.Errorf("could not read attribute") } // Reads properties from device sysfs path/uevent func (d *device) readProps() error { f, err := os.Open(filepath.Join(d.path, "uevent")) if err != nil { return fmt.Errorf("error reading uevent file: %s: %w", d.path, err) } defer f.Close() data, err := io.ReadAll(f) if err != nil { return err } buf := bufio.NewScanner(bytes.NewBuffer(data)) var line string for buf.Scan() { line = buf.Text() field := strings.SplitN(line, "=", 2) if len(field) != 2 { continue } d.properties[field[0]] = field[1] } subsystem, err := subSystemSymlinkFn(filepath.Join(d.path, "subsystem")) if err != nil { return fmt.Errorf("error reading subsystem: %s: %w", d.path, err) } d.properties[subsystemProperty] = filepath.Base(subsystem) return nil } // fetches device type i.e. block, char, fifo func deviceType(devNodePath string) (string, error) { var stat unix.Stat_t if err := unix.Lstat(devNodePath, &stat); err != nil { return "", ErrNotADevice } var mode = stat.Mode switch mode & unix.S_IFMT { case unix.S_IFBLK: return "b", nil case unix.S_IFCHR: return "c", nil case unix.S_IFIFO: return "p", nil } return "", ErrNotADevice }