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 )]
77 pub disable_cache: bool,
78
79 #[clap(
81 name = WASMTIME_CACHE_FILE,
82 long = "cache",
83 env = WASMTIME_CACHE_FILE,
84 conflicts_with = DISABLE_WASMTIME_CACHE,
85 )]
86 pub cache: Option<PathBuf>,
87
88 #[clap(long = "disable-pooling")]
90 pub disable_pooling: bool,
91
92 #[clap(long = "debug-info")]
95 pub debug_info: bool,
96
97 #[clap(
99 name = FOLLOW_LOG_OPT,
100 long = "follow",
101 )]
102 pub follow_components: Vec<String>,
103
104 #[clap(
106 long = "quiet",
107 short = 'q',
108 aliases = &["sh", "shush"],
109 conflicts_with = FOLLOW_LOG_OPT,
110 )]
111 pub silence_component_logs: bool,
112
113 #[clap(
115 name = RUNTIME_CONFIG_FILE,
116 long = "runtime-config-file",
117 env = RUNTIME_CONFIG_FILE,
118 )]
119 pub runtime_config_file: Option<PathBuf>,
120
121 #[cfg(feature = "experimental-wasm-features")]
122 #[clap(long, value_enum)]
123 pub experimental_wasm_feature: Vec<ExperimentalWasmFeature>,
124
125 #[clap(long)]
132 pub state_dir: Option<String>,
133
134 #[clap(flatten)]
135 pub trigger_args: T::CliArgs,
136
137 #[clap(flatten)]
138 pub builder_args: B::CliArgs,
139
140 #[clap(long = "help-args-only", hide = true)]
141 pub help_args_only: bool,
142
143 #[clap(long = "launch-metadata-only", hide = true)]
144 pub launch_metadata_only: bool,
145}
146
147#[cfg(feature = "experimental-wasm-features")]
148#[derive(Clone, Debug, ValueEnum)]
149pub enum ExperimentalWasmFeature {
150 Gc,
151 ReferenceTypes,
152 Exceptions,
153 FunctionReferences,
154}
155
156#[derive(Debug, Default)]
158pub struct FactorsConfig {
159 pub working_dir: PathBuf,
161 pub runtime_config_file: Option<PathBuf>,
163 pub state_dir: UserProvidedPath,
165 pub local_app_dir: Option<String>,
167 pub follow_components: FollowComponents,
169 pub log_dir: UserProvidedPath,
171 pub truncate_logs: bool,
173}
174
175#[derive(Args)]
178pub struct NoCliArgs;
179
180impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> FactorsTriggerCommand<T, B> {
181 pub async fn run(self) -> Result<()> {
183 if self.help_args_only {
185 Self::command()
186 .disable_help_flag(true)
187 .help_template("{all-args}")
188 .print_long_help()?;
189 return Ok(());
190 }
191
192 if self.launch_metadata_only {
194 let lm = LaunchMetadata::infer::<T, B>();
195 let json = serde_json::to_string_pretty(&lm)?;
196 eprintln!("{json}");
197 return Ok(());
198 }
199
200 let working_dir = std::env::var(SPIN_WORKING_DIR).context(SPIN_WORKING_DIR)?;
202 let locked_url = std::env::var(SPIN_LOCKED_URL).context(SPIN_LOCKED_URL)?;
203 let local_app_dir = std::env::var(SPIN_LOCAL_APP_DIR).ok();
204
205 let follow_components = self.follow_components();
206
207 let app = {
209 let path = parse_file_url(&locked_url)?;
210 let contents = std::fs::read(&path)
211 .with_context(|| format!("failed to read manifest at {}", quoted_path(&path)))?;
212 let locked =
213 serde_json::from_slice(&contents).context("failed to parse app lock file JSON")?;
214 App::new(locked_url, locked)
215 };
216
217 if let Err(unmet) = app.ensure_needs_only(T::TYPE, &T::supported_host_requirements()) {
219 anyhow::bail!(
220 "This application requires the following features that are not available in this version of the '{}' trigger: {unmet}",
221 T::TYPE
222 );
223 }
224
225 let trigger = T::new(self.trigger_args, &app)?;
226 let mut builder: TriggerAppBuilder<T, B> = TriggerAppBuilder::new(trigger);
227 let config = builder.engine_config();
228
229 if !self.disable_cache {
231 config.enable_cache(&self.cache)?;
232 }
233
234 if self.disable_pooling {
235 config.disable_pooling();
236 }
237
238 if self.debug_info {
239 config.enable_debug_info();
240 }
241
242 #[cfg(feature = "experimental-wasm-features")]
243 {
244 let wasmtime_config = config.wasmtime_config();
245 for wasm_feature in self.experimental_wasm_feature {
246 match wasm_feature {
247 ExperimentalWasmFeature::Gc => wasmtime_config.wasm_gc(true),
248 ExperimentalWasmFeature::ReferenceTypes => {
249 wasmtime_config.wasm_reference_types(true)
250 }
251 ExperimentalWasmFeature::Exceptions => wasmtime_config.wasm_exceptions(true),
252 ExperimentalWasmFeature::FunctionReferences => {
253 wasmtime_config.wasm_function_references(true)
254 }
255 };
256 }
257 }
258
259 let state_dir = match &self.state_dir {
260 Some(s) if s.is_empty() => UserProvidedPath::Unset,
262 Some(s) => UserProvidedPath::Provided(PathBuf::from(s)),
263 None => UserProvidedPath::Default,
264 };
265 let log_dir = match &self.log {
266 Some(p) if p.as_os_str().is_empty() => UserProvidedPath::Unset,
268 Some(p) => UserProvidedPath::Provided(p.clone()),
269 None => UserProvidedPath::Default,
270 };
271 let common_options = FactorsConfig {
272 working_dir: PathBuf::from(working_dir),
273 runtime_config_file: self.runtime_config_file.clone(),
274 state_dir,
275 local_app_dir: local_app_dir.clone(),
276 follow_components,
277 log_dir,
278 truncate_logs: self.truncate_logs,
279 };
280
281 let loader = ComponentLoaderImpl::new();
282 let run_fut = builder
283 .run(app, common_options, self.builder_args, &loader)
284 .await?;
285
286 let (abortable, abort_handle) = futures::future::abortable(run_fut);
287 ctrlc::set_handler(move || abort_handle.abort())?;
288 match abortable.await {
289 Ok(Ok(())) => {
290 tracing::info!("Trigger executor shut down: exiting");
291 Ok(())
292 }
293 Ok(Err(err)) => {
294 tracing::error!("Trigger executor failed");
295 Err(err)
296 }
297 Err(_aborted) => {
298 tracing::info!("User requested shutdown: exiting");
299 Ok(())
300 }
301 }
302 }
303
304 fn follow_components(&self) -> FollowComponents {
305 if self.silence_component_logs {
306 FollowComponents::None
307 } else if self.follow_components.is_empty() {
308 FollowComponents::All
309 } else {
310 let followed = self.follow_components.clone().into_iter().collect();
311 FollowComponents::Named(followed)
312 }
313 }
314}
315
316const SLOTH_WARNING_DELAY_MILLIS: u64 = 1250;
317
318fn warn_if_wasm_build_slothful() -> sloth::SlothGuard {
319 #[cfg(debug_assertions)]
320 let message = "\
321 This is a debug build - preparing Wasm modules might take a few seconds\n\
322 If you're experiencing long startup times please switch to the release build";
323
324 #[cfg(not(debug_assertions))]
325 let message = "Preparing Wasm modules is taking a few seconds...";
326
327 sloth::warn_if_slothful(SLOTH_WARNING_DELAY_MILLIS, format!("{message}\n"))
328}
329
330fn help_heading<T: Trigger<F>, F: RuntimeFactors>() -> Option<&'static str> {
331 if T::TYPE == <help::HelpArgsOnlyTrigger as Trigger<F>>::TYPE {
332 Some("Trigger Options")
333 } else {
334 let heading = format!("{} Trigger Options", T::display_name());
335 let as_str = Box::new(heading).leak();
336 Some(as_str)
337 }
338}
339
340pub struct TriggerAppBuilder<T, B> {
342 engine_config: spin_core::Config,
343 pub trigger: T,
344 _factors_builder: std::marker::PhantomData<B>,
345}
346
347impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> TriggerAppBuilder<T, B> {
348 pub fn new(trigger: T) -> Self {
349 Self {
350 engine_config: spin_core::Config::default(),
351 trigger,
352 _factors_builder: Default::default(),
353 }
354 }
355
356 pub fn engine_config(&mut self) -> &mut spin_core::Config {
357 &mut self.engine_config
358 }
359
360 pub async fn build(
362 &mut self,
363 app: App,
364 common_options: FactorsConfig,
365 options: B::CliArgs,
366 loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
367 ) -> anyhow::Result<TriggerApp<T, B::Factors>> {
368 let mut core_engine_builder = {
369 self.trigger.update_core_config(&mut self.engine_config)?;
370
371 spin_core::Engine::builder(&self.engine_config)?
372 };
373 self.trigger.add_to_linker(core_engine_builder.linker())?;
374
375 let (factors, runtime_config) = B::build(&common_options, &options)?;
376
377 let mut executor = FactorsExecutor::new(core_engine_builder, factors)?;
378 B::configure_app(&mut executor, &runtime_config, &common_options, &options)?;
379 let executor = Arc::new(executor);
380
381 let configured_app = {
382 let _sloth_guard = warn_if_wasm_build_slothful();
383 executor
384 .load_app(app, runtime_config.into(), loader, Some(T::TYPE))
385 .await?
386 };
387
388 Ok(configured_app)
389 }
390
391 pub async fn run(
393 mut self,
394 app: App,
395 common_options: FactorsConfig,
396 options: B::CliArgs,
397 loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
398 ) -> anyhow::Result<impl Future<Output = anyhow::Result<()>>> {
399 let configured_app = self.build(app, common_options, options, loader).await?;
400 Ok(self.trigger.run(configured_app))
401 }
402}
403
404pub trait RuntimeFactorsBuilder {
406 type Factors: RuntimeFactors;
408 type CliArgs: clap::Args;
410 type RuntimeConfig: Into<<Self::Factors as RuntimeFactors>::RuntimeConfig>;
412
413 fn build(
415 config: &FactorsConfig,
416 args: &Self::CliArgs,
417 ) -> anyhow::Result<(Self::Factors, Self::RuntimeConfig)>;
418
419 fn configure_app<U: Send + 'static>(
421 executor: &mut FactorsExecutor<Self::Factors, U>,
422 runtime_config: &Self::RuntimeConfig,
423 config: &FactorsConfig,
424 args: &Self::CliArgs,
425 ) -> anyhow::Result<()> {
426 let _ = (executor, runtime_config, config, args);
427 Ok(())
428 }
429}
430
431pub mod help {
432 use super::*;
433
434 pub struct HelpArgsOnlyTrigger;
437
438 impl<F: RuntimeFactors> Trigger<F> for HelpArgsOnlyTrigger {
439 const TYPE: &'static str = "help-args-only";
440 type CliArgs = NoCliArgs;
441 type InstanceState = ();
442
443 fn new(_cli_args: Self::CliArgs, _app: &App) -> anyhow::Result<Self> {
444 Ok(Self)
445 }
446
447 async fn run(self, _configured_app: TriggerApp<Self, F>) -> anyhow::Result<()> {
448 Ok(())
449 }
450 }
451}
452
453#[derive(Clone, Debug, Default)]
455pub enum UserProvidedPath {
456 Provided(PathBuf),
458 #[default]
460 Default,
461 Unset,
463}