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(10);
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 poll_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 = fetch_latest_version(&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 #[instrument(skip_all)]
96 pub async fn check_new_version(&self) -> Result<Option<Version>> {
97 fetch_latest_version(&self.storage).await
98 }
99}
100
101async fn fetch_latest_version(storage: &SqliteStorage) -> Result<Option<Version>> {
109 let now = Utc::now();
111 let current = Version::parse(env!("CARGO_PKG_VERSION")).wrap_err("Failed to parse current version")?;
112 let (latest, checked_at) = storage.get_version_info().await?;
113
114 if (now - checked_at) < chrono::Duration::hours(16) {
116 tracing::debug!("Skipping version retrieval as it was checked recently, latest: v{latest}");
117 return Ok(Some(latest).filter(|v| v > ¤t));
118 }
119
120 #[derive(Deserialize, Debug)]
122 struct Release {
123 tag_name: String,
124 }
125
126 let release: Release = reqwest::Client::new()
128 .get("https://api.github.com/repos/lasantosr/intelli-shell/releases/latest")
129 .header(header::USER_AGENT, "intelli-shell")
130 .timeout(REQUEST_TIMEOUT)
131 .send()
132 .await
133 .wrap_err("Failed to fetch latest release from GitHub")?
134 .json()
135 .await
136 .wrap_err("Failed to parse latest release response")?;
137
138 let tag_version = release.tag_name.trim_start_matches('v');
140 let latest = Version::parse(tag_version)
141 .wrap_err_with(|| format!("Failed to parse latest version from tag: {tag_version}"))?;
142
143 tracing::debug!("Latest version fetched: v{latest}");
144
145 storage.update_version_info(latest.clone(), now).await?;
147
148 Ok(Some(latest).filter(|v| v > ¤t))
150}