Unexpected/inconsistent behavior in Process.launch()

Here's launch-nonexistent.swift:

import Foundation
let task = Process()
task.launchPath = "/usr/bin/nonexistantfile"
task.launch()
print("OK")

(/usr/bin/nonexistantfile does not, in fact, exist. :)

Behavior is identical across various releases of 4.0 on each platform and the latest dev snapshot.

Running this on macOS yields: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'launch path not accessible'

Running this on Debian testing yields: fatal error: POSIX command failed with error: 2: file Foundation/Process.swift, line 515

Running this on Ubuntu yields: OK

Inspecting task in the REPL yields:

task: Foundation.Process = {
  Foundation.NSObject = {}
  launchPath = "/usr/bin/nonexistent"
  arguments = nil
  environment = nil
  currentDirectoryPath = "/home/alex"
  standardInput = nil
  standardOutput = nil
  standardError = nil
  runLoopSourceContext = some {
    version = 0
    info = (_rawValue = 0x0000000000445560 -> 0x00007ffff42a7cd8 full type metadata for Foundation.Process + 16)
    retain = 0x00007ffff3d94fa0 libFoundation.so`@objc Foundation.(runLoopSourceRetain in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeRawPointer>) -> Swift.Optional<Swift.UnsafeRawPointer>
    release = 0x00007ffff3d94fb0 libFoundation.so`@objc Foundation.(runLoopSourceRelease in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeRawPointer>) -> ()
    copyDescription = nil
    equal = 0x00007ffff3d98390 libFoundation.so`@objc Foundation.(processIsEqual in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeRawPointer>, Swift.Optional<Swift.UnsafeRawPointer>) -> Swift.Bool
    hash = nil
    schedule = nil
    cancel = nil
    perform = 0x00007ffff3d94fe0 libFoundation.so`@objc Foundation.(emptyRunLoopCallback in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeMutableRawPointer>) -> ()
  }
  runLoopSource = 0x0000000000446910 {
    Foundation.NSObject = {}
    _cfinfo = 9864
  }
  runLoop = 0x0000000000426d80 {
    Foundation.NSObject = {}
    _cfRunLoop = 0x00007fffe8000e00 {
      Foundation.NSObject = {}
      _cfinfo = 9088
    }
  }
  processLaunchedCondition = {
    Foundation.NSObject = {}
    mutex = 0x00000000004468b0
    cond = 0x00000000004454b0
    name = nil
  }
  processIdentifier = 6291
  isRunning = true
  terminationStatus = 0
  terminationReason = exit
  terminationHandler = nil
  qualityOfService = default
}

Compared to a healthy task, such as /bin/ls, there aren't many differences:

task: Foundation.Process = {
  Foundation.NSObject = {}
  launchPath = "/bin/ls"
  arguments = nil
  environment = nil
  currentDirectoryPath = "/home/alex"
  standardInput = nil
  standardOutput = nil
  standardError = nil
  runLoopSourceContext = some {
    version = 0
    info = (_rawValue = 0x0000000000445560 -> 0x00007ffff42a7cd8 full type metadata for Foundation.Process + 16)
    retain = 0x00007ffff3d94fa0 libFoundation.so`@objc Foundation.(runLoopSourceRetain in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeRawPointer>) -> Swift.Optional<Swift.UnsafeRawPointer>
    release = 0x00007ffff3d94fb0 libFoundation.so`@objc Foundation.(runLoopSourceRelease in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeRawPointer>) -> ()
    copyDescription = nil
    equal = 0x00007ffff3d98390 libFoundation.so`@objc Foundation.(processIsEqual in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeRawPointer>, Swift.Optional<Swift.UnsafeRawPointer>) -> Swift.Bool
    hash = nil
    schedule = nil
    cancel = nil
    perform = 0x00007ffff3d94fe0 libFoundation.so`@objc Foundation.(emptyRunLoopCallback in _05FBE738056ADAC488E6D2B534411459)(Swift.Optional<Swift.UnsafeMutableRawPointer>) -> ()
  }
  runLoopSource = 0x0000000000446910 {
    Foundation.NSObject = {}
    _cfinfo = 9864
  }
  runLoop = 0x0000000000426d80 {
    Foundation.NSObject = {}
    _cfRunLoop = 0x00007fffe8000e00 {
      Foundation.NSObject = {}
      _cfinfo = 9088
    }
  }
  processLaunchedCondition = {
    Foundation.NSObject = {}
    mutex = 0x00000000004468b0
    cond = 0x00000000004454b0
    name = nil
  }
  processIdentifier = 6177
  isRunning = true
  terminationStatus = 0
  terminationReason = exit
  terminationHandler = nil
  qualityOfService = default
}

There are a few different consequences of this - I've found code in the wild that depends on this behavior.

Before I go any further, what's the correct/expected behavior here?

Sorry, but how does this depend on the behavior you're mentioning? This seems like a straightforward use of Process to me.

The code I linked depends on Process.launch() on Linux, when launching a nonexistent binary (/usr/bin/xcrun does not exist on Linux), not raising an exception, fatalErroring, etc. This is not the behavior on macOS (since we have ObjC exceptions there), and not the behavior observed on non-Ubuntu Linuxes. (As an aside - I'm not sure how, on Ubuntu, a user could tell the difference between a failed and successful launch.)

If Ubuntu starts fatalErroring this case, that code will yield fatalError instead of the empty string; this causes a number of tests for that project to fail.

As far as I'm aware, this code path in SourceKitten is only run on macOS, since it depends on Xcode. Am I mistaken?

It's hit regardless of OS during tests, for example, SourceKitten/CodeCompletionTests.swift at main · jpsim/SourceKitten · GitHub

As a result, tests pass on Ubuntu but fail on Debian like so:

❯ swift test
Compile Swift Module 'SourceKittenFramework' (34 sources)
Compile Swift Module 'sourcekitten' (10 sources)
Compile Swift Module 'SourceKittenFrameworkTests' (11 sources)
Linking ./.build/x86_64-unknown-linux/debug/sourcekitten
Compile Swift Module 'SourceKittenPackageTests' (1 sources)
Linking ./.build/x86_64-unknown-linux/debug/SourceKittenPackageTests.xctest
Test Suite 'All tests' started at 2018-01-31 21:00:59.468
Test Suite 'debug.xctest' started at 2018-01-31 21:00:59.470
Test Suite 'CodeCompletionTests' started at 2018-01-31 21:00:59.470
Test Case 'CodeCompletionTests.testSimpleCodeCompletion' started at 2018-01-31 21:00:59.470
fatal error: POSIX command failed with error: 2: file Foundation/Process.swift, line 515

Also, regardless of use in the wild, one behavior is correct and the other isn't, and I can't propose a PR without choosing one. :)

I think the compatible behavior here is to fatalError when the path doesn't exist. That said, it's an unfortunate API for swift and probably should throw. Eventually we should introduce one that has an error argument in ObjC and comes to Swift in a friendlier way.

2 Likes