diff --git a/Sources/SwiftShell/Command.swift b/Sources/SwiftShell/Command.swift index 1c6220c..a8f3c9d 100755 --- a/Sources/SwiftShell/Command.swift +++ b/Sources/SwiftShell/Command.swift @@ -10,18 +10,6 @@ import Foundation import Dispatch -#if os(Linux) -import Glibc -#endif - -#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !swift(>=3.1) -typealias Process = Task - -extension Task { - var isRunning: Bool { return running } -} -#endif - // MARK: exit /** @@ -136,43 +124,6 @@ public func == (e1: CommandError, e2: CommandError) -> Bool { } } -// MARK: Process - -extension Process { - - /** - Launch process. - - - throws: CommandError.inAccessibleExecutable if command could not be executed. - */ - public func launchThrowably() throws { - guard Files.isExecutableFile(atPath: self.launchPath!) else { - throw CommandError.inAccessibleExecutable(path: self.launchPath!) - } - launch() - } - - /** - Wait until process is finished. - - - throws: `CommandError.returnedErrorCode(command: String, errorcode: Int)` if the exit code is anything but 0. - */ - public func finish() throws { - self.waitUntilExit() - guard self.terminationStatus == 0 else { - throw CommandError.returnedErrorCode(command: commandAsString()!, errorcode: Int(self.terminationStatus)) - } - } - - /** The full path to the executable + all arguments, each one quoted if it contains a space. */ - func commandAsString() -> String? { - guard let path = self.launchPath else { return nil } - return self.arguments?.reduce(path) { (acc: String, arg: String) in - return acc + " " + ( arg.contains(" ") ? ("\"" + arg + "\"") : arg ) - } - } -} - // MARK: run /// Output from a `run` command. @@ -312,92 +263,7 @@ public final class AsyncCommand { /// Is the command still running? public var isRunning: Bool { return process.isRunning } - #if os(Linux) - fileprivate enum ProcessAttribute: String { - 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 - - returns: A String containing the hexadecimal representation of the mask, - or nil if there is no stdout output - */ - fileprivate func getProcessInfo(_ attr: ProcessAttribute) -> String? { - let attribute = run(bash: "ps --no-headers -q \(process.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(process.processIdentifier, signum) - } - - /// Terminates the command by sending the SIGTERM signal - public func stop() { - // 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 - */ - @discardableResult public func resume() -> Bool { - return signal(SIGCONT) == 0 - } - #else /// Terminates the command by sending the SIGTERM signal public func stop() { process.terminate() @@ -427,7 +293,6 @@ public final class AsyncCommand { @discardableResult public func resume() -> Bool { return process.resume() } - #endif /** Wait for this command to finish. @@ -448,9 +313,9 @@ public final class AsyncCommand { } /** - Wait for the command to finish, then return why the command terminated + Wait for the command to finish, then return why the command terminated. - - returns: .exited if the command exited normally, otherwise it's .uncaughtSignal + - returns: `.exited` if the command exited normally, otherwise `.uncaughtSignal`. */ public func terminationReason() -> Process.TerminationReason { process.waitUntilExit() diff --git a/Sources/SwiftShell/Process.swift b/Sources/SwiftShell/Process.swift new file mode 100644 index 0000000..2bbdc8f --- /dev/null +++ b/Sources/SwiftShell/Process.swift @@ -0,0 +1,128 @@ +// +// Process.swift +// SwiftShell +// +// Created by Kåre Morstøl on 06/04/2018. +// + +import Foundation + +// MARK: Process + +extension Process { + /// Launch process. + /// + /// - throws: CommandError.inAccessibleExecutable if command could not be executed. + public func launchThrowably() throws { + guard Files.isExecutableFile(atPath: self.launchPath!) else { + throw CommandError.inAccessibleExecutable(path: self.launchPath!) + } + launch() + } + + /// Wait until process is finished. + /// + /// - throws: `CommandError.returnedErrorCode(command: String, errorcode: Int)` + /// if the exit code is anything but 0. + public func finish() throws { + self.waitUntilExit() + guard self.terminationStatus == 0 else { + throw CommandError.returnedErrorCode(command: commandAsString()!, errorcode: Int(self.terminationStatus)) + } + } + + /// The full path to the executable + all arguments, each one quoted if it contains a space. + func commandAsString() -> String? { + guard let path = self.launchPath else { return nil } + return self.arguments?.reduce(path) { (acc: String, arg: String) in + return acc + " " + ( arg.contains(" ") ? ("\"" + arg + "\"") : arg ) + } + } +} + +// MARK: Process extensions for Linux + +#if os(Linux) +import Glibc + +extension Process { + fileprivate enum ProcessAttribute: String { + 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 + /// - returns: A String containing the hexadecimal representation of the mask, + /// or nil if there is no stdout output + fileprivate func getProcessInfo(_ attr: ProcessAttribute) -> String? { + 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. + @discardableResult public func resume() -> Bool { + return signal(SIGCONT) == 0 + } +} +#endif diff --git a/SwiftShell.xcodeproj/project.pbxproj b/SwiftShell.xcodeproj/project.pbxproj index 40ab8d6..7612e42 100644 --- a/SwiftShell.xcodeproj/project.pbxproj +++ b/SwiftShell.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + BA930AEB2077EC5C003E5703 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA930AEA2077EC5C003E5703 /* Process.swift */; }; OBJ_45 /* Bash.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Bash.swift */; }; OBJ_46 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Command.swift */; }; OBJ_47 /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* Context.swift */; }; @@ -53,6 +54,7 @@ /* Begin PBXFileReference section */ BA50D28D1E8DB923002BA4A5 /* LinuxMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = ""; }; + BA930AEA2077EC5C003E5703 /* Process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = ""; }; OBJ_10 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Command.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; OBJ_11 /* Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Context.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; OBJ_12 /* FileHandle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FileHandle.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -209,6 +211,7 @@ OBJ_10 /* Command.swift */, OBJ_11 /* Context.swift */, OBJ_12 /* FileHandle.swift */, + BA930AEA2077EC5C003E5703 /* Process.swift */, OBJ_13 /* Files.swift */, OBJ_14 /* String.swift */, OBJ_15 /* General */, @@ -354,6 +357,7 @@ buildActionMask = 0; files = ( OBJ_45 /* Bash.swift in Sources */, + BA930AEB2077EC5C003E5703 /* Process.swift in Sources */, OBJ_46 /* Command.swift in Sources */, OBJ_47 /* Context.swift in Sources */, OBJ_48 /* FileHandle.swift in Sources */,