Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build:

test:
go test ./... -v -race
(cd test; npm test)
(cd test && npm test)

lint:
$(GOPATH)/bin/golangci-lint run
Expand Down
3 changes: 2 additions & 1 deletion devices/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func GetAllControllableDevices() ([]ControllableDevice, error) {
errs = append(errs, fmt.Errorf("ios real: %w", err))
} else {
for _, device := range iosDevices {
allDevices = append(allDevices, device)
allDevices = append(allDevices, &device)
}
}

Expand All @@ -63,6 +63,7 @@ func GetAllControllableDevices() ([]ControllableDevice, error) {
for _, sim := range sims {
allDevices = append(allDevices, &SimulatorDevice{
Simulator: sim,
wdaClient: wda.NewWdaClient("localhost:8100"),
})
}
}
Expand Down
209 changes: 190 additions & 19 deletions devices/ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ import (
"fmt"
"os/exec"
"strings"
"time"

"github.com/mobile-next/mobilecli/devices/ios"
"github.com/mobile-next/mobilecli/devices/wda"
"github.com/mobile-next/mobilecli/utils"
)

var (
portForwarder *ios.PortForwarder
)

type IOSDevice struct {
Udid string `json:"UniqueDeviceID"`
DeviceName string `json:"DeviceName"`

tunnelManager *ios.TunnelManager
wdaClient *wda.WdaClient
}

