Modify installer to be more stateful

This commit is contained in:
James Lonie 2018-05-03 13:30:58 +10:00
parent 7b184b24ff
commit dae36a15b9
6 changed files with 120 additions and 40 deletions

View File

@ -5,7 +5,6 @@ installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff w
[[packages]] [[packages]]
name = "yuzu Nightly" name = "yuzu Nightly"
description = "The nightly build of yuzu contains already reviewed and tested features." description = "The nightly build of yuzu contains already reviewed and tested features."
default = true
[packages.source] [packages.source]
name = "github" name = "github"
match = "^yuzu-#PLATFORM#(-mingw)?-[0-9]*-[0-9a-f]*.zip$" match = "^yuzu-#PLATFORM#(-mingw)?-[0-9]*-[0-9a-f]*.zip$"
@ -20,3 +19,13 @@ description = "The canary build of yuzu has additional features that are still w
match = "^yuzu-#PLATFORM#(-mingw)?-[0-9]*-[0-9a-f]*.zip$" match = "^yuzu-#PLATFORM#(-mingw)?-[0-9]*-[0-9a-f]*.zip$"
[packages.source.config] [packages.source.config]
repo = "yuzu-emu/yuzu-canary" repo = "yuzu-emu/yuzu-canary"
[[packages]]
name = "Test package"
description = "Just a testing package"
default = true
[packages.source]
name = "github"
match = "^TestPackage.zip$"
[packages.source.config]
repo = "j-selby/test-installer"

View File

