Skip to content

Commit 051de9b

Browse files
committed
TSCBasic: unify subprocess handling
This unifies the platforms to Foundation for the process handling. This simplifies the logic and gives a single codepath which can possibly serve as an alternate path to #209.
1 parent fc5b8a5 commit 051de9b

File tree

1 file changed

+1
-278
lines changed

1 file changed

+1
-278
lines changed

Sources/TSCBasic/Process.swift

Lines changed: 1 addition & 278 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import class Foundation.ProcessInfo
1212
import protocol Foundation.CustomNSError
1313
import var Foundation.NSLocalizedDescriptionKey
1414

15-
#if os(Windows)
1615
import Foundation
17-
#endif
1816

1917
@_implementationOnly import TSCclibc
2018
import TSCLibc
@@ -210,14 +208,10 @@ public final class Process: ObjectIdentifierProtocol {
210208
public let workingDirectory: AbsolutePath?
211209

212210
/// The process id of the spawned process, available after the process is launched.
213-
#if os(Windows)
214211
private var _process: Foundation.Process?
215212
public var processID: ProcessID {
216-
return DWORD(_process?.processIdentifier ?? 0)
213+
return ProcessID(_process?.processIdentifier ?? 0)
217214
}
218-
#else
219-
public private(set) var processID = ProcessID()
220-
#endif
221215

222216
/// If the subprocess has launched.
223217
/// Note: This property is not protected by the serial queue because it is only mutated in `launch()`, which will be
@@ -384,7 +378,6 @@ public final class Process: ObjectIdentifierProtocol {
384378
throw Process.Error.missingExecutableProgram(program: executable)
385379
}
386380

387-
#if os(Windows)
388381
_process = Foundation.Process()
389382
_process?.arguments = Array(arguments.dropFirst()) // Avoid including the executable URL twice.
390383
_process?.executableURL = executablePath.asURL
@@ -416,175 +409,11 @@ public final class Process: ObjectIdentifierProtocol {
416409

417410
try _process?.run()
418411
return stdinPipe.fileHandleForWriting
419-
#else
420-
// Initialize the spawn attributes.
421-
#if canImport(Darwin) || os(Android)
422-
var attributes: posix_spawnattr_t? = nil
423-
#else
424-
var attributes = posix_spawnattr_t()
425-
#endif
426-
posix_spawnattr_init(&attributes)
427-
defer { posix_spawnattr_destroy(&attributes) }
428-
429-
// Unmask all signals.
430-
var noSignals = sigset_t()
431-
sigemptyset(&noSignals)
432-
posix_spawnattr_setsigmask(&attributes, &noSignals)
433-
434-
// Reset all signals to default behavior.
435-
#if os(macOS)
436-
var mostSignals = sigset_t()
437-
sigfillset(&mostSignals)
438-
sigdelset(&mostSignals, SIGKILL)
439-
sigdelset(&mostSignals, SIGSTOP)
440-
posix_spawnattr_setsigdefault(&attributes, &mostSignals)
441-
#else
442-
// On Linux, this can only be used to reset signals that are legal to
443-
// modify, so we have to take care about the set we use.
444-
var mostSignals = sigset_t()
445-
sigemptyset(&mostSignals)
446-
for i in 1 ..< SIGSYS {
447-
if i == SIGKILL || i == SIGSTOP {
448-
continue
449-
}
450-
sigaddset(&mostSignals, i)
451-
}
452-
posix_spawnattr_setsigdefault(&attributes, &mostSignals)
453-
#endif
454-
455-
// Set the attribute flags.
456-
var flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF
457-
if startNewProcessGroup {
458-
// Establish a separate process group.
459-
flags |= POSIX_SPAWN_SETPGROUP
460-
posix_spawnattr_setpgroup(&attributes, 0)
461-
}
462-
463-
posix_spawnattr_setflags(&attributes, Int16(flags))
464-
465-
// Setup the file actions.
466-
#if canImport(Darwin) || os(Android)
467-
var fileActions: posix_spawn_file_actions_t? = nil
468-
#else
469-
var fileActions = posix_spawn_file_actions_t()
470-
#endif
471-
posix_spawn_file_actions_init(&fileActions)
472-
defer { posix_spawn_file_actions_destroy(&fileActions) }
473-
474-
if let workingDirectory = workingDirectory?.pathString {
475-
#if os(macOS)
476-
// The only way to set a workingDirectory is using an availability-gated initializer, so we don't need
477-
// to handle the case where the posix_spawn_file_actions_addchdir_np method is unavailable. This check only
478-
// exists here to make the compiler happy.
479-
if #available(macOS 10.15, *) {
480-
posix_spawn_file_actions_addchdir_np(&fileActions, workingDirectory)
481-
}
482-
#elseif os(Linux)
483-
guard SPM_posix_spawn_file_actions_addchdir_np_supported() else {
484-
throw Process.Error.workingDirectoryNotSupported
485-
}
486-
487-
SPM_posix_spawn_file_actions_addchdir_np(&fileActions, workingDirectory)
488-
#else
489-
throw Process.Error.workingDirectoryNotSupported
490-
#endif
491-
}
492-
493-
var stdinPipe: [Int32] = [-1, -1]
494-
try open(pipe: &stdinPipe)
495-
496-
let stdinStream = try LocalFileOutputByteStream(filePointer: fdopen(stdinPipe[1], "wb"), closeOnDeinit: true)
497-
498-
// Dupe the read portion of the remote to 0.
499-
posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], 0)
500-
501-
// Close the other side's pipe since it was dupped to 0.
502-
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0])
503-
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1])
504-
505-
var outputPipe: [Int32] = [-1, -1]
506-
var stderrPipe: [Int32] = [-1, -1]
507-
if outputRedirection.redirectsOutput {
508-
// Open the pipe.
509-
try open(pipe: &outputPipe)
510-
511-
// Open the write end of the pipe.
512-
posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1)
513-
514-
// Close the other ends of the pipe since they were dupped to 1.
515-
posix_spawn_file_actions_addclose(&fileActions, outputPipe[0])
516-
posix_spawn_file_actions_addclose(&fileActions, outputPipe[1])
517-
518-
if outputRedirection.redirectStderr {
519-
// If merged was requested, send stderr to stdout.
520-
posix_spawn_file_actions_adddup2(&fileActions, 1, 2)
521-
} else {
522-
// If no redirect was requested, open the pipe for stderr.
523-
try open(pipe: &stderrPipe)
524-
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2)
525-
526-
// Close the other ends of the pipe since they were dupped to 2.
527-
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])
528-
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])
529-
}
530-
} else {
531-
posix_spawn_file_actions_adddup2(&fileActions, 1, 1)
532-
posix_spawn_file_actions_adddup2(&fileActions, 2, 2)
533-
}
534-
535-
var resolvedArgs = arguments
536-
if workingDirectory != nil {
537-
resolvedArgs[0] = executablePath.pathString
538-
}
539-
let argv = CStringArray(resolvedArgs)
540-
let env = CStringArray(environment.map({ "\($0.0)=\($0.1)" }))
541-
let rv = posix_spawnp(&processID, argv.cArray[0]!, &fileActions, &attributes, argv.cArray, env.cArray)
542-
543-
guard rv == 0 else {
544-
throw SystemError.posix_spawn(rv, arguments)
545-
}
546-
547-
// Close the local read end of the input pipe.
548-
try close(fd: stdinPipe[0])
549-
550-
if outputRedirection.redirectsOutput {
551-
let outputClosures = outputRedirection.outputClosures
552-
553-
// Close the local write end of the output pipe.
554-
try close(fd: outputPipe[1])
555-
556-
// Create a thread and start reading the output on it.
557-
var thread = Thread { [weak self] in
558-
if let readResult = self?.readOutput(onFD: outputPipe[0], outputClosure: outputClosures?.stdoutClosure) {
559-
self?.stdout.result = readResult
560-
}
561-
}
562-
thread.start()
563-
self.stdout.thread = thread
564-
565-
// Only schedule a thread for stderr if no redirect was requested.
566-
if !outputRedirection.redirectStderr {
567-
// Close the local write end of the stderr pipe.
568-
try close(fd: stderrPipe[1])
569-
570-
// Create a thread and start reading the stderr output on it.
571-
thread = Thread { [weak self] in
572-
if let readResult = self?.readOutput(onFD: stderrPipe[0], outputClosure: outputClosures?.stderrClosure) {
573-
self?.stderr.result = readResult
574-
}
575-
}
576-
thread.start()
577-
self.stderr.thread = thread
578-
}
579-
}
580-
return stdinStream
581-
#endif // POSIX implementation
582412
}
583413

