mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2024-11-24 19:15:36 +01:00
Update config files for v7 (#12)
* platform: fix build on Linux and update web-view * deps: replace xz-decom with xz2 and update deps * platform: fix regression... ... that prevents the build on Windows * linux: implement platform-dependent functions * travis: add macos and windows CI * travis: use official Rust Docker image * Update Cargo.lock for new version * Break apart REST into separate services This cleans up locking, ensures consistent futures for all endpoints and enhances code re-use. * Clean up codebase, fixing minor errors * Update packages, use async client for downloading config While this has a hell of a lot more boilerplate, this is quite a bit cleaner. * Add explicit 'dyn's as per Rust nightly requirements * Migrate self updating functions to own module * Migrate assets to server module * Use patched web-view to fix dialogs, remove nfd * Implement basic dark mode * Revert window.close usage * ui: split files and use Webpack * frontend: ui: include prebuilt assets... ... and update rust side stuff * build: integrate webpack building into build.rs * Polish Vue UI split * Add instructions for node + yarn * native: fix uninstall self-destruction behavior...... by not showing the command prompt window and fork-spawning the cmd * native: deal with Unicode issues in native APIs * native: further improve Unicode support on Windows * travis: add cache and fix issues * ui: use Buefy components to... ... beautify the UI * ui: makes error message selectable * Make launcher mode behaviour more robust * Fix error display on launcher pages * Correctly handle exit on error * Bump installer version
This commit is contained in:
parent
6aa5da8795
commit
68109894f1
34
.travis.yml
34
.travis.yml
@ -1,11 +1,27 @@
|
||||
os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
language: cpp
|
||||
sudo: required
|
||||
dist: trusty
|
||||
services: docker
|
||||
install: docker pull rust:1
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cargo
|
||||
- $TRAVIS_BUILD_DIR/ui/node_modules
|
||||
script: docker run -v $HOME/.cargo:/root/.cargo -v $(pwd):/liftinstall rust:1 /bin/bash -ex /liftinstall/.travis/build.sh
|
||||
|
||||
install:
|
||||
- docker pull ubuntu:18.04
|
||||
- os: osx
|
||||
language: rust
|
||||
cache: cargo
|
||||
osx_image: xcode10
|
||||
script: brew install yarn && cargo build
|
||||
|
||||
script:
|
||||
- docker run -v $(pwd):/liftinstall ubuntu:18.04 /bin/bash -ex /liftinstall/.travis/build.sh
|
||||
- os: windows
|
||||
language: rust
|
||||
cache: cargo
|
||||
script:
|
||||
- choco install nodejs yarn
|
||||
- export PATH="$PROGRAMFILES/nodejs/:$PROGRAMFILES (x86)/Yarn/bin/:$PATH"
|
||||
- cargo build
|
||||
|
@ -1,10 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
cd /liftinstall
|
||||
cd /liftinstall || exit 1
|
||||
|
||||
apt update
|
||||
apt install -y libwebkit2gtk-4.0-dev libssl-dev
|
||||
# setup NodeJS
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
# setup Yarn
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
export PATH=~/.cargo/bin:$PATH
|
||||
apt-get update
|
||||
apt-get install -y libwebkit2gtk-4.0-dev libssl-dev nodejs yarn
|
||||
|
||||
yarn --cwd ui
|
||||
|
||||
cargo build
|
||||
|
1721
Cargo.lock
generated
1721
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
47
Cargo.toml
47
Cargo.toml
@ -8,47 +8,50 @@ description = "An adaptable installer for your application."
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
web-view = {git = "https://github.com/Boscop/web-view.git", rev = "555f422d09cbb94e82a728d47e9e07ca91963f6e"}
|
||||
web-view = {git = "https://github.com/j-selby/web-view.git", rev = "752106e4637356cbdb39a0bf1113ea3ae8a14243"}
|
||||
|
||||
hyper = "0.11.27"
|
||||
futures = "*"
|
||||
mime_guess = "1.8.3"
|
||||
url = "*"
|
||||
futures = "0.1.25"
|
||||
mime_guess = "1.8.6"
|
||||
url = "1.7.2"
|
||||
|
||||
reqwest = "0.9.0"
|
||||
number_prefix = "0.2.7"
|
||||
reqwest = "0.9.12"
|
||||
number_prefix = "0.3.0"
|
||||
|
||||
serde = "1.0.27"
|
||||
serde_derive = "1.0.27"
|
||||
serde_json = "1.0.9"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.89"
|
||||
serde_json = "1.0.39"
|
||||
|
||||
toml = "0.4"
|
||||
toml = "0.5.0"
|
||||
|
||||
semver = {version = "0.9.0", features = ["serde"]}
|
||||
regex = "0.2"
|
||||
regex = "1.1.5"
|
||||
|
||||
dirs = "1.0"
|
||||
zip = "0.4.2"
|
||||
xz-decom = {git = "https://github.com/j-selby/xz-decom.git", rev = "9ebf3d00d9ff909c39eec1d2cf7e6e068ce214e5"}
|
||||
dirs = "1.0.5"
|
||||
zip = "0.5.1"
|
||||
xz2 = "0.1.6"
|
||||
tar = "0.4"
|
||||
|
||||
log = "0.4"
|
||||
fern = "0.5"
|
||||
chrono = "0.4.5"
|
||||
chrono = "0.4.6"
|
||||
|
||||
clap = "2.32.0"
|
||||
|
||||
[build-dependencies]
|
||||
walkdir = "2"
|
||||
serde = "1.0.27"
|
||||
serde_derive = "1.0.27"
|
||||
toml = "0.4"
|
||||
walkdir = "2.2.7"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.89"
|
||||
toml = "0.5.0"
|
||||
which = "2.0.1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
# NFD is needed on Windows, as web-view doesn't work correctly here
|
||||
nfd = "0.0.4"
|
||||
|
||||
winapi = { version = "0.3", features = ["psapi", "winbase", "winioctl", "winnt"] }
|
||||
widestring = "0.4.0"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
sysinfo = "0.8.2"
|
||||
slug = "0.1.4"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
@ -22,6 +22,7 @@ For more detailed instructions, look at the usage documentation above.
|
||||
There are are few system dependencies depending on your platform:
|
||||
- For all platforms, `cargo` should be available on your PATH. [Rustup](https://rustup.rs/) is the
|
||||
recommended way to achieve this. Stable or Nightly Rust works fine.
|
||||
- Have node.js and Yarn available on your PATH (for building UI components, not needed at runtime).
|
||||
- For Windows (MSVC), you need Visual Studio installed.
|
||||
- For Windows (Mingw), you need `gcc`/`g++` available on the PATH.
|
||||
- For Mac, you need Xcode installed, and Clang/etc available on the PATH.
|
||||
@ -33,8 +34,8 @@ apt install -y build-essential libwebkit2gtk-4.0-dev libssl-dev
|
||||
|
||||
In order to build yourself an installer, as a bare minimum, you need to:
|
||||
|
||||
- Add your favicon to `static/favicon.ico`
|
||||
- Add your logo to `static/logo.png`
|
||||
- Add your favicon to `ui/public/favicon.ico`
|
||||
- Add your logo to `ui/src/assets/logo.png`
|
||||
- Modify the bootstrap configuration file as needed (`config.PLATFORM.toml`).
|
||||
- Have the main configuration file somewhere useful, reachable over HTTP.
|
||||
- Run:
|
||||
|
3
bootstrap.macos.toml
Normal file
3
bootstrap.macos.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# fake configuration for CI purpose only
|
||||
name = "yuzu"
|
||||
target_url = "https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml"
|
@ -1,2 +1,2 @@
|
||||
name = "yuzu"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v6.toml"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v7.toml"
|
||||
|
116
build.rs
116
build.rs
@ -1,5 +1,3 @@
|
||||
extern crate walkdir;
|
||||
|
||||
#[cfg(windows)]
|
||||
extern crate winres;
|
||||
|
||||
@ -11,24 +9,19 @@ extern crate serde;
|
||||
extern crate serde_derive;
|
||||
extern crate toml;
|
||||
|
||||
use walkdir::WalkDir;
|
||||
extern crate which;
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::fs::copy;
|
||||
use std::fs::create_dir_all;
|
||||
use std::fs::File;
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
use std::env::consts::OS;
|
||||
|
||||
const FILES_TO_PREPROCESS: &'static [&'static str] = &["helpers.js", "views.js"];
|
||||
|
||||
/// Describes the application itself.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BaseAttributes {
|
||||
@ -39,7 +32,7 @@ pub struct BaseAttributes {
|
||||
#[cfg(windows)]
|
||||
fn handle_binary(config: &BaseAttributes) {
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("static/favicon.ico");
|
||||
res.set_icon("ui/public/favicon.ico");
|
||||
res.set(
|
||||
"FileDescription",
|
||||
&format!("Interactive installer for {}", config.name),
|
||||
@ -62,6 +55,8 @@ fn handle_binary(_config: &BaseAttributes) {}
|
||||
|
||||
fn main() {
|
||||
let output_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let current_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let ui_dir = current_dir.join("ui");
|
||||
|
||||
let os = OS.to_lowercase();
|
||||
|
||||
@ -92,80 +87,33 @@ fn main() {
|
||||
// Copy for the main build
|
||||
copy(&target_config, output_dir.join("bootstrap.toml")).expect("Unable to copy config file");
|
||||
|
||||
// Copy files from static/ to build dir
|
||||
for entry in WalkDir::new("static") {
|
||||
let entry = entry.expect("Unable to read output directory");
|
||||
let yarn_binary = which::which("yarn")
|
||||
.expect("Failed to find yarn - please go ahead and install it!");
|
||||
|
||||
let output_file = output_dir.join(entry.path());
|
||||
|
||||
if entry.path().is_dir() {
|
||||
create_dir_all(output_file).expect("Unable to create dir");
|
||||
} else {
|
||||
let filename = entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("Unable to parse filename")
|
||||
// Build and deploy frontend files
|
||||
Command::new(&yarn_binary)
|
||||
.arg("--version")
|
||||
.spawn()
|
||||
.expect("Yarn could not be launched");
|
||||
Command::new(&yarn_binary)
|
||||
.arg("--cwd")
|
||||
.arg(ui_dir.to_str().expect("Unable to covert path"))
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait().expect("Unable to install Node.JS dependencies using Yarn");
|
||||
Command::new(&yarn_binary)
|
||||
.args(&[
|
||||
"--cwd",
|
||||
ui_dir.to_str().expect("Unable to covert path"),
|
||||
"run",
|
||||
"build",
|
||||
"--dest",
|
||||
output_dir
|
||||
.join("static")
|
||||
.to_str()
|
||||
.expect("Unable to convert to string");
|
||||
|
||||
if FILES_TO_PREPROCESS.contains(&filename) {
|
||||
// Do basic preprocessing - transcribe template string
|
||||
let source = BufReader::new(File::open(entry.path()).expect("Unable to copy file"));
|
||||
let mut target = File::create(output_file).expect("Unable to copy file");
|
||||
|
||||
let mut is_template_string = false;
|
||||
|
||||
for line in source.lines() {
|
||||
let line = line.expect("Unable to read line from JS file");
|
||||
|
||||
let mut is_break = false;
|
||||
let mut is_quote = false;
|
||||
|
||||
let mut output_line = String::new();
|
||||
|
||||
if is_template_string {
|
||||
output_line += "\"";
|
||||
}
|
||||
|
||||
for c in line.chars() {
|
||||
if c == '\\' {
|
||||
is_break = true;
|
||||
output_line.push('\\');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\"' || c == '\'') && !is_break && !is_template_string {
|
||||
is_quote = !is_quote;
|
||||
}
|
||||
|
||||
if c == '`' && !is_break && !is_quote {
|
||||
output_line += "\"";
|
||||
is_template_string = !is_template_string;
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '"' && !is_break && is_template_string {
|
||||
output_line += "\\\"";
|
||||
continue;
|
||||
}
|
||||
|
||||
is_break = false;
|
||||
output_line.push(c);
|
||||
}
|
||||
|
||||
if is_template_string {
|
||||
output_line += "\" +";
|
||||
}
|
||||
|
||||
output_line.push('\n');
|
||||
|
||||
target
|
||||
.write(output_line.as_bytes())
|
||||
.expect("Unable to write line");
|
||||
}
|
||||
} else {
|
||||
copy(entry.path(), output_file).expect("Unable to copy file");
|
||||
}
|
||||
}
|
||||
}
|
||||
.expect("Unable to convert path"),
|
||||
])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait().expect("Unable to build frontend assets using Webpack");
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.5/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
|
30
config.windows.v7.toml
Normal file
30
config.windows.v7.toml
Normal file
@ -0,0 +1,30 @@
|
||||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
description = "The nightly build of yuzu contains already reviewed and tested features."
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-nightly"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Nightly"
|
||||
relative_path = "nightly/yuzu.exe"
|
||||
description = "Launch yuzu (Nightly version)"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Canary"
|
||||
description = "The canary build of yuzu has additional features that are still waiting on review."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-canary"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Canary"
|
||||
relative_path = "canary/yuzu.exe"
|
||||
description = "Launch yuzu (Canary version)"
|
||||
|
@ -10,13 +10,13 @@ use std::io::Read;
|
||||
use std::iter::Iterator;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use xz_decom;
|
||||
use xz2::read::XzDecoder;
|
||||
|
||||
pub trait Archive<'a> {
|
||||
/// func: iterator value, max size, file name, file contents
|
||||
fn for_each(
|
||||
&mut self,
|
||||
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
|
||||
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
|
||||
) -> Result<(), String>;
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ struct ZipArchive<'a> {
|
||||
impl<'a> Archive<'a> for ZipArchive<'a> {
|
||||
fn for_each(
|
||||
&mut self,
|
||||
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
|
||||
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
let max = self.archive.len();
|
||||
|
||||
@ -49,13 +49,13 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
|
||||
}
|
||||
|
||||
struct TarArchive<'a> {
|
||||
archive: UpstreamTarArchive<Box<Read + 'a>>,
|
||||
archive: UpstreamTarArchive<Box<dyn Read + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Archive<'a> for TarArchive<'a> {
|
||||
fn for_each(
|
||||
&mut self,
|
||||
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
|
||||
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
let entries = self
|
||||
.archive
|
||||
@ -83,7 +83,7 @@ impl<'a> Archive<'a> for TarArchive<'a> {
|
||||
}
|
||||
|
||||
/// Reads the named archive with an archive implementation.
|
||||
pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<Archive<'a> + 'a>, String> {
|
||||
pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<dyn Archive<'a> + 'a>, String> {
|
||||
if name.ends_with(".zip") {
|
||||
// Decompress a .zip file
|
||||
let archive = UpstreamZipArchive::new(Cursor::new(data))
|
||||
@ -92,10 +92,13 @@ pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<Archive<'a> +
|
||||
Ok(Box::new(ZipArchive { archive }))
|
||||
} else if name.ends_with(".tar.xz") {
|
||||
// Decompress a .tar.xz file
|
||||
let decompressed_data = xz_decom::decompress(data)
|
||||
.map_err(|x| format!("Failed to build decompressor: {:?}", x))?;
|
||||
let mut decompresser = XzDecoder::new(data);
|
||||
let mut decompressed_data = Vec::new();
|
||||
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));
|
||||
let decompressed_contents: Box<dyn Read> = Box::new(Cursor::new(decompressed_data));
|
||||
|
||||
let tar = UpstreamTarArchive::new(decompressed_contents);
|
||||
|
||||
|
29
src/frontend/mod.rs
Normal file
29
src/frontend/mod.rs
Normal file
@ -0,0 +1,29 @@
|
||||
//! frontend/mod.rs
|
||||
//!
|
||||
//! Provides the frontend interface, including HTTP server.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use logging::LoggingErrors;
|
||||
|
||||
pub 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");
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
extern crate mime_guess;
|
||||
|
||||
use assets::mime_guess::{get_mime_type, octet_stream};
|
||||
use self::mime_guess::{get_mime_type, octet_stream};
|
||||
|
||||
macro_rules! include_files_as_assets {
|
||||
( $target_match:expr, $( $file_name:expr ),* ) => {
|
||||
@ -34,18 +34,15 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
|
||||
file_path,
|
||||
"/index.html",
|
||||
"/favicon.ico",
|
||||
"/logo.png",
|
||||
"/how-to-open.png",
|
||||
"/css/bulma.min.css",
|
||||
"/css/main.css",
|
||||
"/img/logo.png",
|
||||
"/img/how-to-open.png",
|
||||
"/css/app.css",
|
||||
"/css/chunk-vendors.css",
|
||||
"/fonts/roboto-v18-latin-regular.eot",
|
||||
"/fonts/roboto-v18-latin-regular.woff",
|
||||
"/fonts/roboto-v18-latin-regular.woff2",
|
||||
"/js/vue.min.js",
|
||||
"/js/vue-router.min.js",
|
||||
"/js/helpers.js",
|
||||
"/js/views.js",
|
||||
"/js/main.js"
|
||||
"/js/chunk-vendors.js",
|
||||
"/js/app.js"
|
||||
)?;
|
||||
|
||||
Some((string_mime, contents))
|
7
src/frontend/rest/mod.rs
Normal file
7
src/frontend/rest/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
//! frontend/rest/mod.rs
|
||||
//!
|
||||
//! Contains the main web server used within the application.
|
||||
|
||||
mod assets;
|
||||
pub mod server;
|
||||
pub mod services;
|
86
src/frontend/rest/server.rs
Normal file
86
src/frontend/rest/server.rs
Normal file
@ -0,0 +1,86 @@
|
||||
//! frontend/rest/server.rs
|
||||
//!
|
||||
//! 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()),
|
||||
)
|
||||
}
|
33
src/frontend/rest/services/attributes.rs
Normal file
33
src/frontend/rest/services/attributes.rs
Normal file
@ -0,0 +1,33 @@
|
||||
//! frontend/rest/services/attributes.rs
|
||||
//!
|
||||
//! 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),
|
||||
)
|
||||
}
|
84
src/frontend/rest/services/config.rs
Normal file
84
src/frontend/rest/services/config.rs
Normal file
@ -0,0 +1,84 @@
|
||||
//! frontend/rest/services/config.rs
|
||||
//!
|
||||
//! 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::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use config::Config;
|
||||
|
||||
use http::build_async_client;
|
||||
|
||||
use futures::stream::Stream;
|
||||
use futures::Future as _;
|
||||
|
||||
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);
|
||||
|
||||
let framework = service.framework.clone();
|
||||
|
||||
// Hyper doesn't allow for clients to do sync network operations in a async future.
|
||||
// This smallish pipeline joins the two together.
|
||||
Box::new(
|
||||
build_async_client()
|
||||
.log_expect("Failed to build async client")
|
||||
.get(&framework_url)
|
||||
.send()
|
||||
.map_err(|x| {
|
||||
error!("HTTP error while downloading configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})
|
||||
.and_then(|x| {
|
||||
x.into_body().concat2().map_err(|x| {
|
||||
error!("HTTP error while parsing configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})
|
||||
})
|
||||
.and_then(move |x| {
|
||||
let x = String::from_utf8(x.to_vec()).map_err(|x| {
|
||||
error!("UTF-8 error while parsing configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})?;
|
||||
|
||||
let config = Config::from_toml_str(&x).map_err(|x| {
|
||||
error!("Serde error while parsing configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})?;
|
||||
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("Failed to get write lock for framework");
|
||||
|
||||
framework.config = Some(config);
|
||||
|
||||
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");
|
||||
|
||||
Ok(Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file))
|
||||
}),
|
||||
)
|
||||
}
|
27
src/frontend/rest/services/dark_mode.rs
Normal file
27
src/frontend/rest/services/dark_mode.rs
Normal file
@ -0,0 +1,27 @@
|
||||
//! frontend/rest/services/dark_mode.rs
|
||||
//!
|
||||
//! This call returns if dark mode is enabled on the system currently.
|
||||
|
||||
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;
|
||||
|
||||
use native::is_dark_mode_active;
|
||||
|
||||
pub fn handle(_service: &WebService, _req: Request) -> Future {
|
||||
let file = serde_json::to_string(&is_dark_mode_active())
|
||||
.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),
|
||||
)
|
||||
}
|
35
src/frontend/rest/services/default_path.rs
Normal file
35
src/frontend/rest/services/default_path.rs
Normal file
@ -0,0 +1,35 @@
|
||||
//! frontend/rest/services/default_path.rs
|
||||
//!
|
||||
//! 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),
|
||||
)
|
||||
}
|
32
src/frontend/rest/services/exit.rs
Normal file
32
src/frontend/rest/services/exit.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! frontend/rest/services/exit.rs
|
||||
//!
|
||||
//! 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(format!("Failed to complete framework shutdown - {}", e)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
70
src/frontend/rest/services/install.rs
Normal file
70
src/frontend/rest/services/install.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! frontend/rest/services/install.rs
|
||||
//!
|
||||
//! 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);
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
32
src/frontend/rest/services/installation_status.rs
Normal file
32
src/frontend/rest/services/installation_status.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! frontend/rest/services/installation_status.rs
|
||||
//!
|
||||
//! 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),
|
||||
)
|
||||
}
|
150
src/frontend/rest/services/mod.rs
Normal file
150
src/frontend/rest/services/mod.rs
Normal file
@ -0,0 +1,150 @@
|
||||
//! frontend/rest/services/mod.rs
|
||||
//!
|
||||
//! 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 dark_mode;
|
||||
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<dyn 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/dark-mode") => dark_mode::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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
src/frontend/rest/services/packages.rs
Normal file
31
src/frontend/rest/services/packages.rs
Normal file
@ -0,0 +1,31 @@
|
||||
//! frontend/rest/services/packages.rs
|
||||
//!
|
||||
//! 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),
|
||||
)
|
||||
}
|
42
src/frontend/rest/services/static_files.rs
Normal file
42
src/frontend/rest/services/static_files.rs
Normal file
@ -0,0 +1,42 @@
|
||||
//! frontend/rest/services/static_files.rs
|
||||
//!
|
||||
//! The static files call returns static files embedded within the executable.
|
||||
//!
|
||||
//! e.g. index.html, main.js, ...
|
||||
|
||||
use frontend::rest::assets;
|
||||
|
||||
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;
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
34
src/frontend/rest/services/uninstall.rs
Normal file
34
src/frontend/rest/services/uninstall.rs
Normal file
@ -0,0 +1,34 @@
|
||||
//! frontend/rest/services/uninstall.rs
|
||||
//!
|
||||
//! 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);
|
||||
}
|
||||
}))
|
||||
}
|
34
src/frontend/rest/services/update_updater.rs
Normal file
34
src/frontend/rest/services/update_updater.rs
Normal file
@ -0,0 +1,34 @@
|
||||
//! frontend/rest/services/update_updater.rs
|
||||
//!
|
||||
//! 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);
|
||||
}
|
||||
}))
|
||||
}
|
71
src/frontend/ui/mod.rs
Normal file
71
src/frontend/ui/mod.rs
Normal file
@ -0,0 +1,71 @@
|
||||
//! frontend/ui/mod.rs
|
||||
//!
|
||||
//! Provides a web-view UI.
|
||||
|
||||
use web_view::Content;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use log::Level;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum CallbackType {
|
||||
SelectInstallDir { callback_name: String },
|
||||
Log { msg: String, kind: String },
|
||||
Test {}
|
||||
}
|
||||
|
||||
/// 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 } => {
|
||||
let result = wv
|
||||
.dialog()
|
||||
.choose_directory("Select a install directory...", "");
|
||||
|
||||
if let Ok(Some(new_path)) = result {
|
||||
if new_path.to_string_lossy().len() > 0 {
|
||||
let result = serde_json::to_string(&new_path)
|
||||
.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);
|
||||
}
|
||||
CallbackType::Test {} => {}
|
||||
}
|
||||
|
||||
cb_result
|
||||
})
|
||||
.run()
|
||||
.log_expect("Unable to launch Web UI!");
|
||||
}
|
21
src/http.rs
21
src/http.rs
@ -7,6 +7,7 @@ use reqwest::header::CONTENT_LENGTH;
|
||||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::async::Client as AsyncClient;
|
||||
use reqwest::Client;
|
||||
|
||||
/// Asserts that a URL is valid HTTPS, else returns an error.
|
||||
@ -14,7 +15,7 @@ pub fn assert_ssl(url: &str) -> Result<(), String> {
|
||||
if url.starts_with("https://") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Specified URL was not https"))
|
||||
Err("Specified URL was not https".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,18 +27,12 @@ pub fn build_client() -> Result<Client, String> {
|
||||
.map_err(|x| format!("Unable to build client: {:?}", x))
|
||||
}
|
||||
|
||||
/// Downloads a text file from the specified URL.
|
||||
pub fn download_text(url: &str) -> Result<String, String> {
|
||||
assert_ssl(url)?;
|
||||
|
||||
let mut client = build_client()?
|
||||
.get(url)
|
||||
.send()
|
||||
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
|
||||
|
||||
client
|
||||
.text()
|
||||
.map_err(|v| format!("Failed to get text from resource: {:?}", v))
|
||||
/// Builds a customised async HTTP client.
|
||||
pub fn build_async_client() -> Result<AsyncClient, String> {
|
||||
AsyncClient::builder()
|
||||
.timeout(Duration::from_secs(8))
|
||||
.build()
|
||||
.map_err(|x| format!("Unable to build client: {:?}", x))
|
||||
}
|
||||
|
||||
/// Streams a file from a HTTP server.
|
||||
|
@ -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;
|
||||
@ -40,7 +40,9 @@ use std::fs::remove_file;
|
||||
|
||||
use http;
|
||||
|
||||
use number_prefix::{decimal_prefix, Prefixed, Standalone};
|
||||
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
||||
|
||||
use native;
|
||||
|
||||
/// A message thrown during the installation of packages.
|
||||
#[derive(Serialize)]
|
||||
@ -105,14 +107,14 @@ pub struct LocalInstallation {
|
||||
|
||||
macro_rules! declare_messenger_callback {
|
||||
($target:expr) => {
|
||||
&|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
&|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
if let Err(v) = $target.send(InstallMessage::Status(msg.to_string(), progress as _))
|
||||
{
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
}
|
||||
&TaskMessage::PackageInstalled => {
|
||||
TaskMessage::PackageInstalled => {
|
||||
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
@ -264,11 +266,11 @@ impl InstallerFramework {
|
||||
};
|
||||
|
||||
// Pretty print data volumes
|
||||
let pretty_current = match decimal_prefix(downloaded as f64) {
|
||||
let pretty_current = match NumberPrefix::decimal(downloaded as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
let pretty_total = match decimal_prefix(size as f64) {
|
||||
let pretty_total = match NumberPrefix::decimal(size as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
@ -328,7 +330,8 @@ impl InstallerFramework {
|
||||
x.to_str()
|
||||
.log_expect("Unable to convert argument to String")
|
||||
.to_string()
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
{
|
||||
let new_app_file = match File::create(&args_file) {
|
||||
@ -393,6 +396,29 @@ impl InstallerFramework {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shuts down the installer instance.
|
||||
pub fn shutdown(&mut self) -> Result<(), String> {
|
||||
info!("Shutting down installer framework...");
|
||||
|
||||
if let Some(ref v) = self.launcher_path.take() {
|
||||
info!("Launching {:?}", v);
|
||||
|
||||
Command::new(v)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|x| format!("Unable to start application: {:?}", x))?;
|
||||
}
|
||||
|
||||
if self.burn_after_exit {
|
||||
info!("Requesting that self be deleted after exit.");
|
||||
native::burn_on_exit(&self.base_attributes.name);
|
||||
self.burn_after_exit = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new instance of the Installer Framework with a specified Config.
|
||||
pub fn new(attrs: BaseAttributes) -> Self {
|
||||
InstallerFramework {
|
||||
|
@ -17,7 +17,8 @@ pub fn setup_logger(file_name: String) -> Result<(), fern::InitError> {
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
}).level(log::LevelFilter::Info)
|
||||
})
|
||||
.level(log::LevelFilter::Info)
|
||||
.chain(io::stdout())
|
||||
.chain(fern::log_file(file_name)?)
|
||||
.apply()?;
|
||||
|
239
src/main.rs
239
src/main.rs
@ -7,9 +7,6 @@
|
||||
#![deny(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#[cfg(windows)]
|
||||
extern crate nfd;
|
||||
|
||||
extern crate web_view;
|
||||
|
||||
extern crate futures;
|
||||
@ -30,7 +27,7 @@ extern crate semver;
|
||||
|
||||
extern crate dirs;
|
||||
extern crate tar;
|
||||
extern crate xz_decom;
|
||||
extern crate xz2;
|
||||
extern crate zip;
|
||||
|
||||
extern crate fern;
|
||||
@ -40,66 +37,45 @@ extern crate log;
|
||||
extern crate chrono;
|
||||
|
||||
extern crate clap;
|
||||
|
||||
#[cfg(windows)]
|
||||
extern crate winapi;
|
||||
#[cfg(windows)]
|
||||
extern crate widestring;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
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 self_update;
|
||||
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;
|
||||
use std::process::Command;
|
||||
use std::{thread, time};
|
||||
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
|
||||
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");
|
||||
|
||||
logging::setup_logger(format!("{}_installer.log", config.name))
|
||||
.expect("Unable to setup logging!");
|
||||
|
||||
// Parse CLI arguments
|
||||
let app_name = config.name.clone();
|
||||
|
||||
let app_about = format!("An interactive installer for {}", app_name);
|
||||
@ -112,7 +88,8 @@ fn main() {
|
||||
.value_name("TARGET")
|
||||
.help("Launches the specified executable after checking for updates")
|
||||
.takes_value(true),
|
||||
).arg(
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("swap")
|
||||
.long("swap")
|
||||
.value_name("TARGET")
|
||||
@ -125,100 +102,19 @@ fn main() {
|
||||
|
||||
info!("{} installer", app_name);
|
||||
|
||||
// Handle self-updating if needed
|
||||
let current_exe = std::env::current_exe().log_expect("Current executable could not be found");
|
||||
let current_path = current_exe
|
||||
.parent()
|
||||
.log_expect("Parent directory of executable could not be found");
|
||||
|
||||
// Check to see if we are currently in a self-update
|
||||
if let Some(to_path) = matches.value_of("swap") {
|
||||
let to_path = PathBuf::from(to_path);
|
||||
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
info!(
|
||||
"Swapping installer from {} to {}",
|
||||
current_exe.display(),
|
||||
to_path.display()
|
||||
);
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = if cfg!(windows) {
|
||||
use std::fs::copy;
|
||||
|
||||
copy(¤t_exe, &to_path).map(|_x| ())
|
||||
} else {
|
||||
use std::fs::rename;
|
||||
|
||||
rename(¤t_exe, &to_path)
|
||||
};
|
||||
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Copy attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
let _: () = Err(e).log_expect("Copying new binary failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
self_update::perform_swap(¤t_exe, matches.value_of("swap"));
|
||||
if let Some(new_matches) = self_update::check_args(reinterpret_app, current_path) {
|
||||
matches = new_matches;
|
||||
}
|
||||
self_update::cleanup(current_path);
|
||||
|
||||
Command::new(to_path)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// If we just finished a update, we need to inject our previous command line arguments
|
||||
let args_file = current_path.join("args.json");
|
||||
|
||||
if args_file.exists() {
|
||||
let database: Vec<String> = {
|
||||
let metadata_file =
|
||||
File::open(&args_file).log_expect("Unable to open args file handle");
|
||||
|
||||
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
|
||||
};
|
||||
|
||||
matches = reinterpret_app.get_matches_from(database);
|
||||
|
||||
info!("Parsed command line arguments from original instance");
|
||||
remove_file(args_file).log_expect("Unable to clean up args file");
|
||||
}
|
||||
|
||||
// Cleanup any remaining new maintenance tool instances if they exist
|
||||
if cfg!(windows) {
|
||||
let updater_executable = current_path.join("maintenancetool_new.exe");
|
||||
|
||||
if updater_executable.exists() {
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = remove_file(&updater_executable);
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Cleanup attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
warn!("Deleting temp binary failed after 5 attempts: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load in metadata as to learn about the environment
|
||||
// Load in metadata + setup the installer framework
|
||||
let metadata_file = current_path.join("metadata.json");
|
||||
let mut framework = if metadata_file.exists() {
|
||||
info!("Using pre-existing metadata file: {:?}", metadata_file);
|
||||
@ -236,97 +132,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;
|
||||
|
||||
run(
|
||||
&format!("{} Installer", app_name),
|
||||
Content::Url(http_address),
|
||||
Some(size),
|
||||
resizable,
|
||||
debug,
|
||||
|_| {},
|
||||
|wv, msg, _| {
|
||||
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) => v,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let result =
|
||||
wv.dialog(Dialog::ChooseDirectory, "Select a install directory...", "");
|
||||
|
||||
if !result.is_empty() {
|
||||
let result = serde_json::to_string(&result)
|
||||
.log_expect("Unable to serialize response");
|
||||
let command = format!("{}({});", callback_name, result);
|
||||
debug!("Injecting response: {}", command);
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
(),
|
||||
);
|
||||
// Start up the UI
|
||||
frontend::launch(&app_name, is_launcher, framework);
|
||||
}
|
||||
|
@ -2,72 +2,95 @@
|
||||
* Misc interop helpers.
|
||||
**/
|
||||
|
||||
// Explicitly use the Unicode version of the APIs
|
||||
#ifndef UNICODE
|
||||
#define UNICODE
|
||||
#endif
|
||||
|
||||
#ifndef _UNICODE
|
||||
#define _UNICODE
|
||||
#endif
|
||||
|
||||
#include "windows.h"
|
||||
#include "winnls.h"
|
||||
#include "shobjidl.h"
|
||||
#include "objbase.h"
|
||||
#include "objidl.h"
|
||||
#include "shlguid.h"
|
||||
#include "shlobj.h"
|
||||
|
||||
// https://stackoverflow.com/questions/52101827/windows-10-getsyscolor-does-not-get-dark-ui-color-theme
|
||||
extern "C" int isDarkThemeActive()
|
||||
{
|
||||
DWORD type;
|
||||
DWORD value;
|
||||
DWORD count = 4;
|
||||
LSTATUS st = RegGetValue(
|
||||
HKEY_CURRENT_USER,
|
||||
TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"),
|
||||
TEXT("AppsUseLightTheme"),
|
||||
RRF_RT_REG_DWORD,
|
||||
&type,
|
||||
&value,
|
||||
&count);
|
||||
if (st == ERROR_SUCCESS && type == REG_DWORD)
|
||||
return value == 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" int saveShortcut(
|
||||
const char *shortcutPath,
|
||||
const char *description,
|
||||
const char *path,
|
||||
const char *args,
|
||||
const char *workingDir) {
|
||||
char* errStr = NULL;
|
||||
const wchar_t *shortcutPath,
|
||||
const wchar_t *description,
|
||||
const wchar_t *path,
|
||||
const wchar_t *args,
|
||||
const wchar_t *workingDir)
|
||||
{
|
||||
char *errStr = NULL;
|
||||
HRESULT h;
|
||||
IShellLink* shellLink = NULL;
|
||||
IPersistFile* persistFile = NULL;
|
||||
|
||||
#ifdef _WIN64
|
||||
wchar_t wName[MAX_PATH+1];
|
||||
#else
|
||||
WORD wName[MAX_PATH+1];
|
||||
#endif
|
||||
|
||||
int id;
|
||||
IShellLink *shellLink = NULL;
|
||||
IPersistFile *persistFile = NULL;
|
||||
|
||||
// Initialize the COM library
|
||||
h = CoInitialize(NULL);
|
||||
if (FAILED(h)) {
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to initialize COM library";
|
||||
goto err;
|
||||
}
|
||||
|
||||
h = CoCreateInstance( CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
|
||||
IID_IShellLink, (PVOID*)&shellLink );
|
||||
if (FAILED(h)) {
|
||||
h = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
|
||||
IID_IShellLink, (PVOID *)&shellLink);
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to create IShellLink";
|
||||
goto err;
|
||||
}
|
||||
|
||||
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID*)&persistFile);
|
||||
if (FAILED(h)) {
|
||||
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID *)&persistFile);
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to get IPersistFile";
|
||||
goto err;
|
||||
}
|
||||
|
||||
//Append the shortcut name to the folder
|
||||
MultiByteToWideChar(CP_UTF8,0,shortcutPath,-1,wName,MAX_PATH);
|
||||
|
||||
// Load the file if it exists, to get the values for anything
|
||||
// that we do not set. Ignore errors, such as if it does not exist.
|
||||
h = persistFile->Load(wName, 0);
|
||||
h = persistFile->Load(shortcutPath, 0);
|
||||
|
||||
// Set the fields for which the application has set a value
|
||||
if (description!=NULL)
|
||||
if (description != NULL)
|
||||
shellLink->SetDescription(description);
|
||||
if (path!=NULL)
|
||||
if (path != NULL)
|
||||
shellLink->SetPath(path);
|
||||
if (args!=NULL)
|
||||
if (args != NULL)
|
||||
shellLink->SetArguments(args);
|
||||
if (workingDir!=NULL)
|
||||
if (workingDir != NULL)
|
||||
shellLink->SetWorkingDirectory(workingDir);
|
||||
|
||||
//Save the shortcut to disk
|
||||
h = persistFile->Save(wName, TRUE);
|
||||
if (FAILED(h)) {
|
||||
h = persistFile->Save(shortcutPath, TRUE);
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to save shortcut";
|
||||
goto err;
|
||||
}
|
||||
@ -87,3 +110,53 @@ err:
|
||||
return h;
|
||||
}
|
||||
|
||||
extern "C" int spawnDetached(const wchar_t *app, const wchar_t *cmdline)
|
||||
{
|
||||
STARTUPINFOW si;
|
||||
PROCESS_INFORMATION pi;
|
||||
// make non-constant copy of the parameters
|
||||
// this is allowed per https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessw#security-remarks
|
||||
wchar_t *app_copy = _wcsdup(app);
|
||||
wchar_t *cmdline_copy = _wcsdup(cmdline);
|
||||
|
||||
if (app_copy == NULL || cmdline_copy == NULL)
|
||||
{
|
||||
return GetLastError();
|
||||
}
|
||||
|
||||
ZeroMemory(&si, sizeof(si));
|
||||
si.cb = sizeof(si);
|
||||
ZeroMemory(&pi, sizeof(pi));
|
||||
|
||||
if (!CreateProcessW(app, // module name
|
||||
(LPWSTR)cmdline, // Command line, unicode is allowed
|
||||
NULL, // Process handle not inheritable
|
||||
NULL, // Thread handle not inheritable
|
||||
FALSE, // Set handle inheritance to FALSE
|
||||
CREATE_NO_WINDOW, // Create without window
|
||||
NULL, // Use parent's environment block
|
||||
NULL, // Use parent's starting directory
|
||||
&si, // Pointer to STARTUPINFO structure
|
||||
&pi) // Pointer to PROCESS_INFORMATION structure
|
||||
)
|
||||
{
|
||||
return GetLastError();
|
||||
}
|
||||
|
||||
// Close process and thread handles.
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" HRESULT getSystemFolder(wchar_t *out_path)
|
||||
{
|
||||
PWSTR path = NULL;
|
||||
HRESULT result = SHGetKnownFolderPath(FOLDERID_System, 0, NULL, &path);
|
||||
if (result == S_OK)
|
||||
{
|
||||
wcscpy_s(out_path, MAX_PATH + 1, path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -3,8 +3,8 @@
|
||||
/// Basic definition of some running process.
|
||||
#[derive(Debug)]
|
||||
pub struct Process {
|
||||
pub pid : usize,
|
||||
pub name : String
|
||||
pub pid: usize,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@ -15,31 +15,40 @@ mod natives {
|
||||
|
||||
const PROCESS_LEN: usize = 10192;
|
||||
|
||||
use std::ffi::CString;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH};
|
||||
|
||||
use winapi::shared::winerror::HRESULT;
|
||||
use winapi::um::processthreadsapi::OpenProcess;
|
||||
use winapi::um::psapi::{
|
||||
EnumProcessModulesEx, GetModuleFileNameExW, K32EnumProcesses, LIST_MODULES_ALL,
|
||||
};
|
||||
use winapi::um::winnt::{
|
||||
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
|
||||
};
|
||||
use winapi::um::processthreadsapi::{OpenProcess};
|
||||
use winapi::um::psapi::{
|
||||
K32EnumProcesses,
|
||||
EnumProcessModulesEx, GetModuleFileNameExW, LIST_MODULES_ALL,
|
||||
};
|
||||
|
||||
use widestring::U16CString;
|
||||
|
||||
extern "C" {
|
||||
pub fn saveShortcut(
|
||||
shortcutPath: *const ::std::os::raw::c_char,
|
||||
description: *const ::std::os::raw::c_char,
|
||||
path: *const ::std::os::raw::c_char,
|
||||
args: *const ::std::os::raw::c_char,
|
||||
workingDir: *const ::std::os::raw::c_char,
|
||||
shortcutPath: *const winapi::ctypes::wchar_t,
|
||||
description: *const winapi::ctypes::wchar_t,
|
||||
path: *const winapi::ctypes::wchar_t,
|
||||
args: *const winapi::ctypes::wchar_t,
|
||||
workingDir: *const winapi::ctypes::wchar_t,
|
||||
) -> ::std::os::raw::c_int;
|
||||
|
||||
pub fn isDarkThemeActive() -> ::std::os::raw::c_uint;
|
||||
|
||||
pub fn spawnDetached(
|
||||
app: *const winapi::ctypes::wchar_t,
|
||||
cmdline: *const winapi::ctypes::wchar_t,
|
||||
) -> ::std::os::raw::c_int;
|
||||
|
||||
pub fn getSystemFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
@ -59,15 +68,16 @@ mod natives {
|
||||
|
||||
info!("Generating shortcut @ {:?}", source_file);
|
||||
|
||||
let native_target_dir = CString::new(source_file.clone())
|
||||
.log_expect("Error while converting to C-style string");
|
||||
let native_target_dir = U16CString::from_str(source_file.clone())
|
||||
.log_expect("Error while converting to wchar_t");
|
||||
let native_description =
|
||||
CString::new(description).log_expect("Error while converting to C-style string");
|
||||
U16CString::from_str(description).log_expect("Error while converting to wchar_t");
|
||||
let native_target =
|
||||
CString::new(target).log_expect("Error while converting to C-style string");
|
||||
let native_args = CString::new(args).log_expect("Error while converting to C-style string");
|
||||
U16CString::from_str(target).log_expect("Error while converting to wchar_t");
|
||||
let native_args =
|
||||
U16CString::from_str(args).log_expect("Error while converting to wchar_t");
|
||||
let native_working_dir =
|
||||
CString::new(working_dir).log_expect("Error while converting to C-style string");
|
||||
U16CString::from_str(working_dir).log_expect("Error while converting to wchar_t");
|
||||
|
||||
let shortcutResult = unsafe {
|
||||
saveShortcut(
|
||||
@ -108,15 +118,41 @@ mod natives {
|
||||
.log_expect("Unable to convert log path to string")
|
||||
.replace(" ", "\\ ");
|
||||
|
||||
let target_arguments = format!("ping 127.0.0.1 -n 3 > nul && del {} {}", tool, log);
|
||||
let target_arguments = format!("/C choice /C Y /N /D Y /T 2 & del {} {}", tool, log);
|
||||
|
||||
info!("Launching cmd with {:?}", target_arguments);
|
||||
|
||||
Command::new("C:\\Windows\\system32\\cmd.exe")
|
||||
.arg("/C")
|
||||
.arg(&target_arguments)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
// Needs to use `spawnDetached` which is an unsafe C/C++ function from interop.cpp
|
||||
#[allow(unsafe_code)]
|
||||
let spawn_result: i32 = unsafe {
|
||||
let mut cmd_path = [0u16; MAX_PATH + 1];
|
||||
let result = getSystemFolder(cmd_path.as_mut_ptr());
|
||||
let mut pos = 0;
|
||||
for x in cmd_path.iter() {
|
||||
if *x == 0 {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
if result != winapi::shared::winerror::S_OK {
|
||||
return;
|
||||
}
|
||||
|
||||
spawnDetached(
|
||||
U16CString::from_str(
|
||||
format!("{}\\cmd.exe", String::from_utf16_lossy(&cmd_path[..pos])).as_str(),
|
||||
)
|
||||
.log_expect("Unable to convert string to wchar_t")
|
||||
.as_ptr(),
|
||||
U16CString::from_str(target_arguments.as_str())
|
||||
.log_expect("Unable to convert string to wchar_t")
|
||||
.as_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
if spawn_result != 0 {
|
||||
warn!("Unable to start child process");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
@ -149,9 +185,7 @@ mod natives {
|
||||
|
||||
let size = ::std::mem::size_of::<DWORD>() * process_ids.len();
|
||||
unsafe {
|
||||
if K32EnumProcesses(process_ids.as_mut_ptr(),
|
||||
size as DWORD,
|
||||
&mut cb_needed) == 0 {
|
||||
if K32EnumProcesses(process_ids.as_mut_ptr(), size as DWORD, &mut cb_needed) == 0 {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
@ -160,7 +194,7 @@ mod natives {
|
||||
|
||||
let mut processes = Vec::new();
|
||||
|
||||
for i in 0 .. nb_processes {
|
||||
for i in 0..nb_processes {
|
||||
let pid = process_ids[i as usize];
|
||||
|
||||
unsafe {
|
||||
@ -169,20 +203,25 @@ mod natives {
|
||||
let mut process_name = [0u16; MAX_PATH + 1];
|
||||
let mut cb_needed = 0;
|
||||
|
||||
if EnumProcessModulesEx(process_handler,
|
||||
if EnumProcessModulesEx(
|
||||
process_handler,
|
||||
&mut h_mod,
|
||||
::std::mem::size_of::<DWORD>() as DWORD,
|
||||
&mut cb_needed,
|
||||
LIST_MODULES_ALL) != 0 {
|
||||
GetModuleFileNameExW(process_handler,
|
||||
LIST_MODULES_ALL,
|
||||
) != 0
|
||||
{
|
||||
GetModuleFileNameExW(
|
||||
process_handler,
|
||||
h_mod,
|
||||
process_name.as_mut_ptr(),
|
||||
MAX_PATH as DWORD + 1);
|
||||
MAX_PATH as DWORD + 1,
|
||||
);
|
||||
|
||||
let mut pos = 0;
|
||||
for x in process_name.iter() {
|
||||
if *x == 0 {
|
||||
break
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
@ -199,6 +238,12 @@ mod natives {
|
||||
|
||||
processes
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
#[allow(unsafe_code)]
|
||||
pub fn is_dark_mode_active() -> bool {
|
||||
unsafe { isDarkThemeActive() == 1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
@ -209,6 +254,15 @@ mod natives {
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use sysinfo::{ProcessExt, SystemExt};
|
||||
|
||||
use dirs;
|
||||
|
||||
use slug::slugify;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn create_shortcut(
|
||||
name: &str,
|
||||
description: &str,
|
||||
@ -216,9 +270,51 @@ mod natives {
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
) -> Result<String, String> {
|
||||
// TODO: no-op
|
||||
warn!("create_shortcut is stubbed!");
|
||||
// FIXME: no icon will be shown since no icon is provided
|
||||
let data_local_dir = dirs::data_local_dir();
|
||||
match data_local_dir {
|
||||
Some(x) => {
|
||||
let mut path = x;
|
||||
path.push("applications");
|
||||
match create_dir_all(path.to_path_buf()) {
|
||||
Ok(_) => (()),
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Local data directory does not exist and cannot be created: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
};
|
||||
path.push(format!("{}.desktop", slugify(name))); // file name
|
||||
let desktop_file = format!(
|
||||
"[Desktop Entry]\nName={}\nExec=\"{}\" {}\nComment={}\nPath={}\n",
|
||||
name, target, args, description, working_dir
|
||||
);
|
||||
let desktop_f = File::create(path);
|
||||
let mut desktop_f = match desktop_f {
|
||||
Ok(file) => file,
|
||||
Err(e) => return Err(format!("Unable to create desktop file: {}", e)),
|
||||
};
|
||||
let mut desktop_f = desktop_f.write_all(desktop_file.as_bytes());
|
||||
match desktop_f {
|
||||
Ok(_) => Ok("".to_string()),
|
||||
Err(e) => Err(format!("Unable to write desktop file: {}", e)),
|
||||
}
|
||||
}
|
||||
// return error when failed to acquire local data directory
|
||||
None => Err("Unable to determine local data directory".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn create_shortcut(
|
||||
name: &str,
|
||||
description: &str,
|
||||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
) -> Result<String, String> {
|
||||
warn!("STUB! Creating shortcut is not implemented on macOS");
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
@ -242,8 +338,23 @@ mod natives {
|
||||
|
||||
/// Returns a list of running processes
|
||||
pub fn get_process_names() -> Vec<super::Process> {
|
||||
// TODO: no-op
|
||||
vec![]
|
||||
// a platform-independent implementation using sysinfo crate
|
||||
let mut processes: Vec<super::Process> = Vec::new();
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_all();
|
||||
for (pid, procs) in system.get_process_list() {
|
||||
processes.push(super::Process {
|
||||
pid: *pid as usize,
|
||||
name: procs.name().to_string(),
|
||||
});
|
||||
}
|
||||
processes // return running processes
|
||||
}
|
||||
|
||||
/// Returns if dark mode is active on this system.
|
||||
pub fn is_dark_mode_active() -> bool {
|
||||
// No-op
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
473
src/rest.rs
473
src/rest.rs
@ -1,473 +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)
|
||||
}
|
111
src/self_update.rs
Normal file
111
src/self_update.rs
Normal file
@ -0,0 +1,111 @@
|
||||
//! self_update.rs
|
||||
//!
|
||||
//! Handles different components of self-updating.
|
||||
|
||||
use std::fs::{remove_file, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{exit, Command};
|
||||
use std::{thread, time};
|
||||
|
||||
use clap::{App, ArgMatches};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
/// Swaps around the main executable if needed.
|
||||
pub fn perform_swap(current_exe: &PathBuf, to_path: Option<&str>) {
|
||||
// Check to see if we are currently in a self-update
|
||||
if let Some(to_path) = to_path {
|
||||
let to_path = PathBuf::from(to_path);
|
||||
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
info!(
|
||||
"Swapping installer from {} to {}",
|
||||
current_exe.display(),
|
||||
to_path.display()
|
||||
);
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = if cfg!(windows) {
|
||||
use std::fs::copy;
|
||||
|
||||
copy(¤t_exe, &to_path).map(|_x| ())
|
||||
} else {
|
||||
use std::fs::rename;
|
||||
|
||||
rename(¤t_exe, &to_path)
|
||||
};
|
||||
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Copy attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
Err::<(), _>(e).log_expect("Copying new binary failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::new(to_path)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_args<'a>(app: App<'a, '_>, current_path: &Path) -> Option<ArgMatches<'a>> {
|
||||
// If we just finished a update, we need to inject our previous command line arguments
|
||||
let args_file = current_path.join("args.json");
|
||||
|
||||
if args_file.exists() {
|
||||
let database: Vec<String> = {
|
||||
let metadata_file =
|
||||
File::open(&args_file).log_expect("Unable to open args file handle");
|
||||
|
||||
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
|
||||
};
|
||||
|
||||
let matches = app.get_matches_from(database);
|
||||
|
||||
info!("Parsed command line arguments from original instance");
|
||||
remove_file(args_file).log_expect("Unable to clean up args file");
|
||||
|
||||
Some(matches)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(current_path: &Path) {
|
||||
// Cleanup any remaining new maintenance tool instances if they exist
|
||||
if cfg!(windows) {
|
||||
let updater_executable = current_path.join("maintenancetool_new.exe");
|
||||
|
||||
if updater_executable.exists() {
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = remove_file(&updater_executable);
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Cleanup attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
warn!("Deleting temp binary failed after 5 attempts: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -41,17 +41,19 @@ impl ReleaseSource for GithubReleases {
|
||||
.get(&format!(
|
||||
"https://api.github.com/repos/{}/releases",
|
||||
config.repo
|
||||
)).header(USER_AGENT, "liftinstall (j-selby)")
|
||||
))
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.send()
|
||||
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::FORBIDDEN => {
|
||||
return Err(format!(
|
||||
return Err(
|
||||
"GitHub is rate limiting you. Try moving to a internet connection \
|
||||
that isn't shared, and/or disabling VPNs."
|
||||
));
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Bad status code: {:?}.", response.status()));
|
||||
@ -87,14 +89,18 @@ impl ReleaseSource for GithubReleases {
|
||||
let string = match asset["name"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release name".to_string())
|
||||
return Err(
|
||||
"JSON payload missing information about release name".to_string()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -9,7 +9,7 @@ pub mod github;
|
||||
use self::types::ReleaseSource;
|
||||
|
||||
/// Returns a ReleaseSource by a name, if possible
|
||||
pub fn get_by_name(name: &str) -> Option<Box<ReleaseSource>> {
|
||||
pub fn get_by_name(name: &str) -> Option<Box<dyn ReleaseSource>> {
|
||||
match name {
|
||||
"github" => Some(Box::new(github::GithubReleases::new())),
|
||||
_ => None,
|
||||
|
@ -12,7 +12,7 @@ use tasks::resolver::ResolvePackageTask;
|
||||
|
||||
use http::stream_file;
|
||||
|
||||
use number_prefix::{decimal_prefix, Prefixed, Standalone};
|
||||
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
@ -25,7 +25,7 @@ impl Task for DownloadPackageTask {
|
||||
&mut self,
|
||||
mut input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 1);
|
||||
|
||||
@ -68,11 +68,11 @@ impl Task for DownloadPackageTask {
|
||||
};
|
||||
|
||||
// Pretty print data volumes
|
||||
let pretty_current = match decimal_prefix(downloaded as f64) {
|
||||
let pretty_current = match NumberPrefix::decimal(downloaded as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
let pretty_total = match decimal_prefix(size as f64) {
|
||||
let pretty_total = match NumberPrefix::decimal(size as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
|
@ -7,8 +7,8 @@ use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
|
||||
use native::Process;
|
||||
use native::get_process_names;
|
||||
use native::Process;
|
||||
|
||||
use std::process;
|
||||
|
||||
@ -19,7 +19,7 @@ impl Task for EnsureOnlyInstanceTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
_messenger: &Fn(&TaskMessage),
|
||||
_messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
|
||||
@ -32,13 +32,13 @@ impl Task for EnsureOnlyInstanceTask {
|
||||
let exe = name;
|
||||
|
||||
if exe.ends_with("maintenancetool.exe") || exe.ends_with("maintenancetool") {
|
||||
return Err(format!("Maintenance tool is already running!"));
|
||||
return Err("Maintenance tool is already running!".to_string());
|
||||
}
|
||||
|
||||
for package in &context.database.packages {
|
||||
for file in &package.files {
|
||||
if exe.ends_with(file) {
|
||||
return Err(format!("The installed application is currently running!"));
|
||||
return Err("The installed application is currently running!".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,6 @@ impl Task for EnsureOnlyInstanceTask {
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!("EnsureOnlyInstanceTask")
|
||||
"EnsureOnlyInstanceTask".to_string()
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ impl Task for InstallTask {
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
_: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
||||
Ok(TaskParamType::None)
|
||||
|
@ -21,7 +21,7 @@ impl Task for VerifyInstallDirTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
|
@ -20,7 +20,7 @@ impl Task for InstallGlobalShortcutsTask {
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
"Generating global shortcut...",
|
||||
|
@ -34,7 +34,7 @@ impl Task for InstallPackageTask {
|
||||
&mut self,
|
||||
mut input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
&format!("Installing package {:?}...", self.name),
|
||||
|
@ -22,7 +22,7 @@ impl Task for InstallShortcutsTask {
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
&format!("Generating shortcuts for package {:?}...", self.name),
|
||||
|
@ -49,12 +49,12 @@ pub enum TaskOrdering {
|
||||
/// A dependency of a task with various properties.
|
||||
pub struct TaskDependency {
|
||||
ordering: TaskOrdering,
|
||||
task: Box<Task>,
|
||||
task: Box<dyn Task>,
|
||||
}
|
||||
|
||||
impl TaskDependency {
|
||||
/// Builds a new dependency from the specified task.
|
||||
pub fn build(ordering: TaskOrdering, task: Box<Task>) -> TaskDependency {
|
||||
pub fn build(ordering: TaskOrdering, task: Box<dyn Task>) -> TaskDependency {
|
||||
TaskDependency { ordering, task }
|
||||
}
|
||||
}
|
||||
@ -74,7 +74,7 @@ pub trait Task {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String>;
|
||||
|
||||
/// Returns a vector containing all dependencies that need to be executed
|
||||
@ -87,7 +87,7 @@ pub trait Task {
|
||||
|
||||
/// The dependency tree allows for smart iteration on a Task struct.
|
||||
pub struct DependencyTree {
|
||||
task: Box<Task>,
|
||||
task: Box<dyn Task>,
|
||||
dependencies: Vec<(TaskOrdering, DependencyTree)>,
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ impl DependencyTree {
|
||||
pub fn execute(
|
||||
&mut self,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
let total_tasks = (self.dependencies.len() + 1) as f64;
|
||||
|
||||
@ -133,8 +133,8 @@ impl DependencyTree {
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
msg,
|
||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||
@ -159,8 +159,8 @@ impl DependencyTree {
|
||||
|
||||
let task_result = self
|
||||
.task
|
||||
.execute(inputs, context, &|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
.execute(inputs, context, &|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
msg,
|
||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||
@ -179,8 +179,8 @@ impl DependencyTree {
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
msg,
|
||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||
@ -206,7 +206,7 @@ impl DependencyTree {
|
||||
}
|
||||
|
||||
/// Builds a new pipeline from the specified task, iterating on dependencies.
|
||||
pub fn build(task: Box<Task>) -> DependencyTree {
|
||||
pub fn build(task: Box<dyn Task>) -> DependencyTree {
|
||||
let dependencies = task
|
||||
.dependencies()
|
||||
.into_iter()
|
||||
|
@ -24,7 +24,7 @@ impl Task for ResolvePackageTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
let mut metadata: Option<PackageDescription> = None;
|
||||
|
@ -14,7 +14,7 @@ impl Task for SaveDatabaseTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
|
@ -23,7 +23,7 @@ impl Task for SaveExecutableTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
|
@ -19,7 +19,7 @@ impl Task for UninstallTask {
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
_: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
||||
Ok(TaskParamType::None)
|
||||
|
@ -18,7 +18,7 @@ impl Task for UninstallGlobalShortcutsTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
|
||||
|
@ -27,7 +27,7 @@ impl Task for UninstallPackageTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 1);
|
||||
|
||||
|
@ -24,7 +24,7 @@ impl Task for UninstallShortcutsTask {
|
||||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
|
||||
|
1
static/css/bulma.min.css
vendored
1
static/css/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1,88 +0,0 @@
|
||||
/* roboto-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/roboto-v18-latin-regular.eot'); /* IE9 Compat Modes */
|
||||
src: local('Roboto'), local('Roboto-Regular'),
|
||||
url('../fonts/roboto-v18-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/roboto-v18-latin-regular.woff') format('woff');
|
||||
}
|
||||
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body, div, span, h1, h2, h3, h4, h5, h6 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
pre {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tile.is-child > .box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.has-padding {
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clickable-box {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable-box label {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.is-max-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-bottom-floating {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.is-right-floating {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.has-padding .is-right-floating {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.is-left-floating {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.has-padding .is-left-floating {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title id="window-title">... Installer</title>
|
||||
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
|
||||
<link rel="stylesheet" href="/css/bulma.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="/css/main.css" type="text/css">
|
||||
</head>
|
||||
<body class="is-max-height">
|
||||
<div class="fullscreen" id="ie-blackout" style="display: none">
|
||||
<div class="title">Your computer is out of date.</div>
|
||||
<div class="subtitle">
|
||||
Make sure that your computer is up to date, and that you have Internet Explorer 11 installed.
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
Please note we do not support pirated or unsupported versions of Windows.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
if (!document.__proto__) {
|
||||
document.getElementById("ie-blackout").style.display = "block";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="app" class="is-max-height">
|
||||
<section class="section is-max-height">
|
||||
<div class="container is-max-height">
|
||||
<div class="columns is-max-height">
|
||||
<div class="column is-one-third has-padding" v-if="!metadata.is_launcher">
|
||||
<img src="/logo.png" width="60%" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 class="subtitle" v-if="!metadata.preexisting_install">
|
||||
Welcome to the {{ attrs.name }} installer!
|
||||
</h2>
|
||||
<h2 class="subtitle" v-if="!metadata.preexisting_install">
|
||||
We will have you up and running in just a few moments.
|
||||
</h2>
|
||||
|
||||
<h2 class="subtitle" v-if="metadata.preexisting_install">
|
||||
Welcome to the {{ attrs.name }} Maintenance Tool.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/js/vue.min.js" type="text/javascript"></script>
|
||||
<script src="/js/vue-router.min.js" type="text/javascript"></script>
|
||||
<script src="/api/attrs" type="text/javascript"></script>
|
||||
<script src="/js/helpers.js" type="text/javascript"></script>
|
||||
<script src="/js/views.js" type="text/javascript"></script>
|
||||
<script src="/js/main.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,144 +0,0 @@
|
||||
/**
|
||||
* helpers.js
|
||||
*
|
||||
* Additional state-less helper methods.
|
||||
*/
|
||||
|
||||
var request_id = 0;
|
||||
|
||||
/**
|
||||
* Makes a AJAX request.
|
||||
*
|
||||
* @param path The path to connect to.
|
||||
* @param successCallback A callback with a JSON payload.
|
||||
* @param failCallback A fail callback. Optional.
|
||||
* @param data POST data. Optional.
|
||||
*/
|
||||
function ajax(path, successCallback, failCallback, data) {
|
||||
if (failCallback === undefined) {
|
||||
failCallback = defaultFailHandler;
|
||||
}
|
||||
|
||||
console.log("Making HTTP request to " + path);
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
req.addEventListener("load", function() {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200 && this.getResponseHeader('Content-Type').indexOf("application/json") !== -1) {
|
||||
successCallback(JSON.parse(this.responseText));
|
||||
} else {
|
||||
failCallback(this.responseText);
|
||||
}
|
||||
});
|
||||
req.addEventListener("error", failCallback);
|
||||
|
||||
req.open(data == null ? "GET" : "POST", path + "?nocache=" + request_id++, true);
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
if (data != null) {
|
||||
var form = "";
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (form !== "") {
|
||||
form += "&";
|
||||
}
|
||||
|
||||
form += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
|
||||
}
|
||||
|
||||
req.send(form);
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a AJAX request, streaming each line as it arrives. Type should be text/plain,
|
||||
* each line will be interpreted as JSON separately.
|
||||
*
|
||||
* @param path The path to connect to.
|
||||
* @param callback A callback with a JSON payload. Called for every line as it comes.
|
||||
* @param successCallback A callback with a raw text payload.
|
||||
* @param failCallback A fail callback. Optional.
|
||||
* @param data POST data. Optional.
|
||||
*/
|
||||
function stream_ajax(path, callback, successCallback, failCallback, data) {
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
console.log("Making streaming HTTP request to " + path);
|
||||
|
||||
req.addEventListener("load", function() {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200) {
|
||||
successCallback(this.responseText);
|
||||
} else {
|
||||
failCallback(this.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
var buffer = "";
|
||||
var seenBytes = 0;
|
||||
|
||||
req.onreadystatechange = function() {
|
||||
if(req.readyState > 2) {
|
||||
buffer += req.responseText.substr(seenBytes);
|
||||
|
||||
var pointer;
|
||||
while ((pointer = buffer.indexOf("\n")) >= 0) {
|
||||
var line = buffer.substring(0, pointer).trim();
|
||||
buffer = buffer.substring(pointer + 1);
|
||||
|
||||
if (line.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var contents = JSON.parse(line);
|
||||
callback(contents);
|
||||
}
|
||||
|
||||
seenBytes = req.responseText.length;
|
||||
}
|
||||
};
|
||||
|
||||
req.addEventListener("error", failCallback);
|
||||
|
||||
req.open(data == null ? "GET" : "POST", path + "?nocache=" + request_id++, true);
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
if (data != null) {
|
||||
var form = "";
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (form !== "") {
|
||||
form += "&";
|
||||
}
|
||||
|
||||
form += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
|
||||
}
|
||||
|
||||
req.send(form);
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default handler if a AJAX request fails. Not to be used directly.
|
||||
*
|
||||
* @param e The XMLHttpRequest that failed.
|
||||
*/
|
||||
function defaultFailHandler(e) {
|
||||
console.error("A AJAX request failed, and was not caught:");
|
||||
console.error(e);
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
// Overwrite loggers with the logging backend
|
||||
if (window.external !== undefined && window.external.invoke !== undefined) {
|
||||
window.onerror = function(msg, url, line) {
|
||||
old_onerror(msg, url, line);
|
||||
window.external.invoke(JSON.stringify({
|
||||
Log: {
|
||||
kind: "error",
|
||||
msg: msg + " @ " + url + ":" + line
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
// Borrowed from http://tobyho.com/2012/07/27/taking-over-console-log/
|
||||
function intercept(method){
|
||||
console[method] = function(){
|
||||
var message = Array.prototype.slice.apply(arguments).join(' ');
|
||||
window.external.invoke(JSON.stringify({
|
||||
Log: {
|
||||
kind: method,
|
||||
msg: message
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
var methods = ['log', 'warn', 'error'];
|
||||
for (var i = 0; i < methods.length; i++) {
|
||||
intercept(methods[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable F5
|
||||
function disable_shortcuts(e) {
|
||||
switch (e.keyCode) {
|
||||
case 116: // F5
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", disable_shortcuts);
|
||||
|
||||
document.getElementById("window-title").innerText = base_attributes.name + " Installer";
|
||||
|
||||
function selectFileCallback(name) {
|
||||
app.install_location = name;
|
||||
}
|
||||
|
||||
var app = new Vue({
|
||||
router: router,
|
||||
data: {
|
||||
attrs: base_attributes,
|
||||
config : {},
|
||||
install_location : "",
|
||||
// If the option to pick an install location should be provided
|
||||
show_install_location : true,
|
||||
metadata : {
|
||||
database : [],
|
||||
install_path : "",
|
||||
preexisting_install : false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
"exit": function() {
|
||||
ajax("/api/exit", function() {});
|
||||
}
|
||||
}
|
||||
}).$mount("#app");
|
@ -1,460 +0,0 @@
|
||||
const DownloadConfig = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Downloading config...</h4>
|
||||
|
||||
<br />
|
||||
<progress class="progress is-info is-medium" value="0" max="100">
|
||||
0%
|
||||
</progress>
|
||||
</div>
|
||||
`,
|
||||
created: function() {
|
||||
this.download_install_status();
|
||||
},
|
||||
methods: {
|
||||
download_install_status: function() {
|
||||
var that = this; // IE workaround
|
||||
|
||||
ajax("/api/installation-status", function(e) {
|
||||
app.metadata = e;
|
||||
|
||||
that.download_config();
|
||||
});
|
||||
},
|
||||
download_config: function() {
|
||||
var that = this; // IE workaround
|
||||
|
||||
ajax("/api/config", function(e) {
|
||||
app.config = e;
|
||||
|
||||
that.choose_next_state();
|
||||
|
||||
}, function(e) {
|
||||
console.error("Got error while downloading config: "
|
||||
+ e);
|
||||
|
||||
if (app.metadata.is_launcher) {
|
||||
// Just launch the target application
|
||||
app.exit();
|
||||
} else {
|
||||
router.replace({name: 'showerr', params: {msg: "Got error while downloading config: "
|
||||
+ e}});
|
||||
}
|
||||
});
|
||||
},
|
||||
choose_next_state: function() {
|
||||
// Update the updater if needed
|
||||
if (app.config.new_tool) {
|
||||
router.push("/install/updater");
|
||||
return;
|
||||
}
|
||||
|
||||
if (app.metadata.preexisting_install) {
|
||||
app.install_location = app.metadata.install_path;
|
||||
|
||||
// Copy over installed packages
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
app.config.packages[x].default = false;
|
||||
app.config.packages[x].installed = false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < app.metadata.database.packages.length; i++) {
|
||||
// Find this config package
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
if (app.config.packages[x].name === app.metadata.database.packages[i].name) {
|
||||
app.config.packages[x].default = true;
|
||||
app.config.packages[x].installed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (app.metadata.is_launcher) {
|
||||
router.replace("/install/regular");
|
||||
} else {
|
||||
router.replace("/modify");
|
||||
}
|
||||
} else {
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
app.config.packages[x].installed = false;
|
||||
}
|
||||
|
||||
// Need to do a bit more digging to get at the
|
||||
// install location.
|
||||
ajax("/api/default-path", function(e) {
|
||||
if (e.path != null) {
|
||||
app.install_location = e.path;
|
||||
}
|
||||
});
|
||||
|
||||
router.replace("/packages");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const SelectPackages = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Select which packages you want to install:</h4>
|
||||
|
||||
<!-- Build options -->
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent" v-for="package in $root.$data.config.packages" :index="package.name">
|
||||
<div class="tile is-child">
|
||||
<div class="box clickable-box" v-on:click.capture.stop="package.default = !package.default">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="package.default" />
|
||||
{{ package.name }}
|
||||
<span v-if="package.installed"><i>(installed)</i></span>
|
||||
</label>
|
||||
<p>
|
||||
{{ package.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install Location</div>
|
||||
<div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="$root.$data.install_location"
|
||||
placeholder="Enter a install path here">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-dark" v-on:click="select_file">
|
||||
Select
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="is-right-floating is-bottom-floating">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
|
||||
v-on:click="advanced = true">Advanced...</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
|
||||
v-on:click="install">Install</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
|
||||
v-on:click="install">Modify</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-left-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install"
|
||||
v-on:click="go_back">Back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
advanced: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select_file: function() {
|
||||
window.external.invoke(JSON.stringify({
|
||||
SelectInstallDir: {
|
||||
callback_name: "selectFileCallback"
|
||||
}
|
||||
}));
|
||||
},
|
||||
install: function() {
|
||||
router.push("/install/regular");
|
||||
},
|
||||
go_back: function() {
|
||||
router.go(-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const InstallPackages = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle" v-if="$root.$data.metadata.is_launcher || is_update">Checking for updates...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_uninstall">Uninstalling...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_updater_update">Downloading self-update...</h4>
|
||||
<h4 class="subtitle" v-else>Installing...</h4>
|
||||
<div v-html="$root.$data.config.installing_message"></div>
|
||||
<br />
|
||||
|
||||
<div v-html="progress_message"></div>
|
||||
<progress class="progress is-info is-medium" v-bind:value="progress" max="100">
|
||||
{{ progress }}%
|
||||
</progress>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
progress: 0.0,
|
||||
progress_message: "Please wait...",
|
||||
is_uninstall: false,
|
||||
is_updater_update: false,
|
||||
is_update: false,
|
||||
failed_with_error: false,
|
||||
packages_installed: 0
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.is_uninstall = this.$route.params.kind === "uninstall";
|
||||
this.is_updater_update = this.$route.params.kind === "updater";
|
||||
this.is_update = this.$route.params.kind === "update";
|
||||
console.log("Installer kind: " + this.$route.params.kind);
|
||||
this.install();
|
||||
},
|
||||
methods: {
|
||||
install: function() {
|
||||
var results = {};
|
||||
|
||||
for (var package_index = 0; package_index < app.config.packages.length; package_index++) {
|
||||
var current_package = app.config.packages[package_index];
|
||||
if (current_package.default != null) {
|
||||
results[current_package.name] = current_package.default;
|
||||
}
|
||||
}
|
||||
|
||||
results["path"] = app.install_location;
|
||||
|
||||
var that = this; // IE workaround
|
||||
|
||||
var targetUrl = "/api/start-install";
|
||||
if (this.is_uninstall) {
|
||||
targetUrl = "/api/uninstall";
|
||||
}
|
||||
if (this.is_updater_update) {
|
||||
targetUrl = "/api/update-updater";
|
||||
}
|
||||
|
||||
stream_ajax(targetUrl, function(line) {
|
||||
if (line.hasOwnProperty("Status")) {
|
||||
that.progress_message = line.Status[0];
|
||||
that.progress = line.Status[1] * 100;
|
||||
}
|
||||
|
||||
if (line.hasOwnProperty("PackageInstalled")) {
|
||||
that.packages_installed += 1;
|
||||
}
|
||||
|
||||
if (line.hasOwnProperty("Error")) {
|
||||
if (app.metadata.is_launcher) {
|
||||
app.exit();
|
||||
} else {
|
||||
that.failed_with_error = true;
|
||||
router.replace({name: 'showerr', params: {msg: line.Error}});
|
||||
}
|
||||
}
|
||||
}, function(e) {
|
||||
if (that.is_updater_update) {
|
||||
// Continue with what we were doing
|
||||
if (app.metadata.is_launcher) {
|
||||
router.replace("/install/regular");
|
||||
} else {
|
||||
if (app.metadata.preexisting_install) {
|
||||
router.replace("/modify");
|
||||
} else {
|
||||
router.replace("/packages");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (app.metadata.is_launcher) {
|
||||
app.exit();
|
||||
} else if (!that.failed_with_error) {
|
||||
if (that.is_uninstall) {
|
||||
router.replace({name: 'complete', params: {
|
||||
uninstall: true,
|
||||
update: that.is_update,
|
||||
installed: that.packages_installed
|
||||
}});
|
||||
} else {
|
||||
router.replace({name: 'complete', params: {
|
||||
uninstall: false,
|
||||
update: that.is_update,
|
||||
installed: that.packages_installed
|
||||
}});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, results);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ErrorView = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">An error occurred:</h4>
|
||||
|
||||
<pre>{{ msg }}</pre>
|
||||
|
||||
<div class="field is-grouped is-right-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-primary is-medium" v-if="remaining" v-on:click="go_back">Back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
msg: this.$route.params.msg,
|
||||
remaining: window.history.length > 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
go_back: function() {
|
||||
router.go(-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const CompleteView = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<div v-if="was_update">
|
||||
<div v-if="has_installed">
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been updated.</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} is already up to date!</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="was_install">
|
||||
<h4 class="subtitle">Thanks for installing {{ $root.$data.attrs.name }}!</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
|
||||
<img src="/how-to-open.png" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been uninstalled.</h4>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-right-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-on:click="exit">Exit</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
was_install: !this.$route.params.uninstall,
|
||||
was_update: this.$route.params.update,
|
||||
has_installed: this.$route.params.packages_installed > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exit: function() {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ModifyView = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Choose an option:</h4>
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="update">
|
||||
Update
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="modify_packages">
|
||||
Modify
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="prepare_uninstall">
|
||||
Uninstall
|
||||
</a>
|
||||
|
||||
<div class="modal is-active" v-if="show_uninstall">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Are you sure you want to uninstall {{ $root.$data.attrs.name }}?</p>
|
||||
</header>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-danger" v-on:click="uninstall">Yes</button>
|
||||
<button class="button" v-on:click="cancel_uninstall">No</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
show_uninstall: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function() {
|
||||
router.push("/install/update");
|
||||
},
|
||||
modify_packages: function() {
|
||||
router.push("/packages");
|
||||
},
|
||||
prepare_uninstall: function() {
|
||||
this.show_uninstall = true;
|
||||
},
|
||||
cancel_uninstall: function() {
|
||||
this.show_uninstall = false;
|
||||
},
|
||||
uninstall: function() {
|
||||
router.push("/install/uninstall");
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{
|
||||
path: '/config',
|
||||
name: 'config',
|
||||
component: DownloadConfig
|
||||
},
|
||||
{
|
||||
path: '/packages',
|
||||
name: 'packages',
|
||||
component: SelectPackages
|
||||
},
|
||||
{
|
||||
path: '/install/:kind',
|
||||
name: 'install',
|
||||
component: InstallPackages
|
||||
},
|
||||
{
|
||||
path: '/showerr',
|
||||
name: 'showerr',
|
||||
component: ErrorView
|
||||
},
|
||||
{
|
||||
path: '/complete/:uninstall/:update/:packages_installed',
|
||||
name: 'complete',
|
||||
component: CompleteView
|
||||
},
|
||||
{
|
||||
path: '/modify',
|
||||
name: 'modify',
|
||||
component: ModifyView
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/config'
|
||||
}
|
||||
]
|
||||
});
|
6
static/js/vue-router.min.js
vendored
6
static/js/vue-router.min.js
vendored
File diff suppressed because one or more lines are too long
6
static/js/vue.min.js
vendored
6
static/js/vue.min.js
vendored
File diff suppressed because one or more lines are too long
3
ui/.browserslistrc
Normal file
3
ui/.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 11
|
5
ui/.editorconfig
Normal file
5
ui/.editorconfig
Normal file
@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
17
ui/.eslintrc.js
Normal file
17
ui/.eslintrc.js
Normal file
@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
}
|
||||
}
|
21
ui/.gitignore
vendored
Normal file
21
ui/.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
29
ui/README.md
Normal file
29
ui/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# ui
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
yarn run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
ui/babel.config.js
Normal file
5
ui/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
95
ui/mock-server.js
Normal file
95
ui/mock-server.js
Normal file
@ -0,0 +1,95 @@
|
||||
'use strict'
|
||||
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const port = 3000
|
||||
|
||||
function progressSimulation (res) {
|
||||
var progress = 0.0
|
||||
var timer = setInterval(() => {
|
||||
var resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n'
|
||||
progress += 0.1
|
||||
res.write(resp)
|
||||
if (progress >= 1) {
|
||||
res.status(200).end()
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
function returnConfig (res) {
|
||||
res.json({
|
||||
installing_message:
|
||||
'Test Banner <strong>Bold</strong> <pre>Code block</pre> <i>Italic</i> <del>Strike</del>',
|
||||
new_tool: null,
|
||||
packages: [
|
||||
{
|
||||
name: 'Test 1',
|
||||
description: 'LiftInstall GUI Test 1',
|
||||
default: true,
|
||||
source: {
|
||||
name: 'github',
|
||||
match: '^test$',
|
||||
config: { repo: 'j-selby/liftinstall' }
|
||||
},
|
||||
shortcuts: []
|
||||
},
|
||||
{
|
||||
name: 'Test 2',
|
||||
description:
|
||||
'Different Banner <strong>Bold</strong> <pre>Code block</pre> <i>Italic</i> <del>Strike</del>',
|
||||
default: null,
|
||||
source: {
|
||||
name: 'github',
|
||||
match: '^test2$',
|
||||
config: { repo: 'j-selby/liftinstall' }
|
||||
},
|
||||
shortcuts: []
|
||||
}
|
||||
],
|
||||
hide_advanced: false
|
||||
})
|
||||
}
|
||||
|
||||
app.get('/api/attrs', (req, res) => {
|
||||
res.send(
|
||||
`var base_attributes = {"name":"yuzu","target_url":"https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml"};`
|
||||
)
|
||||
})
|
||||
|
||||
app.get('/api/dark-mode', (req, res) => {
|
||||
res.json(false)
|
||||
})
|
||||
|
||||
app.get('/api/installation-status', (req, res) => {
|
||||
res.json({
|
||||
database: { packages: [], shortcuts: [] },
|
||||
install_path: null,
|
||||
preexisting_install: false,
|
||||
is_launcher: false,
|
||||
launcher_path: null
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/default-path', (req, res) => {
|
||||
res.json({ path: '/tmp/test/' })
|
||||
})
|
||||
|
||||
app.get('/api/config', (req, res) => {
|
||||
setTimeout(() => {
|
||||
returnConfig(res)
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
app.post('/api/start-install', (req, res) => {
|
||||
console.log(`-- Install: ${req.body}`)
|
||||
progressSimulation(res)
|
||||
})
|
||||
|
||||
app.get('/api/exit', (req, res) => {
|
||||
console.log('-- Exit')
|
||||
res.status(204)
|
||||
})
|
||||
|
||||
console.log(`Listening on ${port}...`)
|
||||
app.listen(port)
|
27
ui/package.json
Normal file
27
ui/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"buefy": "^0.7.7",
|
||||
"vue": "^2.6.6",
|
||||
"vue-router": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.5.0",
|
||||
"@vue/cli-plugin-eslint": "^3.5.0",
|
||||
"@vue/cli-service": "^3.5.0",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"express": "^4.17.1",
|
||||
"http-proxy-middleware": "^0.19.1",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
}
|
||||
}
|
5
ui/postcss.config.js
Normal file
5
ui/postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
35
ui/public/index.html
Normal file
35
ui/public/index.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=11">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<script src="/api/attrs" type="text/javascript"></script>
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title id="window-title">... Installer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="fullscreen" id="ie-blackout" style="display: none">
|
||||
<div class="title">Your computer is out of date.</div>
|
||||
<div class="subtitle">
|
||||
Make sure that your computer is up to date, and that you have Internet Explorer 11 installed.
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
Please note we do not support pirated or unsupported versions of Windows.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
if (!document.__proto__) {
|
||||
document.getElementById("ie-blackout").style.display = "block";
|
||||
}
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<strong>You need JavaScript enabled in your Windows Internet Options to install this application.</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
129
ui/src/App.vue
Normal file
129
ui/src/App.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div id="app" class="is-max-height">
|
||||
<section class="section is-max-height">
|
||||
<div class="container is-max-height">
|
||||
<div class="columns is-max-height">
|
||||
<div class="column is-one-third has-padding" v-if="!$root.$data.metadata.is_launcher">
|
||||
<img src="./assets/logo.png" width="60%" alt="Application icon" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
|
||||
Welcome to the {{ $root.$data.attrs.name }} installer!
|
||||
</h2>
|
||||
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
|
||||
We will have you up and running in just a few moments.
|
||||
</h2>
|
||||
|
||||
<h2 class="subtitle" v-if="$root.$data.metadata.preexisting_install">
|
||||
Welcome to the {{ $root.$data.attrs.name }} Maintenance Tool.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* roboto-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./assets/fonts/roboto-v18-latin-regular.eot'); /* IE9 Compat Modes */
|
||||
src: local('Roboto'), local('Roboto-Regular'),
|
||||
url('./assets/fonts/roboto-v18-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('./assets/fonts/roboto-v18-latin-regular.woff') format('woff');
|
||||
}
|
||||
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body, div, span, h1, h2, h3, h4, h5, h6 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
pre {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tile.is-child > .box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.has-padding {
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clickable-box {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable-box label {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.is-max-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-bottom-floating {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.is-top-floating {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.is-right-floating {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.has-padding .is-right-floating {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.is-left-floating {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.has-padding .is-left-floating {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div {
|
||||
color: hsl(0, 0%, 96%);
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
144
ui/src/helpers.js
Normal file
144
ui/src/helpers.js
Normal file
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* helpers.js
|
||||
*
|
||||
* Additional state-less helper methods.
|
||||
*/
|
||||
|
||||
var request_id = 0
|
||||
|
||||
/**
|
||||
* Makes a AJAX request.
|
||||
*
|
||||
* @param path The path to connect to.
|
||||
* @param successCallback A callback with a JSON payload.
|
||||
* @param failCallback A fail callback. Optional.
|
||||
* @param data POST data. Optional.
|
||||
*/
|
||||
export function ajax (path, successCallback, failCallback, data) {
|
||||
if (failCallback === undefined) {
|
||||
failCallback = defaultFailHandler
|
||||
}
|
||||
|
||||
console.log('Making HTTP request to ' + path)
|
||||
|
||||
var req = new XMLHttpRequest()
|
||||
|
||||
req.addEventListener('load', function () {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200 && this.getResponseHeader('Content-Type').indexOf('application/json') !== -1) {
|
||||
successCallback(JSON.parse(this.responseText))
|
||||
} else {
|
||||
failCallback(this.responseText)
|
||||
}
|
||||
})
|
||||
req.addEventListener('error', failCallback)
|
||||
|
||||
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + request_id++, true)
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
if (data != null) {
|
||||
var form = ''
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (form !== '') {
|
||||
form += '&'
|
||||
}
|
||||
|
||||
form += encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
|
||||
}
|
||||
|
||||
req.send(form)
|
||||
} else {
|
||||
req.send()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a AJAX request, streaming each line as it arrives. Type should be text/plain,
|
||||
* each line will be interpreted as JSON separately.
|
||||
*
|
||||
* @param path The path to connect to.
|
||||
* @param callback A callback with a JSON payload. Called for every line as it comes.
|
||||
* @param successCallback A callback with a raw text payload.
|
||||
* @param failCallback A fail callback. Optional.
|
||||
* @param data POST data. Optional.
|
||||
*/
|
||||
export function stream_ajax (path, callback, successCallback, failCallback, data) {
|
||||
var req = new XMLHttpRequest()
|
||||
|
||||
console.log('Making streaming HTTP request to ' + path)
|
||||
|
||||
req.addEventListener('load', function () {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200) {
|
||||
successCallback(this.responseText)
|
||||
} else {
|
||||
failCallback(this.responseText)
|
||||
}
|
||||
})
|
||||
|
||||
var buffer = ''
|
||||
var seenBytes = 0
|
||||
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState > 2) {
|
||||
buffer += req.responseText.substr(seenBytes)
|
||||
|
||||
var pointer
|
||||
while ((pointer = buffer.indexOf('\n')) >= 0) {
|
||||
var line = buffer.substring(0, pointer).trim()
|
||||
buffer = buffer.substring(pointer + 1)
|
||||
|
||||
if (line.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
var contents = JSON.parse(line)
|
||||
callback(contents)
|
||||
}
|
||||
|
||||
seenBytes = req.responseText.length
|
||||
}
|
||||
}
|
||||
|
||||
req.addEventListener('error', failCallback)
|
||||
|
||||
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + request_id++, true)
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
if (data != null) {
|
||||
var form = ''
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (form !== '') {
|
||||
form += '&'
|
||||
}
|
||||
|
||||
form += encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
|
||||
}
|
||||
|
||||
req.send(form)
|
||||
} else {
|
||||
req.send()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default handler if a AJAX request fails. Not to be used directly.
|
||||
*
|
||||
* @param e The XMLHttpRequest that failed.
|
||||
*/
|
||||
function defaultFailHandler (e) {
|
||||
console.error('A AJAX request failed, and was not caught:')
|
||||
console.error(e)
|
||||
}
|
119
ui/src/main.js
Normal file
119
ui/src/main.js
Normal file
@ -0,0 +1,119 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { ajax, stream_ajax } from './helpers'
|
||||
import Buefy from 'buefy'
|
||||
import 'buefy/dist/buefy.css'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.use(Buefy)
|
||||
|
||||
// Borrowed from http://tobyho.com/2012/07/27/taking-over-console-log/
|
||||
function intercept (method) {
|
||||
console[method] = function () {
|
||||
var message = Array.prototype.slice.apply(arguments).join(' ')
|
||||
window.external.invoke(
|
||||
JSON.stringify({
|
||||
Log: {
|
||||
kind: method,
|
||||
msg: message
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// See if we have access to the JSON interface
|
||||
var has_external_interface = false;
|
||||
try {
|
||||
window.external.invoke(JSON.stringify({
|
||||
Test: {}
|
||||
}))
|
||||
has_external_interface = true;
|
||||
} catch (e) {
|
||||
console.warn("Running without JSON interface - unexpected behaviour may occur!")
|
||||
}
|
||||
|
||||
// Overwrite loggers with the logging backend
|
||||
if (has_external_interface) {
|
||||
window.onerror = function (msg, url, line) {
|
||||
window.external.invoke(
|
||||
JSON.stringify({
|
||||
Log: {
|
||||
kind: 'error',
|
||||
msg: msg + ' @ ' + url + ':' + line
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
var methods = ['log', 'warn', 'error']
|
||||
for (var i = 0; i < methods.length; i++) {
|
||||
intercept(methods[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Disable F5
|
||||
function disable_shortcuts (e) {
|
||||
switch (e.keyCode) {
|
||||
case 116: // F5
|
||||
e.preventDefault()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if we need to enable dark mode
|
||||
ajax('/api/dark-mode', function (enable) {
|
||||
if (enable) {
|
||||
document.body.classList.add('has-background-black-ter')
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', disable_shortcuts)
|
||||
|
||||
document.getElementById('window-title').innerText =
|
||||
base_attributes.name + ' Installer'
|
||||
|
||||
function selectFileCallback (name) {
|
||||
app.install_location = name
|
||||
}
|
||||
|
||||
var app = new Vue({
|
||||
router: router,
|
||||
data: {
|
||||
attrs: base_attributes,
|
||||
config: {},
|
||||
install_location: '',
|
||||
// If the option to pick an install location should be provided
|
||||
show_install_location: true,
|
||||
metadata: {
|
||||
database: [],
|
||||
install_path: '',
|
||||
preexisting_install: false
|
||||
}
|
||||
},
|
||||
render: function (caller) {
|
||||
return caller(App)
|
||||
},
|
||||
methods: {
|
||||
exit: function () {
|
||||
ajax(
|
||||
'/api/exit',
|
||||
function () {},
|
||||
function (msg) {
|
||||
var search_location = app.metadata.install_path.length > 0 ? app.metadata.install_path :
|
||||
"the location where this installer is";
|
||||
|
||||
app.$router.replace({ name: 'showerr', params: { msg: msg +
|
||||
'\n\nPlease upload the log file (in ' + search_location + ') to ' +
|
||||
'the ' + app.attrs.name + ' team'
|
||||
}});
|
||||
}
|
||||
)
|
||||
},
|
||||
ajax: ajax,
|
||||
stream_ajax: stream_ajax
|
||||
}
|
||||
}).$mount('#app')
|
||||
|
||||
console.log("Vue started")
|
49
ui/src/router.js
Normal file
49
ui/src/router.js
Normal file
@ -0,0 +1,49 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import DownloadConfig from './views/DownloadConfig.vue'
|
||||
import SelectPackages from './views/SelectPackages.vue'
|
||||
import ErrorView from './views/ErrorView.vue'
|
||||
import InstallPackages from './views/InstallPackages.vue'
|
||||
import CompleteView from './views/CompleteView.vue'
|
||||
import ModifyView from './views/ModifyView.vue'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
export default new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/config',
|
||||
name: 'config',
|
||||
component: DownloadConfig
|
||||
},
|
||||
{
|
||||
path: '/packages',
|
||||
name: 'packages',
|
||||
component: SelectPackages
|
||||
},
|
||||
{
|
||||
path: '/install/:kind',
|
||||
name: 'install',
|
||||
component: InstallPackages
|
||||
},
|
||||
{
|
||||
path: '/showerr',
|
||||
name: 'showerr',
|
||||
component: ErrorView
|
||||
},
|
||||
{
|
||||
path: '/complete/:uninstall/:update/:packages_installed',
|
||||
name: 'complete',
|
||||
component: CompleteView
|
||||
},
|
||||
{
|
||||
path: '/modify',
|
||||
name: 'modify',
|
||||
component: ModifyView
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/config'
|
||||
}
|
||||
]
|
||||
})
|
50
ui/src/views/CompleteView.vue
Normal file
50
ui/src/views/CompleteView.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="column has-padding">
|
||||
<div v-if="was_update">
|
||||
<div v-if="has_installed">
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been updated.</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} is already up to date!</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="was_install">
|
||||
<h4 class="subtitle">Thanks for installing {{ $root.$data.attrs.name }}!</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
|
||||
<img src="../assets/how-to-open.png" alt="Where yuzu is installed"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been uninstalled.</h4>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-right-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-on:click="exit">Exit</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CompleteView',
|
||||
data: function () {
|
||||
return {
|
||||
was_install: !this.$route.params.uninstall,
|
||||
was_update: this.$route.params.update,
|
||||
has_installed: this.$route.params.packages_installed > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exit: function () {
|
||||
this.$root.exit()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
97
ui/src/views/DownloadConfig.vue
Normal file
97
ui/src/views/DownloadConfig.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Downloading config...</h4>
|
||||
|
||||
<br />
|
||||
<progress class="progress is-info is-medium" max="100">
|
||||
0%
|
||||
</progress>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DownloadConfig',
|
||||
created: function () {
|
||||
this.download_install_status()
|
||||
},
|
||||
methods: {
|
||||
download_install_status: function () {
|
||||
var that = this
|
||||
this.$root.ajax('/api/installation-status', function (e) {
|
||||
that.$root.metadata = e
|
||||
|
||||
that.download_config()
|
||||
})
|
||||
},
|
||||
download_config: function () {
|
||||
var that = this
|
||||
this.$root.ajax('/api/config', function (e) {
|
||||
that.$root.config = e
|
||||
|
||||
that.choose_next_state()
|
||||
}, function (e) {
|
||||
console.error('Got error while downloading config: ' +
|
||||
e)
|
||||
|
||||
if (that.$root.metadata.is_launcher) {
|
||||
// Just launch the target application
|
||||
that.$root.exit()
|
||||
} else {
|
||||
that.$router.replace({ name: 'showerr',
|
||||
params: { msg: 'Got error while downloading config: ' +
|
||||
e } })
|
||||
}
|
||||
})
|
||||
},
|
||||
choose_next_state: function () {
|
||||
var app = this.$root
|
||||
// Update the updater if needed
|
||||
if (app.config.new_tool) {
|
||||
this.$router.push('/install/updater')
|
||||
return
|
||||
}
|
||||
|
||||
if (app.metadata.preexisting_install) {
|
||||
app.install_location = app.metadata.install_path
|
||||
|
||||
// Copy over installed packages
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
app.config.packages[x].default = false
|
||||
app.config.packages[x].installed = false
|
||||
}
|
||||
|
||||
for (var i = 0; i < app.metadata.database.packages.length; i++) {
|
||||
// Find this config package
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
if (app.config.packages[x].name === app.metadata.database.packages[i].name) {
|
||||
app.config.packages[x].default = true
|
||||
app.config.packages[x].installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (app.metadata.is_launcher) {
|
||||
this.$router.replace('/install/regular')
|
||||
} else {
|
||||
this.$router.replace('/modify')
|
||||
}
|
||||
} else {
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
app.config.packages[x].installed = false
|
||||
}
|
||||
|
||||
// Need to do a bit more digging to get at the
|
||||
// install location.
|
||||
this.$root.ajax('/api/default-path', function (e) {
|
||||
if (e.path != null) {
|
||||
app.install_location = e.path
|
||||
}
|
||||
})
|
||||
|
||||
this.$router.replace('/packages')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
59
ui/src/views/ErrorView.vue
Normal file
59
ui/src/views/ErrorView.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="column" v-bind:class="{ 'has-padding': !$root.$data.metadata.is_launcher }">
|
||||
<b-message title="An error occurred" type="is-danger" :closable="false">
|
||||
<div id="error_msg" v-html="msg"></div>
|
||||
</b-message>
|
||||
<div class="field is-grouped is-right-floating" v-bind:class="{ 'is-bottom-floating': !$root.$data.metadata.is_launcher, 'is-top-floating': $root.$data.metadata.is_launcher }">
|
||||
<p class="control">
|
||||
<a class="button is-primary is-medium" v-if="remaining && !$root.$data.metadata.is_launcher" v-on:click="go_back">Back</a>
|
||||
<a class="button is-primary is-medium" v-if="$root.$data.metadata.is_launcher" v-on:click="exit">Exit</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.pre-wrap {
|
||||
/* https://css-tricks.com/snippets/css/make-pre-text-wrap/ */
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
|
||||
#error_msg {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ErrorView',
|
||||
data: function () {
|
||||
return {
|
||||
// https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript
|
||||
msg: this.$route.params.msg
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\n/g, "<br />"),
|
||||
remaining: window.history.length > 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
go_back: function () {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
exit: function () {
|
||||
this.$root.exit()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
117
ui/src/views/InstallPackages.vue
Normal file
117
ui/src/views/InstallPackages.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle" v-if="$root.$data.metadata.is_launcher || is_update">Checking for updates...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_uninstall">Uninstalling...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_updater_update">Downloading self-update...</h4>
|
||||
<h4 class="subtitle" v-else>Installing...</h4>
|
||||
<div v-html="$root.$data.config.installing_message"></div>
|
||||
<br />
|
||||
|
||||
<div v-html="progress_message"></div>
|
||||
<progress class="progress is-info is-medium" v-bind:value="progress" max="100">
|
||||
{{ progress }}%
|
||||
</progress>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'InstallPackages',
|
||||
data: function () {
|
||||
return {
|
||||
progress: 0.0,
|
||||
progress_message: 'Please wait...',
|
||||
is_uninstall: false,
|
||||
is_updater_update: false,
|
||||
is_update: false,
|
||||
failed_with_error: false,
|
||||
packages_installed: 0
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.is_uninstall = this.$route.params.kind === 'uninstall'
|
||||
this.is_updater_update = this.$route.params.kind === 'updater'
|
||||
this.is_update = this.$route.params.kind === 'update'
|
||||
console.log('Installer kind: ' + this.$route.params.kind)
|
||||
this.install()
|
||||
},
|
||||
methods: {
|
||||
install: function () {
|
||||
var that = this
|
||||
var app = this.$root
|
||||
|
||||
var results = {}
|
||||
|
||||
for (var package_index = 0; package_index < app.config.packages.length; package_index++) {
|
||||
var current_package = app.config.packages[package_index]
|
||||
if (current_package.default != null) {
|
||||
results[current_package.name] = current_package.default
|
||||
}
|
||||
}
|
||||
|
||||
results['path'] = app.install_location
|
||||
|
||||
var targetUrl = '/api/start-install'
|
||||
if (this.is_uninstall) {
|
||||
targetUrl = '/api/uninstall'
|
||||
}
|
||||
if (this.is_updater_update) {
|
||||
targetUrl = '/api/update-updater'
|
||||
}
|
||||
|
||||
this.$root.stream_ajax(targetUrl, function (line) {
|
||||
// On progress line received from server
|
||||
|
||||
if (line.hasOwnProperty('Status')) {
|
||||
that.progress_message = line.Status[0]
|
||||
that.progress = line.Status[1] * 100
|
||||
}
|
||||
|
||||
if (line.hasOwnProperty('PackageInstalled')) {
|
||||
that.packages_installed += 1
|
||||
}
|
||||
|
||||
if (line.hasOwnProperty('Error')) {
|
||||
that.failed_with_error = true
|
||||
that.$router.replace({ name: 'showerr', params: { msg: line.Error } })
|
||||
}
|
||||
}, function (e) {
|
||||
// On request completed
|
||||
|
||||
if (that.is_updater_update) {
|
||||
// Continue with what we were doing
|
||||
if (app.metadata.is_launcher) {
|
||||
that.$router.replace('/install/regular')
|
||||
} else {
|
||||
if (app.metadata.preexisting_install) {
|
||||
that.$router.replace('/modify')
|
||||
} else {
|
||||
that.$router.replace('/packages')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (app.metadata.is_launcher) {
|
||||
app.exit()
|
||||
} else if (!that.failed_with_error) {
|
||||
if (that.is_uninstall) {
|
||||
that.$router.replace({ name: 'complete',
|
||||
params: {
|
||||
uninstall: true,
|
||||
update: that.is_update,
|
||||
installed: that.packages_installed
|
||||
} })
|
||||
} else {
|
||||
that.$router.replace({ name: 'complete',
|
||||
params: {
|
||||
uninstall: false,
|
||||
update: that.is_update,
|
||||
installed: that.packages_installed
|
||||
} })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, results)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
62
ui/src/views/ModifyView.vue
Normal file
62
ui/src/views/ModifyView.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Choose an option:</h4>
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="update">
|
||||
Update
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="modify_packages">
|
||||
Modify
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="prepare_uninstall">
|
||||
Uninstall
|
||||
</a>
|
||||
|
||||
<div class="modal is-active" v-if="show_uninstall">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Are you sure you want to uninstall {{ $root.$data.attrs.name }}?</p>
|
||||
</header>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-danger" v-on:click="uninstall">Yes</button>
|
||||
<button class="button" v-on:click="cancel_uninstall">No</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ModifyView',
|
||||
data: function () {
|
||||
return {
|
||||
show_uninstall: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function () {
|
||||
this.$router.push('/install/update')
|
||||
},
|
||||
modify_packages: function () {
|
||||
this.$router.push('/packages')
|
||||
},
|
||||
prepare_uninstall: function () {
|
||||
this.show_uninstall = true
|
||||
},
|
||||
cancel_uninstall: function () {
|
||||
this.show_uninstall = false
|
||||
},
|
||||
uninstall: function () {
|
||||
this.$router.push('/install/uninstall')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
87
ui/src/views/SelectPackages.vue
Normal file
87
ui/src/views/SelectPackages.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Select which packages you want to install:</h4>
|
||||
|
||||
<!-- Build options -->
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent" v-for="Lpackage in $root.$data.config.packages" :key="Lpackage.name" :index="Lpackage.name">
|
||||
<div class="tile is-child">
|
||||
<div class="box clickable-box" v-on:click.capture.stop="Lpackage.default = !Lpackage.default">
|
||||
<label class="checkbox">
|
||||
<b-checkbox v-model="Lpackage.default">
|
||||
{{ Lpackage.name }}
|
||||
</b-checkbox>
|
||||
<span v-if="Lpackage.installed"><i>(installed)</i></span>
|
||||
</label>
|
||||
<p>
|
||||
{{ Lpackage.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install Location</div>
|
||||
<div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="$root.$data.install_location"
|
||||
placeholder="Enter a install path here">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-dark" v-on:click="select_file">
|
||||
Select
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="is-right-floating is-bottom-floating">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
|
||||
v-on:click="advanced = true">Advanced...</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
|
||||
v-on:click="install">Install</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
|
||||
v-on:click="install">Modify</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-left-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install"
|
||||
v-on:click="go_back">Back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SelectPackages',
|
||||
data: function () {
|
||||
return {
|
||||
advanced: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select_file: function () {
|
||||
window.external.invoke(JSON.stringify({
|
||||
SelectInstallDir: {
|
||||
callback_name: 'selectFileCallback'
|
||||
}
|
||||
}))
|
||||
},
|
||||
install: function () {
|
||||
this.$router.push('/install/regular')
|
||||
},
|
||||
go_back: function () {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
6
ui/vue.config.js
Normal file
6
ui/vue.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: 'http://127.0.0.1:3000'
|
||||
},
|
||||
filenameHashing: false
|
||||
}
|
8288
ui/yarn.lock
Normal file
8288
ui/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user