@ -19,11 +19,11 @@ use std::env::var;
use std::env::current_exe; use std::env::current_exe;
use std::env::consts::OS; use std::env::consts::OS;
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::io::Cursor; use std::io::Cursor;
use std::io::copy; use std::io::copy;
use std::io::Write;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
@ -47,6 +47,9 @@ pub enum InstallMessage {
/// etc. /// etc.
pub struct InstallerFramework { pub struct InstallerFramework {
config: Config, config: Config,
database: Vec<LocalInstallation>,
install_path: Option<String>,
preexisting_install: bool
} }
/// Used to track the amount of data that has been downloaded during a HTTP request. /// Used to track the amount of data that has been downloaded during a HTTP request.
@ -83,13 +86,21 @@ impl InstallerFramework {
} }
/// Sends a request for something to be installed. /// Sends a request for something to be installed.
/// items: Array of named packages to be installed
/// messages: Channel used to send progress messages
/// fresh_install: If the install directory must be empty
pub fn install( pub fn install(
&self, &mut self,
items: Vec<String>, items: Vec<String>,
path: &str,
messages: &Sender<InstallMessage>, messages: &Sender<InstallMessage>,
fresh_install: bool
) -> Result<(), String> { ) -> Result<(), String> {
// TODO: Error handling // We have to have a install path for us to be able to do anything
let path = match self.install_path.clone() {
Some(v) => v,
None => return Err(format!("No install directory for installer"))
};
println!("Framework: Installing {:?} to {}", items, path); println!("Framework: Installing {:?} to {}", items, path);
// Create our install directory // Create our install directory
@ -106,13 +117,15 @@ impl InstallerFramework {
} }
// Make sure it is empty // Make sure it is empty
let paths = match read_dir(&path) { if fresh_install {
Ok(v) => v, let paths = match read_dir(&path) {
Err(v) => return Err(format!("Failed to read install destination: {:?}", v)), Ok(v) => v,
}; Err(v) => return Err(format!("Failed to read install destination: {:?}", v)),
};
if paths.count() != 0 { if paths.count() != 0 {
return Err(format!("Install destination is not empty.")); return Err(format!("Install destination is not empty."));
}
} }
// Resolve items in config // Resolve items in config
@ -126,12 +139,12 @@ impl InstallerFramework {
println!("Resolved to {:?}", to_install); println!("Resolved to {:?}", to_install);
// TODO: Uninstall pre-existing packages
// Install packages // Install packages
let mut count = 0.0 as f64; let mut count = 0.0 as f64;
let max = to_install.len() as f64; let max = to_install.len() as f64;
let mut installed_packages = Vec::new();
for package in to_install.iter() { for package in to_install.iter() {
let base_package_percentage = count / max; let base_package_percentage = count / max;
let base_package_range = ((count + 1.0) / max) - base_package_percentage; let base_package_range = ((count + 1.0) / max) - base_package_percentage;
@ -165,6 +178,8 @@ impl InstallerFramework {
Err(v) => return Err(format!("An error occured while compiling regex: {:?}", v)), Err(v) => return Err(format!("An error occured while compiling regex: {:?}", v)),
}; };
println!("Releases: {:?}", results);
// Find the latest release in here // Find the latest release in here
let latest_result = results let latest_result = results
.into_iter() .into_iter()
@ -304,7 +319,8 @@ impl InstallerFramework {
} }
// Save metadata about this package // Save metadata about this package
installed_packages.push(LocalInstallation { // TODO: Check if this package already exists
self.database.push(LocalInstallation {
name: package.name.to_owned(), name: package.name.to_owned(),
version: latest_version, version: latest_version,
files: installed_files, files: installed_files,
@ -313,22 +329,7 @@ impl InstallerFramework {
count += 1.0; count += 1.0;
} }
// Make copy of metadata self.save_database()?;
// TODO: Combine with already installed database, if needed
let metadata_compiled = serde_json::to_string(&installed_packages).unwrap();
{
let metadata_path = path.join("metadata.json");
let mut metadata_file = match File::create(metadata_path) {
Ok(v) => v,
Err(v) => return Err(format!("Unable to open file handle: {:?}", v)),
};
match metadata_file.write_all(metadata_compiled.as_bytes()) {
Ok(v) => v,
Err(v) => return Err(format!("Unable to write to file: {:?}", v)),
};
}
// Copy installer binary to target directory // Copy installer binary to target directory
messages messages
@ -369,8 +370,63 @@ impl InstallerFramework {
Ok(()) Ok(())
} }
/// 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
let path = match self.install_path.clone() {
Some(v) => v,
None => return Err(format!("No install directory for installer"))
};
let metadata_path = Path::new(&path).join("metadata.json");
let metadata_file = match File::create(metadata_path) {
Ok(v) => v,
Err(v) => return Err(format!("Unable to open file handle: {:?}", v)),
};
match serde_json::to_writer(metadata_file, &self.database) {
Ok(v) => v,
Err(v) => return Err(format!("Unable to write to file: {:?}", v)),
};
Ok(())
}
/// Configures this installer to install to the specified location.
/// If there was a currently configured install path, this will be left as-is.
pub fn set_install_dir(&mut self, dir : &str) {
self.install_path = Some(dir.to_owned());
}
/// Creates a new instance of the Installer Framework with a specified Config. /// Creates a new instance of the Installer Framework with a specified Config.
pub fn new(config: Config) -> Self { pub fn new(config: Config) -> Self {
InstallerFramework { config } InstallerFramework {
config,
database : Vec::new(),
install_path : None,
preexisting_install : false
}
}
/// Creates a new instance of the Installer Framework with a specified Config, managing
/// a pre-existing installation.
pub fn new_with_db(config: Config, install_path : String) -> Result<Self, String> {
let metadata_path = Path::new(&install_path).join("metadata.json");
let metadata_file = match File::open(metadata_path) {
Ok(v) => v,
Err(v) => return Err(format!("Unable to open file handle: {:?}", v)),
};
let database : Vec<LocalInstallation> = match serde_json::from_reader(metadata_file) {
Ok(v) => v,
Err(v) => return Err(format!("Unable to read metadata file: {:?}", v)),
};
Ok(InstallerFramework {
config,
database,
install_path : Some(install_path),
preexisting_install : true
})
} }
} }

View File

@ -38,6 +38,7 @@ use config::Config;
use installer::InstallerFramework; use installer::InstallerFramework;
use rest::WebServer; use rest::WebServer;
use std::path::Path;
// TODO: Fetch this over a HTTP request? // TODO: Fetch this over a HTTP request?
static RAW_CONFIG: &'static str = include_str!("../config.toml"); static RAW_CONFIG: &'static str = include_str!("../config.toml");
@ -47,7 +48,12 @@ fn main() {
let app_name = config.general.name.clone(); let app_name = config.general.name.clone();
let framework = InstallerFramework::new(config); let metadata_file = Path::new("metadata.json");
let framework = if metadata_file.exists() {
InstallerFramework::new_with_db(config, format!("./")).unwrap()
} else {
InstallerFramework::new(config)
};
let server = WebServer::new(framework).unwrap(); let server = WebServer::new(framework).unwrap();

View File

@ -31,6 +31,7 @@ use assets;
use installer::InstallerFramework; use installer::InstallerFramework;
use installer::InstallMessage; use installer::InstallMessage;
use std::sync::RwLock;
#[derive(Serialize)] #[derive(Serialize)]
struct FileSelection { struct FileSelection {
@ -63,7 +64,7 @@ impl WebServer {
let (sender, receiver) = channel(); let (sender, receiver) = channel();
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
let shared_framework = Arc::new(framework); let shared_framework = Arc::new(RwLock::new(framework));
let server = Http::new() let server = Http::new()
.bind(&addr, move || { .bind(&addr, move || {
@ -89,7 +90,7 @@ impl WebServer {
/// Holds internal state for Hyper /// Holds internal state for Hyper
struct WebService { struct WebService {
framework: Arc<InstallerFramework>, framework: Arc<RwLock<InstallerFramework>>,
} }
impl Service for WebService { impl Service for WebService {
@ -104,9 +105,11 @@ impl Service for WebService {
// This endpoint should be usable directly from a <script> tag during loading. // This endpoint should be usable directly from a <script> tag during loading.
// TODO: Handle errors // TODO: Handle errors
(&Get, "/api/config") => { (&Get, "/api/config") => {
let framework = self.framework.read().unwrap();
let file = enscapsulate_json( let file = enscapsulate_json(
"config", "config",
&self.framework.get_config().to_json_str().unwrap(), &framework.get_config().to_json_str().unwrap(),
); );
Response::<hyper::Body>::new() Response::<hyper::Body>::new()
@ -133,7 +136,8 @@ impl Service for WebService {
} }
// Returns the default path for a installation // Returns the default path for a installation
(&Get, "/api/default-path") => { (&Get, "/api/default-path") => {
let path = self.framework.get_default_path(); let framework = self.framework.read().unwrap();
let path = framework.get_default_path();
let response = FileSelection { path }; let response = FileSelection { path };
@ -150,7 +154,7 @@ impl Service for WebService {
} }
(&Post, "/api/start-install") => { (&Post, "/api/start-install") => {
// We need to bit of pipelining to get this to work // We need to bit of pipelining to get this to work
let cloned_element = self.framework.clone(); let framework = self.framework.clone();
return Box::new(req.body().concat2().map(move |b| { return Box::new(req.body().concat2().map(move |b| {
let results = form_urlencoded::parse(b.as_ref()) let results = form_urlencoded::parse(b.as_ref())
@ -180,7 +184,11 @@ impl Service for WebService {
// Startup a thread to do this operation for us // Startup a thread to do this operation for us
thread::spawn(move || { thread::spawn(move || {
match cloned_element.install(to_install, &path, &sender) { let mut framework = framework.write().unwrap();
framework.set_install_dir(&path);
match framework.install(to_install, &sender, true) {
Err(v) => sender.send(InstallMessage::Error(v)).unwrap(), Err(v) => sender.send(InstallMessage::Error(v)).unwrap(),
_ => {} _ => {}
} }

View File

@ -36,7 +36,7 @@
<div class="column" v-if="has_error"> <div class="column" v-if="has_error">
<h4 class="subtitle">An error occurred:</h4> <h4 class="subtitle">An error occurred:</h4>
<pre><code>{{ error }}</code></pre> <code>{{ error }}</code>
<a class="button is-primary is-pulled-right" v-on:click="back_to_packages">Back</a> <a class="button is-primary is-pulled-right" v-on:click="back_to_packages">Back</a>
@ -147,7 +147,6 @@
results["path"] = this.install_location; results["path"] = this.install_location;
stream_ajax("/api/start-install", function(line) { stream_ajax("/api/start-install", function(line) {
console.log(line);
if (line.hasOwnProperty("Status")) { if (line.hasOwnProperty("Status")) {
app.progress_message = line.Status[0]; app.progress_message = line.Status[0];
app.progress = line.Status[1] * 100; app.progress = line.Status[1] * 100;

View File

@ -84,6 +84,8 @@ function stream_ajax(path, callback, successCallback, failCallback, data) {
continue; continue;
} }
console.log("Incoming line: " + line);
var contents = JSON.parse(line); var contents = JSON.parse(line);
callback(contents); callback(contents);
} }