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!(
132 "Failed to launch plugins update process; checking using latest local repo anyway. Error: {e:#}"
133 );
134 }
135 }
136
137 fn fire_and_forget_update_impl() -> anyhow::Result<()> {
138 let mut update_cmd = tokio::process::Command::new(std::env::current_exe()?);
139 update_cmd.args(["plugins", "update"]);
140 update_cmd.stdout(std::process::Stdio::null());
141 update_cmd.stderr(std::process::Stdio::null());
142 update_cmd.spawn()?;
143 Ok(())
144 }
145
146 async fn check(&self) -> anyhow::Result<BadgerUI> {
147 let available_upgrades = self.available_upgrades().await?;
148
149 if self
151 .previous_badger
152 .includes_any(&available_upgrades.list())
153 {
154 return Ok(BadgerUI::None);
155 }
156
157 if !available_upgrades.is_none() {
158 self.record_manager
159 .record_badger(
160 &self.plugin_name,
161 &self.current_version,
162 &available_upgrades.list(),
163 )
164 .await
165 };
166
167 Ok(available_upgrades.classify())
168 }
169
170 async fn available_upgrades(&self) -> anyhow::Result<AvailableUpgrades> {
171 let store = self.plugin_manager.store();
172
173 let latest_version = {
174 let latest_lookup = crate::lookup::PluginLookup::new(&self.plugin_name, None);
175 let latest_manifest = latest_lookup
176 .resolve_manifest_exact(store.get_plugins_directory())
177 .await
178 .ok();
179 latest_manifest.and_then(|m| semver::Version::parse(m.version()).ok())
180 };
181
182 let manifests = store.catalogue_manifests()?;
183 let relevant_manifests = manifests
184 .into_iter()
185 .filter(|m| m.name() == self.plugin_name);
186 let compatible_manifests = relevant_manifests.filter(|m| {
187 m.has_compatible_package() && m.is_compatible_spin_version(self.spin_version)
188 });
189 let compatible_plugin_versions =
190 compatible_manifests.filter_map(|m| PluginVersion::try_from(m, &latest_version));
191 let considerable_manifests = compatible_plugin_versions
192 .filter(|pv| !pv.is_prerelease() && pv.is_higher_than(&self.current_version))
193 .collect::<Vec<_>>();
194
195 let (eligible_manifests, questionable_manifests) = if self.current_version.major == 0 {
196 (vec![], considerable_manifests)
197 } else {
198 considerable_manifests
199 .into_iter()
200 .partition(|pv| pv.version.major == self.current_version.major)
201 };
202
203 let highest_eligible_manifest = eligible_manifests
204 .into_iter()
205 .max_by_key(|pv| pv.version.clone());
206 let highest_questionable_manifest = questionable_manifests
207 .into_iter()
208 .max_by_key(|pv| pv.version.clone());
209
210 Ok(AvailableUpgrades {
211 eligible: highest_eligible_manifest,
212 questionable: highest_questionable_manifest,
213 })
214 }
215}
216
217fn has_timeout_expired(from_time: chrono::DateTime<chrono::Utc>) -> bool {
218 let timeout = chrono::Duration::try_days(BADGER_TIMEOUT_DAYS).unwrap();
219 let now = chrono::Utc::now();
220 match now.checked_sub_signed(timeout) {
221 None => true,
222 Some(t) => from_time < t,
223 }
224}
225
226pub struct AvailableUpgrades {
227 eligible: Option<PluginVersion>,
228 questionable: Option<PluginVersion>,
229}
230
231impl AvailableUpgrades {
232 fn is_none(&self) -> bool {
233 self.eligible.is_none() && self.questionable.is_none()
234 }
235
236 fn classify(&self) -> BadgerUI {
237 match (&self.eligible, &self.questionable) {
238 (None, None) => BadgerUI::None,
239 (Some(e), None) => BadgerUI::Eligible(e.clone()),
240 (None, Some(q)) => BadgerUI::Questionable(q.clone()),
241 (Some(e), Some(q)) => BadgerUI::Both {
242 eligible: e.clone(),
243 questionable: q.clone(),
244 },
245 }
246 }
247
248 fn list(&self) -> Vec<&semver::Version> {
249 [self.eligible.as_ref(), self.questionable.as_ref()]
250 .iter()
251 .filter_map(|pv| pv.as_ref())
252 .map(|pv| &pv.version)
253 .collect()
254 }
255}
256
257#[derive(Clone, Debug)]
258pub struct PluginVersion {
259 version: semver::Version,
260 name: String,
261 is_latest: bool,
262}
263
264impl PluginVersion {
265 fn try_from(manifest: PluginManifest, latest: &Option<semver::Version>) -> Option<Self> {
266 match semver::Version::parse(manifest.version()) {
267 Ok(version) => {
268 let name = manifest.name();
269 let is_latest = match latest {
270 None => false,
271 Some(latest) => &version == latest,
272 };
273 Some(Self {
274 version,
275 name,
276 is_latest,
277 })
278 }
279 Err(_) => None,
280 }
281 }
282
283 fn is_prerelease(&self) -> bool {
284 !self.version.pre.is_empty()
285 }
286
287 fn is_higher_than(&self, other: &semver::Version) -> bool {
288 &self.version > other
289 }
290
291 pub fn upgrade_command(&self) -> String {
292 if self.is_latest {
293 format!("spin plugins upgrade {}", self.name)
294 } else {
295 format!("spin plugins upgrade {} -v {}", self.name, self.version)
296 }
297 }
298}
299
300impl std::fmt::Display for PluginVersion {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 write!(f, "{}", self.version)
303 }
304}
305
306pub enum BadgerUI {
307 None,
310 Eligible(PluginVersion),
312 Questionable(PluginVersion),
315 Both {
318 eligible: PluginVersion,
319 questionable: PluginVersion,
320 },
321}