diff --git a/Package.swift b/Package.swift index 58328f9..064ce1f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,22 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.1 import PackageDescription let package = Package( name: "SwiftTools", - platforms: [.macOS(.v11)], + platforms: [.macOS(.v14)], products: [ .library(name: "SwiftTools", targets: ["SwiftTools"]), ], dependencies: [ .package(url: "https://github.com/Swinject/Swinject.git", from: "2.8.1"), .package(url: "https://github.com/Swinject/SwinjectAutoregistration", from: "2.8.1"), - .package(url: "https://github.com/jakeheis/SwiftCLI", exact: "6.0.3"), - .package(url: "https://github.com/cpisciotta/xcbeautify", exact: "2.8.0"), + .package(url: "https://github.com/raptorxcz/xcbeautify", revision: "f2204485ba19bb36c4e62f4245a7b6f41d7d687a"), ], targets: [ .target(name: "SwiftTools", dependencies: [ "Swinject", "SwinjectAutoregistration", - "SwiftCLI", .product(name: "XcbeautifyLib", package: "xcbeautify"), ]), .testTarget( diff --git a/Sources/SwiftTools/Build/Domain/BuildInteractor.swift b/Sources/SwiftTools/Build/Domain/BuildInteractor.swift index 02113e2..66d6af0 100644 --- a/Sources/SwiftTools/Build/Domain/BuildInteractor.swift +++ b/Sources/SwiftTools/Build/Domain/BuildInteractor.swift @@ -6,6 +6,7 @@ // import Foundation +import XcbeautifyLib public protocol BuildInteractor { func build(with arguments: BuildArguments) throws @@ -56,7 +57,34 @@ final class BuildInteractorImpl: BuildInteractor { func test(with arguments: TestArguments) throws { let arguments = try makeArguments(from: arguments) - try shellService.executeWithXCBeautify(arguments: arguments) + + let parser = XCBeautifier( + colored: true, + renderer: .terminal, + preserveUnbeautifiedLines: true, + additionalLines: { nil } + ) + + let output = OutputHandler(quiet: false, quieter: true, isCI: false) { line in + let normalizedLine = line.trimmingCharacters(in: .whitespacesAndNewlines) + if !normalizedLine.isEmpty { + if !normalizedLine.hasPrefix("Executed") && !normalizedLine.hasPrefix("Test Suite") { + print(line) + } + } + } + + try shellService.executeWithProcessing( + arguments: arguments, + onProcessLine: { line in + guard let result = parser.process(line: line) else { + output.write(.undefined, line) + return + } + + output.write(result.outputType, result.formatted) + } + ) } func testWithLog(with arguments: TestArguments) throws -> String { @@ -96,9 +124,6 @@ final class BuildInteractorImpl: BuildInteractor { let destination = try getDestination(for: platform, scheme: scheme, simulatorId: simulatorId) buildArguments += ["-destination", destination] } - if isQuiet { - buildArguments += ["-quiet"] - } return buildArguments + arguments } diff --git a/Sources/SwiftTools/Build/Domain/GetSimulatorIdUseCase.swift b/Sources/SwiftTools/Build/Domain/GetSimulatorIdUseCase.swift index 47db216..ce1c073 100644 --- a/Sources/SwiftTools/Build/Domain/GetSimulatorIdUseCase.swift +++ b/Sources/SwiftTools/Build/Domain/GetSimulatorIdUseCase.swift @@ -5,12 +5,10 @@ public protocol GetSimulatorIdUseCase { final class GetSimulatorIdUseCaseImp: GetSimulatorIdUseCase { private let shellService: ShellService private let printService: PrintService - private let verboseController: VerboseController - init(shellService: ShellService, printService: PrintService, verboseController: VerboseController) { + init(shellService: ShellService, printService: PrintService) { self.shellService = shellService self.printService = printService - self.verboseController = verboseController } func callAsFunction(for platform: Platform, scheme: String) throws -> String { @@ -20,7 +18,6 @@ final class GetSimulatorIdUseCaseImp: GetSimulatorIdUseCase { private func getDeviceId(for keys: [String], scheme: String) throws -> String { let destinations = try shellService.executeWithResult(arguments: ["xcodebuild", "-scheme", scheme, "-showdestinations", "-quiet"]) - printService.printVerbose(destinations) let components = destinations .components(separatedBy: "\n") let destination = try components.first(where: { isRowValid(keys: keys, row: $0) }) ?!+ "missing simulator" diff --git a/Sources/SwiftTools/Common/Shell/Platform/ShellService.swift b/Sources/SwiftTools/Common/Shell/Platform/ShellService.swift index 764229a..2cb480a 100644 --- a/Sources/SwiftTools/Common/Shell/Platform/ShellService.swift +++ b/Sources/SwiftTools/Common/Shell/Platform/ShellService.swift @@ -1,97 +1,102 @@ -// -// File.swift -// -// -// Created by Kryštof Matěj on 05.01.2021. -// - -import SwiftCLI -import XcbeautifyLib +import Foundation public protocol ShellService { func execute(arguments: [String]) throws - func executeWithVisibleOutput(arguments: [String]) throws func executeWithResult(arguments: [String]) throws -> String - func executeWithXCBeautify(arguments: [String]) throws + func executeWithProcessing(arguments: [String], onProcessLine: @escaping (String) -> Void) throws } -final class ShellServiceImpl: ShellService { +nonisolated(unsafe) private var processes: [Process] = [] + +private func handleSignal(_ sig: Int32) { + for p in processes where p.isRunning { + kill(-p.processIdentifier, sig) + } + + exit(sig) +} + +final class ShellServiceImpl: ShellService, @unchecked Sendable { private let printService: PrintService - private let verboseController: VerboseController - init(printService: PrintService, verboseController: VerboseController) { + init(printService: PrintService) { self.printService = printService - self.verboseController = verboseController + self.setupSignalHandlers() } - func execute(arguments: [String]) throws { - try executeTask(arguments: arguments, isOutputVisible: verboseController.isVerbose()) + private func setupSignalHandlers() { + signal(SIGINT) { _ in + print("Killing self SIGINT, processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))") + handleSignal(SIGINT) + } + + signal(SIGTERM) { _ in + print("Killing self SIGTERM, processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))") + handleSignal(SIGTERM) + } } - func executeWithVisibleOutput(arguments: [String]) throws { - try executeTask(arguments: arguments, isOutputVisible: true) + func execute(arguments: [String]) throws { + try executeWithProcessing(arguments: arguments, onProcessLine: { line in + self.printService.printVerbose(line) + }) } func executeWithResult(arguments: [String]) throws -> String { - return try executeTask(arguments: arguments, isOutputVisible: verboseController.isVerbose()) + var output = "" + try executeWithProcessing(arguments: arguments, onProcessLine: { line in + self.printService.printVerbose(line) + output += line + "\n" + }) + return output } - @discardableResult private func executeTask(arguments: [String], isOutputVisible: Bool) throws -> String { - let output = CaptureStream() - let error = CaptureStream() - let outputStream = makeOutputStream(captureStream: output, isOutputVisible: isOutputVisible) - + func executeWithProcessing(arguments: [String], onProcessLine: @escaping (String) -> Void) throws { let command = arguments.joined(separator: " ") - let task = Task(executable: "/bin/bash", arguments: ["-c", command], stdout: outputStream, stderr: error) - printService.printVerbose("shell command: '\(command)'") - let exitCode = task.runSync() - - let outputString = output.readAll() - - guard exitCode == 0 else { - let errorMessage = error.readAll() - printService.printText("Command output: \(outputString)") - printService.printText("Command error: \(errorMessage)") - let message = !errorMessage.isEmpty ? errorMessage : outputString - throw ToolsError(description: "shell command: '\(command)' failed with error: '\(message)'") - } - return outputString - } + let process = Process() + let pipe = Pipe() - private func makeOutputStream(captureStream: CaptureStream, isOutputVisible: Bool) -> WritableStream { - if isOutputVisible { - return SplitStream(streams: [captureStream, WriteStream.stdout]) - } else { - return captureStream - } - } + printService.printVerbose("shell command: '\(command)'") + process.executableURL = URL(fileURLWithPath: "/bin/bash") + process.arguments = ["-c", command] + process.standardOutput = pipe + process.standardError = pipe - func executeWithXCBeautify(arguments: [String]) throws { - let printStream = WriteStream.stdout - let parser = XCBeautifier( - colored: true, - renderer: .terminal, - preserveUnbeautifiedLines: true, - additionalLines: { nil } - ) - let outputStream = makeBeautifyStream(outputStream: printStream, parser: parser) + let handle = pipe.fileHandleForReading + var buffer = Data() - let command = arguments.joined(separator: " ") - let task = Task(executable: "/bin/bash", arguments: ["-c", command], stdout: outputStream) - let exitCode = task.runSync() - guard exitCode == 0 else { + try process.run() + addProcessToChildManagement(process: process) + + while true { + let data = handle.availableData + if data.isEmpty { break } + buffer.append(data) + + while let range = buffer.firstRange(of: Data([0x0A])) { + let lineData = buffer.subdata(in: 0.. ProcessingStream { - return LineStream { line in - if let formatted = parser.format(line: line) { - outputStream.write(formatted + "\n") - } else { - outputStream.write(line + "\n") - } + private func addProcessToChildManagement(process: Process) { + processes.append(process) + printService.printVerbose("Added processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))") + process.terminationHandler = { process in + processes.removeAll { $0 === process } + self.printService.printVerbose("Removed processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))") } } } diff --git a/Tests/SwiftToolsTests/Build/Domain/BuildInteractorTests.swift b/Tests/SwiftToolsTests/Build/Domain/BuildInteractorTests.swift index efa14ef..e9cbe84 100644 --- a/Tests/SwiftToolsTests/Build/Domain/BuildInteractorTests.swift +++ b/Tests/SwiftToolsTests/Build/Domain/BuildInteractorTests.swift @@ -40,27 +40,22 @@ final class ShellServiceSpy: ShellService { let arguments: [String] } - struct ExecuteWithVisibleOutput { - let arguments: [String] - } - struct ExecuteWithResult { let arguments: [String] } - struct ExecuteWithXCBeautify { + struct ExecuteWithProcessing { let arguments: [String] + let onProcessLine: (String) -> Void } var executeThrowBlock: (() throws -> Void)? - var executeWithVisibleOutputThrowBlock: (() throws -> Void)? var executeWithResultThrowBlock: (() throws -> Void)? var executeWithResultReturn: String - var executeWithXCBeautifyThrowBlock: (() throws -> Void)? + var executeWithProcessingThrowBlock: (() throws -> Void)? var execute = [Execute]() - var executeWithVisibleOutput = [ExecuteWithVisibleOutput]() var executeWithResult = [ExecuteWithResult]() - var executeWithXCBeautify = [ExecuteWithXCBeautify]() + var executeWithProcessing = [ExecuteWithProcessing]() init(executeWithResultReturn: String) { self.executeWithResultReturn = executeWithResultReturn @@ -72,12 +67,6 @@ final class ShellServiceSpy: ShellService { try executeThrowBlock?() } - func executeWithVisibleOutput(arguments: [String]) throws { - let item = ExecuteWithVisibleOutput(arguments: arguments) - executeWithVisibleOutput.append(item) - try executeWithVisibleOutputThrowBlock?() - } - func executeWithResult(arguments: [String]) throws -> String { let item = ExecuteWithResult(arguments: arguments) executeWithResult.append(item) @@ -85,10 +74,10 @@ final class ShellServiceSpy: ShellService { return executeWithResultReturn } - func executeWithXCBeautify(arguments: [String]) throws { - let item = ExecuteWithXCBeautify(arguments: arguments) - executeWithXCBeautify.append(item) - try executeWithXCBeautifyThrowBlock?() + func executeWithProcessing(arguments: [String], onProcessLine: @escaping (String) -> Void) throws { + let item = ExecuteWithProcessing(arguments: arguments, onProcessLine: onProcessLine) + executeWithProcessing.append(item) + try executeWithProcessingThrowBlock?() } } diff --git a/Tests/SwiftToolsTests/Build/Domain/GetSimulatorIdUseCaseTests.swift b/Tests/SwiftToolsTests/Build/Domain/GetSimulatorIdUseCaseTests.swift index 7ae9147..50518a7 100644 --- a/Tests/SwiftToolsTests/Build/Domain/GetSimulatorIdUseCaseTests.swift +++ b/Tests/SwiftToolsTests/Build/Domain/GetSimulatorIdUseCaseTests.swift @@ -52,7 +52,6 @@ final class GetSimulatorIdUseCaseTests: XCTestCase { sut = GetSimulatorIdUseCaseImp( shellService: shellServiceSpy, printService: printServiceSpy, - verboseController: verboseControllerSpy, ) }