280 lines
9.3 KiB
Swift
280 lines
9.3 KiB
Swift
// Runner.swift
|
|
// Copyright (c) 2024 FastlaneTools
|
|
|
|
//
|
|
// ** NOTE **
|
|
// This file is provided by fastlane and WILL be overwritten in future updates
|
|
// If you want to add extra functionality to this project, create a new file in a
|
|
// new group so that it won't be marked for upgrade
|
|
//
|
|
|
|
import Foundation
|
|
|
|
let logger: Logger = .init()
|
|
|
|
let runner: Runner = .init()
|
|
|
|
func desc(_: String) {
|
|
// no-op, this is handled in fastlane/lane_list.rb
|
|
}
|
|
|
|
class Runner {
|
|
private var thread: Thread!
|
|
private var socketClient: SocketClient!
|
|
private let dispatchGroup = DispatchGroup()
|
|
private var returnValue: String? // lol, so safe
|
|
private var currentlyExecutingCommand: RubyCommandable?
|
|
private var shouldLeaveDispatchGroupDuringDisconnect = false
|
|
private var executeNext: AtomicDictionary<String, Bool> = {
|
|
if #available(macOS 10.12, *) {
|
|
return UnfairAtomicDictionary<String, Bool>()
|
|
} else {
|
|
return OSSPinAtomicDictionary<String, Bool>()
|
|
}
|
|
}()
|
|
|
|
func executeCommand(_ command: RubyCommandable) -> String {
|
|
dispatchGroup.enter()
|
|
currentlyExecutingCommand = command
|
|
socketClient.send(rubyCommand: command)
|
|
|
|
let secondsToWait = DispatchTimeInterval.seconds(SocketClient.defaultCommandTimeoutSeconds)
|
|
// swiftformat:disable:next redundantSelf
|
|
let timeoutResult = Self.waitWithPolling(self.executeNext[command.id], toEventually: { $0 == true }, timeout: SocketClient.defaultCommandTimeoutSeconds)
|
|
executeNext.removeValue(forKey: command.id)
|
|
let failureMessage = "command didn't execute in: \(SocketClient.defaultCommandTimeoutSeconds) seconds"
|
|
let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait)
|
|
guard success else {
|
|
log(message: "command timeout")
|
|
preconditionFailure()
|
|
}
|
|
|
|
if let _returnValue = returnValue {
|
|
return _returnValue
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
static func waitWithPolling<T>(_ expression: @autoclosure @escaping () throws -> T, toEventually predicate: @escaping (T) -> Bool, timeout: Int, pollingInterval: DispatchTimeInterval = .milliseconds(4)) -> DispatchTimeoutResult {
|
|
func memoizedClosure<T>(_ closure: @escaping () throws -> T) -> (Bool) throws -> T {
|
|
var cache: T?
|
|
return { withoutCaching in
|
|
if withoutCaching || cache == nil {
|
|
cache = try closure()
|
|
}
|
|
guard let cache = cache else {
|
|
preconditionFailure()
|
|
}
|
|
|
|
return cache
|
|
}
|
|
}
|
|
|
|
let runLoop = RunLoop.current
|
|
let timeoutDate = Date(timeInterval: TimeInterval(timeout), since: Date())
|
|
var fulfilled = false
|
|
let _expression = memoizedClosure(expression)
|
|
repeat {
|
|
do {
|
|
let exp = try _expression(true)
|
|
fulfilled = predicate(exp)
|
|
} catch {
|
|
fatalError("Error raised \(error.localizedDescription)")
|
|
}
|
|
if !fulfilled {
|
|
runLoop.run(until: Date(timeIntervalSinceNow: pollingInterval.timeInterval))
|
|
} else {
|
|
break
|
|
}
|
|
} while Date().compare(timeoutDate) == .orderedAscending
|
|
|
|
if fulfilled {
|
|
return .success
|
|
} else {
|
|
return .timedOut
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle threading stuff
|
|
extension Runner {
|
|
func startSocketThread(port: UInt32) {
|
|
let secondsToWait = DispatchTimeInterval.seconds(SocketClient.connectTimeoutSeconds)
|
|
|
|
dispatchGroup.enter()
|
|
|
|
socketClient = SocketClient(port: port, commandTimeoutSeconds: timeout, socketDelegate: self)
|
|
thread = Thread(target: self, selector: #selector(startSocketComs), object: nil)
|
|
guard let thread = thread else {
|
|
preconditionFailure("Thread did not instantiate correctly")
|
|
}
|
|
|
|
thread.name = "socket thread"
|
|
thread.start()
|
|
|
|
let connectTimeout = DispatchTime.now() + secondsToWait
|
|
let timeoutResult = dispatchGroup.wait(timeout: connectTimeout)
|
|
|
|
let failureMessage = "couldn't start socket thread in: \(SocketClient.connectTimeoutSeconds) seconds"
|
|
let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait)
|
|
guard success else {
|
|
log(message: "socket thread timeout")
|
|
preconditionFailure()
|
|
}
|
|
}
|
|
|
|
func disconnectFromFastlaneProcess() {
|
|
shouldLeaveDispatchGroupDuringDisconnect = true
|
|
dispatchGroup.enter()
|
|
socketClient.sendComplete()
|
|
|
|
let connectTimeout = DispatchTime.now() + 2
|
|
_ = dispatchGroup.wait(timeout: connectTimeout)
|
|
}
|
|
|
|
@objc func startSocketComs() {
|
|
guard let socketClient = socketClient else {
|
|
return
|
|
}
|
|
|
|
socketClient.connectAndOpenStreams()
|
|
dispatchGroup.leave()
|
|
}
|
|
|
|
private func testDispatchTimeoutResult(_ timeoutResult: DispatchTimeoutResult, failureMessage: String, timeToWait _: DispatchTimeInterval) -> Bool {
|
|
switch timeoutResult {
|
|
case .success:
|
|
return true
|
|
case .timedOut:
|
|
log(message: "timeout: \(failureMessage)")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Runner: SocketClientDelegateProtocol {
|
|
func commandExecuted(serverResponse: SocketClientResponse, completion: (SocketClient) -> Void) {
|
|
switch serverResponse {
|
|
case let .success(returnedObject, closureArgumentValue):
|
|
verbose(message: "command executed")
|
|
returnValue = returnedObject
|
|
if let command = currentlyExecutingCommand as? RubyCommand {
|
|
if let closureArgumentValue = closureArgumentValue, !closureArgumentValue.isEmpty {
|
|
command.performCallback(callbackArg: closureArgumentValue, socket: socketClient) {
|
|
self.executeNext[command.id] = true
|
|
}
|
|
} else {
|
|
executeNext[command.id] = true
|
|
}
|
|
}
|
|
dispatchGroup.leave()
|
|
completion(socketClient)
|
|
case .clientInitiatedCancelAcknowledged:
|
|
verbose(message: "server acknowledged a cancel request")
|
|
dispatchGroup.leave()
|
|
if let command = currentlyExecutingCommand as? RubyCommand {
|
|
executeNext[command.id] = true
|
|
}
|
|
completion(socketClient)
|
|
case .alreadyClosedSockets, .connectionFailure, .malformedRequest, .malformedResponse, .serverError:
|
|
log(message: "error encountered while executing command:\n\(serverResponse)")
|
|
dispatchGroup.leave()
|
|
if let command = currentlyExecutingCommand as? RubyCommand {
|
|
executeNext[command.id] = true
|
|
}
|
|
completion(socketClient)
|
|
case let .commandTimeout(timeout):
|
|
log(message: "Runner timed out after \(timeout) second(s)")
|
|
}
|
|
}
|
|
|
|
func connectionsOpened() {
|
|
DispatchQueue.main.async {
|
|
verbose(message: "connected!")
|
|
}
|
|
}
|
|
|
|
func connectionsClosed() {
|
|
DispatchQueue.main.async {
|
|
if let thread = self.thread {
|
|
thread.cancel()
|
|
}
|
|
self.thread = nil
|
|
self.socketClient.closeSession()
|
|
self.socketClient = nil
|
|
verbose(message: "connection closed!")
|
|
if self.shouldLeaveDispatchGroupDuringDisconnect {
|
|
self.dispatchGroup.leave()
|
|
}
|
|
exit(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
class Logger {
|
|
enum LogMode {
|
|
init(logMode: String) {
|
|
switch logMode {
|
|
case "normal", "default":
|
|
self = .normal
|
|
case "verbose":
|
|
self = .verbose
|
|
default:
|
|
logger.log(message: "unrecognized log mode: \(logMode), defaulting to 'normal'")
|
|
self = .normal
|
|
}
|
|
}
|
|
|
|
case normal
|
|
case verbose
|
|
}
|
|
|
|
public static var logMode: LogMode = .normal
|
|
|
|
func log(message: String) {
|
|
let timestamp = NSDate().timeIntervalSince1970
|
|
print("[\(timestamp)]: \(message)")
|
|
}
|
|
|
|
func verbose(message: String) {
|
|
if Logger.logMode == .verbose {
|
|
let timestamp = NSDate().timeIntervalSince1970
|
|
print("[\(timestamp)]: \(message)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func log(message: String) {
|
|
logger.log(message: message)
|
|
}
|
|
|
|
func verbose(message: String) {
|
|
logger.verbose(message: message)
|
|
}
|
|
|
|
private extension DispatchTimeInterval {
|
|
var timeInterval: TimeInterval {
|
|
var result: TimeInterval = 0
|
|
switch self {
|
|
case let .seconds(value):
|
|
result = TimeInterval(value)
|
|
case let .milliseconds(value):
|
|
result = TimeInterval(value) * 0.001
|
|
case let .microseconds(value):
|
|
result = TimeInterval(value) * 0.000_001
|
|
case let .nanoseconds(value):
|
|
result = TimeInterval(value) * 0.000_000_001
|
|
case .never:
|
|
fatalError()
|
|
@unknown default:
|
|
fatalError()
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
// Please don't remove the lines below
|
|
// They are used to detect outdated files
|
|
// FastlaneRunnerAPIVersion [0.9.2]
|