Add communications between frontend<->backend for progress

This commit is contained in:
James 2018-01-30 14:35:00 +11:00
parent 722bf73316
commit cfd8c94363
6 changed files with 164 additions and 20 deletions

3
src/http.rs Normal file
View File

@ -0,0 +1,3 @@
/// http.rs
///
/// A simple wrapper around

View File

@ -10,8 +10,18 @@ use std::env::consts::OS;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc::Sender;
use config::Config; 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, /// The installer framework contains metadata about packages, what is installable, what isn't,
/// etc. /// etc.
pub struct InstallerFramework { pub struct InstallerFramework {
@ -39,7 +49,11 @@ impl InstallerFramework {
} }
/// Sends a request for something to be installed. /// Sends a request for something to be installed.
pub fn install(&self, items: Vec<String>) { pub fn install(
&self,
items: Vec<String>,
messages: &Sender<InstallMessage>,
) -> Result<(), String> {
// TODO: Error handling // TODO: Error handling
println!("Framework: Installing {:?}", items); println!("Framework: Installing {:?}", items);
@ -55,20 +69,50 @@ impl InstallerFramework {
println!("Resolved to {:?}", to_install); println!("Resolved to {:?}", to_install);
// Install packages // Install packages
let mut count = 0.0 as f64;
let max = to_install.len() as f64;
for package in to_install.iter() { 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); 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 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 // Find the latest release in here
let latest_result = results let latest_result = results
.into_iter() .into_iter()
.filter(|f| f.files.iter().filter(|x| regex.is_match(&x.name)).count() > 0) .filter(|f| f.files.iter().filter(|x| regex.is_match(&x.name)).count() > 0)
.max_by_key(|f| f.version.clone()) .max_by_key(|f| f.version.clone());
.unwrap();
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 // Find the matching file in here
let latest_file = latest_result let latest_file = latest_result
@ -79,7 +123,13 @@ impl InstallerFramework {
.unwrap(); .unwrap();
println!("{:?}", latest_file); println!("{:?}", latest_file);
// TODO: Download found file
count += 1.0;
} }
Ok(())
} }
/// Creates a new instance of the Installer Framework with a specified Config. /// Creates a new instance of the Installer Framework with a specified Config.

View File

@ -12,6 +12,7 @@ use serde_json;
use futures::Stream; use futures::Stream;
use futures::Future; use futures::Future;
use futures::future; use futures::future;
use futures::Sink;
use hyper::{self, Error as HyperError, Get, Post, StatusCode}; use hyper::{self, Error as HyperError, Get, Post, StatusCode};
use hyper::header::{ContentLength, ContentType}; use hyper::header::{ContentLength, ContentType};
@ -29,6 +30,7 @@ use std::collections::HashMap;
use assets; use assets;
use installer::InstallerFramework; use installer::InstallerFramework;
use installer::InstallMessage;
#[derive(Serialize)] #[derive(Serialize)]
struct FileSelection { 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 // Startup a thread to do this operation for us
thread::spawn(move || { 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::<hyper::Body>::new() Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64)) //.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json()) .with_header(ContentType::plaintext())
.with_body(file) .with_body(rx)
})); }));
} }

View File

@ -66,6 +66,8 @@ pub struct File {
pub url: String, pub url: String,
} }
impl File {}
/// A individual release of an application. /// A individual release of an application.
#[derive(Debug)] #[derive(Debug)]
pub struct Release { pub struct Release {

View File

@ -74,6 +74,7 @@
<div v-html="config.general.installing_message"></div> <div v-html="config.general.installing_message"></div>
<br /> <br />
<div v-html="progress_message"></div>
<progress class="progress is-info is-medium" v-bind:value="progress" max="100"> <progress class="progress is-info is-medium" v-bind:value="progress" max="100">
{{ progress }}% {{ progress }}%
</progress> </progress>
@ -106,7 +107,8 @@
select_packages : true, select_packages : true,
is_installing : false, is_installing : false,
is_finished : false, is_finished : false,
progress : 0 progress : 0,
progress_message : ""
}, },
methods: { methods: {
"select_file": function() { "select_file": function() {
@ -132,15 +134,15 @@
} }
console.log(results); console.log(results);
ajax("/api/start-install", function(e) { stream_ajax("/api/start-install", function(line) {
// TODO: Remove fake loading console.log(line);
setInterval(function() { if (line.hasOwnProperty("Status")) {
app.progress += 5; app.progress_message = line.Status[0];
if (app.progress >= 100) { app.progress = line.Status[1] * 100;
}
}, function(e) {
app.is_installing = false; app.is_installing = false;
app.is_finished = true; app.is_finished = true;
}
}, 100);
}, undefined, results); }, undefined, results);
}, },
"exit": function() { "exit": function() {

View File

@ -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. * The default handler if a AJAX request fails. Not to be used directly.
* *