spin_plugins/badger/
mod.rs1mod store;
2
3use std::io::IsTerminal;
4
5use self::store::{BadgerRecordManager, PreviousBadger};
6use crate::manifest::PluginManifest;
7
8const BADGER_TIMEOUT_DAYS: i64 = 14;
9
10pub enum BadgerChecker {
38 Precomputed(anyhow::Result<BadgerUI>),
39 Deferred(BadgerEvaluator),
40}
41
42pub struct BadgerEvaluator {
43 plugin_name: String,
44 current_version: semver::Version,
45 spin_version: &'static str,
46 plugin_manager: crate::manager::PluginManager,
47 record_manager: BadgerRecordManager,
48 previous_badger: PreviousBadger,
49}
50
51impl BadgerChecker {
52 pub fn start(
53 name: &str,
54 current_version: Option<String>,
55 spin_version: &'static str,
56 ) -> tokio::task::JoinHandle<Self> {
57 let name = name.to_owned();
58
59 tokio::task::spawn(async move {
60 let current_version = match current_version {
61 Some(v) => v.to_owned(),
62 None => return Self::Precomputed(Ok(BadgerUI::None)),
63 };
64
65 if !std::io::stderr().is_terminal() {
66 return Self::Precomputed(Ok(BadgerUI::None));
67 }
68
69 match BadgerEvaluator::new(&name, ¤t_version, spin_version).await {
70 Ok(b) => {
71 if b.should_check() {
72 BadgerEvaluator::fire_and_forget_update();
76 Self::Deferred(b)
77 } else {
78 Self::Precomputed(Ok(BadgerUI::None))
82 }
83 }
84 Err(e) => {
85 Self::Precomputed(Err(e))
88 }
89 }
90 })
91 }
92
93 pub async fn check(self) -> anyhow::Result<BadgerUI> {
94 match self {
95 Self::Precomputed(r) => r,
96 Self::Deferred(b) => b.check().await,
97 }
98 }
99}
100
101impl BadgerEvaluator {
102 async fn new(
103 name: &str,
104 current_version: &str,
105 spin_version: &'static str,
106 ) -> anyhow::Result<Self> {
107 let current_version = semver::Version::parse(current_version)?;
108 let plugin_manager = crate::manager::PluginManager::try_default()?;
109 let record_manager = BadgerRecordManager::default()?;
110 let previous_badger = record_manager.previous_badger(name, ¤t_version).await;
111
112 Ok(Self {
113 plugin_name: name.to_owned(),
114 current_version,
115 spin_version,
116 plugin_manager,
117 record_manager,
118 previous_badger,
119 })
120 }
121
122 fn should_check(&self) -> bool {
123 match self.previous_badger {
124 PreviousBadger::Fresh => true,
125 PreviousBadger::FromCurrent { when, .. } => has_timeout_expired(when),
126 }
127 }
128
129 fn fire_and_forget_update() {
130 if let Err(e) = Self::fire_and_forget_update_impl() {
131 tracing::info!("Failed to launch plugins update process; checking using latest local repo anyway. Error: {e:#}");
132 }
133 }
134
135 fn fire_and_forget_update_impl() -> anyhow::Result<()> {
136 let mut update_cmd = tokio::process::Command::new(std::env::current_exe()?);
137 update_cmd.args(["plugins", "update"]);
138 update_cmd.stdout(std::process::Stdio::null());
139 update_cmd.stderr(std::process::Stdio::null());
140 update_cmd.spawn()?;
141 Ok(())
142 }
143
144 async fn check(&self) -> anyhow::Result<BadgerUI> {
145 let available_upgrades = self.available_upgrades().await?;
146
147 if self
149 .previous_badger
150 .includes_any(&available_upgrades.list())
151 {
152 return Ok(BadgerUI::None);
153 }
154
155 if !available_upgrades.is_none() {
156 self.record_manager
157 .record_badger(
158 &self.plugin_name,
159 &self.current_version,
160 &available_upgrades.list(),
161 )
162 .await
163 };
164
165 Ok(available_upgrades.classify())
166 }
167
168 async fn available_upgrades(&self) -> anyhow::Result<AvailableUpgrades> {
169 let store = self.plugin_manager.store();
170
171 let latest_version = {
172 let latest_lookup = crate::lookup::PluginLookup::new(&self.plugin_name, None);
173 let latest_manifest = latest_lookup
174 .resolve_manifest_exact(store.get_plugins_directory())
175 .await
176 .ok();
177 latest_manifest.and_then(|m| semver::Version::parse(m.version()).ok())
178 };
179
180 let manifests = store.catalogue_manifests()?;
181 let relevant_manifests = manifests
182 .into_iter()
183 .filter(|m| m.name() == self.plugin_name);
184 let compatible_manifests = relevant_manifests.filter(|m| {
185 m.has_compatible_package() && m.is_compatible_spin_version(self.spin_version)
186 });
187 let compatible_plugin_versions =
188 compatible_manifests.filter_map(|m| PluginVersion::try_from(m, &latest_version));
189 let considerable_manifests = compatible_plugin_versions
190 .filter(|pv| !pv.is_prerelease() && pv.is_higher_than(&self.current_version))
191 .collect::<Vec<_>>();
192
193 let (eligible_manifests, questionable_manifests) = if self.current_version.major == 0 {
194 (vec![], considerable_manifests)
195 } else {
196 considerable_manifests
197 .into_iter()
198 .partition(|pv| pv.version.major == self.current_version.major)
199 };
200
201 let highest_eligible_manifest = eligible_manifests
202 .into_iter()
203 .max_by_key(|pv| pv.version.clone());
204 let highest_questionable_manifest = questionable_manifests
205 .into_iter()
206 .max_by_key(|pv| pv.version.clone());
207
208 Ok(AvailableUpgrades {
209 eligible: highest_eligible_manifest,
210 questionable: highest_questionable_manifest,
211 })
212 }
213}
214
215fn has_timeout_expired(from_time: chrono::DateTime<chrono::Utc>) -> bool {
216 let timeout = chrono::Duration::try_days(BADGER_TIMEOUT_DAYS).unwrap();
217 let now = chrono::Utc::now();
218 match now.checked_sub_signed(timeout) {
219 None => true,
220 Some(t) => from_time < t,
221 }
222}
223
224pub struct AvailableUpgrades {
225 eligible: Option<PluginVersion>,
226 questionable: Option<PluginVersion>,
227}
228
229impl AvailableUpgrades {
230 fn is_none(&self) -> bool {
231 self.eligible.is_none() && self.questionable.is_none()
232 }
233
234 fn classify(&self) -> BadgerUI {
235 match (&self.eligible, &self.questionable) {
236 (None, None) => BadgerUI::None,
237 (Some(e), None) => BadgerUI::Eligible(e.clone()),
238 (None, Some(q)) => BadgerUI::Questionable(q.clone()),
239 (Some(e), Some(q)) => BadgerUI::Both {
240 eligible: e.clone(),
241 questionable: q.clone(),
242 },
243 }
244 }
245
246 fn list(&self) -> Vec<&semver::Version> {
247 [self.eligible.as_ref(), self.questionable.as_ref()]
248 .iter()
249 .filter_map(|pv| pv.as_ref())
250 .map(|pv| &pv.version)
251 .collect()
252 }
253}
254
255#[derive(Clone, Debug)]
256pub struct PluginVersion {
257 version: semver::Version,
258 name: String,
259 is_latest: bool,
260}
261
262impl PluginVersion {
263 fn try_from(manifest: PluginManifest, latest: &Option<semver::Version>) -> Option<Self> {
264 match semver::Version::parse(manifest.version()) {
265 Ok(version) => {
266 let name = manifest.name();
267 let is_latest = match latest {
268 None => false,
269 Some(latest) => &version == latest,
270 };
271 Some(Self {
272 version,
273 name,
274 is_latest,
275 })
276 }
277 Err(_) => None,
278 }
279 }
280
281 fn is_prerelease(&self) -> bool {
282 !self.version.pre.is_empty()
283 }
284
285 fn is_higher_than(&self, other: &semver::Version) -> bool {
286 &self.version > other
287 }
288
289 pub fn upgrade_command(&self) -> String {
290 if self.is_latest {
291 format!("spin plugins upgrade {}", self.name)
292 } else {
293 format!("spin plugins upgrade {} -v {}", self.name, self.version)
294 }
295 }
296}
297
298impl std::fmt::Display for PluginVersion {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 write!(f, "{}", self.version)
301 }
302}
303
304pub enum BadgerUI {
305 None,
308 Eligible(PluginVersion),
310 Questionable(PluginVersion),
313 Both {
316 eligible: PluginVersion,
317 questionable: PluginVersion,
318 },
319}