Skip to content

[NSTask] Change argv[0] to full path + fixes combining stdout+stderr #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 25 additions & 29 deletions Foundation/NSTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public class NSTask : NSObject {

// Convert the arguments array into a posix_spawn-friendly format

var args = [launchPath.lastPathComponent]
var args = [launchPath]
if let arguments = self.arguments {
args.append(contentsOf: arguments)
}
Expand Down Expand Up @@ -307,61 +307,52 @@ public class NSTask : NSObject {
#else
var fileActions: posix_spawn_file_actions_t = posix_spawn_file_actions_t()
#endif
posix_spawn_file_actions_init(&fileActions)
try! posix(posix_spawn_file_actions_init(&fileActions))
defer { posix_spawn_file_actions_destroy(&fileActions) }

switch self.standardInput {
switch standardInput {
case let pipe as NSPipe:
posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForReading.fileDescriptor, STDIN_FILENO)
posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForWriting.fileDescriptor)
try! posix(posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForReading.fileDescriptor, STDIN_FILENO))
try! posix(posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForWriting.fileDescriptor))
case let handle as NSFileHandle:
posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDIN_FILENO)
posix_spawn_file_actions_addclose(&fileActions, handle.fileDescriptor)
try! posix(posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDIN_FILENO))
default: break
}

switch self.standardOutput {
switch standardOutput {
case let pipe as NSPipe:
posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForWriting.fileDescriptor)
try! posix(posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO))
try! posix(posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForReading.fileDescriptor))
case let handle as NSFileHandle:
posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDOUT_FILENO)
posix_spawn_file_actions_addclose(&fileActions, handle.fileDescriptor)
try! posix(posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDOUT_FILENO))
default: break
}

switch self.standardError {
switch standardError {
case let pipe as NSPipe:
posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForWriting.fileDescriptor)
try! posix(posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO))
try! posix(posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForReading.fileDescriptor))
case let handle as NSFileHandle:
posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDERR_FILENO)
posix_spawn_file_actions_addclose(&fileActions, handle.fileDescriptor)
try! posix(posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDERR_FILENO))
default: break
}

// Launch

var pid = pid_t()
let status = posix_spawn(&pid, launchPath, &fileActions, nil, argv, envp)

// cleanup file_actions
posix_spawn_file_actions_destroy(&fileActions)
try! posix(posix_spawn(&pid, launchPath, &fileActions, nil, argv, envp))

