diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..a9edba8 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,3 @@ +/// http.rs +/// +/// A simple wrapper around diff --git a/src/installer.rs b/src/installer.rs index bc14949..1c0e989 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -10,8 +10,18 @@ use std::env::consts::OS; use std::path::PathBuf; +use std::sync::mpsc::Sender; + use config::Config; +/// A message thrown during the installation of packages. +#[derive(Serialize)] +pub enum InstallMessage { + Status(String, f64), + Error(String), + EOF, +} + /// The installer framework contains metadata about packages, what is installable, what isn't, /// etc. pub struct InstallerFramework { @@ -39,7 +49,11 @@ impl InstallerFramework { } /// Sends a request for something to be installed. - pub fn install(&self, items: Vec) { + pub fn install( + &self, + items: Vec, + messages: &Sender, + ) -> Result<(), String> { // TODO: Error handling println!("Framework: Installing {:?}", items); @@ -55,20 +69,50 @@ impl InstallerFramework { println!("Resolved to {:?}", to_install); // Install packages + let mut count = 0.0 as f64; + let max = to_install.len() as f64; + for package in to_install.iter() { + let base_package_percentage = count / max; + let base_package_range = ((count + 1.0) / max) - base_package_percentage; + println!("Installing {}", package.name); - let results = package.source.get_current_releases().unwrap(); + messages + .send(InstallMessage::Status( + format!( + "Polling {} for latest version of {}", + package.source.name, package.name + ), + base_package_percentage + base_package_range * 0.25, + )) + .unwrap(); + + let results = package.source.get_current_releases()?; + + messages + .send(InstallMessage::Status( + format!("Resolving dependency for {}", package.name), + base_package_percentage + base_package_range * 0.50, + )) + .unwrap(); let filtered_regex = package.source.match_regex.replace("#PLATFORM#", OS); - let regex = Regex::new(&filtered_regex).unwrap(); + let regex = match Regex::new(&filtered_regex) { + Ok(v) => v, + Err(v) => return Err(format!("An error occured while compiling regex: {:?}", v)), + }; // Find the latest release in here let latest_result = results .into_iter() .filter(|f| f.files.iter().filter(|x| regex.is_match(&x.name)).count() > 0) - .max_by_key(|f| f.version.clone()) - .unwrap(); + .max_by_key(|f| f.version.clone()); + + let latest_result = match latest_result { + Some(v) => v, + None => return Err(format!("No release with correct file found")), + }; // Find the matching file in here let latest_file = latest_result @@ -79,7 +123,13 @@ impl InstallerFramework { .unwrap(); println!("{:?}", latest_file); + + // TODO: Download found file + + count += 1.0; } + + Ok(()) } /// Creates a new instance of the Installer Framework with a specified Config. diff --git a/src/rest.rs b/src/rest.rs index 2ca42a2..cd83bd6 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -12,6 +12,7 @@ use serde_json; use futures::Stream; use futures::Future; use futures::future; +use futures::Sink; use hyper::{self, Error as HyperError, Get, Post, StatusCode}; use hyper::header::{ContentLength, ContentType}; @@ -29,6 +30,7 @@ use std::collections::HashMap; use assets; use installer::InstallerFramework; +use installer::InstallMessage; #[derive(Serialize)] struct FileSelection { @@ -164,17 +166,39 @@ impl Service for WebService { } } + let (sender, receiver) = channel(); + let (tx, rx) = hyper::Body::pair(); + // Startup a thread to do this operation for us thread::spawn(move || { - cloned_element.install(to_install); + match cloned_element.install(to_install, &sender) { + Err(v) => sender.send(InstallMessage::Error(v)).unwrap(), + _ => {} + } + sender.send(InstallMessage::EOF).unwrap(); }); - let file = serde_json::to_string(&{}).unwrap(); + // Spawn a thread for transforming messages to chunk messages + thread::spawn(move || { + let mut tx = tx; + loop { + let response = receiver.recv().unwrap(); + + match &response { + &InstallMessage::EOF => break, + _ => {} + } + + let mut response = serde_json::to_string(&response).unwrap(); + response.push('\n'); + tx = tx.send(Ok(response.into_bytes().into())).wait().unwrap(); + } + }); Response::::new() - .with_header(ContentLength(file.len() as u64)) - .with_header(ContentType::json()) - .with_body(file) + //.with_header(ContentLength(file.len() as u64)) + .with_header(ContentType::plaintext()) + .with_body(rx) })); } diff --git a/src/sources/types.rs b/src/sources/types.rs index b8fcbc6..5c740e9 100644 --- a/src/sources/types.rs +++ b/src/sources/types.rs @@ -66,6 +66,8 @@ pub struct File { pub url: String, } +impl File {} + /// A individual release of an application. #[derive(Debug)] pub struct Release { diff --git a/static/index.html b/static/index.html index 2a5a570..c05cd78 100644 --- a/static/index.html +++ b/static/index.html @@ -74,6 +74,7 @@

+
{{ progress }}% @@ -106,7 +107,8 @@ select_packages : true, is_installing : false, is_finished : false, - progress : 0 + progress : 0, + progress_message : "" }, methods: { "select_file": function() { @@ -132,15 +134,15 @@ } console.log(results); - ajax("/api/start-install", function(e) { - // TODO: Remove fake loading - setInterval(function() { - app.progress += 5; - if (app.progress >= 100) { - app.is_installing = false; - app.is_finished = true; - } - }, 100); + stream_ajax("/api/start-install", function(line) { + console.log(line); + if (line.hasOwnProperty("Status")) { + app.progress_message = line.Status[0]; + app.progress = line.Status[1] * 100; + } + }, function(e) { + app.is_installing = false; + app.is_finished = true; }, undefined, results); }, "exit": function() { diff --git a/static/js/helpers.js b/static/js/helpers.js index 6e5c80c..bce23e0 100644 --- a/static/js/helpers.js +++ b/static/js/helpers.js @@ -51,6 +51,69 @@ function ajax(path, successCallback, failCallback, data) { } } +/** + * Makes a AJAX request, streaming each line as it arrives. Type should be text/plain, + * each line will be interpeted as JSON seperately. + * + * @param path The path to connect to. + * @param callback A callback with a JSON payload. Called for every line as it comes. + * @param successCallback A callback with a raw text payload. + * @param failCallback A fail callback. Optional. + * @param data POST data. Optional. + */ +function stream_ajax(path, callback, successCallback, failCallback, data) { + var req = new XMLHttpRequest(); + + req.addEventListener("load", function() { + // The server can sometimes return a string error. Make sure we handle this. + if (this.status === 200) { + successCallback(this.responseText); + } else { + failCallback(); + } + }); + + req.onreadystatechange = function() { + if(req.readyState > 2) { + var newData = req.responseText.substr(req.seenBytes); + + var lines = newData.split("\n"); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line.length === 0) { + continue; + } + + var contents = JSON.parse(line); + callback(contents); + } + + req.seenBytes = req.responseText.length; + } + }; + + req.addEventListener("error", failCallback); + + req.open(data == null ? "GET" : "POST", path + "?nocache=" + request_id++, true); + // Rocket only currently supports URL encoded forms. + req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + + if (data != null) { + var form = ""; + + for (var key in data) { + if (form !== "") { + form += "&"; + } + form += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]); + } + + req.send(form); + } else { + req.send(); + } +} + /** * The default handler if a AJAX request fails. Not to be used directly. *