mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2024-12-22 19:55:39 +00:00
Break apart REST into separate services
This cleans up locking, ensures consistent futures for all endpoints and enhances code re-use.
This commit is contained in:
parent
30bb49e1fb
commit
9d1f4c2576
|
@ -94,7 +94,8 @@ pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<Archive<'a> +
|
|||
// Decompress a .tar.xz file
|
||||
let mut decompresser = XzDecoder::new(data);
|
||||
let mut decompressed_data = Vec::new();
|
||||
decompresser.read_to_end(&mut decompressed_data)
|
||||
decompresser
|
||||
.read_to_end(&mut decompressed_data)
|
||||
.map_err(|x| format!("Failed to decompress data: {:?}", x))?;
|
||||
|
||||
let decompressed_contents: Box<Read> = Box::new(Cursor::new(decompressed_data));
|
||||
|
|
27
src/frontend/mod.rs
Normal file
27
src/frontend/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
//! Provides the frontend interface, including HTTP server.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use logging::LoggingErrors;
|
||||
|
||||
mod rest;
|
||||
mod ui;
|
||||
|
||||
/// Launches the main web server + UI. Returns when the framework has been consumed + web UI closed.
|
||||
pub fn launch(app_name: &str, is_launcher: bool, framework: InstallerFramework) {
|
||||
let framework = Arc::new(RwLock::new(framework));
|
||||
|
||||
let (servers, address) = rest::server::spawn_servers(framework.clone());
|
||||
|
||||
ui::start_ui(app_name, &address, is_launcher);
|
||||
|
||||
// Explicitly hint that we want the servers instance until here.
|
||||
drop(servers);
|
||||
|
||||
framework
|
||||
.write()
|
||||
.log_expect("Failed to write to framework to finalize")
|
||||
.shutdown()
|
||||
.log_expect("Failed to finalize framework");
|
||||
}
|
4
src/frontend/rest/mod.rs
Normal file
4
src/frontend/rest/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
//! Contains the main web server used within the application.
|
||||
|
||||
pub mod server;
|
||||
mod services;
|
84
src/frontend/rest/server.rs
Normal file
84
src/frontend/rest/server.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
//! Contains the over-arching server object + methods to manipulate it.
|
||||
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use hyper::server::Http;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use std::net::{SocketAddr, TcpListener, ToSocketAddrs};
|
||||
|
||||
use std::thread;
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
|
||||
/// application.
|
||||
pub struct WebServer {
|
||||
_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl WebServer {
|
||||
/// Creates a new web server with the specified address.
|
||||
pub fn with_addr(
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
addr: SocketAddr,
|
||||
) -> Result<Self, hyper::Error> {
|
||||
let handle = thread::spawn(move || {
|
||||
let server = Http::new()
|
||||
.bind(&addr, move || Ok(WebService::new(framework.clone())))
|
||||
.log_expect("Failed to bind to port");
|
||||
|
||||
server.run().log_expect("Failed to run HTTP server");
|
||||
});
|
||||
|
||||
Ok(WebServer { _handle: handle })
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a server instance on all local interfaces.
|
||||
///
|
||||
/// Returns server instances + http address of service running.
|
||||
pub fn spawn_servers(framework: Arc<RwLock<InstallerFramework>>) -> (Vec<WebServer>, String) {
|
||||
// Firstly, allocate us an epidermal port
|
||||
let target_port = {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.log_expect("At least one local address should be free");
|
||||
listener
|
||||
.local_addr()
|
||||
.log_expect("Should be able to pull address from listener")
|
||||
.port()
|
||||
};
|
||||
|
||||
// Now, iterate over all ports
|
||||
let addresses = "localhost:0"
|
||||
.to_socket_addrs()
|
||||
.log_expect("No localhost address found");
|
||||
|
||||
let mut instances = Vec::with_capacity(addresses.len());
|
||||
let mut http_address = None;
|
||||
|
||||
// Startup HTTP server for handling the web view
|
||||
for mut address in addresses {
|
||||
address.set_port(target_port);
|
||||
|
||||
let server = WebServer::with_addr(framework.clone(), address)
|
||||
.log_expect("Failed to bind to address");
|
||||
|
||||
info!("Spawning server instance @ {:?}", address);
|
||||
|
||||
http_address = Some(address);
|
||||
|
||||
instances.push(server);
|
||||
}
|
||||
|
||||
let http_address = http_address.log_expect("No HTTP address found");
|
||||
|
||||
(
|
||||
instances,
|
||||
format!("http://localhost:{}", http_address.port()),
|
||||
)
|
||||
}
|
31
src/frontend/rest/services/attributes.rs
Normal file
31
src/frontend/rest/services/attributes.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
//! The /api/attr call returns an executable script containing session variables.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::encapsulate_json;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
||||
let file = encapsulate_json(
|
||||
"base_attributes",
|
||||
&framework
|
||||
.base_attributes
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config"),
|
||||
);
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
72
src/frontend/rest/services/config.rs
Normal file
72
src/frontend/rest/services/config.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
//! The /api/config call returns the current installer framework configuration.
|
||||
//!
|
||||
//! This endpoint should be usable directly from a <script> tag during loading.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
use hyper::StatusCode;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use http;
|
||||
|
||||
use config::Config;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework_url = {
|
||||
service
|
||||
.get_framework_read()
|
||||
.base_attributes
|
||||
.target_url
|
||||
.clone()
|
||||
};
|
||||
|
||||
info!("Downloading configuration from {:?}...", framework_url);
|
||||
|
||||
default_future(
|
||||
match http::download_text(&framework_url).map(|x| Config::from_toml_str(&x)) {
|
||||
Ok(Ok(config)) => {
|
||||
service.get_framework_write().config = Some(config.clone());
|
||||
|
||||
info!("Configuration file downloaded successfully.");
|
||||
|
||||
let file = service
|
||||
.get_framework_read()
|
||||
.get_config()
|
||||
.log_expect("Config should be loaded by now")
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config");
|
||||
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
Ok(Err(v)) => {
|
||||
error!("Bad configuration file: {:?}", v);
|
||||
|
||||
Response::new()
|
||||
.with_status(StatusCode::ServiceUnavailable)
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body("Bad HTTP response")
|
||||
}
|
||||
Err(v) => {
|
||||
error!(
|
||||
"General connectivity error while downloading config: {:?}",
|
||||
v
|
||||
);
|
||||
|
||||
Response::new()
|
||||
.with_status(StatusCode::ServiceUnavailable)
|
||||
.with_header(ContentLength(v.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(v)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
33
src/frontend/rest/services/default_path.rs
Normal file
33
src/frontend/rest/services/default_path.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
//! The /api/default-path returns the default path for the application to install into.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
/// Struct used by serde to send a JSON payload to the client containing an optional value.
|
||||
#[derive(Serialize)]
|
||||
struct FileSelection {
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let path = { service.get_framework_read().get_default_path() };
|
||||
|
||||
let response = FileSelection { path };
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of default path object");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
30
src/frontend/rest/services/exit.rs
Normal file
30
src/frontend/rest/services/exit.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
//! The /api/exit closes down the application.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::ContentType;
|
||||
use hyper::StatusCode;
|
||||
|
||||
use std::process::exit;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
match service.get_framework_write().shutdown() {
|
||||
Ok(_) => {
|
||||
exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to complete framework shutdown: {:?}", e);
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_status(StatusCode::InternalServerError)
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body("Failed to complete framework shutdown"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
68
src/frontend/rest/services/install.rs
Normal file
68
src/frontend/rest/services/install.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
//! The /api/install call installs a set of packages dictated by a POST request.
|
||||
|
||||
use frontend::rest::services::stream_progress;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use installer::InstallMessage;
|
||||
|
||||
use futures::future::Future as _;
|
||||
use futures::stream::Stream;
|
||||
|
||||
use url::form_urlencoded;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn handle(service: &WebService, req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
||||
Box::new(req.body().concat2().map(move |b| {
|
||||
let results = form_urlencoded::parse(b.as_ref())
|
||||
.into_owned()
|
||||
.collect::<HashMap<String, String>>();
|
||||
|
||||
let mut to_install = Vec::new();
|
||||
let mut path: Option<String> = None;
|
||||
|
||||
// Transform results into just an array of stuff to install
|
||||
for (key, value) in &results {
|
||||
if key == "path" {
|
||||
path = Some(value.to_owned());
|
||||
continue;
|
||||
}
|
||||
|
||||
if value == "true" {
|
||||
to_install.push(key.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// The frontend always provides this
|
||||
let path =
|
||||
path.log_expect("No path specified by frontend when one should have already existed");
|
||||
|
||||
stream_progress(move |sender| {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let new_install = !framework.preexisting_install;
|
||||
if new_install {
|
||||
framework.set_install_dir(&path);
|
||||
}
|
||||
|
||||
if let Err(v) = framework.install(to_install, &sender, new_install) {
|
||||
error!("Install error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send install error: {:?}", v);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
30
src/frontend/rest/services/installation_status.rs
Normal file
30
src/frontend/rest/services/installation_status.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
//! The /api/installation-status call returns metadata relating to the current status of
|
||||
//! the installation.
|
||||
//!
|
||||
//! e.g. if the application is in maintenance mode
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
||||
let response = framework.get_installation_status();
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of installation status object");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
146
src/frontend/rest/services/mod.rs
Normal file
146
src/frontend/rest/services/mod.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
//! Provides all services used by the REST server.
|
||||
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use installer::{InstallMessage, InstallerFramework};
|
||||
|
||||
use hyper::server::Service;
|
||||
use hyper::{Method, StatusCode};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
|
||||
use std::thread;
|
||||
|
||||
use hyper::header::ContentType;
|
||||
|
||||
use futures::future::Future as _;
|
||||
use futures::sink::Sink;
|
||||
|
||||
mod attributes;
|
||||
mod config;
|
||||
mod default_path;
|
||||
mod exit;
|
||||
mod install;
|
||||
mod installation_status;
|
||||
mod packages;
|
||||
mod static_files;
|
||||
mod uninstall;
|
||||
mod update_updater;
|
||||
|
||||
/// Expected incoming Request format from Hyper.
|
||||
pub type Request = hyper::server::Request;
|
||||
|
||||
/// Completed response type returned by the server.
|
||||
pub type Response = hyper::server::Response;
|
||||
|
||||
/// Error type returned by the server.
|
||||
pub type Error = hyper::Error;
|
||||
|
||||
/// The return type used by function calls to the web server.
|
||||
pub type Future = Box<futures::Future<Item = Response, Error = Error>>;
|
||||
|
||||
/// If advanced functionality is not needed, return a default instant future.
|
||||
pub fn default_future(response: Response) -> Future {
|
||||
Box::new(futures::future::ok(response))
|
||||
}
|
||||
|
||||
/// Encapsulates JSON as a injectable Javascript script.
|
||||
pub fn encapsulate_json(field_name: &str, json: &str) -> String {
|
||||
format!("var {} = {};", field_name, json)
|
||||
}
|
||||
|
||||
/// Streams messages from a specified task to the client in a thread.
|
||||
pub fn stream_progress<F: 'static>(function: F) -> Response
|
||||
where
|
||||
F: FnOnce(Sender<InstallMessage>) -> () + Send,
|
||||
{
|
||||
let (sender, receiver) = channel();
|
||||
let (tx, rx) = hyper::Body::pair();
|
||||
|
||||
// Startup a thread to do this operation for us
|
||||
thread::spawn(move || function(sender));
|
||||
|
||||
// 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::new()
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(rx)
|
||||
}
|
||||
|
||||
/// Holds internal state for a single Hyper instance. Multiple will exist.
|
||||
pub struct WebService {
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
}
|
||||
|
||||
impl WebService {
|
||||
/// Returns an immutable reference to the framework. May block.
|
||||
pub fn get_framework_read(&self) -> RwLockReadGuard<InstallerFramework> {
|
||||
self.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied")
|
||||
}
|
||||
|
||||
/// Returns an immutable reference to the framework. May block.
|
||||
pub fn get_framework_write(&self) -> RwLockWriteGuard<InstallerFramework> {
|
||||
self.framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied")
|
||||
}
|
||||
|
||||
/// Creates a new WebService instance. Multiple are likely going to exist at once,
|
||||
/// so create a lock to hold this.
|
||||
pub fn new(framework: Arc<RwLock<InstallerFramework>>) -> WebService {
|
||||
WebService { framework }
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for WebService {
|
||||
type Request = Request;
|
||||
type Response = Response;
|
||||
type Error = Error;
|
||||
type Future = Future;
|
||||
|
||||
fn call(&self, req: Self::Request) -> Self::Future {
|
||||
let method = req.method().clone();
|
||||
let path = req.path().to_string();
|
||||
|
||||
match (method, path.as_str()) {
|
||||
(Method::Get, "/api/attrs") => attributes::handle(self, req),
|
||||
(Method::Get, "/api/config") => config::handle(self, req),
|
||||
(Method::Get, "/api/default-path") => default_path::handle(self, req),
|
||||
(Method::Get, "/api/exit") => exit::handle(self, req),
|
||||
(Method::Get, "/api/packages") => packages::handle(self, req),
|
||||
(Method::Get, "/api/installation-status") => installation_status::handle(self, req),
|
||||
(Method::Post, "/api/start-install") => install::handle(self, req),
|
||||
(Method::Post, "/api/uninstall") => uninstall::handle(self, req),
|
||||
(Method::Post, "/api/update-updater") => update_updater::handle(self, req),
|
||||
(Method::Get, _) => static_files::handle(self, req),
|
||||
e => {
|
||||
info!("Returned 404 for {:?}", e);
|
||||
default_future(Response::new().with_status(StatusCode::NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
src/frontend/rest/services/packages.rs
Normal file
29
src/frontend/rest/services/packages.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
//! The /api/packages call returns all the currently installed packages.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::encapsulate_json;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
||||
let file = encapsulate_json(
|
||||
"packages",
|
||||
&serde_json::to_string(&framework.database)
|
||||
.log_expect("Failed to render JSON representation of database"),
|
||||
);
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
40
src/frontend/rest/services/static_files.rs
Normal file
40
src/frontend/rest/services/static_files.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
//! The static files call returns static files embedded within the executable.
|
||||
//!
|
||||
//! e.g. index.html, main.js, ...
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
use hyper::StatusCode;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use assets;
|
||||
|
||||
pub fn handle(_service: &WebService, req: Request) -> Future {
|
||||
// At this point, we have a web browser client. Search for a index page
|
||||
// if needed
|
||||
let mut path: String = req.path().to_owned();
|
||||
if path.ends_with('/') {
|
||||
path += "index.html";
|
||||
}
|
||||
|
||||
default_future(match assets::file_from_string(&path) {
|
||||
Some((content_type, file)) => {
|
||||
let content_type = ContentType(
|
||||
content_type
|
||||
.parse()
|
||||
.log_expect("Failed to parse content type into correct representation"),
|
||||
);
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(content_type)
|
||||
.with_body(file)
|
||||
}
|
||||
None => Response::new().with_status(StatusCode::NotFound),
|
||||
})
|
||||
}
|
32
src/frontend/rest/services/uninstall.rs
Normal file
32
src/frontend/rest/services/uninstall.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
//! The /api/uninstall call uninstalls all packages.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::stream_progress;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use installer::InstallMessage;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
||||
default_future(stream_progress(move |sender| {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Err(v) = framework.uninstall(&sender) {
|
||||
error!("Uninstall error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send uninstall error: {:?}", v);
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
}))
|
||||
}
|
32
src/frontend/rest/services/update_updater.rs
Normal file
32
src/frontend/rest/services/update_updater.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
//! The /api/update-updater call attempts to update the currently running updater.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::stream_progress;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use installer::InstallMessage;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
||||
default_future(stream_progress(move |sender| {
|
||||
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);
|
||||
}
|
||||
}))
|
||||
}
|
75
src/frontend/ui/mod.rs
Normal file
75
src/frontend/ui/mod.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
//! Provides a web-view UI.
|
||||
|
||||
use web_view::Content;
|
||||
|
||||
use nfd::Response;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use log::Level;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum CallbackType {
|
||||
SelectInstallDir { callback_name: String },
|
||||
Log { msg: String, kind: String },
|
||||
}
|
||||
|
||||
/// Starts the main web UI. Will return when UI is closed.
|
||||
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) {
|
||||
let size = if is_launcher { (600, 300) } else { (1024, 500) };
|
||||
|
||||
info!("Spawning web view instance");
|
||||
|
||||
web_view::builder()
|
||||
.title(&format!("{} Installer", app_name))
|
||||
.content(Content::Url(http_address))
|
||||
.size(size.0, size.1)
|
||||
.resizable(false)
|
||||
.debug(false)
|
||||
.user_data(())
|
||||
.invoke_handler(|wv, msg| {
|
||||
let mut cb_result = Ok(());
|
||||
let command: CallbackType =
|
||||
serde_json::from_str(msg).log_expect(&format!("Unable to parse string: {:?}", msg));
|
||||
|
||||
debug!("Incoming payload: {:?}", command);
|
||||
|
||||
match command {
|
||||
CallbackType::SelectInstallDir { callback_name } => {
|
||||
#[cfg(windows)]
|
||||
let result = match nfd::open_pick_folder(None)
|
||||
.log_expect("Unable to open folder dialog")
|
||||
{
|
||||
Response::Okay(v) => Ok(v),
|
||||
_ => Err(()),
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let result = wv
|
||||
.dialog()
|
||||
.choose_directory("Select a install directory...", "");
|
||||
|
||||
if result.is_ok() {
|
||||
let result = serde_json::to_string(&result.ok())
|
||||
.log_expect("Unable to serialize response");
|
||||
let command = format!("{}({});", callback_name, result);
|
||||
debug!("Injecting response: {}", command);
|
||||
cb_result = wv.eval(&command);
|
||||
}
|
||||
}
|
||||
CallbackType::Log { msg, kind } => {
|
||||
let kind = match kind.as_ref() {
|
||||
"info" | "log" => Level::Info,
|
||||
"warn" => Level::Warn,
|
||||
"error" => Level::Error,
|
||||
_ => Level::Error,
|
||||
};
|
||||
|
||||
log!(target: "liftinstall::frontend-js", kind, "{}", msg);
|
||||
}
|
||||
}
|
||||
cb_result
|
||||
})
|
||||
.run()
|
||||
.log_expect("Unable to launch Web UI!");
|
||||
}
|
|
@ -18,8 +18,8 @@ use std::sync::mpsc::Sender;
|
|||
use std::io::copy;
|
||||
use std::io::Cursor;
|
||||
|
||||
use std::process::exit;
|
||||
use std::process::Command;
|
||||
use std::process::{exit, Stdio};
|
||||
|
||||
use config::BaseAttributes;
|
||||
use config::Config;
|
||||
|
@ -42,6 +42,8 @@ use http;
|
|||
|
||||
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
||||
|
||||
use native;
|
||||
|
||||
/// A message thrown during the installation of packages.
|
||||
#[derive(Serialize)]
|
||||
pub enum InstallMessage {
|
||||
|
@ -80,6 +82,8 @@ pub struct InstallerFramework {
|
|||
// If we just completed an uninstall, and we should clean up after ourselves.
|
||||
pub burn_after_exit: bool,
|
||||
pub launcher_path: Option<String>,
|
||||
|
||||
attempted_shutdown: bool,
|
||||
}
|
||||
|
||||
/// Contains basic properties on the status of the session. Subset of InstallationFramework.
|
||||
|
@ -394,6 +398,34 @@ impl InstallerFramework {
|
|||
}
|
||||
}
|
||||
|
||||
/// Shuts down the installer instance.
|
||||
pub fn shutdown(&mut self) -> Result<(), String> {
|
||||
if self.attempted_shutdown {
|
||||
return Err("Cannot attempt shutdown twice!".to_string());
|
||||
}
|
||||
|
||||
self.attempted_shutdown = true;
|
||||
|
||||
info!("Shutting down installer framework...");
|
||||
|
||||
if let Some(ref v) = self.launcher_path {
|
||||
info!("Launching {:?}", v);
|
||||
|
||||
Command::new(v)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|x| format!("Unable to start child process: {:?}", x))?;
|
||||
}
|
||||
|
||||
if self.burn_after_exit {
|
||||
info!("Requesting that self be deleted after exit.");
|
||||
native::burn_on_exit(&self.base_attributes.name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new instance of the Installer Framework with a specified Config.
|
||||
pub fn new(attrs: BaseAttributes) -> Self {
|
||||
InstallerFramework {
|
||||
|
@ -405,6 +437,7 @@ impl InstallerFramework {
|
|||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
attempted_shutdown: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -432,6 +465,7 @@ impl InstallerFramework {
|
|||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
attempted_shutdown: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
123
src/main.rs
123
src/main.rs
|
@ -44,35 +44,23 @@ extern crate clap;
|
|||
extern crate winapi;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
extern crate sysinfo;
|
||||
extern crate slug;
|
||||
#[cfg(not(windows))]
|
||||
extern crate sysinfo;
|
||||
|
||||
mod archives;
|
||||
mod assets;
|
||||
mod config;
|
||||
mod frontend;
|
||||
mod http;
|
||||
mod installer;
|
||||
mod logging;
|
||||
mod native;
|
||||
mod rest;
|
||||
mod sources;
|
||||
mod tasks;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
|
||||
#[cfg(windows)]
|
||||
use nfd::Response;
|
||||
|
||||
use rest::WebServer;
|
||||
|
||||
use std::net::TcpListener;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::process::exit;
|
||||
|
@ -86,18 +74,11 @@ use logging::LoggingErrors;
|
|||
|
||||
use clap::App;
|
||||
use clap::Arg;
|
||||
use log::Level;
|
||||
|
||||
use config::BaseAttributes;
|
||||
|
||||
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum CallbackType {
|
||||
SelectInstallDir { callback_name: String },
|
||||
Log { msg: String, kind: String },
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
|
||||
|
||||
|
@ -241,100 +222,6 @@ fn main() {
|
|||
false
|
||||
};
|
||||
|
||||
// Firstly, allocate us an epidermal port
|
||||
let target_port = {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.log_expect("At least one local address should be free");
|
||||
listener
|
||||
.local_addr()
|
||||
.log_expect("Should be able to pull address from listener")
|
||||
.port()
|
||||
};
|
||||
|
||||
// Now, iterate over all ports
|
||||
let addresses = "localhost:0"
|
||||
.to_socket_addrs()
|
||||
.log_expect("No localhost address found");
|
||||
|
||||
let mut servers = Vec::new();
|
||||
let mut http_address = None;
|
||||
|
||||
let framework = Arc::new(RwLock::new(framework));
|
||||
|
||||
// Startup HTTP server for handling the web view
|
||||
for mut address in addresses {
|
||||
address.set_port(target_port);
|
||||
|
||||
let server = WebServer::with_addr(framework.clone(), address)
|
||||
.log_expect("Failed to bind to address");
|
||||
|
||||
info!("Server: {:?}", address);
|
||||
|
||||
http_address = Some(address);
|
||||
|
||||
servers.push(server);
|
||||
}
|
||||
|
||||
let http_address = http_address.log_expect("No HTTP address found");
|
||||
|
||||
let http_address = format!("http://localhost:{}", http_address.port());
|
||||
|
||||
// Init the web view
|
||||
let size = if is_launcher { (600, 300) } else { (1024, 500) };
|
||||
|
||||
let resizable = false;
|
||||
let debug = true;
|
||||
|
||||
web_view::builder()
|
||||
.title(&format!("{} Installer", app_name))
|
||||
.content(Content::Url(http_address))
|
||||
.size(size.0, size.1)
|
||||
.resizable(resizable)
|
||||
.debug(debug)
|
||||
.user_data(())
|
||||
.invoke_handler(|wv, msg| {
|
||||
let mut cb_result = Ok(());
|
||||
let command: CallbackType =
|
||||
serde_json::from_str(msg).log_expect(&format!("Unable to parse string: {:?}", msg));
|
||||
|
||||
debug!("Incoming payload: {:?}", command);
|
||||
|
||||
match command {
|
||||
CallbackType::SelectInstallDir { callback_name } => {
|
||||
#[cfg(windows)]
|
||||
let result = match nfd::open_pick_folder(None)
|
||||
.log_expect("Unable to open folder dialog")
|
||||
{
|
||||
Response::Okay(v) => Ok(v),
|
||||
_ => Err(()),
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let result = wv
|
||||
.dialog()
|
||||
.choose_directory("Select a install directory...", "");
|
||||
|
||||
if result.is_ok() {
|
||||
let result = serde_json::to_string(&result.ok())
|
||||
.log_expect("Unable to serialize response");
|
||||
let command = format!("{}({});", callback_name, result);
|
||||
debug!("Injecting response: {}", command);
|
||||
cb_result = wv.eval(&command);
|
||||
}
|
||||
}
|
||||
CallbackType::Log { msg, kind } => {
|
||||
let kind = match kind.as_ref() {
|
||||
"info" | "log" => Level::Info,
|
||||
"warn" => Level::Warn,
|
||||
"error" => Level::Error,
|
||||
_ => Level::Error,
|
||||
};
|
||||
|
||||
log!(target: "liftinstall::frontend-js", kind, "{}", msg);
|
||||
}
|
||||
}
|
||||
cb_result
|
||||
})
|
||||
.run()
|
||||
.expect("Unable to launch Web UI!");
|
||||
// Start up the UI
|
||||
frontend::launch(&app_name, is_launcher, framework);
|
||||
}
|
||||
|
|
474
src/rest.rs
474
src/rest.rs
|
@ -1,474 +0,0 @@
|
|||
//! rest.rs
|
||||
//!
|
||||
//! Provides a HTTP/REST server for both frontend<->backend communication, as well
|
||||
//! as talking to external applications.
|
||||
|
||||
use serde_json;
|
||||
|
||||
use futures::future;
|
||||
use futures::Future;
|
||||
use futures::Sink;
|
||||
use futures::Stream;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
use hyper::server::{Http, Request, Response, Service};
|
||||
use hyper::{self, Error as HyperError, Get, Post, StatusCode};
|
||||
|
||||
use url::form_urlencoded;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::process::exit;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use assets;
|
||||
|
||||
use installer::InstallMessage;
|
||||
use installer::InstallerFramework;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use http;
|
||||
|
||||
use config::Config;
|
||||
|
||||
use native;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FileSelection {
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
|
||||
/// application.
|
||||
pub struct WebServer {
|
||||
_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl WebServer {
|
||||
/// Creates a new web server with the specified address.
|
||||
pub fn with_addr(
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
addr: SocketAddr,
|
||||
) -> Result<Self, HyperError> {
|
||||
let handle = thread::spawn(move || {
|
||||
let server = Http::new()
|
||||
.bind(&addr, move || {
|
||||
Ok(WebService {
|
||||
framework: framework.clone(),
|
||||
})
|
||||
})
|
||||
.log_expect("Failed to bind to port");
|
||||
|
||||
server.run().log_expect("Failed to run HTTP server");
|
||||
});
|
||||
|
||||
Ok(WebServer { _handle: handle })
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds internal state for Hyper
|
||||
struct WebService {
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
}
|
||||
|
||||
impl Service for WebService {
|
||||
type Request = Request;
|
||||
type Response = Response;
|
||||
type Error = hyper::Error;
|
||||
type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
|
||||
|
||||
/// HTTP request handler
|
||||
fn call(&self, req: Self::Request) -> Self::Future {
|
||||
Box::new(future::ok(match (req.method(), req.path()) {
|
||||
// This endpoint should be usable directly from a <script> tag during loading.
|
||||
(&Get, "/api/attrs") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let file = encapsulate_json(
|
||||
"base_attributes",
|
||||
&framework
|
||||
.base_attributes
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config"),
|
||||
);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Returns the web config loaded
|
||||
(&Get, "/api/config") => {
|
||||
let mut framework = self
|
||||
.framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
info!(
|
||||
"Downloading configuration from {:?}...",
|
||||
framework.base_attributes.target_url
|
||||
);
|
||||
|
||||
match http::download_text(&framework.base_attributes.target_url)
|
||||
.map(|x| Config::from_toml_str(&x))
|
||||
{
|
||||
Ok(Ok(config)) => {
|
||||
framework.config = Some(config.clone());
|
||||
|
||||
info!("Configuration file downloaded successfully.");
|
||||
|
||||
let file = framework
|
||||
.get_config()
|
||||
.log_expect("Config should be loaded by now")
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config");
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
Ok(Err(v)) => {
|
||||
error!("Bad configuration file: {:?}", v);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_status(StatusCode::ServiceUnavailable)
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body("Bad HTTP response")
|
||||
}
|
||||
Err(v) => {
|
||||
error!(
|
||||
"General connectivity error while downloading config: {:?}",
|
||||
v
|
||||
);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_status(StatusCode::ServiceUnavailable)
|
||||
.with_header(ContentLength(v.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
// This endpoint should be usable directly from a <script> tag during loading.
|
||||
(&Get, "/api/packages") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let file = encapsulate_json(
|
||||
"packages",
|
||||
&serde_json::to_string(&framework.database)
|
||||
.log_expect("Failed to render JSON representation of database"),
|
||||
);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Returns the default path for a installation
|
||||
(&Get, "/api/default-path") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
let path = framework.get_default_path();
|
||||
|
||||
let response = FileSelection { path };
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of default path object");
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Immediately exits the application
|
||||
(&Get, "/api/exit") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Some(ref v) = framework.launcher_path {
|
||||
Command::new(v)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
}
|
||||
|
||||
if framework.burn_after_exit {
|
||||
native::burn_on_exit(&framework.base_attributes.name);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
// Gets properties such as if the application is in maintenance mode
|
||||
(&Get, "/api/installation-status") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let response = framework.get_installation_status();
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of installation status object");
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Streams the installation of a particular set of packages
|
||||
(&Post, "/api/uninstall") => {
|
||||
// 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.uninstall(&sender) {
|
||||
error!("Uninstall error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send uninstall 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)
|
||||
}));
|
||||
}
|
||||
// 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
|
||||
let framework = self.framework.clone();
|
||||
|
||||
return Box::new(req.body().concat2().map(move |b| {
|
||||
let results = form_urlencoded::parse(b.as_ref())
|
||||
.into_owned()
|
||||
.collect::<HashMap<String, String>>();
|
||||
|
||||
let mut to_install = Vec::new();
|
||||
let mut path: Option<String> = None;
|
||||
|
||||
// Transform results into just an array of stuff to install
|
||||
for (key, value) in &results {
|
||||
if key == "path" {
|
||||
path = Some(value.to_owned());
|
||||
continue;
|
||||
}
|
||||
|
||||
if value == "true" {
|
||||
to_install.push(key.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// The frontend always provides this
|
||||
let path = path.log_expect(
|
||||
"No path specified by frontend when one should have already existed",
|
||||
);
|
||||
|
||||
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");
|
||||
|
||||
let new_install = !framework.preexisting_install;
|
||||
if new_install {
|
||||
framework.set_install_dir(&path);
|
||||
}
|
||||
|
||||
if let Err(v) = framework.install(to_install, &sender, new_install) {
|
||||
error!("Install error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send install 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 mut panic_after_finish = false;
|
||||
|
||||
let response = match receiver
|
||||
.recv() {
|
||||
Ok(v) => v,
|
||||
Err(v) => {
|
||||
error!("Queue message failed: {:?}", v);
|
||||
panic_after_finish = true;
|
||||
InstallMessage::Error("Internal error".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
if panic_after_finish {
|
||||
panic!("Failed to read from queue (flushed error message successfully)");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
//.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(rx)
|
||||
}));
|
||||
}
|
||||
|
||||
// Static file handler
|
||||
(&Get, _) => {
|
||||
// At this point, we have a web browser client. Search for a index page
|
||||
// if needed
|
||||
let mut path: String = req.path().to_owned();
|
||||
if path.ends_with('/') {
|
||||
path += "index.html";
|
||||
}
|
||||
|
||||
match assets::file_from_string(&path) {
|
||||
Some((content_type, file)) => {
|
||||
let content_type = ContentType(content_type.parse().log_expect(
|
||||
"Failed to parse content type into correct representation",
|
||||
));
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(content_type)
|
||||
.with_body(file)
|
||||
}
|
||||
None => Response::new().with_status(StatusCode::NotFound),
|
||||
}
|
||||
}
|
||||
// Fallthrough for POST/PUT/CONNECT/...
|
||||
_ => Response::new().with_status(StatusCode::NotFound),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates JSON as a injectable Javascript script.
|
||||
fn encapsulate_json(field_name: &str, json: &str) -> String {
|
||||
format!("var {} = {};", field_name, json)
|
||||
}
|
|
@ -97,7 +97,9 @@ impl ReleaseSource for GithubReleases {
|
|||
let url = match asset["browser_download_url"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release URL".to_string());
|
||||
return Err(
|
||||
"JSON payload missing information about release URL".to_string()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -62,7 +62,11 @@ var app = new Vue({
|
|||
},
|
||||
methods: {
|
||||
"exit": function() {
|
||||
ajax("/api/exit", function() {});
|
||||
ajax("/api/exit", function() {}, function(msg) {
|
||||
alert("LiftInstall encountered and error while exiting: " + msg
|
||||
+ "\nPlease upload the log file (in the same directory as the installer) to " +
|
||||
"the respective maintainers for this application (where you got it from!)");
|
||||
});
|
||||
}
|
||||
}
|
||||
}).$mount("#app");
|
||||
|
|
Loading…
Reference in a new issue