Proxy self update

This commit is contained in:
IQuant 2024-05-24 00:41:55 +03:00
parent 42e390cdb9
commit 94432c21e1
6 changed files with 247 additions and 18 deletions

34
noita-proxy/Cargo.lock generated
View file

@ -943,6 +943,15 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.1.0"
@ -1398,6 +1407,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.9.0"
@ -1675,7 +1693,7 @@ checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "noita-proxy"
version = "0.4.1"
version = "0.5.0"
dependencies = [
"bitcode",
"clipboard",
@ -1686,6 +1704,7 @@ dependencies = [
"poll-promise",
"rand",
"reqwest",
"self-replace",
"serde",
"serde_json",
"socket2",
@ -2388,6 +2407,17 @@ dependencies = [
"libc",
]
[[package]]
name = "self-replace"
version = "1.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525db198616b2bcd0f245daf7bfd8130222f7ee6af9ff9984c19a61bf1160c55"
dependencies = [
"fastrand 1.9.0",
"tempfile",
"windows-sys 0.48.0",
]
[[package]]
name = "serde"
version = "1.0.199"
@ -2638,7 +2668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"fastrand 2.1.0",
"rustix",
"windows-sys 0.52.0",
]

View file

@ -3,7 +3,7 @@ members = ["tangled"]
[package]
name = "noita-proxy"
version = "0.4.1"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -28,6 +28,7 @@ serde_json = "1.0.117"
thiserror = "1.0.61"
poll-promise = "0.3.0"
zip = "1.3.1"
self-replace = "1.3.7"
[profile.dev]
opt-level = 1

View file

@ -4,8 +4,9 @@ use std::{
use bitcode::{Decode, Encode};
use clipboard::{ClipboardContext, ClipboardProvider};
use eframe::egui::{self, Align2, Color32, Layout};
use eframe::egui::{self, Align2, Color32};
use mod_manager::{Modmanager, ModmanagerSettings};
use self_update::SelfUpdateManager;
use serde::{Deserialize, Serialize};
use steamworks::{LobbyId, SteamAPIInitError};
use tangled::Peer;
@ -13,6 +14,7 @@ use tracing::info;
pub mod messages;
mod mod_manager;
mod self_update;
pub mod releases;
#[derive(Debug, Decode, Encode, Clone)]
@ -28,6 +30,7 @@ enum AppState {
ModManager,
Netman { netman: Arc<net::NetManager> },
Error { message: String },
SelfUpdate,
}
struct SteamState {
@ -57,8 +60,6 @@ struct AppSavedState {
addr: String,
debug_mode: bool,
use_constant_seed: bool,
}
impl Default for AppSavedState {
@ -76,7 +77,8 @@ pub struct App {
modmanager: Modmanager,
steam_state: Result<SteamState, SteamAPIInitError>,
saved_state: AppSavedState,
modmanager_settings: ModmanagerSettings
modmanager_settings: ModmanagerSettings,
self_update: SelfUpdateManager,
}
const MODMANAGER: &str = "modman";
@ -94,6 +96,7 @@ impl App {
modmanager: Modmanager::default(),
steam_state: SteamState::new(),saved_state,
modmanager_settings,
self_update: SelfUpdateManager::new(),
}
}
@ -197,9 +200,10 @@ impl eframe::App for App {
ui.label(format!("Could not init steam networking: {}", err));
}
}
ui.with_layout(Layout::right_to_left(egui::Align::Max), |ui| {
ui.label(concat!("Noita Proxy version ", env!("CARGO_PKG_VERSION")))
})
self.self_update.display_version(ui);
if self.self_update.request_update {
self.state = AppState::SelfUpdate;
}
});
}
AppState::Netman { netman } => {
@ -262,6 +266,12 @@ impl eframe::App for App {
self.state = AppState::Connect;
}
},
AppState::SelfUpdate => {
egui::Window::new("Self update").auto_sized().anchor(Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
self.self_update.self_update(ui);
});
},
};
}

View file

