2023-09-07 18:00:53 -05:00
//
// P r o c e s s + C o n v e n i e n c e s . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 9 / 6 / 2 3 .
// C o p y r i g h t © 2 0 2 3 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
import OSLog
import Combine
2026-02-22 21:27:31 +05:30
#if os ( macOS )
2023-09-07 18:00:53 -05:00
@ available ( macOS 12 , * )
extension Process
{
// B a s e d l o o s e l y o f f o f h t t p s : / / d e v e l o p e r . a p p l e . c o m / f o r u m s / t h r e a d / 6 9 0 3 1 0
class func launch ( _ toolURL : URL , arguments : [ String ] = [ ] , environment : [ String : String ] = ProcessInfo . processInfo . environment ) throws -> Process
{
let inputPipe = Pipe ( )
let outputPipe = Pipe ( )
let process = Process ( )
process . executableURL = toolURL
process . arguments = arguments
process . environment = environment
process . standardInput = inputPipe
process . standardOutput = outputPipe
process . standardError = outputPipe
func posixErr ( _ error : Int32 ) -> Error { NSError ( domain : NSPOSIXErrorDomain , code : Int ( error ) , userInfo : nil ) }
// I f y o u w r i t e t o a p i p e w h o s e r e m o t e e n d h a s c l o s e d , t h e O S r a i s e s a
// ` S I G P I P E ` s i g n a l w h o s e d e f a u l t d i s p o s i t i o n i s t o t e r m i n a t e y o u r
// p r o c e s s . H e l p f u l ! ` F _ S E T N O S I G P I P E ` d i s a b l e s t h a t f e a t u r e , c a u s i n g
// t h e w r i t e t o f a i l w i t h ` E P I P E ` i n s t e a d .
let fcntlResult = fcntl ( inputPipe . fileHandleForWriting . fileDescriptor , F_SETNOSIGPIPE , 1 )
guard fcntlResult >= 0 else { throw posixErr ( errno ) }
// A c t u a l l y r u n t h e p r o c e s s .
try process . run ( )
let outputTask = Task {
do
{
let logger = Logger ( subsystem : Bundle . main . bundleIdentifier ! , category : toolURL . lastPathComponent )
// A u t o m a t i c a l l y c a n c e l s w h e n f i l e H a n d l e c l o s e s .
for try await line in outputPipe . fileHandleForReading . bytes . lines
{
process . output += line + " \n "
process . outputPublisher . send ( line )
logger . notice ( " \( line , privacy : . public ) " )
}
try Task . checkCancellation ( )
process . outputPublisher . send ( completion : . finished )
}
catch let error as CancellationError
{
process . outputPublisher . send ( completion : . failure ( error ) )
}
catch
{
Logger . main . error ( " Failed to read process output. \( error . localizedDescription , privacy : . public ) " )
try Task . checkCancellation ( )
process . outputPublisher . send ( completion : . failure ( error ) )
}
}
process . terminationHandler = { process in
Logger . main . notice ( " Process \( toolURL , privacy : . public ) terminated with exit code \( process . terminationStatus ) . " )
outputTask . cancel ( )
process . outputPublisher . send ( completion : . finished )
}
return process
}
class func launchAndWait ( _ toolURL : URL , arguments : [ String ] = [ ] , environment : [ String : String ] = ProcessInfo . processInfo . environment ) async throws -> String
{
let process = try self . launch ( toolURL , arguments : arguments , environment : environment )
await withCheckedContinuation { ( continuation : CheckedContinuation < Void , Never > ) in
let previousHandler = process . terminationHandler
process . terminationHandler = { process in
previousHandler ? ( process )
continuation . resume ( )
}
}
guard process . terminationStatus = = 0 else {
throw ProcessError . failed ( executableURL : toolURL , exitCode : process . terminationStatus , output : process . output )
}
return process . output
}
}
@ available ( macOS 12 , * )
extension Process
{
private static var outputKey : Int = 0
private static var publisherKey : Int = 0
fileprivate ( set ) var output : String {
get {
let output = objc_getAssociatedObject ( self , & Process . outputKey ) as ? String ? ? " "
return output
}
set {
objc_setAssociatedObject ( self , & Process . outputKey , newValue , . OBJC_ASSOCIATION_COPY_NONATOMIC )
}
}
// S h o u l d b e t y p e - e r a s e d , b u t o h w e l l .
var outputLines : AsyncThrowingPublisher < some Publisher < String , Error > > {
return self . outputPublisher
. buffer ( size : 100 , prefetch : . byRequest , whenFull : . dropOldest )
. values
}
private var outputPublisher : PassthroughSubject < String , Error > {
if let publisher = objc_getAssociatedObject ( self , & Process . publisherKey ) as ? PassthroughSubject < String , Error >
{
return publisher
}
let publisher = PassthroughSubject < String , Error > ( )
objc_setAssociatedObject ( self , & Process . publisherKey , publisher , . OBJC_ASSOCIATION_RETAIN_NONATOMIC )
return publisher
}
// W e m u s t m a n u a l l y c l o s e o u t p u t P i p e i n o r d e r f o r u s t o r e a d a s e c o n d P r o c e s s ' s t a n d a r d O u t p u t v i a a s y n c - a w a i t 🤷 ♂ ️
func stopOutput ( )
{
guard let outputPipe = self . standardOutput as ? Pipe else { return }
do
{
try outputPipe . fileHandleForReading . close ( )
}
catch
{
Logger . main . error ( " Failed to close \( self . executableURL ? . lastPathComponent ? ? " process " , privacy : . public ) 's standardOutput. \( error . localizedDescription , privacy : . public ) " )
}
}
}
2026-02-22 21:27:31 +05:30
#endif