mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2024-12-22 20:45:41 +00: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
|
matrix:
|
||||||
dist: trusty
|
include:
|
||||||
sudo: required
|
- os: linux
|
||||||
services:
|
language: cpp
|
||||||
- docker
|
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:
|
- os: osx
|
||||||
- docker pull ubuntu:18.04
|
language: rust
|
||||||
|
cache: cargo
|
||||||
|
osx_image: xcode10
|
||||||
|
script: brew install yarn && cargo build
|
||||||
|
|
||||||
script:
|
- os: windows
|
||||||
- docker run -v $(pwd):/liftinstall ubuntu:18.04 /bin/bash -ex /liftinstall/.travis/build.sh
|
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
|
#!/usr/bin/env bash
|
||||||
cd /liftinstall
|
cd /liftinstall || exit 1
|
||||||
|
|
||||||
apt update
|
# setup NodeJS
|
||||||
apt install -y libwebkit2gtk-4.0-dev libssl-dev
|
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
|
apt-get update
|
||||||
export PATH=~/.cargo/bin:$PATH
|
apt-get install -y libwebkit2gtk-4.0-dev libssl-dev nodejs yarn
|
||||||
|
|
||||||
|
yarn --cwd ui
|
||||||
|
|
||||||
cargo build
|
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"
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
hyper = "0.11.27"
|
||||||
futures = "*"
|
futures = "0.1.25"
|
||||||
mime_guess = "1.8.3"
|
mime_guess = "1.8.6"
|
||||||
url = "*"
|
url = "1.7.2"
|
||||||
|
|
||||||
reqwest = "0.9.0"
|
reqwest = "0.9.12"
|
||||||
number_prefix = "0.2.7"
|
number_prefix = "0.3.0"
|
||||||
|
|
||||||
serde = "1.0.27"
|
serde = "1.0.89"
|
||||||
serde_derive = "1.0.27"
|
serde_derive = "1.0.89"
|
||||||
serde_json = "1.0.9"
|
serde_json = "1.0.39"
|
||||||
|
|
||||||
toml = "0.4"
|
toml = "0.5.0"
|
||||||
|
|
||||||
semver = {version = "0.9.0", features = ["serde"]}
|
semver = {version = "0.9.0", features = ["serde"]}
|
||||||
regex = "0.2"
|
regex = "1.1.5"
|
||||||
|
|
||||||
dirs = "1.0"
|
dirs = "1.0.5"
|
||||||
zip = "0.4.2"
|
zip = "0.5.1"
|
||||||
xz-decom = {git = "https://github.com/j-selby/xz-decom.git", rev = "9ebf3d00d9ff909c39eec1d2cf7e6e068ce214e5"}
|
xz2 = "0.1.6"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
fern = "0.5"
|
fern = "0.5"
|
||||||
chrono = "0.4.5"
|
chrono = "0.4.6"
|
||||||
|
|
||||||
clap = "2.32.0"
|
clap = "2.32.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
walkdir = "2"
|
walkdir = "2.2.7"
|
||||||
serde = "1.0.27"
|
serde = "1.0.89"
|
||||||
serde_derive = "1.0.27"
|
serde_derive = "1.0.89"
|
||||||
toml = "0.4"
|
toml = "0.5.0"
|
||||||
|
which = "2.0.1"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[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"] }
|
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]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
winres = "0.1"
|
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:
|
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
|
- 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.
|
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 (MSVC), you need Visual Studio installed.
|
||||||
- For Windows (Mingw), you need `gcc`/`g++` available on the PATH.
|
- For Windows (Mingw), you need `gcc`/`g++` available on the PATH.
|
||||||
- For Mac, you need Xcode installed, and Clang/etc 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:
|
In order to build yourself an installer, as a bare minimum, you need to:
|
||||||
|
|
||||||
- Add your favicon to `static/favicon.ico`
|
- Add your favicon to `ui/public/favicon.ico`
|
||||||
- Add your logo to `static/logo.png`
|
- Add your logo to `ui/src/assets/logo.png`
|
||||||
- Modify the bootstrap configuration file as needed (`config.PLATFORM.toml`).
|
- Modify the bootstrap configuration file as needed (`config.PLATFORM.toml`).
|
||||||
- Have the main configuration file somewhere useful, reachable over HTTP.
|
- Have the main configuration file somewhere useful, reachable over HTTP.
|
||||||
- Run:
|
- 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"
|
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)]
|
#[cfg(windows)]
|
||||||
extern crate winres;
|
extern crate winres;
|
||||||
|
|
||||||
|
@ -11,24 +9,19 @@ extern crate serde;
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
|
|
||||||
use walkdir::WalkDir;
|
extern crate which;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use std::fs::copy;
|
use std::fs::copy;
|
||||||
use std::fs::create_dir_all;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
use std::io::BufRead;
|
|
||||||
use std::io::BufReader;
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::process::Command;
|
||||||
|
|
||||||
use std::env::consts::OS;
|
use std::env::consts::OS;
|
||||||
|
|
||||||
const FILES_TO_PREPROCESS: &'static [&'static str] = &["helpers.js", "views.js"];
|
|
||||||
|
|
||||||
/// Describes the application itself.
|
/// Describes the application itself.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BaseAttributes {
|
pub struct BaseAttributes {
|
||||||
|
@ -39,7 +32,7 @@ pub struct BaseAttributes {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn handle_binary(config: &BaseAttributes) {
|
fn handle_binary(config: &BaseAttributes) {
|
||||||
let mut res = winres::WindowsResource::new();
|
let mut res = winres::WindowsResource::new();
|
||||||
res.set_icon("static/favicon.ico");
|
res.set_icon("ui/public/favicon.ico");
|
||||||
res.set(
|
res.set(
|
||||||
"FileDescription",
|
"FileDescription",
|
||||||
&format!("Interactive installer for {}", config.name),
|
&format!("Interactive installer for {}", config.name),
|
||||||
|
@ -62,6 +55,8 @@ fn handle_binary(_config: &BaseAttributes) {}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let output_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
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();
|
let os = OS.to_lowercase();
|
||||||
|
|
||||||
|
@ -92,80 +87,33 @@ fn main() {
|
||||||
// Copy for the main build
|
// Copy for the main build
|
||||||
copy(&target_config, output_dir.join("bootstrap.toml")).expect("Unable to copy config file");
|
copy(&target_config, output_dir.join("bootstrap.toml")).expect("Unable to copy config file");
|
||||||
|
|
||||||
// Copy files from static/ to build dir
|
let yarn_binary = which::which("yarn")
|
||||||
for entry in WalkDir::new("static") {
|
.expect("Failed to find yarn - please go ahead and install it!");
|
||||||
let entry = entry.expect("Unable to read output directory");
|
|
||||||
|
|
||||||
let output_file = output_dir.join(entry.path());
|
// Build and deploy frontend files
|
||||||
|
Command::new(&yarn_binary)
|
||||||
if entry.path().is_dir() {
|
.arg("--version")
|
||||||
create_dir_all(output_file).expect("Unable to create dir");
|
.spawn()
|
||||||
} else {
|
.expect("Yarn could not be launched");
|
||||||
let filename = entry
|
Command::new(&yarn_binary)
|
||||||
.path()
|
.arg("--cwd")
|
||||||
.file_name()
|
.arg(ui_dir.to_str().expect("Unable to covert path"))
|
||||||
.expect("Unable to parse filename")
|
.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()
|
.to_str()
|
||||||
.expect("Unable to convert to string");
|
.expect("Unable to convert path"),
|
||||||
|
])
|
||||||
if FILES_TO_PREPROCESS.contains(&filename) {
|
.spawn()
|
||||||
// Do basic preprocessing - transcribe template string
|
.unwrap()
|
||||||
let source = BufReader::new(File::open(entry.path()).expect("Unable to copy file"));
|
.wait().expect("Unable to build frontend assets using Webpack");
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||||
hide_advanced = true
|
hide_advanced = true
|
||||||
|
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.5/yuzu_install.exe"
|
||||||
|
|
||||||
[[packages]]
|
[[packages]]
|
||||||
name = "yuzu Nightly"
|
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::iter::Iterator;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use xz_decom;
|
use xz2::read::XzDecoder;
|
||||||
|
|
||||||
pub trait Archive<'a> {
|
pub trait Archive<'a> {
|
||||||
/// func: iterator value, max size, file name, file contents
|
/// func: iterator value, max size, file name, file contents
|
||||||
fn for_each(
|
fn for_each(
|
||||||
&mut self,
|
&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>;
|
) -> Result<(), String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ struct ZipArchive<'a> {
|
||||||
impl<'a> Archive<'a> for ZipArchive<'a> {
|
impl<'a> Archive<'a> for ZipArchive<'a> {
|
||||||
fn for_each(
|
fn for_each(
|
||||||
&mut self,
|
&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> {
|
) -> Result<(), String> {
|
||||||
let max = self.archive.len();
|
let max = self.archive.len();
|
||||||
|
|
||||||
|
@ -49,13 +49,13 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TarArchive<'a> {
|
struct TarArchive<'a> {
|
||||||
archive: UpstreamTarArchive<Box<Read + 'a>>,
|
archive: UpstreamTarArchive<Box<dyn Read + 'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Archive<'a> for TarArchive<'a> {
|
impl<'a> Archive<'a> for TarArchive<'a> {
|
||||||
fn for_each(
|
fn for_each(
|
||||||
&mut self,
|
&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> {
|
) -> Result<(), String> {
|
||||||
let entries = self
|
let entries = self
|
||||||
.archive
|
.archive
|
||||||
|
@ -83,7 +83,7 @@ impl<'a> Archive<'a> for TarArchive<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the named archive with an archive implementation.
|
/// 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") {
|
if name.ends_with(".zip") {
|
||||||
// Decompress a .zip file
|
// Decompress a .zip file
|
||||||
let archive = UpstreamZipArchive::new(Cursor::new(data))
|
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 }))
|
Ok(Box::new(ZipArchive { archive }))
|
||||||
} else if name.ends_with(".tar.xz") {
|
} else if name.ends_with(".tar.xz") {
|
||||||
// Decompress a .tar.xz file
|
// Decompress a .tar.xz file
|
||||||
let decompressed_data = xz_decom::decompress(data)
|
let mut decompresser = XzDecoder::new(data);
|
||||||
.map_err(|x| format!("Failed to build decompressor: {:?}", x))?;
|
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);
|
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;
|
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 {
|
macro_rules! include_files_as_assets {
|
||||||
( $target_match:expr, $( $file_name:expr ),* ) => {
|
( $target_match:expr, $( $file_name:expr ),* ) => {
|
||||||
|
@ -34,18 +34,15 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
|
||||||
file_path,
|
file_path,
|
||||||
"/index.html",
|
"/index.html",
|
||||||
"/favicon.ico",
|
"/favicon.ico",
|
||||||
"/logo.png",
|
"/img/logo.png",
|
||||||
"/how-to-open.png",
|
"/img/how-to-open.png",
|
||||||
"/css/bulma.min.css",
|
"/css/app.css",
|
||||||
"/css/main.css",
|
"/css/chunk-vendors.css",
|
||||||
"/fonts/roboto-v18-latin-regular.eot",
|
"/fonts/roboto-v18-latin-regular.eot",
|
||||||
"/fonts/roboto-v18-latin-regular.woff",
|
"/fonts/roboto-v18-latin-regular.woff",
|
||||||
"/fonts/roboto-v18-latin-regular.woff2",
|
"/fonts/roboto-v18-latin-regular.woff2",
|
||||||
"/js/vue.min.js",
|
"/js/chunk-vendors.js",
|
||||||
"/js/vue-router.min.js",
|
"/js/app.js"
|
||||||
"/js/helpers.js",
|
|
||||||
"/js/views.js",
|
|
||||||
"/js/main.js"
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Some((string_mime, contents))
|
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::io::Read;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::async::Client as AsyncClient;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
/// Asserts that a URL is valid HTTPS, else returns an error.
|
/// 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://") {
|
if url.starts_with("https://") {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} 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))
|
.map_err(|x| format!("Unable to build client: {:?}", x))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads a text file from the specified URL.
|
/// Builds a customised async HTTP client.
|
||||||
pub fn download_text(url: &str) -> Result<String, String> {
|
pub fn build_async_client() -> Result<AsyncClient, String> {
|
||||||
assert_ssl(url)?;
|
AsyncClient::builder()
|
||||||
|
.timeout(Duration::from_secs(8))
|
||||||
let mut client = build_client()?
|
.build()
|
||||||
.get(url)
|
.map_err(|x| format!("Unable to build client: {:?}", x))
|
||||||
.send()
|
|
||||||
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
|
|
||||||
|
|
||||||
client
|
|
||||||
.text()
|
|
||||||
.map_err(|v| format!("Failed to get text from resource: {:?}", v))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Streams a file from a HTTP server.
|
/// Streams a file from a HTTP server.
|
||||||
|
|
|
@ -18,8 +18,8 @@ use std::sync::mpsc::Sender;
|
||||||
use std::io::copy;
|
use std::io::copy;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use std::process::exit;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::process::{exit, Stdio};
|
||||||
|
|
||||||
use config::BaseAttributes;
|
use config::BaseAttributes;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
@ -40,7 +40,9 @@ use std::fs::remove_file;
|
||||||
|
|
||||||
use http;
|
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.
|
/// A message thrown during the installation of packages.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -105,14 +107,14 @@ pub struct LocalInstallation {
|
||||||
|
|
||||||
macro_rules! declare_messenger_callback {
|
macro_rules! declare_messenger_callback {
|
||||||
($target:expr) => {
|
($target:expr) => {
|
||||||
&|msg: &TaskMessage| match msg {
|
&|msg: &TaskMessage| match *msg {
|
||||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
TaskMessage::DisplayMessage(msg, progress) => {
|
||||||
if let Err(v) = $target.send(InstallMessage::Status(msg.to_string(), progress as _))
|
if let Err(v) = $target.send(InstallMessage::Status(msg.to_string(), progress as _))
|
||||||
{
|
{
|
||||||
error!("Failed to submit queue message: {:?}", v);
|
error!("Failed to submit queue message: {:?}", v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&TaskMessage::PackageInstalled => {
|
TaskMessage::PackageInstalled => {
|
||||||
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
|
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
|
||||||
error!("Failed to submit queue message: {:?}", v);
|
error!("Failed to submit queue message: {:?}", v);
|
||||||
}
|
}
|
||||||
|
@ -264,11 +266,11 @@ impl InstallerFramework {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pretty print data volumes
|
// 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),
|
Standalone(bytes) => format!("{} bytes", bytes),
|
||||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
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),
|
Standalone(bytes) => format!("{} bytes", bytes),
|
||||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||||
};
|
};
|
||||||
|
@ -328,7 +330,8 @@ impl InstallerFramework {
|
||||||
x.to_str()
|
x.to_str()
|
||||||
.log_expect("Unable to convert argument to String")
|
.log_expect("Unable to convert argument to String")
|
||||||
.to_string()
|
.to_string()
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
{
|
{
|
||||||
let new_app_file = match File::create(&args_file) {
|
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.
|
/// Creates a new instance of the Installer Framework with a specified Config.
|
||||||
pub fn new(attrs: BaseAttributes) -> Self {
|
pub fn new(attrs: BaseAttributes) -> Self {
|
||||||
InstallerFramework {
|
InstallerFramework {
|
||||||
|
|
|
@ -17,7 +17,8 @@ pub fn setup_logger(file_name: String) -> Result<(), fern::InitError> {
|
||||||
record.level(),
|
record.level(),
|
||||||
message
|
message
|
||||||
))
|
))
|
||||||
}).level(log::LevelFilter::Info)
|
})
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
.chain(io::stdout())
|
.chain(io::stdout())
|
||||||
.chain(fern::log_file(file_name)?)
|
.chain(fern::log_file(file_name)?)
|
||||||
.apply()?;
|
.apply()?;
|
||||||
|
|
239
src/main.rs
239
src/main.rs
|
@ -7,9 +7,6 @@
|
||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
extern crate nfd;
|
|
||||||
|
|
||||||
extern crate web_view;
|
extern crate web_view;
|
||||||
|
|
||||||
extern crate futures;
|
extern crate futures;
|
||||||
|
@ -30,7 +27,7 @@ extern crate semver;
|
||||||
|
|
||||||
extern crate dirs;
|
extern crate dirs;
|
||||||
extern crate tar;
|
extern crate tar;
|
||||||
extern crate xz_decom;
|
extern crate xz2;
|
||||||
extern crate zip;
|
extern crate zip;
|
||||||
|
|
||||||
extern crate fern;
|
extern crate fern;
|
||||||
|
@ -40,66 +37,45 @@ extern crate log;
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
|
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
|
#[cfg(windows)]
|
||||||
extern crate winapi;
|
extern crate winapi;
|
||||||
|
#[cfg(windows)]
|
||||||
|
extern crate widestring;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
extern crate slug;
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
extern crate sysinfo;
|
||||||
|
|
||||||
mod archives;
|
mod archives;
|
||||||
mod assets;
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod frontend;
|
||||||
mod http;
|
mod http;
|
||||||
mod installer;
|
mod installer;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod native;
|
mod native;
|
||||||
mod rest;
|
mod self_update;
|
||||||
mod sources;
|
mod sources;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
|
||||||
use web_view::*;
|
|
||||||
|
|
||||||
use installer::InstallerFramework;
|
use installer::InstallerFramework;
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use nfd::Response;
|
|
||||||
|
|
||||||
use rest::WebServer;
|
|
||||||
|
|
||||||
use std::net::TcpListener;
|
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use std::process::exit;
|
|
||||||
use std::process::Command;
|
|
||||||
use std::{thread, time};
|
|
||||||
|
|
||||||
use std::fs::remove_file;
|
|
||||||
use std::fs::File;
|
|
||||||
|
|
||||||
use logging::LoggingErrors;
|
use logging::LoggingErrors;
|
||||||
|
|
||||||
use clap::App;
|
use clap::App;
|
||||||
use clap::Arg;
|
use clap::Arg;
|
||||||
use log::Level;
|
|
||||||
|
|
||||||
use config::BaseAttributes;
|
use config::BaseAttributes;
|
||||||
|
|
||||||
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum CallbackType {
|
|
||||||
SelectInstallDir { callback_name: String },
|
|
||||||
Log { msg: String, kind: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
|
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
|
||||||
|
|
||||||
logging::setup_logger(format!("{}_installer.log", config.name))
|
logging::setup_logger(format!("{}_installer.log", config.name))
|
||||||
.expect("Unable to setup logging!");
|
.expect("Unable to setup logging!");
|
||||||
|
|
||||||
|
// Parse CLI arguments
|
||||||
let app_name = config.name.clone();
|
let app_name = config.name.clone();
|
||||||
|
|
||||||
let app_about = format!("An interactive installer for {}", app_name);
|
let app_about = format!("An interactive installer for {}", app_name);
|
||||||
|
@ -112,7 +88,8 @@ fn main() {
|
||||||
.value_name("TARGET")
|
.value_name("TARGET")
|
||||||
.help("Launches the specified executable after checking for updates")
|
.help("Launches the specified executable after checking for updates")
|
||||||
.takes_value(true),
|
.takes_value(true),
|
||||||
).arg(
|
)
|
||||||
|
.arg(
|
||||||
Arg::with_name("swap")
|
Arg::with_name("swap")
|
||||||
.long("swap")
|
.long("swap")
|
||||||
.value_name("TARGET")
|
.value_name("TARGET")
|
||||||
|
@ -125,100 +102,19 @@ fn main() {
|
||||||
|
|
||||||
info!("{} installer", app_name);
|
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_exe = std::env::current_exe().log_expect("Current executable could not be found");
|
||||||
let current_path = current_exe
|
let current_path = current_exe
|
||||||
.parent()
|
.parent()
|
||||||
.log_expect("Parent directory of executable could not be found");
|
.log_expect("Parent directory of executable could not be found");
|
||||||
|
|
||||||
// Check to see if we are currently in a self-update
|
self_update::perform_swap(¤t_exe, matches.value_of("swap"));
|
||||||
if let Some(to_path) = matches.value_of("swap") {
|
if let Some(new_matches) = self_update::check_args(reinterpret_app, current_path) {
|
||||||
let to_path = PathBuf::from(to_path);
|
matches = new_matches;
|
||||||
|
|
||||||
// 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::cleanup(current_path);
|
||||||
|
|
||||||
Command::new(to_path)
|
// Load in metadata + setup the installer framework
|
||||||
.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
|
|
||||||
let metadata_file = current_path.join("metadata.json");
|
let metadata_file = current_path.join("metadata.json");
|
||||||
let mut framework = if metadata_file.exists() {
|
let mut framework = if metadata_file.exists() {
|
||||||
info!("Using pre-existing metadata file: {:?}", metadata_file);
|
info!("Using pre-existing metadata file: {:?}", metadata_file);
|
||||||
|
@ -236,97 +132,6 @@ fn main() {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Firstly, allocate us an epidermal port
|
// Start up the UI
|
||||||
let target_port = {
|
frontend::launch(&app_name, is_launcher, framework);
|
||||||
let listener = TcpListener::bind("127.0.0.1:0")
|
|
||||||
.log_expect("At least one local address should be free");
|
|
||||||
listener
|
|
||||||
.local_addr()
|
|
||||||
.log_expect("Should be able to pull address from listener")
|
|
||||||
.port()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Now, iterate over all ports
|
|
||||||
let addresses = "localhost:0"
|
|
||||||
.to_socket_addrs()
|
|
||||||
.log_expect("No localhost address found");
|
|
||||||
|
|
||||||
let mut servers = Vec::new();
|
|
||||||
let mut http_address = None;
|
|
||||||
|
|
||||||
let framework = Arc::new(RwLock::new(framework));
|
|
||||||
|
|
||||||
// Startup HTTP server for handling the web view
|
|
||||||
for mut address in addresses {
|
|
||||||
address.set_port(target_port);
|
|
||||||
|
|
||||||
let server = WebServer::with_addr(framework.clone(), address)
|
|
||||||
.log_expect("Failed to bind to address");
|
|
||||||
|
|
||||||
info!("Server: {:?}", address);
|
|
||||||
|
|
||||||
http_address = Some(address);
|
|
||||||
|
|
||||||
servers.push(server);
|
|
||||||
}
|
|
||||||
|
|
||||||
let http_address = http_address.log_expect("No HTTP address found");
|
|
||||||
|
|
||||||
let http_address = format!("http://localhost:{}", http_address.port());
|
|
||||||
|
|
||||||
// Init the web view
|
|
||||||
let size = if is_launcher { (600, 300) } else { (1024, 500) };
|
|
||||||
|
|
||||||
let resizable = false;
|
|
||||||
let debug = true;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,72 +2,95 @@
|
||||||
* Misc interop helpers.
|
* 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 "windows.h"
|
||||||
#include "winnls.h"
|
#include "winnls.h"
|
||||||
#include "shobjidl.h"
|
#include "shobjidl.h"
|
||||||
#include "objbase.h"
|
#include "objbase.h"
|
||||||
#include "objidl.h"
|
#include "objidl.h"
|
||||||
#include "shlguid.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(
|
extern "C" int saveShortcut(
|
||||||
const char *shortcutPath,
|
const wchar_t *shortcutPath,
|
||||||
const char *description,
|
const wchar_t *description,
|
||||||
const char *path,
|
const wchar_t *path,
|
||||||
const char *args,
|
const wchar_t *args,
|
||||||
const char *workingDir) {
|
const wchar_t *workingDir)
|
||||||
char* errStr = NULL;
|
{
|
||||||
|
char *errStr = NULL;
|
||||||
HRESULT h;
|
HRESULT h;
|
||||||
IShellLink* shellLink = NULL;
|
IShellLink *shellLink = NULL;
|
||||||
IPersistFile* persistFile = NULL;
|
IPersistFile *persistFile = NULL;
|
||||||
|
|
||||||
#ifdef _WIN64
|
|
||||||
wchar_t wName[MAX_PATH+1];
|
|
||||||
#else
|
|
||||||
WORD wName[MAX_PATH+1];
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int id;
|
|
||||||
|
|
||||||
// Initialize the COM library
|
// Initialize the COM library
|
||||||
h = CoInitialize(NULL);
|
h = CoInitialize(NULL);
|
||||||
if (FAILED(h)) {
|
if (FAILED(h))
|
||||||
|
{
|
||||||
errStr = "Failed to initialize COM library";
|
errStr = "Failed to initialize COM library";
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
h = CoCreateInstance( CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
|
h = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
|
||||||
IID_IShellLink, (PVOID*)&shellLink );
|
IID_IShellLink, (PVOID *)&shellLink);
|
||||||
if (FAILED(h)) {
|
if (FAILED(h))
|
||||||
|
{
|
||||||
errStr = "Failed to create IShellLink";
|
errStr = "Failed to create IShellLink";
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID*)&persistFile);
|
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID *)&persistFile);
|
||||||
if (FAILED(h)) {
|
if (FAILED(h))
|
||||||
|
{
|
||||||
errStr = "Failed to get IPersistFile";
|
errStr = "Failed to get IPersistFile";
|
||||||
goto err;
|
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
|
// 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.
|
// 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
|
// Set the fields for which the application has set a value
|
||||||
if (description!=NULL)
|
if (description != NULL)
|
||||||
shellLink->SetDescription(description);
|
shellLink->SetDescription(description);
|
||||||
if (path!=NULL)
|
if (path != NULL)
|
||||||
shellLink->SetPath(path);
|
shellLink->SetPath(path);
|
||||||
if (args!=NULL)
|
if (args != NULL)
|
||||||
shellLink->SetArguments(args);
|
shellLink->SetArguments(args);
|
||||||
if (workingDir!=NULL)
|
if (workingDir != NULL)
|
||||||
shellLink->SetWorkingDirectory(workingDir);
|
shellLink->SetWorkingDirectory(workingDir);
|
||||||
|
|
||||||
//Save the shortcut to disk
|
//Save the shortcut to disk
|
||||||
h = persistFile->Save(wName, TRUE);
|
h = persistFile->Save(shortcutPath, TRUE);
|
||||||
if (FAILED(h)) {
|
if (FAILED(h))
|
||||||
|
{
|
||||||
errStr = "Failed to save shortcut";
|
errStr = "Failed to save shortcut";
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
@ -87,3 +110,53 @@ err:
|
||||||
return h;
|
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.
|
/// Basic definition of some running process.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Process {
|
pub struct Process {
|
||||||
pub pid : usize,
|
pub pid: usize,
|
||||||
pub name : String
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
@ -15,31 +15,40 @@ mod natives {
|
||||||
|
|
||||||
const PROCESS_LEN: usize = 10192;
|
const PROCESS_LEN: usize = 10192;
|
||||||
|
|
||||||
use std::ffi::CString;
|
|
||||||
|
|
||||||
use logging::LoggingErrors;
|
use logging::LoggingErrors;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH};
|
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::{
|
use winapi::um::winnt::{
|
||||||
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
|
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
|
||||||
};
|
};
|
||||||
use winapi::um::processthreadsapi::{OpenProcess};
|
|
||||||
use winapi::um::psapi::{
|
use widestring::U16CString;
|
||||||
K32EnumProcesses,
|
|
||||||
EnumProcessModulesEx, GetModuleFileNameExW, LIST_MODULES_ALL,
|
|
||||||
};
|
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
pub fn saveShortcut(
|
pub fn saveShortcut(
|
||||||
shortcutPath: *const ::std::os::raw::c_char,
|
shortcutPath: *const winapi::ctypes::wchar_t,
|
||||||
description: *const ::std::os::raw::c_char,
|
description: *const winapi::ctypes::wchar_t,
|
||||||
path: *const ::std::os::raw::c_char,
|
path: *const winapi::ctypes::wchar_t,
|
||||||
args: *const ::std::os::raw::c_char,
|
args: *const winapi::ctypes::wchar_t,
|
||||||
workingDir: *const ::std::os::raw::c_char,
|
workingDir: *const winapi::ctypes::wchar_t,
|
||||||
) -> ::std::os::raw::c_int;
|
) -> ::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
|
// Needed here for Windows interop
|
||||||
|
@ -59,15 +68,16 @@ mod natives {
|
||||||
|
|
||||||
info!("Generating shortcut @ {:?}", source_file);
|
info!("Generating shortcut @ {:?}", source_file);
|
||||||
|
|
||||||
let native_target_dir = CString::new(source_file.clone())
|
let native_target_dir = U16CString::from_str(source_file.clone())
|
||||||
.log_expect("Error while converting to C-style string");
|
.log_expect("Error while converting to wchar_t");
|
||||||
let native_description =
|
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 =
|
let native_target =
|
||||||
CString::new(target).log_expect("Error while converting to C-style string");
|
U16CString::from_str(target).log_expect("Error while converting to wchar_t");
|
||||||
let native_args = CString::new(args).log_expect("Error while converting to C-style string");
|
let native_args =
|
||||||
|
U16CString::from_str(args).log_expect("Error while converting to wchar_t");
|
||||||
let native_working_dir =
|
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 {
|
let shortcutResult = unsafe {
|
||||||
saveShortcut(
|
saveShortcut(
|
||||||
|
@ -108,15 +118,41 @@ mod natives {
|
||||||
.log_expect("Unable to convert log path to string")
|
.log_expect("Unable to convert log path to string")
|
||||||
.replace(" ", "\\ ");
|
.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);
|
info!("Launching cmd with {:?}", target_arguments);
|
||||||
|
|
||||||
Command::new("C:\\Windows\\system32\\cmd.exe")
|
// Needs to use `spawnDetached` which is an unsafe C/C++ function from interop.cpp
|
||||||
.arg("/C")
|
#[allow(unsafe_code)]
|
||||||
.arg(&target_arguments)
|
let spawn_result: i32 = unsafe {
|
||||||
.spawn()
|
let mut cmd_path = [0u16; MAX_PATH + 1];
|
||||||
.log_expect("Unable to start child process");
|
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)]
|
#[allow(unsafe_code)]
|
||||||
|
@ -149,9 +185,7 @@ mod natives {
|
||||||
|
|
||||||
let size = ::std::mem::size_of::<DWORD>() * process_ids.len();
|
let size = ::std::mem::size_of::<DWORD>() * process_ids.len();
|
||||||
unsafe {
|
unsafe {
|
||||||
if K32EnumProcesses(process_ids.as_mut_ptr(),
|
if K32EnumProcesses(process_ids.as_mut_ptr(), size as DWORD, &mut cb_needed) == 0 {
|
||||||
size as DWORD,
|
|
||||||
&mut cb_needed) == 0 {
|
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +194,7 @@ mod natives {
|
||||||
|
|
||||||
let mut processes = Vec::new();
|
let mut processes = Vec::new();
|
||||||
|
|
||||||
for i in 0 .. nb_processes {
|
for i in 0..nb_processes {
|
||||||
let pid = process_ids[i as usize];
|
let pid = process_ids[i as usize];
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -169,20 +203,25 @@ mod natives {
|
||||||
let mut process_name = [0u16; MAX_PATH + 1];
|
let mut process_name = [0u16; MAX_PATH + 1];
|
||||||
let mut cb_needed = 0;
|
let mut cb_needed = 0;
|
||||||
|
|
||||||
if EnumProcessModulesEx(process_handler,
|
if EnumProcessModulesEx(
|
||||||
|
process_handler,
|
||||||
&mut h_mod,
|
&mut h_mod,
|
||||||
::std::mem::size_of::<DWORD>() as DWORD,
|
::std::mem::size_of::<DWORD>() as DWORD,
|
||||||
&mut cb_needed,
|
&mut cb_needed,
|
||||||
LIST_MODULES_ALL) != 0 {
|
LIST_MODULES_ALL,
|
||||||
GetModuleFileNameExW(process_handler,
|
) != 0
|
||||||
|
{
|
||||||
|
GetModuleFileNameExW(
|
||||||
|
process_handler,
|
||||||
h_mod,
|
h_mod,
|
||||||
process_name.as_mut_ptr(),
|
process_name.as_mut_ptr(),
|
||||||
MAX_PATH as DWORD + 1);
|
MAX_PATH as DWORD + 1,
|
||||||
|
);
|
||||||
|
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
for x in process_name.iter() {
|
for x in process_name.iter() {
|
||||||
if *x == 0 {
|
if *x == 0 {
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
@ -199,6 +238,12 @@ mod natives {
|
||||||
|
|
||||||
processes
|
processes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needed here for Windows interop
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn is_dark_mode_active() -> bool {
|
||||||
|
unsafe { isDarkThemeActive() == 1 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
|
@ -209,6 +254,15 @@ mod natives {
|
||||||
|
|
||||||
use logging::LoggingErrors;
|
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(
|
pub fn create_shortcut(
|
||||||
name: &str,
|
name: &str,
|
||||||
description: &str,
|
description: &str,
|
||||||
|
@ -216,9 +270,51 @@ mod natives {
|
||||||
args: &str,
|
args: &str,
|
||||||
working_dir: &str,
|
working_dir: &str,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
// TODO: no-op
|
// FIXME: no icon will be shown since no icon is provided
|
||||||
warn!("create_shortcut is stubbed!");
|
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())
|
Ok("".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,8 +338,23 @@ mod natives {
|
||||||
|
|
||||||
/// Returns a list of running processes
|
/// Returns a list of running processes
|
||||||
pub fn get_process_names() -> Vec<super::Process> {
|
pub fn get_process_names() -> Vec<super::Process> {
|
||||||
// TODO: no-op
|
// a platform-independent implementation using sysinfo crate
|
||||||
vec![]
|
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!(
|
.get(&format!(
|
||||||
"https://api.github.com/repos/{}/releases",
|
"https://api.github.com/repos/{}/releases",
|
||||||
config.repo
|
config.repo
|
||||||
)).header(USER_AGENT, "liftinstall (j-selby)")
|
))
|
||||||
|
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||||
.send()
|
.send()
|
||||||
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
|
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
StatusCode::OK => {}
|
StatusCode::OK => {}
|
||||||
StatusCode::FORBIDDEN => {
|
StatusCode::FORBIDDEN => {
|
||||||
return Err(format!(
|
return Err(
|
||||||
"GitHub is rate limiting you. Try moving to a internet connection \
|
"GitHub is rate limiting you. Try moving to a internet connection \
|
||||||
that isn't shared, and/or disabling VPNs."
|
that isn't shared, and/or disabling VPNs."
|
||||||
));
|
.to_string(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(format!("Bad status code: {:?}.", response.status()));
|
return Err(format!("Bad status code: {:?}.", response.status()));
|
||||||
|
@ -87,14 +89,18 @@ impl ReleaseSource for GithubReleases {
|
||||||
let string = match asset["name"].as_str() {
|
let string = match asset["name"].as_str() {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
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() {
|
let url = match asset["browser_download_url"].as_str() {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => {
|
||||||
return Err("JSON payload missing information about release URL".to_string())
|
return Err(
|
||||||
|
"JSON payload missing information about release URL".to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub mod github;
|
||||||
use self::types::ReleaseSource;
|
use self::types::ReleaseSource;
|
||||||
|
|
||||||
/// Returns a ReleaseSource by a name, if possible
|
/// 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 {
|
match name {
|
||||||
"github" => Some(Box::new(github::GithubReleases::new())),
|
"github" => Some(Box::new(github::GithubReleases::new())),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
|
@ -12,7 +12,7 @@ use tasks::resolver::ResolvePackageTask;
|
||||||
|
|
||||||
use http::stream_file;
|
use http::stream_file;
|
||||||
|
|
||||||
use number_prefix::{decimal_prefix, Prefixed, Standalone};
|
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
||||||
|
|
||||||
use logging::LoggingErrors;
|
use logging::LoggingErrors;
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ impl Task for DownloadPackageTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
mut input: Vec<TaskParamType>,
|
mut input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 1);
|
assert_eq!(input.len(), 1);
|
||||||
|
|
||||||
|
@ -68,11 +68,11 @@ impl Task for DownloadPackageTask {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pretty print data volumes
|
// 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),
|
Standalone(bytes) => format!("{} bytes", bytes),
|
||||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
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),
|
Standalone(bytes) => format!("{} bytes", bytes),
|
||||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,8 @@ use tasks::TaskDependency;
|
||||||
use tasks::TaskMessage;
|
use tasks::TaskMessage;
|
||||||
use tasks::TaskParamType;
|
use tasks::TaskParamType;
|
||||||
|
|
||||||
use native::Process;
|
|
||||||
use native::get_process_names;
|
use native::get_process_names;
|
||||||
|
use native::Process;
|
||||||
|
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ impl Task for EnsureOnlyInstanceTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
_messenger: &Fn(&TaskMessage),
|
_messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 0);
|
assert_eq!(input.len(), 0);
|
||||||
|
|
||||||
|
@ -32,13 +32,13 @@ impl Task for EnsureOnlyInstanceTask {
|
||||||
let exe = name;
|
let exe = name;
|
||||||
|
|
||||||
if exe.ends_with("maintenancetool.exe") || exe.ends_with("maintenancetool") {
|
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 package in &context.database.packages {
|
||||||
for file in &package.files {
|
for file in &package.files {
|
||||||
if exe.ends_with(file) {
|
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 {
|
fn name(&self) -> String {
|
||||||
format!("EnsureOnlyInstanceTask")
|
"EnsureOnlyInstanceTask".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ impl Task for InstallTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Vec<TaskParamType>,
|
_: Vec<TaskParamType>,
|
||||||
_: &mut InstallerFramework,
|
_: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
||||||
Ok(TaskParamType::None)
|
Ok(TaskParamType::None)
|
||||||
|
|
|
@ -21,7 +21,7 @@ impl Task for VerifyInstallDirTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 0);
|
assert_eq!(input.len(), 0);
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
|
|
|
@ -20,7 +20,7 @@ impl Task for InstallGlobalShortcutsTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Vec<TaskParamType>,
|
_: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
"Generating global shortcut...",
|
"Generating global shortcut...",
|
||||||
|
|
|
@ -34,7 +34,7 @@ impl Task for InstallPackageTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
mut input: Vec<TaskParamType>,
|
mut input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
&format!("Installing package {:?}...", self.name),
|
&format!("Installing package {:?}...", self.name),
|
||||||
|
|
|
@ -22,7 +22,7 @@ impl Task for InstallShortcutsTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Vec<TaskParamType>,
|
_: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
&format!("Generating shortcuts for package {:?}...", self.name),
|
&format!("Generating shortcuts for package {:?}...", self.name),
|
||||||
|
|
|
@ -49,12 +49,12 @@ pub enum TaskOrdering {
|
||||||
/// A dependency of a task with various properties.
|
/// A dependency of a task with various properties.
|
||||||
pub struct TaskDependency {
|
pub struct TaskDependency {
|
||||||
ordering: TaskOrdering,
|
ordering: TaskOrdering,
|
||||||
task: Box<Task>,
|
task: Box<dyn Task>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaskDependency {
|
impl TaskDependency {
|
||||||
/// Builds a new dependency from the specified task.
|
/// 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 }
|
TaskDependency { ordering, task }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ pub trait Task {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String>;
|
) -> Result<TaskParamType, String>;
|
||||||
|
|
||||||
/// Returns a vector containing all dependencies that need to be executed
|
/// 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.
|
/// The dependency tree allows for smart iteration on a Task struct.
|
||||||
pub struct DependencyTree {
|
pub struct DependencyTree {
|
||||||
task: Box<Task>,
|
task: Box<dyn Task>,
|
||||||
dependencies: Vec<(TaskOrdering, DependencyTree)>,
|
dependencies: Vec<(TaskOrdering, DependencyTree)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ impl DependencyTree {
|
||||||
pub fn execute(
|
pub fn execute(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
let total_tasks = (self.dependencies.len() + 1) as f64;
|
let total_tasks = (self.dependencies.len() + 1) as f64;
|
||||||
|
|
||||||
|
@ -133,8 +133,8 @@ impl DependencyTree {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = i.execute(context, &|msg: &TaskMessage| match msg {
|
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
|
||||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
TaskMessage::DisplayMessage(msg, progress) => {
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
msg,
|
msg,
|
||||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||||
|
@ -159,8 +159,8 @@ impl DependencyTree {
|
||||||
|
|
||||||
let task_result = self
|
let task_result = self
|
||||||
.task
|
.task
|
||||||
.execute(inputs, context, &|msg: &TaskMessage| match msg {
|
.execute(inputs, context, &|msg: &TaskMessage| match *msg {
|
||||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
TaskMessage::DisplayMessage(msg, progress) => {
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
msg,
|
msg,
|
||||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||||
|
@ -179,8 +179,8 @@ impl DependencyTree {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = i.execute(context, &|msg: &TaskMessage| match msg {
|
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
|
||||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
TaskMessage::DisplayMessage(msg, progress) => {
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
msg,
|
msg,
|
||||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
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.
|
/// 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
|
let dependencies = task
|
||||||
.dependencies()
|
.dependencies()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
@ -24,7 +24,7 @@ impl Task for ResolvePackageTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 0);
|
assert_eq!(input.len(), 0);
|
||||||
let mut metadata: Option<PackageDescription> = None;
|
let mut metadata: Option<PackageDescription> = None;
|
||||||
|
|
|
@ -14,7 +14,7 @@ impl Task for SaveDatabaseTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 0);
|
assert_eq!(input.len(), 0);
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
|
|
|
@ -23,7 +23,7 @@ impl Task for SaveExecutableTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 0);
|
assert_eq!(input.len(), 0);
|
||||||
messenger(&TaskMessage::DisplayMessage(
|
messenger(&TaskMessage::DisplayMessage(
|
||||||
|
|
|
@ -19,7 +19,7 @@ impl Task for UninstallTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Vec<TaskParamType>,
|
_: Vec<TaskParamType>,
|
||||||
_: &mut InstallerFramework,
|
_: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
||||||
Ok(TaskParamType::None)
|
Ok(TaskParamType::None)
|
||||||
|
|
|
@ -18,7 +18,7 @@ impl Task for UninstallGlobalShortcutsTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 0);
|
assert_eq!(input.len(), 0);
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ impl Task for UninstallPackageTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 1);
|
assert_eq!(input.len(), 1);
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ impl Task for UninstallShortcutsTask {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: Vec<TaskParamType>,
|
input: Vec<TaskParamType>,
|
||||||
context: &mut InstallerFramework,
|
context: &mut InstallerFramework,
|
||||||
messenger: &Fn(&TaskMessage),
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
) -> Result<TaskParamType, String> {
|
) -> Result<TaskParamType, String> {
|
||||||
assert_eq!(input.len(), 0);
|
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 a new issue