584414
/// Blocks the calling process until the subprocess finishes execution.
585415
@discardableResult
586416
public func waitUntilExit() throws -> ProcessResult {
587-
#if os(Windows)
588417
precondition(_process != nil, "The process is not yet launched.")
589418
let p = _process!
590419
p.waitUntilExit()
@@ -599,88 +428,8 @@ public final class Process: ObjectIdentifierProtocol {
599428
stderrOutput: stderr.result
600429
)
601430
return executionResult
602-
#else
603-
return try serialQueue.sync {
604-
precondition(launched, "The process is not yet launched.")
605-
606-
// If the process has already finsihed, return it.
607-
if let existingResult = _result {
608-
return existingResult
609-
}
610-
611-
// If we're reading output, make sure that is finished.
612-
stdout.thread?.join()
613-
stderr.thread?.join()
614-
615-
// Wait until process finishes execution.
616-
var exitStatusCode: Int32 = 0
617-
var result = waitpid(processID, &exitStatusCode, 0)
618-
while result == -1 && errno == EINTR {
619-
result = waitpid(processID, &exitStatusCode, 0)
620-
}
621-
if result == -1 {
622-
throw SystemError.waitpid(errno)
623-
}
624-
625-
// Construct the result.
626-
let executionResult = ProcessResult(
627-
arguments: arguments,
628-
environment: environment,
629-
exitStatusCode: exitStatusCode,
630-
output: stdout.result,
631-
stderrOutput: stderr.result
632-
)
633-
self._result = executionResult
634-
return executionResult
635-
}
636-
#endif
637431
}
638432

