intelli_shell/service/
version.rs1use std::{env, time::Duration};
2
3use chrono::Utc;
4use color_eyre::eyre::Context;
5use reqwest::header;
6use semver::Version;
7use serde::Deserialize;
8use tracing::{Instrument, instrument};
9
10use super::IntelliShellService;
11use crate::{errors::Result, storage::SqliteStorage};
12
13const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
15
16#[derive(Debug)]
18pub(super) enum VersionCheckState {
19 NotStarted,
21 InProgress,
23 Finished(Option<Version>),
25}
26
27impl IntelliShellService {
28 #[instrument(skip_all)]
34 pub fn check_new_version(&self) -> Option<Version> {
35 let mut state = self.version_check_state.lock().expect("poisoned lock");
37
38 match &*state {
39 VersionCheckState::Finished(version) => version.clone(),
41
42 VersionCheckState::InProgress => None,
44
45 VersionCheckState::NotStarted => {
47 if !self.check_updates {
49 tracing::debug!("Skipping version check as it's disabled in the configuration");
50 *state = VersionCheckState::Finished(None);
51 return None;
52 }
53
54 *state = VersionCheckState::InProgress;
56 tracing::trace!("Spawning background task for version check");
57 drop(state);
58
59 let storage = self.storage.clone();
60 let state_clone = self.version_check_state.clone();
61 tokio::spawn(
62 async move {
63 let result = perform_version_check(storage).await;
64
65 let mut state = state_clone.lock().expect("poisoned lock");
67 match result {
68 Ok(version) => {
69 if let Some(ref v) = version {
70 tracing::info!("New version available: v{v}");
71 } else {
72 tracing::debug!("No new version available");
73 }
74 *state = VersionCheckState::Finished(version);
75 }
76 Err(err) => {
77 tracing::error!("Failed to check for new version: {err:#?}");
78 *state = VersionCheckState::Finished(None);
79 }
80 }
81 }
82 .instrument(tracing::info_span!("bg")),
83 );
84
85 None
86 }
87 }
88 }
89}
90
91async fn perform_version_check(storage: SqliteStorage) -> Result<Option<Version>> {
93 let now = Utc::now();
95 let current = Version::parse(env!("CARGO_PKG_VERSION")).wrap_err("Failed to parse current version")?;
96 let (latest, checked_at) = storage.get_version_info().await?;
97
98 if (now - checked_at) < chrono::Duration::hours(16) {
100 tracing::debug!("Skipping version retrieval as it was checked recently, latest: v{latest}");
101 return Ok(Some(latest).filter(|v| v > ¤t));
102 }
103
104 #[derive(Deserialize, Debug)]
106 struct Release {
107 tag_name: String,
108 }
109
110 let release: Release = reqwest::Client::new()
112 .get("https://api.github.com/repos/lasantosr/intelli-shell/releases/latest")
113 .header(header::USER_AGENT, "intelli-shell")
114 .timeout(REQUEST_TIMEOUT)
115 .send()
116 .await
117 .wrap_err("Failed to fetch latest release from GitHub")?
118 .json()
119 .await
120 .wrap_err("Failed to parse latest release response")?;
121
122 let tag_version = release.tag_name.trim_start_matches('v');
124 let latest = Version::parse(tag_version)
125 .wrap_err_with(|| format!("Failed to parse latest version from tag: {tag_version}"))?;
126
127 tracing::debug!("Latest version fetched: v{latest}");
128
129 storage.update_version_info(latest.clone(), now).await?;
131
132 Ok(Some(latest).filter(|v| v > ¤t))
134}