@@ -11,6 +11,7 @@ private let logger = Logger(
1111
1212public enum JSONRPCSetupError : Error {
1313 case missingStandardIO
14+ case standardIOConnectionError( _ message: String )
1415 case couldNotLocateExecutable( executable: String , error: String ? )
1516}
1617
@@ -24,6 +25,8 @@ extension JSONRPCSetupError: LocalizedError {
2425 return " Missing standard IO "
2526 case . couldNotLocateExecutable( let executable, let error) :
2627 return " Could not locate executable \( executable) \( error ?? " " ) " . trimmingCharacters ( in: . whitespaces)
28+ case . standardIOConnectionError( let message) :
29+ return " Could not connect to stdio: \( message) " . trimmingCharacters ( in: . whitespaces)
2730 }
2831 }
2932
@@ -33,6 +36,8 @@ extension JSONRPCSetupError: LocalizedError {
3336 return " Make sure that the Process that is passed as an argument has stdin, stdout and stderr set as a Pipe. "
3437 case . couldNotLocateExecutable:
3538 return " Check that the executable is findable given the PATH environment variable. If needed, pass the right environment to the process. "
39+ case . standardIOConnectionError:
40+ return nil
3641 }
3742 }
3843}
@@ -55,20 +60,31 @@ extension DataChannel {
5560 }
5661
5762 // Create the process
58- func path( for executable: String ) throws -> String {
63+ func path( for executable: String , env : [ String : String ] ? ) -> String ? {
5964 guard !executable. contains ( " / " ) else {
6065 return executable
6166 }
62- let path = try locate ( executable: executable, env: env)
63- return path. isEmpty ? executable : path
67+ do {
68+ let path = try locate ( executable: executable, env: env)
69+ return path. isEmpty ? nil : path
70+ } catch {
71+ // Most likely an error because we could not locate the executable
72+ return nil
73+ }
6474 }
6575
66- // TODO: look at how to use /bin/zsh, at least on MacOS, to avoid needing to specify PATH to locate the executable
6776 let process = Process ( )
68- process. executableURL = URL ( fileURLWithPath: try path ( for: executable) )
69- process. arguments = args
70- if let env {
71- process. environment = env
77+ // In MacOS, zsh is the default since macOS Catalina 10.15.7. We can safely assume it is available.
78+ process. launchPath = " /bin/zsh "
79+ if let executable = path ( for: executable, env: env) {
80+ let command = " \( executable) \( args. joined ( separator: " " ) ) "
81+ process. arguments = [ " -c " ] + [ command]
82+ process. environment = env ?? ProcessInfo . processInfo. environment
83+ } else {
84+ // If we cannot locate the executable, try loading the default environment for zsh, as the current process might not have the correct PATH.
85+ process. environment = try loadZshEnvironment ( )
86+ let command = " \( executable) \( args. joined ( separator: " " ) ) "
87+ process. arguments = [ " -c " ] + [ command]
7288 }
7389
7490 // Working directory
@@ -179,18 +195,40 @@ extension DataChannel {
179195
180196 /// Finds the full path to the executable using the `which` command.
181197 private static func locate( executable: String , env: [ String : String ] ? = nil ) throws -> String {
182- let stdout = Pipe ( )
183- let stderr = Pipe ( )
184198 let process = Process ( )
185- process. standardOutput = stdout
186- process. standardError = stderr
187199 process. executableURL = URL ( fileURLWithPath: " /usr/bin/which " )
188200 process. arguments = [ executable]
189201
190202 if let env {
191203 process. environment = env
192204 }
193205
206+ guard let executablePath = try getProcessStdout ( process: process) , !executablePath. isEmpty
207+ else {
208+ throw JSONRPCSetupError . couldNotLocateExecutable ( executable: executable, error: " " )
209+ }
210+ return executablePath
211+ }
212+
213+ private static func loadZshEnvironment( ) throws -> [ String : String ] {
214+ let process = Process ( )
215+ process. launchPath = " /bin/zsh "
216+ process. arguments = [ " -c " , " source ~/.zshrc && printenv " ]
217+ let env = try getProcessStdout ( process: process)
218+
219+ if let path = env? . split ( separator: " \n " ) . filter ( { $0. starts ( with: " PATH= " ) } ) . first {
220+ return [ " PATH " : String ( path. dropFirst ( " PATH= " . count) ) ]
221+ } else {
222+ return ProcessInfo . processInfo. environment
223+ }
224+ }
225+
226+ private static func getProcessStdout( process: Process ) throws -> String ? {
227+ let stdout = Pipe ( )
228+ let stderr = Pipe ( )
229+ process. standardOutput = stdout
230+ process. standardError = stderr
231+
194232 let group = DispatchGroup ( )
195233 var stdoutData = Data ( )
196234 var stderrData = Data ( )
@@ -208,20 +246,14 @@ extension DataChannel {
208246 stdoutData = stdout. fileHandleForReading. readDataToEndOfFile ( )
209247 try process. finish ( )
210248 } catch {
211- throw JSONRPCSetupError . couldNotLocateExecutable (
212- executable : executable ,
213- error: String ( data: stderrData, encoding: . utf8) )
249+ throw JSONRPCSetupError
250+ . standardIOConnectionError (
251+ " Error loading environment: \( error) . Stderr: \( String ( data: stderrData, encoding: . utf8) ?? " " ) " )
214252 }
215253
216254 group. wait ( )
217255
218- guard
219- let executablePath = String ( data: stdoutData, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines) ,
220- !executablePath. isEmpty
221- else {
222- throw JSONRPCSetupError . couldNotLocateExecutable ( executable: executable, error: String ( data: stderrData, encoding: . utf8) )
223- }
224- return executablePath
256+ return String ( data: stdoutData, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines)
225257 }
226258
227259}
0 commit comments