Skip to content

Commit 3c2fc82

Browse files
authored
Merge pull request #1907 from compnerd/NSTask
Process: port to Windows
2 parents 5894e8a + fc2a0d2 commit 3c2fc82

File tree

2 files changed

+281
-7
lines changed

2 files changed

+281
-7
lines changed

Foundation/FileHandle.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ open class FileHandle : NSObject, NSSecureCoding {
2727
#if os(Windows)
2828
private var _handle: HANDLE
2929

30+
internal var handle: HANDLE {
31+
return _handle
32+
}
33+
3034
@available(Windows, unavailable, message: "Cannot perform non-owning handle to fd conversion")
3135
open var fileDescriptor: Int32 {
3236
NSUnsupported()
@@ -49,7 +53,7 @@ open class FileHandle : NSObject, NSSecureCoding {
4953
private func _checkFileHandle() {
5054
precondition(_fd >= 0, "Bad file descriptor")
5155
}
52-
56+
5357
private var _isPlatformHandleValid: Bool {
5458
return fileDescriptor >= 0
5559
}
@@ -248,7 +252,7 @@ open class FileHandle : NSObject, NSSecureCoding {
248252
}
249253

250254
#if os(Windows)
251-
public init(handle: HANDLE, closeOnDealloc closeopt: Bool) {
255+
internal init(handle: HANDLE, closeOnDealloc closeopt: Bool) {
252256
_handle = handle
253257
_closeOnDealloc = closeopt
254258
}

Foundation/Process.swift

Lines changed: 275 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,23 +232,238 @@ open class Process: NSObject {
232232
}
233233
}
234234

235+
#if os(Windows)
236+
private func _socketpair() -> (first: SOCKET, second: SOCKET) {
237+
let listener: SOCKET = socket(AF_INET, SOCK_STREAM, 0)
238+
if listener == INVALID_SOCKET {
239+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
240+
}
241+
defer { closesocket(listener) }
242+
243+
var result: Int32 = SOCKET_ERROR
244+
245+
var address: sockaddr_in =
246+
sockaddr_in(sin_family: ADDRESS_FAMILY(AF_INET), sin_port: USHORT(0),
247+
sin_addr: IN_ADDR(S_un: in_addr.__Unnamed_union_S_un(S_addr: ULONG("127.0.0.1")!)),
248+
sin_zero: (CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0)))
249+
withUnsafePointer(to: &address) {
250+
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
251+
result = bind(listener, $0, Int32(MemoryLayout<sockaddr_in>.size))
252+
}
253+
}
254+
255+
if result == SOCKET_ERROR {
256+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
257+
}
258+
259+
if listen(listener, 1) == SOCKET_ERROR {
260+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
261+
}
262+
263+
withUnsafeMutablePointer(to: &address) {
264+
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
265+
var value: Int32 = Int32(MemoryLayout<sockaddr_in>.size)
266+
result = getsockname(listener, $0, &value)
267+
}
268+
}
269+
if result == SOCKET_ERROR {
270+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
271+
}
272+
273+
let first: SOCKET = socket(AF_INET, SOCK_STREAM, 0)
274+
if first == INVALID_SOCKET {
275+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
276+
}
277+
278+
var value: u_long = 1
279+
if ioctlsocket(first, FIONBIO, &value) == SOCKET_ERROR {
280+
closesocket(first)
281+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
282+
}
283+
284+
withUnsafePointer(to: &address) {
285+
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
286+
result = connect(first, $0, Int32(MemoryLayout<sockaddr_in>.size))
287+
}
288+
}
289+
290+
if result == SOCKET_ERROR {
291+
closesocket(first)
292+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
293+
}
294+
295+
let second: SOCKET = accept(listener, nil, nil)
296+
if second == INVALID_SOCKET {
297+
closesocket(first)
298+
return (first: INVALID_SOCKET, second: INVALID_SOCKET)
299+
}
300+
301+
return (first: first, second: second)
302+
}
303+
#endif
304+
235305
open func run() throws {
236-
237306
self.processLaunchedCondition.lock()
238307
defer {
239308
self.processLaunchedCondition.broadcast()
240309
self.processLaunchedCondition.unlock()
241310
}
242311

243312
// Dispatch the manager thread if it isn't already running
244-
245313
Process.setup()
246-
314+
247315
// Ensure that the launch path is set
248316
guard let launchPath = self.executableURL?.path else {
249317
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError)
250318
}
251319

