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