Skip to content

Commit b160bd4

Browse files
authored
refactor: cleanups around simulator code (#126)
1 parent 1455138 commit b160bd4

File tree

3 files changed

+93
-88
lines changed

3 files changed

+93
-88
lines changed

devices/android.go

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -111,34 +111,6 @@ func getAdbPath() string {
111111
return "adb"
112112
}
113113

114-
func getAvdManagerPath() string {
115-
sdkPath := getAndroidSdkPath()
116-
if sdkPath != "" {
117-
// try cmdline-tools/latest first
118-
avdPath := filepath.Join(sdkPath, "cmdline-tools", "latest", "bin", "avdmanager")
119-
if runtime.GOOS == "windows" {
120-
avdPath += ".bat"
121-
}
122-
123-
if _, err := os.Stat(avdPath); err == nil {
124-
return avdPath
125-
}
126-
127-
// fallback to tools/bin (older SDK layout)
128-
avdPath = filepath.Join(sdkPath, "tools", "bin", "avdmanager")
129-
if runtime.GOOS == "windows" {
130-
avdPath += ".bat"
131-
}
132-
133-
if _, err := os.Stat(avdPath); err == nil {
134-
return avdPath
135-
}
136-
}
137-
138-
// best effort, look in path
139-
return "avdmanager"
140-
}
141-
142114
func getEmulatorPath() string {
143115
sdkPath := getAndroidSdkPath()
144116
if sdkPath != "" {

devices/simulator.go

Lines changed: 70 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package devices
22

33
import (
4-
"bytes"
5-
"encoding/json"
64
"fmt"
75
"os"
86
"os/exec"
@@ -246,25 +244,15 @@ func UninstallApp(udid string, bundleID string) error {
246244
}
247245

248246
func (s SimulatorDevice) ListInstalledApps() (map[string]interface{}, error) {
249-
// use xcrun simctl
250247
output, err := runSimctl("listapps", s.UDID)
251248
if err != nil {
252249
return nil, fmt.Errorf("failed to list installed apps: %v\n%s", err, output)
253250
}
254251

255-
// convert output to json
256-
cmd := exec.Command("plutil", "-convert", "json", "-o", "-", "-")
257-
cmd.Stdin = bytes.NewReader(output)
258-
output, err = cmd.CombinedOutput()
259-
if err != nil {
260-
return nil, fmt.Errorf("failed to convert output to JSON: %v\n%s", err, output)
261-
}
262-
263-
// parse json
264252
var apps map[string]interface{}
265-
err = json.Unmarshal(output, &apps)
253+
err = utils.ConvertPlistToJSON(output, &apps)
266254
if err != nil {
267-
return nil, fmt.Errorf("failed to parse JSON: %v\n%s", err, output)
255+
return nil, err
268256
}
269257

270258
return apps, nil
@@ -536,17 +524,27 @@ func (s *SimulatorDevice) StartAgent(config StartAgentConfig) error {
536524

537525
if currentPort, err := s.getWdaPort(); err == nil {
538526
// we ran this in the past already (between runs of mobilecli, it's still running on simulator)
527+
528+
// check if we already have a client pointing to the same port
529+
expectedURL := fmt.Sprintf("localhost:%d", currentPort)
530+
if s.wdaClient != nil {
531+
// check if the existing client is already pointing to the same port
532+
if _, err := s.wdaClient.GetStatus(); err == nil {
533+
return nil // already connected to the right port
534+
}
535+
}
536+
539537
utils.Verbose("WebDriverAgent is already running on port %d", currentPort)
540538

541-
// update our instance with new client
542-
s.wdaClient = wda.NewWdaClient(fmt.Sprintf("localhost:%d", currentPort))
539+
// create new client or update with new port
540+
s.wdaClient = wda.NewWdaClient(expectedURL)
543541
if _, err := s.wdaClient.GetStatus(); err == nil {
544542
// double check succeeded
545543
return nil // Already running and accessible
546544
}
547545

548546
// TODO: it's running, but we failed to get status, we might as well kill the process and try again
549-
return err
547+
return fmt.Errorf("WebDriverAgent is running but not accessible on port %d", currentPort)
550548
}
551549

552550
installed, err := s.IsWebDriverAgentInstalled()
@@ -587,8 +585,8 @@ func (s *SimulatorDevice) StartAgent(config StartAgentConfig) error {
587585

588586
webdriverPackageName := "com.facebook.WebDriverAgentRunner.xctrunner"
589587
env := map[string]string{
590-
"MJPEG_SERVER_PORT": strconv.Itoa(mjpegPort),
591588
"USE_PORT": strconv.Itoa(usePort),
589+
"MJPEG_SERVER_PORT": strconv.Itoa(mjpegPort),
592590
}
593591

594592
err = s.LaunchAppWithEnv(webdriverPackageName, env)
@@ -638,41 +636,24 @@ func (s SimulatorDevice) Gesture(actions []wda.TapAction) error {
638636
}
639637

640638
func (s *SimulatorDevice) OpenURL(url string) error {
639+
// #nosec G204 -- udid is controlled, no shell interpretation
641640
return exec.Command("xcrun", "simctl", "openurl", s.ID(), url).Run()
642641
}
643642

644643
func (s *SimulatorDevice) ListApps() ([]InstalledAppInfo, error) {
645-
simctlCmd := exec.Command("xcrun", "simctl", "listapps", s.ID())
646-
plutilCmd := exec.Command("plutil", "-convert", "json", "-o", "-", "-r", "-")
647-
648-
var err error
649-
plutilCmd.Stdin, err = simctlCmd.StdoutPipe()
644+
output, err := runSimctl("listapps", s.ID())
650645
if err != nil {
651-
return nil, fmt.Errorf("failed to create pipe: %w", err)
646+
return nil, fmt.Errorf("failed to list apps: %w\n%s", err, output)
652647
}
653648

654-
var plutilOut bytes.Buffer
655-
plutilCmd.Stdout = &plutilOut
656-
657-
if err := plutilCmd.Start(); err != nil {
658-
return nil, fmt.Errorf("failed to start plutil: %w", err)
659-
}
660-
661-
if err := simctlCmd.Run(); err != nil {
662-
return nil, fmt.Errorf("failed to run simctl: %w", err)
663-
}
664-
665-
if err := plutilCmd.Wait(); err != nil {
666-
return nil, fmt.Errorf("failed to wait for plutil: %w", err)
667-
}
668-
669-
var output map[string]AppInfo
670-
if err := json.Unmarshal(plutilOut.Bytes(), &output); err != nil {
671-
return nil, fmt.Errorf("failed to parse plutil JSON output: %w", err)
649+
var appsMap map[string]AppInfo
650+
err = utils.ConvertPlistToJSON(output, &appsMap)
651+
if err != nil {
652+
return nil, err
672653
}
673654

674655
var apps []InstalledAppInfo
675-
for _, app := range output {
656+
for _, app := range appsMap {
676657
apps = append(apps, InstalledAppInfo{
677658
PackageName: app.CFBundleIdentifier,
678659
AppName: app.CFBundleDisplayName,
@@ -725,33 +706,61 @@ func (s *SimulatorDevice) StartScreenCapture(config ScreenCaptureConfig) error {
725706
return mjpegClient.StartScreenCapture(config.Format, config.OnData)
726707
}
727708

728-
func findWdaProcessForDevice(deviceUDID string) (int, string, error) {
709+
type ProcessInfo struct {
710+
PID int
711+
Command string
712+
}
713+
714+
// listAllProcesses returns a list of all running processes with their PIDs and command info
715+
func listAllProcesses() ([]ProcessInfo, error) {
729716
cmd := exec.Command("/bin/ps", "-o", "pid,command", "-E", "-ww", "-e")
730717
output, err := cmd.Output()
731718
if err != nil {
732-
return 0, "", fmt.Errorf("failed to run ps command: %w", err)
719+
return nil, fmt.Errorf("failed to run ps command: %w", err)
733720
}
734721

735722
lines := strings.Split(string(output), "\n")
736-
devicePath := fmt.Sprintf("/Library/Developer/CoreSimulator/Devices/%s", deviceUDID)
723+
processes := make([]ProcessInfo, 0, len(lines))
737724

738725
for _, line := range lines {
739-
if strings.Contains(line, devicePath) && strings.Contains(line, "WebDriverAgentRunner-Runner") {
740-
// Find the first space to separate PID from the rest
741-
spaceIndex := strings.Index(line, " ")
742-
if spaceIndex == -1 {
743-
continue
744-
}
726+
if line == "" {
727+
continue
728+
}
745729

746-
pidStr := strings.TrimSpace(line[:spaceIndex])
747-
pid, err := strconv.Atoi(pidStr)
748-
if err != nil {
749-
continue
750-
}
730+
// find the first space to separate PID from the rest
731+
spaceIndex := strings.Index(line, " ")
732+
if spaceIndex == -1 {
733+
continue
734+
}
751735

752-
// The rest of the line contains command and environment
753-
processInfo := line[spaceIndex+1:]
754-
return pid, processInfo, nil
736+
pidStr := strings.TrimSpace(line[:spaceIndex])
737+
pid, err := strconv.Atoi(pidStr)
738+
if err != nil {
739+
continue
740+
}
741+
742+
// the rest of the line contains command and environment
743+
command := line[spaceIndex+1:]
744+
processes = append(processes, ProcessInfo{
745+
PID: pid,
746+
Command: command,
747+
})
748+
}
749+
750+
return processes, nil
751+
}
752+
753+
func findWdaProcessForDevice(deviceUDID string) (int, string, error) {
754+
processes, err := listAllProcesses()
755+
if err != nil {
756+
return 0, "", err
757+
}
758+
759+
devicePath := fmt.Sprintf("/Library/Developer/CoreSimulator/Devices/%s", deviceUDID)
760+
761+
for _, proc := range processes {
762+
if strings.Contains(proc.Command, devicePath) && strings.Contains(proc.Command, "WebDriverAgentRunner-Runner") {
763+
return proc.PID, proc.Command, nil
755764
}
756765
}
757766

@@ -838,6 +847,7 @@ func (s SimulatorDevice) InstallApp(path string) error {
838847
if err != nil {
839848
return fmt.Errorf("failed to unzip: %v", err)
840849
}
850+
841851
defer func() { _ = os.RemoveAll(tmpDir) }()
842852

843853
entries, err := os.ReadDir(tmpDir)

utils/plist.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package utils
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"fmt"
57
"os/exec"
68
)
@@ -45,3 +47,24 @@ func AddBundleIconFilesToPlist(plistPath string) error {
4547

4648
return nil
4749
}
50+
51+
// ConvertPlistToJSON converts plist bytes to JSON and unmarshals into the provided result
52+
func ConvertPlistToJSON(plistData []byte, result interface{}) error {
53+
cmd := exec.Command("plutil", "-convert", "json", "-o", "-", "-")
54+
cmd.Stdin = bytes.NewReader(plistData)
55+
56+
var stdout, stderr bytes.Buffer
57+
cmd.Stdout = &stdout
58+
cmd.Stderr = &stderr
59+
err := cmd.Run()
60+
if err != nil {
61+
return fmt.Errorf("failed to convert plist to JSON: %w\n%s", err, stderr.String())
62+
}
63+
64+
err = json.Unmarshal(stdout.Bytes(), result)
65+
if err != nil {
66+
return fmt.Errorf("failed to parse JSON: %w", err)
67+
}
68+
69+
return nil
70+
}

0 commit comments

Comments
 (0)