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:
James 2019-06-23 20:19:43 +10:00
parent 30bb49e1fb
commit 9d1f4c2576
21 changed files with 783 additions and 596 deletions

View file

@ -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
View 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
View file

@ -0,0 +1,4 @@
//! Contains the main web server used within the application.
pub mod server;
mod services;

View 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()),
)
}

View 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),
)
}

View 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)
}
},
)
}

View 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),
)
}

View 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"),
)
}
}
}

View 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);
}
})
}))
}

View 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),
)
}

View 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))
}
}
}
}

View 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),
)
}

View 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),
})
}

View 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);
}
}))
}

View 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
View 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!");
}

View file

@ -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,
})
}
}

View file

@ -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);
}

View file

@ -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)
}

View file

@ -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()
);
}
};

View file

@ -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");