1
1
mirror of https://github.com/wez/wezterm.git synced 2024-09-11 14:25:57 +03:00

wezterm: check for new releases

This commit adds some plumbing that will use the github API to
probe the currently released version of wezterm, and if it is
newer than the running version, show a window with some release
information and links to the changelog and download page.

The checks can be disabled in the config (but require a restart
to take effect!) and default to checking every 24 hours.

If running an AppImage on linux, links directly to the appimage
download.  In the future I'd like to have the download button
use zsync to apply the update to the local image.
This commit is contained in:
Wez Furlong 2020-05-07 08:58:52 -07:00
parent 407e4f855c
commit d39c16c406
8 changed files with 507 additions and 42 deletions

83
Cargo.lock generated
View File

@ -1208,6 +1208,18 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "http_req"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335f373129a4d09664e1ba2b2c6e43e01625aec1eb076e486da4891372c02f14"
dependencies = [
"rustls",
"unicase",
"webpki",
"webpki-roots",
]
[[package]]
name = "humantime"
version = "1.3.0"
@ -1302,6 +1314,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
[[package]]
name = "jobserver"
version = "0.1.21"
@ -2584,6 +2602,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustls"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e"
dependencies = [
"base64 0.10.1",
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rusttype"
version = "0.7.9"
@ -2637,6 +2668,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "semver"
version = "0.9.0"
@ -2672,6 +2713,17 @@ dependencies = [
"syn 1.0.18",
]
[[package]]
name = "serde_json"
version = "1.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7894c8ed05b7a3a279aeb79025fdec1d3158080b75b98a08faf2806bb799edd"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serial"
version = "0.4.0"
@ -3221,6 +3273,15 @@ dependencies = [
"ws2_32-sys",
]
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check 0.9.1",
]
[[package]]
name = "unicode-bidi"
version = "0.3.4"
@ -3534,6 +3595,25 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f50e1972865d6b1adb54167d1c8ed48606004c2c9d0ea5f1eeb34d95e863ef"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4"
dependencies = [
"webpki",
]
[[package]]
name = "wezterm"
version = "0.1.0"
@ -3567,6 +3647,7 @@ dependencies = [
"harfbuzz",
"hdrhistogram",
"hostname",
"http_req",
"image",
"lazy_static",
"leb128",
@ -3586,7 +3667,9 @@ dependencies = [
"rangeset",
"ratelimit_meter",
"rcgen",
"regex",
"serde",
"serde_json",
"serial",
"shared_library",
"ssh2",

View File

@ -52,7 +52,10 @@ portable-pty = { path = "pty", features = ["serde_support", "ssh"]}
promise = { path = "promise" }
ratelimit_meter = "5.0"
rcgen = "0.7"
http_req = {version="0.6", default-features=false, features=["rust-tls"]}
regex = "1"
serde = {version="1.0", features = ["rc", "derive"]}
serde_json = "1.0"
serial = "0.4"
ssh2 = "0.8"
structopt = "0.3"

View File

@ -45,7 +45,7 @@ pub use unix::*;
lazy_static! {
pub static ref HOME_DIR: PathBuf = dirs::home_dir().expect("can't find HOME dir");
pub static ref CONFIG_DIR: PathBuf = xdg_config_home();
static ref RUNTIME_DIR: PathBuf = compute_runtime_dir().unwrap();
pub static ref RUNTIME_DIR: PathBuf = compute_runtime_dir().unwrap();
static ref CONFIG: Configuration = Configuration::new();
}
@ -501,6 +501,16 @@ pub struct Config {
#[serde(default)]
pub launch_menu: Vec<SpawnCommand>,
#[serde(default = "default_true")]
pub check_for_updates: bool,
#[serde(default = "default_update_interval")]
pub check_for_updates_interval_seconds: u64,
}
fn default_update_interval() -> u64 {
86400
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug)]

View File

@ -227,30 +227,40 @@ pub struct ConnectionUI {
impl ConnectionUI {
pub fn new() -> Self {
let (tx, rx) = bounded(16);
promise::spawn::spawn_into_main_thread(termwiztermtab::run(80, 24, move |term| {
let mut ui = ConnectionUIImpl { term, rx };
if let Err(e) = ui.run() {
log::error!("while running ConnectionUI loop: {:?}", e);
}
ui.sleep(
"(this window will close automatically)",
Duration::new(10, 0),
)
.ok();
Ok(())
}));
promise::spawn::spawn_into_main_thread(termwiztermtab::run(
80,
24,
move |term| {
let mut ui = ConnectionUIImpl { term, rx };
if let Err(e) = ui.run() {
log::error!("while running ConnectionUI loop: {:?}", e);
}
ui.sleep(
"(this window will close automatically)",
Duration::new(10, 0),
)
.ok();
Ok(())
},
false,
));
Self { tx }
}
pub fn new_with_no_close_delay() -> Self {
let (tx, rx) = bounded(16);
promise::spawn::spawn_into_main_thread(termwiztermtab::run(80, 24, move |term| {
let mut ui = ConnectionUIImpl { term, rx };
if let Err(e) = ui.run() {
log::error!("while running ConnectionUI loop: {:?}", e);
}
Ok(())
}));
promise::spawn::spawn_into_main_thread(termwiztermtab::run(
80,
24,
move |term| {
let mut ui = ConnectionUIImpl { term, rx };
if let Err(e) = ui.run() {
log::error!("while running ConnectionUI loop: {:?}", e);
}
Ok(())
},
false,
));
Self { tx }
}

View File

@ -593,6 +593,8 @@ impl TermWindow {
let cloned_window = window.clone();
crate::update::start_update_checker();
Connection::get()
.unwrap()
.schedule_timer(std::time::Duration::from_millis(35), {

View File

@ -32,6 +32,7 @@ mod server;
mod ssh;
mod stats;
mod termwiztermtab;
mod update;
use crate::frontend::activity::Activity;
use crate::frontend::{front_end, FrontEndSelection};

View File

@ -193,6 +193,7 @@ pub struct TermWizTerminalTab {
renderable: RefCell<RenderableState>,
writer: RefCell<RenderableWriter>,
reader: Pipe,
mouse_grabbed: bool,
}
impl TermWizTerminalTab {
@ -224,8 +225,13 @@ impl TermWizTerminalTab {
renderable,
writer: RefCell::new(RenderableWriter { input_tx }),
reader,
mouse_grabbed: true,
}
}
pub fn set_mouse_grabbed(&mut self, grabbed: bool) {
self.mouse_grabbed = grabbed;
}
}
impl Tab for TermWizTerminalTab {
@ -314,7 +320,7 @@ impl Tab for TermWizTerminalTab {
}
fn advance_bytes(&self, _buf: &[u8], _host: &mut dyn TerminalHost) {
panic!("advance_bytes is undefed for TermWizTerminalTab");
panic!("advance_bytes is undefined for TermWizTerminalTab");
}
fn is_dead(&self) -> bool {
@ -343,7 +349,7 @@ impl Tab for TermWizTerminalTab {
}
fn is_mouse_grabbed(&self) -> bool {
true
self.mouse_grabbed
}
fn get_current_working_dir(&self) -> Option<Url> {
@ -461,6 +467,7 @@ pub async fn run<
width: usize,
height: usize,
f: F,
mouse_grabbed: bool,
) -> anyhow::Result<T> {
let (render_tx, render_rx) = channel();
let (input_tx, input_rx) = channel();
@ -481,6 +488,7 @@ pub async fn run<
render_rx: Receiver<Vec<Change>>,
width: usize,
height: usize,
mouse_grabbed: bool,
) -> anyhow::Result<WindowId> {
let mux = Mux::get().unwrap();
@ -490,13 +498,10 @@ pub async fn run<
let window_id = mux.new_empty_window();
let tab: Rc<dyn Tab> = Rc::new(TermWizTerminalTab::new(
domain.domain_id(),
width,
height,
input_tx,
render_rx,
));
let mut tab =
TermWizTerminalTab::new(domain.domain_id(), width, height, input_tx, render_rx);
tab.set_mouse_grabbed(mouse_grabbed);
let tab: Rc<dyn Tab> = Rc::new(tab);
mux.add_tab(&tab)?;
mux.add_tab_to_window(&tab, window_id)?;
@ -510,7 +515,7 @@ pub async fn run<
}
let window_id: WindowId = promise::spawn::spawn_into_main_thread(async move {
register_tab(input_tx, render_rx, width, height).await
register_tab(input_tx, render_rx, width, height, mouse_grabbed).await
})
.await
.unwrap_or_else(|| bail!("task panicked or was cancelled"))?;
@ -537,19 +542,24 @@ pub fn message_box_ok(message: &str) {
let title = "wezterm";
let message = message.to_string();
promise::spawn::block_on(run(60, 10, move |mut term| {
term.render(&[
Change::Title(title.to_string()),
Change::Text(message.to_string()),
])
.map_err(Error::msg)?;
promise::spawn::block_on(run(
60,
10,
move |mut term| {
term.render(&[
Change::Title(title.to_string()),
Change::Text(message.to_string()),
])
.map_err(Error::msg)?;
let mut editor = LineEditor::new(&mut term);
editor.set_prompt("press enter to continue.");
let mut editor = LineEditor::new(&mut term);
editor.set_prompt("press enter to continue.");
let mut host = NopLineEditorHost::default();
editor.read_line(&mut host).ok();
Ok(())
}))
let mut host = NopLineEditorHost::default();
editor.read_line(&mut host).ok();
Ok(())
},
false,
))
.ok();
}

346
src/update.rs Normal file
View File

@ -0,0 +1,346 @@
use crate::config::configuration;
use crate::connui::ConnectionUI;
use crate::wezterm_version;
use anyhow::anyhow;
use http_req::request::{HttpVersion, Request};
use http_req::uri::Uri;
use regex::Regex;
use serde::*;
use std::collections::HashMap;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use termwiz::cell::{AttributeChange, Hyperlink, Underline};
use termwiz::surface::{Change, CursorShape};
#[derive(Debug, Deserialize, Clone)]
pub struct Release {
pub url: String,
pub body: String,
pub html_url: String,
pub tag_name: String,
pub assets: Vec<Asset>,
}
impl Release {
pub fn classify_assets(&self) -> HashMap<AssetKind, Asset> {
let mut map = HashMap::new();
for asset in &self.assets {
let kind = classify_asset_name(&asset.name);
map.insert(kind, asset.clone());
}
map
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Asset {
pub name: String,
pub size: usize,
pub url: String,
pub browser_download_url: String,
}
pub type DistVers = String;
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum AssetKind {
SourceCode,
AppImage,
AppImageZSync,
DebianDeb(DistVers),
UbuntuDeb(DistVers),
CentOSRpm(DistVers),
FedoraRpm(DistVers),
MacOSZip,
WindowsZip,
Unknown,
}
fn classify_asset_name(name: &str) -> AssetKind {
let winzip = Regex::new(r"WezTerm-windows-.*\.zip$").unwrap();
let maczip = Regex::new(r"WezTerm-macos-.*\.zip$").unwrap();
let appimage = Regex::new(r"WezTerm-.*\.AppImage$").unwrap();
let appimage_zsync = Regex::new(r"WezTerm-.*\.AppImage\.zsync$").unwrap();
let source = Regex::new(r"wezterm-.*src\.tar\.gz$").unwrap();
let rpm = Regex::new(r"wezterm-.*-1\.([a-z]+)(\d+)\.x86_64\.rpm$").unwrap();
for cap in rpm.captures_iter(name) {
match &cap[1] {
"fc" => return AssetKind::FedoraRpm(cap[2].to_string()),
"el" => return AssetKind::CentOSRpm(cap[2].to_string()),
_ => {}
}
}
let nightly_rpm = Regex::new(r"wezterm-nightly-(fedora|centos)(\d+)\.rpm$").unwrap();
for cap in nightly_rpm.captures_iter(name) {
match &cap[1] {
"fedora" => return AssetKind::FedoraRpm(cap[2].to_string()),
"centos" => return AssetKind::CentOSRpm(cap[2].to_string()),
_ => {}
}
}
let dot_deb = Regex::new(r"wezterm-.*\.(Ubuntu|Debian)([0-9.]+)\.deb$").unwrap();
for cap in dot_deb.captures_iter(name) {
match &cap[1] {
"Ubuntu" => return AssetKind::UbuntuDeb(cap[2].to_string()),
"Debian" => return AssetKind::DebianDeb(cap[2].to_string()),
_ => {}
}
}
if winzip.is_match(name) {
AssetKind::WindowsZip
} else if maczip.is_match(name) {
AssetKind::MacOSZip
} else if appimage.is_match(name) {
AssetKind::AppImage
} else if appimage_zsync.is_match(name) {
AssetKind::AppImageZSync
} else if source.is_match(name) {
AssetKind::SourceCode
} else {
AssetKind::Unknown
}
}
fn get_github_release_info(uri: &str) -> anyhow::Result<Release> {
let uri = uri.parse::<Uri>().expect("URL to be valid!?");
let mut latest = Vec::new();
let _res = Request::new(&uri)
.version(HttpVersion::Http10)
.header("User-Agent", &format!("wez/wezterm-{}", wezterm_version()))
.send(&mut latest)
.map_err(|e| anyhow!("failed to query github releases: {}", e))?;
/*
println!("Status: {} {}", _res.status_code(), _res.reason());
println!("{}", String::from_utf8_lossy(&latest));
*/
let latest: Release = serde_json::from_slice(&latest)?;
Ok(latest)
}
pub fn get_latest_release_info() -> anyhow::Result<Release> {
get_github_release_info("https://api.github.com/repos/wez/wezterm/releases/latest")
}
#[allow(unused)]
pub fn get_nightly_release_info() -> anyhow::Result<Release> {
get_github_release_info("https://api.github.com/repos/wez/wezterm/releases/tags/nightly")
}
lazy_static::lazy_static! {
static ref UPDATER_WINDOW: Mutex<Option<ConnectionUI>> = Mutex::new(None);
}
fn show_update_available(release: Release) {
let mut updater = UPDATER_WINDOW.lock().unwrap();
let ui = ConnectionUI::new_with_no_close_delay();
ui.title("WezTerm Update Available");
let install = "https://wezfurlong.org/wezterm/installation.html";
let change_log = "https://wezfurlong.org/wezterm/changelog.html";
let brief_blurb = release
.body
// The default for the release body is a series of newlines.
// Trim that so that it doesn't make the window look weird
.trim_end()
// Normalize any dos line endings that might have wound
// up in the body field...
.replace("\r\n", "\n")
// ... and then canonicalize the line endings for the terminal
.replace("\n", "\r\n");
ui.output(vec![
Change::CursorShape(CursorShape::Hidden),
Change::Attribute(AttributeChange::Hyperlink(Some(Arc::new(Hyperlink::new(
install,
))))),
format!("Version {} is now available!\r\n", release.tag_name).into(),
Change::Attribute(AttributeChange::Hyperlink(None)),
format!("(this is version {})\r\n", wezterm_version()).into(),
format!("{}\r\n", brief_blurb).into(),
Change::Attribute(AttributeChange::Hyperlink(Some(Arc::new(Hyperlink::new(
change_log,
))))),
Change::Attribute(AttributeChange::Underline(Underline::Single)),
"View Change Log\r\n".into(),
Change::Attribute(AttributeChange::Hyperlink(None)),
]);
let assets = release.classify_assets();
let appimage = assets.get(&AssetKind::AppImage);
if cfg!(target_os = "linux") && std::env::var_os("APPIMAGE").is_some() && appimage.is_some() {
ui.output(vec![
Change::Attribute(AttributeChange::Hyperlink(Some(Arc::new(Hyperlink::new(
&appimage.unwrap().browser_download_url,
))))),
Change::Attribute(AttributeChange::Underline(Underline::Single)),
format!("Download {}\r\n", appimage.unwrap().name).into(),
Change::Attribute(AttributeChange::Hyperlink(None)),
]);
} else {
ui.output(vec![
Change::Attribute(AttributeChange::Hyperlink(Some(Arc::new(Hyperlink::new(
install,
))))),
Change::Attribute(AttributeChange::Underline(Underline::Single)),
"Open Download Page\r\n".into(),
Change::Attribute(AttributeChange::Hyperlink(None)),
]);
}
updater.replace(ui);
}
fn update_checker() {
// Compute how long we should sleep for;
// if we've never checked, give it a few seconds after the first
// launch, otherwise compute the interval based on the time of
// the last check.
let config = configuration();
let update_interval = Duration::new(config.check_for_updates_interval_seconds, 0);
let initial_interval = Duration::new(10, 0);
let update_file_name = crate::config::RUNTIME_DIR.join("check_update");
let delay = update_file_name
.metadata()
.and_then(|metadata| metadata.modified())
.map_err(|_| ())
.and_then(|systime| {
let elapsed = systime.elapsed().unwrap_or(Duration::new(0, 0));
update_interval.checked_sub(elapsed).ok_or(())
})
.unwrap_or(initial_interval);
std::thread::sleep(delay);
loop {
if let Ok(latest) = get_latest_release_info() {
let current = crate::wezterm_version();
if latest.tag_name.as_str() > current {
log::info!(
"latest release {} is newer than current build {}",
latest.tag_name,
current
);
show_update_available(latest.clone());
}
}
crate::create_user_owned_dirs(update_file_name.parent().unwrap()).ok();
// Record the time of this check
if let Ok(mut f) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&update_file_name)
{
f.write(b"_").ok();
}
std::thread::sleep(update_interval);
}
}
pub fn start_update_checker() {
static CHECKER_STARTED: AtomicBool = AtomicBool::new(false);
if crate::frontend::has_gui_front_end() && configuration().check_for_updates {
if CHECKER_STARTED.compare_and_swap(false, true, Ordering::Relaxed) == false {
std::thread::Builder::new()
.name("update_checker".into())
.spawn(update_checker)
.expect("failed to spawn update checker thread");
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn classify_names() {
assert_eq!(
classify_asset_name("WezTerm-windows-20200505-090057-31c6155f.zip"),
AssetKind::WindowsZip
);
assert_eq!(
classify_asset_name("WezTerm-windows-nightly.zip"),
AssetKind::WindowsZip
);
assert_eq!(
classify_asset_name("WezTerm-macos-20200505-090057-31c6155f.zip"),
AssetKind::MacOSZip
);
assert_eq!(
classify_asset_name("WezTerm-macos-nightly.zip"),
AssetKind::MacOSZip
);
assert_eq!(
classify_asset_name("wezterm-20200505_090057_31c6155f-1.fc32.x86_64.rpm"),
AssetKind::FedoraRpm("32".into())
);
assert_eq!(
classify_asset_name("wezterm-nightly-fedora32.rpm"),
AssetKind::FedoraRpm("32".into())
);
assert_eq!(
classify_asset_name("wezterm-20200505_090057_31c6155f-1.fc31.x86_64.rpm"),
AssetKind::FedoraRpm("31".into())
);
assert_eq!(
classify_asset_name("wezterm-20200505_090057_31c6155f-1.el8.x86_64.rpm"),
AssetKind::CentOSRpm("8".into())
);
assert_eq!(
classify_asset_name("wezterm-20200505_090057_31c6155f-1.el7.x86_64.rpm"),
AssetKind::CentOSRpm("7".into())
);
assert_eq!(
classify_asset_name("wezterm-20200505-090057-31c6155f.Ubuntu20.04.tar.xz"),
AssetKind::Unknown
);
assert_eq!(
classify_asset_name("wezterm-20200505-090057-31c6155f.Ubuntu20.04.deb"),
AssetKind::UbuntuDeb("20.04".into())
);
assert_eq!(
classify_asset_name("wezterm-20200505-090057-31c6155f.Ubuntu19.10.deb"),
AssetKind::UbuntuDeb("19.10".into())
);
assert_eq!(
classify_asset_name("wezterm-20200505-090057-31c6155f.Debian9.12.deb"),
AssetKind::DebianDeb("9.12".into())
);
assert_eq!(
classify_asset_name("wezterm-20200505-090057-31c6155f.Debian10.deb"),
AssetKind::DebianDeb("10".into())
);
assert_eq!(
classify_asset_name("WezTerm-20200505-090057-31c6155f-Ubuntu16.04.AppImage.zsync"),
AssetKind::AppImageZSync
);
assert_eq!(
classify_asset_name("WezTerm-20200505-090057-31c6155f-Ubuntu16.04.AppImage"),
AssetKind::AppImage
);
assert_eq!(
classify_asset_name("wezterm-20200505-090057-31c6155f-src.tar.gz"),
AssetKind::SourceCode
);
}
}