639-
#if !os(Windows)
640-
/// Reads the given fd and returns its result.
641-
///
642-
/// Closes the fd before returning.
643-
private func readOutput(onFD fd: Int32, outputClosure: OutputClosure?) -> Result<[UInt8], Swift.Error> {
644-
// Read all of the data from the output pipe.
645-
let N = 4096
646-
var buf = [UInt8](repeating: 0, count: N + 1)
647-
648-
var out = [UInt8]()
649-
var error: Swift.Error? = nil
650-
loop: while true {
651-
let n = read(fd, &buf, N)
652-
switch n {
653-
case -1:
654-
if errno == EINTR {
655-
continue
656-
} else {
657-
error = SystemError.read(errno)
658-
break loop
659-
}
660-
case 0:
661-
// Close the read end of the output pipe.
662-
// We should avoid closing the read end of the pipe in case
663-
// -1 because the child process may still have content to be
664-
// flushed into the write end of the pipe. If the read end of the
665-
// pipe is closed, then a write will cause a SIGPIPE signal to
666-
// be generated for the calling process. If the calling process is
667-
// ignoring this signal, then write fails with the error EPIPE.
668-
close(fd)
669-
break loop
670-
default:
671-
let data = buf[0..<n]
672-
if let outputClosure = outputClosure {
673-
outputClosure(Array(data))
674-
} else {
675-
out += data
676-
}
677-
}
678-
}
679-
// Construct the output result.
680-
return error.map(Result.failure) ?? .success(out)
681-
}
682-
#endif
683-
684433
/// Send a signal to the process.
685434
///
686435
/// Note: This will signal all processes in the process group.
@@ -750,12 +499,6 @@ extension Process {
750499
// MARK: - Private helpers
751500

752501
#if !os(Windows)
753-
#if os(macOS)
754-
private typealias swiftpm_posix_spawn_file_actions_t = posix_spawn_file_actions_t?
755-
#else
756-
private typealias swiftpm_posix_spawn_file_actions_t = posix_spawn_file_actions_t
757-
#endif
758-
759502
private func WIFEXITED(_ status: Int32) -> Bool {
760503
return _WSTATUS(status) == 0
761504
}
@@ -776,26 +519,6 @@ private func WTERMSIG(_ status: Int32) -> Int32 {
776519
return status & 0x7f
777520
}
778521

779-
/// Open the given pipe.
780-
private func open(pipe: inout [Int32]) throws {
781-
let rv = TSCLibc.pipe(&pipe)
782-
guard rv == 0 else {
783-
throw SystemError.pipe(rv)
784-
}
785-
}
786-
787-
/// Close the given fd.
788-
private func close(fd: Int32) throws {
789-
func innerClose(_ fd: inout Int32) throws {
790-
let rv = TSCLibc.close(fd)
791-
guard rv == 0 else {
792-
throw SystemError.close(rv)
793-
}
794-
}
795-
var innerFd = fd
796-
try innerClose(&innerFd)
797-
}
798-
799522
extension Process.Error: CustomStringConvertible {
800523
public var description: String {
801524
switch self {

0 commit comments

Comments
 (0)