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