Pitch: Replacement for Process


(Charles Srstka) #1

MOTIVATION:

In Swift 3, NSTask was renamed to Process, making it the de facto API for spawning external tasks in Swift applications. Unfortunately, NSTask uses Objective-C exceptions to report runtime errors in spawning external tasks, which cannot be caught from Swift code. This means that if something goes wrong, like the file unexpectedly not existing at the launch path, or the current user not having permission to launch it or something, there’s no way to handle that gracefully other than crashing the whole program.

PROPOSED SOLUTION:

Rename Process back to NSTask, and provide a Swift-native Process class for Foundation in Swift 4 that mimics NSTask's interface, but provides a throwing version of the ‘launch’ method:

open class Process {
    // ...
    
    open func launch() throws

    // ...
}

Almost all of the work for this is already done in the swift-corelibs-Foundation project. The only NSUnimplemented() calls are in the suspend(), resume(), interrupt(), and terminate() methods, which do not seem difficult to implement. Beyond that, there are three fatalError() calls in the source file, which would need to be replaced with throws. There would also, of course, need to be “throws” annotations on the declarations for launch() and posix(), and “try” keywords added to the four calls to posix(). All told, not a large undertaking.

For backward source compatibility, the existing, non-throwing APIs could be provided as well, but deprecated. These could simply call the throwing APIs and either call fatalError() or throw an NSException when an error is caught.

I cannot think of too many instances where NSTask objects need to be passed around, so bridging to Objective-C is probably not a major concern.

IMPACT ON EXISTING CODE:

Since the new class would have the same name and public API as the existing Process (NSTask) class, this would not break source compatibility. It would break binary compatibility, which makes it a consideration for Swift 4.

Charles


(Charlie Monroe) #2

MOTIVATION:

In Swift 3, NSTask was renamed to Process, making it the de facto API for spawning external tasks in Swift applications. Unfortunately, NSTask uses Objective-C exceptions to report runtime errors in spawning external tasks, which cannot be caught from Swift code.

I know this may be OOT, but I've solved these for the time being by creating an ObjC class "ExceptionCatcher" that you can call with a block to be executed and one for catching an exception and it will catch the exception for you and pass it to the Swift block handling the exception. This can further be crafted in Swift so that the exception is Swift-thrown as an ExceptionError. But I agree this is a fairly ugly workaround and you need to be aware which methods can actually throw and exception.

Though I think something like this should be part of the ObjC overlay for the time being. We don't need a special language construct.

I believe that the change of these names was very pre-mature in most cases without proper Swift APIs, as you've mentioned.

So huge +1 for both your pitches (and any further in this direction).

···

On Feb 14, 2017, at 4:37 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

This means that if something goes wrong, like the file unexpectedly not existing at the launch path, or the current user not having permission to launch it or something, there’s no way to handle that gracefully other than crashing the whole program.

PROPOSED SOLUTION:

Rename Process back to NSTask, and provide a Swift-native Process class for Foundation in Swift 4 that mimics NSTask's interface, but provides a throwing version of the ‘launch’ method:

open class Process {
    // ...
    
    open func launch() throws

    // ...
}

Almost all of the work for this is already done in the swift-corelibs-Foundation project. The only NSUnimplemented() calls are in the suspend(), resume(), interrupt(), and terminate() methods, which do not seem difficult to implement. Beyond that, there are three fatalError() calls in the source file, which would need to be replaced with throws. There would also, of course, need to be “throws” annotations on the declarations for launch() and posix(), and “try” keywords added to the four calls to posix(). All told, not a large undertaking.

For backward source compatibility, the existing, non-throwing APIs could be provided as well, but deprecated. These could simply call the throwing APIs and either call fatalError() or throw an NSException when an error is caught.

I cannot think of too many instances where NSTask objects need to be passed around, so bridging to Objective-C is probably not a major concern.

IMPACT ON EXISTING CODE:

Since the new class would have the same name and public API as the existing Process (NSTask) class, this would not break source compatibility. It would break binary compatibility, which makes it a consideration for Swift 4.

Charles

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Charles Srstka) #3

I’ve done a little of that, too. Mostly, though, I use custom replacements of my own that I wrote in Objective-C years ago. It’d be nice if there were something standardized, though.

Charles

···

On Feb 14, 2017, at 10:23 AM, Charlie Monroe <charlie@charliemonroe.net> wrote:

On Feb 14, 2017, at 4:37 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

MOTIVATION:

In Swift 3, NSTask was renamed to Process, making it the de facto API for spawning external tasks in Swift applications. Unfortunately, NSTask uses Objective-C exceptions to report runtime errors in spawning external tasks, which cannot be caught from Swift code.

I know this may be OOT, but I've solved these for the time being by creating an ObjC class "ExceptionCatcher" that you can call with a block to be executed and one for catching an exception and it will catch the exception for you and pass it to the Swift block handling the exception. This can further be crafted in Swift so that the exception is Swift-thrown as an ExceptionError. But I agree this is a fairly ugly workaround and you need to be aware which methods can actually throw and exception.