1mod initial_kv_setter;
2mod launch_metadata;
3mod max_instance_memory;
4mod sqlite_statements;
5mod stdio;
6mod summary;
7mod variable;
8
9use std::path::PathBuf;
10use std::{future::Future, sync::Arc};
11
12use anyhow::{Context, Result};
13#[cfg(feature = "experimental-wasm-features")]
14use clap::ValueEnum;
15use clap::{Args, CommandFactory, Parser};
16use spin_app::App;
17use spin_common::sloth;
18use spin_common::ui::quoted_path;
19use spin_common::url::parse_file_url;
20use spin_factors::RuntimeFactors;
21use spin_factors_executor::{ComponentLoader, FactorsExecutor};
22
23use crate::{Trigger, TriggerApp, loader::ComponentLoader as ComponentLoaderImpl};
24pub use initial_kv_setter::InitialKvSetterHook;
25pub use launch_metadata::LaunchMetadata;
26pub use max_instance_memory::MaxInstanceMemoryHook;
27pub use sqlite_statements::SqlStatementExecutorHook;
28use stdio::FollowComponents;
29pub use stdio::StdioLoggingExecutorHooks;
30pub use summary::{KeyValueDefaultStoreSummaryHook, SqliteDefaultStoreSummaryHook};
31pub use variable::VariablesValidatorHook;
32
33pub const APP_LOG_DIR: &str = "APP_LOG_DIR";
34pub const SPIN_TRUNCATE_LOGS: &str = "SPIN_TRUNCATE_LOGS";
35pub const DISABLE_WASMTIME_CACHE: &str = "DISABLE_WASMTIME_CACHE";
36pub const FOLLOW_LOG_OPT: &str = "FOLLOW_ID";
37pub const WASMTIME_CACHE_FILE: &str = "WASMTIME_CACHE_FILE";
38pub const RUNTIME_CONFIG_FILE: &str = "RUNTIME_CONFIG_FILE";
39
40pub const SPIN_LOCKED_URL: &str = "SPIN_LOCKED_URL";
42pub const SPIN_LOCAL_APP_DIR: &str = "SPIN_LOCAL_APP_DIR";
43pub const SPIN_WORKING_DIR: &str = "SPIN_WORKING_DIR";
44
45#[derive(Parser, Debug)]
47#[clap(
48 styles = spin_common::cli::CLAP_STYLES,
49 override_usage = "spin [COMMAND] [OPTIONS]",
50 next_help_heading = help_heading::<T, B::Factors>()
51)]
52pub struct FactorsTriggerCommand<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> {
53 #[clap(
56 name = APP_LOG_DIR,
57 short = 'L',
58 long = "log-dir",
59 env = "SPIN_LOG_DIR",
60 )]
61 pub log: Option<PathBuf>,
62
63 #[clap(
65 name = SPIN_TRUNCATE_LOGS,
66 long = "truncate-logs",
67 )]
68 pub truncate_logs: bool,
69
70 #[clap(
72 name = DISABLE_WASMTIME_CACHE,
73 long = "disable-cache",
74 env = DISABLE_WASMTIME_CACHE,
75 conflicts_with = WASMTIME_CACHE_FILE,
76 value_parser = clap::builder::BoolishValueParser::new(),
77 )]
78 pub disable_cache: bool,
79
80 #[clap(
82 name = WASMTIME_CACHE_FILE,
83 long = "cache",
84 env = WASMTIME_CACHE_FILE,
85 conflicts_with = DISABLE_WASMTIME_CACHE,
86 )]
87 pub cache: Option<PathBuf>,
88
89 #[clap(long = "disable-pooling")]
91 pub disable_pooling: bool,
92
93 #[clap(long = "debug-info")]
96 pub debug_info: bool,
97
98 #[clap(
100 name = FOLLOW_LOG_OPT,
101 long = "follow",
102 )]
103 pub follow_components: Vec<String>,
104
105 #[clap(
107 long = "quiet",
108 short = 'q',
109 aliases = &["sh", "shush"],
110 conflicts_with = FOLLOW_LOG_OPT,
111 )]
112 pub silence_component_logs: bool,
113
114 #[clap(
116 name = RUNTIME_CONFIG_FILE,
117 long = "runtime-config-file",
118 env = RUNTIME_CONFIG_FILE,
119 )]
120 pub runtime_config_file: Option<PathBuf>,
121
122 #[cfg(feature = "experimental-wasm-features")]
123 #[clap(long, value_enum)]
124 pub experimental_wasm_feature: Vec<ExperimentalWasmFeature>,
125
126 #[clap(long)]
133 pub state_dir: Option<String>,
134
135 #[clap(flatten)]
136 pub trigger_args: T::CliArgs,
137
138 #[clap(flatten)]
139 pub builder_args: B::CliArgs,
140
141 #[clap(long = "help-args-only", hide = true)]
142 pub help_args_only: bool,
143
144 #[clap(long = "launch-metadata-only", hide = true)]
145 pub launch_metadata_only: bool,
146
147 #[clap(long = "precompose-only", hide = true)]
148 pub precompose_only: bool,
149 #[clap(long = "precompose-component-id", hide = true)]
150 pub precompose_component_id: Option<String>,
151}
152
153#[cfg(feature = "experimental-wasm-features")]
154#[derive(Clone, Debug, ValueEnum)]
155pub enum ExperimentalWasmFeature {
156 ReferenceTypes,
157 FunctionReferences,
158}
159
160#[derive(Debug, Default)]
162pub struct FactorsConfig {
163 pub working_dir: PathBuf,
165 pub runtime_config_file: Option<PathBuf>,
167 pub state_dir: UserProvidedPath,
169 pub local_app_dir: Option<String>,
171 pub follow_components: FollowComponents,
173 pub log_dir: UserProvidedPath,
175 pub truncate_logs: bool,
177}
178
179#[derive(Args)]
182pub struct NoCliArgs;
183
184impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> FactorsTriggerCommand<T, B> {
185 pub async fn run(self) -> Result<()> {
187 spin_tls::install_default_crypto_provider();
188 if self.help_args_only {
190 Self::command()
191 .disable_help_flag(true)
192 .help_template("{all-args}")
193 .print_long_help()?;
194 return Ok(());
195 }
196
197 if self.launch_metadata_only {
199 let lm = LaunchMetadata::infer::<T, B>();
200 let json = serde_json::to_string_pretty(&lm)?;
201 eprintln!("{json}");
202 return Ok(());
203 }
204
205 let working_dir = std::env::var(SPIN_WORKING_DIR).context(SPIN_WORKING_DIR)?;
207 let locked_url = std::env::var(SPIN_LOCKED_URL).context(SPIN_LOCKED_URL)?;
208 let local_app_dir = std::env::var(SPIN_LOCAL_APP_DIR).ok();
209
210 let follow_components = self.follow_components();
211
212 let app = {
214 let path = parse_file_url(&locked_url)?;
215 let contents = std::fs::read(&path)
216 .with_context(|| format!("failed to read manifest at {}", quoted_path(&path)))?;
217 let locked =
218 serde_json::from_slice(&contents).context("failed to parse app lock file JSON")?;
219 App::new(locked_url, locked)
220 };
221
222 if self.precompose_only {
224 let Some(precompose_component_id) = self.precompose_component_id.as_ref() else {
225 anyhow::bail!("got --precompose-only but no --precompose-component-id");
226 };
227
228 let Some(component) = app.get_component(precompose_component_id) else {
229 anyhow::bail!("--precompose-component-id: component does not exist");
230 };
231
232 let loader = crate::loader::ComponentLoader::new();
233 let composed = loader
234 .load_composed(&component, &T::trigger_dependencies_composer())
235 .await
236 .with_context(|| {
237 format!("failed to precompose component {precompose_component_id}")
238 })?;
239
240 use std::io::Write;
241 std::io::stdout()
242 .write_all(&composed)
243 .context("failed to write composition to stdout")?;
244 return Ok(());
245 }
246
247 if let Err(unmet) = app.ensure_needs_only(T::TYPE, &T::supported_host_requirements()) {
249 anyhow::bail!(
250 "This application requires the following features that are not available in this version of the '{}' trigger: {unmet}",
251 T::TYPE
252 );
253 }
254
255 let trigger = T::new(self.trigger_args, &app)?;
256
257 let mut builder: TriggerAppBuilder<T, B> = TriggerAppBuilder::new(trigger);
258 let config = builder.engine_config();
259
260 if !self.disable_cache {
262 config.enable_cache(&self.cache)?;
263 }
264
265 if self.disable_pooling {
266 config.disable_pooling();
267 }
268
269 if self.debug_info {
270 config.enable_debug_info();
271 }
272
273 #[cfg(feature = "experimental-wasm-features")]
274 {
275 let wasmtime_config = config.wasmtime_config();
276 for wasm_feature in self.experimental_wasm_feature {
277 match wasm_feature {
278 ExperimentalWasmFeature::ReferenceTypes => {
279 wasmtime_config.wasm_reference_types(true)
280 }
281 ExperimentalWasmFeature::FunctionReferences => {
282 wasmtime_config.wasm_function_references(true)
283 }
284 };
285 }
286 }
287
288 let state_dir = match &self.state_dir {
289 Some(s) if s.is_empty() => UserProvidedPath::Unset,
291 Some(s) => UserProvidedPath::Provided(PathBuf::from(s)),
292 None => UserProvidedPath::Default,
293 };
294 let log_dir = match &self.log {
295 Some(p) if p.as_os_str().is_empty() => UserProvidedPath::Unset,
297 Some(p) => UserProvidedPath::Provided(p.clone()),
298 None => UserProvidedPath::Default,
299 };
300 let common_options = FactorsConfig {
301 working_dir: PathBuf::from(working_dir),
302 runtime_config_file: self.runtime_config_file.clone(),
303 state_dir,
304 local_app_dir: local_app_dir.clone(),
305 follow_components,
306 log_dir,
307 truncate_logs: self.truncate_logs,
308 };
309
310 let loader = ComponentLoaderImpl::new();
311 let run_fut = builder
312 .run(app, common_options, self.builder_args, &loader)
313 .await?;
314
315 let (abortable, abort_handle) = futures::future::abortable(run_fut);
316 ctrlc::set_handler(move || abort_handle.abort())?;
317 match abortable.await {
318 Ok(Ok(())) => {
319 tracing::info!("Trigger executor shut down: exiting");
320 Ok(())
321 }
322 Ok(Err(err)) => {
323 tracing::error!("Trigger executor failed");
324 Err(err)
325 }
326 Err(_aborted) => {
327 tracing::info!("User requested shutdown: exiting");
328 Ok(())
329 }
330 }
331 }
332
333 fn follow_components(&self) -> FollowComponents {
334 if self.silence_component_logs {
335 FollowComponents::None
336 } else if self.follow_components.is_empty() {
337 FollowComponents::All
338 } else {
339 let followed = self.follow_components.clone().into_iter().collect();
340 FollowComponents::Named(followed)
341 }
342 }
343}
344
345const SLOTH_WARNING_DELAY_MILLIS: u64 = 1250;
346
347fn warn_if_wasm_build_slothful() -> sloth::SlothGuard {
348 #[cfg(debug_assertions)]
349 let message = "\
350 This is a debug build - preparing Wasm modules might take a few seconds\n\
351 If you're experiencing long startup times please switch to the release build";
352
353 #[cfg(not(debug_assertions))]
354 let message = "Preparing Wasm modules is taking a few seconds...";
355
356 sloth::warn_if_slothful(SLOTH_WARNING_DELAY_MILLIS, format!("{message}\n"))
357}
358
359fn help_heading<T: Trigger<F>, F: RuntimeFactors>() -> Option<&'static str> {
360 if T::TYPE == <help::HelpArgsOnlyTrigger as Trigger<F>>::TYPE {
361 Some("Trigger Options")
362 } else {
363 let heading = format!("{} Trigger Options", T::display_name());
364 let as_str = Box::new(heading).leak();
365 Some(as_str)
366 }
367}
368
369pub struct TriggerAppBuilder<T, B> {
371 engine_config: spin_core::Config,
372 pub trigger: T,
373 _factors_builder: std::marker::PhantomData<B>,
374}
375
376impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> TriggerAppBuilder<T, B> {
377 pub fn new(trigger: T) -> Self {
378 Self {
379 engine_config: spin_core::Config::default(),
380 trigger,
381 _factors_builder: Default::default(),
382 }
383 }
384
385 pub fn engine_config(&mut self) -> &mut spin_core::Config {
386 &mut self.engine_config
387 }
388
389 pub async fn build(
391 &mut self,
392 app: App,
393 common_options: FactorsConfig,
394 options: B::CliArgs,
395 loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
396 ) -> anyhow::Result<TriggerApp<T, B::Factors>> {
397 let mut core_engine_builder = {
398 self.trigger.update_core_config(&mut self.engine_config)?;
399
400 spin_core::Engine::builder(&self.engine_config)?
401 };
402 self.trigger.add_to_linker(core_engine_builder.linker())?;
403
404 let (factors, runtime_config) = B::build(&common_options, &options)?;
405
406 let mut executor = FactorsExecutor::new(core_engine_builder, factors)?;
407 B::configure_app(&mut executor, &runtime_config, &common_options, &options)?;
408 let executor = Arc::new(executor);
409
410 let configured_app = {
411 let _sloth_guard = warn_if_wasm_build_slothful();
412 executor
413 .load_app(
414 app,
415 runtime_config.into(),
416 loader,
417 Some(T::TYPE),
418 T::trigger_dependencies_composer(),
419 )
420 .await?
421 };
422
423 Ok(configured_app)
424 }
425
426 pub async fn run(
428 mut self,
429 app: App,
430 common_options: FactorsConfig,
431 options: B::CliArgs,
432 loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
433 ) -> anyhow::Result<impl Future<Output = anyhow::Result<()>>> {
434 let configured_app = self.build(app, common_options, options, loader).await?;
435 Ok(self.trigger.run(configured_app))
436 }
437}
438
439pub trait RuntimeFactorsBuilder {
441 type Factors: RuntimeFactors;
443 type CliArgs: clap::Args;
445 type RuntimeConfig: Into<<Self::Factors as RuntimeFactors>::RuntimeConfig>;
447
448 fn build(
450 config: &FactorsConfig,
451 args: &Self::CliArgs,
452 ) -> anyhow::Result<(Self::Factors, Self::RuntimeConfig)>;
453
454 fn configure_app<U: Send + 'static>(
456 executor: &mut FactorsExecutor<Self::Factors, U>,
457 runtime_config: &Self::RuntimeConfig,
458 config: &FactorsConfig,
459 args: &Self::CliArgs,
460 ) -> anyhow::Result<()> {
461 let _ = (executor, runtime_config, config, args);
462 Ok(())
463 }
464}
465
466pub mod help {
467 use super::*;
468
469 pub struct HelpArgsOnlyTrigger;
472
473 impl<F: RuntimeFactors> Trigger<F> for HelpArgsOnlyTrigger {
474 const TYPE: &'static str = "help-args-only";
475 type CliArgs = NoCliArgs;
476 type InstanceState = ();
477
478 fn new(_cli_args: Self::CliArgs, _app: &App) -> anyhow::Result<Self> {
479 Ok(Self)
480 }
481
482 async fn run(self, _configured_app: TriggerApp<Self, F>) -> anyhow::Result<()> {
483 Ok(())
484 }
485 }
486}
487
488#[derive(Clone, Debug, Default)]
490pub enum UserProvidedPath {
491 Provided(PathBuf),
493 #[default]
495 Default,
496 Unset,
498}