Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/test-live-view.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Live View Tests

on:
push:
branches: ["**"]
paths:
- "packages/live-view/**"
pull_request:
branches: ["**"]
paths:
- "packages/live-view/**"

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: packages/live-view/pnpm-lock.yaml

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8

- name: Install dependencies
run: |
cd packages/live-view
pnpm install --frozen-lockfile

- name: Run lint
run: |
cd packages/live-view
pnpm lint

- name: Run type-check
run: |
cd packages/live-view
pnpm type-check

- name: Run tests
run: |
cd packages/live-view
pnpm test

- name: Run build
run: |
cd packages/live-view
pnpm build
5 changes: 4 additions & 1 deletion packages/cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ gbox-darwin-*
gbox-linux-*
gbox-windows-*
gbox
gbox-test
gbox-test

assets/scrcpy-server*.jar
internal/server/static/live-view/
28 changes: 26 additions & 2 deletions packages/cli/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,33 @@ clean: ## Clean the build directory
@rm -f $(BINARY_NAME)*
@echo "Cleaning completed"

# Build dependencies (live-view and scrcpy-server)
build-deps: build-live-view download-scrcpy-server ## Build all dependencies

# Build live-view static files and copy to CLI static directory
build-live-view: ## Build live-view static files
@echo "Building live-view static files..."
@$(MAKE) -C ../live-view build
@echo "Cleaning old live-view static files..."
@rm -rf internal/server/static/live-view
@echo "Copying live-view static files to CLI..."
@mkdir -p internal/server/static/live-view
@cp -r ../live-view/static/* internal/server/static/live-view/
@echo "✅ Live-view static files ready for embedding"

# Download scrcpy-server.jar
download-scrcpy-server: ## Download scrcpy-server.jar
@if [ ! -f "assets/scrcpy-server.jar" ]; then \
echo "Downloading scrcpy-server.jar..."; \
./scripts/download-scrcpy-server.sh; \
else \
echo "scrcpy-server.jar already exists"; \
fi

# Build binary for a single platform
binary: ## Build binary for the current platform (GOOS/GOARCH)
binary: build-deps ## Build binary for the current platform (GOOS/GOARCH)
@echo "Building $(BINARY_NAME) binary ($(GOOS)/$(GOARCH))..."
@echo "Note: live-view static files will be embedded in the binary"
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(LDFLAGS) -o $(BINARY_NAME) $(MAIN_FILE)
@echo "Binary built: $(BINARY_NAME)"

Expand All @@ -74,7 +98,7 @@ test: ## Run tests
go test ./... -v

# Build binaries for all supported platforms
binary-all: ## Build binaries for all supported platforms
binary-all: build-deps ## Build binaries for all supported platforms
@echo "Building binaries for all supported platforms..."
@for platform in $(PLATFORMS); do \
os=$$(echo $$platform | cut -d- -f1); \
Expand Down
44 changes: 17 additions & 27 deletions packages/cli/cmd/adb_expose.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ func NewAdbExposeStartCommand() *cobra.Command {
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeBoxIDs(cmd, args, toComplete)
},
SilenceUsage: true, // Don't show usage on error
SilenceErrors: true, // Don't show errors (we handle them ourselves)
}

cmd.Flags().IntVarP(&opts.LocalPort, "port", "p", 0, "Local port to bind to (default: auto-find available port starting from 5555)")
Expand All @@ -103,6 +105,8 @@ func NewAdbExposeStopCommand() *cobra.Command {
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeBoxIDs(cmd, args, toComplete)
},
SilenceUsage: true, // Don't show usage on error
SilenceErrors: true, // Don't show errors (we handle them ourselves)
}
return cmd
}
Expand All @@ -111,11 +115,13 @@ func NewAdbExposeListCommand() *cobra.Command {
opts := &AdbExposeListOptions{}
cmd := &cobra.Command{
Use: "list",
Short: "List all running adb-expose processes",
Short: "List all exposed ADB ports",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return ExecuteAdbExposeList(cmd, opts)
},
SilenceUsage: true, // Don't show usage on error
SilenceErrors: true, // Don't show errors (we handle them ourselves)
}

cmd.Flags().StringVarP(&opts.OutputFormat, "output", "o", "table", "Output format (table|json)")
Expand All @@ -142,19 +148,14 @@ func ExecuteAdbExposeInteractive(cmd *cobra.Command, opts *AdbExposeOptions) err
return fmt.Errorf("interactive mode not available in daemon process")
}

// Get current exposures without running cleanup logic
infos, err := adb_expose.ListPidFiles()
if err != nil {
return fmt.Errorf("failed to list current exposures: %v", err)
}

// Only show current exposures section if there are any
if len(infos) > 0 {
fmt.Println("Current ADB port exposures:")
fmt.Println("============================")
printAdbExposeTable(infos)
fmt.Println()
// Use the new client-server architecture to list current exposures
fmt.Println("Current ADB port exposures:")
fmt.Println("============================")
if err := adb_expose.ListCommand(""); err != nil {
// If server is not running, just show a message
fmt.Println("ADB Expose server is not running")
}
fmt.Println()

// Get available boxes
sdkClient, err := client.NewClientFromProfile()
Expand All @@ -172,22 +173,11 @@ func ExecuteAdbExposeInteractive(cmd *cobra.Command, opts *AdbExposeOptions) err
return nil
}

// Filter running Android boxes and exclude already exposed ones
// Filter running Android boxes
var availableBoxes []client.BoxInfo
exposedBoxIDs := make(map[string]bool)

// Use the infos variable we already got above
for _, info := range infos {
if adb_expose.IsProcessAlive(info.Pid) {
exposedBoxIDs[info.BoxID] = true
}
}

for _, box := range boxes {
if box.Status == "running" && strings.HasPrefix(box.Type, "android") {
if !exposedBoxIDs[box.ID] {
availableBoxes = append(availableBoxes, box)
}
availableBoxes = append(availableBoxes, box)
}
}

Expand Down Expand Up @@ -283,7 +273,7 @@ func ExecuteAdbExposeInteractive(cmd *cobra.Command, opts *AdbExposeOptions) err
// ExecuteAdbExposeStop stops adb-expose processes for a specific box
// This function is now implemented in adb_expose_stop.go

// ExecuteAdbExposeList lists all running adb-expose processes
// ExecuteAdbExposeList lists all exposed ADB ports
// This function is now implemented in adb_expose_list.go

func boxValid(boxID string) bool {
Expand Down
154 changes: 3 additions & 151 deletions packages/cli/cmd/adb_expose_list.go
Original file line number Diff line number Diff line change
@@ -1,160 +1,12 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strconv"
"strings"

"github.com/babelcloud/gbox/packages/cli/internal/adb_expose"
"github.com/spf13/cobra"
)

// ExecuteAdbExposeList lists all running adb-expose processes
// ExecuteAdbExposeList lists all exposed ADB ports using the new client-server architecture
func ExecuteAdbExposeList(cmd *cobra.Command, opts *AdbExposeListOptions) error {
// Step 1: Find all running gbox adb-expose processes (cross-platform, best effort)
psCmd := exec.Command("ps", "aux")
psOut, err := psCmd.Output()
if err != nil {
return fmt.Errorf("failed to run ps aux: %v", err)
}
lines := strings.Split(string(psOut), "\n")
var runningPids = make(map[int]bool)
for _, line := range lines {
if strings.Contains(line, "gbox adb-expose") && !strings.Contains(line, "grep") {
// ignore gbox adb-expose list process itself
if strings.Contains(line, "gbox adb-expose list") {
continue
}
fields := strings.Fields(line)
if len(fields) > 1 {
pid, err := strconv.Atoi(fields[1])
if err == nil {
runningPids[pid] = true
}
}
}
}
// Step 2: List all pid files (registered adb-exposes)
infos, err := adb_expose.ListPidFiles()
if err != nil {
return err
}
registeredPids := make(map[int]adb_expose.PidInfo)
for _, info := range infos {
registeredPids[info.Pid] = info
}
// Step 3: Check for running processes not in pid files
for pid := range runningPids {
if _, ok := registeredPids[pid]; !ok {
fmt.Printf("[WARN] Found running adb-expose process (pid=%d) not in registry. If you want to stop it, run: gbox adb-expose stop <box_id>\n\n", pid)
}
}
// Step 4: Check for pid files whose process is not running, and clean up
for pid, info := range registeredPids {
if !runningPids[pid] && !adb_expose.IsProcessAlive(pid) {
fmt.Printf("[CLEANUP] Removing stale pid file for dead process (pid=%d, boxId=%s, localPorts=%v)\n", pid, info.BoxID, info.LocalPorts)
for _, lp := range info.LocalPorts {
adb_expose.RemovePidFile(info.BoxID, lp)
adb_expose.RemoveLogFile(info.BoxID, lp)
}
}
}
// Step 5: For those pid files exist and process is running, check the box status, if the box is not running, clean up the pid file and kill the process
for pid, info := range registeredPids {
if runningPids[pid] && adb_expose.IsProcessAlive(pid) {
if !boxValid(info.BoxID) {
fmt.Printf("[CLEANUP] Box %s is not running, killing adb-expose process (pid=%d) and removing pid file(s)\n", info.BoxID, pid)
proc, err := os.FindProcess(pid)
if err == nil {
proc.Kill()
}
for _, lp := range info.LocalPorts {
adb_expose.RemovePidFile(info.BoxID, lp)
adb_expose.RemoveLogFile(info.BoxID, lp)
}
}
}
}

// Step 6: Print the current valid adb-exposes
updatedInfos, err := adb_expose.ListPidFiles()
if err != nil {
return fmt.Errorf("failed to list pid files after cleanup: %v", err)
}

// Output based on format
if opts.OutputFormat == "json" {
printAdbExposeJSON(updatedInfos)
} else {
printAdbExposeTable(updatedInfos)
}
return nil
}

// printAdbExposeTable prints the ADB expose table in a formatted way
func printAdbExposeTable(infos []adb_expose.PidInfo) {
if len(infos) == 0 {
fmt.Println("No ADB port exposures found")
return
}

fmt.Printf("| %-8s | %-36s | %-10s | %-8s | %-20s |\n", "PID", "BoxID", "Port", "Status", "StartedAt")
fmt.Println("|----------|--------------------------------------|------------|----------|----------------------|")
for _, info := range infos {
status := "Dead"
if adb_expose.IsProcessAlive(info.Pid) {
status = "Alive"
}
for i := 0; i < len(info.LocalPorts); i++ {
fmt.Printf("| %-8d | %-36s | %-10d | %-8s | %-20s |\n", info.Pid, info.BoxID, info.LocalPorts[i], status, info.StartedAt.Format("2006-01-02 15:04:05"))
}
}
}

// printAdbExposeJSON prints the ADB expose information in JSON format
func printAdbExposeJSON(infos []adb_expose.PidInfo) {
// Debug: check if infos is nil or empty
if infos == nil {
fmt.Println("[]")
return
}

type AdbExposeInfo struct {
PID int `json:"pid"`
BoxID string `json:"boxId"`
LocalPorts []int `json:"localPorts"`
Status string `json:"status"`
StartedAt string `json:"startedAt"`
}

var jsonData []AdbExposeInfo
for _, info := range infos {
status := "Dead"
if adb_expose.IsProcessAlive(info.Pid) {
status = "Alive"
}

jsonInfo := AdbExposeInfo{
PID: info.Pid,
BoxID: info.BoxID,
LocalPorts: info.LocalPorts,
Status: status,
StartedAt: info.StartedAt.Format("2006-01-02T15:04:05Z"),
}
jsonData = append(jsonData, jsonInfo)
}

// Ensure we always output a valid JSON array, even if empty
jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err)
// Fallback to empty array if marshaling fails
fmt.Println("[]")
return
}

fmt.Println(string(jsonBytes))
// Use the new client-server architecture
return adb_expose.ListCommand(opts.OutputFormat)
}
Loading
Loading