320+
#if os(Windows)
321+
// TODO(compnerd) quote the commandline correctly
322+
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
323+
var command: [String] = [launchPath]
324+
if let arguments = self.arguments {
325+
command.append(contentsOf: arguments)
326+
}
327+
328+
var siStartupInfo: STARTUPINFOW = STARTUPINFOW()
329+
siStartupInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)
330+
331+
switch standardInput {
332+
case let pipe as Pipe:
333+
siStartupInfo.hStdInput = pipe.fileHandleForReading.handle
334+
case let handle as FileHandle:
335+
siStartupInfo.hStdInput = handle.handle
336+
default: break
337+
}
338+
339+
switch standardOutput {
340+
case let pipe as Pipe:
341+
siStartupInfo.hStdOutput = pipe.fileHandleForWriting.handle
342+
case let handle as FileHandle:
343+
siStartupInfo.hStdOutput = handle.handle
344+
default: break
345+
}
346+
347+
switch standardError {
348+
case let pipe as Pipe:
349+
siStartupInfo.hStdError = pipe.fileHandleForWriting.handle
350+
case let handle as FileHandle:
351+
siStartupInfo.hStdError = handle.handle
352+
default: break
353+
}
354+
355+
var piProcessInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
356+
357+
var environment: [String:String] = [:]
358+
if let env = self.environment {
359+
environment = env
360+
} else {
361+
environment = ProcessInfo.processInfo.environment
362+
environment["PWD"] = currentDirectoryURL.path
363+
}
364+
365+
let szEnvironment: String = environment.map { $0.key + "=" + $0.value }.joined(separator: "\0")
366+
367+
let sockets: (first: SOCKET, second: SOCKET) = _socketpair()
368+
369+
var context: CFSocketContext = CFSocketContext()
370+
context.version = 0
371+
context.retain = runLoopSourceRetain
372+
context.release = runLoopSourceRelease
373+
context.info = Unmanaged.passUnretained(self).toOpaque()
374+
375+
let socket: CFSocket =
376+
CFSocketCreateWithNative(nil, CFSocketNativeHandle(sockets.first), CFOptionFlags(kCFSocketDataCallBack), { (socket, type, address, data, info) in
377+
let process: Process = NSObject.unretainedReference(info!)
378+
process.processLaunchedCondition.lock()
379+
while process.isRunning == false {
380+
process.processLaunchedCondition.wait()
381+
}
382+
process.processLaunchedCondition.unlock()
383+
384+
WaitForSingleObject(process.processHandle, WinSDK.INFINITE)
385+
386+
var dwExitCode: DWORD = 0
387+
// FIXME(compnerd) how do we handle errors here?
388+
GetExitCodeProcess(process.processHandle, &dwExitCode)
389+
390+
// TODO(compnerd) check if the process terminated abnormally
391+
process._terminationStatus = Int32(dwExitCode)
392+
process._terminationReason = .exit
393+
394+
if let handler = process.terminationHandler {
395+
let thread: Thread = Thread { handler(process) }
396+
thread.start()
397+
}
398+
399+
process.isRunning = false
400+
401+
// Invalidate the source and wake up the run loop, if it is available
402+
CFRunLoopSourceInvalidate(process.runLoopSource)
403+
if let runloop = process.runLoop {
404+
CFRunLoopWakeUp(runloop._cfRunLoop)
405+
}
406+
407+
CFSocketInvalidate(socket)
408+
}, &context)
409+
CFSocketSetSocketFlags(socket, CFOptionFlags(kCFSocketCloseOnInvalidate))
410+
411+
let source: CFRunLoopSource =
412+
CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0)
413+
CFRunLoopAddSource(managerThreadRunLoop?._cfRunLoop, source, kCFRunLoopDefaultMode)
414+
415+
try command.joined(separator: " ").withCString(encodedAs: UTF16.self) { wszCommandLine in
416+
try currentDirectoryURL.path.withCString(encodedAs: UTF16.self) { wszCurrentDirectory in
417+
try szEnvironment.withCString(encodedAs: UTF16.self) { wszEnvironment in
418+
if CreateProcessW(nil, UnsafeMutablePointer<WCHAR>(mutating: wszCommandLine),
419+
nil, nil, TRUE,
420+
DWORD(CREATE_UNICODE_ENVIRONMENT), UnsafeMutableRawPointer(mutating: wszEnvironment),
421+
wszCurrentDirectory,
422+
&siStartupInfo, &piProcessInfo) == FALSE {
423+
throw NSError(domain: _NSWindowsErrorDomain, code: Int(GetLastError()))
424+
}
425+
}
426+
}
427+
}
428+
429+
self.processHandle = piProcessInfo.hProcess
430+
if CloseHandle(piProcessInfo.hThread) == FALSE {
431+
throw NSError(domain: _NSWindowsErrorDomain, code: Int(GetLastError()))
432+
}
433+
434+
if let pipe = standardInput as? Pipe {
435+
pipe.fileHandleForReading.closeFile()
436+
pipe.fileHandleForWriting.closeFile()
437+
}
438+
if let pipe = standardOutput as? Pipe {
439+
pipe.fileHandleForReading.closeFile()
440+
pipe.fileHandleForWriting.closeFile()
441+
}
442+
if let pipe = standardError as? Pipe {
443+
pipe.fileHandleForWriting.closeFile()
444+
pipe.fileHandleForReading.closeFile()
445+
}
446+
447+
self.runLoop = RunLoop.current
448+
self.runLoopSourceContext =
449+
CFRunLoopSourceContext(version: 0,
450+
info: Unmanaged.passUnretained(self).toOpaque(),
451+
retain: { runLoopSourceRetain($0) },
452+
release: { runLoopSourceRelease($0) },
453+
copyDescription: nil,
454+
equal: { processIsEqual($0, $1) },
455+
hash: nil,
456+
schedule: nil,
457+
cancel: nil,
458+
perform: { emptyRunLoopCallback($0) })
459+
self.runLoopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &self.runLoopSourceContext!)
460+
461+
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopDefaultMode)
462+
463+
isRunning = true
464+
465+
closesocket(sockets.second)
466+
#else
252467
// Initial checks that the launchPath points to an executable file. posix_spawn()
253468
// can return success even if executing the program fails, eg fork() works but execve()
254469
// fails, so try and check as much as possible beforehand.
@@ -258,9 +473,11 @@ open class Process: NSObject {
258473
throw _NSErrorWithErrno(errno, reading: true, path: launchPath)
259474
}
260475

