github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/internal/crosbundle/hardware.go (about)

     1  // Copyright 2021 The ChromiumOS Authors
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package crosbundle
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path"
    17  	"path/filepath"
    18  	"regexp"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	configpb "go.chromium.org/chromiumos/config/go/api"
    24  
    25  	"go.chromium.org/tast/core/errors"
    26  	"go.chromium.org/tast/core/internal/logging"
    27  	"go.chromium.org/tast/core/testing/wlan"
    28  
    29  	"go.chromium.org/tast/core/framework/protocol"
    30  )
    31  
    32  // GSCKeyID is a hex value that represents a key used to sign a GSC image.
    33  type GSCKeyID string
    34  
    35  // prodRWGSCKeyIDs is a slice with production keyIDs used to sign the RW GSC image.
    36  var prodRWGSCKeyIDs = []GSCKeyID{"0x87b73b67", "0xde88588d"}
    37  
    38  func crosConfig(path, prop string) (string, error) {
    39  	cmd := exec.Command("cros_config", path, prop)
    40  	var buf bytes.Buffer
    41  	cmd.Stderr = &buf
    42  	b, err := cmd.Output()
    43  	if err != nil {
    44  		return "", errors.Errorf("cros_config failed (stderr: %q): %v", buf.Bytes(), err)
    45  	}
    46  	return string(b), nil
    47  }
    48  
    49  // detectHardwareFeatures returns a device.Config and api.HardwareFeatures instances
    50  // some of whose members are filled based on runtime information.
    51  func detectHardwareFeatures(ctx context.Context) (*protocol.HardwareFeatures, error) {
    52  	platform, err := func() (string, error) {
    53  		out, err := crosConfig("/identity", "platform-name")
    54  		if err != nil {
    55  			return "", err
    56  		}
    57  		return out, nil
    58  	}()
    59  	if err != nil {
    60  		logging.Infof(ctx, "Unknown platform-id: %v", err)
    61  	}
    62  	model, err := func() (string, error) {
    63  		out, err := crosConfig("/", "name")
    64  		if err != nil {
    65  			return "", err
    66  		}
    67  		return out, nil
    68  	}()
    69  	if err != nil {
    70  		logging.Infof(ctx, "Unknown model-id: %v", err)
    71  	}
    72  	brand, err := func() (string, error) {
    73  		out, err := crosConfig("/", "brand-code")
    74  		if err != nil {
    75  			return "", err
    76  		}
    77  		return out, nil
    78  	}()
    79  	if err != nil {
    80  		logging.Infof(ctx, "Unknown brand-id: %v", err)
    81  	}
    82  
    83  	info, err := cpuInfo()
    84  	if err != nil {
    85  		logging.Infof(ctx, "Unknown CPU information: %v", err)
    86  	}
    87  
    88  	vboot2, err := func() (bool, error) {
    89  		out, err := exec.Command("crossystem", "fw_vboot2").Output()
    90  		if err != nil {
    91  			return false, err
    92  		}
    93  		return strings.TrimSpace(string(out)) == "1", nil
    94  	}()
    95  	if err != nil {
    96  		logging.Infof(ctx, "Unknown vboot2 info: %v", err)
    97  	}
    98  
    99  	config := &protocol.DeprecatedDeviceConfig{
   100  		Id: &protocol.DeprecatedConfigId{
   101  			Platform: platform,
   102  			Model:    model,
   103  			Brand:    brand,
   104  		},
   105  		Soc:             info.soc,
   106  		Cpu:             info.cpuArch,
   107  		HasNvmeSelfTest: false,
   108  		HasVboot2:       vboot2,
   109  	}
   110  	features := &configpb.HardwareFeatures{
   111  		Screen:                &configpb.HardwareFeatures_Screen{},
   112  		Fingerprint:           &configpb.HardwareFeatures_Fingerprint{},
   113  		EmbeddedController:    &configpb.HardwareFeatures_EmbeddedController{},
   114  		Storage:               &configpb.HardwareFeatures_Storage{},
   115  		Memory:                &configpb.HardwareFeatures_Memory{},
   116  		Audio:                 &configpb.HardwareFeatures_Audio{},
   117  		PrivacyScreen:         &configpb.HardwareFeatures_PrivacyScreen{},
   118  		Soc:                   &configpb.HardwareFeatures_Soc{},
   119  		Touchpad:              &configpb.HardwareFeatures_Touchpad{},
   120  		Keyboard:              &configpb.HardwareFeatures_Keyboard{},
   121  		FormFactor:            &configpb.HardwareFeatures_FormFactor{},
   122  		DpConverter:           &configpb.HardwareFeatures_DisplayPortConverter{},
   123  		Wifi:                  &configpb.HardwareFeatures_Wifi{},
   124  		Cellular:              &configpb.HardwareFeatures_Cellular{},
   125  		Bluetooth:             &configpb.HardwareFeatures_Bluetooth{},
   126  		Hps:                   &configpb.HardwareFeatures_Hps{},
   127  		Battery:               &configpb.HardwareFeatures_Battery{},
   128  		Camera:                &configpb.HardwareFeatures_Camera{},
   129  		TrustedPlatformModule: &configpb.HardwareFeatures_TrustedPlatformModule{},
   130  		FwConfig:              &configpb.HardwareFeatures_FirmwareConfiguration{},
   131  		RuntimeProbeConfig:    &configpb.HardwareFeatures_RuntimeProbeConfig{},
   132  		HardwareProbeConfig:   &configpb.HardwareFeatures_HardwareProbe{},
   133  		Display:               &configpb.HardwareFeatures_Display{},
   134  	}
   135  
   136  	formFactor, err := func() (string, error) {
   137  		out, err := crosConfig("/hardware-properties", "form-factor")
   138  		if err != nil {
   139  			return "", err
   140  		}
   141  		return out, nil
   142  	}()
   143  	if err != nil {
   144  		logging.Infof(ctx, "Unknown /hardware-properties/form-factor: %v", err)
   145  	}
   146  	lidConvertible, err := func() (string, error) {
   147  		out, err := crosConfig("/hardware-properties", "is-lid-convertible")
   148  		if err != nil {
   149  			return "", err
   150  		}
   151  		return out, nil
   152  	}()
   153  	if err != nil {
   154  		logging.Infof(ctx, "Unknown /hardware-properties/is-lid-convertible: %v", err)
   155  	}
   156  	features.Wifi, err = wifiFeatures()
   157  	if err != nil {
   158  		logging.Infof(ctx, "Error getting Wifi: %v", err)
   159  	}
   160  
   161  	// Battery
   162  	noBatteryBootSupported, err := func() (bool, error) {
   163  		out, err := crosConfig("/battery", "no-battery-boot-supported")
   164  		if err != nil {
   165  			return false, err
   166  		}
   167  		return out == "true", nil
   168  	}()
   169  	features.Battery.NoBatteryBootSupported = noBatteryBootSupported
   170  
   171  	detachableBasePath, err := func() (string, error) {
   172  		out, err := crosConfig("/detachable-base", "usb-path")
   173  		if err != nil {
   174  			return "", err
   175  		}
   176  		return out, nil
   177  	}()
   178  	if err != nil {
   179  		logging.Infof(ctx, "Unknown /detachable-base/usbpath: %v", err)
   180  	}
   181  
   182  	checkHammer := func() bool {
   183  		hammer, err := exec.Command("udevadm", "info", "--export-db").Output()
   184  		if err != nil {
   185  			return false
   186  		}
   187  		return regexp.MustCompile(`(?m)^E: ID_MODEL=Hammer$`).Match(hammer)
   188  	}
   189  
   190  	if formFactorEnum, ok := configpb.HardwareFeatures_FormFactor_FormFactorType_value[formFactor]; ok {
   191  		features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_FormFactorType(formFactorEnum)
   192  	} else if formFactor == "CHROMEBOOK" {
   193  		// Gru devices have formFactor=="CHROMEBOOK", detachableBasePath=="", lidConvertible="", but are really CHROMESLATE
   194  		if platform == "Gru" {
   195  			features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_CHROMESLATE
   196  		} else if detachableBasePath != "" {
   197  			features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_DETACHABLE
   198  		} else if lidConvertible == "true" {
   199  			features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_CONVERTIBLE
   200  		} else {
   201  			features.FormFactor.FormFactor = configpb.HardwareFeatures_FormFactor_CLAMSHELL
   202  		}
   203  	} else {
   204  		logging.Infof(ctx, "Form factor not found: %v", formFactor)
   205  	}
   206  	switch features.FormFactor.FormFactor {
   207  	case configpb.HardwareFeatures_FormFactor_CHROMEBASE, configpb.HardwareFeatures_FormFactor_CHROMEBIT, configpb.HardwareFeatures_FormFactor_CHROMEBOX, configpb.HardwareFeatures_FormFactor_CHROMESLATE:
   208  		features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_NONE
   209  	case configpb.HardwareFeatures_FormFactor_CLAMSHELL, configpb.HardwareFeatures_FormFactor_CONVERTIBLE:
   210  		features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_INTERNAL
   211  	case configpb.HardwareFeatures_FormFactor_DETACHABLE:
   212  		// When the dut is a detachable, check whether hammer exists
   213  		// to determine if a removable keyboard is connected.
   214  		hasHammer := checkHammer()
   215  		if hasHammer {
   216  			features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_DETACHABLE
   217  		} else {
   218  			features.Keyboard.KeyboardType = configpb.HardwareFeatures_Keyboard_NONE
   219  		}
   220  	}
   221  
   222  	keyboardBacklight, err := func() (bool, error) {
   223  		out, err := crosConfig("/keyboard", "backlight")
   224  		if err != nil {
   225  			return false, err
   226  		}
   227  		return out == "true", nil
   228  	}()
   229  	if err != nil {
   230  		logging.Infof(ctx, "Unknown /keyboard/backlight: %v", err)
   231  	}
   232  
   233  	hasKeyboardBacklight, err := func() (string, error) {
   234  		out, err := crosConfig("/power", "has-keyboard-backlight")
   235  		if err != nil {
   236  			return "", err
   237  		}
   238  		return out, nil
   239  	}()
   240  	if err != nil {
   241  		logging.Infof(ctx, "Unknown /power/has-keyboard-backlight: %v", err)
   242  	}
   243  
   244  	hasKeyboardBacklightUnderPowerManager, err := func() (bool, error) {
   245  		const fileName = "/usr/share/power_manager/has_keyboard_backlight"
   246  		content, err := ioutil.ReadFile(fileName)
   247  		if err != nil {
   248  			if os.IsNotExist(err) {
   249  				return false, nil
   250  			}
   251  			return false, errors.Errorf("failed to read file %q: %v", fileName, err)
   252  		}
   253  		return strings.TrimSuffix(string(content), "\n") == "1", nil
   254  	}()
   255  	if err != nil {
   256  		logging.Infof(ctx, "Unknown /usr/share/power_manager: %v", err)
   257  	}
   258  
   259  	switch hasKeyboardBacklight {
   260  	case "1":
   261  		features.Keyboard.Backlight = configpb.HardwareFeatures_PRESENT
   262  	case "":
   263  		if keyboardBacklight || hasKeyboardBacklightUnderPowerManager {
   264  			features.Keyboard.Backlight = configpb.HardwareFeatures_PRESENT
   265  		}
   266  	default:
   267  		features.Keyboard.Backlight = configpb.HardwareFeatures_NOT_PRESENT
   268  	}
   269  
   270  	checkForConnector := func(connectorRegexp string) bool {
   271  		const drmSysFS = "/sys/class/drm"
   272  
   273  		drmFiles, err := ioutil.ReadDir(drmSysFS)
   274  		if err != nil {
   275  			return false
   276  		}
   277  
   278  		cardMatch := regexp.MustCompile(connectorRegexp)
   279  		for _, file := range drmFiles {
   280  			fileName := file.Name()
   281  
   282  			if cardMatch.MatchString(fileName) {
   283  				if cardConnected, err := ioutil.ReadFile(path.Join(drmSysFS, fileName, "status")); err != nil {
   284  					if !os.IsNotExist(err) {
   285  						return false
   286  					}
   287  				} else {
   288  					return strings.HasPrefix(string(cardConnected), "connected")
   289  				}
   290  			}
   291  		}
   292  
   293  		// No indication of internal panel connected and recognised.
   294  		return false
   295  	}
   296  
   297  	// eDP displays show up as card*-eDP-1
   298  	// MIPI panels show up as card*-DSI-1
   299  	// Virtual displays in VMs show up as card*-Virtual-1
   300  	internalDisplayRegexp := `^card[0-9]-(eDP|DSI|Virtual)-1$`
   301  	hasInternalDisplay := checkForConnector(internalDisplayRegexp)
   302  	if hasInternalDisplay {
   303  		features.Screen.PanelProperties = &configpb.Component_DisplayPanel_Properties{}
   304  	}
   305  
   306  	// Display ports show up as card*-DP-[0-9]
   307  	// HDMI ports show up as card*-HDMI-A-1
   308  	// DVI ports show up as card*-DVI-I-1
   309  	externalDisplayRegexp := `^card[0-9]-(DP|HDMI-A|DVI-I)-[0-9]$`
   310  	hasExternalDisplay := checkForConnector(externalDisplayRegexp)
   311  	switch {
   312  	case hasInternalDisplay && hasExternalDisplay:
   313  		features.Display.Type = configpb.HardwareFeatures_Display_TYPE_INTERNAL_EXTERNAL
   314  	case hasExternalDisplay:
   315  		features.Display.Type = configpb.HardwareFeatures_Display_TYPE_EXTERNAL
   316  	case hasInternalDisplay:
   317  		features.Display.Type = configpb.HardwareFeatures_Display_TYPE_INTERNAL
   318  	}
   319  
   320  	hasTouchScreen := func() bool {
   321  		b, err := exec.Command("udevadm", "info", "--export-db").Output()
   322  		if err != nil {
   323  			return false
   324  		}
   325  		return regexp.MustCompile(`(?m)^E: ID_INPUT_TOUCHSCREEN=1$`).Match(b)
   326  	}()
   327  	if hasTouchScreen {
   328  		features.Screen.TouchSupport = configpb.HardwareFeatures_PRESENT
   329  	}
   330  
   331  	hasTouchpad := func() bool {
   332  		tp, err := exec.Command("udevadm", "info", "--export-db").Output()
   333  		if err != nil {
   334  			return false
   335  		}
   336  		return regexp.MustCompile(`(?m)^E: ID_INPUT_TOUCHPAD=1$`).Match(tp)
   337  	}()
   338  	if hasTouchpad {
   339  		features.Touchpad.Present = configpb.HardwareFeatures_PRESENT
   340  	}
   341  
   342  	hasFingerprint := func() bool {
   343  		out, err := crosConfig("/fingerprint", "sensor-location")
   344  		if err != nil {
   345  			return false
   346  		}
   347  		if out == "" || out == "none" {
   348  			return false
   349  		}
   350  		return true
   351  	}()
   352  	features.Fingerprint.Present = hasFingerprint
   353  
   354  	// Device has ChromeEC if /dev/cros_ec exists.
   355  	// TODO(b/173741162): Pull EmbeddedController data directly from Boxster.
   356  	if _, err := os.Stat("/dev/cros_ec"); err == nil {
   357  		features.EmbeddedController.Present = configpb.HardwareFeatures_PRESENT
   358  		features.EmbeddedController.EcType = configpb.HardwareFeatures_EmbeddedController_EC_CHROME
   359  		// Check for EC_FEATURE_TYPEC_CMD.
   360  		output, err := exec.Command("ectool", "inventory").Output()
   361  		if err != nil {
   362  			features.EmbeddedController.FeatureTypecCmd = configpb.HardwareFeatures_PRESENT_UNKNOWN
   363  		} else {
   364  			// The presence of the integer "41" in the inventory output is a sufficient check, since 41 is
   365  			// the bit position associated with this feature.
   366  			if bytes.Contains(output, []byte("41")) {
   367  				features.EmbeddedController.FeatureTypecCmd = configpb.HardwareFeatures_PRESENT
   368  			} else {
   369  				features.EmbeddedController.FeatureTypecCmd = configpb.HardwareFeatures_NOT_PRESENT
   370  			}
   371  		}
   372  		// Check if the detachable base is attached.
   373  		output, err = exec.Command("ectool", "mkbpget", "switches").Output()
   374  		if err != nil {
   375  			features.EmbeddedController.DetachableBase = configpb.HardwareFeatures_PRESENT_UNKNOWN
   376  		} else if strings.Contains(string(output), "Base attached: ON") {
   377  			features.EmbeddedController.DetachableBase = configpb.HardwareFeatures_PRESENT
   378  		} else {
   379  			features.EmbeddedController.DetachableBase = configpb.HardwareFeatures_NOT_PRESENT
   380  		}
   381  		// Running `ectool chargecontrol` with no args will fail if version 2 isn't
   382  		// supported. Check for battery sustainer output if the command doesn't
   383  		// fail to make sure charger control v2 is fully supported.
   384  		if out, err := exec.Command("ectool", "chargecontrol").Output(); err != nil || !regexp.MustCompile(`.*Battery sustainer`).Match(out) {
   385  			logging.Infof(ctx, "Charge control V2 not supported: %v", err)
   386  			features.EmbeddedController.FeatureChargeControlV2 = configpb.HardwareFeatures_NOT_PRESENT
   387  		} else {
   388  			features.EmbeddedController.FeatureChargeControlV2 = configpb.HardwareFeatures_PRESENT
   389  		}
   390  	}
   391  
   392  	// Device has CBI if ectool cbi get doesn't raise error.
   393  	if out, err := exec.Command("ectool", "cbi", "get", "0").Output(); err != nil {
   394  		logging.Infof(ctx, "CBI not present: %v", err)
   395  		features.EmbeddedController.Cbi = configpb.HardwareFeatures_NOT_PRESENT
   396  	} else if strings.Contains(string(out), "As uint:") {
   397  		features.EmbeddedController.Cbi = configpb.HardwareFeatures_PRESENT
   398  	} else {
   399  		features.EmbeddedController.Cbi = configpb.HardwareFeatures_PRESENT_UNKNOWN
   400  	}
   401  
   402  	// Device has GSC with production RW KeyId if gsctool -a -I -M
   403  	// returns RW KeyID with value 0x87b73b67 or 0xde88588d
   404  	func() {
   405  		if out, err := exec.Command("gsctool", "-a", "-f", "-M").Output(); err != nil {
   406  			logging.Infof(ctx, "Failed to exec command for KeyId info: %v", err)
   407  			features.TrustedPlatformModule.ProductionRwKeyId = configpb.HardwareFeatures_PRESENT_UNKNOWN
   408  		} else if keyIDRW, err := findGSCKeyID(string(out), "RW"); err != nil {
   409  			logging.Infof(ctx, "Failed to read RW KeyId: %v", err)
   410  		} else if containsGSCKeyID(prodRWGSCKeyIDs, GSCKeyID(keyIDRW)) {
   411  			features.TrustedPlatformModule.ProductionRwKeyId = configpb.HardwareFeatures_PRESENT
   412  		} else {
   413  			features.TrustedPlatformModule.ProductionRwKeyId = configpb.HardwareFeatures_NOT_PRESENT
   414  		}
   415  	}()
   416  
   417  	// Whether device has TPM enabled can be checked by `tpm_manager_client status`.
   418  	// If TPM is enabled, we can check the version by `tpm_version`.
   419  	func() {
   420  		features.TrustedPlatformModule.RuntimeTpmVersion = configpb.HardwareFeatures_TrustedPlatformModule_TPM_VERSION_DISABLED
   421  		out, err := exec.Command("tpm_manager_client", "status", "--nonsensitive").Output()
   422  		if err != nil {
   423  			logging.Info(ctx, "Failed to exec command `tpm_manager_client status`: ", err)
   424  			return
   425  		}
   426  		if !strings.Contains(string(out), "is_enabled: true") {
   427  			return
   428  		}
   429  		out, err = exec.Command("tpm_version").Output()
   430  		if err != nil {
   431  			logging.Info(ctx, "Failed to exec command `tpm_version`: ", err)
   432  			return
   433  		}
   434  		status := string(out)
   435  		if strings.Contains(status, "TPM 1.2") {
   436  			features.TrustedPlatformModule.RuntimeTpmVersion = configpb.HardwareFeatures_TrustedPlatformModule_TPM_VERSION_V1_2
   437  		} else if strings.Contains(status, "TPM 2.0") {
   438  			features.TrustedPlatformModule.RuntimeTpmVersion = configpb.HardwareFeatures_TrustedPlatformModule_TPM_VERSION_V2
   439  		}
   440  	}()
   441  
   442  	modemVariant, err := crosConfig("/modem", "firmware-variant")
   443  	if err != nil {
   444  		logging.Infof(ctx, "Modem not found: %v", err)
   445  		features.Cellular.Present = configpb.HardwareFeatures_NOT_PRESENT
   446  	} else {
   447  		features.Cellular.Present = configpb.HardwareFeatures_PRESENT
   448  		features.Cellular.Model = modemVariant
   449  		swDynamicSar, err := func() (bool, error) {
   450  			out, err := crosConfig("/power", "use-modemmanager-for-dynamic-sar")
   451  			if err != nil {
   452  				return false, err
   453  			}
   454  			return out == "1", nil
   455  		}()
   456  		if err != nil {
   457  			logging.Infof(ctx, "Unknown /power/use-modemmanager-for-dynamic-sar: %v", err)
   458  		}
   459  		features.Cellular.DynamicPowerReductionConfig = &configpb.HardwareFeatures_Cellular_DynamicPowerReductionConfig{
   460  			DynamicPowerReductionConfig: &configpb.HardwareFeatures_Cellular_DynamicPowerReductionConfig_ModemManager{ModemManager: swDynamicSar}}
   461  	}
   462  
   463  	// bluetoothctl hangs when bluetoothd is not built with asan enabled or
   464  	// crashes. Set state to PRESENT_UNKNOWN on timeout.
   465  	const timeout = 3 * time.Second
   466  	cmdCtx, cancel := context.WithTimeout(ctx, timeout)
   467  	defer cancel()
   468  	if out, err := exec.CommandContext(cmdCtx, "bluetoothctl", "list").Output(); err != nil {
   469  		features.Bluetooth.Present = configpb.HardwareFeatures_PRESENT_UNKNOWN
   470  	} else if len(string(out)) != 0 {
   471  		features.Bluetooth.Present = configpb.HardwareFeatures_PRESENT
   472  	} else {
   473  		logging.Infof(ctx, "bluetooth controller not found")
   474  		features.Bluetooth.Present = configpb.HardwareFeatures_NOT_PRESENT
   475  	}
   476  
   477  	hasEmmcStorage := func() bool {
   478  		matches, err := filepath.Glob("/dev/mmc*")
   479  		if err != nil {
   480  			return false
   481  		}
   482  		if len(matches) > 0 {
   483  			return true
   484  		}
   485  		return false
   486  	}()
   487  	if hasEmmcStorage {
   488  		features.Storage.StorageType = configpb.Component_Storage_EMMC
   489  	}
   490  
   491  	// TODO(b/173741162): Pull storage information from boxster config and add
   492  	// additional storage types.
   493  	hasNvmeStorage := func() bool {
   494  		matches, err := filepath.Glob("/dev/nvme*")
   495  		if err != nil {
   496  			return false
   497  		}
   498  		if len(matches) > 0 {
   499  			return true
   500  		}
   501  		return false
   502  	}()
   503  	if hasNvmeStorage {
   504  		features.Storage.StorageType = configpb.Component_Storage_NVME
   505  	}
   506  
   507  	// TODO(b/211755998): Pull information from boxster config after this got supported in boxster.
   508  	hasNvmeSelfTestStorage := func() bool {
   509  		matches, err := filepath.Glob("/dev/nvme*n1")
   510  		if err != nil {
   511  			return false
   512  		}
   513  		if len(matches) == 0 {
   514  			return false
   515  		}
   516  
   517  		nvmePath := matches[0]
   518  		b, err := exec.Command("nvme", "id-ctrl", "-H", nvmePath).Output()
   519  		if err != nil {
   520  			return false
   521  		}
   522  		return bytes.Contains(b, []byte("Device Self-test Supported"))
   523  	}()
   524  	if hasNvmeStorage && hasNvmeSelfTestStorage {
   525  		config.HasNvmeSelfTest = true
   526  	}
   527  
   528  	hasUfsStorage := func() bool {
   529  		out, err := crosConfig("/hardware-properties", "storage-type")
   530  		if err != nil {
   531  			return false
   532  		}
   533  		return out == "UFS"
   534  	}()
   535  	if hasUfsStorage {
   536  		features.Storage.StorageType = configpb.Component_Storage_UFS
   537  	}
   538  
   539  	func() {
   540  		// This function determines DUT's power supply type and stores it to config.Power.
   541  		// If DUT has a battery, config.Power is DeprecatedDeviceConfig_POWER_SUPPLY_BATTERY.
   542  		// If DUT has AC power supplies only, config.Power is DeprecatedDeviceConfig_POWER_SUPPLY_AC_ONLY.
   543  		// Otherwise, DeprecatedDeviceConfig_POWER_SUPPLY_UNSPECIFIED is populated.
   544  		const sysFsPowerSupplyPath = "/sys/class/power_supply"
   545  		// AC power types come from power_supply driver in Linux kernel (drivers/power/supply/power_supply_sysfs.c)
   546  		acPowerTypes := [...]string{
   547  			"Unknown", "UPS", "Mains", "USB",
   548  			"USB_DCP", "USB_CDP", "USB_ACA", "USB_C",
   549  			"USB_PD", "USB_PD_DRP", "BrickID",
   550  		}
   551  		isACPower := make(map[string]bool)
   552  		for _, s := range acPowerTypes {
   553  			isACPower[s] = true
   554  		}
   555  		config.Power = protocol.DeprecatedDeviceConfig_POWER_SUPPLY_UNSPECIFIED
   556  		files, err := ioutil.ReadDir(sysFsPowerSupplyPath)
   557  		if err != nil {
   558  			logging.Infof(ctx, "Failed to read %v: %v", sysFsPowerSupplyPath, err)
   559  			return
   560  		}
   561  		for _, file := range files {
   562  			devPath := path.Join(sysFsPowerSupplyPath, file.Name())
   563  			supplyTypeBytes, err := ioutil.ReadFile(path.Join(devPath, "type"))
   564  			supplyType := strings.TrimSuffix(string(supplyTypeBytes), "\n")
   565  			if err != nil {
   566  				logging.Infof(ctx, "Failed to read supply type of %v: %v", devPath, err)
   567  				continue
   568  			}
   569  			if strings.HasPrefix(supplyType, "Battery") {
   570  				supplyScopeBytes, err := ioutil.ReadFile(path.Join(devPath, "scope"))
   571  				supplyScope := strings.TrimSuffix(string(supplyScopeBytes), "\n")
   572  				if err != nil && !os.IsNotExist(err) {
   573  					// Ignore NotExist error since /sys/class/power_supply/*/scope may not exist
   574  					logging.Infof(ctx, "Failed to read supply type of %v: %v", devPath, err)
   575  					continue
   576  				}
   577  				if strings.HasPrefix(string(supplyScope), "Device") {
   578  					// Ignore batteries for peripheral devices.
   579  					continue
   580  				}
   581  				config.Power = protocol.DeprecatedDeviceConfig_POWER_SUPPLY_BATTERY
   582  				// Found at least one battery so this device is powered by battery.
   583  				break
   584  			}
   585  			if !isACPower[supplyType] {
   586  				logging.Infof(ctx, "Unknown supply type %v for %v", supplyType, devPath)
   587  				continue
   588  			}
   589  			config.Power = protocol.DeprecatedDeviceConfig_POWER_SUPPLY_AC_ONLY
   590  		}
   591  	}()
   592  
   593  	storageBytes, err := func() (int64, error) {
   594  		b, err := exec.Command("lsblk", "-J", "-b").Output()
   595  		if err != nil {
   596  			return 0, err
   597  		}
   598  		return findDiskSize(b)
   599  	}()
   600  	if err != nil {
   601  		logging.Infof(ctx, "Failed to get disk size: %v", err)
   602  	}
   603  	features.Storage.SizeGb = uint32(storageBytes / 1_000_000_000)
   604  
   605  	memoryBytes, err := func() (int64, error) {
   606  		b, err := ioutil.ReadFile("/proc/meminfo")
   607  		if err != nil {
   608  			return 0, err
   609  		}
   610  		return findMemorySize(b)
   611  	}()
   612  	if err != nil {
   613  		logging.Infof(ctx, "Failed to get memory size: %v", err)
   614  	}
   615  	features.Memory.Profile = &configpb.Component_Memory_Profile{
   616  		SizeMegabytes: int32(memoryBytes / 1_000_000),
   617  	}
   618  
   619  	lidMicrophone, err := matchCrasDeviceType(`(INTERNAL|FRONT)_MIC`)
   620  	if err != nil {
   621  		logging.Infof(ctx, "Failed to get lid microphone: %v", err)
   622  	}
   623  	features.Audio.LidMicrophone = lidMicrophone
   624  	baseMicrophone, err := matchCrasDeviceType(`REAR_MIC`)
   625  	if err != nil {
   626  		logging.Infof(ctx, "Failed to get base microphone: %v", err)
   627  	}
   628  	features.Audio.BaseMicrophone = baseMicrophone
   629  	expectAudio := hasBuiltinAudio(ctx, features.FormFactor.FormFactor)
   630  	if features.Audio.LidMicrophone.GetValue() == 0 && features.Audio.BaseMicrophone.GetValue() == 0 && expectAudio {
   631  		features.Audio.LidMicrophone = &configpb.HardwareFeatures_Count{Value: 1}
   632  	}
   633  	speaker, err := matchCrasDeviceType(`INTERNAL_SPEAKER`)
   634  	if err != nil {
   635  		logging.Infof(ctx, "Failed to get speaker: %v", err)
   636  	}
   637  
   638  	if speaker.GetValue() > 0 || expectAudio {
   639  
   640  		amp, err := findSpeakerAmplifier()
   641  		if err != nil {
   642  			logging.Infof(ctx, "Failed to get amp: %v", err)
   643  		}
   644  		if amp == nil {
   645  			// Do not assume findSpeakerAmplifier() always returns a non-nil amp.
   646  			// Always signal that the device has a hwdep.Speaker().
   647  			amp = &configpb.Component_Amplifier{}
   648  		}
   649  		features.Audio.SpeakerAmplifier = amp
   650  	}
   651  
   652  	hasPrivacyScreen := func() bool {
   653  		// Get list of connectors.
   654  		value, err := exec.Command("modetest", "-c").Output()
   655  		if err != nil {
   656  			logging.Infof(ctx, "Failed to get connectors: %v", err)
   657  			return false
   658  		}
   659  		// Check if privacy-screen prop is present.
   660  		result := strings.Contains(string(value), "privacy-screen:")
   661  
   662  		return result
   663  	}()
   664  	if hasPrivacyScreen {
   665  		features.PrivacyScreen.Present = configpb.HardwareFeatures_PRESENT
   666  	}
   667  
   668  	cpuSMT, err := func() (bool, error) {
   669  		// NB: this sysfs API exists only on kernel >=4.19 (b/195061310). But we don't
   670  		// target SMT-specific tests on earlier kernels.
   671  		b, err := ioutil.ReadFile("/sys/devices/system/cpu/smt/control")
   672  		if err != nil {
   673  			if os.IsNotExist(err) {
   674  				return false, nil
   675  			}
   676  			return false, errors.Wrap(err, "failed to read SMT control file")
   677  		}
   678  		s := strings.TrimSpace(string(b))
   679  		switch s {
   680  		case "on", "off", "forceoff":
   681  			return true, nil
   682  		case "notsupported", "notimplemented":
   683  			return false, nil
   684  		default:
   685  			return false, errors.Errorf("unknown SMT control status: %q", s)
   686  		}
   687  	}()
   688  	if err != nil {
   689  		logging.Infof(ctx, "Failed to determine CPU SMT features: %v", err)
   690  	}
   691  	if cpuSMT {
   692  		features.Soc.Features = append(features.Soc.Features, configpb.Component_Soc_SMT)
   693  	}
   694  
   695  	cpuVulnerabilityPresent := func(vulnerability string) (bool, error) {
   696  		vPath := filepath.Join("/sys/devices/system/cpu/vulnerabilities", vulnerability)
   697  		b, err := ioutil.ReadFile(vPath)
   698  		if err != nil {
   699  			return false, errors.Wrapf(err, "failed to read vulnerability file %q", vPath)
   700  		}
   701  		s := strings.TrimSpace(string(b))
   702  		if s == "Not affected" {
   703  			return false, nil
   704  		}
   705  		return true, nil
   706  	}
   707  
   708  	cpuL1TF, err := cpuVulnerabilityPresent("l1tf")
   709  	if err != nil {
   710  		logging.Infof(ctx, "Failed to determine L1TF vulnerability: %v", err)
   711  	} else if cpuL1TF {
   712  		features.Soc.Vulnerabilities = append(features.Soc.Vulnerabilities, configpb.Component_Soc_L1TF)
   713  	}
   714  
   715  	cpuMDS, err := cpuVulnerabilityPresent("mds")
   716  	if err != nil {
   717  		logging.Infof(ctx, "Failed to determine MDS vulnerability: %v", err)
   718  	} else if cpuMDS {
   719  		features.Soc.Vulnerabilities = append(features.Soc.Vulnerabilities, configpb.Component_Soc_MDS)
   720  	}
   721  
   722  	for _, v := range info.flags {
   723  		if v == "sha_ni" {
   724  			features.Soc.Features = append(features.Soc.Features, configpb.Component_Soc_SHA_NI)
   725  		}
   726  	}
   727  
   728  	func() {
   729  		// Probe for presence of DisplayPort converters
   730  		devices := map[string]string{
   731  			"i2c-10EC2141:00": "RTD2141B",
   732  			"i2c-10EC2142:00": "RTD2142",
   733  			"i2c-1AF80175:00": "PS175",
   734  		}
   735  		for f, name := range devices {
   736  			path := filepath.Join("/sys/bus/i2c/devices", f)
   737  			if _, err := os.Stat(path); err != nil {
   738  				continue
   739  			}
   740  			features.DpConverter.Converters = append(features.DpConverter.Converters, &configpb.Component_DisplayPortConverter{
   741  				Name: name,
   742  			})
   743  		}
   744  	}()
   745  
   746  	hasHps, err := func() (bool, error) {
   747  		out, err := crosConfig("/hps", "has-hps")
   748  		if err != nil {
   749  			return false, err
   750  		}
   751  		return out == "true", nil
   752  	}()
   753  	if err != nil {
   754  		logging.Infof(ctx, "Unknown /hps: %v", err)
   755  	}
   756  	if hasHps {
   757  		features.Hps.Present = configpb.HardwareFeatures_PRESENT
   758  	}
   759  
   760  	camFeatures, err := cameraFeatures(model)
   761  	if err != nil {
   762  		logging.Infof(ctx, "failed to load camera feature profile: %v", err)
   763  	}
   764  	features.Camera.Features = camFeatures
   765  
   766  	if err := parseKConfigs(ctx, features); err != nil {
   767  		logging.Info(ctx, "Failed to parse BIOS kConfig: ", err)
   768  	}
   769  
   770  	rpConfigPresent, err := hasRuntimeProbeConfig(model)
   771  	if err != nil {
   772  		logging.Info(ctx, "Failed to determine if the config of Runtime Probe exists: ", err)
   773  	}
   774  	if rpConfigPresent {
   775  		features.RuntimeProbeConfig.Present = configpb.HardwareFeatures_PRESENT
   776  	} else {
   777  		logging.Infof(ctx, "Config of Runtime Probe not found")
   778  		features.RuntimeProbeConfig.Present = configpb.HardwareFeatures_NOT_PRESENT
   779  	}
   780  
   781  	gpuFamily, gpuVendor, cpuSocFamily, err := func() (gpuFamily, gpuVendor, cpuSocFamily string, fetchErr error) {
   782  		out, err := exec.Command("/usr/local/graphics/hardware_probe", "--gpu-family", "--gpu-vendor", "--cpu-soc-family").Output()
   783  		if err != nil {
   784  			return "", "", "", err
   785  		}
   786  		fetch := func(regexStr, input string) string {
   787  			match := regexp.MustCompile(regexStr).FindStringSubmatch(input)
   788  			if match == nil {
   789  				fetchErr = errors.Wrapf(fetchErr, "regex (%v) not match", regexStr)
   790  				return ""
   791  			}
   792  			return match[1]
   793  		}
   794  		gpuFamily = fetch(`GPU_Family: (\S+)`, string(out))
   795  		gpuVendor = fetch(`GPU_Vendor: (\S+)`, string(out))
   796  		cpuSocFamily = fetch(`CPU_SOC_Family: (\S+)`, string(out))
   797  		return
   798  	}()
   799  	if err != nil {
   800  		logging.Infof(ctx, "failed to parse hardware_probe output: %v", err)
   801  	}
   802  	features.HardwareProbeConfig.GpuFamily = gpuFamily
   803  	features.HardwareProbeConfig.GpuVendor = gpuVendor
   804  	features.HardwareProbeConfig.CpuSocFamily = cpuSocFamily
   805  
   806  	hevcSupport, err := func() (configpb.HardwareFeatures_Present, error) {
   807  		out, err := crosConfig("/ui", "serialized-ash-switches")
   808  		if err != nil {
   809  			return configpb.HardwareFeatures_PRESENT_UNKNOWN, err
   810  		}
   811  		if regexp.MustCompile("disable-features=[^\x00]*PlatformHEVCDecoderSupport").MatchString(out) {
   812  			return configpb.HardwareFeatures_NOT_PRESENT, nil
   813  		}
   814  		if regexp.MustCompile("enable-features=[^\x00]*PlatformHEVCDecoderSupport").MatchString(out) {
   815  			return configpb.HardwareFeatures_PRESENT, nil
   816  		}
   817  		return configpb.HardwareFeatures_PRESENT_UNKNOWN, nil
   818  	}()
   819  	if err != nil {
   820  		logging.Infof(ctx, "Unknown /ui/serialized-ash-switches: %v", err)
   821  	}
   822  	features.Soc.HevcSupport = hevcSupport
   823  
   824  	return &protocol.HardwareFeatures{
   825  		HardwareFeatures:       features,
   826  		DeprecatedDeviceConfig: config,
   827  	}, nil
   828  }
   829  
   830  type lscpuEntry struct {
   831  	Field string `json:"field"` // includes trailing ":"
   832  	Data  string `json:"data"`
   833  }
   834  
   835  type lscpuResult struct {
   836  	Entries []lscpuEntry `json:"lscpu"`
   837  }
   838  
   839  func (r *lscpuResult) find(name string) (data string, ok bool) {
   840  	for _, e := range r.Entries {
   841  		if e.Field == name {
   842  			return e.Data, true
   843  		}
   844  	}
   845  	return "", false
   846  }
   847  
   848  type cpuConfig struct {
   849  	cpuArch protocol.DeprecatedDeviceConfig_Architecture
   850  	soc     protocol.DeprecatedDeviceConfig_SOC
   851  	flags   []string
   852  }
   853  
   854  // cpuInfo returns a structure containing field data from the "lscpu" command
   855  // which outputs CPU architecture information from "sysfs" and "/proc/cpuinfo".
   856  func cpuInfo() (cpuConfig, error) {
   857  	errInfo := cpuConfig{protocol.DeprecatedDeviceConfig_ARCHITECTURE_UNDEFINED, protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, nil}
   858  	b, err := exec.Command("lscpu", "--json").Output()
   859  	if err != nil {
   860  		return errInfo, err
   861  	}
   862  	var parsed lscpuResult
   863  	if err := json.Unmarshal(b, &parsed); err != nil {
   864  		return errInfo, errors.Wrap(err, "failed to parse lscpu result")
   865  	}
   866  	flagsStr, _ := parsed.find("Flags:")
   867  	flags := strings.Split(flagsStr, " ")
   868  	arch, err := findArchitecture(parsed)
   869  	if err != nil {
   870  		return errInfo, errors.Wrap(err, "failed to find CPU architecture")
   871  	}
   872  	soc, err := findSOC(parsed)
   873  	if err != nil {
   874  		return cpuConfig{arch, protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, flags}, errors.Wrap(err, "failed to find SOC")
   875  	}
   876  	return cpuConfig{arch, soc, flags}, nil
   877  }
   878  
   879  // findArchitecture returns an architecture configuration based from parsed output
   880  // data value of the "Architecture" field.
   881  func findArchitecture(parsed lscpuResult) (protocol.DeprecatedDeviceConfig_Architecture, error) {
   882  	arch, ok := parsed.find("Architecture:")
   883  	if !ok {
   884  		return protocol.DeprecatedDeviceConfig_ARCHITECTURE_UNDEFINED, errors.New("failed to find Architecture field")
   885  	}
   886  
   887  	switch arch {
   888  	case "x86_64":
   889  		return protocol.DeprecatedDeviceConfig_X86_64, nil
   890  	case "i686":
   891  		return protocol.DeprecatedDeviceConfig_X86, nil
   892  	case "aarch64":
   893  		return protocol.DeprecatedDeviceConfig_ARM64, nil
   894  	case "armv7l", "armv8l":
   895  		return protocol.DeprecatedDeviceConfig_ARM, nil
   896  	default:
   897  		return protocol.DeprecatedDeviceConfig_ARCHITECTURE_UNDEFINED, errors.Errorf("unknown architecture: %q", arch)
   898  	}
   899  }
   900  
   901  // findSOC returns a SOC configuration based from parsed output data value of the
   902  // "Vendor ID" and other related fields.
   903  func findSOC(parsed lscpuResult) (protocol.DeprecatedDeviceConfig_SOC, error) {
   904  	vendorID, ok := parsed.find("Vendor ID:")
   905  	if !ok {
   906  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Vendor ID field")
   907  	}
   908  
   909  	switch vendorID {
   910  	case "ARM":
   911  		fallthrough
   912  	case "Qualcomm":
   913  		return findARMSOC()
   914  	case "GenuineIntel":
   915  		return findIntelSOC(&parsed)
   916  	case "AuthenticAMD":
   917  		return findAMDSOC(&parsed)
   918  	default:
   919  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown vendor ID: %q", vendorID)
   920  	}
   921  }
   922  
   923  // findARMSOC returns an ARM SOC configuration based on "soc_id" from "/sys/bus/soc/devices".
   924  func findARMSOC() (protocol.DeprecatedDeviceConfig_SOC, error) {
   925  	// Platforms with SMCCC >= 1.2 should implement get_soc functions in firmware
   926  	const socSysFS = "/sys/bus/soc/devices"
   927  	socs, err := ioutil.ReadDir(socSysFS)
   928  	if err == nil {
   929  		for _, soc := range socs {
   930  			c, err := ioutil.ReadFile(path.Join(socSysFS, soc.Name(), "soc_id"))
   931  			if err != nil || !strings.HasPrefix(string(c), "jep106:") {
   932  				continue
   933  			}
   934  			// Trim trailing \x00 and \n
   935  			socID := strings.TrimRight(string(c), "\x00\n")
   936  			switch socID {
   937  			case "jep106:0070:01a9":
   938  				fallthrough
   939  			case "jep106:0070:01ef":
   940  				fallthrough
   941  			case "jep106:0070:7180": // Used by older SC7180 firmware
   942  				return protocol.DeprecatedDeviceConfig_SOC_SC7180, nil
   943  			case "jep106:0070:7280":
   944  				return protocol.DeprecatedDeviceConfig_SOC_SC7280, nil
   945  			case "jep106:0426:8192":
   946  				return protocol.DeprecatedDeviceConfig_SOC_MT8192, nil
   947  			case "jep106:0426:8186":
   948  				return protocol.DeprecatedDeviceConfig_SOC_MT8186, nil
   949  			case "jep106:0426:8195":
   950  				return protocol.DeprecatedDeviceConfig_SOC_MT8195, nil
   951  			case "jep106:0426:8188":
   952  				return protocol.DeprecatedDeviceConfig_SOC_MT8188G, nil
   953  			default:
   954  				return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown ARM model: %s", socID)
   955  			}
   956  		}
   957  	}
   958  
   959  	// For old platforms with SMCCC < 1.2: mt8173, mt8183, rk3288, rk3399,
   960  	// match with their compatible string. Obtain the string after the last , and trim \x00.
   961  	// Example: google,krane-sku176\x00google,krane\x00mediatek,mt8183\x00
   962  	c, err := ioutil.ReadFile("/sys/firmware/devicetree/base/compatible")
   963  	if err != nil {
   964  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Wrap(err, "failed to find ARM model")
   965  	}
   966  
   967  	compatible := string(c)
   968  	model := strings.ToLower(compatible[strings.LastIndex(compatible, ",")+1:])
   969  	model = strings.TrimRight(model, "\x00")
   970  
   971  	switch model {
   972  	case "mt8173":
   973  		return protocol.DeprecatedDeviceConfig_SOC_MT8173, nil
   974  	case "mt8183":
   975  		return protocol.DeprecatedDeviceConfig_SOC_MT8183, nil
   976  	case "rk3288":
   977  		return protocol.DeprecatedDeviceConfig_SOC_RK3288, nil
   978  	case "rk3399":
   979  		return protocol.DeprecatedDeviceConfig_SOC_RK3399, nil
   980  	default:
   981  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown ARM model: %s", model)
   982  	}
   983  }
   984  
   985  // findIntelSOC returns an Intel SOC configuration based on "CPU family", "Model",
   986  // and "Model name" fields.
   987  func findIntelSOC(parsed *lscpuResult) (protocol.DeprecatedDeviceConfig_SOC, error) {
   988  	if family, ok := parsed.find("CPU family:"); !ok {
   989  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel family")
   990  	} else if family != "6" {
   991  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown Intel family: %s", family)
   992  	}
   993  
   994  	modelStr, ok := parsed.find("Model:")
   995  	if !ok {
   996  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel model")
   997  	}
   998  	model, err := strconv.ParseInt(modelStr, 10, 64)
   999  	if err != nil {
  1000  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Wrapf(err, "failed to parse Intel model: %q", modelStr)
  1001  	}
  1002  	switch model {
  1003  	case INTEL_FAM6_KABYLAKE_L:
  1004  		// AMBERLAKE_Y, COMET_LAKE_U, WHISKEY_LAKE_U, KABYLAKE_U, KABYLAKE_U_R, and
  1005  		// KABYLAKE_Y share the same model. Parse model name.
  1006  		// Note that Pentium brand is unsupported.
  1007  		modelName, ok := parsed.find("Model name:")
  1008  		if !ok {
  1009  			return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel model name")
  1010  		}
  1011  		for _, e := range []struct {
  1012  			soc protocol.DeprecatedDeviceConfig_SOC
  1013  			ptn string
  1014  		}{
  1015  			// https://ark.intel.com/content/www/us/en/ark/products/codename/186968/amber-lake-y.html
  1016  			{protocol.DeprecatedDeviceConfig_SOC_AMBERLAKE_Y, `Core.* [mi]\d-(10|8)\d{3}Y`},
  1017  
  1018  			// https://ark.intel.com/content/www/us/en/ark/products/codename/90354/comet-lake.html
  1019  			{protocol.DeprecatedDeviceConfig_SOC_COMET_LAKE_U, `Core.* i\d-10\d{3}U|Celeron.* 5[23]05U|Intel\(R\) Pentium\(R\) CPU 6405U`},
  1020  
  1021  			// https://ark.intel.com/content/www/us/en/ark/products/codename/135883/whiskey-lake.html
  1022  			{protocol.DeprecatedDeviceConfig_SOC_WHISKEY_LAKE_U, `Core.* i\d-8\d{2}5U|Celeron.* 4[23]05U`},
  1023  
  1024  			// https://ark.intel.com/content/www/us/en/ark/products/codename/82879/kaby-lake.html
  1025  			{protocol.DeprecatedDeviceConfig_SOC_KABYLAKE_U, `Core.* i\d-7\d{3}U|Celeron.* 3[89]65U`},
  1026  			{protocol.DeprecatedDeviceConfig_SOC_KABYLAKE_Y, `Core.* [mi]\d-7Y\d{2}|Celeron.* 3965Y`},
  1027  
  1028  			// https://ark.intel.com/content/www/us/en/ark/products/codename/126287/kaby-lake-r.html
  1029  			{protocol.DeprecatedDeviceConfig_SOC_KABYLAKE_U_R, `Core.* i\d-8\d{2}0U|Celeron.* 3867U`},
  1030  		} {
  1031  			r := regexp.MustCompile(e.ptn)
  1032  			if r.MatchString(modelName) {
  1033  				return e.soc, nil
  1034  			}
  1035  		}
  1036  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown model name: %s", modelName)
  1037  	case INTEL_FAM6_ICELAKE_L:
  1038  		return protocol.DeprecatedDeviceConfig_SOC_ICE_LAKE_Y, nil
  1039  	case INTEL_FAM6_ATOM_GOLDMONT_PLUS:
  1040  		return protocol.DeprecatedDeviceConfig_SOC_GEMINI_LAKE, nil
  1041  	case INTEL_FAM6_ATOM_TREMONT_L:
  1042  		return protocol.DeprecatedDeviceConfig_SOC_JASPER_LAKE, nil
  1043  	case INTEL_FAM6_TIGERLAKE_L:
  1044  		return protocol.DeprecatedDeviceConfig_SOC_TIGER_LAKE, nil
  1045  	case INTEL_FAM6_ALDERLAKE_L:
  1046  		return protocol.DeprecatedDeviceConfig_SOC_ALDER_LAKE, nil
  1047  	case INTEL_FAM6_METEORLAKE_L:
  1048  		return protocol.DeprecatedDeviceConfig_SOC_METEOR_LAKE, nil
  1049  	case INTEL_FAM6_CANNONLAKE_L:
  1050  		return protocol.DeprecatedDeviceConfig_SOC_CANNON_LAKE_Y, nil
  1051  	case INTEL_FAM6_ATOM_GOLDMONT:
  1052  		return protocol.DeprecatedDeviceConfig_SOC_APOLLO_LAKE, nil
  1053  	case INTEL_FAM6_SKYLAKE_L:
  1054  		// SKYLAKE_U and SKYLAKE_Y share the same model. Parse model name.
  1055  		modelName, ok := parsed.find("Model name:")
  1056  		if !ok {
  1057  			return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find Intel model name")
  1058  		}
  1059  		for _, e := range []struct {
  1060  			soc protocol.DeprecatedDeviceConfig_SOC
  1061  			ptn string
  1062  		}{
  1063  			// https://ark.intel.com/content/www/us/en/ark/products/codename/37572/skylake.html
  1064  			{protocol.DeprecatedDeviceConfig_SOC_SKYLAKE_U, `Core.* i\d-6\d{3}U|Celeron.*3[89]55U`},
  1065  			{protocol.DeprecatedDeviceConfig_SOC_SKYLAKE_Y, `Core.* m\d-6Y\d{2}`},
  1066  		} {
  1067  			r := regexp.MustCompile(e.ptn)
  1068  			if r.MatchString(modelName) {
  1069  				return e.soc, nil
  1070  			}
  1071  		}
  1072  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown model name: %s", modelName)
  1073  	case INTEL_FAM6_ATOM_AIRMONT:
  1074  		return protocol.DeprecatedDeviceConfig_SOC_BRASWELL, nil
  1075  	case INTEL_FAM6_BROADWELL:
  1076  		return protocol.DeprecatedDeviceConfig_SOC_BROADWELL, nil
  1077  	case INTEL_FAM6_HASWELL, INTEL_FAM6_HASWELL_L:
  1078  		return protocol.DeprecatedDeviceConfig_SOC_HASWELL, nil
  1079  	case INTEL_FAM6_IVYBRIDGE:
  1080  		return protocol.DeprecatedDeviceConfig_SOC_IVY_BRIDGE, nil
  1081  	case INTEL_FAM6_ATOM_SILVERMONT:
  1082  		return protocol.DeprecatedDeviceConfig_SOC_BAY_TRAIL, nil
  1083  	case INTEL_FAM6_SANDYBRIDGE:
  1084  		return protocol.DeprecatedDeviceConfig_SOC_SANDY_BRIDGE, nil
  1085  	case INTEL_FAM6_ATOM_BONNELL:
  1086  		return protocol.DeprecatedDeviceConfig_SOC_PINE_TRAIL, nil
  1087  	default:
  1088  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown Intel model: %d", model)
  1089  	}
  1090  }
  1091  
  1092  // findAMDSOC returns an AMD SOC configuration based on "Model" field.
  1093  func findAMDSOC(parsed *lscpuResult) (protocol.DeprecatedDeviceConfig_SOC, error) {
  1094  	model, ok := parsed.find("Model:")
  1095  	if !ok {
  1096  		return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.New("failed to find AMD model")
  1097  	}
  1098  	if model == "112" {
  1099  		return protocol.DeprecatedDeviceConfig_SOC_STONEY_RIDGE, nil
  1100  	}
  1101  	if family, ok := parsed.find("CPU family:"); ok {
  1102  		if family == "23" {
  1103  			if model == "24" || model == "32" {
  1104  				return protocol.DeprecatedDeviceConfig_SOC_PICASSO, nil
  1105  			} else if model == "160" {
  1106  				return protocol.DeprecatedDeviceConfig_SOC_MENDOCINO, nil
  1107  			}
  1108  		} else if family == "25" {
  1109  			if model == "80" {
  1110  				return protocol.DeprecatedDeviceConfig_SOC_CEZANNE, nil
  1111  			} else if model == "116" {
  1112  				return protocol.DeprecatedDeviceConfig_SOC_PHOENIX, nil
  1113  			}
  1114  		}
  1115  	}
  1116  
  1117  	return protocol.DeprecatedDeviceConfig_SOC_UNSPECIFIED, errors.Errorf("unknown AMD model: %s", model)
  1118  }
  1119  
  1120  // lsblk command output differs depending on the version. Attempt parsing in multiple ways to accept all the cases.
  1121  
  1122  // lsblk from util-linux 2.32
  1123  type blockDevices2_32 struct {
  1124  	Name      string `json:"name"`
  1125  	Removable string `json:"rm"`
  1126  	Size      string `json:"size"`
  1127  	Type      string `json:"type"`
  1128  }
  1129  
  1130  type lsblkRoot2_32 struct {
  1131  	BlockDevices []blockDevices2_32 `json:"blockdevices"`
  1132  }
  1133  
  1134  // lsblk from util-linux 2.36.1
  1135  type blockDevices struct {
  1136  	Name      string `json:"name"`
  1137  	Removable bool   `json:"rm"`
  1138  	Size      int64  `json:"size"`
  1139  	Type      string `json:"type"`
  1140  }
  1141  
  1142  type lsblkRoot struct {
  1143  	BlockDevices []blockDevices `json:"blockdevices"`
  1144  }
  1145  
  1146  func parseLsblk2_32(jsonData []byte) (*lsblkRoot, error) {
  1147  	var old lsblkRoot2_32
  1148  	err := json.Unmarshal(jsonData, &old)
  1149  	if err != nil {
  1150  		return nil, err
  1151  	}
  1152  
  1153  	var r lsblkRoot
  1154  	for _, e := range old.BlockDevices {
  1155  		s, err := strconv.ParseInt(e.Size, 10, 64)
  1156  		if err != nil {
  1157  			return nil, err
  1158  		}
  1159  		var rm bool
  1160  		if e.Removable == "0" || e.Removable == "" {
  1161  			rm = false
  1162  		} else if e.Removable == "1" {
  1163  			rm = true
  1164  		} else {
  1165  			return nil, fmt.Errorf("unknown value for rm: %q", e.Removable)
  1166  		}
  1167  		r.BlockDevices = append(r.BlockDevices, blockDevices{
  1168  			Name:      e.Name,
  1169  			Removable: rm,
  1170  			Size:      s,
  1171  			Type:      e.Type,
  1172  		})
  1173  	}
  1174  	return &r, nil
  1175  }
  1176  
  1177  func parseLsblk2_36(jsonData []byte) (*lsblkRoot, error) {
  1178  	var r lsblkRoot
  1179  	err := json.Unmarshal(jsonData, &r)
  1180  	if err != nil {
  1181  		return nil, err
  1182  	}
  1183  	return &r, nil
  1184  }
  1185  
  1186  func parseLsblk(jsonData []byte) (*lsblkRoot, error) {
  1187  	var errs []error
  1188  	parsers := []func([]byte) (*lsblkRoot, error){parseLsblk2_36, parseLsblk2_32}
  1189  	for _, p := range parsers {
  1190  		r, err := p(jsonData)
  1191  		if err == nil {
  1192  			return r, nil
  1193  		}
  1194  		errs = append(errs, err)
  1195  	}
  1196  	var errStrings []string
  1197  	for _, e := range errs {
  1198  		errStrings = append(errStrings, e.Error())
  1199  	}
  1200  	return nil, fmt.Errorf("failed to parse JSON in all the expected formats: %s", strings.Join(errStrings, "; "))
  1201  }
  1202  
  1203  // findDiskSize detects the size of the storage device from "lsblk -J -b" output in bytes.
  1204  // When there are multiple disks, returns the size of the largest one.
  1205  func findDiskSize(jsonData []byte) (int64, error) {
  1206  	r, err := parseLsblk(jsonData)
  1207  	if err != nil {
  1208  		return 0, err
  1209  	}
  1210  	var maxSize int64
  1211  	var found bool
  1212  	for _, x := range r.BlockDevices {
  1213  		if x.Type == "disk" && !x.Removable && !strings.HasPrefix(x.Name, "zram") {
  1214  			found = true
  1215  			if x.Size > maxSize {
  1216  				maxSize = x.Size
  1217  			}
  1218  		}
  1219  	}
  1220  	if !found {
  1221  		return 0, errors.New("no disk device found")
  1222  	}
  1223  	return maxSize, nil
  1224  }
  1225  
  1226  // findMemorySize parses a content of /proc/meminfo and returns the total memory size in bytes.
  1227  func findMemorySize(meminfo []byte) (int64, error) {
  1228  	r := bytes.NewReader(meminfo)
  1229  	sc := bufio.NewScanner(r)
  1230  	for sc.Scan() {
  1231  		line := sc.Text()
  1232  		tokens := strings.SplitN(line, ":", 2)
  1233  		if len(tokens) != 2 || strings.TrimSpace(tokens[0]) != "MemTotal" {
  1234  			continue
  1235  		}
  1236  		cap := strings.SplitN(strings.TrimSpace(tokens[1]), " ", 2)
  1237  		if len(cap) != 2 {
  1238  			return 0, fmt.Errorf("unexpected line format: input=%s", line)
  1239  		}
  1240  		if cap[1] != "kB" {
  1241  			return 0, fmt.Errorf("unexpected unit: got %s, want kB; input=%s", cap[1], line)
  1242  		}
  1243  		val, err := strconv.ParseInt(cap[0], 10, 64)
  1244  		if err != nil {
  1245  			return 0, err
  1246  		}
  1247  		return val * 1_000, nil
  1248  	}
  1249  	return 0, fmt.Errorf("MemTotal not found; input=%q", string(meminfo))
  1250  }
  1251  
  1252  func matchCrasDeviceType(pattern string) (*configpb.HardwareFeatures_Count, error) {
  1253  	b, err := exec.Command("cras_test_client").Output()
  1254  	if err != nil {
  1255  		return nil, err
  1256  	}
  1257  	if regexp.MustCompile(pattern).Match(b) {
  1258  		return &configpb.HardwareFeatures_Count{Value: 1}, nil
  1259  	}
  1260  	return &configpb.HardwareFeatures_Count{Value: 0}, nil
  1261  }
  1262  
  1263  // findSpeakerAmplifier parses a content of in "/sys/kernel/debug/asoc/components"
  1264  // and returns the speaker amplifier used.
  1265  func findSpeakerAmplifier() (*configpb.Component_Amplifier, error) {
  1266  
  1267  	// This sys path exists only on kernel >=4.14. But we don't
  1268  	// target amp tests on earlier kernels.
  1269  	f, err := os.Open("/sys/kernel/debug/asoc/components")
  1270  	if err != nil {
  1271  		return &configpb.Component_Amplifier{}, err
  1272  	}
  1273  	defer f.Close()
  1274  	scanner := bufio.NewScanner(f)
  1275  	for scanner.Scan() {
  1276  		if amp, found := matchSpeakerAmplifier(scanner.Text()); found {
  1277  			if enabled, err := bootTimeCalibration(); err == nil && enabled {
  1278  				amp.Features = append(amp.Features, configpb.Component_Amplifier_BOOT_TIME_CALIBRATION)
  1279  			}
  1280  			return amp, err
  1281  		}
  1282  	}
  1283  	return &configpb.Component_Amplifier{}, nil
  1284  }
  1285  
  1286  var ampsRegexp = map[string]*regexp.Regexp{
  1287  	configpb.HardwareFeatures_Audio_MAX98357.String(): regexp.MustCompile(`^(i2c-)?ma?x98357a?((:\d*)|([_-]?\d*))?$`),
  1288  	configpb.HardwareFeatures_Audio_MAX98373.String(): regexp.MustCompile(`^(i2c-)?ma?x98373((:\d*)|([_-]?\d*))?$`),
  1289  	configpb.HardwareFeatures_Audio_MAX98360.String(): regexp.MustCompile(`^(i2c-)?ma?x98360a?((:\d*)|([_-]?\d*))?$`),
  1290  	configpb.HardwareFeatures_Audio_RT1015.String():   regexp.MustCompile(`^(i2c-)?((rtl?)|(10ec))?1015(\.\d*)?((:\d*)|([_-]?\d*))?$`),
  1291  	configpb.HardwareFeatures_Audio_RT1015P.String():  regexp.MustCompile(`^(i2c-)?(rtl?)?(10ec)?1015p(\.\d*)?((:\d*)|([_-]?\d*))?$`),
  1292  	configpb.HardwareFeatures_Audio_ALC1011.String():  regexp.MustCompile(`^(i2c-)?((rtl?)|(10ec))?1011(\.\d*)?((:\d*)|([_-]?\d*))?$`),
  1293  	configpb.HardwareFeatures_Audio_MAX98390.String(): regexp.MustCompile(`^(i2c-)?ma?x98390((:\d*)|(\.\d-\d+)|([_-]?\d*))?$`),
  1294  	configpb.HardwareFeatures_Audio_CS35L41.String():  regexp.MustCompile(`^(i2c-)?csc3541((:\d*)|([_-]?\d*))?$`),
  1295  }
  1296  
  1297  func matchSpeakerAmplifier(line string) (*configpb.Component_Amplifier, bool) {
  1298  	for amp, re := range ampsRegexp {
  1299  		if re.MatchString(strings.ToLower(line)) {
  1300  			return &configpb.Component_Amplifier{Name: amp}, true
  1301  		}
  1302  	}
  1303  	return nil, false
  1304  }
  1305  
  1306  // bootTimeCalibration returns whether the boot time calibration is
  1307  // enabled by parsing the sound_card_init config.
  1308  func bootTimeCalibration() (bool, error) {
  1309  	config, err := crosConfig("/audio/main", "sound-card-init-conf")
  1310  	if err != nil {
  1311  		return false, err
  1312  	}
  1313  	path := "/etc/sound_card_init/" + config
  1314  	if _, err := os.Stat(path); err != nil {
  1315  		// Regard config non-existence as boot_time_calibration disabled.
  1316  		if os.IsNotExist(err) {
  1317  			return false, nil
  1318  		}
  1319  		return false, err
  1320  	}
  1321  	b, err := ioutil.ReadFile(path)
  1322  	if err != nil {
  1323  		return false, errors.New("failed to read sound_card_init config")
  1324  	}
  1325  	return isBootTimeCalibrationEnabled(string(b))
  1326  }
  1327  
  1328  func isBootTimeCalibrationEnabled(s string) (bool, error) {
  1329  	re := regexp.MustCompile(`boot_time_calibration_enabled\s*?:\s*?(true|false)`)
  1330  	match := re.FindStringSubmatch(s)
  1331  	if match == nil {
  1332  		return false, errors.New("invalid sound_card_init config")
  1333  	}
  1334  	enabled := match[1]
  1335  	return enabled == "true", nil
  1336  }
  1337  
  1338  func wifiFeatures() (*configpb.HardwareFeatures_Wifi, error) {
  1339  	dev, err := wlan.DeviceInfo()
  1340  	if err != nil {
  1341  		return nil, errors.Wrap(err, "failed to get device")
  1342  	}
  1343  
  1344  	_, err = exec.Command("vpd", "-g", "wifi_sar").Output()
  1345  	vpdSarFound := err == nil
  1346  
  1347  	return &configpb.HardwareFeatures_Wifi{
  1348  		WifiChips: []configpb.HardwareFeatures_Wifi_WifiChip{
  1349  			configpb.HardwareFeatures_Wifi_WifiChip(dev.ID)},
  1350  		WifiVpdSar: vpdSarFound,
  1351  	}, nil
  1352  }
  1353  
  1354  // hasBuiltinAudio tells if a given form factor has built-in audio devices
  1355  func hasBuiltinAudio(ctx context.Context, ff configpb.HardwareFeatures_FormFactor_FormFactorType) bool {
  1356  	switch ff {
  1357  	case configpb.HardwareFeatures_FormFactor_CLAMSHELL,
  1358  		configpb.HardwareFeatures_FormFactor_CONVERTIBLE,
  1359  		configpb.HardwareFeatures_FormFactor_DETACHABLE,
  1360  		configpb.HardwareFeatures_FormFactor_CHROMEBASE,
  1361  		configpb.HardwareFeatures_FormFactor_CHROMESLATE:
  1362  		return true
  1363  	case configpb.HardwareFeatures_FormFactor_CHROMEBIT,
  1364  		configpb.HardwareFeatures_FormFactor_CHROMEBOX:
  1365  		return false
  1366  	default:
  1367  		logging.Infof(ctx, "Unknown form factor: %s", ff)
  1368  		return false
  1369  	}
  1370  }
  1371  
  1372  // cameraFeatures returns the list of configured camera features for the given
  1373  // |model| by inspecting the on-device feature config file.
  1374  func cameraFeatures(model string) ([]string, error) {
  1375  	type modelConfig map[string]struct {
  1376  		FeatureSet []map[string]interface{} `json:"feature_set"`
  1377  	}
  1378  	const featureProfilePath = "/etc/camera/feature_profile.json"
  1379  	jsonInput, err := ioutil.ReadFile(featureProfilePath)
  1380  	if err != nil {
  1381  		return nil, errors.Wrap(err, "cannot load feature profile config")
  1382  	}
  1383  	conf := make(modelConfig)
  1384  	if err := json.Unmarshal(jsonInput, &conf); err != nil {
  1385  		return nil, errors.Wrap(err, "cannot unmarshal feature profile config")
  1386  	}
  1387  	c, ok := conf[model]
  1388  	if !ok {
  1389  		return nil, errors.Errorf("feature set config for model %s doesn't exist", model)
  1390  	}
  1391  	featureSet := make(map[string]bool)
  1392  	for _, f := range c.FeatureSet {
  1393  		var v interface{}
  1394  		// The "type" attribute is always a string.
  1395  		if v, ok = f["type"]; !ok {
  1396  			continue
  1397  		}
  1398  		// There can be multiple entries for a feature with different
  1399  		// constraints.
  1400  		if _, ok := featureSet[v.(string)]; !ok {
  1401  			featureSet[v.(string)] = true
  1402  		}
  1403  	}
  1404  	var ret []string
  1405  	for k := range featureSet {
  1406  		ret = append(ret, k)
  1407  	}
  1408  	return ret, nil
  1409  }
  1410  
  1411  // findGSCKeyID parses a content of "gsctool -a -f -M" and return a required key
  1412  func findGSCKeyID(str, keyIDType string) (string, error) {
  1413  	re := regexp.MustCompile(`(?m)^keyids: RO (0x.+), RW (0x.+)$`)
  1414  
  1415  	switch keyIDType {
  1416  	case "RO":
  1417  		keyID := re.FindAllStringSubmatch(str, -1)[0][1]
  1418  		return keyID, nil
  1419  	case "RW":
  1420  		keyID := re.FindAllStringSubmatch(str, -1)[0][2]
  1421  		return keyID, nil
  1422  	default:
  1423  		return "", errors.Errorf("Unknown keyId type %s", keyIDType)
  1424  	}
  1425  }
  1426  
  1427  // containsGSCKeyID returns true if reqKeyID is in the keyIDs
  1428  func containsGSCKeyID(keyIDs []GSCKeyID, reqKeyID GSCKeyID) bool {
  1429  	for _, keyID := range keyIDs {
  1430  		if keyID == reqKeyID {
  1431  			return true
  1432  		}
  1433  	}
  1434  	return false
  1435  }
  1436  
  1437  // For mocking.
  1438  var flashromExtractCoreBootCmd = func(ctx context.Context, corebootBinName string) error {
  1439  	return exec.CommandContext(ctx, "flashrom", "-p", "host", "-r", "-i", fmt.Sprintf("FW_MAIN_A:%s", corebootBinName)).Run()
  1440  }
  1441  var cbfsToolExtractConfigCmd = func(ctx context.Context, corebootBinName, fwConfigName string) error {
  1442  	return exec.CommandContext(ctx, "cbfstool", corebootBinName, "extract", "-n", "config", "-f", fwConfigName).Run()
  1443  }
  1444  
  1445  var configLineRegexp = regexp.MustCompile(`^(# )?(CONFIG\S*)(=(y)| (is not set))`)
  1446  
  1447  // parseKConfigs updates the provided HardwareFeatures with the features found
  1448  // by reading reading through the BIOS Kconfigs.
  1449  func parseKConfigs(ctx context.Context, features *configpb.HardwareFeatures) error {
  1450  	corebootBin, err := ioutil.TempFile("/var/tmp", "")
  1451  	if err != nil {
  1452  		return errors.Wrap(err, "failed to create temp file")
  1453  	}
  1454  	corebootBin.Close()
  1455  	defer os.Remove(corebootBin.Name())
  1456  
  1457  	fwConfig, err := ioutil.TempFile("/var/tmp", "")
  1458  	if err != nil {
  1459  		return errors.Wrap(err, "failed to create temp file")
  1460  	}
  1461  	fwConfig.Close()
  1462  	defer os.Remove(fwConfig.Name())
  1463  
  1464  	if err := flashromExtractCoreBootCmd(ctx, corebootBin.Name()); err != nil {
  1465  		return errors.Wrap(err, "failed to extract FW_MAIN_A bios section")
  1466  	}
  1467  	if err := cbfsToolExtractConfigCmd(ctx, corebootBin.Name(), fwConfig.Name()); err != nil {
  1468  		return errors.Wrap(err, "failed to extract bios Kconfig file")
  1469  	}
  1470  	inFile, err := os.Open(fwConfig.Name())
  1471  	if err != nil {
  1472  		return errors.Wrap(err, "failed to read bios Kconfig file")
  1473  	}
  1474  	defer inFile.Close()
  1475  
  1476  	importantConfigs := map[string]*configpb.HardwareFeatures_Present{
  1477  		"CONFIG_MAINBOARD_HAS_EARLY_LIBGFXINIT": &features.FwConfig.MainboardHasEarlyLibgfxinit,
  1478  		"CONFIG_VBOOT_CBFS_INTEGRATION":         &features.FwConfig.VbootCbfsIntegration,
  1479  	}
  1480  
  1481  	scanner := bufio.NewScanner(inFile)
  1482  	for scanner.Scan() {
  1483  		line := scanner.Text()
  1484  		if match := configLineRegexp.FindStringSubmatch(line); match != nil {
  1485  			if val, ok := importantConfigs[match[2]]; ok {
  1486  				if match[4] == "y" {
  1487  					*val = configpb.HardwareFeatures_PRESENT
  1488  				} else if match[5] == "is not set" {
  1489  					*val = configpb.HardwareFeatures_NOT_PRESENT
  1490  				}
  1491  			}
  1492  		}
  1493  	}
  1494  	return nil
  1495  }
  1496  
  1497  // hasRuntimeProbeConfig returns true if the corresponding probe config for the
  1498  // model of the DUT exists.  The err is set if the error os.Stat returns is not
  1499  // fs.ErrNotExist.
  1500  func hasRuntimeProbeConfig(model string) (bool, error) {
  1501  	probeConfigRelPath := "etc/runtime_probe/" + model + "/probe_config.json"
  1502  	configRoots := []string{
  1503  		"/usr/local/",
  1504  		"/",
  1505  	}
  1506  	for _, configRoot := range configRoots {
  1507  		_, err := os.Stat(configRoot + probeConfigRelPath)
  1508  		if err == nil {
  1509  			return true, nil
  1510  		}
  1511  		if !os.IsNotExist(err) {
  1512  			return false, err
  1513  		}
  1514  	}
  1515  	return false, nil
  1516  }