Skip to content

Commit afe26ff

Browse files
Boukeparkera
authored andcommitted
NSTask - capture output from commands (#348)
* Implemented NSFileHandle.availableData * Redirect stdin, stdout and stderr for NSTask * [NSTask] Use precondition instead of abort
1 parent 4e22a07 commit afe26ff

File tree

6 files changed

+264
-32
lines changed

6 files changed

+264
-32
lines changed

Foundation.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@
274274
D3E8D6D31C36982700295652 /* NSKeyedUnarchiver-EdgeInsetsTest.plist in Resources */ = {isa = PBXBuildFile; fileRef = D3E8D6D21C36982700295652 /* NSKeyedUnarchiver-EdgeInsetsTest.plist */; };
275275
D3E8D6D51C36AC0C00295652 /* NSKeyedUnarchiver-RectTest.plist in Resources */ = {isa = PBXBuildFile; fileRef = D3E8D6D41C36AC0C00295652 /* NSKeyedUnarchiver-RectTest.plist */; };
276276
D5C40F331CDA1D460005690C /* TestNSOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C40F321CDA1D460005690C /* TestNSOperationQueue.swift */; };
277+
D51239DF1CD9DA0800D433EE /* CFSocket.c in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D88E01BBC9B0300234F36 /* CFSocket.c */; };
278+
D512D17C1CD883F00032E6A5 /* TestNSFileHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D512D17B1CD883F00032E6A5 /* TestNSFileHandle.swift */; };
277279
E1A03F361C4828650023AF4D /* PropertyList-1.0.dtd in Resources */ = {isa = PBXBuildFile; fileRef = E1A03F351C4828650023AF4D /* PropertyList-1.0.dtd */; };
278280
E1A03F381C482C730023AF4D /* NSXMLDTDTestData.xml in Resources */ = {isa = PBXBuildFile; fileRef = E1A03F371C482C730023AF4D /* NSXMLDTDTestData.xml */; };
279281
E1A3726F1C31EBFB0023AF4D /* NSXMLDocumentTestData.xml in Resources */ = {isa = PBXBuildFile; fileRef = E1A3726E1C31EBFB0023AF4D /* NSXMLDocumentTestData.xml */; };
@@ -648,6 +650,7 @@
648650
D3E8D6D21C36982700295652 /* NSKeyedUnarchiver-EdgeInsetsTest.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "NSKeyedUnarchiver-EdgeInsetsTest.plist"; sourceTree = "<group>"; };
649651
D3E8D6D41C36AC0C00295652 /* NSKeyedUnarchiver-RectTest.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "NSKeyedUnarchiver-RectTest.plist"; sourceTree = "<group>"; };
650652
D5C40F321CDA1D460005690C /* TestNSOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSOperationQueue.swift; sourceTree = "<group>"; };
653+
D512D17B1CD883F00032E6A5 /* TestNSFileHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSFileHandle.swift; sourceTree = "<group>"; };
651654
D834F9931C31C4060023812A /* TestNSOrderedSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSOrderedSet.swift; sourceTree = "<group>"; };
652655
DCDBB8321C1768AC00313299 /* TestNSData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSData.swift; sourceTree = "<group>"; };
653656
E19E17DB1C2225930023AF4D /* TestNSXMLDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSXMLDocument.swift; sourceTree = "<group>"; };
@@ -1219,6 +1222,7 @@
12191222
5B6F17961C48631C00935030 /* TestUtils.swift */,
12201223
E19E17DB1C2225930023AF4D /* TestNSXMLDocument.swift */,
12211224
294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */,
1225+
D512D17B1CD883F00032E6A5 /* TestNSFileHandle.swift */,
12221226
);
12231227
name = Tests;
12241228
sourceTree = "<group>";
@@ -1907,6 +1911,7 @@
19071911
5B7C8A921BEA7FEC00C5B690 /* CFXMLInputStream.c in Sources */,
19081912
5B7C8AAA1BEA800D00C5B690 /* CFBurstTrie.c in Sources */,
19091913
5B7C8A9E1BEA7FF900C5B690 /* CFPlugIn_Instance.c in Sources */,
1914+
D51239DF1CD9DA0800D433EE /* CFSocket.c in Sources */,
19101915
5B7C8A801BEA7FCE00C5B690 /* CFDictionary.c in Sources */,
19111916
5BC2C00F1C07833200CC214E /* CFStringTransform.c in Sources */,
19121917
5B7C8AAE1BEA800D00C5B690 /* CFStringScanner.c in Sources */,
@@ -1997,6 +2002,7 @@
19972002
5B13B3401C582D4C00651CE2 /* TestNSRange.swift in Sources */,
19982003
5B13B3371C582D4C00651CE2 /* TestNSNotificationCenter.swift in Sources */,
19992004
5B13B3251C582D4700651CE2 /* main.swift in Sources */,
2005+
D512D17C1CD883F00032E6A5 /* TestNSFileHandle.swift in Sources */,
20002006
5B13B33A1C582D4C00651CE2 /* TestNSNumber.swift in Sources */,
20012007
5B13B3521C582D4C00651CE2 /* TestNSValue.swift in Sources */,
20022008
5B13B3311C582D4C00651CE2 /* TestNSIndexPath.swift in Sources */,

