|
1 | 1 | package devices |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "bytes" |
5 | | - "encoding/json" |
6 | 4 | "fmt" |
7 | 5 | "os" |
8 | 6 | "os/exec" |
@@ -246,25 +244,15 @@ func UninstallApp(udid string, bundleID string) error { |
246 | 244 | } |
247 | 245 |
|
248 | 246 | func (s SimulatorDevice) ListInstalledApps() (map[string]interface{}, error) { |
249 | | - // use xcrun simctl |
250 | 247 | output, err := runSimctl("listapps", s.UDID) |
251 | 248 | if err != nil { |
252 | 249 | return nil, fmt.Errorf("failed to list installed apps: %v\n%s", err, output) |
253 | 250 | } |
254 | 251 |
|
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 |
264 | 252 | var apps map[string]interface{} |
265 | | - err = json.Unmarshal(output, &apps) |
| 253 | + err = utils.ConvertPlistToJSON(output, &apps) |
266 | 254 | if err != nil { |
267 | | - return nil, fmt.Errorf("failed to parse JSON: %v\n%s", err, output) |
| 255 | + return nil, err |
268 | 256 | } |
269 | 257 |
|
270 | 258 | return apps, nil |
@@ -536,17 +524,27 @@ func (s *SimulatorDevice) StartAgent(config StartAgentConfig) error { |
536 | 524 |
|
537 | 525 | if currentPort, err := s.getWdaPort(); err == nil { |
538 | 526 | // 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 | + |
539 | 537 | utils.Verbose("WebDriverAgent is already running on port %d", currentPort) |
540 | 538 |
|
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) |
543 | 541 | if _, err := s.wdaClient.GetStatus(); err == nil { |
544 | 542 | // double check succeeded |
545 | 543 | return nil // Already running and accessible |
546 | 544 | } |
547 | 545 |
|
548 | 546 | // 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) |
550 | 548 | } |
551 | 549 |
|
552 | 550 | installed, err := s.IsWebDriverAgentInstalled() |
@@ -587,8 +585,8 @@ func (s *SimulatorDevice) StartAgent(config StartAgentConfig) error { |
587 | 585 |
|
588 | 586 | webdriverPackageName := "com.facebook.WebDriverAgentRunner.xctrunner" |
589 | 587 | env := map[string]string{ |
590 | | - "MJPEG_SERVER_PORT": strconv.Itoa(mjpegPort), |
591 | 588 | "USE_PORT": strconv.Itoa(usePort), |
| 589 | + "MJPEG_SERVER_PORT": strconv.Itoa(mjpegPort), |
592 | 590 | } |
593 | 591 |
|
594 | 592 | err = s.LaunchAppWithEnv(webdriverPackageName, env) |
@@ -638,41 +636,24 @@ func (s SimulatorDevice) Gesture(actions []wda.TapAction) error { |
638 | 636 | } |
639 | 637 |
|
640 | 638 | func (s *SimulatorDevice) OpenURL(url string) error { |
| 639 | + // #nosec G204 -- udid is controlled, no shell interpretation |
641 | 640 | return exec.Command("xcrun", "simctl", "openurl", s.ID(), url).Run() |
642 | 641 | } |
643 | 642 |
|
644 | 643 | 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()) |
650 | 645 | 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) |
652 | 647 | } |
653 | 648 |
|
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 |
672 | 653 | } |
673 | 654 |
|
674 | 655 | var apps []InstalledAppInfo |
675 | | - for _, app := range output { |
| 656 | + for _, app := range appsMap { |
676 | 657 | apps = append(apps, InstalledAppInfo{ |
677 | 658 | PackageName: app.CFBundleIdentifier, |
678 | 659 | AppName: app.CFBundleDisplayName, |
@@ -725,33 +706,61 @@ func (s *SimulatorDevice) StartScreenCapture(config ScreenCaptureConfig) error { |
725 | 706 | return mjpegClient.StartScreenCapture(config.Format, config.OnData) |
726 | 707 | } |
727 | 708 |
|
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) { |
729 | 716 | cmd := exec.Command("/bin/ps", "-o", "pid,command", "-E", "-ww", "-e") |
730 | 717 | output, err := cmd.Output() |
731 | 718 | 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) |
733 | 720 | } |
734 | 721 |
|
735 | 722 | lines := strings.Split(string(output), "\n") |
736 | | - devicePath := fmt.Sprintf("/Library/Developer/CoreSimulator/Devices/%s", deviceUDID) |
| 723 | + processes := make([]ProcessInfo, 0, len(lines)) |
737 | 724 |
|
738 | 725 | 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 | + } |
745 | 729 |
|
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 | + } |
751 | 735 |
|
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 |
755 | 764 | } |
756 | 765 | } |
757 | 766 |
|
@@ -838,6 +847,7 @@ func (s SimulatorDevice) InstallApp(path string) error { |
838 | 847 | if err != nil { |
839 | 848 | return fmt.Errorf("failed to unzip: %v", err) |
840 | 849 | } |
| 850 | + |
841 | 851 | defer func() { _ = os.RemoveAll(tmpDir) }() |
842 | 852 |
|
843 | 853 | entries, err := os.ReadDir(tmpDir) |
|
0 commit comments