1#![deny(missing_docs)]
3
4use std::{collections::VecDeque, fmt::Debug, fs, path::PathBuf};
5
6use anyhow::{ensure, Context, Result};
7use async_trait::async_trait;
8use spin_common::ui::quoted_path;
9use toml_edit::DocumentMut;
10
11pub mod manifest;
13pub mod rustlang;
15pub(crate) mod test;
17pub mod wasm;
19
20pub struct Checkup {
22 patient: PatientApp,
23 diagnostics: VecDeque<Box<dyn BoxingDiagnostic>>,
24 unprocessed_diagnoses: VecDeque<Box<dyn Diagnosis>>,
25}
26
27impl Checkup {
28 pub fn new(manifest_path: impl Into<PathBuf>) -> Result<Self> {
30 let patient = PatientApp::new(manifest_path)?;
31 let mut checkup = Self {
32 patient,
33 diagnostics: Default::default(),
34 unprocessed_diagnoses: Default::default(),
35 };
36 checkup
37 .add_diagnostic::<manifest::upgrade::UpgradeDiagnostic>()
38 .add_diagnostic::<manifest::version::VersionDiagnostic>()
39 .add_diagnostic::<manifest::trigger::TriggerDiagnostic>()
40 .add_diagnostic::<rustlang::target::TargetDiagnostic>() .add_diagnostic::<wasm::missing::WasmMissingDiagnostic>();
42 Ok(checkup)
43 }
44
45 pub fn patient(&self) -> &PatientApp {
47 &self.patient
48 }
49
50 pub fn add_diagnostic<D: Diagnostic + Default + 'static>(&mut self) -> &mut Self {
52 self.diagnostics.push_back(Box::<D>::default());
53 self
54 }
55
56 pub async fn next_diagnosis(&mut self) -> Result<Option<PatientDiagnosis>> {
58 while self.unprocessed_diagnoses.is_empty() {
59 let Some(diagnostic) = self.diagnostics.pop_front() else {
60 return Ok(None);
61 };
62 self.unprocessed_diagnoses = diagnostic
63 .diagnose_boxed(&self.patient)
64 .await
65 .unwrap_or_else(|err| {
66 tracing::debug!("Diagnose failed: {err:?}");
67 vec![]
68 })
69 .into()
70 }
71 Ok(Some(PatientDiagnosis {
72 patient: &mut self.patient,
73 diagnosis: self.unprocessed_diagnoses.pop_front().unwrap(),
74 }))
75 }
76}
77
78#[derive(Clone)]
80pub struct PatientApp {
81 pub manifest_path: PathBuf,
83 pub manifest_doc: DocumentMut,
85}
86
87impl PatientApp {
88 fn new(manifest_path: impl Into<PathBuf>) -> Result<Self> {
89 let path = manifest_path.into();
90 ensure!(
91 path.is_file(),
92 "No Spin app manifest file found at {}",
93 quoted_path(&path),
94 );
95
96 let contents = fs::read_to_string(&path).with_context(|| {
97 format!(
98 "Couldn't read Spin app manifest file at {}",
99 quoted_path(&path)
100 )
101 })?;
102
103 let manifest_doc: DocumentMut = contents.parse().with_context(|| {
104 format!(
105 "Couldn't parse manifest file at {} as valid TOML",
106 quoted_path(&path)
107 )
108 })?;
109
110 Ok(Self {
111 manifest_path: path,
112 manifest_doc,
113 })
114 }
115}
116
117pub struct PatientDiagnosis<'a> {
119 pub diagnosis: Box<dyn Diagnosis>,
121 pub patient: &'a mut PatientApp,
123}
124
125#[async_trait]
127pub trait Diagnostic: Send + Sync {
128 type Diagnosis: Diagnosis;
130
131 async fn diagnose(&self, patient: &PatientApp) -> Result<Vec<Self::Diagnosis>>;
137}
138
139pub trait Diagnosis: Debug + Send + Sync + 'static {
141 fn description(&self) -> String;
143
144 fn is_critical(&self) -> bool {
148 true
149 }
150
151 fn treatment(&self) -> Option<&dyn Treatment> {
154 None
155 }
156}
157
158#[async_trait]
160pub trait Treatment: Sync {
161 fn summary(&self) -> String;
164
165 async fn dry_run(&self, patient: &PatientApp) -> Result<String> {
171 let _ = patient;
172 Err(DryRunNotSupported.into())
173 }
174
175 async fn treat(&self, patient: &mut PatientApp) -> Result<()>;
178}
179
180#[derive(Debug)]
182pub struct DryRunNotSupported;
183
184impl std::fmt::Display for DryRunNotSupported {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 write!(f, "dry run not implemented for this treatment")
187 }
188}
189
190impl std::error::Error for DryRunNotSupported {}
191
192#[async_trait]
193trait BoxingDiagnostic {
194 async fn diagnose_boxed(&self, patient: &PatientApp) -> Result<Vec<Box<dyn Diagnosis>>>;
195}
196
197#[async_trait]
198impl<Factory: Diagnostic> BoxingDiagnostic for Factory {
199 async fn diagnose_boxed(&self, patient: &PatientApp) -> Result<Vec<Box<dyn Diagnosis>>> {
200 Ok(self
201 .diagnose(patient)
202 .await?
203 .into_iter()
204 .map(|diag| Box::new(diag) as Box<dyn Diagnosis>)
205 .collect())
206 }
207}
208
209#[derive(Debug)]
212pub struct StopDiagnosing {
213 message: String,
214}
215
216impl std::fmt::Display for StopDiagnosing {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 write!(f, "{}", self.message)
219 }
220}
221
222impl StopDiagnosing {
223 pub fn new(message: impl Into<String>) -> Self {
225 Self {
226 message: message.into(),
227 }
228 }
229
230 pub fn message(&self) -> &str {
233 &self.message
234 }
235}