Skip to main content
Glama
commands.go16.4 kB
package adb_expose import ( "bytes" "encoding/json" "fmt" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" sdk "github.com/babelcloud/gbox-sdk-go" gboxsdk "github.com/babelcloud/gbox/packages/cli/internal/client" "github.com/babelcloud/gbox/packages/cli/internal/util" ) // StartCommand starts port forwarding using the main GBOX server API func StartCommand(boxID string, localPorts, remotePorts []int, foreground bool) error { // First check if the box exists if err := checkBoxExists(boxID); err != nil { return err } // Ensure main GBOX server is running if err := ensureServerRunning(); err != nil { return fmt.Errorf("failed to start server: %v", err) } // Create request payload reqBody := map[string]interface{}{ "box_id": boxID, "local_ports": localPorts, "remote_ports": remotePorts, } // Convert to JSON jsonData, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("failed to marshal request: %v", err) } // Send HTTP request to main server client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Post("http://127.0.0.1:29888/api/adb-expose/start", "application/json", bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to send request to server: %v", err) } defer resp.Body.Close() // Parse response var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return fmt.Errorf("failed to parse response: %v", err) } // Check if request was successful if resp.StatusCode == http.StatusConflict { // Handle 409 Conflict - already running fmt.Printf("ADB port is already exposed for box %s\n", boxID) return nil } if resp.StatusCode != http.StatusOK { errorMsg, _ := result["error"].(string) // Check for specific error types and provide user-friendly messages if strings.Contains(errorMsg, "box is not running") { return fmt.Errorf("box %s is not running or does not exist", boxID) } return fmt.Errorf("server error: %s", errorMsg) } success, _ := result["success"].(bool) if !success { errorMsg, _ := result["error"].(string) // Check for specific error types and provide user-friendly messages if strings.Contains(errorMsg, "box is not running") { return fmt.Errorf("box %s is not running or does not exist", boxID) } return fmt.Errorf("failed to start ADB port expose: %s", errorMsg) } // Print success message fmt.Printf("✅ ADB port exposed for box %s on port %v\n", boxID, localPorts[0]) if !foreground { fmt.Printf("\n💡 Use 'gbox adb-expose list' to view all exposed ports\n") fmt.Printf(" Use 'gbox adb-expose stop %s' to stop\n", boxID) } return nil } // StopCommand stops port forwarding using the main GBOX server API func StopCommand(boxID string) error { // First check if the box exists if err := checkBoxExists(boxID); err != nil { return err } // Ensure main GBOX server is running if err := ensureServerRunning(); err != nil { return fmt.Errorf("failed to start server: %v", err) } // Create request payload reqBody := map[string]interface{}{ "box_id": boxID, } // Convert to JSON jsonData, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("failed to marshal request: %v", err) } // Send HTTP request to main server client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Post("http://127.0.0.1:29888/api/adb-expose/stop", "application/json", bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to send request to server: %v", err) } defer resp.Body.Close() // Parse response var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return fmt.Errorf("failed to parse response: %v", err) } // Check if request was successful if resp.StatusCode == http.StatusNotFound { // Box exists but ADB port expose is not active return fmt.Errorf("ADB port expose is not active for box %s", boxID) } if resp.StatusCode != http.StatusOK { errorMsg, _ := result["error"].(string) return fmt.Errorf("server error: %s", errorMsg) } success, _ := result["success"].(bool) if !success { errorMsg, _ := result["error"].(string) return fmt.Errorf("failed to stop ADB port expose: %s", errorMsg) } // Print success message fmt.Printf("✅ ADB port expose stopped for box %s\n", boxID) return nil } // ListCommand lists all running port forwards using the new client-server architecture func ListCommand(outputFormat string) error { // Ensure server is running before making requests if err := ensureServerRunning(); err != nil { return fmt.Errorf("failed to start server: %v", err) } // Check if main server is running by trying to connect to it client := &http.Client{ Timeout: 2 * time.Second, } // Try to get ADB Expose list from the main server resp, err := client.Get("http://127.0.0.1:29888/api/adb-expose/list") if err != nil { fmt.Println("ADB Expose server is not running") return nil } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { fmt.Println("ADB Expose server is not responding properly") return nil } // Parse response var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return fmt.Errorf("failed to parse response: %v", err) } // Display results forwards, ok := result["forwards"].([]interface{}) if !ok || len(forwards) == 0 { fmt.Println("No ADB ports are currently exposed") return nil } // Convert to table data format var tableData []map[string]interface{} for _, forward := range forwards { f, ok := forward.(map[string]interface{}) if !ok { continue } boxID, _ := f["box_id"].(string) localPorts, _ := f["local_ports"].([]interface{}) startedAt, _ := f["started_at"].(string) localPortStr := formatPortsFromInterface(localPorts) // Don't truncate box ID - show full ID tableData = append(tableData, map[string]interface{}{ "box_id": boxID, "port": localPortStr, "started_at": startedAt, }) } // Output based on format if outputFormat == "json" { // Output JSON format jsonData, err := json.MarshalIndent(map[string]interface{}{ "forwards": tableData, }, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %v", err) } fmt.Println(string(jsonData)) } else { // Render table renderTable(tableData) } return nil } // formatPortsFromInterface formats a slice of ports from interface{} as a string func formatPortsFromInterface(ports []interface{}) string { if len(ports) == 0 { return "none" } portStrs := make([]string, len(ports)) for i, port := range ports { if portFloat, ok := port.(float64); ok { portStrs[i] = strconv.Itoa(int(portFloat)) } else if portStr, ok := port.(string); ok { portStrs[i] = portStr } else { portStrs[i] = "unknown" } } return strings.Join(portStrs, ",") } // renderTable renders the ADB Expose list table func renderTable(data []map[string]interface{}) { if len(data) == 0 { fmt.Println("No ADB ports are currently exposed") return } // Prepare data for RenderTable tableData := make([]map[string]interface{}, len(data)) for i, row := range data { boxID, _ := row["box_id"].(string) port, _ := row["port"].(string) startedAt, _ := row["started_at"].(string) tableData[i] = map[string]interface{}{ "box_id": boxID, "port": port, "started_at": startedAt, } } // Define table columns columns := []util.TableColumn{ {Header: "Box ID", Key: "box_id"}, {Header: "Port", Key: "port"}, {Header: "Started At", Key: "started_at"}, } util.RenderTable(columns, tableData) } // ensureServerRunning ensures the GBOX server is running, starting it if necessary func ensureServerRunning() error { // Check if server is already running if isServerRunning() { // Check if server version matches current build if !isServerVersionCompatible() { fmt.Println("🔄 Server version mismatch, restarting server...") // Kill existing server and start new one if err := killExistingServer(); err != nil { fmt.Printf("⚠️ Warning: failed to kill existing server: %v\n", err) } return startServerInBackground() } return nil } // Start server in background return startServerInBackground() } // isServerRunning checks if the server is already running func isServerRunning() bool { conn, err := http.Get("http://127.0.0.1:29888/health") if err != nil { return false } defer conn.Body.Close() return conn.StatusCode == http.StatusOK } // isServerVersionCompatible checks if the running server version matches current build func isServerVersionCompatible() bool { // Get server build ID serverBuildID, err := getServerBuildID() if err != nil { // If we can't get server build ID, assume incompatible return false } // Get current build ID currentBuildID := getCurrentBuildID() // For development, we'll use a more lenient approach: // If both build IDs contain "unknown" (development mode), do exact comparison // This ensures that recompiled binaries trigger server restart if strings.Contains(serverBuildID, "unknown") && strings.Contains(currentBuildID, "unknown") { return currentBuildID == serverBuildID } // For production builds, do exact comparison return currentBuildID == serverBuildID } // getCurrentBuildID returns the current build ID func getCurrentBuildID() string { // For development, use a simple approach that changes when binary is recompiled // In production, this would be set by build scripts execPath, err := os.Executable() if err != nil { return "unknown" } info, err := os.Stat(execPath) if err != nil { return "unknown" } // Use modification time + file size to detect binary changes // This will change when the binary is recompiled buildTime := info.ModTime().Format("2006-01-02T15:04:05") // No timezone, more stable gitCommit := "unknown" fileSize := info.Size() return fmt.Sprintf("%s-%s-%d", buildTime, gitCommit, fileSize) } // getServerBuildID gets the build ID from the running server func getServerBuildID() (string, error) { client := &http.Client{Timeout: 2 * time.Second} resp, err := client.Get("http://127.0.0.1:29888/api/server/info") if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("server returned status %d", resp.StatusCode) } var info map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return "", err } buildID, ok := info["build_id"].(string) if !ok { return "", fmt.Errorf("build_id not found in server response") } return buildID, nil } // killExistingServer kills the existing server process func killExistingServer() error { // Read PID from PID file pidFile := filepath.Join(os.Getenv("HOME"), ".gbox", "cli", "gbox-server.pid") pidData, err := os.ReadFile(pidFile) if err != nil { // PID file doesn't exist, try to find process by port return killServerByPort() } pid := strings.TrimSpace(string(pidData)) if pid == "" { // Empty PID file, try to find process by port return killServerByPort() } // Convert PID to int pidInt, err := strconv.Atoi(pid) if err != nil { // Invalid PID, try to find process by port return killServerByPort() } // Try to kill the process by PID first if err := killProcessByPID(pidInt); err != nil { // If PID-based kill fails, try port-based kill return killServerByPort() } // Remove PID file os.Remove(pidFile) return nil } // killProcessByPID kills a process by its PID func killProcessByPID(pid int) error { process, err := os.FindProcess(pid) if err != nil { return fmt.Errorf("failed to find process %d: %v", pid, err) } // Send SIGTERM first if err := process.Signal(syscall.SIGTERM); err != nil { return fmt.Errorf("failed to send SIGTERM to process %d: %v", pid, err) } // Wait for graceful shutdown for i := 0; i < 10; i++ { time.Sleep(500 * time.Millisecond) // Check if process is still running if err := process.Signal(syscall.Signal(0)); err != nil { // Process is dead return nil } } // Process still running, force kill if err := process.Signal(syscall.SIGKILL); err != nil { return fmt.Errorf("failed to send SIGKILL to process %d: %v", pid, err) } // Wait a bit more for SIGKILL to take effect time.Sleep(1 * time.Second) return nil } // killServerByPort kills the server process by finding it via port func killServerByPort() error { // Use lsof to find the process using port 29888 cmd := exec.Command("lsof", "-ti:29888") output, err := cmd.Output() if err != nil { // No process found on port, that's fine return nil } pids := strings.Fields(string(output)) for _, pidStr := range pids { pid, err := strconv.Atoi(pidStr) if err != nil { continue } // Kill the process if err := killProcessByPID(pid); err != nil { fmt.Printf("Warning: failed to kill process %d: %v\n", pid, err) } } return nil } // startServerInBackground starts the server in background mode with IPC communication func startServerInBackground() error { // Create a pipe for IPC communication reader, writer, err := os.Pipe() if err != nil { return fmt.Errorf("failed to create pipe: %v", err) } defer reader.Close() defer writer.Close() // Get the current executable path execPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %v", err) } // Create command to start server in background with reply fd cmd := exec.Command(execPath, "server", "start", "--reply-fd", "3") // Set up process attributes for daemon mode cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, // Create new process group } // Pass the write end of the pipe as file descriptor 3 cmd.ExtraFiles = []*os.File{writer} // Redirect output to log file homeDir, _ := os.UserHomeDir() gboxDir := filepath.Join(homeDir, ".gbox", "cli") logFile := filepath.Join(gboxDir, "server.log") logFileHandle, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return fmt.Errorf("failed to open log file: %v", err) } defer logFileHandle.Close() cmd.Stdout = logFileHandle cmd.Stderr = logFileHandle // Start the process if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start server: %v", err) } // Close the write end in parent process writer.Close() // Read the reply from the child process replyChan := make(chan error, 1) go func() { buffer := make([]byte, 1024) n, err := reader.Read(buffer) if err != nil { replyChan <- fmt.Errorf("failed to read reply: %v", err) return } reply := string(buffer[:n]) if reply == "OK" { replyChan <- nil } else { replyChan <- fmt.Errorf("server startup failed: %s", reply) } }() // Wait for reply with timeout select { case err := <-replyChan: if err != nil { // Server failed to start, clean up the process cmd.Process.Kill() return err } case <-time.After(10 * time.Second): // Timeout waiting for reply cmd.Process.Kill() return fmt.Errorf("timeout waiting for server startup reply") } // Write PID to file pidFile := filepath.Join(gboxDir, "gbox-server.pid") pidFileHandle, err := os.OpenFile(pidFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return fmt.Errorf("failed to create PID file: %v", err) } defer pidFileHandle.Close() if _, err := pidFileHandle.WriteString(strconv.Itoa(cmd.Process.Pid)); err != nil { return fmt.Errorf("failed to write PID file: %v", err) } return nil } // createGBOXClient creates a GBOX client for API calls func createGBOXClient() (*sdk.Client, error) { return gboxsdk.NewClientFromProfile() } // checkBoxExists checks if a box exists using the GBOX API func checkBoxExists(boxID string) error { // Create a client to check if the box exists client, err := createGBOXClient() if err != nil { return fmt.Errorf("failed to create client: %v", err) } // Check if box exists box, err := gboxsdk.GetBox(client, boxID) if err != nil { // If we can't get the box, it might not exist return fmt.Errorf("box %s does not exist or is not accessible", boxID) } // Check if box is running if box.Status != "running" { return fmt.Errorf("box %s is not running (status: %s)", boxID, box.Status) } return nil }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babelcloud/gru-sandbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server