From c7b04d04fcbdd0478e8f7d74d3a6232f6524e272 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Mon, 28 Aug 2023 15:22:31 +0200 Subject: [PATCH 01/27] Bring back launchBashOldVersion for debugging --- Sources/ShellOut.swift | 97 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index f62c227..0b672a1 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -454,6 +454,103 @@ private extension Process { return outputData.shellOutput() } } + + @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> String { +#if os(Linux) + executableURL = URL(fileURLWithPath: "/bin/bash") +#else + launchPath = "/bin/bash" +#endif + arguments = ["-c", command] + + if let environment = environment { + self.environment = environment + } + + // Because FileHandle's readabilityHandler might be called from a + // different queue from the calling queue, avoid a data race by + // protecting reads and writes to outputData and errorData on + // a single dispatch queue. + let outputQueue = DispatchQueue(label: "bash-output-queue") + + var outputData = Data() + var errorData = Data() + + let outputPipe = Pipe() + standardOutput = outputPipe + + let errorPipe = Pipe() + standardError = errorPipe + + #if !os(Linux) + outputPipe.fileHandleForReading.readabilityHandler = { handler in + let data = handler.availableData + outputQueue.async { + outputData.append(data) + outputHandle?.write(data) + } + } + + errorPipe.fileHandleForReading.readabilityHandler = { handler in + let data = handler.availableData + outputQueue.async { + errorData.append(data) + errorHandle?.write(data) + } + } + #endif + +#if os(Linux) + try run() +#else + launch() +#endif + + #if os(Linux) + outputQueue.sync { + outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() + errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + } + #endif + + waitUntilExit() + + if let handle = outputHandle, !handle.isStandard { + handle.closeFile() + } + + if let handle = errorHandle, !handle.isStandard { + handle.closeFile() + } + + #if !os(Linux) + outputPipe.fileHandleForReading.readabilityHandler = nil + errorPipe.fileHandleForReading.readabilityHandler = nil + #endif + + // Block until all writes have occurred to outputData and errorData, + // and then read the data back out. + return try outputQueue.sync { + if terminationStatus != 0 { + throw ShellOutError( + terminationStatus: terminationStatus, + errorData: errorData, + outputData: outputData + ) + } + + return outputData.shellOutput() + } + } + +} + +private extension FileHandle { + var isStandard: Bool { + return self === FileHandle.standardOutput || + self === FileHandle.standardError || + self === FileHandle.standardInput + } } private extension Data { From ea1adebb27394c781ddfb9fc3bbaa4f161f561be Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Mon, 28 Aug 2023 15:38:04 +0200 Subject: [PATCH 02/27] Pass back both stdout and stderr --- Sources/ShellOut.swift | 12 ++++++------ Tests/ShellOutTests/ShellOutTests.swift | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 0b672a1..a846a20 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -36,7 +36,7 @@ import Dispatch outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil -) throws -> String { +) throws -> (stdout: String, stderr: String) { let command = "cd \(path.escapingSpaces) && \(command) \(arguments.map(\.string).joined(separator: " "))" return try process.launchBash( @@ -72,7 +72,7 @@ import Dispatch outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil -) throws -> String { +) throws -> (stdout: String, stderr: String) { try shellOut( to: command.command, arguments: command.arguments, @@ -391,7 +391,7 @@ extension ShellOutCommand { // MARK: - Private private extension Process { - @discardableResult func launchBash(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> String { + @discardableResult func launchBash(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { executableURL = URL(fileURLWithPath: "/bin/bash") arguments = ["-c", command] @@ -451,11 +451,11 @@ private extension Process { ) } - return outputData.shellOutput() + return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) } } - @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> String { + @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { #if os(Linux) executableURL = URL(fileURLWithPath: "/bin/bash") #else @@ -539,7 +539,7 @@ private extension Process { ) } - return outputData.shellOutput() + return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) } } diff --git a/Tests/ShellOutTests/ShellOutTests.swift b/Tests/ShellOutTests/ShellOutTests.swift index 7c86d58..18876b6 100644 --- a/Tests/ShellOutTests/ShellOutTests.swift +++ b/Tests/ShellOutTests/ShellOutTests.swift @@ -32,12 +32,12 @@ class ShellOutTests: XCTestCase { } func testWithoutArguments() throws { - let uptime = try shellOut(to: "uptime".checked) + let uptime = try shellOut(to: "uptime".checked).stdout XCTAssertTrue(uptime.contains("load average")) } func testWithArguments() throws { - let echo = try shellOut(to: "echo".checked, arguments: ["Hello world".quoted]) + let echo = try shellOut(to: "echo".checked, arguments: ["Hello world".quoted]).stdout XCTAssertEqual(echo, "Hello world") } @@ -52,7 +52,7 @@ class ShellOutTests: XCTestCase { to: "cat".checked, arguments: ["ShellOutTests-SingleCommand.txt".quoted], at: tempDir - ) + ).stdout XCTAssertEqual(textFileContent, "Hello") } @@ -66,12 +66,12 @@ class ShellOutTests: XCTestCase { let output = try shellOut( to: "cat".checked, - arguments: ["\(NSTemporaryDirectory())ShellOut Test Folder/File".quoted]) + arguments: ["\(NSTemporaryDirectory())ShellOut Test Folder/File".quoted]).stdout XCTAssertEqual(output, "Hello") } func testSingleCommandAtPathContainingTilde() throws { - let homeContents = try shellOut(to: "ls".checked, arguments: ["-a"], at: "~") + let homeContents = try shellOut(to: "ls".checked, arguments: ["-a"], at: "~").stdout XCTAssertFalse(homeContents.isEmpty) } @@ -113,7 +113,7 @@ class ShellOutTests: XCTestCase { let pipe = Pipe() let output = try shellOut(to: "echo".checked, arguments: ["Hello".verbatim], - outputHandle: pipe.fileHandleForWriting) + outputHandle: pipe.fileHandleForWriting).stdout let capturedData = pipe.fileHandleForReading.readDataToEndOfFile() XCTAssertEqual(output, "Hello") XCTAssertEqual(output + "\n", String(data: capturedData, encoding: .utf8)) @@ -143,7 +143,7 @@ class ShellOutTests: XCTestCase { let tempFolderPath = NSTemporaryDirectory() try shellOut(to: .createFile(named: "Test", contents: "Hello world"), at: tempFolderPath) - XCTAssertEqual(try shellOut(to: .readFile(at: tempFolderPath + "Test")), + XCTAssertEqual(try shellOut(to: .readFile(at: tempFolderPath + "Test")).stdout, "Hello world") } @@ -170,7 +170,7 @@ class ShellOutTests: XCTestCase { try shellOut(to: .gitClone(url: cloneURL, to: "GitTestClone"), at: tempFolderPath) let filePath = clonePath + "/Test" - XCTAssertEqual(try shellOut(to: .readFile(at: filePath)), "Hello world") + XCTAssertEqual(try shellOut(to: .readFile(at: filePath)).stdout, "Hello world") // Make a new commit in the origin repository try shellOut(to: .createFile(named: "Test", contents: "Hello again"), at: originPath) @@ -178,15 +178,15 @@ class ShellOutTests: XCTestCase { // Pull the commit in the clone repository and read the file again try shellOut(to: .gitPull(), at: clonePath) - XCTAssertEqual(try shellOut(to: .readFile(at: filePath)), "Hello again") + XCTAssertEqual(try shellOut(to: .readFile(at: filePath)).stdout, "Hello again") } func testArgumentQuoting() throws { XCTAssertEqual(try shellOut(to: "echo".checked, - arguments: ["foo ; echo bar".quoted]), + arguments: ["foo ; echo bar".quoted]).stdout, "foo ; echo bar") XCTAssertEqual(try shellOut(to: "echo".checked, - arguments: ["foo ; echo bar".verbatim]), + arguments: ["foo ; echo bar".verbatim]).stdout, "foo\nbar") } From a4bebeba1f5f39efc96a4a52b7f55ef5010a3e01 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Mon, 28 Aug 2023 15:44:30 +0200 Subject: [PATCH 03/27] Wire up old version --- Sources/ShellOut.swift | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index a846a20..3ef0719 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -47,6 +47,25 @@ import Dispatch ) } +@discardableResult public func shellOutOldVersion( + to command: SafeString, + arguments: [Argument] = [], + at path: String = ".", + process: Process = .init(), + outputHandle: FileHandle? = nil, + errorHandle: FileHandle? = nil, + environment: [String : String]? = nil +) throws -> (stdout: String, stderr: String) { + let command = "cd \(path.escapingSpaces) && \(command) \(arguments.map(\.string).joined(separator: " "))" + + return try process.launchBashOldVersion( + with: command, + outputHandle: outputHandle, + errorHandle: errorHandle, + environment: environment + ) +} + /** * Run a pre-defined shell command using Bash * @@ -84,6 +103,25 @@ import Dispatch ) } +@discardableResult public func shellOutOldVersion( + to command: ShellOutCommand, + at path: String = ".", + process: Process = .init(), + outputHandle: FileHandle? = nil, + errorHandle: FileHandle? = nil, + environment: [String : String]? = nil +) throws -> (stdout: String, stderr: String) { + try shellOutOldVersion( + to: command.command, + arguments: command.arguments, + at: path, + process: process, + outputHandle: outputHandle, + errorHandle: errorHandle, + environment: environment + ) +} + /// Structure used to pre-define commands for use with ShellOut public struct ShellOutCommand { /// The string that makes up the command that should be run on the command line From 9cd139c4c2c7227a958582972eebca28bfcc1f8d Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Tue, 29 Aug 2023 12:41:34 +0200 Subject: [PATCH 04/27] Apply Gwynne's proposed fix for empty readabilityHandler responses --- Sources/ShellOut.swift | 14 ++++++++ Tests/ShellOutTests/Fixtures/ErrNo.zip | Bin 0 -> 89040 bytes Tests/ShellOutTests/ShellOutTests.swift | 46 ++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 Tests/ShellOutTests/Fixtures/ErrNo.zip diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 3ef0719..5e17784 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -478,6 +478,20 @@ private extension Process { outputPipe.fileHandleForReading.readabilityHandler = nil errorPipe.fileHandleForReading.readabilityHandler = nil + do { + // According to Gwynne there's an old bug where readability handler might report back an emptry string. + // Advice is to call readDataToEndOfFile() to collect any remaining data. This should not lead to a hang, + // because buffers should have been cleared up sufficiently via readabilityHandler callbacks. + outputQueue.sync { + if outputData.isEmpty { + outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() + } + if errorData.isEmpty { + errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + } + } + } + // Block until all writes have occurred to outputData and errorData, // and then read the data back out. return try outputQueue.sync { diff --git a/Tests/ShellOutTests/Fixtures/ErrNo.zip b/Tests/ShellOutTests/Fixtures/ErrNo.zip new file mode 100644 index 0000000000000000000000000000000000000000..104c6e9feb08bca3f80bcd9dab0d48504808c2b6 GIT binary patch literal 89040 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 QSOueCT3~_HG9Fd%e>x2v=>Px# literal 0 HcmV?d00001 diff --git a/Tests/ShellOutTests/ShellOutTests.swift b/Tests/ShellOutTests/ShellOutTests.swift index 18876b6..989ee55 100644 --- a/Tests/ShellOutTests/ShellOutTests.swift +++ b/Tests/ShellOutTests/ShellOutTests.swift @@ -199,4 +199,50 @@ class ShellOutTests: XCTestCase { XCTAssertEqual(Argument.url(.init(string: "https://example.com")!).string, "https://example.com") } + + func test_git_tags() throws { + // setup + let tempDir = NSTemporaryDirectory().appending("test_stress_\(UUID())") + defer { + try? Foundation.FileManager.default.removeItem(atPath: tempDir) + } + let sampleGitRepoName = "ErrNo" + let sampleGitRepoZipFile = fixturesDirectory() + .appendingPathComponent("\(sampleGitRepoName).zip").path + let path = "\(tempDir)/\(sampleGitRepoName)" + try! Foundation.FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: false, attributes: nil) + try! ShellOut.shellOut(to: .init(command: "unzip", arguments: [sampleGitRepoZipFile.quoted]), at: tempDir) + + // MUT + XCTAssertEqual(try shellOut(to: try ShellOutCommand(command: "git", arguments: ["tag"]), + at: path).stdout, """ + 0.2.0 + 0.2.1 + 0.2.2 + 0.2.3 + 0.2.4 + 0.2.5 + 0.3.0 + 0.4.0 + 0.4.1 + 0.4.2 + 0.5.0 + 0.5.1 + 0.5.2 + v0.0.1 + v0.0.2 + v0.0.3 + v0.0.4 + v0.0.5 + v0.1.0 + """) + } +} + +extension ShellOutTests { + func fixturesDirectory(path: String = #file) -> URL { + let url = URL(fileURLWithPath: path) + let testsDir = url.deletingLastPathComponent() + return testsDir.appendingPathComponent("Fixtures") + } } From 49ad2489e57163d8dda38cbaad67ed079663c66f Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Wed, 30 Aug 2023 09:48:10 +0200 Subject: [PATCH 05/27] Gwynne's latest fixes --- Package.swift | 3 +- Sources/ShellOut.swift | 68 ++++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/Package.swift b/Package.swift index 869a44c..047aaaa 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.2 +// swift-tools-version:5.8 /** * ShellOut @@ -10,6 +10,7 @@ import PackageDescription let package = Package( name: "ShellOut", + platforms: [.macOS("10.15.4")], products: [ .library(name: "ShellOut", targets: ["ShellOut"]) ], diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 5e17784..3dcd1b5 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -430,10 +430,10 @@ extension ShellOutCommand { private extension Process { @discardableResult func launchBash(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { - executableURL = URL(fileURLWithPath: "/bin/bash") - arguments = ["-c", command] + self.executableURL = URL(fileURLWithPath: "/bin/bash") + self.arguments = ["-c", command] - if let environment = environment { + if let environment { self.environment = environment } @@ -447,10 +447,10 @@ private extension Process { var errorData = Data() let outputPipe = Pipe() - standardOutput = outputPipe + self.standardOutput = outputPipe let errorPipe = Pipe() - standardError = errorPipe + self.standardError = errorPipe outputPipe.fileHandleForReading.readabilityHandler = { handler in let data = handler.availableData @@ -468,45 +468,47 @@ private extension Process { } } - try run() + try self.run() - waitUntilExit() - - outputHandle?.closeFile() - errorHandle?.closeFile() + self.waitUntilExit() outputPipe.fileHandleForReading.readabilityHandler = nil errorPipe.fileHandleForReading.readabilityHandler = nil - do { - // According to Gwynne there's an old bug where readability handler might report back an emptry string. - // Advice is to call readDataToEndOfFile() to collect any remaining data. This should not lead to a hang, - // because buffers should have been cleared up sufficiently via readabilityHandler callbacks. - outputQueue.sync { - if outputData.isEmpty { - outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() - } - if errorData.isEmpty { - errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() - } + // Spend as little time as possible inside the sync() block by doing + // this part in an async block. + outputQueue.async { + if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { + outputData.append(extraOutput) + outputHandle?.write(extraOutput) } - } - // Block until all writes have occurred to outputData and errorData, - // and then read the data back out. - return try outputQueue.sync { - if terminationStatus != 0 { - throw ShellOutError( - terminationStatus: terminationStatus, - errorData: errorData, - outputData: outputData - ) + if let extraError = try? errorPipe.fileHandleForReading.readToEnd() { + errorData.append(extraError) + errorHandle?.write(extraError) } + } - return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) + // We don't actually have to do anything further *on* the queue, since + // there are no longer any async processes going on; just do a barrier sync + // with an empty block to guarantee nothing is still running there. + outputQueue.sync(flags: .barrier) {} + + try outputPipe.fileHandleForReading.close() + try errorPipe.fileHandleForReading.close() + try outputHandle?.close() + try errorHandle?.close() + + if self.terminationStatus != 0 { + throw ShellOutError( + terminationStatus: terminationStatus, + errorData: errorData, + outputData: outputData + ) } - } + return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) + } @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { #if os(Linux) executableURL = URL(fileURLWithPath: "/bin/bash") From 1818ff52472a3263de9ba72571bb765c9581b7b8 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Wed, 30 Aug 2023 10:50:47 +0200 Subject: [PATCH 06/27] Don't forward devcontainer ports --- .devcontainer/devcontainer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a43997c..2825e9a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,6 +14,5 @@ } } } - }, - "forwardPorts": [8080] + } } \ No newline at end of file From 480d8451de69a2ae4840820fdbb34c117dc48195 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Wed, 30 Aug 2023 12:42:14 +0200 Subject: [PATCH 07/27] Fix TSAN warning --- Sources/ShellOut.swift | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 3dcd1b5..e3efdb4 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -489,26 +489,24 @@ private extension Process { } } - // We don't actually have to do anything further *on* the queue, since - // there are no longer any async processes going on; just do a barrier sync - // with an empty block to guarantee nothing is still running there. - outputQueue.sync(flags: .barrier) {} - - try outputPipe.fileHandleForReading.close() - try errorPipe.fileHandleForReading.close() - try outputHandle?.close() - try errorHandle?.close() - - if self.terminationStatus != 0 { - throw ShellOutError( - terminationStatus: terminationStatus, - errorData: errorData, - outputData: outputData - ) - } + return try outputQueue.sync(flags: .barrier) { + try outputPipe.fileHandleForReading.close() + try errorPipe.fileHandleForReading.close() + try outputHandle?.close() + try errorHandle?.close() - return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) + if self.terminationStatus != 0 { + throw ShellOutError( + terminationStatus: terminationStatus, + errorData: errorData, + outputData: outputData + ) + } + + return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) + } } + @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { #if os(Linux) executableURL = URL(fileURLWithPath: "/bin/bash") From 481216336d25f604424aa0d35a5199a6231a8d5c Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Wed, 30 Aug 2023 15:17:06 +0200 Subject: [PATCH 08/27] Add shellout2 without the bash gymnastics --- Sources/ShellOut.swift | 98 +++++++++++++++++++++++++ Tests/ShellOutTests/ShellOutTests.swift | 37 ++++++++++ 2 files changed, 135 insertions(+) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index e3efdb4..f6a34e6 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -47,6 +47,25 @@ import Dispatch ) } +@discardableResult public func shellOut2( + to command: String, + arguments: [String] = [], + at path: String? = nil, + process: Process = .init(), + outputHandle: FileHandle? = nil, + errorHandle: FileHandle? = nil, + environment: [String : String]? = nil +) throws -> (stdout: String, stderr: String) { + try process.launch( + command: command, + arguments: arguments, + at: path, + outputHandle: outputHandle, + errorHandle: errorHandle, + environment: environment + ) +} + @discardableResult public func shellOutOldVersion( to command: SafeString, arguments: [Argument] = [], @@ -507,6 +526,85 @@ private extension Process { } } + @discardableResult func launch(command: String, arguments: [String] = [], at path: String? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { + self.executableURL = URL(fileURLWithPath: "/usr/bin/env") + self.currentDirectoryURL = path.map { URL(fileURLWithPath: $0) } + self.arguments = [command] + arguments + + if let environment { + self.environment = environment + } + + // Because FileHandle's readabilityHandler might be called from a + // different queue from the calling queue, avoid a data race by + // protecting reads and writes to outputData and errorData on + // a single dispatch queue. + let outputQueue = DispatchQueue(label: "bash-output-queue") + + var outputData = Data() + var errorData = Data() + + let outputPipe = Pipe() + self.standardOutput = outputPipe + + let errorPipe = Pipe() + self.standardError = errorPipe + + outputPipe.fileHandleForReading.readabilityHandler = { handler in + let data = handler.availableData + outputQueue.async { + outputData.append(data) + outputHandle?.write(data) + } + } + + errorPipe.fileHandleForReading.readabilityHandler = { handler in + let data = handler.availableData + outputQueue.async { + errorData.append(data) + errorHandle?.write(data) + } + } + + try self.run() + + self.waitUntilExit() + + outputPipe.fileHandleForReading.readabilityHandler = nil + errorPipe.fileHandleForReading.readabilityHandler = nil + + // Spend as little time as possible inside the sync() block by doing + // this part in an async block. + outputQueue.async { + if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { + outputData.append(extraOutput) + outputHandle?.write(extraOutput) + } + + if let extraError = try? errorPipe.fileHandleForReading.readToEnd() { + errorData.append(extraError) + errorHandle?.write(extraError) + } + } + + return try outputQueue.sync(flags: .barrier) { + try outputPipe.fileHandleForReading.close() + try errorPipe.fileHandleForReading.close() + try outputHandle?.close() + try errorHandle?.close() + + if self.terminationStatus != 0 { + throw ShellOutError( + terminationStatus: terminationStatus, + errorData: errorData, + outputData: outputData + ) + } + + return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) + } + } + @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { #if os(Linux) executableURL = URL(fileURLWithPath: "/bin/bash") diff --git a/Tests/ShellOutTests/ShellOutTests.swift b/Tests/ShellOutTests/ShellOutTests.swift index 989ee55..c01064b 100644 --- a/Tests/ShellOutTests/ShellOutTests.swift +++ b/Tests/ShellOutTests/ShellOutTests.swift @@ -237,6 +237,43 @@ class ShellOutTests: XCTestCase { v0.1.0 """) } + + func test_git_tags_2() throws { + // setup + let tempDir = NSTemporaryDirectory().appending("test_stress_\(UUID())") + defer { + try? Foundation.FileManager.default.removeItem(atPath: tempDir) + } + let sampleGitRepoName = "ErrNo" + let sampleGitRepoZipFile = fixturesDirectory() + .appendingPathComponent("\(sampleGitRepoName).zip").path + let path = "\(tempDir)/\(sampleGitRepoName)" + try! Foundation.FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: false, attributes: nil) + try! ShellOut.shellOut(to: .init(command: "unzip", arguments: [sampleGitRepoZipFile.quoted]), at: tempDir) + + // MUT + XCTAssertEqual(try shellOut2(to: "git", arguments: ["tag"], at: path).stdout, """ + 0.2.0 + 0.2.1 + 0.2.2 + 0.2.3 + 0.2.4 + 0.2.5 + 0.3.0 + 0.4.0 + 0.4.1 + 0.4.2 + 0.5.0 + 0.5.1 + 0.5.2 + v0.0.1 + v0.0.2 + v0.0.3 + v0.0.4 + v0.0.5 + v0.1.0 + """) + } } extension ShellOutTests { From 0a98c6acff027d8bb8002c4c3c4ac7a3242d6d8a Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Wed, 30 Aug 2023 16:58:16 +0200 Subject: [PATCH 09/27] Revert "Add shellout2 without the bash gymnastics" This reverts commit 481216336d25f604424aa0d35a5199a6231a8d5c. --- Sources/ShellOut.swift | 98 ------------------------- Tests/ShellOutTests/ShellOutTests.swift | 37 ---------- 2 files changed, 135 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index f6a34e6..e3efdb4 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -47,25 +47,6 @@ import Dispatch ) } -@discardableResult public func shellOut2( - to command: String, - arguments: [String] = [], - at path: String? = nil, - process: Process = .init(), - outputHandle: FileHandle? = nil, - errorHandle: FileHandle? = nil, - environment: [String : String]? = nil -) throws -> (stdout: String, stderr: String) { - try process.launch( - command: command, - arguments: arguments, - at: path, - outputHandle: outputHandle, - errorHandle: errorHandle, - environment: environment - ) -} - @discardableResult public func shellOutOldVersion( to command: SafeString, arguments: [Argument] = [], @@ -526,85 +507,6 @@ private extension Process { } } - @discardableResult func launch(command: String, arguments: [String] = [], at path: String? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { - self.executableURL = URL(fileURLWithPath: "/usr/bin/env") - self.currentDirectoryURL = path.map { URL(fileURLWithPath: $0) } - self.arguments = [command] + arguments - - if let environment { - self.environment = environment - } - - // Because FileHandle's readabilityHandler might be called from a - // different queue from the calling queue, avoid a data race by - // protecting reads and writes to outputData and errorData on - // a single dispatch queue. - let outputQueue = DispatchQueue(label: "bash-output-queue") - - var outputData = Data() - var errorData = Data() - - let outputPipe = Pipe() - self.standardOutput = outputPipe - - let errorPipe = Pipe() - self.standardError = errorPipe - - outputPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - outputQueue.async { - outputData.append(data) - outputHandle?.write(data) - } - } - - errorPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - outputQueue.async { - errorData.append(data) - errorHandle?.write(data) - } - } - - try self.run() - - self.waitUntilExit() - - outputPipe.fileHandleForReading.readabilityHandler = nil - errorPipe.fileHandleForReading.readabilityHandler = nil - - // Spend as little time as possible inside the sync() block by doing - // this part in an async block. - outputQueue.async { - if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { - outputData.append(extraOutput) - outputHandle?.write(extraOutput) - } - - if let extraError = try? errorPipe.fileHandleForReading.readToEnd() { - errorData.append(extraError) - errorHandle?.write(extraError) - } - } - - return try outputQueue.sync(flags: .barrier) { - try outputPipe.fileHandleForReading.close() - try errorPipe.fileHandleForReading.close() - try outputHandle?.close() - try errorHandle?.close() - - if self.terminationStatus != 0 { - throw ShellOutError( - terminationStatus: terminationStatus, - errorData: errorData, - outputData: outputData - ) - } - - return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) - } - } - @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { #if os(Linux) executableURL = URL(fileURLWithPath: "/bin/bash") diff --git a/Tests/ShellOutTests/ShellOutTests.swift b/Tests/ShellOutTests/ShellOutTests.swift index c01064b..989ee55 100644 --- a/Tests/ShellOutTests/ShellOutTests.swift +++ b/Tests/ShellOutTests/ShellOutTests.swift @@ -237,43 +237,6 @@ class ShellOutTests: XCTestCase { v0.1.0 """) } - - func test_git_tags_2() throws { - // setup - let tempDir = NSTemporaryDirectory().appending("test_stress_\(UUID())") - defer { - try? Foundation.FileManager.default.removeItem(atPath: tempDir) - } - let sampleGitRepoName = "ErrNo" - let sampleGitRepoZipFile = fixturesDirectory() - .appendingPathComponent("\(sampleGitRepoName).zip").path - let path = "\(tempDir)/\(sampleGitRepoName)" - try! Foundation.FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: false, attributes: nil) - try! ShellOut.shellOut(to: .init(command: "unzip", arguments: [sampleGitRepoZipFile.quoted]), at: tempDir) - - // MUT - XCTAssertEqual(try shellOut2(to: "git", arguments: ["tag"], at: path).stdout, """ - 0.2.0 - 0.2.1 - 0.2.2 - 0.2.3 - 0.2.4 - 0.2.5 - 0.3.0 - 0.4.0 - 0.4.1 - 0.4.2 - 0.5.0 - 0.5.1 - 0.5.2 - v0.0.1 - v0.0.2 - v0.0.3 - v0.0.4 - v0.0.5 - v0.1.0 - """) - } } extension ShellOutTests { From d8182884a0ceda520f525919a9e4a4d57de61f8b Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Thu, 31 Aug 2023 10:25:19 +0200 Subject: [PATCH 10/27] Add swift-log dependency --- .gitignore | 1 + Package.resolved | 33 ++++++++++++++++++++------------- Package.swift | 7 +++++-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 0c62eec..bfb0969 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /Packages /*.xcodeproj .swiftpm +.vscode/ diff --git a/Package.resolved b/Package.resolved index 864b749..421b251 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,23 @@ { - "object": { - "pins": [ - { - "package": "ShellQuote", - "repositoryURL": "https://github.com/SwiftPackageIndex/ShellQuote", - "state": { - "branch": null, - "revision": "5f555550c30ef43d64b36b40c2c291a95d62580c", - "version": "1.0.2" - } + "pins" : [ + { + "identity" : "shellquote", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftPackageIndex/ShellQuote", + "state" : { + "revision" : "5f555550c30ef43d64b36b40c2c291a95d62580c", + "version" : "1.0.2" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 047aaaa..a25fcc5 100644 --- a/Package.swift +++ b/Package.swift @@ -16,18 +16,21 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/SwiftPackageIndex/ShellQuote", from: "1.0.2"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], targets: [ .target( name: "ShellOut", dependencies: [ - .product(name: "ShellQuote", package: "ShellQuote") + .product(name: "ShellQuote", package: "ShellQuote"), + .product(name: "Logging", package: "swift-log"), ], path: "Sources" ), .testTarget( name: "ShellOutTests", - dependencies: ["ShellOut"] + dependencies: ["ShellOut"], + exclude: ["Fixtures"] ) ] ) From 3606d8abf0a3097e0e87c6a86eaff21719f07935 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Thu, 31 Aug 2023 10:33:06 +0200 Subject: [PATCH 11/27] Add stdout/stderr read logging --- Sources/ShellOut.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index e3efdb4..be7d002 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -6,6 +6,7 @@ import Foundation import Dispatch +import Logging // MARK: - API @@ -33,6 +34,7 @@ import Dispatch arguments: [Argument] = [], at path: String = ".", process: Process = .init(), + logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil @@ -41,6 +43,7 @@ import Dispatch return try process.launchBash( with: command, + logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, environment: environment @@ -88,6 +91,7 @@ import Dispatch to command: ShellOutCommand, at path: String = ".", process: Process = .init(), + logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil @@ -97,6 +101,7 @@ import Dispatch arguments: command.arguments, at: path, process: process, + logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, environment: environment @@ -429,7 +434,7 @@ extension ShellOutCommand { // MARK: - Private private extension Process { - @discardableResult func launchBash(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { + @discardableResult func launchBash(with command: String, logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { self.executableURL = URL(fileURLWithPath: "/bin/bash") self.arguments = ["-c", command] @@ -454,6 +459,7 @@ private extension Process { outputPipe.fileHandleForReading.readabilityHandler = { handler in let data = handler.availableData + logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stdout (readabilityHandler)") outputQueue.async { outputData.append(data) outputHandle?.write(data) @@ -462,6 +468,7 @@ private extension Process { errorPipe.fileHandleForReading.readabilityHandler = { handler in let data = handler.availableData + logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stderr (readabilityHandler)") outputQueue.async { errorData.append(data) errorHandle?.write(data) @@ -479,11 +486,13 @@ private extension Process { // this part in an async block. outputQueue.async { if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { + logger?.info("ShellOut.launchBash: Read \(extraOutput.count) bytes from stdout (readToEnd)") outputData.append(extraOutput) outputHandle?.write(extraOutput) } if let extraError = try? errorPipe.fileHandleForReading.readToEnd() { + logger?.info("ShellOut.launchBash: Read \(extraError.count) bytes from stderr (readToEnd)") errorData.append(extraError) errorHandle?.write(extraError) } From 49fcdb68f6eafd0ee3d4a464401bc2134c92b1f0 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Thu, 31 Aug 2023 11:25:04 +0200 Subject: [PATCH 12/27] Log command --- Sources/ShellOut.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index be7d002..63ce8b2 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -459,7 +459,7 @@ private extension Process { outputPipe.fileHandleForReading.readabilityHandler = { handler in let data = handler.availableData - logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stdout (readabilityHandler)") + logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stdout (readabilityHandler, command: \(command))") outputQueue.async { outputData.append(data) outputHandle?.write(data) @@ -468,7 +468,7 @@ private extension Process { errorPipe.fileHandleForReading.readabilityHandler = { handler in let data = handler.availableData - logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stderr (readabilityHandler)") + logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stderr (readabilityHandler, command: \(command))") outputQueue.async { errorData.append(data) errorHandle?.write(data) @@ -486,13 +486,13 @@ private extension Process { // this part in an async block. outputQueue.async { if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { - logger?.info("ShellOut.launchBash: Read \(extraOutput.count) bytes from stdout (readToEnd)") + logger?.info("ShellOut.launchBash: Read \(extraOutput.count) bytes from stdout (readToEnd), command: \(command)") outputData.append(extraOutput) outputHandle?.write(extraOutput) } if let extraError = try? errorPipe.fileHandleForReading.readToEnd() { - logger?.info("ShellOut.launchBash: Read \(extraError.count) bytes from stderr (readToEnd)") + logger?.info("ShellOut.launchBash: Read \(extraError.count) bytes from stderr (readToEnd), command: \(command)") errorData.append(extraError) errorHandle?.write(extraError) } From a04e1e177c6bf9455711579262603a68cade8713 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Thu, 31 Aug 2023 14:31:14 +0200 Subject: [PATCH 13/27] barrier sync changes --- Sources/ShellOut.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 63ce8b2..a2768ce 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -479,12 +479,14 @@ private extension Process { self.waitUntilExit() + outputQueue.sync(flags: .barrier) {} + outputPipe.fileHandleForReading.readabilityHandler = nil errorPipe.fileHandleForReading.readabilityHandler = nil // Spend as little time as possible inside the sync() block by doing // this part in an async block. - outputQueue.async { + return try outputQueue.sync(flags: .barrier) { if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { logger?.info("ShellOut.launchBash: Read \(extraOutput.count) bytes from stdout (readToEnd), command: \(command)") outputData.append(extraOutput) @@ -496,9 +498,7 @@ private extension Process { errorData.append(extraError) errorHandle?.write(extraError) } - } - return try outputQueue.sync(flags: .barrier) { try outputPipe.fileHandleForReading.close() try errorPipe.fileHandleForReading.close() try outputHandle?.close() From 9b1179f5067a9bc2697042989e0901afbe30b8ea Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Thu, 31 Aug 2023 14:41:59 +0200 Subject: [PATCH 14/27] Explicitly flush available data (Linux doesn't signal) --- Sources/ShellOut.swift | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index a2768ce..13fba66 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -458,21 +458,32 @@ private extension Process { self.standardError = errorPipe outputPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stdout (readabilityHandler, command: \(command))") - outputQueue.async { - outputData.append(data) - outputHandle?.write(data) + var data = handler.availableData + while !data.isEmpty { + let readData = data + logger?.info("ShellOut.launchBash: Read \(readData.count) bytes from stdout (readabilityHandler, command: \(command))") + outputQueue.async { + logger?.info("ShellOut.launchBash: Reporting \(readData.count) bytes from stdout (readabilityHandler, command: \(command))") + outputData.append(readData) + outputHandle?.write(readData) + } + data = handler.availableData } } errorPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stderr (readabilityHandler, command: \(command))") - outputQueue.async { - errorData.append(data) - errorHandle?.write(data) + var data = handler.availableData + while !data.isEmpty { + let readData = data + logger?.info("ShellOut.launchBash: Read \(readData.count) bytes from stderr (readabilityHandler, command: \(command))") + outputQueue.async { + logger?.info("ShellOut.launchBash: Reporting \(readData.count) bytes from stderr (readabilityHandler, command: \(command))") + errorData.append(readData) + errorHandle?.write(readData) + } + data = handler.availableData } + } try self.run() From 8c8541aed31ab7bb939bc7c29662e22c466a4310 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Thu, 31 Aug 2023 15:46:07 +0200 Subject: [PATCH 15/27] Moar logs --- Sources/ShellOut.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 13fba66..98c56ab 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -490,6 +490,7 @@ private extension Process { self.waitUntilExit() + logger?.info("ShellOut.launchBash (1): \(outputData as NSData) (readabilityHandler, command: \(command))") outputQueue.sync(flags: .barrier) {} outputPipe.fileHandleForReading.readabilityHandler = nil @@ -497,32 +498,38 @@ private extension Process { // Spend as little time as possible inside the sync() block by doing // this part in an async block. + logger?.info("ShellOut.launchBash (2): \(outputData as NSData) (readabilityHandler, command: \(command))") return try outputQueue.sync(flags: .barrier) { + logger?.info("ShellOut.launchBash (3): \(outputData as NSData) (readabilityHandler, command: \(command))") if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { logger?.info("ShellOut.launchBash: Read \(extraOutput.count) bytes from stdout (readToEnd), command: \(command)") outputData.append(extraOutput) outputHandle?.write(extraOutput) } + logger?.info("ShellOut.launchBash (4): \(outputData as NSData) (readabilityHandler, command: \(command))") if let extraError = try? errorPipe.fileHandleForReading.readToEnd() { logger?.info("ShellOut.launchBash: Read \(extraError.count) bytes from stderr (readToEnd), command: \(command)") errorData.append(extraError) errorHandle?.write(extraError) } + logger?.info("ShellOut.launchBash (5): \(outputData as NSData) (readabilityHandler, command: \(command))") try outputPipe.fileHandleForReading.close() try errorPipe.fileHandleForReading.close() try outputHandle?.close() try errorHandle?.close() + logger?.info("ShellOut.launchBash (6): \(outputData as NSData) (readabilityHandler, command: \(command))") if self.terminationStatus != 0 { + logger?.info("ShellOut.launchBash (7): \(outputData as NSData) (readabilityHandler, command: \(command))") throw ShellOutError( terminationStatus: terminationStatus, errorData: errorData, outputData: outputData ) } - + logger?.info("ShellOut.launchBash (8): \(outputData as NSData) (readabilityHandler, command: \(command))") return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) } } From e250b44d26458fa5124f1ed578994db5be2079c1 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 10:02:25 +0200 Subject: [PATCH 16/27] DispatchGroup version --- Sources/ShellOut.swift | 98 ++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 57 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 98c56ab..8d2746b 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -442,94 +442,78 @@ private extension Process { self.environment = environment } + let outputPipe = Pipe(), errorPipe = Pipe() + self.standardOutput = outputPipe + self.standardError = errorPipe + // Because FileHandle's readabilityHandler might be called from a - // different queue from the calling queue, avoid a data race by + // different queue from the calling queue, avoid data races by // protecting reads and writes to outputData and errorData on // a single dispatch queue. let outputQueue = DispatchQueue(label: "bash-output-queue") + let outputGroup = DispatchGroup() + var outputData = Data(), errorData = Data() - var outputData = Data() - var errorData = Data() - - let outputPipe = Pipe() - self.standardOutput = outputPipe - - let errorPipe = Pipe() - self.standardError = errorPipe - + outputGroup.enter() outputPipe.fileHandleForReading.readabilityHandler = { handler in - var data = handler.availableData - while !data.isEmpty { - let readData = data - logger?.info("ShellOut.launchBash: Read \(readData.count) bytes from stdout (readabilityHandler, command: \(command))") + let data = handler.availableData + + if data.isEmpty { // EOF + logger?.info("ShellOut.launchBash: Reporting EOF on stdout (readabilityHandler, command: \(command))") + outputGroup.leave() + } else { + logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stdout (readabilityHandler, command: \(command))") outputQueue.async { - logger?.info("ShellOut.launchBash: Reporting \(readData.count) bytes from stdout (readabilityHandler, command: \(command))") - outputData.append(readData) - outputHandle?.write(readData) + logger?.info("ShellOut.launchBash: Reporting \(data.count) bytes from stdout (readabilityHandler, command: \(command))") + outputData.append(data) + outputHandle?.write(data) } - data = handler.availableData } } + outputGroup.enter() errorPipe.fileHandleForReading.readabilityHandler = { handler in - var data = handler.availableData - while !data.isEmpty { - let readData = data - logger?.info("ShellOut.launchBash: Read \(readData.count) bytes from stderr (readabilityHandler, command: \(command))") + let data = handler.availableData + + if data.isEmpty { // EOF + logger?.info("ShellOut.launchBash: Reporting EOF on stderr (readabilityHandler, command: \(command))") + outputGroup.leave() + } else { outputQueue.async { - logger?.info("ShellOut.launchBash: Reporting \(readData.count) bytes from stderr (readabilityHandler, command: \(command))") - errorData.append(readData) - errorHandle?.write(readData) + errorData.append(data) + errorHandle?.write(data) } - data = handler.availableData } - } try self.run() - self.waitUntilExit() - logger?.info("ShellOut.launchBash (1): \(outputData as NSData) (readabilityHandler, command: \(command))") - outputQueue.sync(flags: .barrier) {} - - outputPipe.fileHandleForReading.readabilityHandler = nil - errorPipe.fileHandleForReading.readabilityHandler = nil - - // Spend as little time as possible inside the sync() block by doing - // this part in an async block. - logger?.info("ShellOut.launchBash (2): \(outputData as NSData) (readabilityHandler, command: \(command))") - return try outputQueue.sync(flags: .barrier) { - logger?.info("ShellOut.launchBash (3): \(outputData as NSData) (readabilityHandler, command: \(command))") - if let extraOutput = try? outputPipe.fileHandleForReading.readToEnd() { - logger?.info("ShellOut.launchBash: Read \(extraOutput.count) bytes from stdout (readToEnd), command: \(command)") - outputData.append(extraOutput) - outputHandle?.write(extraOutput) - } - logger?.info("ShellOut.launchBash (4): \(outputData as NSData) (readabilityHandler, command: \(command))") + logger?.info("ShellOut.launchBash: Waiting on EOF... (command: \(command))") + if outputGroup.wait(timeout: .now() + .seconds(1)) == .timedOut { + logger?.info("ShellOut.launchBash: Warning: Timed out waiting for EOF! (command: \(command))") + } else { + logger?.info("ShellOut.launchBash: EOFs received (command: \(command))") + } - if let extraError = try? errorPipe.fileHandleForReading.readToEnd() { - logger?.info("ShellOut.launchBash: Read \(extraError.count) bytes from stderr (readToEnd), command: \(command)") - errorData.append(extraError) - errorHandle?.write(extraError) - } - logger?.info("ShellOut.launchBash (5): \(outputData as NSData) (readabilityHandler, command: \(command))") + // We know as of this point that either all blocks have been submitted to the + // queue already, or we've reached our wait timeout. + return try outputQueue.sync { + logger?.info("ShellOut.launchBash: Stdout: \(outputData as NSData) (command: \(command))") - try outputPipe.fileHandleForReading.close() - try errorPipe.fileHandleForReading.close() + // Do not try to readToEnd() here; if we already got an EOF, there's definitely + // nothing to read, and if we timed out, trying to read here will just block + // even longer. try outputHandle?.close() try errorHandle?.close() - logger?.info("ShellOut.launchBash (6): \(outputData as NSData) (readabilityHandler, command: \(command))") - if self.terminationStatus != 0 { - logger?.info("ShellOut.launchBash (7): \(outputData as NSData) (readabilityHandler, command: \(command))") + guard self.terminationStatus == 0, self.terminationReason == .exit else { throw ShellOutError( terminationStatus: terminationStatus, errorData: errorData, outputData: outputData ) } - logger?.info("ShellOut.launchBash (8): \(outputData as NSData) (readabilityHandler, command: \(command))") return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) } } From e8cee8e629d019be110682ca8a0398b1c064930b Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 11:07:43 +0200 Subject: [PATCH 17/27] 100ms timeout --- Sources/ShellOut.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 8d2746b..ffee0ce 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -490,7 +490,7 @@ private extension Process { self.waitUntilExit() logger?.info("ShellOut.launchBash: Waiting on EOF... (command: \(command))") - if outputGroup.wait(timeout: .now() + .seconds(1)) == .timedOut { + if outputGroup.wait(timeout: .now() + .milliseconds(100)) == .timedOut { logger?.info("ShellOut.launchBash: Warning: Timed out waiting for EOF! (command: \(command))") } else { logger?.info("ShellOut.launchBash: EOFs received (command: \(command))") From d8c7bb5d3a2eec4609eea968406262ba4235e5f3 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 12:40:08 +0200 Subject: [PATCH 18/27] Reset readabilityHandler --- Sources/ShellOut.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index ffee0ce..2044684 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -460,6 +460,7 @@ private extension Process { if data.isEmpty { // EOF logger?.info("ShellOut.launchBash: Reporting EOF on stdout (readabilityHandler, command: \(command))") + handler.readabilityHandler = nil outputGroup.leave() } else { logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stdout (readabilityHandler, command: \(command))") @@ -477,6 +478,7 @@ private extension Process { if data.isEmpty { // EOF logger?.info("ShellOut.launchBash: Reporting EOF on stderr (readabilityHandler, command: \(command))") + handler.readabilityHandler = nil outputGroup.leave() } else { outputQueue.async { From f8510128183b02dd2341ec660482c150f65b333e Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 12:43:40 +0200 Subject: [PATCH 19/27] Reduce logging --- Sources/ShellOut.swift | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 2044684..f33a4a7 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -459,13 +459,10 @@ private extension Process { let data = handler.availableData if data.isEmpty { // EOF - logger?.info("ShellOut.launchBash: Reporting EOF on stdout (readabilityHandler, command: \(command))") handler.readabilityHandler = nil outputGroup.leave() } else { - logger?.info("ShellOut.launchBash: Read \(data.count) bytes from stdout (readabilityHandler, command: \(command))") outputQueue.async { - logger?.info("ShellOut.launchBash: Reporting \(data.count) bytes from stdout (readabilityHandler, command: \(command))") outputData.append(data) outputHandle?.write(data) } @@ -477,7 +474,6 @@ private extension Process { let data = handler.availableData if data.isEmpty { // EOF - logger?.info("ShellOut.launchBash: Reporting EOF on stderr (readabilityHandler, command: \(command))") handler.readabilityHandler = nil outputGroup.leave() } else { @@ -491,18 +487,13 @@ private extension Process { try self.run() self.waitUntilExit() - logger?.info("ShellOut.launchBash: Waiting on EOF... (command: \(command))") if outputGroup.wait(timeout: .now() + .milliseconds(100)) == .timedOut { - logger?.info("ShellOut.launchBash: Warning: Timed out waiting for EOF! (command: \(command))") - } else { - logger?.info("ShellOut.launchBash: EOFs received (command: \(command))") + logger?.warning("ShellOut.launchBash: Timed out waiting for EOF! (command: \(command))") } // We know as of this point that either all blocks have been submitted to the // queue already, or we've reached our wait timeout. return try outputQueue.sync { - logger?.info("ShellOut.launchBash: Stdout: \(outputData as NSData) (command: \(command))") - // Do not try to readToEnd() here; if we already got an EOF, there's definitely // nothing to read, and if we timed out, trying to read here will just block // even longer. From 37912360e701f8b12789758999035bf58529bc35 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 12:45:48 +0200 Subject: [PATCH 20/27] Make eofTimeout configurable --- Sources/ShellOut.swift | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index f33a4a7..632066f 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -37,7 +37,8 @@ import Logging logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, - environment: [String : String]? = nil + environment: [String : String]? = nil, + eofTimeout: DispatchTimeInterval = .milliseconds(10) ) throws -> (stdout: String, stderr: String) { let command = "cd \(path.escapingSpaces) && \(command) \(arguments.map(\.string).joined(separator: " "))" @@ -46,7 +47,8 @@ import Logging logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, - environment: environment + environment: environment, + eofTimeout: eofTimeout ) } @@ -94,7 +96,8 @@ import Logging logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, - environment: [String : String]? = nil + environment: [String : String]? = nil, + eofTimeout: DispatchTimeInterval = .milliseconds(10) ) throws -> (stdout: String, stderr: String) { try shellOut( to: command.command, @@ -104,7 +107,8 @@ import Logging logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, - environment: environment + environment: environment, + eofTimeout: eofTimeout ) } @@ -434,7 +438,14 @@ extension ShellOutCommand { // MARK: - Private private extension Process { - @discardableResult func launchBash(with command: String, logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { + @discardableResult func launchBash( + with command: String, + logger: Logger? = nil, + outputHandle: FileHandle? = nil, + errorHandle: FileHandle? = nil, + environment: [String : String]? = nil, + eofTimeout: DispatchTimeInterval = .milliseconds(10) + ) throws -> (stdout: String, stderr: String) { self.executableURL = URL(fileURLWithPath: "/bin/bash") self.arguments = ["-c", command] @@ -487,7 +498,7 @@ private extension Process { try self.run() self.waitUntilExit() - if outputGroup.wait(timeout: .now() + .milliseconds(100)) == .timedOut { + if outputGroup.wait(timeout: .now() + eofTimeout) == .timedOut { logger?.warning("ShellOut.launchBash: Timed out waiting for EOF! (command: \(command))") } From 826125cf5d53267cd5029243a7bc483c94207c38 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 16:45:54 +0200 Subject: [PATCH 21/27] Remove swift-log dependency --- Package.resolved | 9 --------- Package.swift | 6 ++---- Sources/ShellOut.swift | 10 +--------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/Package.resolved b/Package.resolved index 421b251..36a38f9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -8,15 +8,6 @@ "revision" : "5f555550c30ef43d64b36b40c2c291a95d62580c", "version" : "1.0.2" } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" - } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index a25fcc5..9620830 100644 --- a/Package.swift +++ b/Package.swift @@ -15,15 +15,13 @@ let package = Package( .library(name: "ShellOut", targets: ["ShellOut"]) ], dependencies: [ - .package(url: "https://github.com/SwiftPackageIndex/ShellQuote", from: "1.0.2"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), + .package(url: "https://github.com/SwiftPackageIndex/ShellQuote", from: "1.0.2") ], targets: [ .target( name: "ShellOut", dependencies: [ - .product(name: "ShellQuote", package: "ShellQuote"), - .product(name: "Logging", package: "swift-log"), + .product(name: "ShellQuote", package: "ShellQuote") ], path: "Sources" ), diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 632066f..bb4a9e4 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -6,7 +6,6 @@ import Foundation import Dispatch -import Logging // MARK: - API @@ -34,7 +33,6 @@ import Logging arguments: [Argument] = [], at path: String = ".", process: Process = .init(), - logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil, @@ -44,7 +42,6 @@ import Logging return try process.launchBash( with: command, - logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, environment: environment, @@ -93,7 +90,6 @@ import Logging to command: ShellOutCommand, at path: String = ".", process: Process = .init(), - logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil, @@ -104,7 +100,6 @@ import Logging arguments: command.arguments, at: path, process: process, - logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, environment: environment, @@ -440,7 +435,6 @@ extension ShellOutCommand { private extension Process { @discardableResult func launchBash( with command: String, - logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil, @@ -498,9 +492,7 @@ private extension Process { try self.run() self.waitUntilExit() - if outputGroup.wait(timeout: .now() + eofTimeout) == .timedOut { - logger?.warning("ShellOut.launchBash: Timed out waiting for EOF! (command: \(command))") - } + _ = outputGroup.wait(timeout: .now() + eofTimeout) // We know as of this point that either all blocks have been submitted to the // queue already, or we've reached our wait timeout. From 5077225255a303e94c8422360b15607892bd3fc2 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 16:53:54 +0200 Subject: [PATCH 22/27] Use SocketPair --- Sources/ShellOut.swift | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index bb4a9e4..84211e3 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -447,9 +447,10 @@ private extension Process { self.environment = environment } - let outputPipe = Pipe(), errorPipe = Pipe() - self.standardOutput = outputPipe - self.standardError = errorPipe + let outputPipe = SocketPair() + let errorPipe = SocketPair() + self.standardOutput = outputPipe.fileHandleForWriting + self.standardError = errorPipe.fileHandleForWriting // Because FileHandle's readabilityHandler might be called from a // different queue from the calling queue, avoid data races by @@ -646,3 +647,30 @@ private extension String { self = appending(arguments: arguments) } } + + +final class SocketPair { + let fileHandleForReading: FileHandle + let fileHandleForWriting: FileHandle + + init() { + let fds = UnsafeMutablePointer.allocate(capacity: 2) + defer { fds.deallocate() } + +#if os(macOS) + let ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fds) +#else + let ret = socketpair(AF_UNIX, Int32(SOCK_STREAM.rawValue), 0, fds) +#endif + switch (ret, errno) { + case (0, _): + self.fileHandleForReading = FileHandle(fileDescriptor: fds.pointee, closeOnDealloc: true) + self.fileHandleForWriting = FileHandle(fileDescriptor: fds.successor().pointee, closeOnDealloc: true) + case (-1, EMFILE), (-1, ENFILE): + self.fileHandleForReading = FileHandle(fileDescriptor: -1, closeOnDealloc: false) + self.fileHandleForWriting = FileHandle(fileDescriptor: -1, closeOnDealloc: false) + default: + fatalError("Error calling socketpair(): \(errno)") + } + } +} From 07fea997c9068e5e2fe78d8acf77c02f94c2f3ee Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 17:14:51 +0200 Subject: [PATCH 23/27] Revert "Remove swift-log dependency" This reverts commit 826125cf5d53267cd5029243a7bc483c94207c38. --- Package.resolved | 9 +++++++++ Package.swift | 6 ++++-- Sources/ShellOut.swift | 10 +++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 36a38f9..421b251 100644 --- a/Package.resolved +++ b/Package.resolved @@ -8,6 +8,15 @@ "revision" : "5f555550c30ef43d64b36b40c2c291a95d62580c", "version" : "1.0.2" } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 9620830..a25fcc5 100644 --- a/Package.swift +++ b/Package.swift @@ -15,13 +15,15 @@ let package = Package( .library(name: "ShellOut", targets: ["ShellOut"]) ], dependencies: [ - .package(url: "https://github.com/SwiftPackageIndex/ShellQuote", from: "1.0.2") + .package(url: "https://github.com/SwiftPackageIndex/ShellQuote", from: "1.0.2"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], targets: [ .target( name: "ShellOut", dependencies: [ - .product(name: "ShellQuote", package: "ShellQuote") + .product(name: "ShellQuote", package: "ShellQuote"), + .product(name: "Logging", package: "swift-log"), ], path: "Sources" ), diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 84211e3..092e0aa 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -6,6 +6,7 @@ import Foundation import Dispatch +import Logging // MARK: - API @@ -33,6 +34,7 @@ import Dispatch arguments: [Argument] = [], at path: String = ".", process: Process = .init(), + logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil, @@ -42,6 +44,7 @@ import Dispatch return try process.launchBash( with: command, + logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, environment: environment, @@ -90,6 +93,7 @@ import Dispatch to command: ShellOutCommand, at path: String = ".", process: Process = .init(), + logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil, @@ -100,6 +104,7 @@ import Dispatch arguments: command.arguments, at: path, process: process, + logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, environment: environment, @@ -435,6 +440,7 @@ extension ShellOutCommand { private extension Process { @discardableResult func launchBash( with command: String, + logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil, @@ -493,7 +499,9 @@ private extension Process { try self.run() self.waitUntilExit() - _ = outputGroup.wait(timeout: .now() + eofTimeout) + if outputGroup.wait(timeout: .now() + eofTimeout) == .timedOut { + logger?.warning("ShellOut.launchBash: Timed out waiting for EOF! (command: \(command))") + } // We know as of this point that either all blocks have been submitted to the // queue already, or we've reached our wait timeout. From 0450bd9fb74a486a2b926dc1c121bcb780eddf4f Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 18:56:42 +0200 Subject: [PATCH 24/27] Revert "Use SocketPair" This reverts commit 5077225255a303e94c8422360b15607892bd3fc2. --- Sources/ShellOut.swift | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 092e0aa..632066f 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -453,10 +453,9 @@ private extension Process { self.environment = environment } - let outputPipe = SocketPair() - let errorPipe = SocketPair() - self.standardOutput = outputPipe.fileHandleForWriting - self.standardError = errorPipe.fileHandleForWriting + let outputPipe = Pipe(), errorPipe = Pipe() + self.standardOutput = outputPipe + self.standardError = errorPipe // Because FileHandle's readabilityHandler might be called from a // different queue from the calling queue, avoid data races by @@ -655,30 +654,3 @@ private extension String { self = appending(arguments: arguments) } } - - -final class SocketPair { - let fileHandleForReading: FileHandle - let fileHandleForWriting: FileHandle - - init() { - let fds = UnsafeMutablePointer.allocate(capacity: 2) - defer { fds.deallocate() } - -#if os(macOS) - let ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fds) -#else - let ret = socketpair(AF_UNIX, Int32(SOCK_STREAM.rawValue), 0, fds) -#endif - switch (ret, errno) { - case (0, _): - self.fileHandleForReading = FileHandle(fileDescriptor: fds.pointee, closeOnDealloc: true) - self.fileHandleForWriting = FileHandle(fileDescriptor: fds.successor().pointee, closeOnDealloc: true) - case (-1, EMFILE), (-1, ENFILE): - self.fileHandleForReading = FileHandle(fileDescriptor: -1, closeOnDealloc: false) - self.fileHandleForWriting = FileHandle(fileDescriptor: -1, closeOnDealloc: false) - default: - fatalError("Error calling socketpair(): \(errno)") - } - } -} From 6571403f63cbb51857de4eab0211cbc7c1810810 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 1 Sep 2023 18:57:41 +0200 Subject: [PATCH 25/27] Change timeout warning to debug level --- Sources/ShellOut.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 632066f..49b8b0c 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -499,7 +499,7 @@ private extension Process { self.waitUntilExit() if outputGroup.wait(timeout: .now() + eofTimeout) == .timedOut { - logger?.warning("ShellOut.launchBash: Timed out waiting for EOF! (command: \(command))") + logger?.debug("ShellOut.launchBash: Timed out waiting for EOF! (command: \(command))") } // We know as of this point that either all blocks have been submitted to the From deba1178b71334c9be74a7e320e795307cf9af85 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Fri, 1 Sep 2023 12:55:50 -0500 Subject: [PATCH 26/27] Reimplement launchBash() based on TSCBasic.Process, which is much more advanced than Foundation's version at this time. --- Package.resolved | 36 ++++++ Package.swift | 4 + Sources/ShellOut.swift | 147 +++++++++--------------- Tests/ShellOutTests/ShellOutTests.swift | 107 +++++++++-------- 4 files changed, 156 insertions(+), 138 deletions(-) diff --git a/Package.resolved b/Package.resolved index 421b251..671fd93 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", + "version" : "1.0.0" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -17,6 +26,33 @@ "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", "version" : "1.5.3" } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-tools-support-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-tools-support-core.git", + "state" : { + "revision" : "93784c59434dbca8e8a9e4b700d0d6d94551da6a", + "version" : "0.5.2" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index a25fcc5..22b2edd 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/SwiftPackageIndex/ShellQuote", from: "1.0.2"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.5.2"), + .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), ], targets: [ .target( @@ -24,6 +26,8 @@ let package = Package( dependencies: [ .product(name: "ShellQuote", package: "ShellQuote"), .product(name: "Logging", package: "swift-log"), + .product(name: "Algorithms", package: "swift-algorithms"), + .product(name: "TSCBasic", package: "swift-tools-support-core"), ], path: "Sources" ), diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index 49b8b0c..b8cb36b 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -7,6 +7,8 @@ import Foundation import Dispatch import Logging +import TSCBasic +import Algorithms // MARK: - API @@ -33,22 +35,23 @@ import Logging to command: SafeString, arguments: [Argument] = [], at path: String = ".", - process: Process = .init(), logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, - environment: [String : String]? = nil, - eofTimeout: DispatchTimeInterval = .milliseconds(10) -) throws -> (stdout: String, stderr: String) { - let command = "cd \(path.escapingSpaces) && \(command) \(arguments.map(\.string).joined(separator: " "))" + environment: [String : String]? = nil +) async throws -> (stdout: String, stderr: String) { + let command = "\(command) \(arguments.map(\.string).joined(separator: " "))" - return try process.launchBash( + return try await TSCBasic.Process.launchBash( with: command, logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, environment: environment, - eofTimeout: eofTimeout + at: path == "." ? nil : + (path == "~" ? TSCBasic.localFileSystem.homeDirectory.pathString : + (path.starts(with: "~/") ? "\(TSCBasic.localFileSystem.homeDirectory.pathString)/\(path.dropFirst(2))" : + path)) ) } @@ -56,7 +59,7 @@ import Logging to command: SafeString, arguments: [Argument] = [], at path: String = ".", - process: Process = .init(), + process: Foundation.Process = .init(), outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil @@ -92,30 +95,26 @@ import Logging @discardableResult public func shellOut( to command: ShellOutCommand, at path: String = ".", - process: Process = .init(), logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, - environment: [String : String]? = nil, - eofTimeout: DispatchTimeInterval = .milliseconds(10) -) throws -> (stdout: String, stderr: String) { - try shellOut( + environment: [String : String]? = nil +) async throws -> (stdout: String, stderr: String) { + try await shellOut( to: command.command, arguments: command.arguments, at: path, - process: process, logger: logger, outputHandle: outputHandle, errorHandle: errorHandle, - environment: environment, - eofTimeout: eofTimeout + environment: environment ) } @discardableResult public func shellOutOldVersion( to command: ShellOutCommand, at path: String = ".", - process: Process = .init(), + process: Foundation.Process = .init(), outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil @@ -437,91 +436,53 @@ extension ShellOutCommand { // MARK: - Private -private extension Process { - @discardableResult func launchBash( +private extension TSCBasic.Process { + @discardableResult static func launchBash( with command: String, logger: Logger? = nil, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil, - eofTimeout: DispatchTimeInterval = .milliseconds(10) - ) throws -> (stdout: String, stderr: String) { - self.executableURL = URL(fileURLWithPath: "/bin/bash") - self.arguments = ["-c", command] - - if let environment { - self.environment = environment - } - - let outputPipe = Pipe(), errorPipe = Pipe() - self.standardOutput = outputPipe - self.standardError = errorPipe - - // Because FileHandle's readabilityHandler might be called from a - // different queue from the calling queue, avoid data races by - // protecting reads and writes to outputData and errorData on - // a single dispatch queue. - let outputQueue = DispatchQueue(label: "bash-output-queue") - let outputGroup = DispatchGroup() - var outputData = Data(), errorData = Data() - - outputGroup.enter() - outputPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - - if data.isEmpty { // EOF - handler.readabilityHandler = nil - outputGroup.leave() - } else { - outputQueue.async { - outputData.append(data) - outputHandle?.write(data) - } + at: String? = nil + ) async throws -> (stdout: String, stderr: String) { + let process = try Self.init( + arguments: ["/bin/bash", "-c", command], + environment: environment ?? ProcessEnv.vars, + workingDirectory: at.map { try .init(validating: $0) } ?? TSCBasic.localFileSystem.currentWorkingDirectory ?? .root, + outputRedirection: .collect(redirectStderr: false), + startNewProcessGroup: false, + loggingHandler: nil + ) + + try process.launch() + + let result = try await process.waitUntilExit() + + try outputHandle?.write(contentsOf: (try? result.output.get()) ?? []) + try outputHandle?.close() + try errorHandle?.write(contentsOf: (try? result.stderrOutput.get()) ?? []) + try errorHandle?.close() + + guard case .terminated(code: let code) = result.exitStatus, code == 0 else { + let code: Int32 + switch result.exitStatus { + case .terminated(code: let termCode): code = termCode + case .signalled(signal: let sigNo): code = -sigNo } + throw ShellOutError( + terminationStatus: code, + errorData: Data((try? result.stderrOutput.get()) ?? []), + outputData: Data((try? result.output.get()) ?? []) + ) } - - outputGroup.enter() - errorPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - - if data.isEmpty { // EOF - handler.readabilityHandler = nil - outputGroup.leave() - } else { - outputQueue.async { - errorData.append(data) - errorHandle?.write(data) - } - } - } - - try self.run() - self.waitUntilExit() - - if outputGroup.wait(timeout: .now() + eofTimeout) == .timedOut { - logger?.debug("ShellOut.launchBash: Timed out waiting for EOF! (command: \(command))") - } - - // We know as of this point that either all blocks have been submitted to the - // queue already, or we've reached our wait timeout. - return try outputQueue.sync { - // Do not try to readToEnd() here; if we already got an EOF, there's definitely - // nothing to read, and if we timed out, trying to read here will just block - // even longer. - try outputHandle?.close() - try errorHandle?.close() - - guard self.terminationStatus == 0, self.terminationReason == .exit else { - throw ShellOutError( - terminationStatus: terminationStatus, - errorData: errorData, - outputData: outputData - ) - } - return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) - } + return try ( + stdout: String(result.utf8Output().trimmingSuffix(while: \.isNewline)), + stderr: String(result.utf8stderrOutput().trimmingSuffix(while: \.isNewline)) + ) } +} +extension Foundation.Process { @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { #if os(Linux) executableURL = URL(fileURLWithPath: "/bin/bash") diff --git a/Tests/ShellOutTests/ShellOutTests.swift b/Tests/ShellOutTests/ShellOutTests.swift index 989ee55..d5fb448 100644 --- a/Tests/ShellOutTests/ShellOutTests.swift +++ b/Tests/ShellOutTests/ShellOutTests.swift @@ -7,6 +7,24 @@ import XCTest @testable import ShellOut +func XCTAssertEqualAsync( + _ expression1: @autoclosure () async throws -> T, + _ expression2: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) async where T: Equatable { + do { + let expr1 = try await expression1() + let expr2 = try await expression2() + + return XCTAssertEqual(expr1, expr2, message(), file: file, line: line) + } catch { + // Trick XCTest into behaving correctly for a thrown error. + return XCTAssertEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line) + } +} + class ShellOutTests: XCTestCase { func test_appendArguments() throws { var cmd = try ShellOutCommand(command: "foo") @@ -31,24 +49,24 @@ class ShellOutTests: XCTestCase { ) } - func testWithoutArguments() throws { - let uptime = try shellOut(to: "uptime".checked).stdout + func testWithoutArguments() async throws { + let uptime = try await shellOut(to: "uptime".checked).stdout XCTAssertTrue(uptime.contains("load average")) } - func testWithArguments() throws { - let echo = try shellOut(to: "echo".checked, arguments: ["Hello world".quoted]).stdout + func testWithArguments() async throws { + let echo = try await shellOut(to: "echo".checked, arguments: ["Hello world".quoted]).stdout XCTAssertEqual(echo, "Hello world") } - func testSingleCommandAtPath() throws { + func testSingleCommandAtPath() async throws { let tempDir = NSTemporaryDirectory() - try shellOut( + try await shellOut( to: "echo".checked, arguments: ["Hello", ">".verbatim, "\(tempDir)ShellOutTests-SingleCommand.txt".quoted] ) - let textFileContent = try shellOut( + let textFileContent = try await shellOut( to: "cat".checked, arguments: ["ShellOutTests-SingleCommand.txt".quoted], at: tempDir @@ -57,27 +75,27 @@ class ShellOutTests: XCTestCase { XCTAssertEqual(textFileContent, "Hello") } - func testSingleCommandAtPathContainingSpace() throws { - try shellOut(to: "mkdir".checked, + func testSingleCommandAtPathContainingSpace() async throws { + try await shellOut(to: "mkdir".checked, arguments: ["-p".verbatim, "ShellOut Test Folder".quoted], at: NSTemporaryDirectory()) - try shellOut(to: "echo".checked, arguments: ["Hello", ">", "File"].verbatim, + try await shellOut(to: "echo".checked, arguments: ["Hello", ">", "File"].verbatim, at: NSTemporaryDirectory() + "ShellOut Test Folder") - let output = try shellOut( + let output = try await shellOut( to: "cat".checked, arguments: ["\(NSTemporaryDirectory())ShellOut Test Folder/File".quoted]).stdout XCTAssertEqual(output, "Hello") } - func testSingleCommandAtPathContainingTilde() throws { - let homeContents = try shellOut(to: "ls".checked, arguments: ["-a"], at: "~").stdout + func testSingleCommandAtPathContainingTilde() async throws { + let homeContents = try await shellOut(to: "ls".checked, arguments: ["-a"], at: "~").stdout XCTAssertFalse(homeContents.isEmpty) } - func testThrowingError() { + func testThrowingError() async { do { - try shellOut(to: "cd".checked, arguments: ["notADirectory".verbatim]) + try await shellOut(to: "cd".checked, arguments: ["notADirectory".verbatim]) XCTFail("Expected expression to throw") } catch let error as ShellOutError { XCTAssertTrue(error.message.contains("notADirectory")) @@ -109,9 +127,9 @@ class ShellOutTests: XCTestCase { XCTAssertEqual(error.localizedDescription, expectedErrorDescription) } - func testCapturingOutputWithHandle() throws { + func testCapturingOutputWithHandle() async throws { let pipe = Pipe() - let output = try shellOut(to: "echo".checked, + let output = try await shellOut(to: "echo".checked, arguments: ["Hello".verbatim], outputHandle: pipe.fileHandleForWriting).stdout let capturedData = pipe.fileHandleForReading.readDataToEndOfFile() @@ -119,11 +137,11 @@ class ShellOutTests: XCTestCase { XCTAssertEqual(output + "\n", String(data: capturedData, encoding: .utf8)) } - func testCapturingErrorWithHandle() throws { + func testCapturingErrorWithHandle() async throws { let pipe = Pipe() do { - try shellOut(to: "cd".checked, + try await shellOut(to: "cd".checked, arguments: ["notADirectory".verbatim], errorHandle: pipe.fileHandleForWriting) XCTFail("Expected expression to throw") @@ -139,53 +157,52 @@ class ShellOutTests: XCTestCase { } } - func test_createFile() throws { + func test_createFile() async throws { let tempFolderPath = NSTemporaryDirectory() - try shellOut(to: .createFile(named: "Test", contents: "Hello world"), - at: tempFolderPath) - XCTAssertEqual(try shellOut(to: .readFile(at: tempFolderPath + "Test")).stdout, - "Hello world") + try await shellOut(to: .createFile(named: "Test", contents: "Hello world"), + at: tempFolderPath, logger: .init(label: "test")) + await XCTAssertEqualAsync(try await shellOut(to: .readFile(at: tempFolderPath + "Test")).stdout, "Hello world") } - func testGitCommands() throws { + func testGitCommands() async throws { // Setup & clear state let tempFolderPath = NSTemporaryDirectory() - try shellOut(to: "rm".checked, + try await shellOut(to: "rm".checked, arguments: ["-rf", "GitTestOrigin"].verbatim, - at: tempFolderPath) - try shellOut(to: "rm".checked, + at: tempFolderPath, logger: .init(label: "test")) + try await shellOut(to: "rm".checked, arguments: ["-rf", "GitTestClone"].verbatim, - at: tempFolderPath) + at: tempFolderPath, logger: .init(label: "test")) // Create a origin repository and make a commit with a file let originPath = tempFolderPath + "/GitTestOrigin" - try shellOut(to: .createFolder(named: "GitTestOrigin"), at: tempFolderPath) - try shellOut(to: .gitInit(), at: originPath) - try shellOut(to: .createFile(named: "Test", contents: "Hello world"), at: originPath) - try shellOut(to: .gitCommit(message: "Commit"), at: originPath) + try await shellOut(to: .createFolder(named: "GitTestOrigin"), at: tempFolderPath, logger: .init(label: "test")) + try await shellOut(to: .gitInit(), at: originPath, logger: .init(label: "test")) + try await shellOut(to: .createFile(named: "Test", contents: "Hello world"), at: originPath, logger: .init(label: "test")) + try await shellOut(to: .gitCommit(message: "Commit"), at: originPath, logger: .init(label: "test")) // Clone to a new repository and read the file let clonePath = tempFolderPath + "/GitTestClone" let cloneURL = URL(fileURLWithPath: originPath) - try shellOut(to: .gitClone(url: cloneURL, to: "GitTestClone"), at: tempFolderPath) + try await shellOut(to: .gitClone(url: cloneURL, to: "GitTestClone"), at: tempFolderPath, logger: .init(label: "test")) let filePath = clonePath + "/Test" - XCTAssertEqual(try shellOut(to: .readFile(at: filePath)).stdout, "Hello world") + await XCTAssertEqualAsync(try await shellOut(to: .readFile(at: filePath), logger: .init(label: "test")).stdout, "Hello world") // Make a new commit in the origin repository - try shellOut(to: .createFile(named: "Test", contents: "Hello again"), at: originPath) - try shellOut(to: .gitCommit(message: "Commit"), at: originPath) + try await shellOut(to: .createFile(named: "Test", contents: "Hello again"), at: originPath, logger: .init(label: "test")) + try await shellOut(to: .gitCommit(message: "Commit"), at: originPath, logger: .init(label: "test")) // Pull the commit in the clone repository and read the file again - try shellOut(to: .gitPull(), at: clonePath) - XCTAssertEqual(try shellOut(to: .readFile(at: filePath)).stdout, "Hello again") + try await shellOut(to: .gitPull(), at: clonePath) + await XCTAssertEqualAsync(try await shellOut(to: .readFile(at: filePath), logger: .init(label: "test")).stdout, "Hello again") } - func testArgumentQuoting() throws { - XCTAssertEqual(try shellOut(to: "echo".checked, + func testArgumentQuoting() async throws { + await XCTAssertEqualAsync(try await shellOut(to: "echo".checked, arguments: ["foo ; echo bar".quoted]).stdout, "foo ; echo bar") - XCTAssertEqual(try shellOut(to: "echo".checked, + await XCTAssertEqualAsync(try await shellOut(to: "echo".checked, arguments: ["foo ; echo bar".verbatim]).stdout, "foo\nbar") } @@ -200,7 +217,7 @@ class ShellOutTests: XCTestCase { "https://example.com") } - func test_git_tags() throws { + func test_git_tags() async throws { // setup let tempDir = NSTemporaryDirectory().appending("test_stress_\(UUID())") defer { @@ -211,10 +228,10 @@ class ShellOutTests: XCTestCase { .appendingPathComponent("\(sampleGitRepoName).zip").path let path = "\(tempDir)/\(sampleGitRepoName)" try! Foundation.FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: false, attributes: nil) - try! ShellOut.shellOut(to: .init(command: "unzip", arguments: [sampleGitRepoZipFile.quoted]), at: tempDir) + try! await ShellOut.shellOut(to: .init(command: "unzip", arguments: [sampleGitRepoZipFile.quoted]), at: tempDir) // MUT - XCTAssertEqual(try shellOut(to: try ShellOutCommand(command: "git", arguments: ["tag"]), + await XCTAssertEqualAsync(try await shellOut(to: try ShellOutCommand(command: "git", arguments: ["tag"]), at: path).stdout, """ 0.2.0 0.2.1 From 27af7ad0b4e9e470a940ec0172c01ee1ffa07f70 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Sat, 2 Sep 2023 08:52:54 +0200 Subject: [PATCH 27/27] Remove old version --- Sources/ShellOut.swift | 159 ----------------------------------------- 1 file changed, 159 deletions(-) diff --git a/Sources/ShellOut.swift b/Sources/ShellOut.swift index b8cb36b..9566b85 100644 --- a/Sources/ShellOut.swift +++ b/Sources/ShellOut.swift @@ -55,25 +55,6 @@ import Algorithms ) } -@discardableResult public func shellOutOldVersion( - to command: SafeString, - arguments: [Argument] = [], - at path: String = ".", - process: Foundation.Process = .init(), - outputHandle: FileHandle? = nil, - errorHandle: FileHandle? = nil, - environment: [String : String]? = nil -) throws -> (stdout: String, stderr: String) { - let command = "cd \(path.escapingSpaces) && \(command) \(arguments.map(\.string).joined(separator: " "))" - - return try process.launchBashOldVersion( - with: command, - outputHandle: outputHandle, - errorHandle: errorHandle, - environment: environment - ) -} - /** * Run a pre-defined shell command using Bash * @@ -111,25 +92,6 @@ import Algorithms ) } -@discardableResult public func shellOutOldVersion( - to command: ShellOutCommand, - at path: String = ".", - process: Foundation.Process = .init(), - outputHandle: FileHandle? = nil, - errorHandle: FileHandle? = nil, - environment: [String : String]? = nil -) throws -> (stdout: String, stderr: String) { - try shellOutOldVersion( - to: command.command, - arguments: command.arguments, - at: path, - process: process, - outputHandle: outputHandle, - errorHandle: errorHandle, - environment: environment - ) -} - /// Structure used to pre-define commands for use with ShellOut public struct ShellOutCommand { /// The string that makes up the command that should be run on the command line @@ -482,105 +444,6 @@ private extension TSCBasic.Process { } } -extension Foundation.Process { - @discardableResult func launchBashOldVersion(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> (stdout: String, stderr: String) { -#if os(Linux) - executableURL = URL(fileURLWithPath: "/bin/bash") -#else - launchPath = "/bin/bash" -#endif - arguments = ["-c", command] - - if let environment = environment { - self.environment = environment - } - - // Because FileHandle's readabilityHandler might be called from a - // different queue from the calling queue, avoid a data race by - // protecting reads and writes to outputData and errorData on - // a single dispatch queue. - let outputQueue = DispatchQueue(label: "bash-output-queue") - - var outputData = Data() - var errorData = Data() - - let outputPipe = Pipe() - standardOutput = outputPipe - - let errorPipe = Pipe() - standardError = errorPipe - - #if !os(Linux) - outputPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - outputQueue.async { - outputData.append(data) - outputHandle?.write(data) - } - } - - errorPipe.fileHandleForReading.readabilityHandler = { handler in - let data = handler.availableData - outputQueue.async { - errorData.append(data) - errorHandle?.write(data) - } - } - #endif - -#if os(Linux) - try run() -#else - launch() -#endif - - #if os(Linux) - outputQueue.sync { - outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() - errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() - } - #endif - - waitUntilExit() - - if let handle = outputHandle, !handle.isStandard { - handle.closeFile() - } - - if let handle = errorHandle, !handle.isStandard { - handle.closeFile() - } - - #if !os(Linux) - outputPipe.fileHandleForReading.readabilityHandler = nil - errorPipe.fileHandleForReading.readabilityHandler = nil - #endif - - // Block until all writes have occurred to outputData and errorData, - // and then read the data back out. - return try outputQueue.sync { - if terminationStatus != 0 { - throw ShellOutError( - terminationStatus: terminationStatus, - errorData: errorData, - outputData: outputData - ) - } - - return (stdout: outputData.shellOutput(), stderr: errorData.shellOutput()) - } - } - -} - -private extension FileHandle { - var isStandard: Bool { - return self === FileHandle.standardOutput || - self === FileHandle.standardError || - self === FileHandle.standardInput - } -} - private extension Data { func shellOutput() -> String { let output = String(decoding: self, as: UTF8.self) @@ -593,25 +456,3 @@ private extension Data { } } - -private extension String { - var escapingSpaces: String { - return replacingOccurrences(of: " ", with: "\\ ") - } - - func appending(argument: String) -> String { - return "\(self) \"\(argument)\"" - } - - func appending(arguments: [String]) -> String { - return appending(argument: arguments.joined(separator: "\" \"")) - } - - mutating func append(argument: String) { - self = appending(argument: argument) - } - - mutating func append(arguments: [String]) { - self = appending(arguments: arguments) - } -}