Skip to content

Commit d0b8ed5

Browse files
authored
⭐ feat: enhance device connection management (#262)
- Added IsConnected field to DeviceDTO for connection status. - Implemented ReconnectRegisteredDevices for server start reconnections. - Updated device listing to show connection status. - Refactored server and handler interfaces for new checks.
1 parent af95996 commit d0b8ed5

File tree

5 files changed

+99
-272
lines changed

5 files changed

+99
-272
lines changed

packages/cli/cmd/device_connect_list.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
)
1313

1414
const (
15+
statusConnected = "Connected"
1516
statusRegistered = "Registered"
1617
statusNotRegistered = "Not Registered"
1718
)
@@ -30,6 +31,7 @@ type DeviceDTO struct {
3031
OS string `json:"os"` // android, linux, windows, macos
3132
DeviceType string `json:"deviceType"` // physical, emulator, vm
3233
IsRegistered bool `json:"isRegistered"`
34+
IsConnected bool `json:"isConnected"` // true if device is currently connected to AP
3335
RegId string `json:"regId"`
3436
IsLocal bool `json:"isLocal"` // true if this is the local desktop device
3537
Metadata map[string]interface{} `json:"metadata"` // Device-specific metadata
@@ -126,11 +128,18 @@ func outputDevicesTextFromAPI(devices []DeviceDTO) error {
126128
serialNo := device.Serialno
127129
transportID := device.TransportID
128130
isRegistered := device.IsRegistered
129-
130-
status := statusNotRegistered
131-
if isRegistered {
132-
// Green for Registered
133-
status = "\x1b[32m" + statusRegistered + "\x1b[0m"
131+
isConnected := device.IsConnected
132+
133+
// Determine status based on connection state
134+
var status string
135+
if isConnected {
136+
// Green for Connected
137+
status = "\x1b[32m" + statusConnected + "\x1b[0m"
138+
} else if isRegistered {
139+
// Yellow for Registered but not connected
140+
status = "\x1b[33m" + statusRegistered + "\x1b[0m"
141+
} else {
142+
status = statusNotRegistered
134143
}
135144

136145
// Get OS and DeviceType from device

packages/cli/internal/server/device_keeper.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ func (dm *DeviceKeeper) Start() error {
113113
}
114114
}()
115115

116+
// Reconnect all registered devices (both Android and desktop)
117+
go func() {
118+
// Give adb watcher some time to detect online devices first
119+
time.Sleep(2 * time.Second)
120+
if err := dm.ReconnectRegisteredDevices(); err != nil {
121+
log.Printf("Failed to reconnect registered devices: %v", err)
122+
}
123+
}()
124+
116125
return nil
117126
}
118127

@@ -302,6 +311,12 @@ func (dm *DeviceKeeper) getDevice(serial string) (*DeviceSession, bool) {
302311
return session, ok
303312
}
304313

314+
// IsDeviceConnected checks if a device is currently connected to AP
315+
func (dm *DeviceKeeper) IsDeviceConnected(serial string) bool {
316+
_, ok := dm.getDevice(serial)
317+
return ok
318+
}
319+
305320
func (dm *DeviceKeeper) hasDevice(serial string) bool {
306321
dm.mu.RLock()
307322
defer dm.mu.RUnlock()
@@ -402,6 +417,45 @@ func (dm *DeviceKeeper) CleanupExpiredDeviceInfos() {
402417
}
403418
}
404419

420+
// ReconnectRegisteredDevices reconnects all registered devices on server start
421+
// This is called after server startup to restore device connections
422+
func (dm *DeviceKeeper) ReconnectRegisteredDevices() error {
423+
// Get all registered devices from cloud
424+
deviceList, err := dm.deviceAPI.GetAll()
425+
if err != nil {
426+
return errors.Wrap(err, "failed to get registered devices from cloud")
427+
}
428+
429+
log.Printf("Attempting to reconnect %d registered device(s)", len(deviceList.Data))
430+
431+
for _, dev := range deviceList.Data {
432+
// Skip if device is already connected
433+
if dm.IsDeviceConnected(dev.Metadata.Serialno) {
434+
log.Printf("Device %s (%s) already connected, skipping", dev.Id, dev.Metadata.Serialno)
435+
continue
436+
}
437+
438+
deviceType := dev.Metadata.DeviceType
439+
osType := dev.Metadata.OsType
440+
serialno := dev.Metadata.Serialno
441+
442+
// For Android devices, wait for adb watcher to handle connection
443+
// Only reconnect desktop devices here
444+
if deviceType == "desktop" && serialno != "" {
445+
log.Printf("Reconnecting desktop device %s (%s)", dev.Id, serialno)
446+
go func(id, sn, dt, ot string) {
447+
if err := dm.connectAPUsingDeviceId(sn, id, dt, ot); err != nil {
448+
log.Printf("Failed to reconnect device %s: %v", id, err)
449+
} else {
450+
log.Printf("Successfully reconnected device %s", id)
451+
}
452+
}(dev.Id, serialno, deviceType, osType)
453+
}
454+
}
455+
456+
return nil
457+
}
458+
405459
func connectAP(url, token, protocol, serial string) (*smux.Session, error) {
406460
req, err := http.NewRequest(http.MethodGet, url, nil)
407461
if err != nil {

packages/cli/internal/server/handlers/devices.go

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type DeviceDTO struct {
3535
OS string `json:"os"` // android, linux, windows, macos
3636
DeviceType string `json:"deviceType"` // physical, emulator, vm
3737
IsRegistered bool `json:"isRegistered"`
38+
IsConnected bool `json:"isConnected"` // true if device is currently connected to AP
3839
RegId string `json:"regId"`
3940
IsLocal bool `json:"isLocal"` // true if this is the local desktop device
4041
Metadata map[string]interface{} `json:"metadata"` // Device-specific metadata
@@ -187,6 +188,9 @@ func (h *DeviceHandlers) HandleDeviceList(w http.ResponseWriter, r *http.Request
187188
}
188189
}
189190

191+
// Check if device is currently connected to AP
192+
dto.IsConnected = h.serverService.IsDeviceConnected(d.SerialNo)
193+
190194
// Update device info cache with complete device information
191195
h.serverService.UpdateDeviceInfo(&dto)
192196

@@ -296,6 +300,9 @@ func (h *DeviceHandlers) HandleDeviceList(w http.ResponseWriter, r *http.Request
296300
}
297301
}
298302

303+
// Check if desktop device is currently connected to AP
304+
desktopDTO.IsConnected = h.serverService.IsDeviceConnected(desktopDTO.Serialno)
305+
299306
// Update device info cache with complete desktop device information
300307
h.serverService.UpdateDeviceInfo(&desktopDTO)
301308

@@ -1057,8 +1064,8 @@ func (h *DeviceHandlers) HandleDeviceControl(w http.ResponseWriter, req *http.Re
10571064
// HandleDeviceExec executes a shell command on the server, scoped under a device path
10581065
// Path: /api/devices/{serial}/exec
10591066
// Method: POST
1060-
// Body JSON: { "cmd": "echo hello", "timeout_sec": 60 }
1061-
// Response JSON: { stdout, stderr, exit_code, duration_ms }
1067+
// Body JSON: { "cmd": "echo hello", "timeoutSec": 60 }
1068+
// Response JSON: { stdout, stderr, exitCode, durationMs }
10621069
func (h *DeviceHandlers) HandleDeviceExec(w http.ResponseWriter, req *http.Request) {
10631070
// Extract device serial from path
10641071
path := strings.TrimPrefix(req.URL.Path, "/api/devices/")
@@ -1081,7 +1088,7 @@ func (h *DeviceHandlers) HandleDeviceExec(w http.ResponseWriter, req *http.Reque
10811088

10821089
var payload struct {
10831090
Cmd string `json:"cmd"`
1084-
TimeoutSec int `json:"timeout_sec"`
1091+
TimeoutSec int `json:"timeoutSec"`
10851092
}
10861093
decoder := json.NewDecoder(req.Body)
10871094
if err := decoder.Decode(&payload); err != nil {
@@ -1137,11 +1144,11 @@ func (h *DeviceHandlers) HandleDeviceExec(w http.ResponseWriter, req *http.Reque
11371144
}
11381145

11391146
RespondJSON(w, http.StatusOK, map[string]interface{}{
1140-
"device": deviceSerial,
1141-
"stdout": stdoutBuf.String(),
1142-
"stderr": stderrBuf.String(),
1143-
"exit_code": exitCode,
1144-
"duration_ms": duration.Milliseconds(),
1147+
"device": deviceSerial,
1148+
"stdout": stdoutBuf.String(),
1149+
"stderr": stderrBuf.String(),
1150+
"exitCode": exitCode,
1151+
"durationMs": duration.Milliseconds(),
11451152
})
11461153
}
11471154

@@ -1371,9 +1378,9 @@ func (h *DeviceHandlers) handleDeviceConnect(w http.ResponseWriter, r *http.Requ
13711378
}
13721379

13731380
RespondJSON(w, http.StatusOK, map[string]interface{}{
1374-
"success": true,
1375-
"device_id": deviceID,
1376-
"status": "connected",
1381+
"success": true,
1382+
"deviceId": deviceID,
1383+
"status": "connected",
13771384
})
13781385
}
13791386

@@ -1389,9 +1396,9 @@ func (h *DeviceHandlers) handleDeviceDisconnect(w http.ResponseWriter, r *http.R
13891396
}
13901397

13911398
RespondJSON(w, http.StatusOK, map[string]interface{}{
1392-
"success": true,
1393-
"device_id": deviceID,
1394-
"status": "disconnected",
1399+
"success": true,
1400+
"deviceId": deviceID,
1401+
"status": "disconnected",
13951402
})
13961403
}
13971404

packages/cli/internal/server/handlers/interfaces.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ type ServerService interface {
2525

2626
// Static file serving
2727
GetStaticFS() fs.FS
28-
FindLiveViewStaticPath() string
29-
FindStaticPath() string
3028

3129
// Server lifecycle
3230
Stop() error
@@ -41,6 +39,8 @@ type ServerService interface {
4139
GetSerialByDeviceId(deviceId string) string // Gets device serialno by device ID (supports both Android and desktop)
4240
GetDeviceInfo(serial string) interface{} // Returns DeviceDTO or nil
4341
UpdateDeviceInfo(device interface{}) // Accepts DeviceDTO
42+
IsDeviceConnected(serial string) bool // Checks if device is currently connected to AP
43+
ReconnectRegisteredDevices() error // Reconnects all registered devices on server start
4444
}
4545

4646
// Bridge defines the interface for device bridge operations

0 commit comments

Comments
 (0)