Compare commits

..

No commits in common. "master" and "1.3" have entirely different histories.
master ... 1.3

146 changed files with 2905 additions and 16573 deletions

View file

@ -1,58 +0,0 @@
name: Rust
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: hecrj/setup-rust-action@master
with:
rust-version: stable
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev libssl-dev libappindicator3-dev
if: runner.os == 'Linux'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Get cargo cache directory path
id: cargo-cache-dir-path
run: echo "::set-output name=dir::$HOME/.cargo/"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- uses: actions/cache@v2
id: cargo-cache
with:
path: ${{ steps.cargo-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- uses: actions/setup-node@v1
with:
node-version: '16.x'
- run: npm install -g yarn
- uses: actions/checkout@v2
- name: Download Webview2
run: Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'MicrosoftEdgeWebview2Setup.exe'
if: runner.os == 'Windows'
shell: pwsh
- name: Build
run: cargo build --verbose

2
.gitignore vendored
View file

@ -5,5 +5,3 @@
**/*.rs.bk **/*.rs.bk
*.log *.log
*.exe

11
.travis.yml Normal file
View file

@ -0,0 +1,11 @@
os: linux
dist: trusty
sudo: required
services:
- docker
install:
- docker pull ubuntu:18.04
script:
- docker run -v $(pwd):/liftinstall ubuntu:18.04 /bin/bash -ex /liftinstall/.travis/build.sh

View file

@ -1,6 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
cd /liftinstall || exit 1 cd /liftinstall
yarn --cwd ui apt update
apt install -y libwebkit2gtk-4.0-dev libssl-dev
cargo build --release curl https://sh.rustup.rs -sSf | sh -s -- -y
export PATH=~/.cargo/bin:$PATH
cargo build

View file

@ -1,4 +0,0 @@
#!/bin/bash -ex
# the UID for the container yuzu user is 1027
docker run -u root -v $(pwd):/liftinstall -t yuzuemu/build-environments:linux-liftinstall /bin/bash /liftinstall/.travis/build.sh

View file

@ -1,8 +0,0 @@
[main]
host = https://www.transifex.com
[o:yuzu-emulator:p:yuzu:r:installer]
file_filter = ui/translations/<lang>.po
source_file = ui/translations/en.po
source_lang = en
type = PO

4131
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,84 +1,55 @@
[package] [package]
name = "liftinstall" name = "liftinstall"
version = "0.2.0" version = "0.1.0"
edition = "2018"
authors = ["James <jselby@jselby.net>"] authors = ["James <jselby@jselby.net>"]
repository = "https://github.com/j-selby/liftinstall.git" repository = "https://github.com/j-selby/liftinstall.git"
documentation = "https://liftinstall.jselby.net" documentation = "https://liftinstall.jselby.net"
description = "An adaptable installer for your application." description = "An adaptable installer for your application."
build = "build.rs" build = "build.rs"
resolver = "2"
[dependencies] [dependencies]
anyhow = "^1" web-view = {git = "https://github.com/Boscop/web-view.git", rev = "555f422d09cbb94e82a728d47e9e07ca91963f6e"}
wry = "0.12"
tinyfiledialogs = "3.8"
hyper = "0.11.27" hyper = "0.11.27"
futures = "0.1.29" futures = "*"
mime_guess = "2.0" mime_guess = "1.8.3"
url = "2.2" url = "*"
reqwest = "0.9.22" reqwest = "0.9.0"
number_prefix = "0.4" number_prefix = "0.2.7"
serde = "1.0" serde = "1.0.27"
serde_derive = "1.0" serde_derive = "1.0.27"
serde_json = "1.0" serde_json = "1.0.9"
toml = "0.5" toml = "0.4"
semver = {version = "1.0", features = ["serde"]} semver = {version = "0.9.0", features = ["serde"]}
regex = "1.4" regex = "0.2"
dirs = "^4" dirs = "1.0"
zip = "0.6" zip = "0.4.2"
xz2 = "0.1" xz-decom = {git = "https://github.com/j-selby/xz-decom.git", rev = "9ebf3d00d9ff909c39eec1d2cf7e6e068ce214e5"}
tar = "0.4" tar = "0.4"
log = "0.4" log = "0.4"
fern = "0.6" fern = "0.5"
chrono = "0.4" chrono = "0.4.5"
clap = "2.33" clap = "2.32.0"
# used to open a link to the users default browser sysinfo = "*"
webbrowser = "0.8"
# used in JWT based package authentication
jsonwebtoken = "^8"
# used to decode the public key for verifying JWT tokens
base64 = "0.13"
[build-dependencies] [build-dependencies]
walkdir = "2.3" walkdir = "2"
serde = "1.0" serde = "1.0.27"
serde_derive = "1.0" serde_derive = "1.0.27"
toml = "0.5" toml = "0.4"
which = "4.0"
image = { version = "0.24", default-features = false, features = ["ico"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["psapi", "winbase", "winioctl", "winnt"] } # NFD is needed on Windows, as web-view doesn't work correctly here
widestring = "^1" nfd = "0.0.4"
webview2 = "0.1"
tempfile = "3"
[target.'cfg(not(windows))'.dependencies]
sysinfo = "0.26"
slug = "0.1"
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winres = "0.1" winres = "0.1"
cc = "1.0" cc = "1.0"
[profile.release]
#panic = "abort"
lto = true
opt-level = "z"
codegen-units = 1
incremental = false
#[profile.release.overrides."*"] # +
#opt-level = "z"
#codegen-units = 1
#incremental = false

View file

@ -1,18 +0,0 @@
ui-build:
yarn --cwd {{ justfile_directory() }}/ui/ install
yarn --cwd {{ justfile_directory() }}/ui/ build
ui-test:
cd {{ justfile_directory() }}/ui/ && node mock-server.js &
yarn --cwd {{ justfile_directory() }}/ui/ serve
update-i18n:
#!/bin/bash -e
[ -z "${TX_PULL}" ] || tx pull -a --minimum-perc 85
for i in {{ justfile_directory() }}/ui/translations/*.po; do
TARGET_FILE="$(basename $i)"
TARGET_LANG="${TARGET_FILE/.po/}"
OUTPUT="{{ justfile_directory() }}/ui/src/locales/${TARGET_LANG}.json"
i18next-conv -l en -s "$i" -t "$OUTPUT" -K
node {{ justfile_directory() }}/ui/unbreak-translations.js "$OUTPUT" "$OUTPUT"
done

View file

@ -22,7 +22,6 @@ 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.
@ -34,8 +33,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 `ui/public/favicon.ico` - Add your favicon to `static/favicon.ico`
- Add your logo to `ui/src/assets/logo.png` - Add your logo to `static/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:

View file

@ -1,13 +0,0 @@
# Security Policy
## Supported Versions
As `liftinstall` is a template for your project, no specific versioning is
provided at this time, though a rough version is given in the Cargo file.
Only the latest version from this file will be supported.
## Reporting a Vulnerability
For any specific security concerns/vulnerabilities, please email me directly
at security *at* jselby.net.

View file

@ -1,2 +1,2 @@
name = "yuzu" name = "yuzu"
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v3.toml" target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v1.toml"

View file

@ -1,3 +0,0 @@
# fake configuration for CI purpose only
name = "yuzu"
target_url = "https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml"

View file

@ -1,2 +1,2 @@
name = "yuzu" name = "yuzu"
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v10.toml" target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v5.toml"

133
build.rs
View file

@ -1,3 +1,5 @@
extern crate walkdir;
#[cfg(windows)] #[cfg(windows)]
extern crate winres; extern crate winres;
@ -9,21 +11,23 @@ extern crate serde;
extern crate serde_derive; extern crate serde_derive;
extern crate toml; extern crate toml;
extern crate which; use walkdir::WalkDir;
use std::env; use std::env;
use std::io::Write;
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::process::Command; use std::io::Write;
use std::env::consts::OS; use std::env::consts::OS;
use image::imageops::FilterType; 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)]
@ -35,7 +39,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("ui/public/favicon.ico"); res.set_icon("static/favicon.ico");
res.set( res.set(
"FileDescription", "FileDescription",
&format!("Interactive installer for {}", config.name), &format!("Interactive installer for {}", config.name),
@ -49,8 +53,6 @@ fn handle_binary(config: &BaseAttributes) {
cc::Build::new() cc::Build::new()
.cpp(true) .cpp(true)
.define("_WIN32_WINNT", Some("0x0600"))
.define("WINVER", Some("0x0600"))
.file("src/native/interop.cpp") .file("src/native/interop.cpp")
.compile("interop"); .compile("interop");
} }
@ -60,18 +62,9 @@ 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();
#[cfg(windows)]
{
if std::fs::metadata("MicrosoftEdgeWebview2Setup.exe").is_err() {
panic!("Please download MicrosoftEdgeWebview2Setup.exe from https://go.microsoft.com/fwlink/p/?LinkId=2124703 and put the file at the workspace root!");
}
}
// Find target config // Find target config
let target_config = PathBuf::from(format!("bootstrap.{}.toml", os)); let target_config = PathBuf::from(format!("bootstrap.{}.toml", os));
@ -99,42 +92,80 @@ 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");
let yarn_binary = // Copy files from static/ to build dir
which::which("yarn").expect("Failed to find yarn - please go ahead and install it!"); for entry in WalkDir::new("static") {
let entry = entry.expect("Unable to read output directory");
// bundle the icon let output_file = output_dir.join(entry.path());
let mut f = File::create(output_dir.join("icon-data.bin")).unwrap();
let icon_file = image::open("ui/public/favicon.ico").expect("Unable to read the icon file");
let icon_data = icon_file
.resize_exact(48, 48, FilterType::Triangle)
.to_rgba8();
f.write_all(&icon_data.into_vec()).unwrap();
// Build and deploy frontend files if entry.path().is_dir() {
Command::new(&yarn_binary) create_dir_all(output_file).expect("Unable to create dir");
.arg("--version") } else {
.spawn() let filename = entry
.expect("Yarn could not be launched"); .path()
Command::new(&yarn_binary) .file_name()
.arg("--cwd") .expect("Unable to parse filename")
.arg(ui_dir.to_str().expect("Unable to covert path"))
.spawn()
.unwrap()
.wait()
.expect("Unable to install Node.JS dependencies using Yarn");
let return_code = 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 path"), .expect("Unable to convert to string");
])
.status() if FILES_TO_PREPROCESS.contains(&filename) {
.expect("Unable to build frontend assets using Webpack"); // Do basic preprocessing - transcribe template string
assert!(return_code.success()); let source = BufReader::new(File::open(entry.path()).expect("Unable to copy file"));
let mut target = File::create(output_file).expect("Unable to copy file");
let mut is_template_string = false;
for line in source.lines() {
let line = line.expect("Unable to read line from JS file");
let mut is_break = false;
let mut is_quote = false;
let mut output_line = String::new();
if is_template_string {
output_line += "\"";
}
for c in line.chars() {
if c == '\\' {
is_break = true;
output_line.push('\\');
continue;
}
if (c == '\"' || c == '\'') && !is_break && !is_template_string {
is_quote = !is_quote;
}
if c == '`' && !is_break && !is_quote {
output_line += "\"";
is_template_string = !is_template_string;
continue;
}
if c == '"' && !is_break && is_template_string {
output_line += "\\\"";
continue;
}
is_break = false;
output_line.push(c);
}
if is_template_string {
output_line += "\" +";
}
output_line.push('\n');
target
.write(output_line.as_bytes())
.expect("Unable to write line");
}
} else {
copy(entry.path(), output_file).expect("Unable to copy file");
}
}
}
} }

View file

@ -1,52 +0,0 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
default = true
[packages.source]
name = "github"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"
[[packages]]
name = "yuzu Early Access"
description = "Bonus preview release for project supporters. Thanks for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.source]
name = "patreon"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-linux-earlyaccess/yuzu.exe"
description = "Launch yuzu Early Access"

View file

@ -1,58 +0,0 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu Early Access"
description = "Preview release with the newest features for the supporters."
icon = "thicc_logo_installer__ea_shadow.png"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.extended_description]
no_action_description = "Thank you for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
[packages.source]
name = "patreon"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-linux-early-access/yuzu-early-access.AppImage"
description = "Launch yuzu Early Access"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
icon = "thicc_logo_installer_shadow.png"
default = true
[packages.source]
name = "github"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-linux-mainline/yuzu-mainline.AppImage"
description = "Launch yuzu"

View file

@ -1,58 +0,0 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu Early Access"
description = "Preview release with the newest features for the supporters."
icon = "thicc_logo_installer__ea_shadow.png"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.extended_description]
no_action_description = "Thank you for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
[packages.source]
name = "patreon"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
description = "Launch yuzu Early Access"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
icon = "thicc_logo_installer_shadow.png"
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"

View file

@ -1,6 +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.4/yuzu_install.exe" new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.3/yuzu_install.exe"
[[packages]] [[packages]]
name = "yuzu Nightly" name = "yuzu Nightly"

View file

@ -1,6 +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.4/yuzu_install.exe" new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.3/yuzu_install.exe"
[[packages]] [[packages]]
name = "yuzu Nightly" name = "yuzu Nightly"

View file

@ -1,6 +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.4/yuzu_install.exe" new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.3/yuzu_install.exe"
[[packages]] [[packages]]
name = "yuzu Nightly" name = "yuzu Nightly"

View file

@ -1,6 +1,5 @@
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.4/yuzu_install.exe"
[[packages]] [[packages]]
name = "yuzu Nightly" name = "yuzu Nightly"

View file

@ -1,31 +0,0 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.5/yuzu_install.exe"
[[packages]]
name = "yuzu Nightly"
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)"

View file

@ -1,31 +0,0 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.6/yuzu_install.exe"
[[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)"

View file

@ -1,17 +0,0 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.7/yuzu_install.exe"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"

View file

@ -1,52 +0,0 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"
[[packages]]
name = "yuzu Early Access"
description = "Bonus preview release for project supporters. Thanks for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.source]
name = "patreon"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
description = "Launch yuzu Early Access"

View file

@ -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 xz2::read::XzDecoder; use xz_decom;
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 dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>, func: &mut FnMut(usize, Option<usize>, PathBuf, &mut 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 dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>, func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
) -> Result<(), String> { ) -> Result<(), String> {
let max = self.archive.len(); let max = self.archive.len();
@ -41,7 +41,7 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
continue; continue;
} }
func(i, Some(max), archive.mangled_name(), &mut archive)?; func(i, Some(max), archive.sanitized_name(), &mut archive)?;
} }
Ok(()) Ok(())
@ -49,13 +49,13 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
} }
struct TarArchive<'a> { struct TarArchive<'a> {
archive: UpstreamTarArchive<Box<dyn Read + 'a>>, archive: UpstreamTarArchive<Box<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 dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>, func: &mut FnMut(usize, Option<usize>, PathBuf, &mut 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<dyn Archive<'a> + 'a>, String> { pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<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,13 +92,10 @@ pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<dyn 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 mut decompresser = XzDecoder::new(data); let decompressed_data = xz_decom::decompress(data)
let mut decompressed_data = Vec::new(); .map_err(|x| format!("Failed to build decompressor: {:?}", x))?;
decompresser
.read_to_end(&mut decompressed_data)
.map_err(|x| format!("Failed to decompress data: {:?}", x))?;
let decompressed_contents: Box<dyn Read> = Box::new(Cursor::new(decompressed_data)); let decompressed_contents: Box<Read> = Box::new(Cursor::new(decompressed_data));
let tar = UpstreamTarArchive::new(decompressed_contents); let tar = UpstreamTarArchive::new(decompressed_contents);

View file

@ -2,8 +2,7 @@
extern crate mime_guess; extern crate mime_guess;
use self::mime_guess::from_ext; use assets::mime_guess::{get_mime_type, octet_stream};
use self::mime_guess::mime::APPLICATION_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 ),* ) => {
@ -24,9 +23,9 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
Some(ext_ptr) => { Some(ext_ptr) => {
let ext = &file_path[ext_ptr + 1..]; let ext = &file_path[ext_ptr + 1..];
from_ext(ext).first_or_octet_stream() get_mime_type(ext)
} }
None => APPLICATION_OCTET_STREAM, None => octet_stream(),
}; };
let string_mime = guessed_mime.to_string(); let string_mime = guessed_mime.to_string();
@ -35,21 +34,18 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
file_path, file_path,
"/index.html", "/index.html",
"/favicon.ico", "/favicon.ico",
"/img/light_mode_installer_logo.png", "/logo.png",
"/img/dark_mode_installer_logo.png", "/how-to-open.png",
"/thicc_logo_installer__ea_shadow.png", "/css/bulma.min.css",
"/thicc_logo_installer_shadow.png", "/css/main.css",
"/img/how-to-open.png",
"/css/app.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",
"/fonts/materialdesignicons-webfont.eot", "/js/vue.min.js",
"/fonts/materialdesignicons-webfont.woff", "/js/vue-router.min.js",
"/fonts/materialdesignicons-webfont.woff2", "/js/helpers.js",
"/js/chunk-vendors.js", "/js/views.js",
"/js/app.js" "/js/main.js"
)?; )?;
Some((string_mime, contents)) Some((string_mime, contents))

View file

@ -7,8 +7,8 @@ use toml::de::Error as TomlError;
use serde_json::{self, Error as SerdeError}; use serde_json::{self, Error as SerdeError};
use crate::sources::get_by_name; use sources::get_by_name;
use crate::sources::types::Release; use sources::types::Release;
/// Description of the source of a package. /// Description of the source of a package.
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@ -25,23 +25,6 @@ pub struct PackageShortcut {
pub name: String, pub name: String,
pub relative_path: String, pub relative_path: String,
pub description: String, pub description: String,
#[serde(default)]
pub has_desktop_shortcut: bool,
}
/// Extra description for authentication and authorization state for a package
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PackageExtendedDescription {
#[serde(default)]
pub no_action_description: Option<String>,
#[serde(default)]
pub need_authentication_description: Option<String>,
#[serde(default)]
pub need_link_description: Option<String>,
#[serde(default)]
pub need_subscription_description: Option<String>,
#[serde(default)]
pub need_reward_tier_description: Option<String>,
} }
/// Describes a overview of a individual package. /// Describes a overview of a individual package.
@ -49,34 +32,10 @@ pub struct PackageExtendedDescription {
pub struct PackageDescription { pub struct PackageDescription {
pub name: String, pub name: String,
pub description: String, pub description: String,
#[serde(default)]
pub icon: Option<String>,
pub default: Option<bool>, pub default: Option<bool>,
pub source: PackageSource, pub source: PackageSource,
#[serde(default)] #[serde(default)]
pub shortcuts: Vec<PackageShortcut>, pub shortcuts: Vec<PackageShortcut>,
#[serde(default)]
pub requires_authorization: Option<bool>,
#[serde(default)]
pub is_new: Option<bool>,
#[serde(default)]
pub extended_description: Option<PackageExtendedDescription>,
}
/// Configuration for validating the JWT token
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JWTValidation {
pub iss: Option<String>,
// This can technically be a Vec as well, but thats a pain to support atm
pub aud: Option<String>,
}
/// The configuration for this release.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AuthenticationConfig {
pub pub_key_base64: String,
pub auth_url: String,
pub validation: Option<JWTValidation>,
} }
/// Describes the application itself. /// Describes the application itself.
@ -84,8 +43,6 @@ pub struct AuthenticationConfig {
pub struct BaseAttributes { pub struct BaseAttributes {
pub name: String, pub name: String,
pub target_url: String, pub target_url: String,
#[serde(default)]
pub recovery: bool,
} }
impl BaseAttributes { impl BaseAttributes {
@ -109,8 +66,6 @@ pub struct Config {
pub packages: Vec<PackageDescription>, pub packages: Vec<PackageDescription>,
#[serde(default)] #[serde(default)]
pub hide_advanced: bool, pub hide_advanced: bool,
#[serde(default)]
pub authentication: Option<AuthenticationConfig>,
} }
impl Config { impl Config {

View file

@ -1,29 +0,0 @@
//! frontend/mod.rs
//!
//! Provides the frontend interface, including HTTP server.
use std::sync::{Arc, RwLock};
use crate::installer::InstallerFramework;
use crate::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).log_expect("Failed to start UI");
// 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");
}

View file

@ -1,7 +0,0 @@
//! frontend/rest/mod.rs
//!
//! Contains the main web server used within the application.
mod assets;
pub mod server;
pub mod services;

View file

@ -1,86 +0,0 @@
//! frontend/rest/server.rs
//!
//! Contains the over-arching server object + methods to manipulate it.
use crate::frontend::rest::services::WebService;
use crate::installer::InstallerFramework;
use crate::logging::LoggingErrors;
use hyper::server::Http;
use std::sync::{Arc, RwLock};
use std::net::{SocketAddr, TcpListener, ToSocketAddrs};
use std::thread;
use std::thread::JoinHandle;
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
/// application.
pub struct WebServer {
_handle: JoinHandle<()>,
}
impl WebServer {
/// Creates a new web server with the specified address.
pub fn with_addr(
framework: Arc<RwLock<InstallerFramework>>,
addr: SocketAddr,
) -> Result<Self, hyper::Error> {
let handle = thread::spawn(move || {
let server = Http::new()
.bind(&addr, move || Ok(WebService::new(framework.clone())))
.log_expect("Failed to bind to port");
server.run().log_expect("Failed to run HTTP server");
});
Ok(WebServer { _handle: handle })
}
}
/// Spawns a server instance on all local interfaces.
///
/// Returns server instances + http address of service running.
pub fn spawn_servers(framework: Arc<RwLock<InstallerFramework>>) -> (Vec<WebServer>, String) {
// Firstly, allocate us an epidermal port
let target_port = {
let listener = TcpListener::bind("127.0.0.1:0")
.log_expect("At least one local address should be free");
listener
.local_addr()
.log_expect("Should be able to pull address from listener")
.port()
};
// Now, iterate over all ports
let addresses = "localhost:0"
.to_socket_addrs()
.log_expect("No localhost address found");
let mut instances = Vec::with_capacity(addresses.len());
let mut http_address = None;
// Startup HTTP server for handling the web view
for mut address in addresses {
address.set_port(target_port);
let server = WebServer::with_addr(framework.clone(), address)
.log_expect("Failed to bind to address");
info!("Spawning server instance @ {:?}", address);
http_address = Some(address);
instances.push(server);
}
let http_address = http_address.log_expect("No HTTP address found");
(
instances,
format!("http://localhost:{}", http_address.port()),
)
}

View file

@ -1,29 +0,0 @@
//! frontend/rest/services/attributes.rs
//!
//! The /api/attr call returns an executable script containing session variables.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read();
let file = framework
.base_attributes
.to_json_str()
.log_expect("Failed to render JSON representation of config");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -1,293 +0,0 @@
//! frontend/rest/services/authentication.rs
//!
//! Provides mechanisms to authenticate users using JWT.
use std::collections::HashSet;
use std::sync::Arc;
use futures::{Future, Stream};
use hyper::header::{ContentLength, ContentType};
use jsonwebtoken::DecodingKey;
use jwt::{decode, Algorithm, Validation};
use reqwest::header::USER_AGENT;
use crate::frontend::rest::services::Future as InternalFuture;
use crate::frontend::rest::services::{default_future, Request, Response, WebService};
use crate::http::{build_async_client, build_client};
use crate::config::JWTValidation;
use crate::logging::LoggingErrors;
#[derive(Debug, Serialize, Deserialize)]
struct Auth {
username: String,
token: String,
jwt_token: Option<JWTClaims>,
}
/// claims struct, it needs to derive `Serialize` and/or `Deserialize`
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JWTClaims {
pub sub: String,
pub iss: String,
pub aud: String,
pub exp: usize,
#[serde(default)]
pub roles: Vec<String>,
#[serde(rename = "releaseChannels", default)]
pub channels: Vec<String>,
#[serde(rename = "isPatreonAccountLinked")]
pub is_linked: bool,
#[serde(rename = "isPatreonSubscriptionActive")]
pub is_subscribed: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AuthRequest {
username: String,
token: String,
}
/// Calls the given server to obtain a JWT token and returns a Future<String> with the response
pub fn authenticate_async(
url: String,
username: String,
token: String,
) -> Box<dyn futures::Future<Item = String, Error = String>> {
// Build the HTTP client up
let client = match build_async_client() {
Ok(v) => v,
Err(_) => {
return Box::new(futures::future::err(
"Unable to build async web client".to_string(),
));
}
};
Box::new(client.post(&url)
.header(USER_AGENT, "liftinstall (j-selby)")
.header("X-USERNAME", username.clone())
.header("X-TOKEN", token.clone())
.send()
.map_err(|err| {
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
err, err.is_client_error(), err.is_http(), err.is_redirect(),
err.is_serialization(), err.is_timeout(), err.is_server_error())
})
.map(|mut response| {
match response.status() {
reqwest::StatusCode::OK =>
Ok(response.text()
.map_err(|e| {
format!("Error while converting the response to text {:?}", e)
})),
_ => {
Err(format!("Error wrong response code from server {:?}", response.status()))
}
}
})
.and_then(|x| x)
.flatten()
)
}
pub fn authenticate_sync(url: String, username: String, token: String) -> Result<String, String> {
// Build the HTTP client up
let client = build_client()?;
let mut response = client.post(&url)
.header(USER_AGENT, "liftinstall (j-selby)")
.header("X-USERNAME", username.clone())
.header("X-TOKEN", token.clone())
.send()
.map_err(|err| {
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
err, err.is_client_error(), err.is_http(), err.is_redirect(),
err.is_serialization(), err.is_timeout(), err.is_server_error())
})?;
match response.status() {
reqwest::StatusCode::OK => Ok(response
.text()
.map_err(|e| format!("Error while converting the response to text {:?}", e))?),
_ => Err(format!(
"Error wrong response code from server {:?}",
response.status()
)),
}
}
pub fn validate_token(
body: String,
pub_key_base64: String,
validation: Option<JWTValidation>,
) -> Result<JWTClaims, String> {
// Get the public key for this authentication url
let pub_key = if pub_key_base64.is_empty() {
vec![]
} else {
base64::decode(&pub_key_base64).map_err(|e| {
format!(
"Configured public key was not empty and did not decode as base64 {:?}",
e
)
})?
};
// Configure validation for audience and issuer if the configuration provides it
let mut validation = match validation {
Some(v) => {
let mut valid = Validation::new(Algorithm::RS256);
valid.iss = v.iss.map(|iss| {
let mut issuer = HashSet::new();
issuer.insert(iss);
issuer
});
if let &Some(ref v) = &v.aud {
valid.set_audience(&[v]);
}
valid
}
None => Validation::default(),
};
validation.validate_exp = false;
validation.validate_nbf = false;
// Verify the JWT token
decode::<JWTClaims>(&body, &DecodingKey::from_rsa_der(&pub_key), &validation)
.map(|tok| tok.claims)
.map_err(|err| {
format!(
"Error while decoding the JWT. error: {:?} jwt: {:?}",
err, body
)
})
}
pub fn handle(service: &WebService, _req: Request) -> InternalFuture {
info!("Handling authentication");
let framework = service
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let credentials = framework.database.credentials.clone();
let config = framework
.config
.clone()
.log_expect("No in-memory configuration found");
// If authentication isn't configured, just return immediately
if config.authentication.is_none() {
return default_future(Response::new().with_status(hyper::Ok).with_body("{}"));
}
// Create moveable framework references so that the lambdas can write to them later
let write_cred_fw = Arc::clone(&service.framework);
Box::new(
_req.body()
.concat2()
.map(move |body| {
let req = serde_json::from_slice::<AuthRequest>(&body);
if req.is_err() {
warn!("Failed to parse auth request from the frontend");
return default_future(
Response::new().with_status(hyper::StatusCode::BadRequest),
);
}
let req = req.unwrap();
// Determine which credentials we should use
let (username, token) = {
let req_username = req.username;
let req_token = req.token;
// if the user didn't provide credentials, and theres nothing stored in the
// database, return an early error
let req_cred_valid = !req_username.is_empty() && !req_token.is_empty();
let stored_cred_valid =
!credentials.username.is_empty() && !credentials.token.is_empty();
if !req_cred_valid && !stored_cred_valid {
info!("No passed in credential and no stored credentials to validate");
return default_future(Response::new().with_status(hyper::BadRequest));
}
if req_cred_valid {
(req_username.clone(), req_token.clone())
} else {
(credentials.username.clone(), credentials.token.clone())
}
};
// second copy of the credentials so we can move them into a different closure
let (username_clone, token_clone) = (username.clone(), token.clone());
let authentication = config
.authentication
.log_expect("No authentication configuration");
let auth_url = authentication.auth_url.clone();
let pub_key_base64 = authentication.pub_key_base64.clone();
let validation = authentication.validation.clone();
// call the authentication URL to see if we are authenticated
Box::new(
authenticate_async(auth_url, username.clone(), token.clone())
.map(|body| validate_token(body, pub_key_base64, validation))
.and_then(|res| res)
.map(move |claims| {
let out = Auth {
username: username_clone,
token: token_clone,
jwt_token: Some(claims.clone()),
};
// Convert the json to a string and return the json token
match serde_json::to_string(&out) {
Ok(v) => Ok(v),
Err(e) => Err(format!(
"Error while converting the claims to JSON string: {:?}",
e
)),
}
})
.and_then(|res| res)
.map(move |json| {
{
// Store the validated username and password into the installer database
let mut framework = write_cred_fw
.write()
.log_expect("InstallerFramework has been dirtied");
framework.database.credentials.username = username;
framework.database.credentials.token = token;
}
// Finally return the JSON with the response
info!("successfully verified username and token");
Response::new()
.with_header(ContentLength(json.len() as u64))
.with_header(ContentType::json())
.with_status(hyper::StatusCode::Ok)
.with_body(json)
})
.map_err(|err| {
error!(
"Got an internal error while processing user token: {:?}",
err
);
Response::new().with_status(hyper::StatusCode::InternalServerError)
})
.or_else(|err| {
// Convert the Err value into an Ok value since the error code from
// this HTTP request is an Ok(response)
Ok(err)
}),
)
})
// Flatten the internal future into the output response future
.flatten(),
)
}

View file

@ -1,31 +0,0 @@
//! frontend/rest/services/browser.rs
//!
//! Launches the user's web browser on request from the frontend.
use crate::frontend::rest::services::Future as InternalFuture;
use crate::frontend::rest::services::{Request, Response, WebService};
use crate::logging::LoggingErrors;
use futures::{Future, Stream};
use hyper::header::ContentType;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct OpenRequest {
url: String,
}
pub fn handle(_service: &WebService, req: Request) -> InternalFuture {
Box::new(req.body().concat2().map(move |body| {
let req: OpenRequest = serde_json::from_slice(&body).log_expect("Malformed request");
if webbrowser::open(&req.url).is_ok() {
Response::new()
.with_status(hyper::Ok)
.with_header(ContentType::json())
.with_body("{}")
} else {
Response::new()
.with_status(hyper::BadRequest)
.with_header(ContentType::json())
.with_body("{}")
}
}))
}

View file

@ -1,84 +0,0 @@
//! 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 crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
use crate::config::Config;
use crate::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))
}),
)
}

View file

@ -1,27 +0,0 @@
//! frontend/rest/services/dark_mode.rs
//!
//! This call returns if dark mode is enabled on the system currently.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
use crate::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),
)
}

View file

@ -1,35 +0,0 @@
//! frontend/rest/services/default_path.rs
//!
//! The /api/default-path returns the default path for the application to install into.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
/// Struct used by serde to send a JSON payload to the client containing an optional value.
#[derive(Serialize)]
struct FileSelection {
path: Option<String>,
}
pub fn handle(service: &WebService, _req: Request) -> Future {
let path = { service.get_framework_read().get_default_path() };
let response = FileSelection { path };
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of default path object");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -1,32 +0,0 @@
//! frontend/rest/services/exit.rs
//!
//! The /api/exit closes down the application.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::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)),
)
}
}
}

View file

@ -1,100 +0,0 @@
//! frontend/rest/services/install.rs
//!
//! The /api/install call installs a set of packages dictated by a POST request.
use crate::frontend::rest::services::stream_progress;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::WebService;
use crate::logging::LoggingErrors;
use crate::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;
let mut force_install = false;
let mut install_desktop_shortcut = false;
// Transform results into just an array of stuff to install
for (key, value) in &results {
if key == "path" {
path = Some(value.to_owned());
continue;
} else if key == "installDesktopShortcut" {
info!("Found installDesktopShortcut {:?}", value);
install_desktop_shortcut = value == "true";
continue;
}
if key == "mode" && value == "force" {
force_install = true;
continue;
}
if value == "true" {
to_install.push(key.to_owned());
}
}
if !install_desktop_shortcut {
let framework_ref = framework
.read()
.log_expect("InstallerFramework has been dirtied");
install_desktop_shortcut = framework_ref.preexisting_install
&& framework_ref
.database
.packages
.first()
.and_then(|x| Some(x.shortcuts.len() > 1))
.unwrap_or(false);
}
// 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,
install_desktop_shortcut,
force_install,
) {
error!("Install error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send install error: {:?}", v);
}
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
})
}))
}

View file

@ -1,32 +0,0 @@
//! 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 crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read();
let response = framework.get_installation_status();
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of installation status object");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -1,158 +0,0 @@
//! frontend/rest/services/mod.rs
//!
//! Provides all services used by the REST server.
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::installer::{InstallMessage, InstallerFramework};
use hyper::server::Service;
use hyper::{Method, StatusCode};
use crate::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;
pub mod authentication;
mod browser;
mod config;
mod dark_mode;
mod default_path;
mod exit;
mod install;
mod installation_status;
mod packages;
mod static_files;
mod uninstall;
mod update_updater;
mod verify_path;
mod view_folder;
/// 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::Get, "/api/view-local-folder") => view_folder::handle(self, req),
(Method::Post, "/api/check-auth") => authentication::handle(self, req),
(Method::Post, "/api/start-install") => install::handle(self, req),
(Method::Post, "/api/open-browser") => browser::handle(self, req),
(Method::Post, "/api/uninstall") => uninstall::handle(self, req),
(Method::Post, "/api/update-updater") => update_updater::handle(self, req),
(Method::Post, "/api/verify-path") => verify_path::handle(self, req),
(Method::Get, _) => static_files::handle(self, req),
e => {
info!("Returned 404 for {:?}", e);
default_future(Response::new().with_status(StatusCode::NotFound))
}
}
}
}

View file

@ -1,31 +0,0 @@
//! frontend/rest/services/packages.rs
//!
//! The /api/packages call returns all the currently installed packages.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::encapsulate_json;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read();
let file = encapsulate_json(
"packages",
&serde_json::to_string(&framework.database)
.log_expect("Failed to render JSON representation of database"),
);
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -1,42 +0,0 @@
//! frontend/rest/services/static_files.rs
//!
//! The static files call returns static files embedded within the executable.
//!
//! e.g. index.html, main.js, ...
use crate::frontend::rest::assets;
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use hyper::StatusCode;
use crate::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),
})
}

View file

@ -1,34 +0,0 @@
//! frontend/rest/services/uninstall.rs
//!
//! The /api/uninstall call uninstalls all packages.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::stream_progress;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::WebService;
use crate::logging::LoggingErrors;
use crate::installer::InstallMessage;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.framework.clone();
default_future(stream_progress(move |sender| {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
if let Err(v) = framework.uninstall(&sender) {
error!("Uninstall error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send uninstall error: {:?}", v);
};
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
}))
}

View file

@ -1,34 +0,0 @@
//! frontend/rest/services/update_updater.rs
//!
//! The /api/update-updater call attempts to update the currently running updater.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::stream_progress;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::WebService;
use crate::logging::LoggingErrors;
use crate::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);
}
}))
}

View file

@ -1,48 +0,0 @@
//! frontend/rest/services/verify_path.rs
//!
//! The /api/verify-path returns whether the path exists or not.
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use url::form_urlencoded;
use hyper::header::{ContentLength, ContentType};
use futures::future::Future as _;
use futures::stream::Stream;
use crate::logging::LoggingErrors;
use std::collections::HashMap;
use std::path::PathBuf;
/// Struct used by serde to send a JSON payload to the client containing an optional value.
#[derive(Serialize)]
struct VerifyResponse {
exists: bool,
}
pub fn handle(_service: &WebService, req: Request) -> Future {
Box::new(req.body().concat2().map(move |b| {
let results = form_urlencoded::parse(b.as_ref())
.into_owned()
.collect::<HashMap<String, String>>();
let mut exists = false;
if let Some(path) = results.get("path") {
let path = PathBuf::from(path);
exists = path.is_dir();
}
let response = VerifyResponse { exists };
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of default path object");
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}))
}

View file

@ -1,35 +0,0 @@
//! frontend/rest/services/view_folder.rs
//!
//! The /api/view-local-folder returns whether the path exists or not.
//! Side-effect: will open the folder in the default file manager if it exists.
use super::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
use crate::native::open_in_shell;
pub fn handle(service: &WebService, _: Request) -> Future {
let framework = service.get_framework_read();
let mut response = false;
let path = framework.install_path.clone();
if let Some(path) = path {
response = true;
open_in_shell(path.as_path());
}
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of installation status object");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -1,86 +0,0 @@
//! frontend/ui/mod.rs
//!
//! Provides a web-view UI.
use anyhow::Result;
use wry::{
application::{
dpi::LogicalSize,
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Icon, WindowBuilder},
},
webview::{RpcResponse, WebViewBuilder},
};
use log::Level;
use crate::logging::LoggingErrors;
const ICON_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/icon-data.bin"));
/// Starts the main web UI. Will return when UI is closed.
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) -> Result<()> {
#[cfg(windows)]
{
crate::native::prepare_install_webview2(app_name).log_expect("Unable to install webview2");
}
let size = if is_launcher {
(600.0, 300.0)
} else {
(1024.0, 600.0)
};
info!("Spawning web view instance");
let window_icon =
Icon::from_rgba(ICON_DATA.to_vec(), 48, 48).log_expect("Unable to construct window icon");
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title(format!("{} Installer", app_name))
.with_window_icon(Some(window_icon))
.with_inner_size(LogicalSize::new(size.0, size.1))
.with_resizable(false)
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_url(http_address)?
.with_rpc_handler(|_, mut event| {
debug!("Incoming payload: {:?}", event);
match event.method.as_str() {
"Test" => (),
"Log" => {
if let Some(msg) = event.params.take() {
if let Ok(msg) = serde_json::from_value::<(String, String)>(msg) {
let kind = match msg.0.as_str() {
"info" | "log" => Level::Info,
"warn" => Level::Warn,
_ => Level::Error,
};
log!(target: "liftinstall::frontend::js", kind, "{}", msg.1);
}
}
}
"SelectInstallDir" => {
let result =
tinyfiledialogs::select_folder_dialog("Select a install directory...", "")
.and_then(|v| serde_json::to_value(v).ok());
return Some(RpcResponse::new_result(event.id, result));
}
_ => warn!("Unknown RPC method: {}", event.method),
}
None
})
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => info!("Webview started"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

View file

@ -7,16 +7,14 @@ use reqwest::header::CONTENT_LENGTH;
use std::io::Read; use std::io::Read;
use std::time::Duration; use std::time::Duration;
use reqwest::r#async::Client as AsyncClient;
use reqwest::Client; use reqwest::Client;
use reqwest::StatusCode;
/// Asserts that a URL is valid HTTPS, else returns an error. /// Asserts that a URL is valid HTTPS, else returns an error.
pub fn assert_ssl(url: &str) -> Result<(), String> { pub fn assert_ssl(url: &str) -> Result<(), String> {
if url.starts_with("https://") { if url.starts_with("https://") {
Ok(()) Ok(())
} else { } else {
Err("Specified URL was not https".to_string()) Err(format!("Specified URL was not https"))
} }
} }
@ -28,49 +26,32 @@ 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))
} }
/// Builds a customised async HTTP client. /// Downloads a text file from the specified URL.
pub fn build_async_client() -> Result<AsyncClient, String> { pub fn download_text(url: &str) -> Result<String, String> {
AsyncClient::builder() assert_ssl(url)?;
.timeout(Duration::from_secs(8))
.build() let mut client = build_client()?
.map_err(|x| format!("Unable to build client: {:?}", x)) .get(url)
.send()
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
client
.text()
.map_err(|v| format!("Failed to get text from resource: {:?}", v))
} }
/// Streams a file from a HTTP server. /// Streams a file from a HTTP server.
pub fn stream_file<F>( pub fn stream_file<F>(url: &str, mut callback: F) -> Result<(), String>
url: &str,
authorization: Option<String>,
mut callback: F,
) -> Result<(), String>
where where
F: FnMut(Vec<u8>, u64) -> (), F: FnMut(Vec<u8>, u64) -> (),
{ {
assert_ssl(url)?; assert_ssl(url)?;
let mut client = build_client()?.get(url); let mut client = build_client()?
.get(url)
if let Some(auth) = authorization {
client = client.header("Authorization", format!("Bearer {}", auth));
}
let mut client = client
.send() .send()
.map_err(|x| format!("Failed to GET resource: {:?}", x))?; .map_err(|x| format!("Failed to GET resource: {:?}", x))?;
match client.status() {
StatusCode::OK => {}
StatusCode::TOO_MANY_REQUESTS => {
return Err(
"Your token has exceeded the number of daily allowable IP addresses. \
Please wait 24 hours and try again."
.to_string(),
);
}
x => {
return Err(format!("Bad status code: {:?}.", x));
}
}
let size = match client.headers().get(CONTENT_LENGTH) { let size = match client.headers().get(CONTENT_LENGTH) {
Some(ref v) => v Some(ref v) => v
.to_str() .to_str()

View file

@ -18,32 +18,29 @@ 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 crate::config::BaseAttributes; use config::BaseAttributes;
use crate::config::Config; use config::Config;
use crate::sources::types::Version; use sources::types::Version;
use crate::tasks::install::InstallTask; use tasks::install::InstallTask;
use crate::tasks::uninstall::UninstallTask; use tasks::uninstall::UninstallTask;
use crate::tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask; use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
use crate::tasks::DependencyTree; use tasks::DependencyTree;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
use dirs::home_dir; use dirs::home_dir;
use std::collections::HashSet;
use std::fs::remove_file; use std::fs::remove_file;
use crate::http; use http;
use number_prefix::NumberPrefix::{self, Prefixed, Standalone}; use number_prefix::{decimal_prefix, Prefixed, Standalone};
use crate::native;
/// A message thrown during the installation of packages. /// A message thrown during the installation of packages.
#[derive(Serialize)] #[derive(Serialize)]
@ -51,25 +48,14 @@ pub enum InstallMessage {
Status(String, f64), Status(String, f64),
PackageInstalled, PackageInstalled,
Error(String), Error(String),
AuthorizationRequired(String),
EOF, EOF,
} }
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Credentials {
#[serde(default)]
pub username: String,
#[serde(default)]
pub token: String,
}
/// Metadata about the current installation itself. /// Metadata about the current installation itself.
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct InstallationDatabase { pub struct InstallationDatabase {
pub packages: Vec<LocalInstallation>, pub packages: Vec<LocalInstallation>,
pub shortcuts: Vec<String>, pub shortcuts: Vec<String>,
#[serde(default)]
pub credentials: Credentials,
} }
impl InstallationDatabase { impl InstallationDatabase {
@ -78,10 +64,6 @@ impl InstallationDatabase {
InstallationDatabase { InstallationDatabase {
packages: Vec::new(), packages: Vec::new(),
shortcuts: Vec::new(), shortcuts: Vec::new(),
credentials: Credentials {
username: String::new(),
token: String::new(),
},
} }
} }
} }
@ -98,7 +80,6 @@ pub struct InstallerFramework {
// If we just completed an uninstall, and we should clean up after ourselves. // If we just completed an uninstall, and we should clean up after ourselves.
pub burn_after_exit: bool, pub burn_after_exit: bool,
pub launcher_path: Option<String>, pub launcher_path: Option<String>,
pub is_windows: bool,
} }
/// Contains basic properties on the status of the session. Subset of InstallationFramework. /// Contains basic properties on the status of the session. Subset of InstallationFramework.
@ -119,25 +100,19 @@ pub struct LocalInstallation {
/// Relative paths to generated files /// Relative paths to generated files
pub files: Vec<String>, pub files: Vec<String>,
/// Absolute paths to generated shortcut files /// Absolute paths to generated shortcut files
pub shortcuts: HashSet<String>, pub shortcuts: Vec<String>,
} }
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::AuthorizationRequired(msg) => { &TaskMessage::PackageInstalled => {
if let Err(v) = $target.send(InstallMessage::AuthorizationRequired(msg.to_string()))
{
error!("Failed to submit queue message: {:?}", v);
}
}
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);
} }
@ -176,14 +151,11 @@ impl InstallerFramework {
/// items: Array of named packages to be installed/kept /// items: Array of named packages to be installed/kept
/// messages: Channel used to send progress messages /// messages: Channel used to send progress messages
/// fresh_install: If the install directory must be empty /// fresh_install: If the install directory must be empty
/// force_install: If the install directory should be erased first
pub fn install( pub fn install(
&mut self, &mut self,
items: Vec<String>, items: Vec<String>,
messages: &Sender<InstallMessage>, messages: &Sender<InstallMessage>,
fresh_install: bool, fresh_install: bool,
create_desktop_shortcuts: bool,
force_install: bool,
) -> Result<(), String> { ) -> Result<(), String> {
info!( info!(
"Framework: Installing {:?} to {:?}", "Framework: Installing {:?} to {:?}",
@ -212,8 +184,6 @@ impl InstallerFramework {
items, items,
uninstall_items, uninstall_items,
fresh_install, fresh_install,
create_desktop_shortcuts,
force_install,
}); });
let mut tree = DependencyTree::build(task); let mut tree = DependencyTree::build(task);
@ -280,7 +250,7 @@ impl InstallerFramework {
let mut downloaded = 0; let mut downloaded = 0;
let mut data_storage: Vec<u8> = Vec::new(); let mut data_storage: Vec<u8> = Vec::new();
http::stream_file(tool, None, |data, size| { http::stream_file(tool, |data, size| {
{ {
data_storage.extend_from_slice(&data); data_storage.extend_from_slice(&data);
} }
@ -294,11 +264,11 @@ impl InstallerFramework {
}; };
// Pretty print data volumes // Pretty print data volumes
let pretty_current = match NumberPrefix::decimal(downloaded as f64) { let pretty_current = match decimal_prefix(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 NumberPrefix::decimal(size as f64) { let pretty_total = match decimal_prefix(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),
}; };
@ -358,8 +328,7 @@ 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) {
@ -424,29 +393,6 @@ 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 {
@ -458,25 +404,6 @@ impl InstallerFramework {
is_launcher: false, is_launcher: false,
burn_after_exit: false, burn_after_exit: false,
launcher_path: None, launcher_path: None,
is_windows: cfg!(windows),
}
}
/// The special recovery mode for the Installer Framework.
pub fn new_recovery_mode(attrs: BaseAttributes, install_path: &Path) -> Self {
InstallerFramework {
base_attributes: BaseAttributes {
recovery: true,
..attrs
},
config: None,
database: InstallationDatabase::new(),
install_path: Some(install_path.to_path_buf()),
preexisting_install: true,
is_launcher: false,
burn_after_exit: false,
launcher_path: None,
is_windows: cfg!(windows),
} }
} }
@ -504,7 +431,6 @@ impl InstallerFramework {
is_launcher: false, is_launcher: false,
burn_after_exit: false, burn_after_exit: false,
launcher_path: None, launcher_path: None,
is_windows: cfg!(windows),
}) })
} }
} }

View file

@ -17,8 +17,7 @@ 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()?;

View file

@ -7,7 +7,10 @@
#![deny(unsafe_code)] #![deny(unsafe_code)]
#![deny(missing_docs)] #![deny(missing_docs)]
extern crate wry; #[cfg(windows)]
extern crate nfd;
extern crate web_view;
extern crate futures; extern crate futures;
extern crate hyper; extern crate hyper;
@ -27,7 +30,7 @@ extern crate semver;
extern crate dirs; extern crate dirs;
extern crate tar; extern crate tar;
extern crate xz2; extern crate xz_decom;
extern crate zip; extern crate zip;
extern crate fern; extern crate fern;
@ -37,44 +40,59 @@ extern crate log;
extern crate chrono; extern crate chrono;
extern crate clap; extern crate clap;
#[cfg(windows)]
extern crate widestring;
#[cfg(windows)]
extern crate winapi;
#[cfg(not(windows))]
extern crate slug;
#[cfg(not(windows))]
extern crate sysinfo; extern crate sysinfo;
extern crate jsonwebtoken as jwt;
extern crate base64;
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 self_update; mod rest;
mod sources; mod sources;
mod tasks; mod tasks;
use web_view::*;
use installer::InstallerFramework; use installer::InstallerFramework;
use logging::LoggingErrors; #[cfg(windows)]
use nfd::Response;
use rest::WebServer;
use std::net::TcpListener;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::sync::RwLock;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit;
use std::process::Command;
use std::{thread, time};
use std::fs::remove_file;
use std::fs::File;
use logging::LoggingErrors;
use clap::App; use clap::App;
use clap::Arg; use clap::Arg;
use log::Level;
use config::BaseAttributes; use config::BaseAttributes;
use std::fs;
use std::process::{exit, Command, Stdio};
const RAW_CONFIG: &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");
@ -82,7 +100,6 @@ fn main() {
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);
@ -95,8 +112,7 @@ 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")
@ -109,43 +125,109 @@ 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");
// Handle self-updating if needed // Check to see if we are currently in a self-update
self_update::perform_swap(&current_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;
}
self_update::cleanup(current_path);
// Load in metadata + setup the installer framework // Sleep a little bit to allow Windows to close the previous file handle
let mut fresh_install = false; 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(&current_exe, &to_path).map(|_x| ())
} else {
use std::fs::rename;
rename(&current_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");
}
}
}
}
Command::new(to_path)
.spawn()
.log_expect("Unable to start child process");
exit(0);
}
// If we just finished a update, we need to inject our previous command line arguments
let args_file = current_path.join("args.json");
if args_file.exists() {
let database: Vec<String> = {
let metadata_file =
File::open(&args_file).log_expect("Unable to open args file handle");
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
};
matches = reinterpret_app.get_matches_from(database);
info!("Parsed command line arguments from original instance");
remove_file(args_file).log_expect("Unable to clean up args file");
}
// Cleanup any remaining new maintenance tool instances if they exist
if cfg!(windows) {
let updater_executable = current_path.join("maintenancetool_new.exe");
if updater_executable.exists() {
// Sleep a little bit to allow Windows to close the previous file handle
thread::sleep(time::Duration::from_millis(3000));
// Attempt it a few times because Windows can hold a lock
for i in 1..=5 {
let swap_result = remove_file(&updater_executable);
match swap_result {
Ok(_) => break,
Err(e) => {
if i < 5 {
info!("Cleanup attempt failed: {:?}, retrying in 3 seconds.", e);
thread::sleep(time::Duration::from_millis(3000));
} else {
warn!("Deleting temp binary failed after 5 attempts: {:?}", e);
}
}
}
}
}
}
// Load in metadata as to learn about the environment
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);
InstallerFramework::new_with_db(config.clone(), current_path).unwrap_or_else(|e| { InstallerFramework::new_with_db(config, current_path).log_expect("Unable to parse metadata")
error!("Failed to load metadata: {:?}", e);
warn!("Entering recovery mode");
InstallerFramework::new_recovery_mode(config, current_path)
})
} else { } else {
info!("Starting fresh install"); info!("Starting fresh install");
fresh_install = true;
InstallerFramework::new(config) InstallerFramework::new(config)
}; };
// check for existing installs if we are running as a fresh install
let installed_path = PathBuf::from(framework.get_default_path().unwrap());
if fresh_install && installed_path.join("metadata.json").exists() {
info!("Existing install detected! Copying Trying to launch this install instead");
// Ignore the return value from this since it should exit the application if its successful
let _ = replace_existing_install(&current_exe, &installed_path);
}
let is_launcher = if let Some(string) = matches.value_of("launcher") { let is_launcher = if let Some(string) = matches.value_of("launcher") {
framework.is_launcher = true; framework.is_launcher = true;
framework.launcher_path = Some(string.to_string()); framework.launcher_path = Some(string.to_string());
@ -154,53 +236,97 @@ fn main() {
false false
}; };
// Start up the UI // Firstly, allocate us an epidermal port
frontend::launch(&app_name, is_launcher, framework); let target_port = {
} let listener = TcpListener::bind("127.0.0.1:0")
.log_expect("At least one local address should be free");
fn replace_existing_install(current_exe: &PathBuf, installed_path: &PathBuf) -> Result<(), String> { listener
// Generate installer path .local_addr()
let platform_extension = if cfg!(windows) { .log_expect("Should be able to pull address from listener")
"maintenancetool.exe" .port()
} else {
"maintenancetool"
}; };
let new_tool = if cfg!(windows) { // Now, iterate over all ports
"maintenancetool_new.exe" let addresses = "localhost:0"
} else { .to_socket_addrs()
"maintenancetool_new" .log_expect("No localhost address found");
};
if let Err(v) = fs::copy(current_exe, installed_path.join(new_tool)) { let mut servers = Vec::new();
return Err(format!("Unable to copy installer binary: {:?}", v)); 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 existing = installed_path let http_address = http_address.log_expect("No HTTP address found");
.join(platform_extension)
.into_os_string() let http_address = format!("http://localhost:{}", http_address.port());
.into_string();
let new = installed_path.join(new_tool).into_os_string().into_string(); // Init the web view
if existing.is_ok() && new.is_ok() { let size = if is_launcher { (600, 300) } else { (1024, 500) };
// Remove NTFS alternate stream which tells the operating system that the updater was downloaded from the internet
if cfg!(windows) { let resizable = false;
let _ = fs::remove_file( let debug = true;
installed_path.join("maintenancetool_new.exe:Zone.Identifier:$DATA"),
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);
}
}
},
(),
); );
}
info!("Launching {:?}", existing);
let success = Command::new(new.unwrap())
.arg("--swap")
.arg(existing.unwrap())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
if success.is_ok() {
exit(0);
} else {
error!("Unable to start existing yuzu maintenance tool. Launching old one instead");
}
}
Ok(())
} }

View file

@ -2,107 +2,76 @@
* 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 wchar_t *shortcutPath, const char *shortcutPath,
const wchar_t *description, const char *description,
const wchar_t *path, const char *path,
const wchar_t *args, const char *args,
const wchar_t *workingDir, const char *workingDir) {
const wchar_t *exePath) 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(shortcutPath, 0); h = persistFile->Load(wName, 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);
// default to using the first icon in the exe (usually correct) if (args!=NULL)
if (exePath != NULL)
shellLink->SetIconLocation(exePath, 0);
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(shortcutPath, TRUE); h = persistFile->Save(wName, TRUE);
if (FAILED(h)) if (FAILED(h)) {
{
errStr = "Failed to save shortcut"; errStr = "Failed to save shortcut";
goto err; goto err;
} }
// Notify that a new shortcut was created using the shell api
SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, shortcutPath, NULL);
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH, shortcutPath, NULL);
persistFile->Release(); persistFile->Release();
shellLink->Release(); shellLink->Release();
CoUninitialize(); CoUninitialize();
@ -118,65 +87,3 @@ 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;
}
extern "C" HRESULT getDesktopFolder(wchar_t *out_path)
{
PWSTR path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &path);
if (result == S_OK)
{
wcscpy_s(out_path, MAX_PATH + 1, path);
CoTaskMemFree(path);
}
return result;
}

View file

@ -1,95 +1,26 @@
//! Natives/platform specific interactions. //! Natives/platform specific interactions.
/// Basic definition of some running process.
#[derive(Debug)]
pub struct Process {
pub pid: usize,
pub name: String,
}
#[cfg(windows)] #[cfg(windows)]
mod natives { mod natives {
#![allow(non_upper_case_globals)] #![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)] #![allow(non_camel_case_types)]
#![allow(non_snake_case)] #![allow(non_snake_case)]
const PROCESS_LEN: usize = 10192; use std::ffi::CString;
const WV2_INSTALLER_DATA: &[u8] = include_bytes!("../../MicrosoftEdgeWebview2Setup.exe");
use crate::logging::LoggingErrors; use logging::LoggingErrors;
use std::env; use std::env;
use std::io::Write;
use std::os::windows::ffi::OsStrExt;
use std::path::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::shellapi::ShellExecuteW;
use winapi::um::winnt::{
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
};
use winapi::um::winuser::SW_SHOWDEFAULT;
use std::process::Command; use std::process::Command;
use tempfile::Builder;
use tinyfiledialogs::{message_box_yes_no, MessageBoxIcon, YesNo};
use webview2::EnvironmentBuilder;
use widestring::U16CString;
extern "C" { extern "C" {
pub fn saveShortcut( pub fn saveShortcut(
shortcutPath: *const winapi::ctypes::wchar_t, shortcutPath: *const ::std::os::raw::c_char,
description: *const winapi::ctypes::wchar_t, description: *const ::std::os::raw::c_char,
path: *const winapi::ctypes::wchar_t, path: *const ::std::os::raw::c_char,
args: *const winapi::ctypes::wchar_t, args: *const ::std::os::raw::c_char,
workingDir: *const winapi::ctypes::wchar_t, workingDir: *const ::std::os::raw::c_char,
exePath: *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;
pub fn getDesktopFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
}
pub fn prepare_install_webview2(name: &str) -> Result<(), String> {
if EnvironmentBuilder::default()
.get_available_browser_version_string()
.is_ok()
{
return Ok(());
}
if message_box_yes_no(&format!("{} installer", name), &format!("{} installer now requires Webview2 runtime to function properly.\nDo you wish to install it now?", name), MessageBoxIcon::Question, YesNo::Yes) == YesNo::No {
std::process::exit(1);
}
let mut installer_file = Builder::new()
.suffix(".exe")
.tempfile()
.log_expect("Unable to open the webview2 installer file");
installer_file
.write_all(&WV2_INSTALLER_DATA)
.log_expect("Unable to write the webview2 installer file");
let path = installer_file.path().to_owned();
installer_file.keep().log_unwrap();
Command::new(&path)
.arg("/install")
.spawn()
.log_expect("Unable to run the webview2 installer")
.wait()
.log_unwrap();
Ok(())
} }
// Needed here for Windows interop // Needed here for Windows interop
@ -100,77 +31,24 @@ mod natives {
target: &str, target: &str,
args: &str, args: &str,
working_dir: &str, working_dir: &str,
exe_path: &str,
) -> Result<String, String> { ) -> Result<String, String> {
let source_file = format!( let source_file = format!(
"{}\\Microsoft\\Windows\\Start Menu\\Programs\\{}.lnk", "{}\\Microsoft\\Windows\\Start Menu\\Programs\\{}.lnk",
env::var("APPDATA").log_expect("APPDATA is bad, apparently"), env::var("APPDATA").log_expect("APPDATA is bad, apparently"),
name name
); );
create_shortcut_inner(
source_file,
name,
description,
target,
args,
working_dir,
exe_path,
)
}
// Needed here for Windows interop
#[allow(unsafe_code)]
pub fn create_desktop_shortcut(
name: &str,
description: &str,
target: &str,
args: &str,
working_dir: &str,
exe_path: &str,
) -> Result<String, String> {
let mut cmd_path = [0u16; MAX_PATH + 1];
let _result = unsafe { getDesktopFolder(cmd_path.as_mut_ptr()) };
let source_path = format!(
"{}\\{}.lnk",
String::from_utf16_lossy(&cmd_path[..count_u16(&cmd_path)]).as_str(),
name
);
create_shortcut_inner(
source_path,
name,
description,
target,
args,
working_dir,
exe_path,
)
}
// Needed here for Windows interop
#[allow(unsafe_code)]
fn create_shortcut_inner(
source_file: String,
_name: &str,
description: &str,
target: &str,
args: &str,
working_dir: &str,
exe_path: &str,
) -> Result<String, String> {
info!("Generating shortcut @ {:?}", source_file); info!("Generating shortcut @ {:?}", source_file);
let native_target_dir = U16CString::from_str(source_file.clone()) let native_target_dir = CString::new(source_file.clone())
.log_expect("Error while converting to wchar_t"); .log_expect("Error while converting to C-style string");
let native_description = let native_description =
U16CString::from_str(description).log_expect("Error while converting to wchar_t"); CString::new(description).log_expect("Error while converting to C-style string");
let native_target = let native_target =
U16CString::from_str(target).log_expect("Error while converting to wchar_t"); CString::new(target).log_expect("Error while converting to C-style string");
let native_args = let native_args = CString::new(args).log_expect("Error while converting to C-style string");
U16CString::from_str(args).log_expect("Error while converting to wchar_t");
let native_working_dir = let native_working_dir =
U16CString::from_str(working_dir).log_expect("Error while converting to wchar_t"); CString::new(working_dir).log_expect("Error while converting to C-style string");
let native_exe_path =
U16CString::from_str(exe_path).log_expect("Error while converting to wchar_t");
let shortcutResult = unsafe { let shortcutResult = unsafe {
saveShortcut( saveShortcut(
@ -179,7 +57,6 @@ mod natives {
native_target.as_ptr(), native_target.as_ptr(),
native_args.as_ptr(), native_args.as_ptr(),
native_working_dir.as_ptr(), native_working_dir.as_ptr(),
native_exe_path.as_ptr(),
) )
}; };
@ -192,37 +69,6 @@ mod natives {
} }
} }
// Needed to call unsafe function `ShellExecuteW` from `winapi` crate
#[allow(unsafe_code)]
pub fn open_in_shell(path: &Path) {
let native_verb = U16CString::from_str("open").unwrap();
// https://doc.rust-lang.org/std/os/windows/ffi/trait.OsStrExt.html#tymethod.encode_wide
let mut native_path: Vec<u16> = path.as_os_str().encode_wide().collect();
native_path.push(0); // NULL terminator
unsafe {
ShellExecuteW(
std::ptr::null_mut(),
native_verb.as_ptr(),
native_path.as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
SW_SHOWDEFAULT,
);
}
}
#[inline]
fn count_u16(u16str: &[u16]) -> usize {
let mut pos = 0;
for x in u16str.iter() {
if *x == 0 {
break;
}
pos += 1;
}
pos
}
/// Cleans up the installer /// Cleans up the installer
pub fn burn_on_exit(app_name: &str) { pub fn burn_on_exit(app_name: &str) {
let current_exe = env::current_exe().log_expect("Current executable could not be found"); let current_exe = env::current_exe().log_expect("Current executable could not be found");
@ -236,7 +82,6 @@ mod natives {
.to_str() .to_str()
.log_expect("Unable to convert tool path to string") .log_expect("Unable to convert tool path to string")
.replace(" ", "\\ "); .replace(" ", "\\ ");
let tool_wv = format!("{}.WebView2", tool);
let log = path.join(format!("{}_installer.log", app_name)); let log = path.join(format!("{}_installer.log", app_name));
let log = log let log = log
@ -244,271 +89,55 @@ mod natives {
.log_expect("Unable to convert log path to string") .log_expect("Unable to convert log path to string")
.replace(" ", "\\ "); .replace(" ", "\\ ");
let install_path = path let target_arguments = format!("ping 127.0.0.1 -n 3 > nul && del {} {}", tool, log);
.to_str()
.log_expect("Unable to convert path to string")
.replace(" ", "\\ ");
let target_arguments = format!(
"/C choice /C Y /N /D Y /T 2 & del {} {} & rmdir /Q /S {} & rmdir {}",
tool, log, tool_wv, install_path
);
info!("Launching cmd with {:?}", target_arguments); info!("Launching cmd with {:?}", target_arguments);
// Needs to use `spawnDetached` which is an unsafe C/C++ function from interop.cpp Command::new("C:\\Windows\\system32\\cmd.exe")
#[allow(unsafe_code)] .arg("/C")
let spawn_result: i32 = unsafe { .arg(&target_arguments)
let mut cmd_path = [0u16; MAX_PATH + 1]; .spawn()
let result = getSystemFolder(cmd_path.as_mut_ptr()); .log_expect("Unable to start child process");
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)]
fn get_process_handler(pid: u32) -> Option<HANDLE> {
if pid == 0 {
return None;
}
let options = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE;
let process_handler = unsafe { OpenProcess(options, FALSE, pid as DWORD) };
if process_handler.is_null() {
let options = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
let process_handler = unsafe { OpenProcess(options, FALSE, pid as DWORD) };
if process_handler.is_null() {
None
} else {
Some(process_handler)
}
} else {
Some(process_handler)
}
}
/// Returns a list of running processes
#[allow(unsafe_code)]
pub fn get_process_names() -> Vec<super::Process> {
// Port from https://github.com/GuillaumeGomez/sysinfo/blob/master/src/windows/system.rs
// I think that 10192 as length will be enough to get all processes at once...
let mut process_ids = [0 as DWORD; PROCESS_LEN];
let mut cb_needed = 0;
let size = ::std::mem::size_of::<DWORD>() * process_ids.len();
unsafe {
if K32EnumProcesses(process_ids.as_mut_ptr(), size as DWORD, &mut cb_needed) == 0 {
return vec![];
}
}
let nb_processes = cb_needed / ::std::mem::size_of::<DWORD>() as DWORD;
let mut processes = Vec::new();
for i in 0..nb_processes {
let pid = process_ids[i as usize];
unsafe {
if let Some(process_handler) = get_process_handler(pid) {
let mut h_mod = ::std::ptr::null_mut();
let mut process_name = [0u16; MAX_PATH + 1];
let mut cb_needed = 0;
if EnumProcessModulesEx(
process_handler,
&mut h_mod,
::std::mem::size_of::<DWORD>() as DWORD,
&mut cb_needed,
LIST_MODULES_ALL,
) != 0
{
GetModuleFileNameExW(
process_handler,
h_mod,
process_name.as_mut_ptr(),
MAX_PATH as DWORD + 1,
);
let mut pos = 0;
for x in process_name.iter() {
if *x == 0 {
break;
}
pos += 1;
}
let name = String::from_utf16_lossy(&process_name[..pos]);
processes.push(super::Process {
pid: pid as _,
name,
});
}
}
}
}
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))]
mod natives { mod natives {
use std::fs::{remove_dir, remove_file}; use std::fs::remove_file;
use std::env; use std::env;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
use sysinfo::{PidExt, ProcessExt, SystemExt};
use dirs;
use slug::slugify;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::Path;
use std::process::Command;
#[cfg(target_os = "linux")]
pub fn create_shortcut( pub fn create_shortcut(
name: &str, name: &str,
description: &str, description: &str,
target: &str, target: &str,
args: &str, args: &str,
working_dir: &str, working_dir: &str,
exe_path: &str,
) -> Result<String, String> { ) -> Result<String, String> {
// FIXME: no icon will be shown since no icon is provided // TODO: no-op
let data_local_dir = dirs::data_local_dir(); warn!("create_shortcut is stubbed!");
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!("yuzu-maintenance-tool_{}.desktop", slugify(name))); // file name
let desktop_file = format!(
"[Desktop Entry]\nType=Application\nName={}\nExec=\"{}\" {}\nComment={}\nPath={}\nIcon=yuzu\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 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,
_exe_path: &str,
) -> Result<String, String> {
warn!("STUB! Creating shortcut is not implemented on macOS");
Ok("".to_string()) Ok("".to_string())
} }
pub fn open_in_shell(path: &Path) {
let shell: &str;
if cfg!(target_os = "linux") {
shell = "xdg-open";
} else if cfg!(target_os = "macos") {
shell = "open";
} else {
warn!("Unsupported platform");
return;
}
Command::new(shell).arg(path).spawn().ok();
}
/// Cleans up the installer /// Cleans up the installer
pub fn burn_on_exit(app_name: &str) { pub fn burn_on_exit(app_name: &str) {
let current_exe = env::current_exe().log_expect("Current executable could not be found"); let current_exe = env::current_exe().log_expect("Current executable could not be found");
let exe_dir = current_exe
.parent()
.log_expect("Current executable directory cannot be found");
if let Err(e) = remove_file(exe_dir.join(format!("{}_installer.log", app_name))) {
// No regular logging now.
eprintln!("Failed to delete maintenance log: {:?}", e);
};
// Thank god for *nix platforms // Thank god for *nix platforms
if let Err(e) = remove_file(&current_exe) { if let Err(e) = remove_file(&current_exe) {
// No regular logging now. // No regular logging now.
eprintln!("Failed to delete maintenancetool: {:?}", e); eprintln!("Failed to delete maintenancetool: {:?}", e);
}; };
// delete the directory if not empty and ignore errors (since we can't handle errors anymore)
remove_dir(exe_dir).ok();
}
/// Returns a list of running processes let current_dir = env::current_dir().log_expect("Current directory cannot be found");
pub fn get_process_names() -> Vec<super::Process> {
// a platform-independent implementation using sysinfo crate
let mut processes: Vec<super::Process> = Vec::new();
let mut system = sysinfo::System::new();
system.refresh_all();
for (pid, procs) in system.processes() {
processes.push(super::Process {
pid: pid.as_u32() as usize,
name: procs.name().to_string(),
});
}
processes // return running processes
}
/// Returns if dark mode is active on this system. if let Err(e) = remove_file(current_dir.join(format!("{}_installer.log", app_name))) {
pub fn is_dark_mode_active() -> bool { // No regular logging now.
// No-op eprintln!("Failed to delete installer log: {:?}", e);
false };
} }
} }

473
src/rest.rs Normal file
View file

@ -0,0 +1,473 @@
//! rest.rs
//!
//! Provides a HTTP/REST server for both frontend<->backend communication, as well
//! as talking to external applications.
use serde_json;
use futures::future;
use futures::Future;
use futures::Sink;
use futures::Stream;
use hyper::header::{ContentLength, ContentType};
use hyper::server::{Http, Request, Response, Service};
use hyper::{self, Error as HyperError, Get, Post, StatusCode};
use url::form_urlencoded;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::process::exit;
use std::process::Command;
use std::process::Stdio;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::sync::RwLock;
use std::thread::{self, JoinHandle};
use assets;
use installer::InstallMessage;
use installer::InstallerFramework;
use logging::LoggingErrors;
use http;
use config::Config;
use native;
#[derive(Serialize)]
struct FileSelection {
path: Option<String>,
}
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
/// application.
pub struct WebServer {
_handle: JoinHandle<()>,
}
impl WebServer {
/// Creates a new web server with the specified address.
pub fn with_addr(
framework: Arc<RwLock<InstallerFramework>>,
addr: SocketAddr,
) -> Result<Self, HyperError> {
let handle = thread::spawn(move || {
let server = Http::new()
.bind(&addr, move || {
Ok(WebService {
framework: framework.clone(),
})
}).log_expect("Failed to bind to port");
server.run().log_expect("Failed to run HTTP server");
});
Ok(WebServer { _handle: handle })
}
}
/// Holds internal state for Hyper
struct WebService {
framework: Arc<RwLock<InstallerFramework>>,
}
impl Service for WebService {
type Request = Request;
type Response = Response;
type Error = hyper::Error;
type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
/// HTTP request handler
fn call(&self, req: Self::Request) -> Self::Future {
Box::new(future::ok(match (req.method(), req.path()) {
// This endpoint should be usable directly from a <script> tag during loading.
(&Get, "/api/attrs") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let file = encapsulate_json(
"base_attributes",
&framework
.base_attributes
.to_json_str()
.log_expect("Failed to render JSON representation of config"),
);
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Returns the web config loaded
(&Get, "/api/config") => {
let mut framework = self
.framework
.write()
.log_expect("InstallerFramework has been dirtied");
info!(
"Downloading configuration from {:?}...",
framework.base_attributes.target_url
);
match http::download_text(&framework.base_attributes.target_url)
.map(|x| Config::from_toml_str(&x))
{
Ok(Ok(config)) => {
framework.config = Some(config.clone());
info!("Configuration file downloaded successfully.");
let file = framework
.get_config()
.log_expect("Config should be loaded by now")
.to_json_str()
.log_expect("Failed to render JSON representation of config");
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
Ok(Err(v)) => {
error!("Bad configuration file: {:?}", v);
Response::<hyper::Body>::new()
.with_status(StatusCode::ServiceUnavailable)
.with_header(ContentType::plaintext())
.with_body("Bad HTTP response")
}
Err(v) => {
error!(
"General connectivity error while downloading config: {:?}",
v
);
Response::<hyper::Body>::new()
.with_status(StatusCode::ServiceUnavailable)
.with_header(ContentLength(v.len() as u64))
.with_header(ContentType::plaintext())
.with_body(v)
}
}
}
// This endpoint should be usable directly from a <script> tag during loading.
(&Get, "/api/packages") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let file = encapsulate_json(
"packages",
&serde_json::to_string(&framework.database)
.log_expect("Failed to render JSON representation of database"),
);
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Returns the default path for a installation
(&Get, "/api/default-path") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let path = framework.get_default_path();
let response = FileSelection { path };
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of default path object");
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Immediately exits the application
(&Get, "/api/exit") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
if let Some(ref v) = framework.launcher_path {
Command::new(v)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.log_expect("Unable to start child process");
}
if framework.burn_after_exit {
native::burn_on_exit(&framework.base_attributes.name);
}
exit(0);
}
// Gets properties such as if the application is in maintenance mode
(&Get, "/api/installation-status") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let response = framework.get_installation_status();
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of installation status object");
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Streams the installation of a particular set of packages
(&Post, "/api/uninstall") => {
// We need to bit of pipelining to get this to work
let framework = self.framework.clone();
return Box::new(req.body().concat2().map(move |_b| {
let (sender, receiver) = channel();
let (tx, rx) = hyper::Body::pair();
// Startup a thread to do this operation for us
thread::spawn(move || {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
if let Err(v) = framework.uninstall(&sender) {
error!("Uninstall error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send uninstall error: {:?}", v);
};
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
});
// Spawn a thread for transforming messages to chunk messages
thread::spawn(move || {
let mut tx = tx;
loop {
let response = receiver
.recv()
.log_expect("Failed to receive message from runner thread");
if let InstallMessage::EOF = response {
break;
}
let mut response = serde_json::to_string(&response)
.log_expect("Failed to render JSON logging response payload");
response.push('\n');
tx = tx
.send(Ok(response.into_bytes().into()))
.wait()
.log_expect("Failed to write JSON response payload to client");
}
});
Response::<hyper::Body>::new()
//.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::plaintext())
.with_body(rx)
}));
}
// Updates the installer
(&Post, "/api/update-updater") => {
// We need to bit of pipelining to get this to work
let framework = self.framework.clone();
return Box::new(req.body().concat2().map(move |_b| {
let (sender, receiver) = channel();
let (tx, rx) = hyper::Body::pair();
// Startup a thread to do this operation for us
thread::spawn(move || {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
if let Err(v) = framework.update_updater(&sender) {
error!("Self-update error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send self-update error: {:?}", v);
};
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
});
// Spawn a thread for transforming messages to chunk messages
thread::spawn(move || {
let mut tx = tx;
loop {
let response = receiver
.recv()
.log_expect("Failed to receive message from runner thread");
if let InstallMessage::EOF = response {
break;
}
let mut response = serde_json::to_string(&response)
.log_expect("Failed to render JSON logging response payload");
response.push('\n');
tx = tx
.send(Ok(response.into_bytes().into()))
.wait()
.log_expect("Failed to write JSON response payload to client");
}
});
Response::<hyper::Body>::new()
//.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::plaintext())
.with_body(rx)
}));
}
// Streams the installation of a particular set of packages
(&Post, "/api/start-install") => {
// We need to bit of pipelining to get this to work
let framework = self.framework.clone();
return Box::new(req.body().concat2().map(move |b| {
let results = form_urlencoded::parse(b.as_ref())
.into_owned()
.collect::<HashMap<String, String>>();
let mut to_install = Vec::new();
let mut path: Option<String> = None;
// Transform results into just an array of stuff to install
for (key, value) in &results {
if key == "path" {
path = Some(value.to_owned());
continue;
}
if value == "true" {
to_install.push(key.to_owned());
}
}
// The frontend always provides this
let path = path.log_expect(
"No path specified by frontend when one should have already existed",
);
let (sender, receiver) = channel();
let (tx, rx) = hyper::Body::pair();
// Startup a thread to do this operation for us
thread::spawn(move || {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
let new_install = !framework.preexisting_install;
if new_install {
framework.set_install_dir(&path);
}
if let Err(v) = framework.install(to_install, &sender, new_install) {
error!("Install error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send install error: {:?}", v);
}
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
});
// Spawn a thread for transforming messages to chunk messages
thread::spawn(move || {
let mut tx = tx;
loop {
let mut panic_after_finish = false;
let response = match receiver
.recv() {
Ok(v) => v,
Err(v) => {
error!("Queue message failed: {:?}", v);
panic_after_finish = true;
InstallMessage::Error("Internal error".to_string())
}
};
if let InstallMessage::EOF = response {
break;
}
let mut response = serde_json::to_string(&response)
.log_expect("Failed to render JSON logging response payload");
response.push('\n');
tx = tx
.send(Ok(response.into_bytes().into()))
.wait()
.log_expect("Failed to write JSON response payload to client");
if panic_after_finish {
panic!("Failed to read from queue (flushed error message successfully)");
}
}
});
Response::<hyper::Body>::new()
//.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::plaintext())
.with_body(rx)
}));
}
// Static file handler
(&Get, _) => {
// At this point, we have a web browser client. Search for a index page
// if needed
let mut path: String = req.path().to_owned();
if path.ends_with('/') {
path += "index.html";
}
match assets::file_from_string(&path) {
Some((content_type, file)) => {
let content_type = ContentType(content_type.parse().log_expect(
"Failed to parse content type into correct representation",
));
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(content_type)
.with_body(file)
}
None => Response::new().with_status(StatusCode::NotFound),
}
}
// Fallthrough for POST/PUT/CONNECT/...
_ => Response::new().with_status(StatusCode::NotFound),
}))
}
}
/// Encapsulates JSON as a injectable Javascript script.
fn encapsulate_json(field_name: &str, json: &str) -> String {
format!("var {} = {};", field_name, json)
}

View file

@ -1,111 +0,0 @@
//! 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 crate::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(&current_exe, &to_path).map(|_x| ())
} else {
use std::fs::rename;
rename(&current_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);
}
}
}
}
}
}
}

View file

@ -7,9 +7,9 @@ use reqwest::StatusCode;
use serde_json; use serde_json;
use crate::sources::types::*; use sources::types::*;
use crate::http::build_client; use http::build_client;
pub struct GithubReleases {} pub struct GithubReleases {}
@ -41,19 +41,17 @@ 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( return Err(format!(
"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()));
@ -89,25 +87,20 @@ 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( return Err("JSON payload missing information about release name".to_string())
"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( return Err("JSON payload missing information about release URL".to_string())
"JSON payload missing information about release URL".to_string()
);
} }
}; };
files.push(File { files.push(File {
name: string.to_string(), name: string.to_string(),
url: url.to_string(), url: url.to_string(),
requires_authorization: false,
}); });
} }

View file

@ -6,15 +6,12 @@ pub mod types;
pub mod github; pub mod github;
pub mod patreon;
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<dyn ReleaseSource>> { pub fn get_by_name(name: &str) -> Option<Box<ReleaseSource>> {
match name { match name {
"github" => Some(Box::new(github::GithubReleases::new())), "github" => Some(Box::new(github::GithubReleases::new())),
"patreon" => Some(Box::new(patreon::PatreonReleases::new())),
_ => None, _ => None,
} }
} }

View file

@ -1,102 +0,0 @@
//! patreon.rs
//!
//! Contains the yuzu-emu core API implementation of a release source.
use crate::http::build_client;
use crate::sources::types::*;
use reqwest::header::USER_AGENT;
use reqwest::StatusCode;
pub struct PatreonReleases {}
/// The configuration for this release.
#[derive(Serialize, Deserialize)]
struct PatreonConfig {
repo: String,
}
impl PatreonReleases {
pub fn new() -> Self {
PatreonReleases {}
}
}
impl ReleaseSource for PatreonReleases {
fn get_current_releases(&self, _config: &TomlValue) -> Result<Vec<Release>, String> {
let config: PatreonConfig = match _config.clone().try_into() {
Ok(v) => v,
Err(v) => return Err(format!("Failed to parse release config: {:?}", v)),
};
let mut results: Vec<Release> = Vec::new();
// Build the HTTP client up
let client = build_client()?;
let mut response = client
.get(&format!(
"https://api.yuzu-emu.org/downloads/{}/",
config.repo
))
.header(USER_AGENT, "liftinstall (j-selby)")
.send()
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
match response.status() {
StatusCode::OK => {}
StatusCode::FORBIDDEN => {
return Err("You are not eligible to download this release".to_string());
}
_ => {
return Err(format!("Bad status code: {:?}.", response.status()));
}
}
let body = response
.text()
.map_err(|x| format!("Failed to decode HTTP response body: {:?}", x))?;
let result: serde_json::Value = serde_json::from_str(&body)
.map_err(|x| format!("Failed to parse response: {:?}", x))?;
// Parse JSON from server
let mut files = Vec::new();
let id: u64 = match result["version"].as_u64() {
Some(v) => v,
None => return Err("JSON payload missing information about ID".to_string()),
};
let downloads = match result["files"].as_array() {
Some(v) => v,
None => return Err("JSON payload not an array".to_string()),
};
for file in downloads.iter() {
let string = match file["name"].as_str() {
Some(v) => v,
None => {
return Err("JSON payload missing information about release name".to_string());
}
};
let url = match file["url"].as_str() {
Some(v) => v,
None => {
return Err("JSON payload missing information about release URL".to_string());
}
};
files.push(File {
name: string.to_string(),
url: url.to_string(),
requires_authorization: true,
});
}
results.push(Release {
version: Version::new_number(id),
files,
});
Ok(results)
}
}

View file

@ -23,7 +23,9 @@ impl Version {
fn coarse_into_semver(&self) -> SemverVersion { fn coarse_into_semver(&self) -> SemverVersion {
match *self { match *self {
Version::Semver(ref version) => version.to_owned(), Version::Semver(ref version) => version.to_owned(),
Version::Integer(ref version) => SemverVersion::new(version.to_owned(), 0u64, 0u64), Version::Integer(ref version) => {
SemverVersion::from((version.to_owned(), 0 as u64, 0 as u64))
}
} }
} }
@ -64,7 +66,6 @@ impl Ord for Version {
pub struct File { pub struct File {
pub name: String, pub name: String,
pub url: String, pub url: String,
pub requires_authorization: bool,
} }
impl File {} impl File {}

View file

@ -1,88 +0,0 @@
//! Validates that users have correct authorization to download packages.
use crate::frontend::rest::services::authentication;
use crate::installer::InstallerFramework;
use crate::logging::LoggingErrors;
use crate::tasks::resolver::ResolvePackageTask;
use crate::tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType};
pub struct CheckAuthorizationTask {
pub name: String,
}
impl Task for CheckAuthorizationTask {
fn execute(
&mut self,
mut input: Vec<TaskParamType>,
context: &mut InstallerFramework,
_messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1);
let params = input
.pop()
.log_expect("Check Authorization Task should have input from resolver!");
let (version, file) = match params {
TaskParamType::File(v, f) => Ok((v, f)),
_ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"),
}?;
if !file.requires_authorization {
return Ok(TaskParamType::Authentication(version, file, None));
}
let username = context.database.credentials.username.clone();
let token = context.database.credentials.token.clone();
let authentication = context
.config
.clone()
.log_expect("In-memory configuration doesn't exist")
.authentication
.log_expect("No authentication configuration exists while checking authorization");
let auth_url = authentication.auth_url.clone();
let pub_key_base64 = authentication.pub_key_base64.clone();
let validation = authentication.validation.clone();
// Authorizaion is required for this package so post the username and token and get a jwt_token response
let jwt_token = match authentication::authenticate_sync(auth_url, username, token) {
Ok(jwt) => jwt,
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
};
let claims =
match authentication::validate_token(jwt_token.clone(), pub_key_base64, validation) {
Ok(c) => c,
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
};
// Validate that they are authorized
if !claims.roles.contains(&"vip".to_string())
&& !claims.channels.contains(&"early-access".to_string())
{
return Ok(TaskParamType::Authentication(version, file, None));
}
Ok(TaskParamType::Authentication(
version,
file,
Some(jwt_token),
))
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![TaskDependency::build(
TaskOrdering::Pre,
Box::new(ResolvePackageTask {
name: self.name.clone(),
}),
)]
}
fn name(&self) -> String {
format!("CheckAuthorizationTask (for {:?})", self.name)
}
}

View file

@ -1,19 +1,20 @@
//! Downloads a package into memory. //! Downloads a package into memory.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::check_authorization::CheckAuthorizationTask; use tasks::Task;
use crate::tasks::Task; use tasks::TaskDependency;
use crate::tasks::TaskDependency; use tasks::TaskMessage;
use crate::tasks::TaskMessage; use tasks::TaskOrdering;
use crate::tasks::TaskOrdering; use tasks::TaskParamType;
use crate::tasks::TaskParamType;
use crate::http::stream_file; use tasks::resolver::ResolvePackageTask;
use number_prefix::NumberPrefix::{self, Prefixed, Standalone}; use http::stream_file;
use crate::logging::LoggingErrors; use number_prefix::{decimal_prefix, Prefixed, Standalone};
use logging::LoggingErrors;
pub struct DownloadPackageTask { pub struct DownloadPackageTask {
pub name: String, pub name: String,
@ -24,25 +25,16 @@ impl Task for DownloadPackageTask {
&mut self, &mut self,
mut input: Vec<TaskParamType>, mut input: Vec<TaskParamType>,
context: &mut InstallerFramework, context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1); assert_eq!(input.len(), 1);
let file = input let file = input.pop().log_expect("Should have input from resolver!");
.pop() let (version, file) = match file {
.log_expect("Download Package Task should have input from resolver!"); TaskParamType::File(v, f) => (v, f),
let (version, file, auth) = match file {
TaskParamType::Authentication(v, f, auth) => (v, f, auth),
_ => return Err("Unexpected param type to download package".to_string()), _ => return Err("Unexpected param type to download package".to_string()),
}; };
// TODO: move this back below checking for latest version after testing is done
if file.requires_authorization && auth.is_none() {
info!("Authorization required to update this package!");
messenger(&TaskMessage::AuthorizationRequired("AuthorizationRequired"));
return Ok(TaskParamType::Break);
}
// Check to see if this is the newest file available already // Check to see if this is the newest file available already
for element in &context.database.packages { for element in &context.database.packages {
if element.name == self.name { if element.name == self.name {
@ -62,7 +54,7 @@ impl Task for DownloadPackageTask {
let mut downloaded = 0; let mut downloaded = 0;
let mut data_storage: Vec<u8> = Vec::new(); let mut data_storage: Vec<u8> = Vec::new();
stream_file(&file.url, auth, |data, size| { stream_file(&file.url, |data, size| {
{ {
data_storage.extend_from_slice(&data); data_storage.extend_from_slice(&data);
} }
@ -76,11 +68,11 @@ impl Task for DownloadPackageTask {
}; };
// Pretty print data volumes // Pretty print data volumes
let pretty_current = match NumberPrefix::decimal(downloaded as f64) { let pretty_current = match decimal_prefix(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 NumberPrefix::decimal(size as f64) { let pretty_total = match decimal_prefix(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),
}; };
@ -100,7 +92,7 @@ impl Task for DownloadPackageTask {
fn dependencies(&self) -> Vec<TaskDependency> { fn dependencies(&self) -> Vec<TaskDependency> {
vec![TaskDependency::build( vec![TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Pre,
Box::new(CheckAuthorizationTask { Box::new(ResolvePackageTask {
name: self.name.clone(), name: self.name.clone(),
}), }),
)] )]

View file

@ -1,16 +1,16 @@
//! Verifies that this is the only running instance of the installer, and that no application is running. //! Verifies that this is the only running instance of the installer, and that no application is running.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::native::get_process_names; use sysinfo;
use crate::native::Process; use sysinfo::get_current_pid;
use sysinfo::ProcessExt;
use std::process; use sysinfo::SystemExt;
pub struct EnsureOnlyInstanceTask {} pub struct EnsureOnlyInstanceTask {}
@ -19,26 +19,27 @@ impl Task for EnsureOnlyInstanceTask {
&mut self, &mut self,
input: Vec<TaskParamType>, input: Vec<TaskParamType>,
context: &mut InstallerFramework, context: &mut InstallerFramework,
_messenger: &dyn Fn(&TaskMessage), _messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
let current_pid = process::id() as usize; let system = sysinfo::System::new();
for Process { pid, name } in get_process_names() { let current_pid = get_current_pid();
if pid == current_pid { for (pid, process) in system.get_process_list() {
if pid == &current_pid {
continue; continue;
} }
let exe = name; let exe = process.exe();
if exe.ends_with("maintenancetool.exe") || exe.ends_with("maintenancetool") { if exe.ends_with("maintenancetool.exe") || exe.ends_with("maintenancetool") {
return Err("Maintenance tool is already running!".to_string()); return Err(format!("Maintenance tool is already running!"));
} }
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("The installed application is currently running!".to_string()); return Err(format!("The installed application is currently running!"));
} }
} }
} }
@ -52,6 +53,6 @@ impl Task for EnsureOnlyInstanceTask {
} }
fn name(&self) -> String { fn name(&self) -> String {
"EnsureOnlyInstanceTask".to_string() format!("EnsureOnlyInstanceTask")
} }
} }

View file

@ -1,29 +1,24 @@
//! Overall hierarchy for installing a installation of the application. //! Overall hierarchy for installing a installation of the application.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::ensure_only_instance::EnsureOnlyInstanceTask; use tasks::ensure_only_instance::EnsureOnlyInstanceTask;
use crate::tasks::install_dir::VerifyInstallDirTask; use tasks::install_dir::VerifyInstallDirTask;
use crate::tasks::install_global_shortcut::InstallGlobalShortcutsTask; use tasks::install_global_shortcut::InstallGlobalShortcutsTask;
use crate::tasks::install_pkg::InstallPackageTask; use tasks::install_pkg::InstallPackageTask;
use crate::tasks::launch_installed_on_exit::LaunchOnExitTask; use tasks::save_executable::SaveExecutableTask;
use crate::tasks::remove_target_dir::RemoveTargetDirTask; use tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::save_executable::SaveExecutableTask;
use crate::tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskOrdering; use tasks::TaskOrdering;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
pub struct InstallTask { pub struct InstallTask {
pub items: Vec<String>, pub items: Vec<String>,
pub uninstall_items: Vec<String>, pub uninstall_items: Vec<String>,
pub fresh_install: bool, pub fresh_install: bool,
pub create_desktop_shortcuts: bool,
// force_install: remove the target directory before installing
pub force_install: bool,
} }
impl Task for InstallTask { impl Task for InstallTask {
@ -31,7 +26,7 @@ impl Task for InstallTask {
&mut self, &mut self,
_: Vec<TaskParamType>, _: Vec<TaskParamType>,
_: &mut InstallerFramework, _: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage), messenger: &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)
@ -45,13 +40,6 @@ impl Task for InstallTask {
Box::new(EnsureOnlyInstanceTask {}), Box::new(EnsureOnlyInstanceTask {}),
)); ));
if self.force_install {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(RemoveTargetDirTask {}),
));
}
elements.push(TaskDependency::build( elements.push(TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Pre,
Box::new(VerifyInstallDirTask { Box::new(VerifyInstallDirTask {
@ -59,6 +47,13 @@ impl Task for InstallTask {
}), }),
)); ));
for item in &self.items {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(InstallPackageTask { name: item.clone() }),
));
}
for item in &self.uninstall_items { for item in &self.uninstall_items {
elements.push(TaskDependency::build( elements.push(TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Pre,
@ -69,16 +64,6 @@ impl Task for InstallTask {
)); ));
} }
for item in &self.items {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(InstallPackageTask {
name: item.clone(),
create_desktop_shortcuts: self.create_desktop_shortcuts,
}),
));
}
if self.fresh_install { if self.fresh_install {
elements.push(TaskDependency::build( elements.push(TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Pre,
@ -89,11 +74,6 @@ impl Task for InstallTask {
TaskOrdering::Pre, TaskOrdering::Pre,
Box::new(InstallGlobalShortcutsTask {}), Box::new(InstallGlobalShortcutsTask {}),
)); ));
elements.push(TaskDependency::build(
TaskOrdering::Post,
Box::new(LaunchOnExitTask {}),
))
} }
elements elements

View file

@ -1,133 +0,0 @@
//! Generates shortcuts for a specified file.
use crate::installer::InstallerFramework;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use crate::config::PackageDescription;
use crate::logging::LoggingErrors;
#[cfg(windows)]
use crate::native::create_desktop_shortcut;
#[cfg(target_os = "linux")]
use crate::native::create_shortcut;
pub struct InstallDesktopShortcutTask {
pub name: String,
pub should_run: bool,
}
impl Task for InstallDesktopShortcutTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
if !self.should_run {
return Ok(TaskParamType::GeneratedShortcuts(Vec::new()));
}
messenger(&TaskMessage::DisplayMessage(
&format!(
"Generating desktop shortcuts for package {:?}...",
self.name
),
0.0,
));
let path = context
.install_path
.as_ref()
.log_expect("No install path specified");
let starting_dir = path
.to_str()
.log_expect("Unable to build shortcut metadata (startingdir)");
let mut installed_files = Vec::new();
let mut metadata: Option<PackageDescription> = None;
for description in &context
.config
.as_ref()
.log_expect("Should have packages by now")
.packages
{
if self.name == description.name {
metadata = Some(description.clone());
break;
}
}
let package = match metadata {
Some(v) => v,
None => return Err(format!("Package {:?} could not be found.", self.name)),
};
// Generate installer path
let platform_extension = if cfg!(windows) {
"maintenancetool.exe"
} else {
"maintenancetool"
};
for shortcut in package.shortcuts {
let tool_path = path.join(platform_extension);
let tool_path = tool_path
.to_str()
.log_expect("Unable to build shortcut metadata (tool)");
let exe_path = path.join(shortcut.relative_path);
let exe_path = exe_path
.to_str()
.log_expect("Unable to build shortcut metadata (exe)");
#[cfg(windows)]
installed_files.push(create_desktop_shortcut(
&shortcut.name,
&shortcut.description,
tool_path,
// TODO: Send by list
&format!("--launcher \"{}\"", exe_path),
&starting_dir,
exe_path,
)?);
#[cfg(target_os = "linux")]
installed_files.push(create_shortcut(
&shortcut.name,
&shortcut.description,
tool_path,
&format!("--launcher \"{}\"", exe_path),
&starting_dir,
exe_path,
)?);
}
// Update the installed packages shortcuts information in the database
let packages = &mut context.database.packages;
for pack in packages {
if pack.name == self.name {
pack.shortcuts.extend(installed_files.clone());
}
}
Ok(TaskParamType::GeneratedShortcuts(installed_files))
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
format!(
"InstallDesktopShortcutTask (for {:?}, should_run = {:?})",
self.name, self.should_run
)
}
}

View file

@ -1,16 +1,16 @@
//! Verifies properties about the installation directory. //! Verifies properties about the installation directory.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::fs::read_dir; use std::fs::read_dir;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
pub struct VerifyInstallDirTask { pub struct VerifyInstallDirTask {
pub clean_install: bool, pub clean_install: bool,
@ -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: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
messenger(&TaskMessage::DisplayMessage( messenger(&TaskMessage::DisplayMessage(

View file

@ -1,17 +1,17 @@
//! Generates the global shortcut for this application. //! Generates the global shortcut for this application.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
use crate::native::create_shortcut; use native::create_shortcut;
use crate::tasks::save_database::SaveDatabaseTask; use tasks::save_database::SaveDatabaseTask;
use crate::tasks::TaskOrdering; use tasks::TaskOrdering;
pub struct InstallGlobalShortcutsTask {} pub struct InstallGlobalShortcutsTask {}
@ -20,7 +20,7 @@ impl Task for InstallGlobalShortcutsTask {
&mut self, &mut self,
_: Vec<TaskParamType>, _: Vec<TaskParamType>,
context: &mut InstallerFramework, context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage( messenger(&TaskMessage::DisplayMessage(
"Generating global shortcut...", "Generating global shortcut...",
@ -58,10 +58,9 @@ impl Task for InstallGlobalShortcutsTask {
// TODO: Send by list // TODO: Send by list
"", "",
&starting_dir, &starting_dir,
"",
)?; )?;
if !shortcut_file.is_empty() && !context.database.shortcuts.contains(&shortcut_file) { if !shortcut_file.is_empty() {
context.database.shortcuts.push(shortcut_file); context.database.shortcuts.push(shortcut_file);
} }

View file

@ -1,35 +1,32 @@
//! Installs a specific package. //! Installs a specific package.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::download_pkg::DownloadPackageTask; use tasks::download_pkg::DownloadPackageTask;
use crate::tasks::install_shortcuts::InstallShortcutsTask; use tasks::install_shortcuts::InstallShortcutsTask;
use crate::tasks::save_database::SaveDatabaseTask; use tasks::save_database::SaveDatabaseTask;
use crate::tasks::uninstall_pkg::UninstallPackageTask; use tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskOrdering; use tasks::TaskOrdering;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::config::PackageDescription; use config::PackageDescription;
use crate::installer::LocalInstallation; use installer::LocalInstallation;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::io::copy; use std::io::copy;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
use crate::archives; use archives;
use crate::tasks::install_desktop_shortcut::InstallDesktopShortcutTask;
use std::collections::HashSet;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::path::Path; use std::path::Path;
pub struct InstallPackageTask { pub struct InstallPackageTask {
pub name: String, pub name: String,
pub create_desktop_shortcuts: bool,
} }
impl Task for InstallPackageTask { impl Task for InstallPackageTask {
@ -37,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: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage( messenger(&TaskMessage::DisplayMessage(
&format!("Installing package {:?}...", self.name), &format!("Installing package {:?}...", self.name),
@ -69,20 +66,20 @@ impl Task for InstallPackageTask {
None => return Err(format!("Package {:?} could not be found.", self.name)), None => return Err(format!("Package {:?} could not be found.", self.name)),
}; };
// Ignore input from the uninstaller - no useful information passed // Grab data from the shortcut generator
// If a previous task Breaks, then just early exit let shortcuts = input.pop().log_expect("Should have input from resolver!");
match input let shortcuts = match shortcuts {
.pop() TaskParamType::GeneratedShortcuts(files) => files,
.log_expect("Install Package Task should have guaranteed output!") // If the resolver returned early, we need to unwind
{
TaskParamType::Break => return Ok(TaskParamType::None), TaskParamType::Break => return Ok(TaskParamType::None),
_ => (), _ => return Err("Unexpected shortcuts param type to install package".to_string()),
}; };
// Ignore input from the uninstaller - no useful information passed
input.pop();
// Grab data from the resolver // Grab data from the resolver
let data = input let data = input.pop().log_expect("Should have input from resolver!");
.pop()
.log_expect("Install Package Task should have input from resolver!");
let (version, file, data) = match data { let (version, file, data) = match data {
TaskParamType::FileContents(version, file, data) => (version, file, data), TaskParamType::FileContents(version, file, data) => (version, file, data),
_ => return Err("Unexpected file contents param type to install package".to_string()), _ => return Err("Unexpected file contents param type to install package".to_string()),
@ -142,7 +139,7 @@ impl Task for InstallPackageTask {
info!("Creating file: {:?}", string_name); info!("Creating file: {:?}", string_name);
if !installed_files.contains(&string_name) { if !installed_files.contains(&string_name) {
installed_files.push(string_name); installed_files.push(string_name.to_string());
} }
let mut file_metadata = OpenOptions::new(); let mut file_metadata = OpenOptions::new();
@ -171,9 +168,9 @@ impl Task for InstallPackageTask {
// Save metadata about this package // Save metadata about this package
context.database.packages.push(LocalInstallation { context.database.packages.push(LocalInstallation {
name: package.name, name: package.name.to_owned(),
version, version,
shortcuts: HashSet::new(), shortcuts,
files: installed_files, files: installed_files,
}); });
@ -198,18 +195,11 @@ impl Task for InstallPackageTask {
}), }),
), ),
TaskDependency::build( TaskDependency::build(
TaskOrdering::Post, TaskOrdering::Pre,
Box::new(InstallShortcutsTask { Box::new(InstallShortcutsTask {
name: self.name.clone(), name: self.name.clone(),
}), }),
), ),
TaskDependency::build(
TaskOrdering::Post,
Box::new(InstallDesktopShortcutTask {
name: self.name.clone(),
should_run: self.create_desktop_shortcuts,
}),
),
TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})), TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})),
] ]
} }

View file

@ -1,17 +1,17 @@
//! Generates shortcuts for a specified file. //! Generates shortcuts for a specified file.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::config::PackageDescription; use config::PackageDescription;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
use crate::native::create_shortcut; use native::create_shortcut;
pub struct InstallShortcutsTask { pub struct InstallShortcutsTask {
pub name: String, pub name: String,
@ -22,7 +22,7 @@ impl Task for InstallShortcutsTask {
&mut self, &mut self,
_: Vec<TaskParamType>, _: Vec<TaskParamType>,
context: &mut InstallerFramework, context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage), messenger: &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),
@ -83,18 +83,9 @@ impl Task for InstallShortcutsTask {
// TODO: Send by list // TODO: Send by list
&format!("--launcher \"{}\"", exe_path), &format!("--launcher \"{}\"", exe_path),
&starting_dir, &starting_dir,
exe_path,
)?); )?);
} }
// Update the installed packages shortcuts information in the database
let packages = &mut context.database.packages;
for pack in packages {
if pack.name == self.name {
pack.shortcuts.extend(installed_files.clone());
}
}
Ok(TaskParamType::GeneratedShortcuts(installed_files)) Ok(TaskParamType::GeneratedShortcuts(installed_files))
} }

View file

@ -1,76 +0,0 @@
//! Configures lift to launch the new package on fresh install after its closed
//! If theres multiple launchable packages, then choose the first listed in config
//! If there are multiple shortcuts for the first package, then launch the first.
use crate::installer::InstallerFramework;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use crate::config::PackageDescription;
use crate::logging::LoggingErrors;
pub struct LaunchOnExitTask {}
impl Task for LaunchOnExitTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
_: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
let pkg = &context.database.packages.first();
if pkg.is_none() {
return Ok(TaskParamType::None);
}
let pkg = pkg.unwrap();
// look up the first shortcut for the first listed package in the database
let path = context
.install_path
.as_ref()
.log_expect("No install path specified");
let mut metadata: Option<PackageDescription> = None;
for description in &context
.config
.as_ref()
.log_expect("Should have packages by now")
.packages
{
if pkg.name == description.name {
metadata = Some(description.clone());
break;
}
}
let package_desc = match metadata {
Some(v) => v,
// Package metadata is missing. Dunno what went wrong but we can skip this then
None => return Ok(TaskParamType::None),
};
let shortcut = package_desc.shortcuts.first();
// copy the path to the actual exe into launcher_path so it'll load it on exit
context.launcher_path = shortcut.map(|s| {
path.join(s.relative_path.clone())
.to_str()
.map(|t| t.to_string())
.unwrap()
});
Ok(TaskParamType::None)
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
"LaunchOnExitTask".to_string()
}
}

View file

@ -4,22 +4,18 @@
use std::fmt; use std::fmt;
use std::fmt::Display; use std::fmt::Display;
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::sources::types::File; use sources::types::File;
use crate::sources::types::Version; use sources::types::Version;
pub mod check_authorization;
pub mod download_pkg; pub mod download_pkg;
pub mod ensure_only_instance; pub mod ensure_only_instance;
pub mod install; pub mod install;
pub mod install_desktop_shortcut;
pub mod install_dir; pub mod install_dir;
pub mod install_global_shortcut; pub mod install_global_shortcut;
pub mod install_pkg; pub mod install_pkg;
pub mod install_shortcuts; pub mod install_shortcuts;
pub mod launch_installed_on_exit;
pub mod remove_target_dir;
pub mod resolver; pub mod resolver;
pub mod save_database; pub mod save_database;
pub mod save_executable; pub mod save_executable;
@ -33,8 +29,6 @@ pub enum TaskParamType {
None, None,
/// Metadata about a file /// Metadata about a file
File(Version, File), File(Version, File),
/// Authentication token for a package
Authentication(Version, File, Option<String>),
/// Downloaded contents of a file /// Downloaded contents of a file
FileContents(Version, File, Vec<u8>), FileContents(Version, File, Vec<u8>),
/// List of shortcuts that have been generated /// List of shortcuts that have been generated
@ -55,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<dyn Task>, task: Box<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<dyn Task>) -> TaskDependency { pub fn build(ordering: TaskOrdering, task: Box<Task>) -> TaskDependency {
TaskDependency { ordering, task } TaskDependency { ordering, task }
} }
} }
@ -68,7 +62,6 @@ impl TaskDependency {
/// A message from a task. /// A message from a task.
pub enum TaskMessage<'a> { pub enum TaskMessage<'a> {
DisplayMessage(&'a str, f64), DisplayMessage(&'a str, f64),
AuthorizationRequired(&'a str),
PackageInstalled, PackageInstalled,
} }
@ -81,7 +74,7 @@ pub trait Task {
&mut self, &mut self,
input: Vec<TaskParamType>, input: Vec<TaskParamType>,
context: &mut InstallerFramework, context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage), messenger: &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
@ -94,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<dyn Task>, task: Box<Task>,
dependencies: Vec<(TaskOrdering, DependencyTree)>, dependencies: Vec<(TaskOrdering, DependencyTree)>,
} }
@ -127,7 +120,7 @@ impl DependencyTree {
pub fn execute( pub fn execute(
&mut self, &mut self,
context: &mut InstallerFramework, context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage), messenger: &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;
@ -140,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),
@ -166,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),
@ -186,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),
@ -213,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<dyn Task>) -> DependencyTree { pub fn build(task: Box<Task>) -> DependencyTree {
let dependencies = task let dependencies = task
.dependencies() .dependencies()
.into_iter() .into_iter()

View file

@ -1,64 +0,0 @@
//! remove the whole target directory from the existence
use crate::installer::InstallerFramework;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
pub struct RemoveTargetDirTask {}
impl Task for RemoveTargetDirTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage(
"Removing previous install...",
0.1,
));
// erase the database as well
context.database.packages = Vec::new();
if let Some(path) = context.install_path.as_ref() {
let entries = std::fs::read_dir(path)
.map_err(|e| format!("Error reading {}: {}", path.to_string_lossy(), e))?;
// remove everything under the path
if !context.preexisting_install {
std::fs::remove_dir_all(path)
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
return Ok(TaskParamType::None);
}
// remove everything except the maintenancetool if repairing
for entry in entries {
let path = entry
.map_err(|e| format!("Error reading file: {}", e))?
.path();
if let Some(filename) = path.file_name() {
if filename.to_string_lossy().starts_with("maintenancetool") {
continue;
}
}
if path.is_dir() {
std::fs::remove_dir_all(&path)
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
} else {
std::fs::remove_file(&path)
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
}
}
}
Ok(TaskParamType::None)
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
"RemoveTargetDirTask".to_string()
}
}

View file

@ -2,18 +2,18 @@
use std::env::consts::OS; use std::env::consts::OS;
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::config::PackageDescription; use config::PackageDescription;
use regex::Regex; use regex::Regex;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
pub struct ResolvePackageTask { pub struct ResolvePackageTask {
pub name: String, pub name: String,
@ -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: &dyn Fn(&TaskMessage), messenger: &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;

View file

@ -1,11 +1,11 @@
//! Saves the main database into the installation directory. //! Saves the main database into the installation directory.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
pub struct SaveDatabaseTask {} pub struct SaveDatabaseTask {}
@ -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: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
messenger(&TaskMessage::DisplayMessage( messenger(&TaskMessage::DisplayMessage(

View file

@ -1,11 +1,11 @@
//! Saves the installer executable into the install directory. //! Saves the installer executable into the install directory.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use std::fs::File; use std::fs::File;
use std::fs::OpenOptions; use std::fs::OpenOptions;
@ -14,7 +14,7 @@ use std::io::copy;
use std::env::current_exe; use std::env::current_exe;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
pub struct SaveExecutableTask {} pub struct SaveExecutableTask {}
@ -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: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
messenger(&TaskMessage::DisplayMessage( messenger(&TaskMessage::DisplayMessage(

View file

@ -1,14 +1,14 @@
//! Uninstalls a set of packages. //! Uninstalls a set of packages.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::tasks::uninstall_pkg::UninstallPackageTask; use tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskOrdering; use tasks::TaskOrdering;
pub struct UninstallTask { pub struct UninstallTask {
pub items: Vec<String>, pub items: Vec<String>,
@ -19,7 +19,7 @@ impl Task for UninstallTask {
&mut self, &mut self,
_: Vec<TaskParamType>, _: Vec<TaskParamType>,
_: &mut InstallerFramework, _: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage), messenger: &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)

View file

@ -1,15 +1,15 @@
//! Uninstalls a specific package. //! Uninstalls a specific package.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::TaskOrdering;
use std::fs::remove_file; use std::fs::remove_file;
use tasks::save_database::SaveDatabaseTask;
use tasks::TaskOrdering;
pub struct UninstallGlobalShortcutsTask {} pub struct UninstallGlobalShortcutsTask {}
@ -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: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);

View file

@ -1,21 +1,21 @@
//! Uninstalls a specific package. //! Uninstalls a specific package.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::save_database::SaveDatabaseTask; use tasks::save_database::SaveDatabaseTask;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskOrdering; use tasks::TaskOrdering;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::installer::LocalInstallation; use installer::LocalInstallation;
use std::fs::remove_dir; use std::fs::remove_dir;
use std::fs::remove_file; use std::fs::remove_file;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
use crate::tasks::uninstall_shortcuts::UninstallShortcutsTask; use tasks::uninstall_shortcuts::UninstallShortcutsTask;
pub struct UninstallPackageTask { pub struct UninstallPackageTask {
pub name: String, pub name: String,
@ -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: &dyn Fn(&TaskMessage), messenger: &Fn(&TaskMessage),
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1); assert_eq!(input.len(), 1);
@ -44,7 +44,7 @@ impl Task for UninstallPackageTask {
} }
} }
let package = match metadata { let mut package = match metadata {
Some(v) => v, Some(v) => v,
None => { None => {
if self.optional { if self.optional {
@ -63,7 +63,8 @@ impl Task for UninstallPackageTask {
0.0, 0.0,
)); ));
let mut directories = Vec::new(); // Reverse, as to delete directories last
package.files.reverse();
let max = package.files.len(); let max = package.files.len();
for (i, file) in package.files.iter().enumerate() { for (i, file) in package.files.iter().enumerate() {
@ -77,9 +78,7 @@ impl Task for UninstallPackageTask {
)); ));
let result = if file.is_dir() { let result = if file.is_dir() {
// we don't delete directory just yet remove_dir(file)
directories.push(file);
Ok(())
} else { } else {
remove_file(file) remove_file(file)
}; };
@ -89,17 +88,6 @@ impl Task for UninstallPackageTask {
} }
} }
// sort directories by reverse depth order
directories.sort_by(|a, b| {
let depth_a = a.components().fold(0usize, |acc, _| acc + 1);
let depth_b = b.components().fold(0usize, |acc, _| acc + 1);
depth_b.cmp(&depth_a)
});
for i in directories.iter() {
info!("Deleting directory: {:?}", i);
remove_dir(i).ok();
}
Ok(TaskParamType::None) Ok(TaskParamType::None)
} }

View file

@ -1,18 +1,18 @@
//! Uninstalls a specific package. //! Uninstalls a specific package.
use crate::installer::InstallerFramework; use installer::InstallerFramework;
use crate::tasks::Task; use tasks::Task;
use crate::tasks::TaskDependency; use tasks::TaskDependency;
use crate::tasks::TaskMessage; use tasks::TaskMessage;
use crate::tasks::TaskParamType; use tasks::TaskParamType;
use crate::installer::LocalInstallation; use installer::LocalInstallation;
use std::fs::remove_dir; use std::fs::remove_dir;
use std::fs::remove_file; use std::fs::remove_file;
use crate::logging::LoggingErrors; use logging::LoggingErrors;
pub struct UninstallShortcutsTask { pub struct UninstallShortcutsTask {
pub name: String, pub name: String,
@ -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: &dyn Fn(&TaskMessage), messenger: &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 Normal file

File diff suppressed because one or more lines are too long

88
static/css/main.css Normal file
View file

@ -0,0 +1,88 @@
/* 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;
}

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/how-to-open.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

67
static/index.html Normal file
View file

@ -0,0 +1,67 @@
<!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>

144
static/js/helpers.js Normal file
View 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.
*/
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);
}

68
static/js/main.js Normal file
View file

@ -0,0 +1,68 @@
// 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");

460
static/js/views.js Normal file
View file

@ -0,0 +1,460 @@
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 Normal file

File diff suppressed because one or more lines are too long

6
static/js/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -1 +0,0 @@
> 1%

View file

@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View file

@ -1,16 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/recommended',
'@vue/standard'
],
rules: {
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-redeclare': 'off',
camelcase: 'off'
}
}

21
ui/.gitignore vendored
View file

@ -1,21 +0,0 @@
.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*

View file

@ -1,29 +0,0 @@
# 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/).

View file

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/app'
]
}

Some files were not shown because too many files have changed in this diff Show more