mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2025-07-21 17:48:37 +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
|
// Decompress a .tar.xz file
|
||||||
let mut decompresser = XzDecoder::new(data);
|
let mut decompresser = XzDecoder::new(data);
|
||||||
let mut decompressed_data = Vec::new();
|
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))?;
|
.map_err(|x| format!("Failed to decompress data: {:?}", x))?;
|
||||||
|
|
||||||
let decompressed_contents: Box<Read> = Box::new(Cursor::new(decompressed_data));
|
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::copy;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use std::process::exit;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::process::{exit, Stdio};
|
||||||
|
|
||||||
use config::BaseAttributes;
|
use config::BaseAttributes;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
@ -42,6 +42,8 @@ use http;
|
||||||
|
|
||||||
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
||||||
|
|
||||||
|
use native;
|
||||||
|
|
||||||
/// A message thrown during the installation of packages.
|
/// A message thrown during the installation of packages.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub enum InstallMessage {
|
pub enum InstallMessage {
|
||||||
|
@ -80,6 +82,8 @@ pub struct InstallerFramework {
|
||||||
// If we just completed an uninstall, and we should clean up after ourselves.
|
// If we just completed an uninstall, and we should clean up after ourselves.
|
||||||
pub burn_after_exit: bool,
|
pub burn_after_exit: bool,
|
||||||
pub launcher_path: Option<String>,
|
pub launcher_path: Option<String>,
|
||||||
|
|
||||||
|
attempted_shutdown: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains basic properties on the status of the session. Subset of InstallationFramework.
|
/// 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.
|
/// Creates a new instance of the Installer Framework with a specified Config.
|
||||||
pub fn new(attrs: BaseAttributes) -> Self {
|
pub fn new(attrs: BaseAttributes) -> Self {
|
||||||
InstallerFramework {
|
InstallerFramework {
|
||||||
|
@ -405,6 +437,7 @@ impl InstallerFramework {
|
||||||
is_launcher: false,
|
is_launcher: false,
|
||||||
burn_after_exit: false,
|
burn_after_exit: false,
|
||||||
launcher_path: None,
|
launcher_path: None,
|
||||||
|
attempted_shutdown: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,6 +465,7 @@ impl InstallerFramework {
|
||||||
is_launcher: false,
|
is_launcher: false,
|
||||||
burn_after_exit: false,
|
burn_after_exit: false,
|
||||||
launcher_path: None,
|
launcher_path: None,
|
||||||
|
attempted_shutdown: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
123
src/main.rs
123
src/main.rs
|
@ -44,35 +44,23 @@ extern crate clap;
|
||||||
extern crate winapi;
|
extern crate winapi;
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
extern crate sysinfo;
|
|
||||||
extern crate slug;
|
extern crate slug;
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
extern crate sysinfo;
|
||||||
|
|
||||||
mod archives;
|
mod archives;
|
||||||
mod assets;
|
mod assets;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod frontend;
|
||||||
mod http;
|
mod http;
|
||||||
mod installer;
|
mod installer;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod native;
|
mod native;
|
||||||
mod rest;
|
|
||||||
mod sources;
|
mod sources;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
|
||||||
use web_view::*;
|
|
||||||
|
|
||||||
use installer::InstallerFramework;
|
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::path::PathBuf;
|
||||||
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
@ -86,18 +74,11 @@ use logging::LoggingErrors;
|
||||||
|
|
||||||
use clap::App;
|
use clap::App;
|
||||||
use clap::Arg;
|
use clap::Arg;
|
||||||
use log::Level;
|
|
||||||
|
|
||||||
use config::BaseAttributes;
|
use config::BaseAttributes;
|
||||||
|
|
||||||
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
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() {
|
fn main() {
|
||||||
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
|
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
|
||||||
|
|
||||||
|
@ -241,100 +222,6 @@ fn main() {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Firstly, allocate us an epidermal port
|
// Start up the UI
|
||||||
let target_port = {
|
frontend::launch(&app_name, is_launcher, framework);
|
||||||
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!");
|
|
||||||
}
|
}
|
||||||
|
|
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() {
|
let url = match asset["browser_download_url"].as_str() {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
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: {
|
methods: {
|
||||||
"exit": function() {
|
"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");
|
}).$mount("#app");
|
||||||
|
|
Loading…
Reference in a new issue