diff --git a/README.md b/README.md index af2e166..888bd9d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ The following commands will be available in code in your (test) targets: - Trigger iCloud Sync - Open URLs including registered URL schemes - Erase the contents and settings of the simulator +- Get app container ## ❔ Why would you (not) use this diff --git a/Sources/Simctl/SimctlClient.swift b/Sources/Simctl/SimctlClient.swift index 281cfb7..092ca95 100644 --- a/Sources/Simctl/SimctlClient.swift +++ b/Sources/Simctl/SimctlClient.swift @@ -129,6 +129,10 @@ public class SimctlClient { public func openUrl(_ url: URL, completion: @escaping DataTaskCallback) { dataTask(.openURL(env, URLContainer(url: url)), completion) } + + public func getAppContainer(_ container: AppContainer? = nil, completion: @escaping DataTaskCallback) { + dataTask(.getAppContainer(env, container), completion) + } } // MARK: - Enviroment { @@ -264,12 +268,14 @@ extension SimctlClient { case setStatusBarOverrides(SimctlClientEnvironment, Set) case clearStatusBarOverrides(SimctlClientEnvironment) case openURL(SimctlClientEnvironment, URLContainer) + case getAppContainer(SimctlClientEnvironment, AppContainer?) @inlinable var httpMethod: HttpMethod { switch self { case .sendPushNotification, .setStatusBarOverrides, - .openURL: + .openURL, + .getAppContainer: return .post case .setPrivacy, @@ -318,6 +324,9 @@ extension SimctlClient { case .openURL: return .openURL + + case .getAppContainer: + return .getAppContainer } } @@ -338,7 +347,8 @@ extension SimctlClient { let .triggerICloudSync(env), let .setStatusBarOverrides(env, _), let .clearStatusBarOverrides(env), - let .openURL(env, _): + let .openURL(env, _), + let .getAppContainer(env, _): return setEnv(env) case let .setPrivacy(env, action, service): @@ -377,6 +387,9 @@ extension SimctlClient { case let .openURL(_, urlContainer): return try? encoder.encode(urlContainer) + case let .getAppContainer(_, container): + return try? encoder.encode(container) + case .setPrivacy, .renameDevice, .terminateApp, diff --git a/Sources/SimctlCLI/Commands.swift b/Sources/SimctlCLI/Commands.swift index c1b91ee..cea5e09 100644 --- a/Sources/SimctlCLI/Commands.swift +++ b/Sources/SimctlCLI/Commands.swift @@ -137,6 +137,31 @@ extension ShellOutCommand { static func simctlSetStatusBarOverrides(device: UUID, overrides: Set) -> ShellOutCommand { .init(string: simctl("status_bar \(device.uuidString) override \(overrides.map { $0.command }.joined(separator: " "))")) } + + /// Install an xcappdata package to a device, replacing the current contents of the container. + /// + /// Usage: simctl install_app_data + /// This will replace the current contents of the container. If the app is currently running it will be terminated before the container is replaced. + static func simctlInstallAppData(device: UUID, appData: URL) -> ShellOutCommand { + .init(string: simctl("install_app_data \(device.uuidString) \(appData.path)")) + } + + /// Print the path of the installed app's container + /// + /// Usage: simctl get_app_container [] + /// + /// container Optionally specify the container. Defaults to app. + /// app The .app bundle + /// data The application's data container + /// groups The App Group containers + /// A specific App Group container + static func simctlGetAppContainer(device: UUID, appBundleIdentifier: String, container: AppContainer? = nil) -> ShellOutCommand { + if let container = container { + return .init(string: simctl("get_app_container \(device.uuidString) \(appBundleIdentifier) \(container.container)")) + } else { + return .init(string: simctl("get_app_container \(device.uuidString) \(appBundleIdentifier)")) + } + } } internal enum ListFilterType: String { diff --git a/Sources/SimctlCLI/SimctlServer.swift b/Sources/SimctlCLI/SimctlServer.swift index 3d64857..a046a36 100644 --- a/Sources/SimctlCLI/SimctlServer.swift +++ b/Sources/SimctlCLI/SimctlServer.swift @@ -337,4 +337,35 @@ internal final class SimctlServer { } } } + + func onGetAppContainer(_ closure: @escaping (UUID, String, AppContainer) -> Result) { + server.POST[ServerPath.getAppContainer.rawValue] = { request in + guard let deviceId = request.headerValue(for: .deviceUdid, UUID.init) else { + return .badRequest(.text("Device Udid missing or corrupt.")) + } + + guard let bundleId = request.headerValue(for: .bundleIdentifier) else { + return .badRequest(.text("Bundle Id missing or corrupt.")) + } + + let bodyData = Data(request.body) + + let appContainer: AppContainer + do { + appContainer = try JSONDecoder().decode(AppContainer.self, from: bodyData) + } catch { + return .badRequest(.text(error.localizedDescription)) + } + + let result = closure(deviceId, bundleId, appContainer) + + switch result { + case let .success(output): + return .ok(.text(output)) + + case let .failure(error): + return .badRequest(.text(error.localizedDescription)) + } + } + } } diff --git a/Sources/SimctlCLI/StartServer.swift b/Sources/SimctlCLI/StartServer.swift index f506dca..bcabebf 100644 --- a/Sources/SimctlCLI/StartServer.swift +++ b/Sources/SimctlCLI/StartServer.swift @@ -68,6 +68,10 @@ struct StartServer: ParsableCommand { runCommand(.simctlOpen(url: url, on: deviceId)) } + server.onGetAppContainer { deviceId, appBundleId, container -> Result in + runCommand(.simctlGetAppContainer(device: deviceId, appBundleIdentifier: appBundleId, container: container)) + } + server.startServer(on: port) } } diff --git a/Sources/SimctlShared/SimctlShared.swift b/Sources/SimctlShared/SimctlShared.swift index fb44d9c..5bb6063 100644 --- a/Sources/SimctlShared/SimctlShared.swift +++ b/Sources/SimctlShared/SimctlShared.swift @@ -85,6 +85,7 @@ public enum ServerPath: String { case uninstallApp = "/simctl/uninstallApp" case statusBarOverrides = "/simctl/statusBarOverrides" case openURL = "/simctl/openUrl" + case getAppContainer = "/simctl/getAppContainer" } /// Some permission changes will terminate the application if running. @@ -353,3 +354,65 @@ public struct URLContainer: Codable { self.url = url } } + +public enum AppContainer: Codable { + case app + case data + case groups + case groupIdentifier(String) + + public var container: String { + switch self { + case .app: + return "app" + case .data: + return "data" + case .groups: + return "groups" + case let .groupIdentifier(groupId): + return groupId + } + } + + enum Keys: String, CodingKey { + case key + case value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Keys.self) + let key = try container.decode(String.self, forKey: .key) + switch key { + case "app": + self = .app + case "data": + self = .data + case "groups": + self = .groups + case "groupID": + let groupID = try container.decode(String.self, forKey: .value) + self = .groupIdentifier(groupID) + + default: + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unexpected key \(key)", underlyingError: nil)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: Keys.self) + switch self { + case .app: + try container.encode("app", forKey: .key) + + case .data: + try container.encode("data", forKey: .key) + + case .groups: + try container.encode("groups", forKey: .key) + + case .groupIdentifier(let groupID): + try container.encode("groupID", forKey: .key) + try container.encode(groupID, forKey: .value) + } + } +} diff --git a/bin/SimctlCLI b/bin/SimctlCLI index 2f39dea..c750504 100755 Binary files a/bin/SimctlCLI and b/bin/SimctlCLI differ