Foundation/NSFileHandle.swift

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@ public class NSFileHandle : NSObject, NSSecureCoding {
2020
internal var _closeOnDealloc: Bool
2121
internal var _closed: Bool = false
2222

23-
/*@NSCopying*/ public var availableData: NSData {
24-
NSUnimplemented()
23+
public var availableData: NSData {
24+
return _readDataOfLength(Int.max, untilEOF: false)
2525
}
2626

2727
public func readDataToEndOfFile() -> NSData {
2828
return readDataOfLength(Int.max)
2929
}
30-
30+
3131
public func readDataOfLength(_ length: Int) -> NSData {
32+
return _readDataOfLength(length, untilEOF: true)
33+
}
34+
35+
internal func _readDataOfLength(_ length: Int, untilEOF: Bool) -> NSData {
3236
var statbuf = stat()
3337
var dynamicBuffer: UnsafeMutablePointer<UInt8>? = nil
3438
var total = 0
@@ -49,21 +53,21 @@ public class NSFileHandle : NSObject, NSSecureCoding {
4953
if dynamicBuffer == nil {
5054
fatalError("unable to allocate backing buffer")
5155
}
52-
let amtRead = read(_fd, dynamicBuffer!.advanced(by: total), amountToRead)
53-
if 0 > amtRead {
54-
free(dynamicBuffer)
55-
fatalError("read failure")
56-
}
57-
if 0 == amtRead {
58-
break // EOF
59-
}
60-
61-
total += amtRead
62-
remaining -= amtRead
63-
64-
if total == length {
65-
break // We read everything the client asked for.
66-
}
56+
}
57+
let amtRead = read(_fd, dynamicBuffer!.advanced(by: total), amountToRead)
58+
if 0 > amtRead {
59+
free(dynamicBuffer)
60+
fatalError("read failure")
61+
}
62+
if 0 == amtRead {
63+
break // EOF
64+
}
65+
66+
total += amtRead
67+
remaining -= amtRead
68+
69+
if total == length || !untilEOF {
70+
break // We read everything the client asked for.
6771
}
6872
}
6973
} else {

Foundation/NSTask.swift

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,24 @@ public class NSTask : NSObject {
151151
public var currentDirectoryPath: String = NSFileManager.defaultInstance.currentDirectoryPath
152152

153153
// standard I/O channels; could be either an NSFileHandle or an NSPipe
154-
public var standardInput: AnyObject?
155-
public var standardOutput: AnyObject?
156-
public var standardError: AnyObject?
154+
public var standardInput: AnyObject? {
155+
willSet {
156+
precondition(newValue is NSPipe || newValue is NSFileHandle,
157+
"standardInput must be either NSPipe or NSFileHandle")
158+
}
159+
}
160+
public var standardOutput: AnyObject? {
161+
willSet {
162+
precondition(newValue is NSPipe || newValue is NSFileHandle,
163+
"standardOutput must be either NSPipe or NSFileHandle")
164+
}
165+
}
166+
public var standardError: AnyObject? {
167+
willSet {
168+
precondition(newValue is NSPipe || newValue is NSFileHandle,
169+
"standardError must be either NSPipe or NSFileHandle")
170+
}
171+
}
157172

158173
private var runLoopSourceContext : CFRunLoopSourceContext?
159174
private var runLoopSource : CFRunLoopSource?
@@ -284,16 +299,68 @@ public class NSTask : NSObject {
284299

285300
let source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0)
286301
CFRunLoopAddSource(managerThreadRunLoop?._cfRunLoop, source, kCFRunLoopDefaultMode)
287-
302+
303+
// file_actions
304+
#if os(OSX) || os(iOS)
305+
var fileActions: posix_spawn_file_actions_t? = nil
306+
#else
307+
var fileActions: posix_spawn_file_actions_t = posix_spawn_file_actions_t()
308+
#endif
309+
posix_spawn_file_actions_init(&fileActions)
310+
311+
switch self.standardInput {
312+
case let pipe as NSPipe:
313+
posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForReading.fileDescriptor, STDIN_FILENO)
314+
posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForWriting.fileDescriptor)
315+
case let handle as NSFileHandle:
316+
posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDIN_FILENO)
317+
posix_spawn_file_actions_addclose(&fileActions, handle.fileDescriptor)
318+
default: break
319+
}
320+
321+
switch self.standardOutput {
322+
case let pipe as NSPipe:
323+
posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
324+
posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForWriting.fileDescriptor)
325+
case let handle as NSFileHandle:
326+
posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDOUT_FILENO)
327+
posix_spawn_file_actions_addclose(&fileActions, handle.fileDescriptor)
328+
default: break
329+
}
330+
331+
switch self.standardError {
332+
case let pipe as NSPipe:
333+
posix_spawn_file_actions_adddup2(&fileActions, pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
334+
posix_spawn_file_actions_addclose(&fileActions, pipe.fileHandleForWriting.fileDescriptor)
335+
case let handle as NSFileHandle:
336+
posix_spawn_file_actions_adddup2(&fileActions, handle.fileDescriptor, STDERR_FILENO)
337+
posix_spawn_file_actions_addclose(&fileActions, handle.fileDescriptor)
338+
default: break
339+
}
340+
288341
// Launch
289-
342+
290343
var pid = pid_t()
291-
let status = posix_spawn(&pid, launchPath, nil, nil, argv, envp)
292-
344+
let status = posix_spawn(&pid, launchPath, &fileActions, nil, argv, envp)
345+
346+
// cleanup file_actions
347+
posix_spawn_file_actions_destroy(&fileActions)
348+
349+
// Close the write end of the input and output pipes.
350+
if let pipe = self.standardInput as? NSPipe {
351+
pipe.fileHandleForReading.closeFile()
352+
}
353+
if let pipe = self.standardOutput as? NSPipe {
354+
pipe.fileHandleForWriting.closeFile()
355+
}
356+
if let pipe = self.standardError as? NSPipe {
357+
pipe.fileHandleForWriting.closeFile()
358+
}
359+
293360
guard status == 0 else {
294361
fatalError()
295362
}
296-
363+
297364
close(taskSocketPair[1])
298365

299366
self.runLoop = NSRunLoop.currentRunLoop()

TestFoundation/TestNSFileHandle.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
10+
#if DEPLOYMENT_RUNTIME_OBJC || os(Linux)
11+
import Foundation
12+
import XCTest
13+
#else
14+
import SwiftFoundation
15+
import SwiftXCTest
16+
#endif
17+
18+
class TestNSFileHandle : XCTestCase {
19+
static var allTests : [(String, TestNSFileHandle -> () throws -> ())] {
20+
return [
21+
("test_pipe", test_pipe),
22+
]
23+
}
24+
25+
func test_pipe() {
26+
let pipe = NSPipe()
27+
let inputs = ["Hello", "world", "🐶"]
28+
29+
for input in inputs {
30+
let inputData = input.data(using: NSUTF8StringEncoding)!
31+
32+
// write onto pipe
33+
pipe.fileHandleForWriting.writeData(inputData)
34+
35+
let outputData = pipe.fileHandleForReading.availableData
36+
let output = String(data: outputData, encoding: NSUTF8StringEncoding)
37+
38+
XCTAssertEqual(output, input)
39+
}
40+
}
41+
}

TestFoundation/TestNSTask.swift

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ import CoreFoundation
1818

1919
class TestNSTask : XCTestCase {
2020
static var allTests: [(String, TestNSTask -> () throws -> Void)] {
21-
return [("test_exit0" , test_exit0),
22-
("test_exit1" , test_exit1),
23-
("test_exit100" , test_exit100),
24-
("test_sleep2", test_sleep2),
25-
("test_sleep2_exit1", test_sleep2_exit1)]
21+
return [
22+
("test_exit0" , test_exit0),
23+
("test_exit1" , test_exit1),
24+
("test_exit100" , test_exit100),
25+
("test_sleep2", test_sleep2),
26+
("test_sleep2_exit1", test_sleep2_exit1),
27+
("test_pipe_stdin", test_pipe_stdin),
28+
("test_pipe_stdout", test_pipe_stdout),
29+
("test_pipe_stderr", test_pipe_stderr),
30+
("test_file_stdout", test_file_stdout),
31+
]
2632
}
2733

2834
func test_exit0() {
@@ -84,4 +90,111 @@ class TestNSTask : XCTestCase {
8490
task.waitUntilExit()
8591
XCTAssertEqual(task.terminationStatus, 1)
8692
}
93+
94+
95+
func test_pipe_stdin() {
96+
let task = NSTask()
97+
98+
task.launchPath = "/bin/cat"
99+
100+
let outputPipe = NSPipe()
101+
task.standardOutput = outputPipe
102+
103+
let inputPipe = NSPipe()
104+
task.standardInput = inputPipe
105+
106+
task.launch()
107+
108+
inputPipe.fileHandleForWriting.writeData("Hello, 🐶.\n".data(using: NSUTF8StringEncoding)!)
109+
110+
// Close the input pipe to send EOF to cat.
111+
inputPipe.fileHandleForWriting.closeFile()
112+
113+
task.waitUntilExit()
114+
XCTAssertEqual(task.terminationStatus, 0)
115+
116+
let data = outputPipe.fileHandleForReading.availableData
117+
guard let string = String(data: data, encoding: NSUTF8StringEncoding) else {
118+
XCTFail("Could not read stdout")
119+
return
120+
}
121+
XCTAssertEqual(string, "Hello, 🐶.\n")
122+
}
123+
124+
func test_pipe_stdout() {
125+
let task = NSTask()
126+
127+
task.launchPath = "/usr/bin/which"
128+
task.arguments = ["which"]
129+
130+
let pipe = NSPipe()
131+
task.standardOutput = pipe
132+
133+
task.launch()
134+
task.waitUntilExit()
135+
XCTAssertEqual(task.terminationStatus, 0)
136+
137+
let data = pipe.fileHandleForReading.availableData
138+
guard let string = String(data: data, encoding: NSASCIIStringEncoding) else {
139+
XCTFail("Could not read stdout")
140+
return
141+
}
142+
XCTAssertEqual(string, "/usr/bin/which\n")
143+
}
144+
145+
func test_pipe_stderr() {
146+
let task = NSTask()
147+
148+
task.launchPath = "/bin/cat"
149+
task.arguments = ["invalid_file_name"]
150+
151+
let errorPipe = NSPipe()
152+
task.standardError = errorPipe
153+
154+
task.launch()
155+
task.waitUntilExit()
156+
XCTAssertEqual(task.terminationStatus, 1)
157+
158+
let data = errorPipe.fileHandleForReading.availableData
159+
guard let string = String(data: data, encoding: NSASCIIStringEncoding) else {
160+
XCTFail("Could not read stdout")
161+
return
162+
}
163+
XCTAssertEqual(string, "cat: invalid_file_name: No such file or directory\n")
164+
}
165+
166+
func test_file_stdout() {
167+
let task = NSTask()
168+
169+
task.launchPath = "/usr/bin/which"
170+
task.arguments = ["which"]
171+
172+
mkstemp(template: "TestNSTask.XXXXXX") { handle in
173+
task.standardOutput = handle
174+
175+
task.launch()
176+
task.waitUntilExit()
177+
XCTAssertEqual(task.terminationStatus, 0)
178+
179+
handle.seekToFileOffset(0)
180+
let data = handle.readDataToEndOfFile()
181+
guard let string = String(data: data, encoding: NSASCIIStringEncoding) else {
182+
XCTFail("Could not read stdout")
183+
return
184+
}
185+
XCTAssertEqual(string, "/usr/bin/which\n")
186+
}
187+
}
87188
}
189+
190+
private func mkstemp(template: String, body: @noescape (NSFileHandle) throws -> Void) rethrows {
191+
let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("TestNSTask.XXXXXX")!
192+
var buffer = [Int8](repeating: 0, count: Int(PATH_MAX))
193+
url.getFileSystemRepresentation(&buffer, maxLength: buffer.count)
194+
switch mkstemp(&buffer) {
195+
case -1: XCTFail("Could not create temporary file")
196+
case let fd:
197+
defer { unlink(&buffer) }
198+
try body(NSFileHandle(fileDescriptor: fd, closeOnDealloc: true))
199+
}
200+
}

TestFoundation/main.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ XCTMain([
5858
testCase(TestNSScanner.allTests),
5959
testCase(TestNSSet.allTests),
6060
testCase(TestNSString.allTests),
61-
// testCase(TestNSTask.allTests),
6261
// testCase(TestNSThread.allTests),
62+
testCase(TestNSTask.allTests),
6363
testCase(TestNSTimer.allTests),
6464
testCase(TestNSTimeZone.allTests),
6565
testCase(TestNSURL.allTests),
@@ -75,4 +75,5 @@ XCTMain([
7575
testCase(TestNSXMLParser.allTests),
7676
testCase(TestNSXMLDocument.allTests),
7777
testCase(TestNSAttributedString.allTests),
78+
testCase(TestNSFileHandle.allTests),
7879
])

0 commit comments

Comments
 (0)