Skip to content

Commit b1647ac

Browse files
authored
Merge pull request #1558 from spevans/pr_process_deprecate
2 parents 3b79f5d + 36e19d3 commit b1647ac

File tree

3 files changed

+141
-70
lines changed

3 files changed

+141
-70
lines changed

DarwinCompatibilityTests.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@
642642
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
643643
GCC_WARN_UNUSED_FUNCTION = YES;
644644
GCC_WARN_UNUSED_VARIABLE = YES;
645-
MACOSX_DEPLOYMENT_TARGET = 10.12;
645+
MACOSX_DEPLOYMENT_TARGET = 10.13;
646646
MTL_ENABLE_DEBUG_INFO = YES;
647647
ONLY_ACTIVE_ARCH = YES;
648648
SDKROOT = macosx;
@@ -693,7 +693,7 @@
693693
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
694694
GCC_WARN_UNUSED_FUNCTION = YES;
695695
GCC_WARN_UNUSED_VARIABLE = YES;
696-
MACOSX_DEPLOYMENT_TARGET = 10.12;
696+
MACOSX_DEPLOYMENT_TARGET = 10.13;
697697
MTL_ENABLE_DEBUG_INFO = NO;
698698
SDKROOT = macosx;
699699
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";

Foundation/Process.swift

