idb_companion/SwiftServer/MethodHandlers/InstallMethodHandler.swift (182 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Foundation
import IDBGRPCSwift
import FBControlCore
import GRPC
struct InstallMethodHandler {
let commandExecutor: FBIDBCommandExecutor
let targetLogger: FBControlCoreLogger
func handle(requestStream: GRPCAsyncRequestStream<Idb_InstallRequest>, responseStream: GRPCAsyncResponseStreamWriter<Idb_InstallResponse>, context: GRPCAsyncServerCallContext) async throws {
let artifact = try await install(requestStream: requestStream, responseStream: responseStream)
let response = Idb_InstallResponse.with {
$0.name = artifact.name
$0.uuid = artifact.uuid?.uuidString ?? ""
}
try await responseStream.send(response)
}
private func install(requestStream: GRPCAsyncRequestStream<Idb_InstallRequest>, responseStream: GRPCAsyncResponseStreamWriter<Idb_InstallResponse>) async throws -> FBInstalledArtifact {
var request: Idb_InstallRequest!
func consumeNextElement() async throws {
guard let next = try await requestStream.next else {
throw GRPCStatus(code: .failedPrecondition, message: "Install request not provided")
}
request = next
}
func extractPayloadFromRequest() throws -> Idb_Payload {
guard let payload = request.extractPayload() else {
throw GRPCStatus(code: .invalidArgument, message: "Expected the next item in the stream to be a payload")
}
return payload
}
try await consumeNextElement()
guard case let .destination(destination) = request.value else {
throw GRPCStatus(code: .failedPrecondition, message: "Expected destination as first request in stream")
}
try await consumeNextElement()
var name = UUID().uuidString
if case let .nameHint(nameHint) = request.value {
name = nameHint
try await consumeNextElement()
}
var makeDebuggable = false
if case let .makeDebuggable(debuggable) = request.value {
makeDebuggable = debuggable
try await consumeNextElement()
}
var linkToBundle: FBDsymInstallLinkToBundle?
//(2022-03-02) REMOVE! Keeping only for retrocompatibility
if case let .bundleID(id) = request.value {
linkToBundle = .init(id, bundle_type: .app)
try await consumeNextElement()
}
if case let .linkDsymToBundle(link) = request.value {
linkToBundle = readLinkBundleToDsym(from: link)
try await consumeNextElement()
}
var payload = try extractPayloadFromRequest()
var compression = FBCompressionFormat.GZIP
if case let .compression(format) = payload.source {
compression = readCompressionFormat(from: format)
try await consumeNextElement()
payload = try extractPayloadFromRequest()
}
return try await installData(from: payload.source,
to: destination,
requestStream: requestStream,
name: name,
makeDebuggable: makeDebuggable,
linkToBundle: linkToBundle,
compression: compression)
}
private func installData(from source: Idb_Payload.OneOf_Source?,
to destination: Idb_InstallRequest.Destination,
requestStream: GRPCAsyncRequestStream<Idb_InstallRequest>,
name: String,
makeDebuggable: Bool,
linkToBundle: FBDsymInstallLinkToBundle?,
compression: FBCompressionFormat) async throws -> FBInstalledArtifact {
func installSource(dataStream: FBProcessInput<AnyObject>) async throws -> FBInstalledArtifact {
switch destination {
case .app:
return try await FutureBox(
commandExecutor.install_app_stream(dataStream, compression: compression, make_debuggable: makeDebuggable)
).value
case .xctest:
return try await FutureBox(
commandExecutor.install_xctest_app_stream(dataStream)
).value
case .dsym:
return try await FutureBox(
commandExecutor.install_dsym_stream(dataStream, compression: compression, linkTo: linkToBundle)
).value
case .dylib:
return try await FutureBox(
commandExecutor.install_dylib_stream(dataStream, name: name)
).value
case .framework:
return try await FutureBox(
commandExecutor.install_framework_stream(dataStream)
).value
case .UNRECOGNIZED:
throw GRPCStatus(code: .invalidArgument, message: "Unrecognized destination")
}
}
switch source {
case let .data(data):
let dataStream = pipeToInputOutput(initial: data, requestStream: requestStream) as! FBProcessInput<AnyObject>
return try await installSource(dataStream: dataStream)
case let .url(urlString):
guard let url = URL(string: urlString) else {
throw GRPCStatus(code: .invalidArgument, message: "Invalid url source")
}
let download = FBDataDownloadInput.dataDownload(with: url, logger: targetLogger)
let input = download.input as! FBProcessInput<AnyObject>
return try await installSource(dataStream: input)
case let .filePath(filePath):
switch destination {
case .app:
return try await FutureBox(
commandExecutor.install_app_file_path(filePath, make_debuggable: makeDebuggable)
).value
case .xctest:
return try await FutureBox(
commandExecutor.install_xctest_app_file_path(filePath)
).value
case .dsym:
return try await FutureBox(
commandExecutor.install_dsym_file_path(filePath, linkTo: linkToBundle)
).value
case .dylib:
return try await FutureBox(
commandExecutor.install_dylib_file_path(filePath)
).value
case .framework:
return try await FutureBox(
commandExecutor.install_framework_file_path(filePath)
).value
case .UNRECOGNIZED:
throw GRPCStatus(code: .invalidArgument, message: "Unrecognized destination")
}
default:
throw GRPCStatus(code: .invalidArgument, message: "Incorrect payload source")
}
}
private func pipeToInputOutput(initial: Data, requestStream: GRPCAsyncRequestStream<Idb_InstallRequest>) -> FBProcessInput<OutputStream> {
let input = FBProcessInput<OutputStream>.fromStream()
let appStream = input.contents
Task {
appStream.open()
defer { appStream.close() }
var buffer = [UInt8](initial)
appStream.write(&buffer, maxLength: buffer.count)
for try await request in requestStream {
guard let data = request.extractDataFrame() else {
continue
}
var buffer = [UInt8](data)
appStream.write(&buffer, maxLength: buffer.count)
}
}
return input
}
private func readLinkBundleToDsym(from link: Idb_InstallRequest.LinkDsymToBundle) -> FBDsymInstallLinkToBundle {
return .init(link.bundleID,
bundle_type: readDsymBundleType(from: link.bundleType))
}
private func readDsymBundleType(from bundleType: Idb_InstallRequest.LinkDsymToBundle.BundleType) -> FBDsymBundleType {
switch bundleType {
case .app:
return .app
case .xctest:
return .xcTest
case .UNRECOGNIZED:
return .app
}
}
private func readCompressionFormat(from compression: Idb_Payload.Compression) -> FBCompressionFormat {
switch compression {
case .gzip, .UNRECOGNIZED:
return .GZIP
case .zstd:
return .ZSTD
}
}
}