| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package starfish contains utilities for interacting with starfish devices. |
| package starfish |
| |
| import ( |
| "context" |
| "fmt" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| |
| "go.chromium.org/luci/common/errors" |
| |
| "go.chromium.org/infra/cros/recovery/internal/components" |
| "go.chromium.org/infra/cros/recovery/internal/components/cros" |
| "go.chromium.org/infra/cros/recovery/internal/components/cros/cellular" |
| "go.chromium.org/infra/cros/recovery/internal/log" |
| "go.chromium.org/infra/cros/recovery/internal/retry" |
| "go.chromium.org/infra/cros/recovery/tlw" |
| ) |
| |
| // getDevice gets the starfish device path. |
| func getDevice(ctx context.Context, runner components.Runner) (string, error) { |
| // Get the symlink to the starfish device by finding a device with Starfish in its ID. |
| const idCmd = "find /dev/serial/by-id/ | grep Starfish" |
| idDevPath, err := runner(ctx, 5*time.Second, idCmd) |
| if err != nil { |
| return "", errors.Annotate(err, "get device: failed to find device with starfish in id") |
| } |
| idDevPath = strings.TrimSpace(idDevPath) |
| |
| if len(strings.Split(idDevPath, "\n")) != 1 { |
| return "", errors.Reason("get device: more than one starfish found") |
| } |
| |
| // Get the underlying dev path. |
| cmd := fmt.Sprintf("readlink -f %q", idDevPath) |
| devPath, err := runner(ctx, 5*time.Second, cmd) |
| if err != nil { |
| return "", errors.Annotate(err, "get device: failed to find device with starfish in id") |
| } |
| return strings.TrimSpace(devPath), nil |
| } |
| |
| // EjectSIM ejects any connected SIM. |
| func EjectSIM(ctx context.Context, runner components.Runner) error { |
| device, err := getDevice(ctx, runner) |
| if err != nil { |
| return errors.Annotate(err, "eject sim: failed to find starfish device") |
| } |
| |
| if _, err := sendCmd(ctx, runner, "sim eject", device); err != nil { |
| return errors.Annotate(err, "eject sim: failed to send eject command") |
| } |
| return nil |
| } |
| |
| // InsertSIM inserts the SIM in the given slot. |
| func InsertSIM(ctx context.Context, runner components.Runner, slot int, modem cellular.CellularClient) error { |
| device, err := getDevice(ctx, runner) |
| if err != nil { |
| return errors.Annotate(err, "insert sim: failed to find starfish device") |
| } |
| cmd := fmt.Sprintf("sim connect -n %d", slot) |
| if out, err := sendCmd(ctx, runner, cmd, device); err != nil { |
| return errors.Annotate(err, "insert sim: failed to switch SIM slot") |
| } else if !strings.Contains(out, fmt.Sprintf("Mux set to %d", slot)) { |
| return errors.Reason("insert sim: failed to verify that device was set to correct slot") |
| } |
| |
| // Normally, when you insert a SIM the device is able to detect the SIM being |
| // physically inserted into the slot. Since this is an electronic switch the |
| // device may not notice the new SIM. Power cycle it to ensure we have the correct SIM. |
| if err := cros.Reboot(ctx, runner, 5*time.Second); err != nil { |
| // cros reboot will always time out. |
| log.Infof(ctx, "Error received during reboot: %v", err) |
| } |
| if err := cros.WaitUntilSSHable(ctx, 120*time.Second, 5*time.Second, runner); err != nil { |
| return errors.Annotate(err, "insert sim: failed to connect to device after rebooting") |
| } |
| |
| // Make sure we're on a PSIM slot first. |
| predicate := func(sim *tlw.Cellular_SIMInfo) bool { |
| return sim.GetType() == tlw.Cellular_SIM_PHYSICAL |
| } |
| |
| // Wait for the SIM to be detected. |
| return retry.WithTimeout(ctx, time.Second, 60*time.Second, func() error { |
| if err := cellular.SwitchToMatchingSIMSlot(ctx, runner, predicate, modem); err != nil { |
| return errors.Annotate(err, "failed to switch to psim slot") |
| } |
| simInfo, err := modem.GetSIMInfo(ctx, runner) |
| if err != nil { |
| return errors.Annotate(err, "failed to query info for sim slot: %d", slot) |
| } |
| // If the SIM slot is empty, it may just not be detected yet. |
| if simInfo == nil || len(simInfo.GetProfileInfos()) == 0 { |
| return errors.Reason("sim info is empty") |
| } |
| return nil |
| }, "insert sim") |
| } |
| |
| // GetAllSIMInfo switches to all occupied starfish slots and queries the SIM info. |
| func GetAllSIMInfo(ctx context.Context, runner components.Runner, modem cellular.CellularClient) ([]*tlw.Cellular_SIMInfo, error) { |
| slots, err := GetOccupiedSlots(ctx, runner) |
| if err != nil { |
| return nil, errors.Annotate(err, "get all sim info: failed to determine occupied SIM slots") |
| } |
| |
| // Try to eject the SIM as cleanup. |
| defer func() { |
| if err := EjectSIM(ctx, runner); err != nil { |
| log.Errorf(ctx, "Failed to eject SIM from starfish: %v", err) |
| } |
| }() |
| |
| infos := make([]*tlw.Cellular_SIMInfo, 0) |
| for _, slot := range slots { |
| if err := InsertSIM(ctx, runner, slot, modem); err != nil { |
| return nil, errors.Annotate(err, "get all sim info: failed to insert sim into starfish") |
| } |
| |
| simInfo, err := modem.GetSIMInfo(ctx, runner) |
| if err != nil { |
| return nil, errors.Annotate(err, "get all sim info: failed to query info for sim slot: %d", slot) |
| } |
| |
| // Starfish 0 indexes while ModemManager 1 indexes SIM slots. |
| simInfo.SlotId = int32(slot) + 1 |
| infos = append(infos, simInfo) |
| } |
| return infos, nil |
| } |
| |
| // GetOccupiedSlots returns a list of the occupied SIM slots on the starfish. |
| func GetOccupiedSlots(ctx context.Context, runner components.Runner) ([]int, error) { |
| device, err := getDevice(ctx, runner) |
| if err != nil { |
| return nil, errors.Annotate(err, "get occupied slots: failed to find starfish device") |
| } |
| |
| /* |
| Get the status of the SIMs in the starfish. |
| |
| example output: |
| Starfish:~$ sim status |
| [0000000027318797] <inf> console: SIM 0 = Found |
| [0000000027318797] <inf> console: SIM 1 = Found |
| [0000000027318797] <inf> console: SIM 2 = Found |
| [0000000027318797] <inf> console: SIM 3 = Found |
| [0000000027318797] <inf> console: SIM 4 = None |
| [0000000027318798] <inf> console: SIM 5 = None |
| [0000000027318798] <inf> console: SIM 6 = None |
| [0000000027318798] <inf> console: SIM 7 = None |
| Starfish:~$ |
| */ |
| out, err := sendCmd(ctx, runner, "sim status", device) |
| if err != nil { |
| return nil, errors.Annotate(err, "get occupied slots: failed to find starfish device") |
| } |
| |
| slots, err := parseActiveSIMSlots(out) |
| if err != nil { |
| return nil, errors.Annotate(err, "get occupied slots: failed to parse 'sim status' output: %s", out) |
| } |
| log.Infof(ctx, "Found occupied slots: %v", slots) |
| return slots, nil |
| } |
| |
| // simStatusRegex returns a match on an occupied SIM slot in the starfish, the match |
| // is the integer slot of the occupied slot e.g. |
| // |
| // [0000000027318797] <inf> console: SIM 0 = Found |
| // |
| // would return a match with value "0" |
| var simStatusRegex = regexp.MustCompile(`SIM\s?(\d+)\s?=\s?Found`) |
| |
| // parseActiveSIMSlots parses the output of the starfish "sim status" command. |
| func parseActiveSIMSlots(out string) ([]int, error) { |
| var res []int |
| sims := make(map[int]bool) |
| for _, line := range strings.Split(out, "\n") { |
| m := simStatusRegex.FindStringSubmatch(line) |
| if m == nil || len(m) != 2 { |
| continue |
| } |
| |
| i, err := strconv.Atoi(m[1]) |
| if err != nil { |
| return nil, errors.Reason("parse occupied sim slots: failed to parse slot id: %v", m) |
| } |
| if sims[i] { |
| return nil, errors.Reason("parse occupied sim slots: duplicate SIM slot: %d", i) |
| } |
| |
| sims[i] = true |
| res = append(res, i) |
| } |
| |
| return res, nil |
| } |
| |
| // sendCmd sends a command to the starfish device and returns the output. |
| // |
| // The starfish won't return the output of the command directly. Instead we read the |
| // starfish output for several seconds after sending the command. Since the starfish |
| // is a device and not a regular file there is no EOF in it's stdout for us to terminate |
| // on either so we force terminate the read using the "timeout" command. |
| func sendCmd(ctx context.Context, runner components.Runner, cmd, device string) (string, error) { |
| // Configure tty for the device. |
| configCmd := fmt.Sprintf("stty -F %q 115200 raw -echo", device) |
| if _, err := runner(ctx, 5*time.Second, configCmd); err != nil { |
| return "", errors.Annotate(err, "send cmd: failed to configure starfish tty") |
| } |
| |
| // Send command to the device. |
| sendCmd := fmt.Sprintf("echo -ne %q > %q", "\r"+cmd+"\r", device) |
| if _, err := runner(ctx, 15*time.Second, sendCmd); err != nil { |
| return "", errors.Annotate(err, "send cmd: failed to run starfish command") |
| } |
| |
| // Read the current stdout buffer and all output for the next several seconds. |
| // There is no EOF returned by the starfish device so we have no concrete way to know |
| // when it is done outputting data, instead we will force terminate the command using "timeout." |
| readCmd := fmt.Sprintf("timeout 5 cat %q || true", device) |
| out, err := runner(ctx, 10*time.Second, readCmd) |
| if err != nil { |
| return "", errors.Annotate(err, "send cmd: failed to get starfish output") |
| } |
| return strings.TrimSpace(out), nil |
| } |