@ -262,11 +262,11 @@ fn mod_downloader_for(
.and_then(|asset| asset.download(&client, &download_path))
}
fn extract_and_remove_zip(zip_file: PathBuf, extact_to: PathBuf) -> Result<(), ReleasesError> {
fn extract_and_remove_zip(zip_file: PathBuf, extract_to: PathBuf) -> Result<(), ReleasesError> {
let reader = File::open(&zip_file)?;
let mut zip = zip::ZipArchive::new(reader)?;
info!("Extracting zip file");
zip.extract(extact_to)?;
zip.extract(extract_to)?;
info!("Zip file extracted");
fs::remove_file(&zip_file).ok();
Ok(())

View file

@ -1,4 +1,5 @@
use std::{
fmt::Display,
fs::File,
io::{self, Read, Write},
path::{Path, PathBuf},
@ -45,7 +46,7 @@ impl From<ZipError> for ReleasesError {
#[derive(Debug, Deserialize)]
pub struct Release {
pub tag_name: String,
pub tag_name: Tag,
assets_url: String,
}
@ -157,9 +158,10 @@ impl AssetList {
}
}
#[derive(Debug, Deserialize)]
pub struct Tag(String);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct Version {
pub major: u32,
pub minor: u32,
@ -185,6 +187,15 @@ impl Version {
pub fn current() -> Self {
Self::parse_from_string(env!("CARGO_PKG_VERSION")).expect("can always parse crate version")
}
pub fn parse_from_tag(tag: Tag) -> Option<Self> {
Self::parse_from_string(tag.0.strip_prefix("v")?)
}
}
impl Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl From<Version> for Tag {

View file

@ -0,0 +1,177 @@
use std::{
cmp::Ordering,
fs::{self, File},
io,
path::{Path, PathBuf},
};
use eframe::egui::{Align, Layout, Ui};
use poll_promise::Promise;
use reqwest::blocking::Client;
use tracing::info;
use crate::releases::{get_latest_release, Downloader, ReleasesError, Version};
struct VersionCheckResult {
newest: Version,
ord: Ordering,
}
enum State {
Initial,
Download(Promise<Result<Downloader, ReleasesError>>),
ReleasesError(ReleasesError),
Unpack(Promise<Result<(), ReleasesError>>),
}
pub struct SelfUpdateManager {
latest_check: Promise<Option<VersionCheckResult>>,
pub request_update: bool,
state: State,
}
impl SelfUpdateManager {
pub fn new() -> Self {
let latest_check = Promise::spawn_thread("version check", || {
let client = Client::new();
get_latest_release(&client)
.map(|release| release.tag_name)
.ok()
.and_then(|tag| Version::parse_from_tag(tag))
.map(|ver| VersionCheckResult {
ord: ver.cmp(&Version::current()),
newest: ver,
})
});
Self {
latest_check,
request_update: false,
state: State::Initial,
}
}
pub fn display_version(&mut self, ui: &mut Ui) {
ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
match self.latest_check.ready() {
Some(&Some(VersionCheckResult {
newest: _,
ord: Ordering::Equal,
})) => {
ui.label("(latest)");
}
Some(&Some(VersionCheckResult { newest, ord: _ })) => {
if ui
.small_button(format!("Update available to {}", newest))
.clicked()
{
self.request_update = true;
}
}
Some(None) => {
ui.label("(could not check for updates)");
}
None => {
ui.label("(checking for updates)");
}
}
ui.label(concat!("Noita Proxy version v", env!("CARGO_PKG_VERSION"),));
});
}
pub fn self_update(&mut self, ui: &mut Ui) {
let ctx = ui.ctx();
match &self.state {
State::Initial => {
if ui.button("Confirm update").clicked() {
let promise = Promise::spawn_thread("get_release", || {
proxy_downloader_for("newer.zip".into())
});
self.state = State::Download(promise)
}
}
State::Download(promise) => match promise.ready() {
Some(Ok(downloader)) => {
downloader.show_progress(ui);
match downloader.ready() {
Some(Ok(_)) => {
let path = downloader.path().to_path_buf();
let promise: Promise<Result<(), ReleasesError>> =
Promise::spawn_thread("unpack", move || {
extract_and_remove_zip(path)
});
self.state = State::Unpack(promise);
}
Some(Err(err)) => self.state = State::ReleasesError(err.clone()),
None => {}
}
}
Some(Err(err)) => self.state = State::ReleasesError(err.clone()),
None => {
ui.label("Receiving release info...");
ui.spinner();
}
},
State::Unpack(promise) => match promise.ready() {
Some(Ok(_)) => {
ui.label("Proxy updated! Restart it now.");
}
Some(Err(err)) => {
ui.label(format!("Could not update proxy: {}", err));
}
None => {
ctx.request_repaint();
ui.label("Unpacking...");
ui.spinner();
}
},
State::ReleasesError(err) => {
ui.label(format!("Encountered an error: {}", err));
}
}
}
}
fn proxy_asset_name() -> &'static str {
if cfg!(target_os = "windows") {
"noita-proxy-win.zip"
} else {
"noita-proxy-linux.zip"
}
}
fn proxy_bin_name() -> &'static str {
if cfg!(target_os = "windows") {
"noita_proxy.exe"
} else {
"noita_proxy.x86_64"
}
}
fn proxy_downloader_for(download_path: PathBuf) -> Result<Downloader, ReleasesError> {
let client = reqwest::blocking::Client::builder()
.timeout(None)
.build()
.unwrap();
get_latest_release(&client)
.and_then(|release| release.get_release_assets(&client))
.and_then(|asset_list| asset_list.find_by_name(proxy_asset_name()).cloned())
.and_then(|asset| asset.download(&client, &download_path))
}
fn extract_and_remove_zip(zip_file: PathBuf) -> Result<(), ReleasesError> {
let extract_to = Path::new("tmp.exec");
let bin_name = proxy_bin_name();
let reader = File::open(&zip_file)?;
let mut zip = zip::ZipArchive::new(reader)?;
info!("Extracting zip file");
let mut src = zip.by_name(&bin_name)?;
let mut dst = File::create(extract_to)?;
io::copy(&mut src, &mut dst)?;
self_replace::self_replace(extract_to)?;
info!("Zip file extracted");
fs::remove_file(&zip_file).ok();
fs::remove_file(extract_to).ok();
Ok(())
}