package agent import ( "context" "fmt" "os" "path/filepath" "slices" "strings" "time" "github.com/anoopengineer/edidparser/edid" "github.com/hashicorp/go-multierror" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "edge-infra.dev/pkg/k8s/runtime/sap" "edge-infra.dev/pkg/k8s/unstructured" "edge-infra.dev/pkg/lib/kernel/devices" "edge-infra.dev/pkg/lib/kernel/drm" cc "edge-infra.dev/pkg/sds/devices/agent/common" dsv1 "edge-infra.dev/pkg/sds/devices/k8s/apis/v1" "edge-infra.dev/pkg/sds/devices/logger" ) const ( waitReadinessTimeout = time.Second * 5 ) const ( usbSubsystem = "usb" ) // deviceNamingMapper contains a map of subsystems to appropriate function // to fetch and source the friendly name for a device var deviceNamingMapper = map[string]func(devices.Device) string{ usbSubsystem: fetchUSBDeviceName, drm.SubSystem: fetchDisplayName, } // startDeviceClassPatchingWorker starts a worker to send patch updates to device class APIs func startDeviceClassPatchingWorker(ctx context.Context, resourceManager *sap.ResourceManager, deviceClassQueue chan map[string]*dsv1.DeviceClass) { log := logger.FromContext(ctx) for { select { case <-ctx.Done(): return case deviceClasses := <-deviceClassQueue: go func() { // avoid blocking main thread if err := applyDeviceClassStatuses(ctx, resourceManager, deviceClasses); err != nil && !strings.Contains(err.Error(), "etcdserver: leader changed") { log.Error("error patching device class", "error", err) } }() } } } func applyDeviceClassStatuses(ctx context.Context, resourceManager *sap.ResourceManager, deviceClasses map[string]*dsv1.DeviceClass) error { log := logger.FromContext(ctx) deviceStatuses := &dsv1.DeviceStatuses{ TypeMeta: metav1.TypeMeta{ Kind: "DeviceStatuses", APIVersion: dsv1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: nodeName(), }, Spec: dsv1.DeviceStatusesSpec{ Devices: map[string][]dsv1.DeviceState{}, DeviceGroups: map[string][]int64{}, }, } deviceNames := map[string]string{} for _, class := range deviceClasses { deviceStates, deviceGroups, err := deviceStates(class) if err != nil { log.Debug("could not find device names", "error", err) } deviceStatuses.Spec.DeviceGroups[class.ClassName()] = deviceGroups deviceStatuses.Spec.Devices[class.ClassName()] = deviceStates for _, devState := range deviceStates { deviceNames[devState.Name] = "connected" } } uobj, err := unstructured.ToUnstructured(deviceStatuses) if err != nil { log.Error("error converting object to unstructured object", "error", err) return err } changeset, err := resourceManager.Apply(ctx, uobj, sap.ApplyOptions{Force: true, WaitTimeout: waitReadinessTimeout}) if err != nil || changeset == nil || slices.Contains([]sap.Action{sap.DeletedAction, sap.UnchangedAction}, sap.Action(changeset.Action)) { return err } log.Log(ctx, logger.LevelTrace, "applied device statuses", "action", changeset.Action, "devices", deviceNames) return nil } // devicStates will return a list of friendly device names connected to the device class func deviceStates(dc *dsv1.DeviceClass) ([]dsv1.DeviceState, []int64, error) { var errs error deviceStates := []dsv1.DeviceState{} deviceGroups := []int64{cc.DeviceGroupID} for _, device := range dc.DeviceIter() { name := fetchDeviceName(device) if name == "" { errs = multierror.Append(errs, fmt.Errorf("could not find friendly name for device: %s", device.Path())) } else { deviceStates = append(deviceStates, dsv1.DeviceState{Name: name}) } node, err := device.Node() if err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to fetch device node: %s :%w", device.Path(), err)) continue } groupOwner, err := node.GroupID() if err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to fetch device node group owner: %s :%w", node.Path(), err)) continue } if groupOwner == 0 { continue } deviceGroups = append(deviceGroups, groupOwner) } slices.Sort(deviceGroups) deviceGroups = slices.Compact(deviceGroups) return deviceStates, deviceGroups, errs } // fetchDeviceName will return a friendly device name based on the subsystem of device func fetchDeviceName(device devices.Device) string { subsystem, exists, err := device.Property("SUBSYSTEM") if err != nil || !exists { return "" } namingFn, ok := deviceNamingMapper[subsystem] if !ok { return "" } return namingFn(device) } // fetchUSBDeviceName attempts to fetch a friendly usb device name func fetchUSBDeviceName(device devices.Device) string { productName, exists, err := device.Attribute("product") if !exists || err != nil { return "" } vid, exists, err := device.Attribute("idVendor") if !exists || err != nil { return "" } pid, exists, err := device.Attribute("idProduct") if !exists || err != nil { return "" } friendlyName := fmt.Sprintf("%s:%s %s", vid, pid, productName) return fmtName(friendlyName) } // fetchDisplayName attempts to fetch a friendly display name from edid func fetchDisplayName(device devices.Device) string { edidBytes, err := os.ReadFile(filepath.Join(device.Path(), "edid")) if err != nil || len(edidBytes) == 0 { return "" } edid, err := edid.NewEdid(edidBytes) if err != nil || edid == nil { return "" } connectorID := filepath.Base(device.Path()) displayName := fmt.Sprintf("%s %s-%d", connectorID, edid.ManufacturerId, edid.ProductCode) return fmtName(displayName) } func fmtName(name string) string { name = strings.ReplaceAll(name, "\n", "") name = strings.TrimPrefix(name, "-") name = strings.TrimSuffix(name, "-") name = strings.TrimPrefix(name, " ") name = strings.TrimSuffix(name, " ") return name }