Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/localization/locales/de.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Update wird installiert..."
update_to = "Aktualisieren auf"
update_available = "Update verfügbar"
update_prompt_now = "Version {version} ist verfügbar. Möchten Sie jetzt aktualisieren?"
update_pending_winget = "Version {version} wurde veröffentlicht, ist aber noch nicht über WinGet verfügbar. Bitte versuchen Sie es später erneut."
update_prompt_winget_behind = "Version {release} wurde veröffentlicht, ist aber noch nicht über WinGet verfügbar. Version {version} ist verfügbar. Möchten Sie jetzt aktualisieren?"
exit = "Beenden"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Applying update..."
update_to = "Update to"
update_available = "Update available"
update_prompt_now = "Version {version} is available. Do you want to update now?"
update_pending_winget = "Version {version} has been released but is not yet available on WinGet. Please try again later."
update_prompt_winget_behind = "Version {release} has been released but is not yet available on WinGet. Version {version} is available. Do you want to update now?"
exit = "Exit"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/es.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Aplicando actualización..."
update_to = "Actualizar a"
update_available = "Actualización disponible"
update_prompt_now = "La versión {version} está disponible. ¿Quieres actualizar ahora?"
update_pending_winget = "La versión {version} se ha publicado, pero aún no está disponible en WinGet. Inténtalo de nuevo más tarde."
update_prompt_winget_behind = "La versión {release} se ha publicado, pero aún no está disponible en WinGet. La versión {version} está disponible. ¿Quieres actualizar ahora?"
exit = "Salir"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/fr.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Application de la mise à jour..."
update_to = "Mettre à jour vers"
update_available = "Mise à jour disponible"
update_prompt_now = "La version {version} est disponible. Voulez-vous mettre à jour maintenant ?"
update_pending_winget = "La version {version} a été publiée mais n’est pas encore disponible sur WinGet. Veuillez réessayer plus tard."
update_prompt_winget_behind = "La version {release} a été publiée mais n’est pas encore disponible sur WinGet. La version {version} est disponible. Voulez-vous mettre à jour maintenant ?"
exit = "Quitter"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/ja.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ applying_update = "更新を適用しています..."
update_to = "更新先"
update_available = "更新が利用可能です"
update_prompt_now = "バージョン {version} が利用可能です。今すぐ更新しますか?"
update_pending_winget = "バージョン {version} はリリースされましたが、WinGet ではまだ利用できません。しばらくしてから再度お試しください。"
update_prompt_winget_behind = "バージョン {release} はリリースされましたが、WinGet ではまだ利用できません。バージョン {version} が利用可能です。今すぐ更新しますか?"
exit = "終了"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/ko.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ applying_update = "업데이트 적용 중..."
update_to = "업데이트 대상"
update_available = "업데이트 사용 가능"
update_prompt_now = "버전 {version}을 사용할 수 있습니다. 지금 업데이트하시겠습니까?"
update_pending_winget = "버전 {version}이 출시되었지만 아직 WinGet에서 사용할 수 없습니다. 나중에 다시 시도하세요."
update_prompt_winget_behind = "버전 {release}이 출시되었지만 아직 WinGet에서 사용할 수 없습니다. 버전 {version}을 사용할 수 있습니다. 지금 업데이트하시겠습니까?"
exit = "종료"
session_window = "5시간"
weekly_window = "7일"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/nl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Update wordt toegepast..."
update_to = "Bijwerken naar"
update_available = "Update beschikbaar"
update_prompt_now = "Versie {version} is beschikbaar. Wil je nu bijwerken?"
update_pending_winget = "Versie {version} is uitgebracht, maar is nog niet beschikbaar via WinGet. Probeer het later opnieuw."
update_prompt_winget_behind = "Versie {release} is uitgebracht, maar is nog niet beschikbaar via WinGet. Versie {version} is beschikbaar. Wil je nu bijwerken?"
exit = "Afsluiten"
session_window = "5u"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/pl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Instalowanie aktualizacji..."
update_to = "Zaktualizuj do"
update_available = "Dostępna aktualizacja"
update_prompt_now = "Wersja {version} jest dostępna. Czy chcesz ją zainstalować teraz?"
update_pending_winget = "Wersja {version} została wydana, ale nie jest jeszcze dostępna w WinGet. Spróbuj ponownie później."
update_prompt_winget_behind = "Wersja {release} została wydana, ale nie jest jeszcze dostępna w WinGet. Wersja {version} jest dostępna. Czy chcesz ją zainstalować teraz?"
exit = "Zakończ"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/pt-BR.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Aplicando atualização..."
update_to = "Atualizar para"
update_available = "Atualização disponível"
update_prompt_now = "Versão {version} está disponível. Deseja atualizar agora?"
update_pending_winget = "A versão {version} foi lançada, mas ainda não está disponível no WinGet. Tente novamente mais tarde."
update_prompt_winget_behind = "A versão {release} foi lançada, mas ainda não está disponível no WinGet. A versão {version} está disponível. Deseja atualizar agora?"
exit = "Sair"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/ru.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Применение обновления..."
update_to = "Обновить до"
update_available = "Доступно обновление"
update_prompt_now = "Доступна версия {version}. Обновить сейчас?"
update_pending_winget = "Версия {version} выпущена, но пока недоступна в WinGet. Повторите попытку позже."
update_prompt_winget_behind = "Версия {release} выпущена, но пока недоступна в WinGet. Доступна версия {version}. Обновить сейчас?"
exit = "Выход"
session_window = "5ч"
weekly_window = "7д"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/th.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ applying_update = "กำลังติดตั้งการอัปเด
update_to = "อัปเดตเป็น"
update_available = "มีการอัปเดตใหม่"
update_prompt_now = "มีเวอร์ชัน {version} ให้ใช้งานแล้ว ต้องการอัปเดตตอนนี้หรือไม่?"
update_pending_winget = "เวอร์ชัน {version} เผยแพร่แล้ว แต่ยังไม่พร้อมใช้งานใน WinGet โปรดลองอีกครั้งในภายหลัง"
update_prompt_winget_behind = "เวอร์ชัน {release} เผยแพร่แล้ว แต่ยังไม่พร้อมใช้งานใน WinGet มีเวอร์ชัน {version} ให้ใช้งานแล้ว ต้องการอัปเดตตอนนี้หรือไม่?"
exit = "ออกจากโปรแกรม"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/tr.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ applying_update = "Güncelleme uygulanıyor..."
update_to = "Şu sürüme güncelle"
update_available = "Güncelleme mevcut"
update_prompt_now = "{version} sürümü mevcut. Şimdi güncellemek ister misiniz?"
update_pending_winget = "{version} sürümü yayımlandı ancak henüz WinGet üzerinde mevcut değil. Lütfen daha sonra tekrar deneyin."
update_prompt_winget_behind = "{release} sürümü yayımlandı ancak henüz WinGet üzerinde mevcut değil. {version} sürümü mevcut. Şimdi güncellemek ister misiniz?"
exit = "Çıkış"
session_window = "5 sa"
weekly_window = "7 g"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/zh-CN.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ applying_update = "正在应用更新..."
update_to = "更新至"
update_available = "有可用更新"
update_prompt_now = "版本 {version} 已可用。是否立即更新?"
update_pending_winget = "版本 {version} 已发布,但尚未在 WinGet 上提供。请稍后再试。"
update_prompt_winget_behind = "版本 {release} 已发布,但尚未在 WinGet 上提供。版本 {version} 已可用。是否立即更新?"
exit = "退出"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locales/zh-TW.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ applying_update = "正在套用更新..."
update_to = "更新至"
update_available = "有可用更新"
update_prompt_now = "版本 {version} 已可用。是否立即更新?"
update_pending_winget = "版本 {version} 已發布,但尚未在 WinGet 上提供。請稍後再試。"
update_prompt_winget_behind = "版本 {release} 已發布,但尚未在 WinGet 上提供。版本 {version} 已可用。是否立即更新?"
exit = "結束"
session_window = "5h"
weekly_window = "7d"
Expand Down
2 changes: 2 additions & 0 deletions src/localization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ pub struct Strings {
pub update_to: &'static str,
pub update_available: &'static str,
pub update_prompt_now: &'static str,
pub update_pending_winget: &'static str,
pub update_prompt_winget_behind: &'static str,
pub exit: &'static str,
pub session_window: &'static str,
pub weekly_window: &'static str,
Expand Down
143 changes: 136 additions & 7 deletions src/updater.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fs::File;
use std::io;
use std::io::{self, Read};
use std::os::windows::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
Expand All @@ -18,13 +18,28 @@ const CREATE_NO_WINDOW: u32 = 0x08000000;
const CREATE_NEW_CONSOLE: u32 = 0x00000010;
// Keep this aligned with the package identifier used in winget-pkgs.
const WINGET_PACKAGE_ID: &str = "CodeZeno.ClaudeCodeUsageMonitor";
// `winget show` exit code when the package is not in the source.
const WINGET_NO_APPLICATIONS_FOUND: u32 = 0x8A15_0014;
const WINGET_SHOW_TIMEOUT: Duration = Duration::from_secs(60);
const WINGET_SHOW_VERSIONS_ARGS: [&str; 9] = [
"show",
"--id",
WINGET_PACKAGE_ID,
"--exact",
"--versions",
"--source",
"winget",
"--accept-source-agreements",
"--disable-interactivity",
];

mod download;
mod release;

use download::{download_release_asset, open_verified_source, AssetIntegrity};
pub use release::ReleaseDescriptor;
use release::{release_descriptor, GitHubRelease};
use release::{parse_version, release_descriptor, GitHubRelease};
use semver::Version;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InstallChannel {
Expand All @@ -35,7 +50,32 @@ pub enum InstallChannel {
#[derive(Debug)]
pub enum UpdateCheckResult {
UpToDate,
Available(ReleaseDescriptor),
Available(AvailableUpdate),
/// Released on GitHub, but the WinGet source has nothing newer than the current version.
Pending(String),
}

#[derive(Clone, Debug)]
pub enum AvailableUpdate {
/// Portable installs download this GitHub release asset.
Release(ReleaseDescriptor),
/// WinGet installs upgrade to the newest version in the WinGet source,
/// which can trail GitHub. `unlisted_release` names the newer GitHub
/// release that WinGet does not list yet.
Winget {
version: String,
unlisted_release: Option<String>,
},
}

impl AvailableUpdate {
/// The version this update installs.
pub fn version(&self) -> &str {
match self {
Self::Release(release) => &release.latest_version,
Self::Winget { version, .. } => version,
}
}
}

pub fn handle_cli_mode(args: &[String]) -> Option<i32> {
Expand Down Expand Up @@ -89,10 +129,44 @@ pub fn current_install_channel() -> InstallChannel {
}
}

pub fn check_for_updates() -> Result<UpdateCheckResult, String> {
match fetch_latest_release()? {
Some(release) => Ok(UpdateCheckResult::Available(release)),
None => Ok(UpdateCheckResult::UpToDate),
pub fn check_for_updates(channel: InstallChannel) -> Result<UpdateCheckResult, String> {
let Some(release) = fetch_latest_release()? else {
return Ok(UpdateCheckResult::UpToDate);
};
match channel {
InstallChannel::Portable => Ok(UpdateCheckResult::Available(AvailableUpdate::Release(
release,
))),
// GitHub releases publish before the winget-pkgs manifest PR merges, so
// offer the newest version `winget upgrade` can actually install.
InstallChannel::Winget => Ok(winget_update_result(
&parse_version(env!("CARGO_PKG_VERSION"))?,
&parse_version(&release.latest_version)?,
&winget_listed_versions()?,
)),
}
}

fn winget_update_result(
current: &Version,
released: &Version,
listed: &[Version],
) -> UpdateCheckResult {
let newest_listed = listed
.iter()
.filter(|version| version.pre.is_empty())
.max_by(|a, b| a.cmp_precedence(b));
match newest_listed {
Some(listed) if listed.cmp_precedence(current).is_gt() => {
UpdateCheckResult::Available(AvailableUpdate::Winget {
version: listed.to_string(),
unlisted_release: released
.cmp_precedence(listed)
.is_gt()
.then(|| released.to_string()),
})
}
_ => UpdateCheckResult::Pending(released.to_string()),
}
}

Expand Down Expand Up @@ -320,6 +394,61 @@ fn updates_dir() -> Result<PathBuf, String> {
.ok_or_else(|| "Unable to resolve a writable local updates directory.".to_string())
}

fn winget_listed_versions() -> Result<Vec<Version>, String> {
let mut child = Command::new("winget.exe")
.args(WINGET_SHOW_VERSIONS_ARGS)
.creation_flags(CREATE_NO_WINDOW)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| format!("Unable to run WinGet: {e}"))?;

// Drain stdout on another thread so a full pipe cannot stall WinGet.
let mut stdout = child.stdout.take().expect("WinGet stdout is piped");
let reader = std::thread::spawn(move || {
let mut output = Vec::new();
stdout.read_to_end(&mut output).map(|_| output)
});

let started = std::time::Instant::now();
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) if started.elapsed() < WINGET_SHOW_TIMEOUT => {
std::thread::sleep(Duration::from_millis(250));
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
return Err("Timed out waiting for WinGet to list available versions.".into());
}
Err(error) => return Err(format!("Unable to wait for WinGet: {error}")),
}
};
let output = reader
.join()
.map_err(|_| "Unable to read WinGet output.".to_string())?
.map_err(|e| format!("Unable to read WinGet output: {e}"))?;
winget_versions_outcome(status.code(), &String::from_utf8_lossy(&output))
}

fn winget_versions_outcome(exit_code: Option<i32>, stdout: &str) -> Result<Vec<Version>, String> {
// Windows exit codes are HRESULTs; Rust reports them as signed values.
match exit_code.map(|code| code as u32) {
// Headers are localized, so keep only the lines that are versions.
Some(0) => Ok(stdout
.lines()
.filter_map(|line| Version::parse(line.trim()).ok())
.collect()),
Some(WINGET_NO_APPLICATIONS_FOUND) => Ok(Vec::new()),
Some(code) => Err(format!(
"WinGet could not list available versions (exit code 0x{code:08X})."
)),
None => Err("WinGet exited without a status code.".into()),
}
}

fn winget_upgrade_command(pid: u32, target: &str, working_dir: &str) -> String {
let target = powershell_single_quoted(target);
let working_dir = powershell_single_quoted(working_dir);
Expand Down
2 changes: 1 addition & 1 deletion src/updater/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub(super) fn release_descriptor(
}))
}

fn parse_version(version: &str) -> Result<Version, String> {
pub(super) fn parse_version(version: &str) -> Result<Version, String> {
Version::parse(version.strip_prefix('v').unwrap_or(version))
.map_err(|e| format!("Invalid release version {version:?}: {e}"))
}
Expand Down
Loading
Loading