@@ -8,6 +8,7 @@ use std::{
8
8
9
9
use anyhow:: { anyhow, bail, Context , Result } ;
10
10
use clap:: { CommandFactory , Parser } ;
11
+ use itertools:: Itertools ;
11
12
use reqwest:: Url ;
12
13
use spin_app:: locked:: LockedApp ;
13
14
use spin_common:: ui:: quoted_path;
@@ -16,6 +17,8 @@ use spin_oci::OciLoader;
16
17
use spin_trigger:: cli:: { SPIN_LOCAL_APP_DIR , SPIN_LOCKED_URL , SPIN_WORKING_DIR } ;
17
18
use tempfile:: TempDir ;
18
19
20
+ use futures:: StreamExt ;
21
+
19
22
use crate :: opts:: * ;
20
23
21
24
use self :: app_source:: { AppSource , ResolvedAppSource } ;
@@ -128,9 +131,11 @@ impl UpCommand {
128
131
129
132
if app_source == AppSource :: None {
130
133
if self . help {
131
- return self
132
- . run_trigger ( trigger_command ( HELP_ARGS_ONLY_TRIGGER_TYPE ) , None )
133
- . await ;
134
+ let mut child = self
135
+ . start_trigger ( trigger_command ( HELP_ARGS_ONLY_TRIGGER_TYPE ) , None )
136
+ . await ?;
137
+ let _ = child. wait ( ) . await ?;
138
+ return Ok ( ( ) ) ;
134
139
} else {
135
140
bail ! ( "Default file '{DEFAULT_MANIFEST_FILE}' not found. Run `spin up --from <APPLICATION>`, or `spin up --help` for usage." ) ;
136
141
}
@@ -150,28 +155,51 @@ impl UpCommand {
150
155
151
156
let resolved_app_source = self . resolve_app_source ( & app_source, & working_dir) . await ?;
152
157
153
- let trigger_cmd = trigger_command_for_resolved_app_source ( & resolved_app_source)
158
+ let trigger_cmds = trigger_command_for_resolved_app_source ( & resolved_app_source)
154
159
. with_context ( || format ! ( "Couldn't find trigger executor for {app_source}" ) ) ?;
155
160
156
161
if self . help {
157
- return self . run_trigger ( trigger_cmd, None ) . await ;
162
+ for cmd in trigger_cmds {
163
+ let mut help_process = self . start_trigger ( cmd. clone ( ) , None ) . await ?;
164
+ _ = help_process. wait ( ) . await ;
165
+ }
166
+ return Ok ( ( ) ) ;
158
167
}
159
168
160
169
let mut locked_app = self
161
170
. load_resolved_app_source ( resolved_app_source, & working_dir)
162
171
. await ?;
163
172
164
173
self . update_locked_app ( & mut locked_app) ;
174
+ let locked_url = self . write_locked_app ( & locked_app, & working_dir) . await ?;
165
175
166
176
let local_app_dir = app_source. local_app_dir ( ) . map ( Into :: into) ;
167
177
168
178
let run_opts = RunTriggerOpts {
169
- locked_app ,
179
+ locked_url ,
170
180
working_dir,
171
181
local_app_dir,
172
182
} ;
173
183
174
- self . run_trigger ( trigger_cmd, Some ( run_opts) ) . await
184
+ let mut trigger_processes = self . start_trigger_processes ( trigger_cmds, run_opts) . await ?;
185
+
186
+ set_kill_on_ctrl_c ( & trigger_processes) ?;
187
+
188
+ let mut trigger_tasks = trigger_processes
189
+ . iter_mut ( )
190
+ . map ( |ch| ch. wait ( ) )
191
+ . collect :: < futures:: stream:: FuturesUnordered < _ > > ( ) ;
192
+
193
+ let first_to_finish = trigger_tasks. next ( ) . await ;
194
+
195
+ if let Some ( process_result) = first_to_finish {
196
+ let status = process_result?;
197
+ if !status. success ( ) {
198
+ return Err ( crate :: subprocess:: ExitStatusError :: new ( status) . into ( ) ) ;
199
+ }
200
+ }
201
+
202
+ Ok ( ( ) )
175
203
}
176
204
177
205
fn get_canonical_working_dir ( & self ) -> Result < WorkingDirectory , anyhow:: Error > {
@@ -190,57 +218,57 @@ impl UpCommand {
190
218
Ok ( working_dir_holder)
191
219
}
192
220
193
- async fn run_trigger (
221
+ async fn start_trigger_processes (
194
222
self ,
223
+ trigger_cmds : Vec < Vec < String > > ,
224
+ run_opts : RunTriggerOpts ,
225
+ ) -> anyhow:: Result < Vec < tokio:: process:: Child > > {
226
+ let mut trigger_processes = Vec :: with_capacity ( trigger_cmds. len ( ) ) ;
227
+
228
+ for cmd in trigger_cmds {
229
+ let child = self
230
+ . start_trigger ( cmd. clone ( ) , Some ( run_opts. clone ( ) ) )
231
+ . await
232
+ . context ( "Failed to start trigger process" ) ?;
233
+ trigger_processes. push ( child) ;
234
+ }
235
+
236
+ Ok ( trigger_processes)
237
+ }
238
+
239
+ async fn start_trigger (
240
+ & self ,
195
241
trigger_cmd : Vec < String > ,
196
242
opts : Option < RunTriggerOpts > ,
197
- ) -> Result < ( ) , anyhow:: Error > {
243
+ ) -> Result < tokio :: process :: Child , anyhow:: Error > {
198
244
// The docs for `current_exe` warn that this may be insecure because it could be executed
199
245
// via hard-link. I think it should be fine as long as we aren't `setuid`ing this binary.
200
- let mut cmd = std :: process:: Command :: new ( std:: env:: current_exe ( ) . unwrap ( ) ) ;
246
+ let mut cmd = tokio :: process:: Command :: new ( std:: env:: current_exe ( ) . unwrap ( ) ) ;
201
247
cmd. args ( & trigger_cmd) ;
202
248
203
249
if let Some ( RunTriggerOpts {
204
- locked_app ,
250
+ locked_url ,
205
251
working_dir,
206
252
local_app_dir,
207
253
} ) = opts
208
254
{
209
- let locked_url = self . write_locked_app ( & locked_app, & working_dir) . await ?;
210
-
211
255
cmd. env ( SPIN_LOCKED_URL , locked_url)
212
256
. env ( SPIN_WORKING_DIR , & working_dir)
213
257
. args ( & self . trigger_args ) ;
214
258
215
259
if let Some ( local_app_dir) = local_app_dir {
216
260
cmd. env ( SPIN_LOCAL_APP_DIR , local_app_dir) ;
217
261
}
262
+
263
+ cmd. kill_on_drop ( true ) ;
218
264
} else {
219
265
cmd. arg ( "--help-args-only" ) ;
220
266
}
221
267
222
268
tracing:: trace!( "Running trigger executor: {:?}" , cmd) ;
223
269
224
- let mut child = cmd. spawn ( ) . context ( "Failed to execute trigger" ) ?;
225
-
226
- // Terminate trigger executor if `spin up` itself receives a termination signal
227
- #[ cfg( not( windows) ) ]
228
- {
229
- // https://github.com/nix-rust/nix/issues/656
230
- let pid = nix:: unistd:: Pid :: from_raw ( child. id ( ) as i32 ) ;
231
- ctrlc:: set_handler ( move || {
232
- if let Err ( err) = nix:: sys:: signal:: kill ( pid, nix:: sys:: signal:: SIGTERM ) {
233
- tracing:: warn!( "Failed to kill trigger handler process: {:?}" , err)
234
- }
235
- } ) ?;
236
- }
237
-
238
- let status = child. wait ( ) ?;
239
- if status. success ( ) {
240
- Ok ( ( ) )
241
- } else {
242
- Err ( crate :: subprocess:: ExitStatusError :: new ( status) . into ( ) )
243
- }
270
+ let child = cmd. spawn ( ) . context ( "Failed to execute trigger" ) ?;
271
+ Ok ( child)
244
272
}
245
273
246
274
fn app_source ( & self ) -> AppSource {
@@ -358,8 +386,31 @@ impl UpCommand {
358
386
}
359
387
}
360
388
389
+ #[ cfg( windows) ]
390
+ fn set_kill_on_ctrl_c ( trigger_processes : & Vec < tokio:: process:: Child > ) -> Result < ( ) , anyhow:: Error > {
391
+ Ok ( ( ) )
392
+ }
393
+
394
+ #[ cfg( not( windows) ) ]
395
+ fn set_kill_on_ctrl_c ( trigger_processes : & [ tokio:: process:: Child ] ) -> Result < ( ) , anyhow:: Error > {
396
+ // https://github.com/nix-rust/nix/issues/656
397
+ let pids = trigger_processes
398
+ . iter ( )
399
+ . flat_map ( |child| child. id ( ) . map ( |id| nix:: unistd:: Pid :: from_raw ( id as i32 ) ) )
400
+ . collect_vec ( ) ;
401
+ ctrlc:: set_handler ( move || {
402
+ for pid in & pids {
403
+ if let Err ( err) = nix:: sys:: signal:: kill ( * pid, nix:: sys:: signal:: SIGTERM ) {
404
+ tracing:: warn!( "Failed to kill trigger handler process: {:?}" , err)
405
+ }
406
+ }
407
+ } ) ?;
408
+ Ok ( ( ) )
409
+ }
410
+
411
+ #[ derive( Clone ) ]
361
412
struct RunTriggerOpts {
362
- locked_app : LockedApp ,
413
+ locked_url : String ,
363
414
working_dir : PathBuf ,
364
415
local_app_dir : Option < PathBuf > ,
365
416
}
@@ -424,16 +475,20 @@ fn trigger_command(trigger_type: &str) -> Vec<String> {
424
475
vec ! [ "trigger" . to_owned( ) , trigger_type. to_owned( ) ]
425
476
}
426
477
427
- fn trigger_command_for_resolved_app_source ( resolved : & ResolvedAppSource ) -> Result < Vec < String > > {
428
- let trigger_type = resolved. trigger_type ( ) ?;
429
-
430
- match trigger_type {
431
- "http" | "redis" => Ok ( trigger_command ( trigger_type) ) ,
432
- _ => {
433
- let cmd = resolve_trigger_plugin ( trigger_type) ?;
434
- Ok ( vec ! [ cmd] )
435
- }
436
- }
478
+ fn trigger_command_for_resolved_app_source (
479
+ resolved : & ResolvedAppSource ,
480
+ ) -> Result < Vec < Vec < String > > > {
481
+ let trigger_type = resolved. trigger_types ( ) ?;
482
+ trigger_type
483
+ . iter ( )
484
+ . map ( |& t| match t {
485
+ "http" | "redis" => Ok ( trigger_command ( t) ) ,
486
+ _ => {
487
+ let cmd = resolve_trigger_plugin ( t) ?;
488
+ Ok ( vec ! [ cmd] )
489
+ }
490
+ } )
491
+ . collect ( )
437
492
}
438
493
439
494
#[ cfg( test) ]
0 commit comments