Lines changed: 94 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This source file is part of the Swift.org open source project
22
//
3-
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
3+
// Copyright (c) 2014 - 2016, 2018 Apple Inc. and the Swift project authors
44
// Licensed under Apache License v2.0 with Runtime Library Exception
55
//
66
// See http://swift.org/LICENSE.txt for license information
@@ -153,7 +153,7 @@ open class Process: NSObject {
153153
}
154154
}
155155
}
156-
156+
157157
// Create an Process which can be run at a later time
158158
// An Process can only be run once. Subsequent attempts to
159159
// run an Process will raise.
@@ -162,48 +162,36 @@ open class Process: NSObject {
162162
//
163163

164164
public override init() {
165-
165+
166166
}
167-
168-
// These methods can only be set before a launch.
169-
170-
open var launchPath: String?
167+
168+
// These properties can only be set before a launch.
169+
open var executableURL: URL?
170+
open var currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true)
171171
open var arguments: [String]?
172172
open var environment: [String : String]? // if not set, use current
173-
174-
open var currentDirectoryPath: String = FileManager.default.currentDirectoryPath
175-
176-
open var executableURL: URL? {
177-
get {
178-
guard let launchPath = self.launchPath else {
179-
return nil
180-
}
181-
182-
return URL(fileURLWithPath: launchPath)
183-
}
184-
set {
185-
self.launchPath = newValue?.path
186-
}
173+
174+
@available(*, deprecated, renamed: "executableURL")
175+
open var launchPath: String? {
176+
get { return executableURL?.path }
177+
set { executableURL = (newValue != nil) ? URL(fileURLWithPath: newValue!) : nil }
187178
}
188-
189-
open var currentDirectoryURL: URL {
190-
get {
191-
return URL(fileURLWithPath: self.currentDirectoryPath)
192-
}
193-
set {
194-
self.currentDirectoryPath = newValue.path
195-
}
179+
180+
@available(*, deprecated, renamed: "currentDirectoryURL")
181+
open var currentDirectoryPath: String {
182+
get { return currentDirectoryURL.path }
183+
set { currentDirectoryURL = URL(fileURLWithPath: newValue) }
196184
}
197-
185+
198186
// Standard I/O channels; could be either a FileHandle or a Pipe
199-
187+
200188
open var standardInput: Any? {
201189
willSet {
202190
precondition(newValue is Pipe || newValue is FileHandle,
203191
"standardInput must be either Pipe or FileHandle")
204192
}
205193
}
206-
194+
207195
open var standardOutput: Any? {
208196
willSet {
209197
precondition(newValue is Pipe || newValue is FileHandle,
@@ -227,20 +215,62 @@ open class Process: NSObject {
227215

228216
// Actions
229217

218+
@available(*, deprecated, renamed: "run")
230219
open func launch() {
220+
do {
221+
try run()
222+
} catch let nserror as NSError {
223+
if let path = nserror.userInfo[NSFilePathErrorKey] as? String, path == currentDirectoryPath {
224+
// Foundation throws an NSException when changing the working directory fails,
225+
// and unfortunately launch() is not marked `throws`, so we get away with a
226+
// fatalError.
227+
switch CocoaError.Code(rawValue: nserror.code) {
228+
case .fileReadNoSuchFile:
229+
fatalError("Process: The specified working directory does not exist.")
230+
case .fileReadNoPermission:
231+
fatalError("Process: The specified working directory cannot be accessed.")
232+
default:
233+
fatalError("Process: The specified working directory cannot be set.")
234+
}
235+
}
236+
} catch {
237+
fatalError(String(describing: error))
238+
}
239+
}
240+
241+
open func run() throws {
231242

232243
self.processLaunchedCondition.lock()
233-
244+
defer {
245+
self.processLaunchedCondition.unlock()
246+
self.processLaunchedCondition.broadcast()
247+
}
248+
234249
// Dispatch the manager thread if it isn't already running
235250

236251
Process.setup()
237252

238253
// Ensure that the launch path is set
239-
240-
guard let launchPath = self.launchPath else {
241-
fatalError()
254+
guard let launchPath = self.executableURL?.path else {
255+
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError)
242256
}
243-
257+
258+
// Initial checks that the launchPath points to an executable file. posix_spawn()
259+
// can return success even if executing the program fails, eg fork() works but execve()
260+
// fails, so try and check as much as possible beforehand.
261+
try FileManager.default._fileSystemRepresentation(withPath: launchPath, { fsRep in
262+
var statInfo = stat()
263+
guard stat(fsRep, &statInfo) == 0 else {
264+
throw _NSErrorWithErrno(errno, reading: true, path: launchPath)
265+
}
266+
267+
guard statInfo.st_mode & S_IFMT == S_IFREG else {
268+
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError)
269+
}
270+
guard access(fsRep, X_OK) == 0 else {
271+
throw _NSErrorWithErrno(errno, reading: true, path: launchPath)
272+
}
273+
})
244274
// Convert the arguments array into a posix_spawn-friendly format
245275

246276
var args = [launchPath]
@@ -260,7 +290,6 @@ open class Process: NSObject {
260290
for arg in argv ..< argv + args.count {
261291
free(UnsafeMutableRawPointer(arg.pointee))
262292
}
263-
264293
argv.deallocate()
265294
}
266295

@@ -294,7 +323,7 @@ open class Process: NSObject {
294323
context.version = 0
295324
context.retain = runLoopSourceRetain
296325
context.release = runLoopSourceRelease
297-
context.info = Unmanaged.passUnretained(self).toOpaque()
326+
context.info = Unmanaged.passUnretained(self).toOpaque()
298327

299328
let socket = CFSocketCreateWithNative( nil, taskSocketPair[0], CFOptionFlags(kCFSocketDataCallBack), {
300329
(socket, type, address, data, info ) in
@@ -316,7 +345,7 @@ open class Process: NSObject {
316345
}
317346
#endif
318347
var waitResult : Int32 = 0
319-
348+
320349
repeat {
321350
#if CYGWIN
322351
waitResult = waitpid( process.processIdentifier, exitCodePtrWrapper, 0)
@@ -344,9 +373,9 @@ open class Process: NSObject {
344373
}
345374

346375
// Set the running flag to false
347-
348376
process.isRunning = false
349-
377+
process.processIdentifier = -1
378+
350379
// Invalidate the source and wake up the run loop, if it's available
351380

352381
CFRunLoopSourceInvalidate(process.runLoopSource)
@@ -417,27 +446,20 @@ open class Process: NSObject {
417446

418447
let fileManager = FileManager()
419448
let previousDirectoryPath = fileManager.currentDirectoryPath
420-
if !fileManager.changeCurrentDirectoryPath(currentDirectoryPath) {
421-
// Foundation throws an NSException when changing the working directory fails,
422-
// and unfortunately launch() is not marked `throws`, so we get away with a
423-
// fatalError.
424-
switch errno {
425-
case ENOENT:
426-
fatalError("Process: The specified working directory does not exist.")
427-
case EACCES:
428-
fatalError("Process: The specified working directory cannot be accessed.")
429-
default:
430-
fatalError("Process: The specified working directory cannot be set.")
431-
}
449+
if !fileManager.changeCurrentDirectoryPath(currentDirectoryURL.path) {
450+
throw _NSErrorWithErrno(errno, reading: true, url: currentDirectoryURL)
432451
}
433452

434-
// Launch
453+
defer {
454+
// Reset the previous working directory path.
455+
fileManager.changeCurrentDirectoryPath(previousDirectoryPath)
456+
}
435457

458+
// Launch
436459
var pid = pid_t()
437-
posix(posix_spawn(&pid, launchPath, &fileActions, nil, argv, envp))
438-
439-
// Reset the previous working directory path.
440-
fileManager.changeCurrentDirectoryPath(previousDirectoryPath)
460+
guard posix_spawn(&pid, launchPath, &fileActions, nil, argv, envp) == 0 else {
461+
throw _NSErrorWithErrno(errno, reading: true, path: launchPath)
462+
}
441463

442464
// Close the write end of the input and output pipes.
443465
if let pipe = standardInput as? Pipe {
@@ -451,7 +473,7 @@ open class Process: NSObject {
451473
}
452474

453475
close(taskSocketPair[1])
454-
476+
455477
self.runLoop = RunLoop.current
456478
self.runLoopSourceContext = CFRunLoopSourceContext(version: 0,
457479
info: Unmanaged.passUnretained(self).toOpaque(),
@@ -483,9 +505,6 @@ open class Process: NSObject {
483505
isRunning = true
484506

485507
self.processIdentifier = pid
486-
487-
self.processLaunchedCondition.unlock()
488-
self.processLaunchedCondition.broadcast()
489508
}
490509

491510
open func interrupt() { NSUnimplemented() } // Not always possible. Sends SIGINT.
@@ -506,10 +525,18 @@ open class Process: NSObject {
506525
*/
507526
open var terminationHandler: ((Process) -> Void)?
508527
open var qualityOfService: QualityOfService = .default // read-only after the process is launched
509-
}
510528

511-
extension Process {
512-
529+
530+
open class func run(_ url: URL, arguments: [String], terminationHandler: ((Process) -> Void)? = nil) throws -> Process {
531+
let process = Process()
532+
process.executableURL = url
533+
process.arguments = arguments
534+
process.terminationHandler = terminationHandler
535+
try process.run()
536+
return process
537+
}
538+
539+
@available(*, deprecated, renamed: "run(_:arguments:terminationHandler:)")
513540
// convenience; create and launch
514541
open class func launchedProcess(launchPath path: String, arguments: [String]) -> Process {
515542
let process = Process()
@@ -519,7 +546,7 @@ extension Process {
519546

520547
return process
521548
}
522-
549+
523550
// poll the runLoop in defaultMode until process completes
524551
open func waitUntilExit() {
525552

TestFoundation/TestProcess.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This source file is part of the Swift.org open source project
22
//
3-
// Copyright (c) 2015 - 2016 Apple Inc. and the Swift project authors
3+
// Copyright (c) 2015 - 2016, 2018 Apple Inc. and the Swift project authors
44
// Licensed under Apache License v2.0 with Runtime Library Exception
55
//
66
// See http://swift.org/LICENSE.txt for license information
@@ -28,6 +28,7 @@ class TestProcess : XCTestCase {
2828
("test_passthrough_environment", test_passthrough_environment),
2929
("test_no_environment", test_no_environment),
3030
("test_custom_environment", test_custom_environment),
31+
("test_run", test_run),
3132
]
3233
#endif
3334
}
@@ -286,6 +287,49 @@ class TestProcess : XCTestCase {
286287
XCTFail("Test failed: \(error)")
287288
}
288289
}
290+
291+
func test_run() {
292+
let fm = FileManager.default
293+
let cwd = fm.currentDirectoryPath
294+
295+
do {
296+
let process = try Process.run(URL(fileURLWithPath: "/bin/sh", isDirectory: false), arguments: ["-c", "exit 123"], terminationHandler: nil)
297+
process.waitUntilExit()
298+
XCTAssertEqual(process.terminationReason, .exit)
299+
XCTAssertEqual(process.terminationStatus, 123)
300+
} catch {
301+
XCTFail("Cant execute /bin/sh: \(error)")
302+
}
303+
XCTAssertEqual(fm.currentDirectoryPath, cwd)
304+
305+
do {
306+
let process = Process()
307+
process.executableURL = URL(fileURLWithPath: "/bin/sh", isDirectory: false)
308+
process.arguments = ["-c", "exit 0"]
309+
process.currentDirectoryURL = URL(fileURLWithPath: "/.../_no_such_directory", isDirectory: true)
310+
try process.run()
311+
XCTFail("Executed /bin/sh with invalid currentDirectoryURL")
312+
process.terminate()
313+
process.waitUntilExit()
314+
} catch {
315+
}
316+
XCTAssertEqual(fm.currentDirectoryPath, cwd)
317+
318+
do {
319+
let process = Process()
320+
process.executableURL = URL(fileURLWithPath: "/..", isDirectory: false)
321+
process.arguments = []
322+
process.currentDirectoryURL = URL(fileURLWithPath: "/tmp")
323+
try process.run()
324+
XCTFail("Somehow executed a directory!")
325+
process.terminate()
326+
process.waitUntilExit()
327+
} catch {
328+
}
329+
XCTAssertEqual(fm.currentDirectoryPath, cwd)
330+
fm.changeCurrentDirectoryPath(cwd)
331+
}
332+
289333
#endif
290334
}
291335

0 commit comments

Comments
 (0)