@@ -182,54 +182,85 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
182
182
}
183
183
184
184
fileprivate func invoke( compiledExec: AbsolutePath , pluginArguments: [ String ] , toolsVersion: ToolsVersion , writableDirectories: [ AbsolutePath ] , input: Data ) throws -> ( outputJSON: Data , stdoutText: Data ) {
185
- // Construct the command line.
186
-
187
- // FIXME: Need to pass down the arguments and not ignore them.
188
-
189
- // FIXME: It would be more robust to pass it as `stdin` data, but we need TSC support for that. When this is
190
- // changed, PackagePlugin will need to change as well (but no plugins need to change).
191
- var command = [ compiledExec. pathString]
185
+ // Construct the command line. We just pass along any arguments intended for the plugin.
186
+ var command = [ compiledExec. pathString] + pluginArguments
192
187
command += [ String ( decoding: input, as: UTF8 . self) ]
193
188
194
189
// If enabled, run command in a sandbox.
195
190
// This provides some safety against arbitrary code execution when invoking the plugin.
196
191
// We only allow the permissions which are absolutely necessary.
197
192
if self . enableSandbox {
198
- command = Sandbox . apply ( command: command, writableDirectories: writableDirectories)
193
+ command = Sandbox . apply ( command: command, writableDirectories: writableDirectories + [ self . cacheDir] )
194
+ }
195
+
196
+ // Create and configure a Process.
197
+ let process = Process ( )
198
+ process. launchPath = command. first!
199
+ process. arguments = Array ( command. dropFirst ( ) )
200
+ process. environment = ProcessInfo . processInfo. environment
201
+ process. currentDirectoryURL = self . cacheDir. asURL
202
+
203
+ // Create a dispatch group for waiting until the process has terminated and the data has been read.
204
+ let waiters = DispatchGroup ( )
205
+ // Set up for capturing stdout data.
206
+ let stdoutPipe = Pipe ( )
207
+ var stdoutData = Data ( )
208
+ waiters. enter ( )
209
+ stdoutPipe. fileHandleForReading. readabilityHandler = { ( fileHandle: FileHandle ) -> Void in
210
+ let newData = fileHandle. availableData
211
+ if newData. isEmpty {
212
+ fileHandle. readabilityHandler = nil
213
+ waiters. leave ( )
214
+ }
215
+ else {
216
+ stdoutData. append ( contentsOf: newData)
217
+ }
218
+ }
219
+ process. standardOutput = stdoutPipe
220
+
221
+ // Set up for capturing stderr data.
222
+ waiters. enter ( )
223
+ let stderrPipe = Pipe ( )
224
+ var stderrData = Data ( )
225
+ stderrPipe. fileHandleForReading. readabilityHandler = { ( fileHandle: FileHandle ) -> Void in
226
+ let newData = fileHandle. availableData
227
+ if newData. isEmpty {
228
+ fileHandle. readabilityHandler = nil
229
+ waiters. leave ( )
230
+ }
231
+ else {
232
+ stderrData. append ( contentsOf: newData)
233
+ }
234
+ }
235
+ process. standardError = stderrPipe
236
+
237
+ // Set up a termination handler.
238
+ process. terminationHandler = { _ in
239
+ waiters. leave ( )
199
240
}
200
241
201
- // Invoke the plugin script as a subprocess .
202
- let result : ProcessResult
242
+ // Start the process .
243
+ waiters . enter ( )
203
244
do {
204
- result = try Process . popen ( arguments : command )
245
+ try process . run ( )
205
246
} catch {
206
247
throw DefaultPluginScriptRunnerError . subprocessDidNotStart ( " \( error) " , command: command)
207
248
}
208
249
209
- // Collect the output. The `PackagePlugin` runtime library writes the output as a zero byte followed by
210
- // the JSON-serialized PluginEvaluationResult. Since this appears after any free-form output from the
211
- // script, it can be safely split out while maintaining the ability to see debug output without resorting
212
- // to side-channel communication that might be not be very cross-platform (e.g. pipes, file handles, etc).
213
- // We end up with an optional Data for the JSON, and two Datas for stdout and stderr respectively.
214
- var stdoutPieces = ( try ? result. output. get ( ) . split ( separator: 0 , omittingEmptySubsequences: false ) ) ?? [ ]
215
- let jsonData = ( stdoutPieces. count > 1 ) ? Data ( stdoutPieces. removeLast ( ) ) : nil
216
- let stdoutData = Data ( stdoutPieces. joined ( ) )
217
- let stderrData = ( try ? Data ( result. stderrOutput. get ( ) ) ) ?? Data ( )
250
+ // Wait for the process to terminate and the readers to finish collecting all output.
251
+ waiters. wait ( )
218
252
219
- // Throw an error if we the subprocess ended badly.
220
- if result. exitStatus != . terminated( code: 0 ) {
221
- let output = String ( decoding: stdoutData + stderrData, as: UTF8 . self)
222
- throw DefaultPluginScriptRunnerError . subprocessFailed ( " \( result. exitStatus) " , command: command, output: output)
223
- }
253
+
254
+ // Now `stdoutData` contains a JSON-encoded output structure, and `stderrData` contains any free text output from the plugin process.
255
+ let stderrText = String ( decoding: stderrData, as: UTF8 . self)
224
256
225
- // Throw an error if we didn't get the JSON data.
226
- guard let json = jsonData else {
227
- let output = String ( decoding: stdoutData + stderrData, as: UTF8 . self)
228
- throw DefaultPluginScriptRunnerError . missingPluginJSON ( " didn't receive JSON output data " , command: command, output: output)
257
+ // Throw an error if we the subprocess ended badly.
258
+ if !( process. terminationReason == . exit && process. terminationStatus == 0 ) {
259
+ throw DefaultPluginScriptRunnerError . subprocessFailed ( " \( process. terminationStatus) " , command: command, output: stderrText)
229
260
}
230
261
231
262
// Otherwise return the JSON data and any output text.
232
- return ( outputJSON: json , stdoutText: stdoutData + stderrData)
263
+ return ( outputJSON: stdoutData , stdoutText: stderrData)
233
264
}
234
265
}
235
266
0 commit comments