type listDevicesResponse struct {
Expand All @@ -35,18 +45,14 @@ func (d IOSDevice) DeviceType() string {
}

func runGoIosCommand(args ...string) ([]byte, error) {
cmd := exec.Command("go-ios", args...)
output, err := cmd.Output()
cmdName, err := findGoIosPath()
if err != nil {
// try with "ios" (if installed through brew)
cmd = exec.Command("ios", args...)
output, err = cmd.Output()
if err != nil {
return nil, fmt.Errorf("go-ios command failed: %w", err)
}
return nil, fmt.Errorf("failed to find go-ios path: %w", err)
}

return output, nil
cmd := exec.Command(cmdName, args...)
output, err := cmd.Output()
return output, err
}

func getDeviceInfo(udid string) (IOSDevice, error) {
Expand All @@ -61,6 +67,8 @@ func getDeviceInfo(udid string) (IOSDevice, error) {
return IOSDevice{}, err
}

device.tunnelManager = ios.NewTunnelManager(udid)
device.wdaClient = wda.NewWdaClient("localhost:8100")
return device, nil
}

Expand Down Expand Up @@ -89,7 +97,7 @@ func ListIOSDevices() ([]IOSDevice, error) {
}

func (d IOSDevice) TakeScreenshot() ([]byte, error) {
return wda.TakeScreenshot()
return d.wdaClient.TakeScreenshot()
}

func (d IOSDevice) Reboot() error {
Expand All @@ -98,20 +106,174 @@ func (d IOSDevice) Reboot() error {
}

func (d IOSDevice) Tap(x, y int) error {
return wda.Tap(x, y)
return d.wdaClient.Tap(x, y)
}

func (d IOSDevice) Gesture(actions []wda.TapAction) error {
return wda.Gesture(actions)
return d.wdaClient.Gesture(actions)
}

type Tunnel struct {
Address string `json:"address"`
RsdPort int `json:"rsdPort"`
UDID string `json:"udid"`
UserspaceTun bool `json:"userspaceTun"`
UserspaceTunPort int `json:"userspaceTunPort"`
}

func (d IOSDevice) ListTunnels() ([]Tunnel, error) {
output, err := runGoIosCommand("tunnel", "ls", "--udid", d.ID())
if err != nil {
// if no tunnels found, go-ios might return err 1
return []Tunnel{}, nil
}

var tunnels []Tunnel
err = json.Unmarshal(output, &tunnels)
if err != nil {
return nil, fmt.Errorf("failed to parse tunnel list: %w", err)
}

return tunnels, nil
}

func (d IOSDevice) StartAgent() error {
_, err := wda.GetWebDriverAgentStatus()
func findGoIosPath() (string, error) {
if path, err := exec.LookPath("go-ios"); err == nil {
return path, nil
}

if path, err := exec.LookPath("ios"); err == nil {
return path, nil
}

return "", fmt.Errorf("neither go-ios nor ios found in PATH")
}

func (d *IOSDevice) StartTunnel() error {
return d.tunnelManager.StartTunnel()
}

func (d *IOSDevice) StartTunnelWithCallback(onProcessDied func(error)) error {
return d.tunnelManager.StartTunnelWithCallback(onProcessDied)
}

func (d *IOSDevice) StopTunnel() error {
return d.tunnelManager.StopTunnel()
}

func (d *IOSDevice) GetTunnelPID() int {
return d.tunnelManager.GetTunnelPID()
}

func (d *IOSDevice) StartAgent() error {

// starting an agent on a real device requires quite a few things to happen in the right order:
// 1. we check if agent is installed on device (with custom bundle identifier). if we don't have it, this is the process:
// a. we download the wda bundle from github
// b. we need to unzip it to a temp directory
// c. we need to modify the Info.plist to set the correct bundle identifier
// d. we need to create an entitlements file
// e. we need to sign the bundle
// f. we need to install the bundle to the device
// 2. we need to launch the agent ✅
// 3. we need to make sure there's a tunnel running for iOS17+
// 4. we need to set up a forward proxy to port 8100 on the device
// 5. we need to wait for the agent to be ready

_, err := d.wdaClient.GetStatus()
if err != nil {
utils.Verbose("WebdriverAgent is not running, starting it")

// list apps on device
apps, err := d.ListApps()
if err != nil {
return fmt.Errorf("failed to list apps: %w", err)
}

// check if WebDriverAgent is installed
webdriverBundleId := ""
for _, app := range apps {
if app.AppName == "WebDriverAgentRunner-Runner" {
utils.Verbose("WebDriverAgent is installed, launching it")
webdriverBundleId = app.PackageName
break
}
}

if webdriverBundleId == "" {
return fmt.Errorf("WebDriverAgent is not installed")
}

// check if tunnel is running
tunnels, err := d.ListTunnels()
if err != nil {
return fmt.Errorf("failed to list tunnels: %w", err)
}

if len(tunnels) > 0 {
utils.Verbose("Tunnels available for this device: %v", tunnels)
}

if len(tunnels) == 0 {
utils.Verbose("No tunnels found, starting a new tunnel")
err = d.StartTunnel()
if err != nil {
return fmt.Errorf("failed to start tunnel: %w", err)
}

time.Sleep(1 * time.Second)
}

// check that forward proxy is running
port, err := findAvailablePort()
if err != nil {
return fmt.Errorf("failed to find available port: %w", err)
}

portForwarder = ios.NewPortForwarder(d.ID())
err = portForwarder.Forward(port, 8100)
if err != nil {
return fmt.Errorf("failed to forward port: %w", err)
}

d.wdaClient = wda.NewWdaClient(fmt.Sprintf("http://localhost:%d", port))

// check if wda is already running, now that we have a port forwarder set up
_, err = d.wdaClient.GetStatus()
if err == nil {
utils.Verbose("WebDriverAgent is already running")
}

if err != nil {
// launch WebDriverAgent
utils.Verbose("Launching WebDriverAgent")
err = d.LaunchApp(webdriverBundleId)
if err != nil {
return fmt.Errorf("failed to launch WebDriverAgent: %w", err)
}

// wait for WebDriverAgent to start
utils.Verbose("Waiting for WebDriverAgent to start")
err = d.wdaClient.WaitForAgent()
if err != nil {
return fmt.Errorf("failed to wait for WebDriverAgent: %w", err)
}

// wait 1 second after pressing home, so we make sure wda is in the background
d.wdaClient.PressButton("HOME")
time.Sleep(1 * time.Second)

utils.Verbose("WebDriverAgent started")
}

return nil
}

return err
}

func (d IOSDevice) PressButton(key string) error {
return wda.PressButton(key)
return d.wdaClient.PressButton(key)
}

func (d IOSDevice) LaunchApp(bundleID string) error {
Expand All @@ -125,11 +287,11 @@ func (d IOSDevice) TerminateApp(bundleID string) error {
}

func (d IOSDevice) SendKeys(text string) error {
return wda.SendKeys(text)
return d.wdaClient.SendKeys(text)
}

func (d IOSDevice) OpenURL(url string) error {
return wda.OpenURL(url)
return d.wdaClient.OpenURL(url)
}

func (d IOSDevice) ListApps() ([]InstalledAppInfo, error) {
Expand Down Expand Up @@ -164,7 +326,7 @@ func (d IOSDevice) ListApps() ([]InstalledAppInfo, error) {
}

func (d IOSDevice) Info() (*FullDeviceInfo, error) {
wdaSize, err := wda.GetWindowSize()
wdaSize, err := d.wdaClient.GetWindowSize()
if err != nil {
return nil, fmt.Errorf("failed to get window size from WDA: %w", err)
}
Expand All @@ -185,5 +347,14 @@ func (d IOSDevice) Info() (*FullDeviceInfo, error) {
}

func (d IOSDevice) StartScreenCapture(format string, callback func([]byte) bool) error {
return wda.StartScreenCapture(format, callback)
return d.wdaClient.StartScreenCapture(format, callback)
}

func findAvailablePort() (int, error) {
for port := 8100; port <= 8199; port++ {
if utils.IsPortAvailable("localhost", port) {
return port, nil
}
}
return 0, fmt.Errorf("no available ports found in range 8101-8199")
}
Loading
Loading