Skip to content
Open
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
85 changes: 81 additions & 4 deletions devices/ios.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package devices

import (
"encoding/binary"
"encoding/json"
"context"
"errors"
"fmt"
Expand All @@ -10,6 +12,7 @@
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"

Expand Down Expand Up @@ -45,9 +48,11 @@
DeviceName string `json:"DeviceName"`
OSVersion string `json:"Version"`

tunnelManager *ios.TunnelManager
wdaClient *wda.WdaClient
mjpegClient *mjpeg.WdaMjpegClient
tunnelManager *ios.TunnelManager
wdaClient *wda.WdaClient
mjpegClient *mjpeg.WdaMjpegClient
screenCaptureConn net.Conn
screenCaptureConnLock sync.Mutex
}

func (d IOSDevice) ID() string {
Expand Down Expand Up @@ -713,6 +718,11 @@
return fmt.Errorf("failed to connect to stream port: %w", err)
}

// store connection for configuration updates
d.screenCaptureConnLock.Lock()
d.screenCaptureConn = conn
d.screenCaptureConnLock.Unlock()

// setup signal handling for Ctrl+C
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
Expand All @@ -722,7 +732,12 @@

// stream data in a goroutine
go func() {
defer conn.Close()
defer func() {
conn.Close()

Check failure on line 736 in devices/ios.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `conn.Close` is not checked (errcheck)
d.screenCaptureConnLock.Lock()
d.screenCaptureConn = nil
d.screenCaptureConnLock.Unlock()
}()
Comment on lines +735 to +740
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and look at the specified lines
git ls-files | grep -E "devices/ios\.go"

Repository: mobile-next/mobilecli

Length of output: 79


🏁 Script executed:

# Read the file around the specified lines (735-740)
if [ -f "devices/ios.go" ]; then
  wc -l devices/ios.go
  sed -n '730,745p' devices/ios.go
fi

Repository: mobile-next/mobilecli

Length of output: 469


🏁 Script executed:

# Check if linter configuration exists
git ls-files | grep -E "(\.golangci|\.eslintrc|linter|lint)" | head -20

Repository: mobile-next/mobilecli

Length of output: 78


🏁 Script executed:

# Search for utils.Verbose function
find . -type f -name "*.go" | head -20 | xargs grep -l "func.*Verbose\|utils.Verbose" 2>/dev/null | head -5

Repository: mobile-next/mobilecli

Length of output: 84


🏁 Script executed:

# Check for any errcheck configuration
grep -r "errcheck" . --include="*.yml" --include="*.yaml" --include="*.toml" --include="*.cfg" 2>/dev/null | head -10

Repository: mobile-next/mobilecli

Length of output: 158


🏁 Script executed:

# Get exact lines 735-740
sed -n '735,740p' devices/ios.go

Repository: mobile-next/mobilecli

Length of output: 208


🏁 Script executed:

# Check the golangci.yml errcheck configuration
cat .golangci.yml | grep -A 5 "errcheck:"

Repository: mobile-next/mobilecli

Length of output: 156


🏁 Script executed:

# Find utils package and Verbose function
find . -type f -name "*.go" -path "*/utils/*" | head -10

Repository: mobile-next/mobilecli

Length of output: 270


🏁 Script executed:

# Search for utils.Verbose function definition
rg "func.*Verbose" --type go -A 2

Repository: mobile-next/mobilecli

Length of output: 423


🏁 Script executed:

# Check what type conn is by looking at context around the defer
sed -n '715,740p' devices/ios.go

Repository: mobile-next/mobilecli

Length of output: 817


🏁 Script executed:

# Search for Close() error handling patterns in Go files
rg "Close\(\)" --type go -A 1 -B 1 | head -50

Repository: mobile-next/mobilecli

Length of output: 1492


🏁 Script executed:

# Look for conn.Close() specifically
rg "conn\.Close\(\)" --type go -A 1 -B 1

Repository: mobile-next/mobilecli

Length of output: 303


🏁 Script executed:

# Check how errors are handled in defer blocks
rg "defer.*Close" --type go -A 2 | head -60

Repository: mobile-next/mobilecli

Length of output: 2134


🏁 Script executed:

# Check project conventions for handling errors in defer
rg "_.*:=" --type go | grep -i close | head -20

Repository: mobile-next/mobilecli

Length of output: 47


Fix conn.Close() error handling to match project conventions.

Line 736 ignores the error from conn.Close(), which fails lint (errcheck). The project standard for handling Close() errors in defer blocks is to use blank assignment, not logging. Apply this throughout the defer block.

🔧 Suggested fix
 		defer func() {
-			conn.Close()
+			_ = conn.Close()
 			d.screenCaptureConnLock.Lock()
 			d.screenCaptureConn = nil
 			d.screenCaptureConnLock.Unlock()
 		}()

Additionally, line 762 has an identical unhandled conn.Close() that should be fixed the same way.

🧰 Tools
🪛 GitHub Check: lint

[failure] 736-736:
Error return value of conn.Close is not checked (errcheck)

🤖 Prompt for AI Agents
In `@devices/ios.go` around lines 735 - 740, The defer that currently calls
conn.Close() and then clears d.screenCaptureConn should handle the Close error
per project convention by discarding it (use blank assignment) rather than
ignoring it; update the defer closure that references conn,
d.screenCaptureConnLock, and d.screenCaptureConn to call the close as a blank
assignment (e.g., _ = conn.Close()), and make the same change for the other
identical conn.Close() occurrence related to screen capture teardown that also
manipulates d.screenCaptureConn or uses conn in a defer.

buffer := make([]byte, 65536)
for {
n, err := conn.Read(buffer)
Expand All @@ -748,7 +763,7 @@
// wait for either signal or stream completion
select {
case <-sigChan:
conn.Close()

Check failure on line 766 in devices/ios.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `conn.Close` is not checked (errcheck)
utils.Verbose("stream closed by user")
return nil
case err := <-done:
Expand All @@ -775,6 +790,68 @@
return d.mjpegClient.StartScreenCapture(config.Format, config.OnData)
}

// SendScreenCaptureConfiguration sends encoder configuration updates to the device over the TCP stream.
//
// This method sends a length-prefixed JSON-RPC message to update the H.264 encoder
// bitrate and optionally frame rate dynamically without restarting the stream.
//
// Parameters:
// - bitrate: Target bitrate in bits per second (100000 - 8000000)
// - frameRate: Optional target frame rate (nil to keep current, 1-60 if provided)
//
// The message format is:
// [4-byte big-endian length][JSON-RPC payload]
//
// Returns an error if:
// - Screen capture is not active
// - JSON marshaling fails
// - TCP write fails
func (d *IOSDevice) SendScreenCaptureConfiguration(bitrate int, frameRate *int) error {
// Check if screen capture is active (TCP connection exists)
d.screenCaptureConnLock.Lock()
conn := d.screenCaptureConn
d.screenCaptureConnLock.Unlock()

if conn == nil {
return fmt.Errorf("screen capture not active, start screencapture first")
}

// Build JSON-RPC request
request := map[string]interface{}{
"jsonrpc": "2.0",
"method": "screencapture.setConfiguration",
"params": map[string]interface{}{
"bitrate": bitrate,
"frameRate": frameRate,
},
"id": 1,
}

jsonData, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}

// Prepend 4-byte length (big-endian)
length := uint32(len(jsonData))
lengthBytes := make([]byte, 4)
binary.BigEndian.PutUint32(lengthBytes, length)

// Send: [4-byte length][JSON payload]
message := append(lengthBytes, jsonData...)

d.screenCaptureConnLock.Lock()
_, err = conn.Write(message)
d.screenCaptureConnLock.Unlock()

if err != nil {
return fmt.Errorf("failed to send configuration: %w", err)
}

utils.Verbose("Sent screen capture configuration: bitrate=%d bps, frameRate=%v", bitrate, frameRate)
return nil
}

func (d IOSDevice) DumpSource() ([]ScreenElement, error) {
return d.wdaClient.GetSourceElements()
}
Expand Down
39 changes: 20 additions & 19 deletions server/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,26 @@ type HandlerFunc func(params json.RawMessage) (interface{}, error)
// This is used by both the HTTP server and embedded clients
func GetMethodRegistry() map[string]HandlerFunc {
return map[string]HandlerFunc{
"devices": handleDevicesList,
"screenshot": handleScreenshot,
"io_tap": handleIoTap,
"io_longpress": handleIoLongPress,
"io_text": handleIoText,
"io_button": handleIoButton,
"io_swipe": handleIoSwipe,
"io_gesture": handleIoGesture,
"url": handleURL,
"device_info": handleDeviceInfo,
"io_orientation_get": handleIoOrientationGet,
"io_orientation_set": handleIoOrientationSet,
"device_boot": handleDeviceBoot,
"device_shutdown": handleDeviceShutdown,
"device_reboot": handleDeviceReboot,
"dump_ui": handleDumpUI,
"apps_launch": handleAppsLaunch,
"apps_terminate": handleAppsTerminate,
"apps_list": handleAppsList,
"devices": handleDevicesList,
"screenshot": handleScreenshot,
"screencapture.setConfiguration": handleScreenCaptureSetConfiguration,
"io_tap": handleIoTap,
"io_longpress": handleIoLongPress,
"io_text": handleIoText,
"io_button": handleIoButton,
"io_swipe": handleIoSwipe,
"io_gesture": handleIoGesture,
"url": handleURL,
"device_info": handleDeviceInfo,
"io_orientation_get": handleIoOrientationGet,
"io_orientation_set": handleIoOrientationSet,
"device_boot": handleDeviceBoot,
"device_shutdown": handleDeviceShutdown,
"device_reboot": handleDeviceReboot,
"dump_ui": handleDumpUI,
"apps_launch": handleAppsLaunch,
"apps_terminate": handleAppsTerminate,
"apps_list": handleAppsList,
}
}

Expand Down
65 changes: 65 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const (
IdleTimeout = 120 * time.Second
)

const (
ScreenCaptureMinBitrate = 100000
ScreenCaptureMaxBitrate = 8000000
)

var okResponse = map[string]interface{}{"status": "ok"}

type JSONRPCRequest struct {
Expand Down Expand Up @@ -292,6 +297,66 @@ func handleScreenshot(params json.RawMessage) (interface{}, error) {
return nil, fmt.Errorf("unexpected response format")
}

type ScreenCaptureSetConfigurationParams struct {
DeviceID string `json:"deviceId"`
Bitrate int `json:"bitrate"`
FrameRate *int `json:"frameRate,omitempty"`
}

type ScreenCaptureSetConfigurationResponse struct {
Success bool `json:"success"`
DeviceID string `json:"deviceId"`
Bitrate int `json:"bitrate"`
FrameRate *int `json:"frameRate,omitempty"`
}

func handleScreenCaptureSetConfiguration(params json.RawMessage) (interface{}, error) {
var req ScreenCaptureSetConfigurationParams
if err := json.Unmarshal(params, &req); err != nil {
return nil, fmt.Errorf("invalid parameters: %v", err)
}

// Validate bitrate range (100 kbps to 8 Mbps)
if req.Bitrate < ScreenCaptureMinBitrate || req.Bitrate > ScreenCaptureMaxBitrate {
return nil, fmt.Errorf("bitrate must be between %d and %d bps", ScreenCaptureMinBitrate, ScreenCaptureMaxBitrate)
}

// Validate frame rate if provided
if req.FrameRate != nil {
if *req.FrameRate < 1 || *req.FrameRate > 60 {
return nil, fmt.Errorf("frame rate must be between 1 and 60")
}
}

targetDevice, err := commands.FindDeviceOrAutoSelect(req.DeviceID)
if err != nil {
return nil, fmt.Errorf("error finding device: %w", err)
}

// Only iOS real devices support this
if targetDevice.Platform() != "ios" || targetDevice.DeviceType() != "real" {
return nil, fmt.Errorf("screencapture.setConfiguration only supported on real iOS devices")
}

iosDevice, ok := targetDevice.(*devices.IOSDevice)
if !ok {
return nil, fmt.Errorf("device is not an iOS device")
}

// Send to device over TCP socket
err = iosDevice.SendScreenCaptureConfiguration(req.Bitrate, req.FrameRate)
if err != nil {
return nil, fmt.Errorf("failed to update configuration: %w", err)
}

return ScreenCaptureSetConfigurationResponse{
Success: true,
DeviceID: req.DeviceID,
Bitrate: req.Bitrate,
FrameRate: req.FrameRate,
}, nil
}

type IoTapParams struct {
DeviceID string `json:"deviceId"`
X int `json:"x"`
Expand Down
Loading