Skip to content

Commit

Permalink
Add runAsyncAndPrint command (#61).
Browse files Browse the repository at this point in the history
  • Loading branch information
kareman committed Apr 9, 2018
1 parent 51c3134 commit a2203d7
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 34 deletions.
8 changes: 8 additions & 0 deletions Sources/SwiftShell/Bash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ extension CommandRunning {
return runAsync("/bin/bash", "-c", bashcommand, file: file, line: line)
}

public func runAsyncAndPrint(bash bashcommand: String, file: String = #file, line: Int = #line) -> PrintedAsyncCommand {
return runAsyncAndPrint("/bin/bash", "-c", bashcommand, file: file, line: line)
}

/**
Run bash command and print output and errors.
Expand Down Expand Up @@ -70,6 +74,10 @@ public func runAsync(bash bashcommand: String, file: String = #file, line: Int =
return main.runAsync(bash: bashcommand, file: file, line: line)
}

public func runAsyncAndPrint(bash bashcommand: String, file: String = #file, line: Int = #line) -> PrintedAsyncCommand {
return main.runAsyncAndPrint(bash: bashcommand, file: file, line: line)
}

/**
Run bash command and print output and errors.
Expand Down
51 changes: 35 additions & 16 deletions Sources/SwiftShell/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,25 +229,14 @@ extension CommandRunning {

// MARK: runAsync

/** Output from the 'runAsync' methods. */
public final class AsyncCommand {
public let stdout: ReadableStream
public let stderror: ReadableStream
public class PrintedAsyncCommand {
fileprivate let process: Process

init(unlaunched process: Process, combineOutput: Bool) {
self.process = process

let outpipe = Pipe()
process.standardOutput = outpipe
stdout = FileHandleStream(outpipe.fileHandleForReading, encoding: main.encoding)

if combineOutput {
stderror = stdout
} else {
let errorpipe = Pipe()
process.standardError = errorpipe
stderror = FileHandleStream(errorpipe.fileHandleForReading, encoding: main.encoding)
process.standardError = process.standardOutput
}
}

Expand Down Expand Up @@ -301,7 +290,7 @@ public final class AsyncCommand {
- returns: self
- throws: `CommandError.returnedErrorCode(command: String, errorcode: Int)` if the exit code is anything but 0.
*/
@discardableResult public func finish() throws -> AsyncCommand {
@discardableResult public func finish() throws -> Self {
try process.finish()
return self
}
Expand All @@ -326,16 +315,37 @@ public final class AsyncCommand {
///
/// - Parameter handler: A closure taking this AsyncCommand as input, returning nothing.
/// - Returns: This AsyncCommand.
@discardableResult public func onCompletion(_ handler: @escaping (AsyncCommand) -> Void) -> AsyncCommand {
@discardableResult public func onCompletion(_ handler: @escaping (PrintedAsyncCommand) -> Void) -> Self {
process.terminationHandler = { _ in
handler(self)
}
return self
}
}

extension CommandRunning {
/** Output from the 'runAsync' methods. */
public final class AsyncCommand: PrintedAsyncCommand {
public let stdout: ReadableStream
public let stderror: ReadableStream

override init(unlaunched process: Process, combineOutput: Bool) {
let outpipe = Pipe()
process.standardOutput = outpipe
stdout = FileHandleStream(outpipe.fileHandleForReading, encoding: main.encoding)

if combineOutput {
stderror = stdout
} else {
let errorpipe = Pipe()
process.standardError = errorpipe
stderror = FileHandleStream(errorpipe.fileHandleForReading, encoding: main.encoding)
}

super.init(unlaunched: process, combineOutput: combineOutput)
}
}

extension CommandRunning {
/**
Run executable and return before it is finished.
Expand All @@ -348,6 +358,11 @@ extension CommandRunning {
let stringargs = args.flatten().map(String.init(describing:))
return AsyncCommand(launch: createProcess(executable, args: stringargs), file: file, line: line)
}

public func runAsyncAndPrint(_ executable: String, _ args: Any ..., file: String = #file, line: Int = #line) -> PrintedAsyncCommand {
let stringargs = args.flatten().map(String.init(describing:))
return PrintedAsyncCommand(launch: createProcess(executable, args: stringargs), file: file, line: line)
}
}

// MARK: runAndPrint
Expand Down Expand Up @@ -401,6 +416,10 @@ public func runAsync(_ executable: String, _ args: Any ..., file: String = #file
return main.runAsync(executable, args, file: file, line: line)
}

public func runAsyncAndPrint(_ executable: String, _ args: Any ..., file: String = #file, line: Int = #line) -> PrintedAsyncCommand {
return main.runAsyncAndPrint(executable, args, file: file, line: line)
}

/**
Run executable and print output and errors.
Expand Down
32 changes: 16 additions & 16 deletions Sources/SwiftShell/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension Process {
}
launch()
}

/// Wait until process is finished.
///
/// - throws: `CommandError.returnedErrorCode(command: String, errorcode: Int)`
Expand Down Expand Up @@ -49,7 +49,7 @@ extension Process {
case blockedSignals = "blocked"
case ignoredSignals = "ignored"
}

/// Gets a specified process attribute using the `ps` command installed on all Linux systems
///
/// - parameter attr: Which specific attribute to return
Expand All @@ -59,64 +59,64 @@ extension Process {
let attribute = run(bash: "ps --no-headers -q \(self.processIdentifier) -o \(attr.rawValue)").stdout
return attribute.isEmpty ? nil : attribute
}

/// Determines whether the running process is blocking the specified signal
fileprivate func isBlockingSignal(_ signum: Int32) -> Bool {
// If there is no mask, then the signal isn't blocked
guard let blockedMask = getProcessInfo(.blockedSignals) else { return false }

// If the output isn't in proper hexadecimal (like it should be), then
// it could be ignored, but we can't be sure. Return true, just to be safe
guard let blocked = Int(blockedMask, radix: 16) else { return true }

// Checks if the signals bit in the mask is 1 (1 == blocked)
return blocked & (1 << signum) == 1
}

/// Determines whether the running process is ignoring the specified signal
fileprivate func isIgnoringSignal(_ signum: Int32) -> Bool {
// If there is no mask, then the signal isn't ignored
guard let ignoredMask = getProcessInfo(.ignoredSignals) else { return false }

// If the output isn't in proper hexadecimal (like it should be), then
// it could be ignored, but we can't be sure. Return true, just to be safe
guard let ignored = Int(ignoredMask, radix: 16) else { return true }

// Checks if the signals bit in the mask is 1 (1 == ignored)
return ignored & (1 << signum) == 1
}

/// Sends the specified signal to the currently running process
@discardableResult fileprivate func signal(_ signum: Int32) -> Int32 {
return kill(self.processIdentifier, signum)
}


/// Terminates the command by sending the SIGTERM signal
public func terminate() {
// If the SIGTERM signal is being blocked or ignored by the process,
// then don't bother sending it
guard !(isBlockingSignal(SIGTERM) || isIgnoringSignal(SIGTERM)) else { return }

signal(SIGTERM)
}

/// Interrupts the command by sending the SIGINT signal
public func interrupt() {
// If the SIGINT signal is being blocked or ignored by the process,
// then don't bother sending it
guard !(isBlockingSignal(SIGINT) || isIgnoringSignal(SIGINT)) else { return }

signal(SIGINT)
}

/// Temporarily suspends a command. Call resume() to resume a suspended command
///
/// - returns: true if the command was successfully suspended
@discardableResult public func suspend() -> Bool {
return signal(SIGTSTP) == 0
}

/// Resumes a command previously suspended with suspend().
///
/// - returns: true if the command was successfully resumed.
Expand Down
1 change: 1 addition & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let tests: [XCTestCaseEntry] = [
testCase(Open.allTests),
testCase(Run_Tests.allTests),
testCase(RunAsync_Tests.allTests),
testCase(RunAsyncAndPrint_Tests.allTests),
testCase(RunAndPrint_Tests.allTests),
testCase(MainContext_Tests.allTests),
testCase(CopiedCustomContext_Tests.allTests),
Expand Down
35 changes: 33 additions & 2 deletions Tests/SwiftShellTests/Command_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@testable import SwiftShell
import XCTest
import Foundation

#if os(Linux)
import Glibc
#endif
Expand Down Expand Up @@ -43,6 +44,12 @@ public class Run_Tests: XCTestCase {
XCTAssertEqual( SwiftShell.run(bash:"echo one 1>&2").stderror, "one" )
}

/** FIXME: Alternates between passing and failing
func testCombinesOutput() {
XCTAssertEqual( SwiftShell.run(bash: "echo stdout && echo stderr > /dev/stderr", combineOutput: true).stdout, "stdout\nstderr\n" )
}
*/

func testExecutableWithoutPath() {
XCTAssertEqual( SwiftShell.run("echo", "one").stdout, "one")
}
Expand Down Expand Up @@ -198,8 +205,7 @@ public class RunAsync_Tests: XCTestCase {
*/
}

public class RunAndPrint_Tests: XCTestCase {

public class XCTestCase_TestOutput: XCTestCase {
var test_stdout: FileHandle!
var test_stderr: FileHandle!

Expand All @@ -212,6 +218,23 @@ public class RunAndPrint_Tests: XCTestCase {
main.stderror = FileHandleStream(errorpipe.fileHandleForWriting, encoding: .utf8)
test_stderr = errorpipe.fileHandleForReading
}
}

public class RunAsyncAndPrint_Tests: XCTestCase_TestOutput {
func testReturnsStandardOutput() {
AssertDoesNotThrow { try runAsyncAndPrint("/bin/echo", "one", "two" ).finish() }

XCTAssertEqual( test_stdout.readSome(encoding: .utf8), "one two\n" )
}

func testReturnsStandardError() {
AssertDoesNotThrow { try runAsyncAndPrint(bash: "echo one two > /dev/stderr" ).finish() }

XCTAssertEqual( test_stderr.readSome(encoding: .utf8), "one two\n" )
}
}

public class RunAndPrint_Tests: XCTestCase_TestOutput {

func testReturnsStandardOutput() {
AssertDoesNotThrow { try runAndPrint("/bin/echo", "one", "two" ) }
Expand Down Expand Up @@ -250,6 +273,7 @@ extension Run_Tests {
("testSinglelineOutput", testSinglelineOutput),
("testMultilineOutput", testMultilineOutput),
("testStandardErrorOutput", testStandardErrorOutput),
//("testCombinesOutput", testCombinesOutput),
("testExecutableWithoutPath", testExecutableWithoutPath),
("testSuccess", testSuccess),
("testAnd", testAnd),
Expand All @@ -272,6 +296,13 @@ extension RunAsync_Tests {
]
}

extension RunAsyncAndPrint_Tests {
public static var allTests = [
("testReturnsStandardOutput", testReturnsStandardOutput),
("testReturnsStandardError", testReturnsStandardError),
]
}

extension RunAndPrint_Tests {
public static var allTests = [
("testReturnsStandardOutput", testReturnsStandardOutput),
Expand Down

0 comments on commit a2203d7

Please sign in to comment.