261-
guard statInfo.st_mode & S_IFMT == S_IFREG else {
476+
let isRegularFile: Bool = statInfo.st_mode & S_IFMT == S_IFREG
477+
guard isRegularFile == true else {
262478
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError)
263479
}
480+
264481
guard access(fsRep, X_OK) == 0 else {
265482
throw _NSErrorWithErrno(errno, reading: true, path: launchPath)
266483
}
@@ -484,46 +701,99 @@ open class Process: NSObject {
484701
isRunning = true
485702

486703
self.processIdentifier = pid
704+
#endif
487705
}
488706

489707
open func interrupt() {
490708
precondition(hasStarted, "task not launched")
709+
#if os(Windows)
710+
TerminateProcess(processHandle, UINT(SIGINT))
711+
#else
491712
kill(processIdentifier, SIGINT)
713+
#endif
492714
}
493715

494716
open func terminate() {
495717
precondition(hasStarted, "task not launched")
718+
#if os(Windows)
719+
TerminateProcess(processHandle, UINT(SIGTERM))
720+
#else
496721
kill(processIdentifier, SIGTERM)
722+
#endif
497723
}
498724

499725
// Every suspend() has to be balanced with a resume() so keep a count of both.
500726
private var suspendCount = 0
501727

502728
open func suspend() -> Bool {
729+
#if os(Windows)
730+
let pNTSuspendProcess: Optional<(HANDLE) -> LONG> =
731+
unsafeBitCast(GetProcAddress(GetModuleHandleA("ntdll.dll"),
732+
"NtSuspendProcess"),
733+
to: Optional<(HANDLE) -> LONG>.self)
734+
if let pNTSuspendProcess = pNTSuspendProcess {
735+
if pNTSuspendProcess(processHandle) < 0 {
736+
return false
737+
}
738+
suspendCount += 1
739+
return true
740+
}
741+
return false
742+
#else
503743
if kill(processIdentifier, SIGSTOP) == 0 {
504744
suspendCount += 1
505745
return true
506746
} else {
507747
return false
508748
}
749+
#endif
509750
}
510751

511752
open func resume() -> Bool {
512-
var success = true
753+
var success: Bool = true
754+
#if os(Windows)
755+
if suspendCount == 1 {
756+
let pNTResumeProcess: Optional<(HANDLE) -> NTSTATUS> =
757+
unsafeBitCast(GetProcAddress(GetModuleHandleA("ntdll.dll"),
758+
"NtResumeProcess"),
759+
to: Optional<(HANDLE) -> NTSTATUS>.self)
760+
if let pNTResumeProcess = pNTResumeProcess {
761+
if pNTResumeProcess(processHandle) < 0 {
762+
success = false
763+
}
764+
}
765+
}
766+
#else
513767
if suspendCount == 1 {
514768
success = kill(processIdentifier, SIGCONT) == 0
515769
}
770+
#endif
516771
if success {
517772
suspendCount -= 1
518773
}
519774
return success
520775
}
521776

522777
// status
778+
#if os(Windows)
779+
open private(set) var processHandle: HANDLE = INVALID_HANDLE_VALUE
780+
open private(set) var processIdentifier: Int32 {
781+
return Int32(GetProcessId(processHandle))
782+
}
783+
open private(set) var isRunning: Bool = false
784+
785+
private var hasStarted: Bool {
786+
return processHandle != INVALID_HANDLE_VALUE
787+
}
788+
private var hasFinished: Bool {
789+
return hasStarted && !isRunning
790+
}
791+
#else
523792
open private(set) var processIdentifier: Int32 = 0
524793
open private(set) var isRunning: Bool = false
525794
private var hasStarted: Bool { return processIdentifier > 0 }
526795
private var hasFinished: Bool { return !isRunning && processIdentifier > 0 }
796+
#endif
527797

528798
private var _terminationStatus: Int32 = 0
529799
public var terminationStatus: Int32 {

0 commit comments

Comments
 (0)