// Close the write end of the input and output pipes.
if let pipe = self.standardInput as? NSPipe {
if let pipe = standardInput as? NSPipe {
pipe.fileHandleForReading.closeFile()
}
if let pipe = self.standardOutput as? NSPipe {
if let pipe = standardOutput as? NSPipe {
pipe.fileHandleForWriting.closeFile()
}
if let pipe = self.standardError as? NSPipe {
if let pipe = standardError as? NSPipe {
pipe.fileHandleForWriting.closeFile()
}

guard status == 0 else {
fatalError()
}

close(taskSocketPair[1])

self.runLoop = NSRunLoop.currentRunLoop()
Expand Down Expand Up @@ -427,4 +418,9 @@ extension NSTask {

public let NSTaskDidTerminateNotification: String = "NSTaskDidTerminateNotification"


private func posix(_ code: Int32) throws {
switch code {
case 0: return
default: throw NSError(domain: NSPOSIXErrorDomain, code: Int(code), userInfo: nil)
}
}
94 changes: 75 additions & 19 deletions TestFoundation/TestNSTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class TestNSTask : XCTestCase {
("test_pipe_stdin", test_pipe_stdin),
("test_pipe_stdout", test_pipe_stdout),
("test_pipe_stderr", test_pipe_stderr),
("test_pipe_stdout_and_stderr_same_pipe", test_pipe_stdout_and_stderr_same_pipe),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to remove this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, will put it back in.

("test_file_stdout", test_file_stdout),
("test_passthrough_environment", test_passthrough_environment),
("test_no_environment", test_no_environment),
Expand Down Expand Up @@ -163,7 +164,29 @@ class TestNSTask : XCTestCase {
XCTFail("Could not read stdout")
return
}
XCTAssertEqual(string, "cat: invalid_file_name: No such file or directory\n")
XCTAssertEqual(string, "/bin/cat: invalid_file_name: No such file or directory\n")
}

func test_pipe_stdout_and_stderr_same_pipe() {
let task = NSTask()

task.launchPath = "/bin/cat"
task.arguments = ["invalid_file_name"]

let pipe = NSPipe()
task.standardOutput = pipe
task.standardError = pipe

task.launch()
task.waitUntilExit()
XCTAssertEqual(task.terminationStatus, 1)

let data = pipe.fileHandleForReading.availableData
guard let string = String(data: data, encoding: NSASCIIStringEncoding) else {
XCTFail("Could not read stdout")
return
}
XCTAssertEqual(string, "/bin/cat: invalid_file_name: No such file or directory\n")
}

func test_file_stdout() {
Expand All @@ -188,19 +211,36 @@ class TestNSTask : XCTestCase {
XCTAssertEqual(string, "/usr/bin/which\n")
}
}

func test_passthrough_environment() {
XCTAssertGreaterThan(env(environment: nil).count, 0)
do {
let output = try runTask(["/usr/bin/env"], environment: nil)
let env = try parseEnv(output)
XCTAssertGreaterThan(env.count, 0)
} catch let error {
XCTFail("Test failed: \(error)")
}
}

func test_no_environment() {
XCTAssertEqual(env(environment: [:]).count, 0)
do {
let output = try runTask(["/usr/bin/env"], environment: [:])
let env = try parseEnv(output)
XCTAssertEqual(env.count, 0)
} catch let error {
XCTFail("Test failed: \(error)")
}
}

func test_custom_environment() {
let input = ["HELLO": "WORLD", "HOME": "CUPERTINO"]
let output = env(environment: input)
XCTAssertEqual(output, input)
do {
let input = ["HELLO": "WORLD", "HOME": "CUPERTINO"]
let output = try runTask(["/usr/bin/env"], environment: input)
let env = try parseEnv(output)
XCTAssertEqual(env, input)
} catch let error {
XCTFail("Test failed: \(error)")
}
}
}

Expand All @@ -216,31 +256,47 @@ private func mkstemp(template: String, body: @noescape (NSFileHandle) throws ->
}
}

private func env(environment: [String: String]?) -> [String: String] {
private enum Error: ErrorProtocol {
case TerminationStatus(Int32)
case UnicodeDecodingError(NSData)
case InvalidEnvironmentVariable(String)
}

private func runTask(_ arguments: [String], environment: [String: String]? = nil) throws -> String {
let task = NSTask()
task.launchPath = "/usr/bin/env"

var arguments = arguments
task.launchPath = arguments.removeFirst()
task.arguments = arguments
task.environment = environment

let pipe = NSPipe()
task.standardOutput = pipe

task.standardError = pipe
task.launch()
task.waitUntilExit()
XCTAssertEqual(task.terminationStatus, 0)

guard task.terminationStatus == 0 else {
throw Error.TerminationStatus(task.terminationStatus)
}

let data = pipe.fileHandleForReading.availableData
guard let string = String(data: data, encoding: NSUTF8StringEncoding) else {
XCTFail("Could not read stdout")
return [:]
guard let output = String(data: data, encoding: NSUTF8StringEncoding) else {
throw Error.UnicodeDecodingError(data)
}

return output
}

private func parseEnv(_ env: String) throws -> [String: String] {
var result = [String: String]()
for variable in string.components(separatedBy: "\n") where variable != "" {
guard let range = variable.range(of: "=") else {
XCTFail("Could not parse environment variable: \"\(variable)\"")
continue
for line in env.components(separatedBy: "\n") where line != "" {
guard let range = line.range(of: "=") else {
throw Error.InvalidEnvironmentVariable(line)
}
result[variable.substring(to: range.lowerBound)] = variable.substring(from: range.upperBound)
result[line.substring(to: range.lowerBound)] = line.substring(from: range.upperBound)
}
return result
}