| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package crosbundle |
| |
| import ( |
| "bufio" |
| "bytes" |
| "context" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strconv" |
| "strings" |
| "time" |
| |
| configpb "go.chromium.org/chromiumos/config/go/api" |
| softwarepb "go.chromium.org/chromiumos/config/go/api/software" |
| |
| "go.chromium.org/tast/core/errors" |
| "go.chromium.org/tast/core/internal/logging" |
| "go.chromium.org/tast/core/testing/wlan" |
| |
| "go.chromium.org/tast/core/framework/protocol" |
| ) |
| |
| // GSCKeyID is a hex value that represents a key used to sign a GSC image. |
| type GSCKeyID string |
| |
| // prodRWGSCKeyIDs is a slice with production keyIDs used to sign the RW GSC image. |
| var prodRWGSCKeyIDs = []GSCKeyID{"0x87b73b67", "0xde88588d"} |
| |
| func crosConfig(path, prop string) (string, error) { |
| cmd := exec.Command("cros_config", path, prop) |
| var buf bytes.Buffer |
| cmd.Stderr = &buf |
| b, err := cmd.Output() |
| if err != nil { |
| if xerr, ok := err.(*exec.ExitError); !ok || xerr.ExitCode() != 1 { |
| return "", errors.Errorf("cros_config failed (stderr: %q): %v", buf.Bytes(), err) |
| } |
| } |
| return string(b), nil |
| } |
| |
| // detectHardwareFeatures returns a device.Config and api.HardwareFeatures instances |
| // some of whose members are filled based on runtime information. |
| func detectHardwareFeatures(ctx context.Context) (*protocol.HardwareFeatures, error) { |
| platform, err := func() (string, error) { |
| out, err := crosConfig("/identity", "platform-name") |
| if err != nil { |
| return "", err |
| } |
| return out, nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown platform-id: %v", err) |
| } |
| model, err := func() (string, error) { |
| out, err := crosConfig("/", "name") |
| if err != nil { |
| return "", err |
| } |
| return out, nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown model-id: %v", err) |
| } |
| brand, err := func() (string, error) { |
| out, err := crosConfig("/", "brand-code") |
| if err != nil { |
| return "", err |
| } |
| return out, nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown brand-id: %v", err) |
| } |
| |
| info, err := cpuInfo() |
| if err != nil { |
| logging.Infof(ctx, "Unknown CPU information: %v", err) |
| } |
| |
| vboot2, err := func() (bool, error) { |
| out, err := exec.Command("crossystem", "fw_vboot2").Output() |
| if err != nil { |
| return false, err |
| } |
| return strings.TrimSpace(string(out)) == "1", nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown vboot2 info: %v", err) |
| } |
| |
| hasSideVolumeButton, err := func() (bool, error) { |
| out, err := crosConfig("/hardware-properties", "has-side-volume-button") |
| if err != nil { |
| return false, err |
| } |
| return strings.TrimSpace(string(out)) == "true", nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown has-side-volume-button: %v", err) |
| } |
| |
| config := &protocol.DeprecatedDeviceConfig{ |
| Id: &protocol.DeprecatedConfigId{ |
| Platform: platform, |
| Model: model, |
| Brand: brand, |
| }, |
| Soc: info.soc, |
| Cpu: info.cpuArch, |
| HasNvmeSelfTest: false, |
| HasVboot2: vboot2, |
| HasSideVolumeButton: hasSideVolumeButton, |
| } |
| features := &configpb.HardwareFeatures{ |
| Screen: &configpb.HardwareFeatures_Screen{}, |
| Fingerprint: &configpb.HardwareFeatures_Fingerprint{}, |
| EmbeddedController: &configpb.HardwareFeatures_EmbeddedController{}, |
| Storage: &configpb.HardwareFeatures_Storage{}, |
| Memory: &configpb.HardwareFeatures_Memory{}, |
| Audio: &configpb.HardwareFeatures_Audio{}, |
| PrivacyScreen: &configpb.HardwareFeatures_PrivacyScreen{}, |
| Soc: &configpb.HardwareFeatures_Soc{}, |
| Touchpad: &configpb.HardwareFeatures_Touchpad{}, |
| Keyboard: &configpb.HardwareFeatures_Keyboard{}, |
| FormFactor: &configpb.HardwareFeatures_FormFactor{}, |
| DpConverter: &configpb.HardwareFeatures_DisplayPortConverter{}, |
| Wifi: &configpb.HardwareFeatures_Wifi{}, |
| Cellular: &configpb.HardwareFeatures_Cellular{}, |
| Bluetooth: &configpb.HardwareFeatures_Bluetooth{}, |
| Hps: &configpb.HardwareFeatures_Hps{}, |
| Battery: &configpb.HardwareFeatures_Battery{}, |
| Camera: &configpb.HardwareFeatures_Camera{}, |
| TrustedPlatformModule: &configpb.HardwareFeatures_TrustedPlatformModule{}, |
| FwConfig: &configpb.HardwareFeatures_FirmwareConfiguration{}, |
| RuntimeProbeConfig: &configpb.HardwareFeatures_RuntimeProbeConfig{}, |
| HardwareProbeConfig: &configpb.HardwareFeatures_HardwareProbe{}, |
| Display: &configpb.HardwareFeatures_Display{}, |
| Vrr: &configpb.HardwareFeatures_Vrr{}, |
| Hdmi: &configpb.HardwareFeatures_Hdmi{}, |
| InterruptControllerInfo: &configpb.HardwareFeatures_InterruptControllerInfo{}, |
| } |
| |
| swConfig := &softwarepb.SoftwareConfig{ |
| FirmwareInfo: &softwarepb.FirmwareInfo{}, |
| } |
| |
| formFactor, err := func() (string, error) { |
| out, err := crosConfig("/hardware-properties", "form-factor") |
| if err != nil { |
| return "", err |
| } |
| return out, nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown /hardware-properties/form-factor: %v", err) |
| } |
| lidConvertible, err := func() (string, error) { |
| out, err := crosConfig("/hardware-properties", "is-lid-convertible") |
| if err != nil { |
| return "", err |
| } |
| return out, nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown /hardware-properties/is-lid-convertible: %v", err) |
| } |
| features.Wifi, err = wifiFeatures() |
| if err != nil { |
| logging.Infof(ctx, "Error getting Wifi: %v", err) |
| } |
| |
| // Battery |
| noBatteryBootSupported, err := func() (bool, error) { |
| out, err := crosConfig("/battery", "no-battery-boot-supported") |
| if err != nil { |
| return false, err |
| } |
| return out == "true", nil |
| }() |
| features.Battery.NoBatteryBootSupported = noBatteryBootSupported |
| |
| detachableBasePath, err := func() (string, error) { |
| out, err := crosConfig("/detachable-base", "usb-path") |
| if err != nil { |
| return "", err |
| } |
| return out, nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown /detachable-base/usbpath: %v", err) |
| } |
| |
| checkHammer := func() bool { |
| hammer, err := exec.Command("udevadm", "info", "--export-db").Output() |
| if err != nil { |
| return false |
| } |
| return regexp.MustCompile(`(?m)^E: ID_MODEL=Hammer$`).Match(hammer) |
| } |
| |
| if formFactorEnum, ok := configpb.HardwareFeatures_FormFactor_FormFactorType_value[formFactor]; ok { |
| features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_FormFactorType(formFactorEnum) |
| } else if formFactor == "CHROMEBOOK" { |
| // Gru devices have formFactor=="CHROMEBOOK", detachableBasePath=="", lidConvertible="", but are really CHROMESLATE |
| if platform == "Gru" { |
| features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_CHROMESLATE |
| } else if detachableBasePath != "" { |
| features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_DETACHABLE |
| } else if lidConvertible == "true" { |
| features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_CONVERTIBLE |
| } else { |
| features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_CLAMSHELL |
| } |
| } else { |
| logging.Infof(ctx, "Form factor not found: %v", formFactor) |
| } |
| switch features.FormFactor.FormFactor { |
| case configpb.HardwareFeatures_FormFactor_CHROMEBASE, configpb.HardwareFeatures_FormFactor_CHROMEBIT, configpb.HardwareFeatures_FormFactor_CHROMEBOX, configpb.HardwareFeatures_FormFactor_CHROMESLATE: |
| features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_NONE |
| features.Touchpad.TouchpadType = configpb.HardwareFeatures_Touchpad_NONE |
| case configpb.HardwareFeatures_FormFactor_CLAMSHELL, configpb.HardwareFeatures_FormFactor_CONVERTIBLE: |
| features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_INTERNAL |
| features.Touchpad.TouchpadType = configpb.HardwareFeatures_Touchpad_INTERNAL |
| case configpb.HardwareFeatures_FormFactor_DETACHABLE: |
| // When the dut is a detachable, check whether hammer exists |
| // to determine if a removable keyboard is connected. |
| hasHammer := checkHammer() |
| if hasHammer { |
| features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_DETACHABLE |
| features.Touchpad.TouchpadType = configpb.HardwareFeatures_Touchpad_DETACHABLE |
| } else { |
| features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_NONE |
| features.Touchpad.TouchpadType = configpb.HardwareFeatures_Touchpad_NONE |
| } |
| } |
| |
| keyboardBacklight, err := func() (bool, error) { |
| out, err := crosConfig("/keyboard", "backlight") |
| if err != nil { |
| return false, err |
| } |
| return out == "true", nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown /keyboard/backlight: %v", err) |
| } |
| |
| if keyboardBacklight { |
| features.Keyboard.Backlight = configpb.HardwareFeatures_PRESENT |
| } |
| |
| checkForConnector := func(connectorRegexp string) bool { |
| const drmSysFS = "/sys/class/drm" |
| |
| drmFiles, err := ioutil.ReadDir(drmSysFS) |
| if err != nil { |
| return false |
| } |
| |
| cardMatch := regexp.MustCompile(connectorRegexp) |
| for _, file := range drmFiles { |
| fileName := file.Name() |
| |
| if cardMatch.MatchString(fileName) { |
| if cardConnected, err := ioutil.ReadFile(path.Join(drmSysFS, fileName, "status")); err != nil { |
| if !os.IsNotExist(err) { |
| return false |
| } |
| } else { |
| if strings.HasPrefix(string(cardConnected), "connected") { |
| return true |
| } |
| } |
| } |
| } |
| |
| // No indication of internal panel connected and recognised. |
| return false |
| } |
| |
| // eDP displays show up as card*-eDP-1 |
| // MIPI panels show up as card*-DSI-1 |
| // Virtual displays in VMs show up as card*-Virtual-1 |
| internalDisplayRegexp := `^card[0-9]-(eDP|DSI|Virtual)-1$` |
| hasInternalDisplay := checkForConnector(internalDisplayRegexp) |
| if hasInternalDisplay { |
| features.Screen.PanelProperties = &configpb.Component_DisplayPanel_Properties{} |
| } |
| |
| // Display ports show up as card*-DP-[0-9] |
| // HDMI ports show up as card*-HDMI-A-1 |
| // DVI ports show up as card*-DVI-I-1 |
| externalDisplayRegexp := `^card[0-9]-(DP|HDMI-A|DVI-I)-[0-9]$` |
| hasExternalDisplay := checkForConnector(externalDisplayRegexp) |
| switch { |
| case hasInternalDisplay && hasExternalDisplay: |
| features.Display.Type = configpb.HardwareFeatures_Display_TYPE_INTERNAL_EXTERNAL |
| case hasExternalDisplay: |
| features.Display.Type = configpb.HardwareFeatures_Display_TYPE_EXTERNAL |
| case hasInternalDisplay: |
| features.Display.Type = configpb.HardwareFeatures_Display_TYPE_INTERNAL |
| } |
| |
| isHdmiConnected := func() bool { |
| // HDMI ports show up as card*-HDMI-A-1 |
| hdmiCardRegex := `^card[0-9]-HDMI-A-[0-9]$` |
| return checkForConnector(hdmiCardRegex) |
| }() |
| if isHdmiConnected { |
| features.Hdmi.Present = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.Hdmi.Present = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| |
| hasTouchScreen := func() bool { |
| b, err := exec.Command("udevadm", "info", "--export-db").Output() |
| if err != nil { |
| return false |
| } |
| return regexp.MustCompile(`(?m)^E: ID_INPUT_TOUCHSCREEN=1$`).Match(b) |
| }() |
| if hasTouchScreen { |
| features.Screen.TouchSupport = configpb.HardwareFeatures_PRESENT |
| } |
| |
| hasTouchpad := func() bool { |
| tp, err := exec.Command("udevadm", "info", "--export-db").Output() |
| if err != nil { |
| return false |
| } |
| return regexp.MustCompile(`(?m)^E: ID_INPUT_TOUCHPAD=1$`).Match(tp) |
| }() |
| if hasTouchpad { |
| features.Touchpad.Present = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.Touchpad.Present = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| features.Touchpad.TouchpadType = configpb.HardwareFeatures_Touchpad_TYPE_UNKNOWN |
| } |
| |
| hasFingerprint := func() bool { |
| out, err := crosConfig("/fingerprint", "sensor-location") |
| if err != nil { |
| return false |
| } |
| if out == "" || out == "none" { |
| return false |
| } |
| return true |
| }() |
| features.Fingerprint.Present = hasFingerprint |
| |
| fingerprintBoard := func() string { |
| out, err := crosConfig("/fingerprint", "board") |
| if err != nil { |
| return "" |
| } |
| return out |
| }() |
| features.Fingerprint.Board = fingerprintBoard |
| |
| features.Fingerprint.FingerprintDiag = &configpb.HardwareFeatures_Fingerprint_FingerprintDiag{} |
| |
| fingerprintDiagRoutineEnabled := func() bool { |
| out, err := crosConfig("/cros-healthd/routines/fingerprint-diag", "routine-enable") |
| return err == nil && out == "true" |
| }() |
| features.Fingerprint.FingerprintDiag.RoutineEnable = fingerprintDiagRoutineEnabled |
| |
| features.FwConfig.FwRwVersion = &configpb.HardwareFeatures_FirmwareConfiguration_SemVer{} |
| if err := func() error { |
| out, err := exec.Command("crossystem", "fwid").Output() |
| if err != nil { |
| return err |
| } |
| rwFwIds := strings.Split(string(out), ".") |
| wantRwIds := rwFwIds[1:] |
| versions := []*uint32{ |
| &features.FwConfig.FwRwVersion.MajorVersion, |
| &features.FwConfig.FwRwVersion.MinorVersion, |
| &features.FwConfig.FwRwVersion.PatchVersion, |
| } |
| if len(versions) != len(wantRwIds) { |
| return errors.Errorf("got unexpected length of fwid: %s", out) |
| } |
| for idx, strID := range wantRwIds { |
| id, err := strconv.Atoi(strID) |
| if err != nil { |
| return err |
| } |
| *versions[idx] = uint32(id) |
| } |
| return nil |
| }(); err != nil { |
| logging.Infof(ctx, "Unable to parse fwid versions: %v", err) |
| } |
| |
| features.FwConfig.FwRoVersion = &configpb.HardwareFeatures_FirmwareConfiguration_SemVer{} |
| if err := func() error { |
| out, err := exec.Command("crossystem", "ro_fwid").Output() |
| if err != nil { |
| return err |
| } |
| roFwIds := strings.Split(string(out), ".") |
| wantIds := roFwIds[1:] |
| versions := []*uint32{ |
| &features.FwConfig.FwRoVersion.MajorVersion, |
| &features.FwConfig.FwRoVersion.MinorVersion, |
| &features.FwConfig.FwRoVersion.PatchVersion, |
| } |
| if len(versions) != len(wantIds) { |
| return errors.Errorf("got unexpected length of ro_fwid: %s", out) |
| } |
| for idx, strID := range wantIds { |
| // There are some fwids that look like Google_Foob.11297.437.0~RO |
| numericPart := strings.Split(strID, "~") |
| id, err := strconv.Atoi(numericPart[0]) |
| if err != nil { |
| return err |
| } |
| *versions[idx] = uint32(id) |
| } |
| return nil |
| }(); err != nil { |
| logging.Infof(ctx, "Unable to parse ro_fwid versions: %v", err) |
| } |
| |
| // Device has ChromeEC if /dev/cros_ec exists. |
| // TODO(b/173741162): Pull EmbeddedController data directly from Boxster. |
| if _, err := os.Stat("/dev/cros_ec"); err == nil { |
| features.EmbeddedController.Present = configpb.HardwareFeatures_PRESENT |
| features.EmbeddedController.EcType = configpb.HardwareFeatures_EmbeddedController_EC_CHROME |
| // Check for features inferred from ectool inventory. |
| output, err := exec.Command("ectool", "inventory").Output() |
| if err != nil { |
| features.EmbeddedController.FeatureTypecCmd = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| features.EmbeddedController.FeatureSystemSafeMode = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| features.EmbeddedController.FeatureAssertsPanic = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| features.EmbeddedController.FeatureMemoryDumpCommands = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| } else { |
| // The presence of the integer "41" in the inventory output is a sufficient check, since 41 is |
| // the bit position associated with this feature. |
| if bytes.Contains(output, []byte("41")) { |
| features.EmbeddedController.FeatureTypecCmd = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.EmbeddedController.FeatureTypecCmd = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| // EC_FEATURE_SYSTEM_SAFE_MODE == 47 |
| if bytes.Contains(output, []byte("47")) { |
| features.EmbeddedController.FeatureSystemSafeMode = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.EmbeddedController.FeatureSystemSafeMode = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| // EC_FEATURE_ASSERT_REBOOTS == 48 |
| if bytes.Contains(output, []byte("48")) { |
| features.EmbeddedController.FeatureAssertsPanic = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.EmbeddedController.FeatureAssertsPanic = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| // EC_FEATURE_ASSERT_REBOOTS == 51 |
| if bytes.Contains(output, []byte("51")) { |
| features.EmbeddedController.FeatureMemoryDumpCommands = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.EmbeddedController.FeatureMemoryDumpCommands = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| } |
| // Check if the detachable base is attached. |
| output, err = exec.Command("ectool", "mkbpget", "switches").Output() |
| if err != nil { |
| features.EmbeddedController.DetachableBase = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| } else if strings.Contains(string(output), "Base attached: ON") { |
| features.EmbeddedController.DetachableBase = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.EmbeddedController.DetachableBase = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| // Running `ectool chargecontrol` with no args will fail if version 2 isn't |
| // supported. Check for battery sustainer output if the command doesn't |
| // fail to make sure charger control v2 is fully supported. |
| if out, err := exec.Command("ectool", "chargecontrol").Output(); err != nil || !regexp.MustCompile(`.*Battery sustainer`).Match(out) { |
| logging.Infof(ctx, "Charge control V2 not supported: %v", err) |
| features.EmbeddedController.FeatureChargeControlV2 = configpb.HardwareFeatures_NOT_PRESENT |
| } else { |
| features.EmbeddedController.FeatureChargeControlV2 = configpb.HardwareFeatures_PRESENT |
| } |
| } |
| |
| // Device has CBI if ectool cbi get doesn't raise error. |
| if out, err := exec.Command("ectool", "cbi", "get", "0").Output(); err != nil { |
| logging.Infof(ctx, "CBI not present: %v", err) |
| features.EmbeddedController.Cbi = configpb.HardwareFeatures_NOT_PRESENT |
| } else if strings.Contains(string(out), "As uint:") { |
| features.EmbeddedController.Cbi = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.EmbeddedController.Cbi = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| } |
| |
| // Get number of PD ports from ectool, if possible. |
| if out, err := exec.Command("ectool", "usbpdpower").Output(); err == nil { |
| // Returns multiple lines like: |
| // Port 0: SNK Charger PD 15263mV / 3000mA, max 15000mV / 3000mA / 45000mW |
| // Port 1: Disconnected |
| // We want to match the last port number seen to get the count. |
| re := regexp.MustCompile(`Port (\d+):`) |
| match := re.FindAllSubmatch(out, -1) |
| if match == nil { |
| // If ectool succeeds but doesn't match the regex something is very wrong. I.e. someone modified the output of ectool. |
| return nil, errors.Errorf("ectool usbpdpower output unexpected: %v", string(out)) |
| } |
| count, err := strconv.Atoi(string(match[len(match)-1][1])) |
| if err != nil { |
| return nil, errors.Errorf("Failed to parse: %v", string(match[len(match)-1][1])) |
| } |
| features.UsbC = &configpb.HardwareFeatures_UsbC{Count: &configpb.HardwareFeatures_Count{Value: uint32(count + 1)}} |
| } |
| |
| // Device has GSC with production RW KeyId if gsctool -a -I -M |
| // returns RW KeyID with value 0x87b73b67 or 0xde88588d |
| func() { |
| if out, err := exec.Command("gsctool", "-a", "-f", "-M").Output(); err != nil { |
| logging.Infof(ctx, "Failed to exec command for KeyId info: %v", err) |
| features.TrustedPlatformModule.ProductionRwKeyId = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| } else if keyIDRW, err := findGSCKeyID(string(out), "RW"); err != nil { |
| logging.Infof(ctx, "Failed to read RW KeyId: %v", err) |
| } else if containsGSCKeyID(prodRWGSCKeyIDs, GSCKeyID(keyIDRW)) { |
| features.TrustedPlatformModule.ProductionRwKeyId = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.TrustedPlatformModule.ProductionRwKeyId = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| }() |
| |
| // Whether device has TPM enabled can be checked by `tpm_manager_client status`. |
| // If TPM is enabled, we can check the version by `tpm_version`. |
| func() { |
| features.TrustedPlatformModule.RuntimeTpmVersion = configpb.HardwareFeatures_TrustedPlatformModule_TPM_VERSION_DISABLED |
| out, err := exec.Command("tpm_manager_client", "status", "--nonsensitive").Output() |
| if err != nil { |
| logging.Info(ctx, "Failed to exec command `tpm_manager_client status`: ", err) |
| return |
| } |
| if !strings.Contains(string(out), "is_enabled: true") { |
| return |
| } |
| out, err = exec.Command("tpm_version").Output() |
| if err != nil { |
| logging.Info(ctx, "Failed to exec command `tpm_version`: ", err) |
| return |
| } |
| status := string(out) |
| if strings.Contains(status, "TPM 1.2") { |
| features.TrustedPlatformModule.RuntimeTpmVersion = configpb.HardwareFeatures_TrustedPlatformModule_TPM_VERSION_V1_2 |
| } else if strings.Contains(status, "TPM 2.0") { |
| features.TrustedPlatformModule.RuntimeTpmVersion = configpb.HardwareFeatures_TrustedPlatformModule_TPM_VERSION_V2 |
| } |
| }() |
| |
| // Check if rollback NVRAM space is present in the TPM. |
| func() { |
| out, err := exec.Command("tpm_manager_client", "list_spaces").Output() |
| if err != nil { |
| logging.Info(ctx, "Failed to exec command `tpm_manager_client list_spaces`: ", err) |
| features.TrustedPlatformModule.EnterpriseRollbackSpace = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| return |
| } |
| if bytes.Contains(out, []byte("0x0000100E")) { |
| features.TrustedPlatformModule.EnterpriseRollbackSpace = configpb.HardwareFeatures_PRESENT |
| return |
| } |
| features.TrustedPlatformModule.EnterpriseRollbackSpace = configpb.HardwareFeatures_NOT_PRESENT |
| }() |
| |
| // Obtain GSC name from existence of /opt/google/{cr50,ti50}/firmware directory. |
| func() { |
| if _, err := os.Stat("/opt/google/cr50/firmware"); err == nil { |
| features.TrustedPlatformModule.GscFwName = configpb.HardwareFeatures_TrustedPlatformModule_GSC_CR50 |
| } else if _, err := os.Stat("/opt/google/ti50/firmware"); err == nil { |
| features.TrustedPlatformModule.GscFwName = configpb.HardwareFeatures_TrustedPlatformModule_GSC_TI50 |
| } else { |
| features.TrustedPlatformModule.GscFwName = configpb.HardwareFeatures_TrustedPlatformModule_GSC_NONE |
| } |
| }() |
| |
| modemVariant, err := crosConfig("/modem", "firmware-variant") |
| features.Cellular.Present = configpb.HardwareFeatures_NOT_PRESENT |
| if err != nil { |
| logging.Infof(ctx, "Modem not found with err: %v", err) |
| } else if modemVariant == "" { |
| logging.Info(ctx, "Modem not found") |
| } else { |
| features.Cellular.Present = configpb.HardwareFeatures_PRESENT |
| features.Cellular.Model = modemVariant |
| swDynamicSar, err := func() (bool, error) { |
| out, err := crosConfig("/power", "use-modemmanager-for-dynamic-sar") |
| if err != nil { |
| return false, err |
| } |
| return out == "1", nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown /power/use-modemmanager-for-dynamic-sar: %v", err) |
| } |
| features.Cellular.DynamicPowerReductionConfig = &configpb.HardwareFeatures_Cellular_DynamicPowerReductionConfig{ |
| DynamicPowerReductionConfig: &configpb.HardwareFeatures_Cellular_DynamicPowerReductionConfig_ModemManager{ModemManager: swDynamicSar}} |
| } |
| |
| // bluetoothctl hangs when bluetoothd is not built with asan enabled or |
| // crashes. Set state to PRESENT_UNKNOWN on timeout. |
| const timeout = 3 * time.Second |
| cmdCtx, cancel := context.WithTimeout(ctx, timeout) |
| defer cancel() |
| if out, err := exec.CommandContext(cmdCtx, "bluetoothctl", "list").Output(); err != nil { |
| features.Bluetooth.Present = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| } else if len(string(out)) != 0 { |
| features.Bluetooth.Present = configpb.HardwareFeatures_PRESENT |
| } else { |
| logging.Infof(ctx, "bluetooth controller not found") |
| features.Bluetooth.Present = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| |
| readSysfsString := func(dev, relPath string) string { |
| path := "/sys/block/" + dev + "/" + relPath |
| out, err := exec.CommandContext(cmdCtx, "cat", path).Output() |
| if err != nil { |
| return "" |
| } |
| return strings.TrimSpace(string(out)) |
| } |
| |
| hasEmmcStorage := func() bool { |
| matches, err := filepath.Glob("/dev/mmc*") |
| if err != nil { |
| return false |
| } |
| for _, dev := range matches { |
| pathComponenets := strings.Split(dev, "/") |
| phyDev := pathComponenets[len(pathComponenets)-1] |
| out := readSysfsString(phyDev, "device/type") |
| if out == "MMC" { |
| return true |
| } |
| } |
| return false |
| }() |
| if hasEmmcStorage { |
| features.Storage.StorageType = configpb.Component_Storage_EMMC |
| } |
| |
| hasEmmcBridgeStorage := func() bool { |
| const bh799Ven = 0x1217 |
| const bh799Dev = 0x0002 |
| var readSysfsHex func(string, string) int64 |
| readSysfsHex = func(devPath, relPath string) int64 { |
| pathComponenets := strings.Split(devPath, "/") |
| phyDev := pathComponenets[len(pathComponenets)-1] |
| outStr := readSysfsString(phyDev, relPath) |
| if outStr == "" { |
| return 0 |
| } |
| if outStr[0:2] == "0x" { |
| outStr = outStr[2:] |
| } |
| val, err := strconv.ParseInt(outStr, 16, 64) |
| if err != nil { |
| return 0 |
| } |
| return val |
| } |
| matches, err := filepath.Glob("/dev/nvme*") |
| if err != nil { |
| return false |
| } |
| for _, dev := range matches { |
| subsysVen := readSysfsHex(dev, "device/device/subsystem_vendor") |
| subsysDev := readSysfsHex(dev, "device/device/subsystem_device") |
| if subsysVen == bh799Ven && subsysDev == bh799Dev { |
| return true |
| } |
| } |
| return false |
| }() |
| if hasEmmcBridgeStorage { |
| features.Storage.StorageType = configpb.Component_Storage_BRIDGED_EMMC |
| } |
| |
| hasNvmeStorage := func() bool { |
| matches, err := filepath.Glob("/dev/nvme*") |
| if err != nil { |
| return false |
| } |
| if len(matches) > 0 { |
| return true |
| } |
| return false |
| }() |
| if hasNvmeStorage && !hasEmmcBridgeStorage { |
| features.Storage.StorageType = configpb.Component_Storage_NVME |
| } |
| |
| hasNvmeSelfTestStorage := func() bool { |
| matches, err := filepath.Glob("/dev/nvme*n1") |
| if err != nil { |
| return false |
| } |
| if len(matches) == 0 { |
| return false |
| } |
| |
| nvmePath := matches[0] |
| b, err := exec.Command("nvme", "id-ctrl", "-H", nvmePath).Output() |
| if err != nil { |
| return false |
| } |
| return bytes.Contains(b, []byte("Device Self-test Supported")) |
| }() |
| if hasNvmeStorage && hasNvmeSelfTestStorage { |
| config.HasNvmeSelfTest = true |
| } |
| |
| hasSataStorage := func() bool { |
| matches, err := filepath.Glob("/dev/sd*") |
| if err != nil { |
| return false |
| } |
| for _, dev := range matches { |
| pathComponenets := strings.Split(dev, "/") |
| physDev := pathComponenets[len(pathComponenets)-1] |
| outStr := readSysfsString(physDev, "device/inquiry") |
| if strings.Contains(outStr, "ATA") { |
| return true |
| } |
| return false |
| } |
| return false |
| }() |
| if hasSataStorage { |
| features.Storage.StorageType = configpb.Component_Storage_SATA |
| } |
| |
| hasUfsStorage := func() bool { |
| matches, err := filepath.Glob("/dev/sd*") |
| if err != nil { |
| return false |
| } |
| for _, dev := range matches { |
| pathComponenets := strings.Split(dev, "/") |
| physDev := pathComponenets[len(pathComponenets)-1] |
| // /sys/block/<dev>/device is a symlink to the scsi target node, |
| // the symlink resolution will resolve .. directory against the linked one |
| // so we can get to the controller's node |
| path := "/sys/block/" + physDev + "/device/../../../driver" |
| out, err := exec.Command("readlink", "-f", path).Output() |
| if err != nil { |
| return false |
| } |
| resComponents := strings.Split(strings.TrimSpace(string(out)), "/") |
| res := resComponents[len(resComponents)-1] |
| if res == "ufshcd" { |
| return true |
| } |
| return false |
| } |
| return false |
| }() |
| if hasUfsStorage { |
| features.Storage.StorageType = configpb.Component_Storage_UFS |
| } |
| |
| func() { |
| // This function determines DUT's power supply type and stores it to config.Power. |
| // If DUT has a battery, config.Power is DeprecatedDeviceConfig_POWER_SUPPLY_BATTERY. |
| // If DUT has AC power supplies only, config.Power is DeprecatedDeviceConfig_POWER_SUPPLY_AC_ONLY. |
| // Otherwise, DeprecatedDeviceConfig_POWER_SUPPLY_UNSPECIFIED is populated. |
| const sysFsPowerSupplyPath = "/sys/class/power_supply" |
| // AC power types come from power_supply driver in Linux kernel (drivers/power/supply/power_supply_sysfs.c) |
| acPowerTypes := [...]string{ |
| "Unknown", "UPS", "Mains", "USB", |
| "USB_DCP", "USB_CDP", "USB_ACA", "USB_C", |
| "USB_PD", "USB_PD_DRP", "BrickID", |
| } |
| isACPower := make(map[string]bool) |
| for _, s := range acPowerTypes { |
| isACPower[s] = true |
| } |
| config.Power = protocol.DeprecatedDeviceConfig_POWER_SUPPLY_UNSPECIFIED |
| files, err := ioutil.ReadDir(sysFsPowerSupplyPath) |
| if err != nil { |
| logging.Infof(ctx, "Failed to read %v: %v", sysFsPowerSupplyPath, err) |
| return |
| } |
| for _, file := range files { |
| devPath := path.Join(sysFsPowerSupplyPath, file.Name()) |
| supplyTypeBytes, err := ioutil.ReadFile(path.Join(devPath, "type")) |
| supplyType := strings.TrimSuffix(string(supplyTypeBytes), "\n") |
| if err != nil { |
| logging.Infof(ctx, "Failed to read supply type of %v: %v", devPath, err) |
| continue |
| } |
| if strings.HasPrefix(supplyType, "Battery") { |
| supplyScopeBytes, err := ioutil.ReadFile(path.Join(devPath, "scope")) |
| supplyScope := strings.TrimSuffix(string(supplyScopeBytes), "\n") |
| if err != nil && !os.IsNotExist(err) { |
| // Ignore NotExist error since /sys/class/power_supply/*/scope may not exist |
| logging.Infof(ctx, "Failed to read supply type of %v: %v", devPath, err) |
| continue |
| } |
| if strings.HasPrefix(string(supplyScope), "Device") { |
| // Ignore batteries for peripheral devices. |
| continue |
| } |
| config.Power = protocol.DeprecatedDeviceConfig_POWER_SUPPLY_BATTERY |
| // Found at least one battery so this device is powered by battery. |
| break |
| } |
| if !isACPower[supplyType] { |
| logging.Infof(ctx, "Unknown supply type %v for %v", supplyType, devPath) |
| continue |
| } |
| config.Power = protocol.DeprecatedDeviceConfig_POWER_SUPPLY_AC_ONLY |
| } |
| }() |
| |
| storageBytes, err := func() (int64, error) { |
| b, err := exec.Command("lsblk", "-J", "-b").Output() |
| if err != nil { |
| return 0, err |
| } |
| return findDiskSize(b) |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Failed to get disk size: %v", err) |
| } |
| features.Storage.SizeGb = uint32(storageBytes / 1_000_000_000) |
| |
| memoryBytes, err := func() (int64, error) { |
| b, err := ioutil.ReadFile("/proc/meminfo") |
| if err != nil { |
| return 0, err |
| } |
| return findMemorySize(b) |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Failed to get memory size: %v", err) |
| } |
| features.Memory.Profile = &configpb.Component_Memory_Profile{ |
| // Use 2^20 megabytes to be consistent with how memory size is advertised. |
| SizeMegabytes: int32(memoryBytes >> 20), |
| } |
| |
| lidMicrophone, err := matchCrasDeviceType(`(INTERNAL|FRONT)_MIC`) |
| if err != nil { |
| logging.Infof(ctx, "Failed to get lid microphone: %v", err) |
| } |
| features.Audio.LidMicrophone = lidMicrophone |
| baseMicrophone, err := matchCrasDeviceType(`REAR_MIC`) |
| if err != nil { |
| logging.Infof(ctx, "Failed to get base microphone: %v", err) |
| } |
| features.Audio.BaseMicrophone = baseMicrophone |
| expectAudio := hasBuiltinAudio(ctx, features.FormFactor.FormFactor) |
| if features.Audio.LidMicrophone.GetValue() == 0 && features.Audio.BaseMicrophone.GetValue() == 0 && expectAudio { |
| features.Audio.LidMicrophone = &configpb.HardwareFeatures_Count{Value: 1} |
| } |
| speaker, err := matchCrasDeviceType(`INTERNAL_SPEAKER`) |
| if err != nil { |
| logging.Infof(ctx, "Failed to get speaker: %v", err) |
| } |
| |
| if speaker.GetValue() > 0 || expectAudio { |
| |
| amp, err := findSpeakerAmplifier() |
| if err != nil { |
| logging.Infof(ctx, "Failed to get amp: %v", err) |
| } |
| if amp == nil { |
| // Do not assume findSpeakerAmplifier() always returns a non-nil amp. |
| // Always signal that the device has a hwdep.Speaker(). |
| amp = &configpb.Component_Amplifier{} |
| } |
| features.Audio.SpeakerAmplifier = amp |
| } |
| |
| hasPrivacyScreen := func() bool { |
| // Get list of connectors. |
| value, err := exec.Command("modetest", "-c").Output() |
| if err != nil { |
| logging.Infof(ctx, "Failed to get connectors: %v", err) |
| return false |
| } |
| |
| modetestInfo := string(value) |
| // Check if privacy-screen prop is present. |
| if strings.Contains(modetestInfo, "privacy-screen sw-state:") && strings.Contains(modetestInfo, "privacy-screen hw-state:") { |
| return true |
| } |
| |
| // Fallback to legacy style. |
| return strings.Contains(modetestInfo, "privacy-screen:") |
| }() |
| if hasPrivacyScreen { |
| features.PrivacyScreen.Present = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.PrivacyScreen.Present = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| |
| cpuSMT, err := func() (bool, error) { |
| // NB: this sysfs API exists only on kernel >=4.19 (b/195061310). But we don't |
| // target SMT-specific tests on earlier kernels. |
| b, err := ioutil.ReadFile("/sys/devices/system/cpu/smt/control") |
| if err != nil { |
| if os.IsNotExist(err) { |
| return false, nil |
| } |
| return false, errors.Wrap(err, "failed to read SMT control file") |
| } |
| s := strings.TrimSpace(string(b)) |
| switch s { |
| case "on", "off", "forceoff": |
| return true, nil |
| case "notsupported", "notimplemented": |
| return false, nil |
| default: |
| return false, errors.Errorf("unknown SMT control status: %q", s) |
| } |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Failed to determine CPU SMT features: %v", err) |
| } |
| if cpuSMT { |
| features.Soc.Features = append(features.Soc.Features, configpb.Component_Soc_SMT) |
| } |
| |
| cpuVulnerabilityPresent := func(vulnerability string) (bool, error) { |
| vPath := filepath.Join("/sys/devices/system/cpu/vulnerabilities", vulnerability) |
| b, err := ioutil.ReadFile(vPath) |
| if err != nil { |
| return false, errors.Wrapf(err, "failed to read vulnerability file %q", vPath) |
| } |
| s := strings.TrimSpace(string(b)) |
| if s == "Not affected" { |
| return false, nil |
| } |
| return true, nil |
| } |
| |
| cpuL1TF, err := cpuVulnerabilityPresent("l1tf") |
| if err != nil { |
| logging.Infof(ctx, "Failed to determine L1TF vulnerability: %v", err) |
| } else if cpuL1TF { |
| features.Soc.Vulnerabilities = append(features.Soc.Vulnerabilities, configpb.Component_Soc_L1TF) |
| } |
| |
| cpuMDS, err := cpuVulnerabilityPresent("mds") |
| if err != nil { |
| logging.Infof(ctx, "Failed to determine MDS vulnerability: %v", err) |
| } else if cpuMDS { |
| features.Soc.Vulnerabilities = append(features.Soc.Vulnerabilities, configpb.Component_Soc_MDS) |
| } |
| |
| for _, v := range info.flags { |
| if v == "sha_ni" { |
| features.Soc.Features = append(features.Soc.Features, configpb.Component_Soc_SHA_NI) |
| } |
| } |
| |
| func() { |
| // Probe for presence of DisplayPort converters |
| devices := map[string]string{ |
| "i2c-10EC2141:00": "RTD2141B", |
| "i2c-10EC2142:00": "RTD2142", |
| "i2c-1AF80175:00": "PS175", |
| } |
| for f, name := range devices { |
| path := filepath.Join("/sys/bus/i2c/devices", f) |
| if _, err := os.Stat(path); err != nil { |
| continue |
| } |
| features.DpConverter.Converters = append(features.DpConverter.Converters, &configpb.Component_DisplayPortConverter{ |
| Name: name, |
| }) |
| } |
| }() |
| |
| hasHps, err := func() (bool, error) { |
| out, err := crosConfig("/hps", "has-hps") |
| if err != nil { |
| return false, err |
| } |
| return out == "true", nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown /hps: %v", err) |
| } |
| if hasHps { |
| features.Hps.Present = configpb.HardwareFeatures_PRESENT |
| } |
| |
| camFeatures, err := cameraFeatures(model) |
| if err != nil { |
| logging.Infof(ctx, "failed to load camera feature profile: %v", err) |
| } |
| features.Camera.Features = camFeatures |
| camEnumerated, camEnumeratedUsbIds, err := cameraEnumerated() |
| if err != nil { |
| logging.Infof(ctx, "failed to load camera enumeration status: %v", err) |
| } |
| features.Camera.Enumerated = camEnumerated |
| features.Camera.EnumeratedUsbIds = camEnumeratedUsbIds |
| |
| if err := parseKConfigs(ctx, features); err != nil { |
| logging.Info(ctx, "Failed to parse BIOS kConfig: ", err) |
| } |
| |
| if err := parseECBuildConfig(ctx, features); err != nil { |
| logging.Info(ctx, "Failed to parse EC Config: ", err) |
| } |
| |
| rpConfigPresent, err := hasRuntimeProbeConfig(model, false) |
| if err != nil { |
| logging.Info(ctx, "Failed to determine if the config of Runtime Probe exists: ", err) |
| } |
| if rpConfigPresent { |
| features.RuntimeProbeConfig.Present = configpb.HardwareFeatures_PRESENT |
| } else { |
| logging.Infof(ctx, "Config of Runtime Probe not found") |
| features.RuntimeProbeConfig.Present = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| |
| rpEncryptedConfigPresent, err := hasRuntimeProbeConfig(model, true) |
| if err != nil { |
| logging.Info(ctx, "Failed to determine if the encrypted config of Runtime Probe exists: ", err) |
| } |
| if rpEncryptedConfigPresent { |
| features.RuntimeProbeConfig.EncryptedConfigPresent = configpb.HardwareFeatures_PRESENT |
| } else { |
| logging.Infof(ctx, "Encrypted config of Runtime Probe not found") |
| features.RuntimeProbeConfig.EncryptedConfigPresent = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| |
| gpuFamily, gpuVendor, cpuSocFamily, err := func() (gpuFamily, gpuVendor, cpuSocFamily string, fetchErr error) { |
| outBin, err := ioutil.TempFile("/tmp", "hardware_probe") |
| if err != nil { |
| return "", "", "", errors.Wrap(err, "failed to create temp file") |
| } |
| outBin.Close() |
| defer os.Remove(outBin.Name()) |
| out, err := exec.Command("/usr/local/graphics/hardware_probe", "-output", outBin.Name()).CombinedOutput() |
| if err != nil { |
| return "", "", "", errors.Wrapf(err, "failed to run hardware_probe, output: %v", string(out)) |
| } |
| type gpuInfo struct { |
| Family string `json:"Family"` |
| Vendor string `json:"GPUVendor"` |
| } |
| |
| type hardwareProbeResult struct { |
| GPUInfo []gpuInfo `json:"GPU_Family"` |
| CPUFamily string `json:"CPU_SOC_Family"` |
| } |
| b, err := os.ReadFile(outBin.Name()) |
| if err != nil { |
| return "", "", "", errors.Wrap(err, "failed to read hardware_probe.json") |
| } |
| var result hardwareProbeResult |
| if err := json.Unmarshal(b, &result); err != nil { |
| return "", "", "", err |
| } |
| if len(result.GPUInfo) > 0 { |
| gpuFamily = result.GPUInfo[0].Family |
| gpuVendor = result.GPUInfo[0].Vendor |
| if len(result.GPUInfo) > 1 { |
| logging.Infof(ctx, "Found multiple GPUInfo(%v), only use the first one detected.", result.GPUInfo) |
| } |
| } |
| cpuSocFamily = result.CPUFamily |
| return |
| }() |
| if err != nil { |
| logging.Infof(ctx, "failed to parse hardware_probe output: %v", err) |
| } |
| features.HardwareProbeConfig.GpuFamily = gpuFamily |
| features.HardwareProbeConfig.GpuVendor = gpuVendor |
| features.HardwareProbeConfig.CpuSocFamily = cpuSocFamily |
| |
| hevcSupport, err := func() (configpb.HardwareFeatures_Present, error) { |
| out, err := crosConfig("/ui", "serialized-ash-switches") |
| if err != nil { |
| return configpb.HardwareFeatures_PRESENT_UNKNOWN, err |
| } |
| if regexp.MustCompile("disable-features=[^\x00]*PlatformHEVCDecoderSupport").MatchString(out) { |
| return configpb.HardwareFeatures_NOT_PRESENT, nil |
| } |
| if regexp.MustCompile("enable-features=[^\x00]*PlatformHEVCDecoderSupport").MatchString(out) { |
| return configpb.HardwareFeatures_PRESENT, nil |
| } |
| return configpb.HardwareFeatures_PRESENT_UNKNOWN, nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown /ui/serialized-ash-switches: %v", err) |
| } |
| features.Soc.HevcSupport = hevcSupport |
| |
| hasVrrSupport := func() bool { |
| modetestOutput, err := exec.Command("modetest").Output() |
| if err != nil { |
| logging.Infof(ctx, "failed to run modetest: %v", err) |
| return false |
| } |
| // check if there exists a vrr_capable property with a value set to 1 in |
| // the modetest command output |
| regex := regexp.MustCompile("\n\t.*vrr_capable:(\n\t\t.*)*\n\t\tvalue: 1") |
| return regex.Match(modetestOutput) |
| }() |
| if hasVrrSupport { |
| features.Vrr.Present = configpb.HardwareFeatures_PRESENT |
| } else { |
| features.Vrr.Present = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| |
| features.FeatureLevel, err = featureLevel() |
| if err != nil { |
| logging.Infof(ctx, "Failed to get feature level: %v", err) |
| } |
| |
| features.OemInfo = &configpb.HardwareFeatures_OEMInfo{ |
| Name: oemName(), |
| } |
| |
| // FirmwareFeatures |
| altfw, err := func() (bool, error) { |
| out, err := crosConfig("/firmware", "has-alt-firmware") |
| if err != nil { |
| return false, err |
| } |
| return out == "true", nil |
| }() |
| if err != nil { |
| logging.Infof(ctx, "Unknown firmware/altfw-present : %v", err) |
| } |
| if altfw { |
| swConfig.FirmwareInfo.HasAltFirmware = true |
| } else { |
| swConfig.FirmwareInfo.HasAltFirmware = false |
| } |
| |
| // InterruptControllerInfo |
| |
| // Default to assuming we have NMI support. We only say NMI is |
| // unsupported if we've explicitly decided we can't support it for |
| // a given configuration. Note that upon any errors we'll leave this |
| // as "present". If a test doesn't care then that's fine. If a test |
| // relies on NMI support then we _want_ the test to fail and point out |
| // that we failed to get the config properly. |
| features.InterruptControllerInfo.NmiSupport = configpb.HardwareFeatures_PRESENT |
| if runtime.GOARCH == "arm" { |
| features.InterruptControllerInfo.NmiSupport = configpb.HardwareFeatures_NOT_PRESENT |
| } else if runtime.GOARCH == "arm64" { |
| // GICv2 can't support NMIs. There's no wonderful way to detect |
| // the version of the GIC, but /proc/interrupts will contain |
| // GICv2 in this case so we'll use that. |
| if interruptsOutput, err := os.ReadFile("/proc/interrupts"); err != nil { |
| // Give out a warning because /proc/interrupts should always exist. |
| logging.Infof(ctx, "Couldn't read /proc/interrupts: %v", err) |
| } else if strings.Contains(string(interruptsOutput), "GICv2") { |
| features.InterruptControllerInfo.NmiSupport = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| |
| // If the device tree indicates broken firmware then we know we |
| // don't support NMI. |
| if brokenGicOutput, err := exec.Command("find", "/proc/device-tree/", "-name", "mediatek,broken-save-restore-fw").Output(); err != nil { |
| // All arm64 should have /proc/device-tree/ |
| logging.Infof(ctx, "Failed to search device-tree for GIC FW Quirk: %v", err) |
| } else if string(brokenGicOutput) != "" { |
| features.InterruptControllerInfo.NmiSupport = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| } |
| |
| features.Accelerometer = &configpb.HardwareFeatures_Accelerometer{} |
| func() { |
| out, err := crosConfig("/hardware-properties", "has-base-accelerometer") |
| if err != nil { |
| features.Accelerometer.BaseAccelerometer = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| return |
| } |
| if out == "" || out == "false" { |
| features.Accelerometer.BaseAccelerometer = configpb.HardwareFeatures_NOT_PRESENT |
| return |
| } |
| features.Accelerometer.BaseAccelerometer = configpb.HardwareFeatures_PRESENT |
| }() |
| |
| func() { |
| out, err := exec.CommandContext(cmdCtx, "lspci", "-mm").Output() |
| if err != nil { |
| features.FwConfig.IntelIsh = configpb.HardwareFeatures_PRESENT_UNKNOWN |
| return |
| } |
| lines := strings.Split(string(out), "\n") |
| for _, line := range lines { |
| if strings.Contains(line, "Integrated Sensor Hub") { |
| features.FwConfig.IntelIsh = configpb.HardwareFeatures_PRESENT |
| return |
| } |
| } |
| features.FwConfig.IntelIsh = configpb.HardwareFeatures_NOT_PRESENT |
| }() |
| |
| return &protocol.HardwareFeatures{ |
| HardwareFeatures: features, |
| DeprecatedDeviceConfig: config, |
| SoftwareConfig: swConfig, |
| }, nil |
| } |
| |
| type lscpuEntry struct { |
| Field string `json:"field"` // includes trailing ":" |
| Data string `json:"data"` |
| } |
| |
| type lscpuResult struct { |
| Entries []lscpuEntry `json:"lscpu"` |
| } |
| |
| func (r *lscpuResult) find(name string) (data string, ok bool) { |
| for _, e := range r.Entries { |
| if e.Field == name { |
| return e.Data, true |
| } |
| } |
| return "", false |
| } |
| |
| type cpuConfig struct { |
| cpuArch protocol.DeprecatedDeviceConfig_Architecture |
| soc protocol.DeprecatedDeviceConfig_SOC |
| flags []string |
| } |
| |
| // cpuInfo returns a structure containing field data from the "lscpu" command |
| // which outputs CPU architecture information from "sysfs" and "/proc/cpuinfo". |
| func cpuInfo() (cpuConfig, error) { |
| errInfo := cpuConfig{protocol.DeprecatedDeviceConfig_ARCHITECTURE_UNDEFINED, protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, nil} |
| b, err := exec.Command("lscpu", "--json").Output() |
| if err != nil { |
| return errInfo, err |
| } |
| var parsed lscpuResult |
| if err := json.Unmarshal(b, &parsed); err != nil { |
| return errInfo, errors.Wrap(err, "failed to parse lscpu result") |
| } |
| flagsStr, _ := parsed.find("Flags:") |
| flags := strings.Split(flagsStr, " ") |
| arch, err := findArchitecture(parsed) |
| if err != nil { |
| return errInfo, errors.Wrap(err, "failed to find CPU architecture") |
| } |
| soc, err := findSOC(parsed) |
| if err != nil { |
| return cpuConfig{arch, protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, flags}, errors.Wrap(err, "failed to find SOC") |
| } |
| return cpuConfig{arch, soc, flags}, nil |
| } |
| |
| // findArchitecture returns an architecture configuration based from parsed output |
| // data value of the "Architecture" field. |
| func findArchitecture(parsed lscpuResult) (protocol.DeprecatedDeviceConfig_Architecture, error) { |
| arch, ok := parsed.find("Architecture:") |
| if !ok { |
| return protocol.DeprecatedDeviceConfig_ARCHITECTURE_UNDEFINED, errors.New("failed to find Architecture field") |
| } |
| |
| switch arch { |
| case "x86_64": |
| return protocol.DeprecatedDeviceConfig_X86_64, nil |
| case "i686": |
| return protocol.DeprecatedDeviceConfig_X86, nil |
| case "aarch64": |
| return protocol.DeprecatedDeviceConfig_ARM64, nil |
| case "armv7l", "armv8l": |
| return protocol.DeprecatedDeviceConfig_ARM, nil |
| default: |
| return protocol.DeprecatedDeviceConfig_ARCHITECTURE_UNDEFINED, errors.Errorf("unknown architecture: %q", arch) |
| } |
| } |
| |
| // findSOC returns a SOC configuration based from parsed output data value of the |
| // "Vendor ID" and other related fields. |
| func findSOC(parsed lscpuResult) (protocol.DeprecatedDeviceConfig_SOC, error) { |
| vendorID, ok := parsed.find("Vendor ID:") |
| if !ok { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Vendor ID field") |
| } |
| |
| switch vendorID { |
| case "ARM": |
| fallthrough |
| case "Qualcomm": |
| return findARMSOC() |
| case "GenuineIntel": |
| return findIntelSOC(&parsed) |
| case "AuthenticAMD": |
| return findAMDSOC(&parsed) |
| default: |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown vendor ID: %q", vendorID) |
| } |
| } |
| |
| // findARMSOC returns an ARM SOC configuration based on "soc_id" from "/sys/bus/soc/devices". |
| func findARMSOC() (protocol.DeprecatedDeviceConfig_SOC, error) { |
| // Platforms with SMCCC >= 1.2 should implement get_soc functions in firmware |
| const socSysFS = "/sys/bus/soc/devices" |
| socs, err := ioutil.ReadDir(socSysFS) |
| if err == nil { |
| for _, soc := range socs { |
| c, err := ioutil.ReadFile(path.Join(socSysFS, soc.Name(), "soc_id")) |
| if err != nil || !strings.HasPrefix(string(c), "jep106:") { |
| continue |
| } |
| // Trim trailing \x00 and \n |
| socID := strings.TrimRight(string(c), "\x00\n") |
| switch socID { |
| case "jep106:0070:01a9": |
| fallthrough |
| case "jep106:0070:01ef": |
| fallthrough |
| case "jep106:0070:7180": // Used by older SC7180 firmware |
| return protocol.DeprecatedDeviceConfig_SOC_SC7180, nil |
| case "jep106:0070:7280": |
| return protocol.DeprecatedDeviceConfig_SOC_SC7280, nil |
| case "jep106:0426:8192": |
| return protocol.DeprecatedDeviceConfig_SOC_MT8192, nil |
| case "jep106:0426:8186": |
| return protocol.DeprecatedDeviceConfig_SOC_MT8186, nil |
| case "jep106:0426:8195": |
| return protocol.DeprecatedDeviceConfig_SOC_MT8195, nil |
| case "jep106:0426:8188": |
| return protocol.DeprecatedDeviceConfig_SOC_MT8188G, nil |
| default: |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown ARM model: %s", socID) |
| } |
| } |
| } |
| |
| // For old platforms with SMCCC < 1.2: mt8173, mt8183, rk3288, rk3399, |
| // match with their compatible string. Obtain the string after the last , and trim \x00. |
| // Example: google,krane-sku176\x00google,krane\x00mediatek,mt8183\x00 |
| c, err := ioutil.ReadFile("/sys/firmware/devicetree/base/compatible") |
| if err != nil { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Wrap(err, "failed to find ARM model") |
| } |
| |
| compatible := string(c) |
| model := strings.ToLower(compatible[strings.LastIndex(compatible, ",")+1:]) |
| model = strings.TrimRight(model, "\x00") |
| |
| switch model { |
| case "mt8173": |
| return protocol.DeprecatedDeviceConfig_SOC_MT8173, nil |
| case "mt8183": |
| return protocol.DeprecatedDeviceConfig_SOC_MT8183, nil |
| case "rk3288": |
| return protocol.DeprecatedDeviceConfig_SOC_RK3288, nil |
| case "rk3399": |
| return protocol.DeprecatedDeviceConfig_SOC_RK3399, nil |
| default: |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown ARM model: %s", model) |
| } |
| } |
| |
| // findIntelSOC returns an Intel SOC configuration based on "CPU family", "Model", |
| // and "Model name" fields. |
| func findIntelSOC(parsed *lscpuResult) (protocol.DeprecatedDeviceConfig_SOC, error) { |
| if family, ok := parsed.find("CPU family:"); !ok { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel family") |
| } else if family != "6" { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown Intel family: %s", family) |
| } |
| |
| modelStr, ok := parsed.find("Model:") |
| if !ok { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel model") |
| } |
| model, err := strconv.ParseInt(modelStr, 10, 64) |
| if err != nil { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Wrapf(err, "failed to parse Intel model: %q", modelStr) |
| } |
| switch model { |
| case INTEL_FAM6_KABYLAKE_L: |
| // AMBERLAKE_Y, COMET_LAKE_U, WHISKEY_LAKE_U, KABYLAKE_U, KABYLAKE_U_R, and |
| // KABYLAKE_Y share the same model. Parse model name. |
| // Note that Pentium brand is unsupported. |
| modelName, ok := parsed.find("Model name:") |
| if !ok { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel model name") |
| } |
| for _, e := range []struct { |
| soc protocol.DeprecatedDeviceConfig_SOC |
| ptn string |
| }{ |
| // https://ark.intel.com/content/www/us/en/ark/products/codename/186968/amber-lake-y.html |
| {protocol.DeprecatedDeviceConfig_SOC_AMBERLAKE_Y, `Core.* [mi]\d-(10|8)\d{3}Y`}, |
| |
| // https://ark.intel.com/content/www/us/en/ark/products/codename/90354/comet-lake.html |
| {protocol.DeprecatedDeviceConfig_SOC_COMET_LAKE_U, `Core.* i\d-10\d{3}U|Celeron.* 5[23]05U|Intel\(R\) Pentium\(R\) CPU 6405U`}, |
| |
| // https://ark.intel.com/content/www/us/en/ark/products/codename/135883/whiskey-lake.html |
| {protocol.DeprecatedDeviceConfig_SOC_WHISKEY_LAKE_U, `Core.* i\d-8\d{2}5U|Celeron.* 4[23]05U`}, |
| |
| // https://ark.intel.com/content/www/us/en/ark/products/codename/82879/kaby-lake.html |
| {protocol.DeprecatedDeviceConfig_SOC_KABYLAKE_U, `Core.* i\d-7\d{3}U|Celeron.* 3[89]65U`}, |
| {protocol.DeprecatedDeviceConfig_SOC_KABYLAKE_Y, `Core.* [mi]\d-7Y\d{2}|Celeron.* 3965Y`}, |
| |
| // https://ark.intel.com/content/www/us/en/ark/products/codename/126287/kaby-lake-r.html |
| {protocol.DeprecatedDeviceConfig_SOC_KABYLAKE_U_R, `Core.* i\d-8\d{2}0U|Celeron.* 3867U`}, |
| } { |
| r := regexp.MustCompile(e.ptn) |
| if r.MatchString(modelName) { |
| return e.soc, nil |
| } |
| } |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown model name: %s", modelName) |
| case INTEL_FAM6_ICELAKE_L: |
| return protocol.DeprecatedDeviceConfig_SOC_ICE_LAKE_Y, nil |
| case INTEL_FAM6_ATOM_GOLDMONT_PLUS: |
| return protocol.DeprecatedDeviceConfig_SOC_GEMINI_LAKE, nil |
| case INTEL_FAM6_ATOM_TREMONT_L: |
| return protocol.DeprecatedDeviceConfig_SOC_JASPER_LAKE, nil |
| case INTEL_FAM6_TIGERLAKE_L: |
| return protocol.DeprecatedDeviceConfig_SOC_TIGER_LAKE, nil |
| case INTEL_FAM6_ALDERLAKE_L: |
| return protocol.DeprecatedDeviceConfig_SOC_ALDER_LAKE, nil |
| case INTEL_FAM6_METEORLAKE_L: |
| return protocol.DeprecatedDeviceConfig_SOC_METEOR_LAKE, nil |
| case INTEL_FAM6_CANNONLAKE_L: |
| return protocol.DeprecatedDeviceConfig_SOC_CANNON_LAKE_Y, nil |
| case INTEL_FAM6_ATOM_GOLDMONT: |
| return protocol.DeprecatedDeviceConfig_SOC_APOLLO_LAKE, nil |
| case INTEL_FAM6_SKYLAKE_L: |
| // SKYLAKE_U and SKYLAKE_Y share the same model. Parse model name. |
| modelName, ok := parsed.find("Model name:") |
| if !ok { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel model name") |
| } |
| for _, e := range []struct { |
| soc protocol.DeprecatedDeviceConfig_SOC |
| ptn string |
| }{ |
| // https://ark.intel.com/content/www/us/en/ark/products/codename/37572/skylake.html |
| {protocol.DeprecatedDeviceConfig_SOC_SKYLAKE_U, `Core.* i\d-6\d{3}U|Celeron.*3[89]55U`}, |
| {protocol.DeprecatedDeviceConfig_SOC_SKYLAKE_Y, `Core.* m\d-6Y\d{2}`}, |
| } { |
| r := regexp.MustCompile(e.ptn) |
| if r.MatchString(modelName) { |
| return e.soc, nil |
| } |
| } |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown model name: %s", modelName) |
| case INTEL_FAM6_ATOM_AIRMONT: |
| return protocol.DeprecatedDeviceConfig_SOC_BRASWELL, nil |
| case INTEL_FAM6_BROADWELL: |
| return protocol.DeprecatedDeviceConfig_SOC_BROADWELL, nil |
| case INTEL_FAM6_HASWELL, INTEL_FAM6_HASWELL_L: |
| return protocol.DeprecatedDeviceConfig_SOC_HASWELL, nil |
| case INTEL_FAM6_IVYBRIDGE: |
| return protocol.DeprecatedDeviceConfig_SOC_IVY_BRIDGE, nil |
| case INTEL_FAM6_ATOM_SILVERMONT: |
| return protocol.DeprecatedDeviceConfig_SOC_BAY_TRAIL, nil |
| case INTEL_FAM6_SANDYBRIDGE: |
| return protocol.DeprecatedDeviceConfig_SOC_SANDY_BRIDGE, nil |
| case INTEL_FAM6_ATOM_BONNELL: |
| return protocol.DeprecatedDeviceConfig_SOC_PINE_TRAIL, nil |
| default: |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown Intel model: %d", model) |
| } |
| } |
| |
| // findAMDSOC returns an AMD SOC configuration based on "Model" field. |
| func findAMDSOC(parsed *lscpuResult) (protocol.DeprecatedDeviceConfig_SOC, error) { |
| model, ok := parsed.find("Model:") |
| if !ok { |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find AMD model") |
| } |
| if model == "112" { |
| return protocol.DeprecatedDeviceConfig_SOC_STONEY_RIDGE, nil |
| } |
| if family, ok := parsed.find("CPU family:"); ok { |
| if family == "23" { |
| if model == "24" || model == "32" { |
| return protocol.DeprecatedDeviceConfig_SOC_PICASSO, nil |
| } else if model == "160" { |
| return protocol.DeprecatedDeviceConfig_SOC_MENDOCINO, nil |
| } |
| } else if family == "25" { |
| if model == "80" { |
| return protocol.DeprecatedDeviceConfig_SOC_CEZANNE, nil |
| } else if model == "116" { |
| return protocol.DeprecatedDeviceConfig_SOC_PHOENIX, nil |
| } |
| } |
| } |
| |
| return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown AMD model: %s", model) |
| } |
| |
| // lsblk command output differs depending on the version. Attempt parsing in multiple ways to accept all the cases. |
| |
| // lsblk from util-linux 2.32 |
| type blockDevices2_32 struct { |
| Name string `json:"name"` |
| Removable string `json:"rm"` |
| Size string `json:"size"` |
| Type string `json:"type"` |
| } |
| |
| type lsblkRoot2_32 struct { |
| BlockDevices []blockDevices2_32 `json:"blockdevices"` |
| } |
| |
| // lsblk from util-linux 2.36.1 |
| type blockDevices struct { |
| Name string `json:"name"` |
| Removable bool `json:"rm"` |
| Size int64 `json:"size"` |
| Type string `json:"type"` |
| } |
| |
| type lsblkRoot struct { |
| BlockDevices []blockDevices `json:"blockdevices"` |
| } |
| |
| func parseLsblk2_32(jsonData []byte) (*lsblkRoot, error) { |
| var old lsblkRoot2_32 |
| err := json.Unmarshal(jsonData, &old) |
| if err != nil { |
| return nil, err |
| } |
| |
| var r lsblkRoot |
| for _, e := range old.BlockDevices { |
| s, err := strconv.ParseInt(e.Size, 10, 64) |
| if err != nil { |
| return nil, err |
| } |
| var rm bool |
| if e.Removable == "0" || e.Removable == "" { |
| rm = false |
| } else if e.Removable == "1" { |
| rm = true |
| } else { |
| return nil, fmt.Errorf("unknown value for rm: %q", e.Removable) |
| } |
| r.BlockDevices = append(r.BlockDevices, blockDevices{ |
| Name: e.Name, |
| Removable: rm, |
| Size: s, |
| Type: e.Type, |
| }) |
| } |
| return &r, nil |
| } |
| |
| func parseLsblk2_36(jsonData []byte) (*lsblkRoot, error) { |
| var r lsblkRoot |
| err := json.Unmarshal(jsonData, &r) |
| if err != nil { |
| return nil, err |
| } |
| return &r, nil |
| } |
| |
| func parseLsblk(jsonData []byte) (*lsblkRoot, error) { |
| var errs []error |
| parsers := []func([]byte) (*lsblkRoot, error){parseLsblk2_36, parseLsblk2_32} |
| for _, p := range parsers { |
| r, err := p(jsonData) |
| if err == nil { |
| return r, nil |
| } |
| errs = append(errs, err) |
| } |
| var errStrings []string |
| for _, e := range errs { |
| errStrings = append(errStrings, e.Error()) |
| } |
| return nil, fmt.Errorf("failed to parse JSON in all the expected formats: %s", strings.Join(errStrings, "; ")) |
| } |
| |
| // findDiskSize detects the size of the storage device from "lsblk -J -b" output in bytes. |
| // When there are multiple disks, returns the size of the largest one. |
| func findDiskSize(jsonData []byte) (int64, error) { |
| r, err := parseLsblk(jsonData) |
| if err != nil { |
| return 0, err |
| } |
| var maxSize int64 |
| var found bool |
| for _, x := range r.BlockDevices { |
| if x.Type == "disk" && !x.Removable && !strings.HasPrefix(x.Name, "zram") { |
| found = true |
| if x.Size > maxSize { |
| maxSize = x.Size |
| } |
| } |
| } |
| if !found { |
| return 0, errors.New("no disk device found") |
| } |
| return maxSize, nil |
| } |
| |
| // findMemorySize parses a content of /proc/meminfo and returns the total memory size in bytes. |
| func findMemorySize(meminfo []byte) (int64, error) { |
| r := bytes.NewReader(meminfo) |
| sc := bufio.NewScanner(r) |
| for sc.Scan() { |
| line := sc.Text() |
| tokens := strings.SplitN(line, ":", 2) |
| if len(tokens) != 2 || strings.TrimSpace(tokens[0]) != "MemTotal" { |
| continue |
| } |
| cap := strings.SplitN(strings.TrimSpace(tokens[1]), " ", 2) |
| if len(cap) != 2 { |
| return 0, fmt.Errorf("unexpected line format: input=%s", line) |
| } |
| if cap[1] != "kB" { |
| return 0, fmt.Errorf("unexpected unit: got %s, want kB; input=%s", cap[1], line) |
| } |
| val, err := strconv.ParseInt(cap[0], 10, 64) |
| if err != nil { |
| return 0, err |
| } |
| // meminfo reported kB is 2^10 bytes not 10^3. |
| return val << 10, nil |
| } |
| return 0, fmt.Errorf("MemTotal not found; input=%q", string(meminfo)) |
| } |
| |
| func matchCrasDeviceType(pattern string) (*configpb.HardwareFeatures_Count, error) { |
| b, err := exec.Command("cras_test_client").Output() |
| if err != nil { |
| return nil, err |
| } |
| if regexp.MustCompile(pattern).Match(b) { |
| return &configpb.HardwareFeatures_Count{Value: 1}, nil |
| } |
| return &configpb.HardwareFeatures_Count{Value: 0}, nil |
| } |
| |
| // findSpeakerAmplifier parses a content of in "/sys/kernel/debug/asoc/components" |
| // and returns the speaker amplifier used. |
| func findSpeakerAmplifier() (*configpb.Component_Amplifier, error) { |
| |
| // This sys path exists only on kernel >=4.14. But we don't |
| // target amp tests on earlier kernels. |
| f, err := os.Open("/sys/kernel/debug/asoc/components") |
| if err != nil { |
| return &configpb.Component_Amplifier{}, err |
| } |
| defer f.Close() |
| scanner := bufio.NewScanner(f) |
| for scanner.Scan() { |
| if amp, found := matchSpeakerAmplifier(scanner.Text()); found { |
| if enabled, err := bootTimeCalibration(); err == nil && enabled { |
| amp.Features = append(amp.Features, configpb.Component_Amplifier_BOOT_TIME_CALIBRATION) |
| } |
| return amp, err |
| } |
| } |
| return &configpb.Component_Amplifier{}, nil |
| } |
| |
| var ampsRegexp = map[string]*regexp.Regexp{ |
| configpb.HardwareFeatures_Audio_MAX98357.String(): regexp.MustCompile(`^(i2c-)?ma?x98357a?((:\d*)|([_-]?\d*))?$`), |
| configpb.HardwareFeatures_Audio_MAX98373.String(): regexp.MustCompile(`^(i2c-)?ma?x98373((:\d*)|([_-]?\d*))?$`), |
| configpb.HardwareFeatures_Audio_MAX98360.String(): regexp.MustCompile(`^(i2c-)?ma?x98360a?((:\d*)|([_-]?\d*))?$`), |
| configpb.HardwareFeatures_Audio_RT1015.String(): regexp.MustCompile(`^(i2c-)?((rtl?)|(10ec))?1015(\.\d*)?((:\d*)|([_-]?\d*))?$`), |
| configpb.HardwareFeatures_Audio_RT1015P.String(): regexp.MustCompile(`^(i2c-)?(rtl?)?(10ec)?1015p(\.\d*)?((:\d*)|([_-]?\d*))?$`), |
| configpb.HardwareFeatures_Audio_ALC1011.String(): regexp.MustCompile(`^(i2c-)?((rtl?)|(10ec))?1011(\.\d*)?((:\d*)|([_-]?\d*))?$`), |
| configpb.HardwareFeatures_Audio_MAX98390.String(): regexp.MustCompile(`^(i2c-)?ma?x98390((:\d*)|(\.\d-\d+)|([_-]?\d*))?$`), |
| configpb.HardwareFeatures_Audio_CS35L41.String(): regexp.MustCompile(`^(i2c-)?csc3541((:\d*)|([_-]?\d*))?$`), |
| } |
| |
| func matchSpeakerAmplifier(line string) (*configpb.Component_Amplifier, bool) { |
| for amp, re := range ampsRegexp { |
| if re.MatchString(strings.ToLower(line)) { |
| return &configpb.Component_Amplifier{Name: amp}, true |
| } |
| } |
| return nil, false |
| } |
| |
| // bootTimeCalibration returns whether the boot time calibration is |
| // enabled by parsing the sound_card_init config. |
| func bootTimeCalibration() (bool, error) { |
| config, err := crosConfig("/audio/main", "sound-card-init-conf") |
| if err != nil { |
| return false, err |
| } |
| path := "/etc/sound_card_init/" + config |
| if _, err := os.Stat(path); err != nil { |
| // Regard config non-existence as boot_time_calibration disabled. |
| if os.IsNotExist(err) { |
| return false, nil |
| } |
| return false, err |
| } |
| b, err := ioutil.ReadFile(path) |
| if err != nil { |
| return false, errors.New("failed to read sound_card_init config") |
| } |
| return isBootTimeCalibrationEnabled(string(b)) |
| } |
| |
| func isBootTimeCalibrationEnabled(s string) (bool, error) { |
| re := regexp.MustCompile(`boot_time_calibration_enabled\s*?:\s*?(true|false)`) |
| match := re.FindStringSubmatch(s) |
| if match == nil { |
| return false, errors.New("invalid sound_card_init config") |
| } |
| enabled := match[1] |
| return enabled == "true", nil |
| } |
| |
| func wifiFeatures() (*configpb.HardwareFeatures_Wifi, error) { |
| dev, err := wlan.DeviceInfo() |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get device") |
| } |
| |
| _, err = exec.Command("vpd", "-g", "wifi_sar").Output() |
| vpdSarFound := err == nil |
| |
| return &configpb.HardwareFeatures_Wifi{ |
| WifiChips: []configpb.HardwareFeatures_Wifi_WifiChip{ |
| configpb.HardwareFeatures_Wifi_WifiChip(dev.ID)}, |
| WifiVpdSar: vpdSarFound, |
| }, nil |
| } |
| |
| // hasBuiltinAudio tells if a given form factor has built-in audio devices |
| func hasBuiltinAudio(ctx context.Context, ff configpb.HardwareFeatures_FormFactor_FormFactorType) bool { |
| switch ff { |
| case configpb.HardwareFeatures_FormFactor_CLAMSHELL, |
| configpb.HardwareFeatures_FormFactor_CONVERTIBLE, |
| configpb.HardwareFeatures_FormFactor_DETACHABLE, |
| configpb.HardwareFeatures_FormFactor_CHROMEBASE, |
| configpb.HardwareFeatures_FormFactor_CHROMESLATE: |
| return true |
| case configpb.HardwareFeatures_FormFactor_CHROMEBIT, |
| configpb.HardwareFeatures_FormFactor_CHROMEBOX: |
| return false |
| default: |
| logging.Infof(ctx, "Unknown form factor: %s", ff) |
| return false |
| } |
| } |
| |
| // cameraFeatures returns the list of configured camera features for the given |
| // |model| by inspecting the on-device feature config file. |
| func cameraFeatures(model string) ([]string, error) { |
| type modelConfig map[string]struct { |
| FeatureSet []map[string]interface{} `json:"feature_set"` |
| } |
| const featureProfilePath = "/etc/camera/feature_profile.json" |
| jsonInput, err := ioutil.ReadFile(featureProfilePath) |
| if err != nil { |
| return nil, errors.Wrap(err, "cannot load feature profile config") |
| } |
| conf := make(modelConfig) |
| if err := json.Unmarshal(jsonInput, &conf); err != nil { |
| return nil, errors.Wrap(err, "cannot unmarshal feature profile config") |
| } |
| c, ok := conf[model] |
| if !ok { |
| return nil, errors.Errorf("feature set config for model %s doesn't exist", model) |
| } |
| featureSet := make(map[string]bool) |
| for _, f := range c.FeatureSet { |
| var v interface{} |
| // The "type" attribute is always a string. |
| if v, ok = f["type"]; !ok { |
| continue |
| } |
| // There can be multiple entries for a feature with different |
| // constraints. |
| if _, ok := featureSet[v.(string)]; !ok { |
| featureSet[v.(string)] = true |
| } |
| } |
| var ret []string |
| for k := range featureSet { |
| ret = append(ret, k) |
| } |
| return ret, nil |
| } |
| |
| // cameraUsbEnumerated returns the list of enumerated USB cameras. |
| func cameraUsbEnumerated() ([]string, error) { |
| var ids []string |
| out, err := exec.Command("media_v4l2_test", "--list_builtin_usbcam").Output() |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to run media_v4l2_test") |
| } |
| for _, name := range strings.Fields(string(out)) { |
| path := "/sys/class/video4linux/" + filepath.Base(name) + "/device" |
| path, err := filepath.EvalSymlinks(path) |
| if err != nil { |
| return nil, err |
| } |
| path = filepath.Dir(string(path)) |
| |
| out, err := os.ReadFile(path + "/idVendor") |
| if err != nil { |
| return nil, err |
| } |
| vid := strings.TrimSuffix(string(out), "\n") |
| |
| out, err = os.ReadFile(path + "/idProduct") |
| if err != nil { |
| return nil, err |
| } |
| pid := strings.TrimSuffix(string(out), "\n") |
| ids = append(ids, vid+":"+pid) |
| } |
| return ids, nil |
| } |
| |
| // cameraEnumerated returns whether camera devices have been all enumerated, together with |
| // the list of enumerated USB cameras. |
| func cameraEnumerated() (bool, []string, error) { |
| countStr, err := crosConfig("/camera", "count") |
| if err != nil { |
| return false, nil, errors.Wrap(err, "failed to get count from crosConfig") |
| } |
| count, err := strconv.Atoi(countStr) |
| if err != nil { |
| return false, nil, errors.Wrap(err, "failed to parse crosConfig output") |
| } |
| |
| usbCamIds, err := cameraUsbEnumerated() |
| if err != nil { |
| return false, nil, errors.Wrap(err, "failed to get enumerated USB cams") |
| } |
| usbCams := len(usbCamIds) |
| mipiCams := func() int { |
| out, err := exec.Command("cros-camera-tool", "modules", "list").Output() |
| if err != nil { |
| return 0 |
| } |
| var cams []map[string]string |
| if err := json.Unmarshal(out, &cams); err != nil { |
| return 0 |
| } |
| return len(cams) |
| }() |
| |
| return usbCams+mipiCams == count, usbCamIds, nil |
| } |
| |
| // findGSCKeyID parses a content of "gsctool -a -f -M" and return a required key |
| func findGSCKeyID(str, keyIDType string) (string, error) { |
| re := regexp.MustCompile(`(?m)^keyids: RO (0x.+), RW (0x.+)$`) |
| |
| switch keyIDType { |
| case "RO": |
| keyID := re.FindAllStringSubmatch(str, -1)[0][1] |
| return keyID, nil |
| case "RW": |
| keyID := re.FindAllStringSubmatch(str, -1)[0][2] |
| return keyID, nil |
| default: |
| return "", errors.Errorf("Unknown keyId type %s", keyIDType) |
| } |
| } |
| |
| // containsGSCKeyID returns true if reqKeyID is in the keyIDs |
| func containsGSCKeyID(keyIDs []GSCKeyID, reqKeyID GSCKeyID) bool { |
| for _, keyID := range keyIDs { |
| if keyID == reqKeyID { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // For mocking. |
| var flashromExtractCoreBootCmd = func(ctx context.Context, corebootBinName string) error { |
| return exec.CommandContext(ctx, "flashrom", "-p", "internal", "-r", "-i", fmt.Sprintf("FW_MAIN_A:%s", corebootBinName)).Run() |
| } |
| var cbfsToolExtractCmd = func(ctx context.Context, corebootBinName, fileName, outputPath string) error { |
| return exec.CommandContext(ctx, "cbfstool", corebootBinName, "extract", "-n", fileName, "-f", outputPath).Run() |
| } |
| |
| var configLineRegexp = regexp.MustCompile(`^(# )?(CONFIG\S*)(=(y)| (is not set))`) |
| |
| // parseKConfigs updates the provided HardwareFeatures with the features found |
| // by reading reading through the BIOS Kconfigs. |
| func parseKConfigs(ctx context.Context, features *configpb.HardwareFeatures) error { |
| corebootBin, err := ioutil.TempFile("/var/tmp", "") |
| if err != nil { |
| return errors.Wrap(err, "failed to create temp file") |
| } |
| corebootBin.Close() |
| defer os.Remove(corebootBin.Name()) |
| |
| fwConfig, err := ioutil.TempFile("/var/tmp", "") |
| if err != nil { |
| return errors.Wrap(err, "failed to create temp file") |
| } |
| fwConfig.Close() |
| defer os.Remove(fwConfig.Name()) |
| |
| if err := flashromExtractCoreBootCmd(ctx, corebootBin.Name()); err != nil { |
| return errors.Wrap(err, "failed to extract FW_MAIN_A bios section") |
| } |
| if err := cbfsToolExtractCmd(ctx, corebootBin.Name(), "config", fwConfig.Name()); err != nil { |
| return errors.Wrap(err, "failed to extract bios Kconfig file") |
| } |
| inFile, err := os.Open(fwConfig.Name()) |
| if err != nil { |
| return errors.Wrap(err, "failed to read bios Kconfig file") |
| } |
| defer inFile.Close() |
| |
| importantConfigs := map[string]*configpb.HardwareFeatures_Present{ |
| "CONFIG_MAINBOARD_HAS_EARLY_LIBGFXINIT": &features.FwConfig.MainboardHasEarlyLibgfxinit, |
| "CONFIG_VBOOT_CBFS_INTEGRATION": &features.FwConfig.VbootCbfsIntegration, |
| "CONFIG_BMP_LOGO": &features.FwConfig.BmpLogo, |
| "CONFIG_CHROMEOS_FW_SPLASH_SCREEN": &features.FwConfig.FwSplashScreen, |
| } |
| |
| scanner := bufio.NewScanner(inFile) |
| for scanner.Scan() { |
| line := scanner.Text() |
| if match := configLineRegexp.FindStringSubmatch(line); match != nil { |
| if val, ok := importantConfigs[match[2]]; ok { |
| if match[4] == "y" { |
| *val = configpb.HardwareFeatures_PRESENT |
| } else if match[5] == "is not set" { |
| *val = configpb.HardwareFeatures_NOT_PRESENT |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| func parseECBuildConfig(ctx context.Context, features *configpb.HardwareFeatures) error { |
| corebootBin, err := ioutil.TempFile("/var/tmp", "") |
| if err != nil { |
| return errors.Wrap(err, "failed to create temp file") |
| } |
| corebootBin.Close() |
| defer os.Remove(corebootBin.Name()) |
| |
| ecConfig, err := ioutil.TempFile("/var/tmp", "") |
| if err != nil { |
| return errors.Wrap(err, "failed to create temp file") |
| } |
| ecConfig.Close() |
| defer os.Remove(ecConfig.Name()) |
| |
| if err := flashromExtractCoreBootCmd(ctx, corebootBin.Name()); err != nil { |
| return errors.Wrap(err, "failed to extract FW_MAIN_A bios section") |
| } |
| if err := cbfsToolExtractCmd(ctx, corebootBin.Name(), "ecrw.config", ecConfig.Name()); err != nil { |
| return errors.Wrap(err, "failed to extract ec config file") |
| } |
| inFile, err := os.Open(ecConfig.Name()) |
| if err != nil { |
| return errors.Wrap(err, "failed to read ec Kconfig file") |
| } |
| defer inFile.Close() |
| |
| ecBuildConfig := make(map[string]configpb.HardwareFeatures_Present) |
| scanner := bufio.NewScanner(inFile) |
| for scanner.Scan() { |
| line := scanner.Text() |
| if match := configLineRegexp.FindStringSubmatch(line); match != nil { |
| if match[4] == "y" { |
| ecBuildConfig[match[2]] = configpb.HardwareFeatures_PRESENT |
| } else if match[5] == "is not set" { |
| ecBuildConfig[match[2]] = configpb.HardwareFeatures_NOT_PRESENT |
| } else { |
| return errors.Errorf("Malformed config line: %s", line) |
| } |
| } |
| } |
| features.EmbeddedController.BuildConfig = ecBuildConfig |
| return nil |
| } |
| |
| // hasRuntimeProbeConfig returns true if the corresponding probe config for the |
| // model of the DUT exists. The err is set if the error os.Stat returns is not |
| // fs.ErrNotExist. |
| func hasRuntimeProbeConfig(model string, encrypted bool) (bool, error) { |
| probeConfigRelPath := "etc/runtime_probe/" + model + "/probe_config.json" |
| if encrypted { |
| probeConfigRelPath += ".enc" |
| } |
| configRoots := []string{ |
| "/usr/local/", |
| "/", |
| } |
| for _, configRoot := range configRoots { |
| _, err := os.Stat(configRoot + probeConfigRelPath) |
| if err == nil { |
| return true, nil |
| } |
| if !os.IsNotExist(err) { |
| return false, err |
| } |
| } |
| return false, nil |
| } |
| |
| // featureLevel returns the feature level number of the DUT. |
| func featureLevel() (level uint32, err error) { |
| cmd := exec.Command("feature_explorer", "--feature_level") |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| return 0, errors.Wrapf(err, "failed to call feature_explorer %s", string(out)) |
| } |
| value, err := strconv.ParseUint(strings.TrimSpace(string(out)), 0, 64) |
| if err != nil { |
| return 0, errors.Wrapf(err, "failed to parse feature level %s", string(out)) |
| } |
| return uint32(value), nil |
| } |
| |
| func oemName() string { |
| if out, err := crosConfig("/branding", "oem-name"); err == nil { |
| if out != "" { |
| return out |
| } |
| } |
| |
| if out, err := os.ReadFile("/sys/firmware/vpd/ro/oem_name"); err == nil { |
| if string(out) != "" { |
| return string(out) |
| } |
| } |
| |
| return "" |
| } |