mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2024-12-22 19:15:34 +00:00
Add self-updating feature (closes #2)
This commit is contained in:
parent
b6fba61080
commit
351c4c7c1f
|
@ -60,6 +60,9 @@ impl BaseAttributes {
|
|||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
pub installing_message: String,
|
||||
/// URL to a new updater, if required
|
||||
#[serde(default)]
|
||||
pub new_tool: Option<String>,
|
||||
pub packages: Vec<PackageDescription>,
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ use reqwest::Client;
|
|||
/// Builds a customised HTTP client.
|
||||
pub fn build_client() -> Result<Client, String> {
|
||||
Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(8))
|
||||
.build()
|
||||
.map_err(|x| format!("Unable to build cient: {:?}", x))
|
||||
}
|
||||
|
|
130
src/installer.rs
130
src/installer.rs
|
@ -5,7 +5,9 @@
|
|||
use serde_json;
|
||||
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use std::env;
|
||||
use std::env::var;
|
||||
|
||||
use std::path::Path;
|
||||
|
@ -13,6 +15,12 @@ use std::path::PathBuf;
|
|||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use std::io::copy;
|
||||
use std::io::Cursor;
|
||||
|
||||
use std::process::exit;
|
||||
use std::process::Command;
|
||||
|
||||
use config::BaseAttributes;
|
||||
use config::Config;
|
||||
|
||||
|
@ -20,6 +28,7 @@ use sources::types::Version;
|
|||
|
||||
use tasks::install::InstallTask;
|
||||
use tasks::uninstall::UninstallTask;
|
||||
use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
|
||||
use tasks::DependencyTree;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
@ -27,7 +36,10 @@ use logging::LoggingErrors;
|
|||
use dirs::home_dir;
|
||||
|
||||
use std::fs::remove_file;
|
||||
use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
|
||||
|
||||
use http;
|
||||
|
||||
use number_prefix::{decimal_prefix, Prefixed, Standalone};
|
||||
|
||||
/// A message thrown during the installation of packages.
|
||||
#[derive(Serialize)]
|
||||
|
@ -206,6 +218,122 @@ impl InstallerFramework {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifies that the config has all requirements met (no need to update the
|
||||
/// updater, for example). This will terminate if this is the case after applying
|
||||
/// the correct actions.
|
||||
pub fn update_updater(&mut self, messages: &Sender<InstallMessage>) -> Result<(), String> {
|
||||
let tool = self
|
||||
.config
|
||||
.as_ref()
|
||||
.log_expect("Config should exist by now")
|
||||
.new_tool
|
||||
.as_ref()
|
||||
.log_expect("Frontend asked for updater update when one doesn't exist");
|
||||
|
||||
let mut downloaded = 0;
|
||||
let mut data_storage: Vec<u8> = Vec::new();
|
||||
|
||||
http::stream_file(tool, |data, size| {
|
||||
{
|
||||
data_storage.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
downloaded += data.len();
|
||||
|
||||
let percentage = if size == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(downloaded as f64) / (size as f64)
|
||||
};
|
||||
|
||||
// Pretty print data volumes
|
||||
let pretty_current = match decimal_prefix(downloaded as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
let pretty_total = match decimal_prefix(size as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
|
||||
if let Err(v) = messages.send(InstallMessage::Status(
|
||||
format!(
|
||||
"Downloading self-update ({} of {})...",
|
||||
pretty_current, pretty_total
|
||||
),
|
||||
percentage as _,
|
||||
)) {
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
})?;
|
||||
|
||||
info!("Launching new updater...");
|
||||
|
||||
// Save to file in current dir
|
||||
let current_exe = env::current_exe().log_expect("Current executable could not be found");
|
||||
let path = current_exe
|
||||
.parent()
|
||||
.log_expect("Parent directory of executable could not be found");
|
||||
|
||||
let platform_extension = if cfg!(windows) {
|
||||
"maintenancetool_new.exe"
|
||||
} else {
|
||||
"maintenancetool_new"
|
||||
};
|
||||
|
||||
let new_app = path.join(platform_extension);
|
||||
|
||||
let mut file_metadata = OpenOptions::new();
|
||||
file_metadata.write(true).create(true);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
file_metadata.mode(0o770);
|
||||
}
|
||||
|
||||
{
|
||||
let mut new_app_file = match file_metadata.open(&new_app) {
|
||||
Ok(v) => v,
|
||||
Err(v) => return Err(format!("Unable to open installer binary: {:?}", v)),
|
||||
};
|
||||
|
||||
if let Err(v) = copy(&mut Cursor::new(data_storage), &mut new_app_file) {
|
||||
return Err(format!("Unable to copy installer binary: {:?}", v));
|
||||
}
|
||||
}
|
||||
|
||||
// Save current command line arguments
|
||||
let args_file = path.join("args.json");
|
||||
let args: Vec<String> = env::args_os()
|
||||
.map(|x| {
|
||||
x.to_str()
|
||||
.log_expect("Unable to convert argument to String")
|
||||
.to_string()
|
||||
}).collect();
|
||||
|
||||
{
|
||||
let new_app_file = match File::create(&args_file) {
|
||||
Ok(v) => v,
|
||||
Err(v) => return Err(format!("Unable to open args file: {:?}", v)),
|
||||
};
|
||||
|
||||
serde_json::to_writer(new_app_file, &args).log_expect("Unable to write args");
|
||||
}
|
||||
|
||||
let current_exe = env::current_exe().log_expect("Current executable could not be found");
|
||||
|
||||
// Launch this new process
|
||||
Command::new(new_app)
|
||||
.arg("--swap")
|
||||
.arg(current_exe)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/// Saves the applications database.
|
||||
pub fn save_database(&self) -> Result<(), String> {
|
||||
// We have to have a install path for us to be able to do anything
|
||||
|
|
87
src/main.rs
87
src/main.rs
|
@ -61,12 +61,21 @@ use nfd::Response;
|
|||
|
||||
use rest::WebServer;
|
||||
|
||||
use std::net::TcpListener;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use std::net::TcpListener;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::process::exit;
|
||||
use std::process::Command;
|
||||
use std::{thread, time};
|
||||
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use clap::App;
|
||||
|
@ -91,16 +100,26 @@ fn main() {
|
|||
|
||||
let app_name = config.name.clone();
|
||||
|
||||
let matches = App::new(format!("{} installer", app_name))
|
||||
let app_about = format!("An interactive installer for {}", app_name);
|
||||
let app = App::new(format!("{} installer", app_name))
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about(format!("An interactive installer for {}", app_name).as_ref())
|
||||
.about(app_about.as_ref())
|
||||
.arg(
|
||||
Arg::with_name("launcher")
|
||||
.long("launcher")
|
||||
.value_name("TARGET")
|
||||
.help("Launches the specified executable after checking for updates")
|
||||
.takes_value(true),
|
||||
).get_matches();
|
||||
).arg(
|
||||
Arg::with_name("swap")
|
||||
.long("swap")
|
||||
.value_name("TARGET")
|
||||
.help("Internal usage - swaps around a new installer executable")
|
||||
.takes_value(true),
|
||||
);
|
||||
|
||||
let reinterpret_app = app.clone(); // In case a reparse is needed
|
||||
let mut matches = app.get_matches();
|
||||
|
||||
info!("{} installer", app_name);
|
||||
|
||||
|
@ -108,6 +127,66 @@ fn main() {
|
|||
let current_path = current_exe
|
||||
.parent()
|
||||
.log_expect("Parent directory of executable could not be found");
|
||||
|
||||
// Check to see if we are currently in a self-update
|
||||
if let Some(to_path) = matches.value_of("swap") {
|
||||
let to_path = PathBuf::from(to_path);
|
||||
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
info!(
|
||||
"Swapping installer from {} to {}",
|
||||
current_exe.display(),
|
||||
to_path.display()
|
||||
);
|
||||
|
||||
if cfg!(windows) {
|
||||
use std::fs::copy;
|
||||
|
||||
copy(¤t_exe, &to_path).log_expect("Unable to copy new installer");
|
||||
} else {
|
||||
use std::fs::rename;
|
||||
|
||||
rename(¤t_exe, &to_path).log_expect("Unable to move new installer");
|
||||
}
|
||||
|
||||
Command::new(to_path)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
let args_file = current_path.join("args.json");
|
||||
|
||||
if args_file.exists() {
|
||||
let database: Vec<String> = {
|
||||
let metadata_file =
|
||||
File::open(&args_file).log_expect("Unable to open args file handle");
|
||||
|
||||
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
|
||||
};
|
||||
|
||||
matches = reinterpret_app.get_matches_from(database);
|
||||
|
||||
info!("Reparsed command line arguments from original instance");
|
||||
remove_file(args_file).log_expect("Unable to clean up args file");
|
||||
|
||||
if cfg!(windows) {
|
||||
let updater_executable = current_path.join("maintenancetool.exe");
|
||||
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
if updater_executable.exists() {
|
||||
remove_file(updater_executable)
|
||||
.log_expect("Unable to clean up previous updater file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load in metadata as to learn about the environment
|
||||
let metadata_file = current_path.join("metadata.json");
|
||||
let mut framework = if metadata_file.exists() {
|
||||
info!("Using pre-existing metadata file: {:?}", metadata_file);
|
||||
|
|
|
@ -11,7 +11,6 @@ mod natives {
|
|||
use logging::LoggingErrors;
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/interop.rs"));
|
||||
|
@ -63,7 +62,12 @@ mod natives {
|
|||
}
|
||||
|
||||
/// Cleans up the installer
|
||||
pub fn burn_on_exit(path: &PathBuf) {
|
||||
pub fn burn_on_exit() {
|
||||
let current_exe = env::current_exe().log_expect("Current executable could not be found");
|
||||
let path = current_exe
|
||||
.parent()
|
||||
.log_expect("Parent directory of executable could not be found");
|
||||
|
||||
// Need a cmd workaround here.
|
||||
let tool = path.join("maintenancetool.exe");
|
||||
let tool = tool
|
||||
|
@ -77,7 +81,7 @@ mod natives {
|
|||
.log_expect("Unable to convert log path to string")
|
||||
.replace(" ", "\\ ");
|
||||
|
||||
let target_arguments = format!("ping 127.0.0.1 -n 6 > nul && del {} {}", tool, log);
|
||||
let target_arguments = format!("ping 127.0.0.1 -n 3 > nul && del {} {}", tool, log);
|
||||
|
||||
info!("Launching cmd with {:?}", target_arguments);
|
||||
|
||||
|
@ -108,7 +112,13 @@ mod natives {
|
|||
}
|
||||
|
||||
/// Cleans up the installer
|
||||
pub fn burn_on_exit(path: &PathBuf) {
|
||||
pub fn burn_on_exit() {
|
||||
let current_exe =
|
||||
std::env::current_exe().log_expect("Current executable could not be found");
|
||||
let path = current_exe
|
||||
.parent()
|
||||
.log_expect("Parent directory of executable could not be found");
|
||||
|
||||
// Thank god for *nix platforms
|
||||
if let Err(e) = remove_file(path.join("/maintenancetool")) {
|
||||
// No regular logging now.
|
||||
|
|
62
src/rest.rs
62
src/rest.rs
|
@ -207,12 +207,7 @@ impl Service for WebService {
|
|||
}
|
||||
|
||||
if framework.burn_after_exit {
|
||||
let path = framework
|
||||
.install_path
|
||||
.as_ref()
|
||||
.log_expect("No install path when one should have existed?");
|
||||
|
||||
native::burn_on_exit(path);
|
||||
native::burn_on_exit();
|
||||
}
|
||||
|
||||
exit(0);
|
||||
|
@ -289,6 +284,61 @@ impl Service for WebService {
|
|||
.with_body(rx)
|
||||
}));
|
||||
}
|
||||
// Updates the installer
|
||||
(&Post, "/api/update-updater") => {
|
||||
// We need to bit of pipelining to get this to work
|
||||
let framework = self.framework.clone();
|
||||
|
||||
return Box::new(req.body().concat2().map(move |_b| {
|
||||
let (sender, receiver) = channel();
|
||||
let (tx, rx) = hyper::Body::pair();
|
||||
|
||||
// Startup a thread to do this operation for us
|
||||
thread::spawn(move || {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Err(v) = framework.update_updater(&sender) {
|
||||
error!("Self-update error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send self-update error: {:?}", v);
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn a thread for transforming messages to chunk messages
|
||||
thread::spawn(move || {
|
||||
let mut tx = tx;
|
||||
loop {
|
||||
let response = receiver
|
||||
.recv()
|
||||
.log_expect("Failed to receive message from runner thread");
|
||||
|
||||
if let InstallMessage::EOF = response {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut response = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON logging response payload");
|
||||
response.push('\n');
|
||||
tx = tx
|
||||
.send(Ok(response.into_bytes().into()))
|
||||
.wait()
|
||||
.log_expect("Failed to write JSON response payload to client");
|
||||
}
|
||||
});
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
//.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(rx)
|
||||
}));
|
||||
}
|
||||
// Streams the installation of a particular set of packages
|
||||
(&Post, "/api/start-install") => {
|
||||
// We need to bit of pipelining to get this to work
|
||||
|
|
|
@ -22,6 +22,13 @@ body, div, span, h1, h2, h3, h4, h5, h6 {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
pre {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.tile.is-child > .box {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ function ajax(path, successCallback, failCallback, data) {
|
|||
failCallback = defaultFailHandler;
|
||||
}
|
||||
|
||||
console.log("Making HTTP request to " + path);
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
req.addEventListener("load", function() {
|
||||
|
@ -69,6 +71,8 @@ function ajax(path, successCallback, failCallback, data) {
|
|||
function stream_ajax(path, callback, successCallback, failCallback, data) {
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
console.log("Making streaming HTTP request to " + path);
|
||||
|
||||
req.addEventListener("load", function() {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200) {
|
||||
|
|
|
@ -26,6 +26,17 @@ for (var i = 0; i < methods.length; i++) {
|
|||
intercept(methods[i]);
|
||||
}
|
||||
|
||||
// Disable F5
|
||||
function disable_shortcuts(e) {
|
||||
switch (e.keyCode) {
|
||||
case 116: // F5
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", disable_shortcuts);
|
||||
|
||||
document.getElementById("window-title").innerText = base_attributes.name + " Installer";
|
||||
|
||||
function selectFileCallback(name) {
|
||||
|
|
|
@ -44,6 +44,12 @@ const DownloadConfig = {
|
|||
});
|
||||
},
|
||||
choose_next_state: function() {
|
||||
// Update the updater if needed
|
||||
if (app.config.new_tool) {
|
||||
router.push("/install/updater");
|
||||
return;
|
||||
}
|
||||
|
||||
if (app.metadata.preexisting_install) {
|
||||
app.install_location = app.metadata.install_path;
|
||||
|
||||
|
@ -153,6 +159,7 @@ const InstallPackages = {
|
|||
<div class="column has-padding">
|
||||
<h4 class="subtitle" v-if="$root.$data.metadata.is_launcher">Checking for updates...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_uninstall">Uninstalling...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_updater_update">Downloading update for updater...</h4>
|
||||
<h4 class="subtitle" v-else>Installing...</h4>
|
||||
<div v-html="$root.$data.config.installing_message"></div>
|
||||
<br />
|
||||
|
@ -168,11 +175,14 @@ const InstallPackages = {
|
|||
progress: 0.0,
|
||||
progress_message: "Please wait...",
|
||||
is_uninstall: false,
|
||||
is_updater_update: false,
|
||||
failed_with_error: false
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.is_uninstall = this.$route.params.kind === "uninstall";
|
||||
this.is_updater_update = this.$route.params.kind === "updater";
|
||||
console.log("Installer kind: " + this.$route.params.kind);
|
||||
this.install();
|
||||
},
|
||||
methods: {
|
||||
|
@ -190,8 +200,15 @@ const InstallPackages = {
|
|||
|
||||
var that = this; // IE workaround
|
||||
|
||||
stream_ajax(this.is_uninstall ? "/api/uninstall" :
|
||||
"/api/start-install", function(line) {
|
||||
var targetUrl = "/api/start-install";
|
||||
if (this.is_uninstall) {
|
||||
targetUrl = "/api/uninstall";
|
||||
}
|
||||
if (this.is_updater_update) {
|
||||
targetUrl = "/api/update-updater";
|
||||
}
|
||||
|
||||
stream_ajax(targetUrl, function(line) {
|
||||
if (line.hasOwnProperty("Status")) {
|
||||
that.progress_message = line.Status[0];
|
||||
that.progress = line.Status[1] * 100;
|
||||
|
@ -206,10 +223,23 @@ const InstallPackages = {
|
|||
}
|
||||
}
|
||||
}, function(e) {
|
||||
if (app.metadata.is_launcher) {
|
||||
app.exit();
|
||||
} else if (!that.failed_with_error) {
|
||||
router.push("/complete");
|
||||
if (that.is_updater_update) {
|
||||
// Continue with what we were doing
|
||||
if (app.metadata.is_launcher) {
|
||||
router.replace("/install/regular");
|
||||
} else {
|
||||
if (app.metadata.preexisting_install) {
|
||||
router.replace("/modify");
|
||||
} else {
|
||||
router.replace("/packages");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (app.metadata.is_launcher) {
|
||||
app.exit();
|
||||
} else if (!that.failed_with_error) {
|
||||
router.replace("/complete");
|
||||
}
|
||||
}
|
||||
}, undefined, results);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue