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};
13use clap::{Args, IntoApp, Parser};
14use spin_app::App;
15use spin_common::sloth;
16use spin_common::ui::quoted_path;
17use spin_common::url::parse_file_url;
18use spin_factors::RuntimeFactors;
19use spin_factors_executor::{ComponentLoader, FactorsExecutor};
20
21use crate::{loader::ComponentLoader as ComponentLoaderImpl, Trigger, TriggerApp};
22pub use initial_kv_setter::InitialKvSetterHook;
23pub use launch_metadata::LaunchMetadata;
24pub use max_instance_memory::MaxInstanceMemoryHook;
25pub use sqlite_statements::SqlStatementExecutorHook;
26use stdio::FollowComponents;
27pub use stdio::StdioLoggingExecutorHooks;
28pub use summary::{KeyValueDefaultStoreSummaryHook, SqliteDefaultStoreSummaryHook};
29pub use variable::VariablesValidatorHook;
30
31pub const APP_LOG_DIR: &str = "APP_LOG_DIR";
32pub const SPIN_TRUNCATE_LOGS: &str = "SPIN_TRUNCATE_LOGS";
33pub const DISABLE_WASMTIME_CACHE: &str = "DISABLE_WASMTIME_CACHE";
34pub const FOLLOW_LOG_OPT: &str = "FOLLOW_ID";
35pub const WASMTIME_CACHE_FILE: &str = "WASMTIME_CACHE_FILE";
36pub const RUNTIME_CONFIG_FILE: &str = "RUNTIME_CONFIG_FILE";
37
38pub const SPIN_LOCKED_URL: &str = "SPIN_LOCKED_URL";
40pub const SPIN_LOCAL_APP_DIR: &str = "SPIN_LOCAL_APP_DIR";
41pub const SPIN_WORKING_DIR: &str = "SPIN_WORKING_DIR";
42
43#[derive(Parser, Debug)]
45#[clap(
46 override_usage = "spin [COMMAND] [OPTIONS]",
47 next_help_heading = help_heading::<T, B::Factors>()
48)]
49pub struct FactorsTriggerCommand<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> {
50 #[clap(
53 name = APP_LOG_DIR,
54 short = 'L',
55 long = "log-dir",
56 env = "SPIN_LOG_DIR",
57 )]
58 pub log: Option<PathBuf>,
59
60 #[clap(
62 name = SPIN_TRUNCATE_LOGS,
63 long = "truncate-logs",
64 )]
65 pub truncate_logs: bool,
66
67 #[clap(
69 name = DISABLE_WASMTIME_CACHE,
70 long = "disable-cache",
71 env = DISABLE_WASMTIME_CACHE,
72 conflicts_with = WASMTIME_CACHE_FILE,
73 takes_value = false,
74 )]
75 pub disable_cache: bool,
76
77 #[clap(
79 name = WASMTIME_CACHE_FILE,
80 long = "cache",
81 env = WASMTIME_CACHE_FILE,
82 conflicts_with = DISABLE_WASMTIME_CACHE,
83 )]
84 pub cache: Option<PathBuf>,
85
86 #[clap(long = "disable-pooling")]
88 pub disable_pooling: bool,
89
90 #[clap(
92 name = FOLLOW_LOG_OPT,
93 long = "follow",
94 )]
95 pub follow_components: Vec<String>,
96
97 #[clap(
99 long = "quiet",
100 short = 'q',
101 aliases = &["sh", "shush"],
102 conflicts_with = FOLLOW_LOG_OPT,
103 )]
104 pub silence_component_logs: bool,
105
106 #[clap(
108 name = RUNTIME_CONFIG_FILE,
109 long = "runtime-config-file",
110 env = RUNTIME_CONFIG_FILE,
111 )]
112 pub runtime_config_file: Option<PathBuf>,
113
114 #[clap(long)]
121 pub state_dir: Option<String>,
122
123 #[clap(flatten)]
124 pub trigger_args: T::CliArgs,
125
126 #[clap(flatten)]
127 pub builder_args: B::CliArgs,
128
129 #[clap(long = "help-args-only", hide = true)]
130 pub help_args_only: bool,
131
132 #[clap(long = "launch-metadata-only", hide = true)]
133 pub launch_metadata_only: bool,
134}
135
136#[derive(Debug, Default)]
138pub struct FactorsConfig {
139 pub working_dir: PathBuf,
141 pub runtime_config_file: Option<PathBuf>,
143 pub state_dir: UserProvidedPath,
145 pub local_app_dir: Option<String>,
147 pub follow_components: FollowComponents,
149 pub log_dir: UserProvidedPath,
151 pub truncate_logs: bool,
153}
154
155#[derive(Args)]
158pub struct NoCliArgs;
159
160impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> FactorsTriggerCommand<T, B> {
161 pub async fn run(self) -> Result<()> {
163 if self.help_args_only {
165 Self::command()
166 .disable_help_flag(true)
167 .help_template("{all-args}")
168 .print_long_help()?;
169 return Ok(());
170 }
171
172 if self.launch_metadata_only {
174 let lm = LaunchMetadata::infer::<T, B>();
175 let json = serde_json::to_string_pretty(&lm)?;
176 eprintln!("{json}");
177 return Ok(());
178 }
179
180 let working_dir = std::env::var(SPIN_WORKING_DIR).context(SPIN_WORKING_DIR)?;
182 let locked_url = std::env::var(SPIN_LOCKED_URL).context(SPIN_LOCKED_URL)?;
183 let local_app_dir = std::env::var(SPIN_LOCAL_APP_DIR).ok();
184
185 let follow_components = self.follow_components();
186
187 let app = {
189 let path = parse_file_url(&locked_url)?;
190 let contents = std::fs::read(&path)
191 .with_context(|| format!("failed to read manifest at {}", quoted_path(&path)))?;
192 let locked =
193 serde_json::from_slice(&contents).context("failed to parse app lock file JSON")?;
194 App::new(locked_url, locked)
195 };
196
197 if let Err(unmet) = app.ensure_needs_only(T::TYPE, &T::supported_host_requirements()) {
199 anyhow::bail!("This application requires the following features that are not available in this version of the '{}' trigger: {unmet}", T::TYPE);
200 }
201
202 let trigger = T::new(self.trigger_args, &app)?;
203 let mut builder: TriggerAppBuilder<T, B> = TriggerAppBuilder::new(trigger);
204 let config = builder.engine_config();
205
206 if !self.disable_cache {
208 config.enable_cache(&self.cache)?;
209 }
210
211 if self.disable_pooling {
212 config.disable_pooling();
213 }
214
215 let state_dir = match &self.state_dir {
216 Some(s) if s.is_empty() => UserProvidedPath::Unset,
218 Some(s) => UserProvidedPath::Provided(PathBuf::from(s)),
219 None => UserProvidedPath::Default,
220 };
221 let log_dir = match &self.log {
222 Some(p) if p.as_os_str().is_empty() => UserProvidedPath::Unset,
224 Some(p) => UserProvidedPath::Provided(p.clone()),
225 None => UserProvidedPath::Default,
226 };
227 let common_options = FactorsConfig {
228 working_dir: PathBuf::from(working_dir),
229 runtime_config_file: self.runtime_config_file.clone(),
230 state_dir,
231 local_app_dir: local_app_dir.clone(),
232 follow_components,
233 log_dir,
234 truncate_logs: self.truncate_logs,
235 };
236
237 let run_fut = builder
238 .run(
239 app,
240 common_options,
241 self.builder_args,
242 &ComponentLoaderImpl::new(),
243 )
244 .await?;
245
246 let (abortable, abort_handle) = futures::future::abortable(run_fut);
247 ctrlc::set_handler(move || abort_handle.abort())?;
248 match abortable.await {
249 Ok(Ok(())) => {
250 tracing::info!("Trigger executor shut down: exiting");
251 Ok(())
252 }
253 Ok(Err(err)) => {
254 tracing::error!("Trigger executor failed");
255 Err(err)
256 }
257 Err(_aborted) => {
258 tracing::info!("User requested shutdown: exiting");
259 Ok(())
260 }
261 }
262 }
263
264 fn follow_components(&self) -> FollowComponents {
265 if self.silence_component_logs {
266 FollowComponents::None
267 } else if self.follow_components.is_empty() {
268 FollowComponents::All
269 } else {
270 let followed = self.follow_components.clone().into_iter().collect();
271 FollowComponents::Named(followed)
272 }
273 }
274}
275
276const SLOTH_WARNING_DELAY_MILLIS: u64 = 1250;
277
278fn warn_if_wasm_build_slothful() -> sloth::SlothGuard {
279 #[cfg(debug_assertions)]
280 let message = "\
281 This is a debug build - preparing Wasm modules might take a few seconds\n\
282 If you're experiencing long startup times please switch to the release build";
283
284 #[cfg(not(debug_assertions))]
285 let message = "Preparing Wasm modules is taking a few seconds...";
286
287 sloth::warn_if_slothful(SLOTH_WARNING_DELAY_MILLIS, format!("{message}\n"))
288}
289
290fn help_heading<T: Trigger<F>, F: RuntimeFactors>() -> Option<&'static str> {
291 if T::TYPE == <help::HelpArgsOnlyTrigger as Trigger<F>>::TYPE {
292 Some("TRIGGER OPTIONS")
293 } else {
294 let heading = format!("{} TRIGGER OPTIONS", T::TYPE.to_uppercase());
295 let as_str = Box::new(heading).leak();
296 Some(as_str)
297 }
298}
299
300pub struct TriggerAppBuilder<T, B> {
302 engine_config: spin_core::Config,
303 pub trigger: T,
304 _factors_builder: std::marker::PhantomData<B>,
305}
306
307impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> TriggerAppBuilder<T, B> {
308 pub fn new(trigger: T) -> Self {
309 Self {
310 engine_config: spin_core::Config::default(),
311 trigger,
312 _factors_builder: Default::default(),
313 }
314 }
315
316 pub fn engine_config(&mut self) -> &mut spin_core::Config {
317 &mut self.engine_config
318 }
319
320 pub async fn build(
322 &mut self,
323 app: App,
324 common_options: FactorsConfig,
325 options: B::CliArgs,
326 loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
327 ) -> anyhow::Result<TriggerApp<T, B::Factors>> {
328 let mut core_engine_builder = {
329 self.trigger.update_core_config(&mut self.engine_config)?;
330
331 spin_core::Engine::builder(&self.engine_config)?
332 };
333 self.trigger.add_to_linker(core_engine_builder.linker())?;
334
335 let (factors, runtime_config) = B::build(&common_options, &options)?;
336
337 let mut executor = FactorsExecutor::new(core_engine_builder, factors)?;
338 B::configure_app(&mut executor, &runtime_config, &common_options, &options)?;
339 let executor = Arc::new(executor);
340
341 let configured_app = {
342 let _sloth_guard = warn_if_wasm_build_slothful();
343 executor
344 .load_app(app, runtime_config.into(), loader, Some(T::TYPE))
345 .await?
346 };
347
348 Ok(configured_app)
349 }
350
351 pub async fn run(
353 mut self,
354 app: App,
355 common_options: FactorsConfig,
356 options: B::CliArgs,
357 loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
358 ) -> anyhow::Result<impl Future<Output = anyhow::Result<()>>> {
359 let configured_app = self.build(app, common_options, options, loader).await?;
360 Ok(self.trigger.run(configured_app))
361 }
362}
363
364pub trait RuntimeFactorsBuilder {
366 type Factors: RuntimeFactors;
368 type CliArgs: clap::Args;
370 type RuntimeConfig: Into<<Self::Factors as RuntimeFactors>::RuntimeConfig>;
372
373 fn build(
375 config: &FactorsConfig,
376 args: &Self::CliArgs,
377 ) -> anyhow::Result<(Self::Factors, Self::RuntimeConfig)>;
378
379 fn configure_app<U: Send + 'static>(
381 executor: &mut FactorsExecutor<Self::Factors, U>,
382 runtime_config: &Self::RuntimeConfig,
383 config: &FactorsConfig,
384 args: &Self::CliArgs,
385 ) -> anyhow::Result<()> {
386 let _ = (executor, runtime_config, config, args);
387 Ok(())
388 }
389}
390
391pub mod help {
392 use super::*;
393
394 pub struct HelpArgsOnlyTrigger;
397
398 impl<F: RuntimeFactors> Trigger<F> for HelpArgsOnlyTrigger {
399 const TYPE: &'static str = "help-args-only";
400 type CliArgs = NoCliArgs;
401 type InstanceState = ();
402
403 fn new(_cli_args: Self::CliArgs, _app: &App) -> anyhow::Result<Self> {
404 Ok(Self)
405 }
406
407 async fn run(self, _configured_app: TriggerApp<Self, F>) -> anyhow::Result<()> {
408 Ok(())
409 }
410 }
411}
412
413#[derive(Clone, Debug, Default)]
415pub enum UserProvidedPath {
416 Provided(PathBuf),
418 #[default]
420 Default,
421 Unset,
423}