From 344aeaa3a4dd69f1712d6845ba1158fa56d275e6 Mon Sep 17 00:00:00 2001 From: Seungjae Yoo Date: Fri, 10 Jul 2026 07:59:32 +0000 Subject: [PATCH 1/3] Refine output format of `cvd ps` --- .../host/commands/cvd/cli/commands/ps.cpp | 21 ++++++++++++------- .../host/commands/cvd/cli/commands/ps.h | 5 +++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.cpp index d7103572c66..101b2a95f47 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.cpp @@ -51,9 +51,13 @@ usage: cvd ps [--help] )"; const std::map kHeaders = { - {PsColumns::kNames, "NAMES"}, {PsColumns::kId, "ID"}, - {PsColumns::kStatus, "STATUS"}, {PsColumns::kCreated, "CREATED"}, - {PsColumns::kPorts, "PORTS"}, {PsColumns::kWebAccess, "WEB_ACCESS"}, + {PsColumns::kGroups, "GROUPS"}, + {PsColumns::kNames, "NAMES"}, + {PsColumns::kId, "ID"}, + {PsColumns::kStatus, "STATUS"}, + {PsColumns::kCreated, "CREATED"}, + {PsColumns::kAdbSerial, "ADB_SERIAL"}, + {PsColumns::kWebAccess, "WEB_ACCESS"}, }; void PrintTable(const std::map& headers, @@ -130,14 +134,14 @@ PsRow CvdPsCommandHandler::InstanceToRow(const LocalInstanceGroup& group, LocalInstance& instance) const { PsRow row; - row[PsColumns::kNames] = - fmt::format("{}/{}", group.GroupName(), instance.Name()); + row[PsColumns::kGroups] = group.GroupName(); + row[PsColumns::kNames] = instance.Name(); row[PsColumns::kId] = std::to_string(instance.Id()); // Fetch live status const Result status_json_res = instance.FetchStatus(); std::string status_str = "Unknown (Fetch Failed)"; - std::string ports_str = "-"; + std::string adb_serial_str = "-"; std::string web_access_str = "-"; if (status_json_res.ok()) { @@ -146,7 +150,8 @@ PsRow CvdPsCommandHandler::InstanceToRow(const LocalInstanceGroup& group, if (instance.IsActive()) { if (status_json.isMember("adb_port")) { - ports_str = fmt::format("adb:{}", status_json["adb_port"].asInt()); + adb_serial_str = + fmt::format("localhost:{}", status_json["adb_port"].asInt()); } if (status_json.isMember("web_access")) { web_access_str = status_json["web_access"].asString(); @@ -156,7 +161,7 @@ PsRow CvdPsCommandHandler::InstanceToRow(const LocalInstanceGroup& group, row[PsColumns::kStatus] = status_str; row[PsColumns::kCreated] = Format(group.StartTime()); - row[PsColumns::kPorts] = ports_str; + row[PsColumns::kAdbSerial] = adb_serial_str; row[PsColumns::kWebAccess] = web_access_str; return row; diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.h index cc2fbc63770..3c3c2e29b74 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/ps.h @@ -27,11 +27,12 @@ namespace cuttlefish { enum class PsColumns { - kNames = 0, + kGroups = 0, + kNames, kId, kStatus, kCreated, - kPorts, + kAdbSerial, kWebAccess, }; From 9e84a6f56fa45010c283c145882f7cf81f61a592 Mon Sep 17 00:00:00 2001 From: Seungjae Yoo Date: Mon, 13 Jul 2026 02:13:28 +0000 Subject: [PATCH 2/3] Prefactor: Introduce new function for handling exec on multiple hosts --- container/src/podcvd/internal/cvd.go | 18 ++++---- container/src/podcvd/internal/host.go | 54 ++++++++++++++++++++++ container/src/podcvd/internal/main.go | 64 +++++++++------------------ 3 files changed, 85 insertions(+), 51 deletions(-) diff --git a/container/src/podcvd/internal/cvd.go b/container/src/podcvd/internal/cvd.go index 1b2714ef3d9..7b731e1f515 100644 --- a/container/src/podcvd/internal/cvd.go +++ b/container/src/podcvd/internal/cvd.go @@ -91,16 +91,18 @@ func UpdateCvdGroupJsonRaw(data any, podcvdHomeDir, ipAddr string) { } } -var cvdPathRegex = regexp.MustCompile(`^/var/tmp/cvd/[0-9]+/[0-9]+/home`) - -func updateStringOnCvdGroupJsonRaw(data, podcvdHomeDir, ipAddr string) string { - operatorEndpointOnHost := fmt.Sprintf("%s:%d", ipAddr, portOperatorHttpsOnHost) +func updateIPAndPortString(data, ipAddr string) string { + operatorIpAndPortOnHost := fmt.Sprintf("%s:%d", ipAddr, portOperatorHttpsOnHost) for _, host := range []string{"0.0.0.0", "localhost", "127.0.0.1"} { - data = strings.ReplaceAll(data, fmt.Sprintf("%s:%d", host, portOperatorHttps), operatorEndpointOnHost) + data = strings.ReplaceAll(data, fmt.Sprintf("%s:%d", host, portOperatorHttps), operatorIpAndPortOnHost) data = strings.ReplaceAll(data, host, ipAddr) } - if cvdPathRegex.MatchString(data) { - data = cvdPathRegex.ReplaceAllString(data, podcvdHomeDir) - } return data } + +var cvdPathRegex = regexp.MustCompile(`^/var/tmp/cvd/[0-9]+/[0-9]+/home`) + +func updateStringOnCvdGroupJsonRaw(data, podcvdHomeDir, ipAddr string) string { + data = updateIPAndPortString(data, ipAddr) + return cvdPathRegex.ReplaceAllString(data, podcvdHomeDir) +} diff --git a/container/src/podcvd/internal/host.go b/container/src/podcvd/internal/host.go index 8895ab06370..8d5be596875 100644 --- a/container/src/podcvd/internal/host.go +++ b/container/src/podcvd/internal/host.go @@ -15,10 +15,13 @@ package internal import ( + "bytes" "context" "crypto/tls" "encoding/json" + "errors" "fmt" + "io" "log" "math/rand" "net" @@ -29,6 +32,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "time" "github.com/google/uuid" @@ -117,6 +121,56 @@ func ExecFetchCmdOnDisposableHost(ccm CuttlefishContainerManager, cvdArgs *CvdAr return nil } +type HostExecResult struct { + GroupName string + IP string + Stdout []byte +} + +func ExecOnAllCuttlefishHosts(ccm CuttlefishContainerManager, subcommandArgs []string, stderr io.Writer) ([]HostExecResult, error) { + groupNameIpAddrMap, err := Ipv4AddressesByGroupNames(ccm, false) + if err != nil { + return nil, fmt.Errorf("failed to get IPv4 addresses for group names: %w", err) + } + + var wg sync.WaitGroup + wg.Add(len(groupNameIpAddrMap)) + resCh := make(chan HostExecResult, len(groupNameIpAddrMap)) + errCh := make(chan error, len(groupNameIpAddrMap)) + for groupName, ip := range groupNameIpAddrMap { + go func(groupName, ip string) { + defer wg.Done() + containerName := ContainerName(groupName) + var stdoutBuf bytes.Buffer + if err := ccm.ExecOnContainer(context.Background(), containerName, subcommandArgs, nil, &stdoutBuf, stderr); err != nil { + errCh <- fmt.Errorf("failed to run %v on container %q: %w", subcommandArgs, containerName, err) + return + } + resCh <- HostExecResult{ + GroupName: groupName, + IP: ip, + Stdout: stdoutBuf.Bytes(), + } + }(groupName, ip) + } + wg.Wait() + close(resCh) + close(errCh) + + var errs []error + for err := range errCh { + errs = append(errs, err) + } + if err := errors.Join(errs...); err != nil { + return nil, err + } + var results []HostExecResult + for res := range resCh { + results = append(results, res) + } + return results, nil +} + func pullContainerImage(ccm CuttlefishContainerManager) error { if exists, err := ccm.ImageExists(context.Background(), imageName); err != nil { return err diff --git a/container/src/podcvd/internal/main.go b/container/src/podcvd/internal/main.go index 00f0422201b..2ff390c67ed 100644 --- a/container/src/podcvd/internal/main.go +++ b/container/src/podcvd/internal/main.go @@ -285,57 +285,35 @@ func fleetAllCuttlefishHosts(ccm CuttlefishContainerManager) error { Groups []any `json:"groups"` } - groupNameIpAddrMap, err := Ipv4AddressesByGroupNames(ccm, false) + results, err := ExecOnAllCuttlefishHosts(ccm, []string{"cvd", "fleet"}, nil) if err != nil { - return fmt.Errorf("failed to get IPv4 addresses for group names: %w", err) - } - var wg sync.WaitGroup - wg.Add(len(groupNameIpAddrMap)) - resCh := make(chan cvdFleetResponse, len(groupNameIpAddrMap)) - errCh := make(chan error, len(groupNameIpAddrMap)) - for groupName, ip := range groupNameIpAddrMap { - go func(groupName, ip string) { - defer wg.Done() - containerName := ContainerName(groupName) - var stdoutBuf bytes.Buffer - if err := ccm.ExecOnContainer(context.Background(), containerName, []string{"cvd", "fleet"}, nil, &stdoutBuf, nil); err != nil { - errCh <- err - return - } - var res cvdFleetResponse - if err := json.Unmarshal(stdoutBuf.Bytes(), &res); err != nil { - errCh <- err - return - } - containerInfo, err := ccm.InspectContainer(context.Background(), containerName) - if err != nil { - errCh <- err - return - } - attemptID := containerInfo.Config.Labels["attempt_id"] - podcvdHomeDir := filepath.Join("/var/tmp/podcvd", strconv.Itoa(os.Getuid()), attemptID) - for idx := range res.Groups { - UpdateCvdGroupJsonRaw(res.Groups[idx], podcvdHomeDir, ip) - } - resCh <- res - }(groupName, ip) + return err } - wg.Wait() - close(resCh) - close(errCh) - var errs []error - for err := range errCh { - errs = append(errs, err) + containers, err := ccm.ListContainers(context.Background(), false) + if err != nil { + return fmt.Errorf("failed to list containers: %w", err) } - if err := errors.Join(errs...); err != nil { - return err + podcvdHomeDirMap := make(map[string]string) + uid := strconv.Itoa(os.Getuid()) + for _, c := range containers { + if groupName, ok := c.Labels[labelGroupName]; ok { + podcvdHomeDirMap[groupName] = filepath.Join("/var/tmp/podcvd", uid, c.Labels[labelAttemptID]) + } } + combinedRes := cvdFleetResponse{ Groups: []any{}, } - for res := range resCh { - combinedRes.Groups = append(combinedRes.Groups, res.Groups...) + for _, res := range results { + var fleetRes cvdFleetResponse + if err := json.Unmarshal(res.Stdout, &fleetRes); err != nil { + return err + } + for idx := range fleetRes.Groups { + UpdateCvdGroupJsonRaw(fleetRes.Groups[idx], podcvdHomeDirMap[res.GroupName], res.IP) + } + combinedRes.Groups = append(combinedRes.Groups, fleetRes.Groups...) } combinedOutput, err := json.MarshalIndent(combinedRes, "", " ") if err != nil { From 07a99bb8af2642423d10aca5ff372ff26920803e Mon Sep 17 00:00:00 2001 From: Seungjae Yoo Date: Mon, 13 Jul 2026 04:54:17 +0000 Subject: [PATCH 3/3] Support `podcvd ps` --- container/src/podcvd/internal/cvd.go | 99 +++++++++++++++++++++++++++ container/src/podcvd/internal/main.go | 18 +++++ 2 files changed, 117 insertions(+) diff --git a/container/src/podcvd/internal/cvd.go b/container/src/podcvd/internal/cvd.go index 7b731e1f515..25a91377e14 100644 --- a/container/src/podcvd/internal/cvd.go +++ b/container/src/podcvd/internal/cvd.go @@ -17,8 +17,11 @@ package internal import ( "encoding/json" "fmt" + "io" "regexp" + "sort" "strings" + "text/tabwriter" "github.com/go-playground/validator/v10" ) @@ -91,6 +94,31 @@ func UpdateCvdGroupJsonRaw(data any, podcvdHomeDir, ipAddr string) { } } +func PrintPsOutputs(results []HostExecResult, out io.Writer) error { + var allRows []map[string]string + colMap := make(map[string]int) + for _, res := range results { + rows, cols, err := parsePsOutput(string(res.Stdout), res.IP) + if err != nil { + return fmt.Errorf("failed to parse cvd ps output from group %q: %w", res.GroupName, err) + } + allRows = append(allRows, rows...) + for _, col := range cols { + if _, exists := colMap[col.name]; !exists { + colMap[col.name] = col.start + } + } + } + var colNames []string + for name := range colMap { + colNames = append(colNames, name) + } + sort.Slice(colNames, func(i, j int) bool { + return colMap[colNames[i]] < colMap[colNames[j]] + }) + return combinePsOutputs(allRows, colNames, out) +} + func updateIPAndPortString(data, ipAddr string) string { operatorIpAndPortOnHost := fmt.Sprintf("%s:%d", ipAddr, portOperatorHttpsOnHost) for _, host := range []string{"0.0.0.0", "localhost", "127.0.0.1"} { @@ -106,3 +134,74 @@ func updateStringOnCvdGroupJsonRaw(data, podcvdHomeDir, ipAddr string) string { data = updateIPAndPortString(data, ipAddr) return cvdPathRegex.ReplaceAllString(data, podcvdHomeDir) } + +type psColumn struct { + name string + start int +} + +var psHeaderRegex = regexp.MustCompile(`\S+`) + +func parsePsHeader(headerLine string) ([]psColumn, error) { + matches := psHeaderRegex.FindAllStringIndex(headerLine, -1) + if len(matches) == 0 { + return nil, fmt.Errorf("invalid header line %q: no column headers found", headerLine) + } + var cols []psColumn + for _, m := range matches { + cols = append(cols, psColumn{ + name: headerLine[m[0]:m[1]], + start: m[0], + }) + } + return cols, nil +} + +func parsePsRow(line string, cols []psColumn, ipAddr string) map[string]string { + rowMap := make(map[string]string) + lineLen := len(line) + for i, col := range cols { + if col.start >= lineLen { + rowMap[col.name] = "" + continue + } + end := lineLen + if i < len(cols)-1 && cols[i+1].start < lineLen { + end = cols[i+1].start + } + rowMap[col.name] = updateIPAndPortString(strings.TrimSpace(line[col.start:end]), ipAddr) + } + return rowMap +} + +func parsePsOutput(stdout, ipAddr string) ([]map[string]string, []psColumn, error) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + if len(lines) < 1 || lines[0] == "" { + return nil, nil, fmt.Errorf("empty cvd ps output: missing header") + } + cols, err := parsePsHeader(lines[0]) + if err != nil { + return nil, nil, err + } + var rows []map[string]string + for _, line := range lines[1:] { + rows = append(rows, parsePsRow(line, cols, ipAddr)) + } + return rows, cols, nil +} + +func combinePsOutputs(allRows []map[string]string, colNames []string, out io.Writer) error { + w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + fmt.Fprintln(w, strings.Join(colNames, "\t")) + for _, rowMap := range allRows { + var rowValues []string + for _, colName := range colNames { + rowValues = append(rowValues, rowMap[colName]) + } + fmt.Fprintln(w, strings.Join(rowValues, "\t")) + } + if err := w.Flush(); err != nil { + return fmt.Errorf("failed to flush output: %w", err) + } + return nil +} diff --git a/container/src/podcvd/internal/main.go b/container/src/podcvd/internal/main.go index 2ff390c67ed..54907539359 100644 --- a/container/src/podcvd/internal/main.go +++ b/container/src/podcvd/internal/main.go @@ -65,6 +65,10 @@ func Main(args []string) error { if err := fleetAllCuttlefishHosts(ccm); err != nil { return err } + case "ps": + if err := psAllCuttlefishHosts(ccm); err != nil { + return err + } case "cache", "help", "lint", "login", "version": if err := handleToolingSubcommands(ccm, cvdArgs); err != nil { return err @@ -323,6 +327,20 @@ func fleetAllCuttlefishHosts(ccm CuttlefishContainerManager) error { return nil } +func psAllCuttlefishHosts(ccm CuttlefishContainerManager) error { + results, err := ExecOnAllCuttlefishHosts(ccm, []string{"cvd", "ps"}, os.Stderr) + if err != nil { + return err + } + if len(results) > 0 { + return PrintPsOutputs(results, os.Stdout) + } + if err := CreateToolingHost(ccm); err != nil { + return err + } + return ccm.ExecOnContainer(context.Background(), ToolingContainerName, []string{"cvd", "ps"}, nil, os.Stdout, os.Stderr) +} + func handleToolingSubcommands(ccm CuttlefishContainerManager, cvdArgs *CvdArgs) error { if err := CreateToolingHost(ccm); err != nil { return err