diff --git a/SimVirtualLocation.xcodeproj/project.pbxproj b/SimVirtualLocation.xcodeproj/project.pbxproj index a8f759d..447074a 100644 --- a/SimVirtualLocation.xcodeproj/project.pbxproj +++ b/SimVirtualLocation.xcodeproj/project.pbxproj @@ -383,7 +383,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 3.5.0; + MARKETING_VERSION = 3.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.devnex.SimVirtualLocation; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -413,7 +413,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 3.5.0; + MARKETING_VERSION = 3.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.devnex.SimVirtualLocation; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/SimVirtualLocation/Logic/LocationController.swift b/SimVirtualLocation/Logic/LocationController.swift index 233ccef..896fdf9 100644 --- a/SimVirtualLocation/Logic/LocationController.swift +++ b/SimVirtualLocation/Logic/LocationController.swift @@ -121,29 +121,28 @@ class LocationController: NSObject, ObservableObject, MKMapViewDelegate, CLLocat mapView.mkMapView.delegate = self mapView.viewHolder.clickAction = handleMapClick - refreshDevices() - - deviceType = defaults.integer(forKey: "device_type") - adbPath = defaults.string(forKey: "adb_path") ?? "" - adbDeviceId = defaults.string(forKey: "adb_device_id") ?? "" - isEmulator = defaults.bool(forKey: "is_emulator") - xcodePath = defaults.string(forKey: Constants.defaultsXcodePathKey) ?? "/Applications/Xcode.app" + Task { @MainActor in + await refreshDevices() + + deviceType = defaults.integer(forKey: "device_type") + adbPath = defaults.string(forKey: "adb_path") ?? "" + adbDeviceId = defaults.string(forKey: "adb_device_id") ?? "" + isEmulator = defaults.bool(forKey: "is_emulator") + xcodePath = defaults.string(forKey: Constants.defaultsXcodePathKey) ?? "/Applications/Xcode.app" - loadLocations() + loadLocations() + } } // MARK: - Public - func refreshDevices() { + @MainActor + func refreshDevices() async { bootedSimulators = (try? getBootedSimulators()) ?? [] selectedSimulator = bootedSimulators.first?.id ?? "" - do { - connectedDevices = try getConnectedDevices() - selectedDevice = connectedDevices.first?.id ?? "" - } catch { - showAlert(error.localizedDescription) - } + connectedDevices = (try? await getConnectedDevices()) ?? [] + selectedDevice = connectedDevices.first?.id ?? "" } func setCurrentLocation() { @@ -393,86 +392,92 @@ class LocationController: NSObject, ObservableObject, MKMapViewDelegate, CLLocat return } - let mountTask = runner.taskForIOS( - args: [ - "mounter", - "mount-developer", - "--udid", - device.id, - makeDeveloperImageDmgPath(iOSVersion: device.version), - makeDeveloperImageSignaturePath(iOSVersion: device.version) - ] - ) + Task { @MainActor in + let mountTask = try await runner.taskForIOS( + args: [ + "mounter", + "mount-developer", + "--udid", + device.id, + makeDeveloperImageDmgPath(iOSVersion: device.version), + makeDeveloperImageSignaturePath(iOSVersion: device.version) + ], + showAlert: showAlert + ) - let pipe = Pipe() - mountTask.standardOutput = pipe + let pipe = Pipe() + mountTask.standardOutput = pipe - let errorPipe = Pipe() - mountTask.standardError = errorPipe + let errorPipe = Pipe() + mountTask.standardError = errorPipe - do { - try mountTask.run() - mountTask.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - pipe.fileHandleForReading.closeFile() - - if - let errorData = try? errorPipe.fileHandleForReading.readToEnd(), - let errorText = String(data: errorData, encoding: .utf8), - !errorText.isEmpty { - if errorText.range(of: "{'Error': 'DeviceLocked'}") != nil { - showAlert("Error: Device is locked") - } else { - showAlert(errorText) + do { + try mountTask.run() + mountTask.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + pipe.fileHandleForReading.closeFile() + + if + let errorData = try? errorPipe.fileHandleForReading.readToEnd(), + let errorText = String(data: errorData, encoding: .utf8), + !errorText.isEmpty { + if errorText.range(of: "{'Error': 'DeviceLocked'}") != nil { + showAlert("Error: Device is locked") + } else { + showAlert(errorText) + } } - } - if let text = String(data: data, encoding: .utf8), !text.isEmpty { - showAlert(text) + if let text = String(data: data, encoding: .utf8), !text.isEmpty { + showAlert(text) + } + } catch { + showAlert(error.localizedDescription) } - } catch { - showAlert(error.localizedDescription) } } func unmountDeveloperImage() { - let mountTask = runner.taskForIOS( - args: [ - "mounter", - "umount-developer" - ] - ) + Task { @MainActor in + let mountTask = try await runner.taskForIOS( + args: [ + "mounter", + "umount-developer" + ], + showAlert: showAlert + ) - let pipe = Pipe() - mountTask.standardOutput = pipe + let pipe = Pipe() + mountTask.standardOutput = pipe - let errorPipe = Pipe() - mountTask.standardError = errorPipe + let errorPipe = Pipe() + mountTask.standardError = errorPipe - do { - try mountTask.run() - mountTask.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - pipe.fileHandleForReading.closeFile() - - if - let errorData = try? errorPipe.fileHandleForReading.readToEnd(), - let errorText = String(data: errorData, encoding: .utf8), - !errorText.isEmpty { - if errorText.range(of: "{'Error': 'DeviceLocked'}") != nil { - showAlert("Error: Device is locked") - } else { - showAlert(errorText) + do { + try mountTask.run() + mountTask.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + pipe.fileHandleForReading.closeFile() + + if + let errorData = try? errorPipe.fileHandleForReading.readToEnd(), + let errorText = String(data: errorData, encoding: .utf8), + !errorText.isEmpty { + if errorText.range(of: "{'Error': 'DeviceLocked'}") != nil { + showAlert("Error: Device is locked") + } else { + showAlert(errorText) + } } - } - if let text = String(data: data, encoding: .utf8), !text.isEmpty { - showAlert(text) + if let text = String(data: data, encoding: .utf8), !text.isEmpty { + showAlert(text) + } + } catch { + showAlert(error.localizedDescription) } - } catch { - showAlert(error.localizedDescription) } } @@ -749,17 +754,22 @@ class LocationController: NSObject, ObservableObject, MKMapViewDelegate, CLLocat } if deviceMode == .device { if useRSD { - runner.runOnNewIos( - location: location, - RSDAddress: RSDAddress, - RSDPort: RSDPort, - showAlert: showAlert - ) + Task { + try await runner.runOnNewIos( + location: location, + RSDAddress: RSDAddress, + RSDPort: RSDPort, + showAlert: showAlert + ) + } + } else { - runner.runOnIos( - location: location, - showAlert: showAlert - ) + Task { + try await runner.runOnIos( + location: location, + showAlert: showAlert + ) + } } } else { if bootedSimulators.isEmpty { @@ -832,8 +842,9 @@ class LocationController: NSObject, ObservableObject, MKMapViewDelegate, CLLocat private extension LocationController { - private func getConnectedDevices() throws -> [Device] { - let task = runner.taskForIOS(args: ["usbmux", "list", "--no-color", "-u"]) + @MainActor + private func getConnectedDevices() async throws -> [Device] { + let task = try await runner.taskForIOS(args: ["usbmux", "list", "--no-color", "-u"], showAlert: showAlert) log("getConnectedDevices: \(task.executableURL!.absoluteString) \(task.arguments!.joined(separator: " "))") diff --git a/SimVirtualLocation/Logic/Runner.swift b/SimVirtualLocation/Logic/Runner.swift index 8634715..64e3e35 100644 --- a/SimVirtualLocation/Logic/Runner.swift +++ b/SimVirtualLocation/Logic/Runner.swift @@ -14,6 +14,7 @@ class Runner { var timeDelay: TimeInterval = 0.5 var log: ((String) -> Void)? + var pymobiledevicePath: String? // MARK: - Private Properties @@ -54,60 +55,59 @@ class Runner { func runOnIos( location: CLLocationCoordinate2D, showAlert: @escaping (String) -> Void - ) { + ) async throws { self.isStopped = false - executionQueue.async { - guard !self.isStopped else { - return - } + guard !self.isStopped else { + return + } - let task = self.taskForIOS( - args: [ - "developer", - "simulate-location", - "set", - "--", - "\(String(format: "%.5f", location.latitude))", - "\(String(format: "%.5f", location.longitude))" - ] - ) - - self.log?("set iOS location \(location.description)") - self.log?("task: \(task.logDescription)") + let task = try await self.taskForIOS( + args: [ + "developer", + "simulate-location", + "set", + "--", + "\(String(format: "%.5f", location.latitude))", + "\(String(format: "%.5f", location.longitude))" + ], + showAlert: showAlert + ) - self.currentTask = task + self.log?("set iOS location \(location.description)") + self.log?("task: \(task.logDescription)") - let inputPipe = Pipe() - let outputPipe = Pipe() - let errorPipe = Pipe() + self.currentTask = task - task.standardInput = inputPipe - task.standardOutput = outputPipe - task.standardError = errorPipe + let inputPipe = Pipe() + let outputPipe = Pipe() + let errorPipe = Pipe() - do { - try task.run() - self.runnerQueue.async { - if self.tasks.count > self.maxTasksCount { - self.stop() - } - self.tasks.append(task) + task.standardInput = inputPipe + task.standardOutput = outputPipe + task.standardError = errorPipe + + do { + try task.run() + self.runnerQueue.async { + if self.tasks.count > self.maxTasksCount { + self.stop() } + self.tasks.append(task) + } - task.waitUntilExit() + task.waitUntilExit() - if let errorData = try errorPipe.fileHandleForReading.readToEnd() { - let error = String(decoding: errorData, as: UTF8.self) + if let errorData = try errorPipe.fileHandleForReading.readToEnd() { + let error = String(decoding: errorData, as: UTF8.self) - if !error.isEmpty { - showAlert(error) - } + if !error.isEmpty { + showAlert(error) } - } catch { - showAlert(error.localizedDescription) - return } + } catch { + showAlert(error.localizedDescription) + return } } @@ -116,7 +116,7 @@ class Runner { RSDAddress: String, RSDPort: String, showAlert: @escaping (String) -> Void - ) { + ) async throws { guard !RSDAddress.isEmpty, !RSDPort.isEmpty else { showAlert("Please specify RSD ID and Port") return @@ -124,61 +124,60 @@ class Runner { self.isStopped = false - executionQueue.async { - guard !self.isStopped else { - return - } + guard !self.isStopped else { + return + } - let task = self.taskForIOS( - args: [ - "developer", - "dvt", - "simulate-location", - "set", - "--rsd", - RSDAddress, - RSDPort, - "--", - "\(location.latitude)", - "\(location.longitude)" - ] - ) - - self.log?("set iOS location \(location.description)") - self.log?("task: \(task.logDescription)") + let task = try await self.taskForIOS( + args: [ + "developer", + "dvt", + "simulate-location", + "set", + "--rsd", + RSDAddress, + RSDPort, + "--", + "\(location.latitude)", + "\(location.longitude)" + ], + showAlert: showAlert + ) - self.currentTask = task + self.log?("set iOS location \(location.description)") + self.log?("task: \(task.logDescription)") - let inputPipe = Pipe() - let outputPipe = Pipe() - let errorPipe = Pipe() + self.currentTask = task - task.standardInput = inputPipe - task.standardOutput = outputPipe - task.standardError = errorPipe + let inputPipe = Pipe() + let outputPipe = Pipe() + let errorPipe = Pipe() - do { - try task.run() - self.runnerQueue.async { - if self.tasks.count > self.maxTasksCount { - self.stop() - } - self.tasks.append(task) + task.standardInput = inputPipe + task.standardOutput = outputPipe + task.standardError = errorPipe + + do { + try task.run() + self.runnerQueue.async { + if self.tasks.count > self.maxTasksCount { + self.stop() } + self.tasks.append(task) + } - task.waitUntilExit() + task.waitUntilExit() - if let errorData = try errorPipe.fileHandleForReading.readToEnd() { - let error = String(decoding: errorData, as: UTF8.self) + if let errorData = try errorPipe.fileHandleForReading.readToEnd() { + let error = String(decoding: errorData, as: UTF8.self) - if !error.isEmpty { - showAlert(error) - } + if !error.isEmpty { + showAlert(error) } - } catch { - showAlert(error.localizedDescription) - return } + } catch { + showAlert(error.localizedDescription) + return } } @@ -273,12 +272,54 @@ class Runner { task.waitUntilExit() } - func taskForIOS(args: [String]) -> Process { - #if arch(arm64) - let path: URL = URL(string: "file:///opt/homebrew/bin/pymobiledevice3")! - #else - let path: URL = URL(string: "file:///usr/local/bin/pymobiledevice3")! - #endif + func taskForIOS(args: [String], showAlert: (String) -> Void) async throws -> Process { + let whichTask = Process() + let whichURL = URL(fileURLWithPath: "/usr/bin/find") + let userPath = "/Users/\(NSUserName())/Library" + whichTask.executableURL = whichURL + whichTask.currentDirectoryURL = URL(fileURLWithPath: userPath) + whichTask.arguments = ["Python", "-name", "pymobiledevice3"] + + let outputPipe = Pipe() + let errorPipe = Pipe() + + whichTask.standardOutput = outputPipe + whichTask.standardError = errorPipe + + try whichTask.run() + whichTask.waitUntilExit() + + if pymobiledevicePath == nil || pymobiledevicePath == "" { + let data = outputPipe.fileHandleForReading.readDataToEndOfFile() + try outputPipe.fileHandleForReading.close() + let rawValue = String(decoding: data, as: UTF8.self) + + if let path = rawValue.split(separator: "\n").filter({ $0.contains("3.12") }).first { + pymobiledevicePath = "\(userPath)/\(String(path))" + } else { + showAlert(""" + pymobiledevice3 not found, it should be installed with python 3.12 + to install pymobiledevice3 properly try install it with following command: + `brew install python@3.12 && python3 -m pip install -U pymobiledevice3 --break-system-packages --user` + """) + pymobiledevicePath = "" + } + + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + let error = String(decoding: errorData, as: UTF8.self) + if !error.isEmpty { + showAlert(error) + } + try? errorPipe.fileHandleForReading.close() + } + +// #if arch(arm64) +// let path: URL = URL(string: "file:///opt/homebrew/bin/pymobiledevice3")! +// #else +// let path: URL = URL(string: "file:///usr/local/bin/pymobiledevice3")! +// #endif + + let path: URL = URL(fileURLWithPath: pymobiledevicePath!) let task = Process() task.executableURL = path diff --git a/SimVirtualLocation/Views/iOS/iOSDeviceSettings.swift b/SimVirtualLocation/Views/iOS/iOSDeviceSettings.swift index dbe6d93..65f3cb5 100644 --- a/SimVirtualLocation/Views/iOS/iOSDeviceSettings.swift +++ b/SimVirtualLocation/Views/iOS/iOSDeviceSettings.swift @@ -25,7 +25,9 @@ struct iOSDeviceSettings: View { } Button(action: { - locationController.refreshDevices() + Task { + await locationController.refreshDevices() + } }, label: { Text("Refresh").frame(maxWidth: .infinity) }) @@ -47,7 +49,9 @@ struct iOSDeviceSettings: View { } Button(action: { - locationController.refreshDevices() + Task { + await locationController.refreshDevices() + } }, label: { Text("Refresh").frame(maxWidth: .infinity) })