7
7
import Foundation
8
8
import Dispatch
9
9
import Logging
10
+ import TSCBasic
11
+ import Algorithms
10
12
11
13
// MARK: - API
12
14
@@ -33,30 +35,31 @@ import Logging
33
35
to command: SafeString ,
34
36
arguments: [ Argument ] = [ ] ,
35
37
at path: String = " . " ,
36
- process: Process = . init( ) ,
37
38
logger: Logger ? = nil ,
38
39
outputHandle: FileHandle ? = nil ,
39
40
errorHandle: FileHandle ? = nil ,
40
- environment: [ String : String ] ? = nil ,
41
- eofTimeout: DispatchTimeInterval = . milliseconds( 10 )
42
- ) throws -> ( stdout: String , stderr: String ) {
43
- let command = " cd \( path. escapingSpaces) && \( command) \( arguments. map ( \. string) . joined ( separator: " " ) ) "
41
+ environment: [ String : String ] ? = nil
42
+ ) async throws -> ( stdout: String , stderr: String ) {
43
+ let command = " \( command) \( arguments. map ( \. string) . joined ( separator: " " ) ) "
44
44
45
- return try process . launchBash (
45
+ return try await TSCBasic . Process . launchBash (
46
46
with: command,
47
47
logger: logger,
48
48
outputHandle: outputHandle,
49
49
errorHandle: errorHandle,
50
50
environment: environment,
51
- eofTimeout: eofTimeout
51
+ at: path == " . " ? nil :
52
+ ( path == " ~ " ? TSCBasic . localFileSystem. homeDirectory. pathString :
53
+ ( path. starts ( with: " ~/ " ) ? " \( TSCBasic . localFileSystem. homeDirectory. pathString) / \( path. dropFirst ( 2 ) ) " :
54
+ path) )
52
55
)
53
56
}
54
57
55
58
@discardableResult public func shellOutOldVersion(
56
59
to command: SafeString ,
57
60
arguments: [ Argument ] = [ ] ,
58
61
at path: String = " . " ,
59
- process: Process = . init( ) ,
62
+ process: Foundation . Process = . init( ) ,
60
63
outputHandle: FileHandle ? = nil ,
61
64
errorHandle: FileHandle ? = nil ,
62
65
environment: [ String : String ] ? = nil
@@ -92,30 +95,26 @@ import Logging
92
95
@discardableResult public func shellOut(
93
96
to command: ShellOutCommand ,
94
97
at path: String = " . " ,
95
- process: Process = . init( ) ,
96
98
logger: Logger ? = nil ,
97
99
outputHandle: FileHandle ? = nil ,
98
100
errorHandle: FileHandle ? = nil ,
99
- environment: [ String : String ] ? = nil ,
100
- eofTimeout: DispatchTimeInterval = . milliseconds( 10 )
101
- ) throws -> ( stdout: String , stderr: String ) {
102
- try shellOut (
101
+ environment: [ String : String ] ? = nil
102
+ ) async throws -> ( stdout: String , stderr: String ) {
103
+ try await shellOut (
103
104
to: command. command,
104
105
arguments: command. arguments,
105
106
at: path,
106
- process: process,
107
107
logger: logger,
108
108
outputHandle: outputHandle,
109
109
errorHandle: errorHandle,
110
- environment: environment,
111
- eofTimeout: eofTimeout
110
+ environment: environment
112
111
)
113
112
}
114
113
115
114
@discardableResult public func shellOutOldVersion(
116
115
to command: ShellOutCommand ,
117
116
at path: String = " . " ,
118
- process: Process = . init( ) ,
117
+ process: Foundation . Process = . init( ) ,
119
118
outputHandle: FileHandle ? = nil ,
120
119
errorHandle: FileHandle ? = nil ,
121
120
environment: [ String : String ] ? = nil
@@ -437,91 +436,53 @@ extension ShellOutCommand {
437
436
438
437
// MARK: - Private
439
438
440
- private extension Process {
441
- @discardableResult func launchBash(
439
+ private extension TSCBasic . Process {
440
+ @discardableResult static func launchBash(
442
441
with command: String ,
443
442
logger: Logger ? = nil ,
444
443
outputHandle: FileHandle ? = nil ,
445
444
errorHandle: FileHandle ? = nil ,
446
445
environment: [ String : String ] ? = nil ,
447
- eofTimeout: DispatchTimeInterval = . milliseconds( 10 )
448
- ) throws -> ( stdout: String , stderr: String ) {
449
- self . executableURL = URL ( fileURLWithPath: " /bin/bash " )
450
- self . arguments = [ " -c " , command]
451
-
452
- if let environment {
453
- self . environment = environment
454
- }
455
-
456
- let outputPipe = Pipe ( ) , errorPipe = Pipe ( )
457
- self . standardOutput = outputPipe
458
- self . standardError = errorPipe
459
-
460
- // Because FileHandle's readabilityHandler might be called from a
461
- // different queue from the calling queue, avoid data races by
462
- // protecting reads and writes to outputData and errorData on
463
- // a single dispatch queue.
464
- let outputQueue = DispatchQueue ( label: " bash-output-queue " )
465
- let outputGroup = DispatchGroup ( )
466
- var outputData = Data ( ) , errorData = Data ( )
467
-
468
- outputGroup. enter ( )
469
- outputPipe. fileHandleForReading. readabilityHandler = { handler in
470
- let data = handler. availableData
471
-
472
- if data. isEmpty { // EOF
473
- handler. readabilityHandler = nil
474
- outputGroup. leave ( )
475
- } else {
476
- outputQueue. async {
477
- outputData. append ( data)
478
- outputHandle? . write ( data)
479
- }
446
+ at: String ? = nil
447
+ ) async throws -> ( stdout: String , stderr: String ) {
448
+ let process = try Self . init (
449
+ arguments: [ " /bin/bash " , " -c " , command] ,
450
+ environment: environment ?? ProcessEnv . vars,
451
+ workingDirectory: at. map { try . init( validating: $0) } ?? TSCBasic . localFileSystem. currentWorkingDirectory ?? . root,
452
+ outputRedirection: . collect( redirectStderr: false ) ,
453
+ startNewProcessGroup: false ,
454
+ loggingHandler: nil
455
+ )
456
+
457
+ try process. launch ( )
458
+
459
+ let result = try await process. waitUntilExit ( )
460
+
461
+ try outputHandle? . write ( contentsOf: ( try ? result. output. get ( ) ) ?? [ ] )
462
+ try outputHandle? . close ( )
463
+ try errorHandle? . write ( contentsOf: ( try ? result. stderrOutput. get ( ) ) ?? [ ] )
464
+ try errorHandle? . close ( )
465
+
466
+ guard case . terminated( code: let code) = result. exitStatus, code == 0 else {
467
+ let code : Int32
468
+ switch result. exitStatus {
469
+ case . terminated( code: let termCode) : code = termCode
470
+ case . signalled( signal: let sigNo) : code = - sigNo
480
471
}
472
+ throw ShellOutError (
473
+ terminationStatus: code,
474
+ errorData: Data ( ( try ? result. stderrOutput. get ( ) ) ?? [ ] ) ,
475
+ outputData: Data ( ( try ? result. output. get ( ) ) ?? [ ] )
476
+ )
481
477
}
482
-
483
- outputGroup. enter ( )
484
- errorPipe. fileHandleForReading. readabilityHandler = { handler in
485
- let data = handler. availableData
486
-
487
- if data. isEmpty { // EOF
488
- handler. readabilityHandler = nil
489
- outputGroup. leave ( )
490
- } else {
491
- outputQueue. async {
492
- errorData. append ( data)
493
- errorHandle? . write ( data)
494
- }
495
- }
496
- }
497
-
498
- try self . run ( )
499
- self . waitUntilExit ( )
500
-
501
- if outputGroup. wait ( timeout: . now( ) + eofTimeout) == . timedOut {
502
- logger? . debug ( " ShellOut.launchBash: Timed out waiting for EOF! (command: \( command) ) " )
503
- }
504
-
505
- // We know as of this point that either all blocks have been submitted to the
506
- // queue already, or we've reached our wait timeout.
507
- return try outputQueue. sync {
508
- // Do not try to readToEnd() here; if we already got an EOF, there's definitely
509
- // nothing to read, and if we timed out, trying to read here will just block
510
- // even longer.
511
- try outputHandle? . close ( )
512
- try errorHandle? . close ( )
513
-
514
- guard self . terminationStatus == 0 , self . terminationReason == . exit else {
515
- throw ShellOutError (
516
- terminationStatus: terminationStatus,
517
- errorData: errorData,
518
- outputData: outputData
519
- )
520
- }
521
- return ( stdout: outputData. shellOutput ( ) , stderr: errorData. shellOutput ( ) )
522
- }
478
+ return try (
479
+ stdout: String ( result. utf8Output ( ) . trimmingSuffix ( while: \. isNewline) ) ,
480
+ stderr: String ( result. utf8stderrOutput ( ) . trimmingSuffix ( while: \. isNewline) )
481
+ )
523
482
}
483
+ }
524
484
485
+ extension Foundation . Process {
525
486
@discardableResult func launchBashOldVersion( with command: String , outputHandle: FileHandle ? = nil , errorHandle: FileHandle ? = nil , environment: [ String : String ] ? = nil ) throws -> ( stdout: String , stderr: String ) {
526
487
#if os(Linux)
527
488
executableURL = URL ( fileURLWithPath: " /bin/bash " )
0 commit comments