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::{
12 errors::{Result, UserFacingError},
13 storage::SqliteStorage,
14};
15
16const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
18
19#[derive(Debug)]
21pub(super) enum VersionCheckState {
22 NotStarted,
24 InProgress,
26 Finished(Option<Version>),
28}
29
30impl IntelliShellService {
31 #[instrument(skip_all)]
37 pub fn poll_new_version(&self) -> Option<Version> {
38 let mut state = self.version_check_state.lock().expect("poisoned lock");
40
41 match &*state {
42 VersionCheckState::Finished(version) => version.clone(),
44
45 VersionCheckState::InProgress => None,
47
48 VersionCheckState::NotStarted => {
50 if !self.check_updates {
52 tracing::debug!("Skipping version check as it's disabled in the configuration");
53 *state = VersionCheckState::Finished(None);
54 return None;
55 }
56
57 *state = VersionCheckState::InProgress;
59 tracing::trace!("Spawning background task for version check");
60 drop(state);
61
62 let storage = self.storage.clone();
63 let state_clone = self.version_check_state.clone();
64 tokio::spawn(
65 async move {
66 let result = fetch_latest_version(&storage, false).await;
67
68 let mut state = state_clone.lock().expect("poisoned lock");
70 match result {
71 Ok(version) => {
72 if let Some(ref v) = version {
73 tracing::info!("New version available: v{v}");
74 } else {
75 tracing::debug!("No new version available");
76 }
77 *state = VersionCheckState::Finished(version);
78 }
79 Err(err) => {
80 tracing::error!("Failed to check for new version: {err:#?}");
81 *state = VersionCheckState::Finished(None);
82 }
83 }
84 }
85 .instrument(tracing::info_span!("bg")),
86 );
87
88 None
89 }
90 }
91 }
92
93 #[instrument(skip_all)]
99 pub async fn check_new_version(&self, force_fetch: bool) -> Result<Option<Version>> {
100 fetch_latest_version(&self.storage, force_fetch).await
101 }
102}
103
104async fn fetch_latest_version(storage: &SqliteStorage, force_fetch: bool) -> Result<Option<Version>> {
112 let now = Utc::now();
114 let current = Version::parse(env!("CARGO_PKG_VERSION")).wrap_err("Failed to parse current version")?;
115
116 if !force_fetch {
118 let (latest, checked_at) = storage.get_version_info().await?;
119
120 if (now - checked_at) < chrono::Duration::hours(16) {
122 tracing::debug!("Skipping version retrieval as it was checked recently, latest: v{latest}");
123 return Ok(Some(latest).filter(|v| v > ¤t));
124 }
125 }
126
127 #[derive(Deserialize, Debug)]
129 struct Release {
130 tag_name: String,
131 }
132
133 let res = reqwest::Client::new()
135 .get("https://api.github.com/repos/lasantosr/intelli-shell/releases/latest")
136 .header(header::USER_AGENT, "intelli-shell")
137 .timeout(REQUEST_TIMEOUT)
138 .send()
139 .await
140 .map_err(|err| {
141 tracing::error!("{err:?}");
142 UserFacingError::LatestVersionRequestFailed(err.to_string())
143 })?;
144
145 if !res.status().is_success() {
146 let status = res.status();
147 let status_str = status.as_str();
148 let body = res.text().await.unwrap_or_default();
149 let message = serde_json::from_str::<serde_json::Value>(&body)
150 .ok()
151 .and_then(|v| v.get("message").and_then(|m| m.as_str()).map(|s| s.to_string()))
152 .unwrap_or_else(|| format!("received {status_str} response"));
153 if let Some(reason) = status.canonical_reason() {
154 tracing::error!("Got response [{status_str}] {reason}:\n{body}");
155 return Err(UserFacingError::LatestVersionRequestFailed(message).into());
156 } else {
157 tracing::error!("Got response [{status_str}]:\n{body}");
158 return Err(UserFacingError::LatestVersionRequestFailed(message).into());
159 }
160 }
161
162 let release: Release = res.json().await.wrap_err("Failed to parse latest release response")?;
163
164 let tag_version = release.tag_name.trim_start_matches('v');
166 let latest = Version::parse(tag_version)
167 .wrap_err_with(|| format!("Failed to parse latest version from tag: {tag_version}"))?;
168
169 tracing::debug!("Latest version fetched: v{latest}");
170
171 storage.update_version_info(latest.clone(), now).await?;
173
174 Ok(Some(latest).filter(|v| v > ¤t))
176}