A bunch of stuff

This commit is contained in:
Michael Pfaff 2022-11-08 08:40:01 -05:00
parent 4ad392d83a
commit e7a3ce4976
Signed by: michael
GPG Key ID: CF402C4A012AA9D4
4 changed files with 770 additions and 166 deletions

201
Cargo.lock generated
View File

@ -32,6 +32,18 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "async-scoped"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1181d2a07303ac3e8df0b3bdaaf648b4ac968d352e61158f5c1897db70d22a09"
dependencies = [
"futures",
"pin-project",
"slab",
"tokio",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -204,6 +216,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.21"
@ -211,6 +238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -219,6 +247,34 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.21"
@ -237,10 +293,16 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@ -281,9 +343,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.11.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
@ -387,9 +449,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "1.8.2"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown",
@ -542,7 +604,7 @@ dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
@ -563,6 +625,16 @@ dependencies = [
"tempfile",
]
[[package]]
name = "num_cpus"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "object"
version = "0.28.4"
@ -574,9 +646,12 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.12.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
dependencies = [
"parking_lot_core",
]
[[package]]
name = "openssl"
@ -629,12 +704,45 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]]
name = "parking_lot_core"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.42.0",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
@ -834,22 +942,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [
"lazy_static",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
name = "school-computer-toolkit"
version = "0.1.0"
dependencies = [
"async-scoped",
"dirs",
"futures-util",
"miette",
"once_cell",
"ramhorns",
"reqwest",
"serde",
"serde_json",
"tokio",
"url",
"windows-sys 0.42.0",
]
[[package]]
@ -933,6 +1044,12 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "smawk"
version = "0.3.1"
@ -1068,6 +1185,7 @@ dependencies = [
"libc",
"memchr",
"mio",
"num_cpus",
"once_cell",
"pin-project-lite",
"signal-hook-registry",
@ -1344,43 +1462,100 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "winreg"
version = "0.10.1"

View File

@ -6,13 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-scoped = { version = "0.7", features = ["use-tokio"] }
dirs = "4"
futures-util = { version = "0.3.21", default-features = false, features = [ "alloc" ] }
miette = { version = "4.7", features = [ "fancy" ] }
futures-util = { version = "0.3.21", default-features = false, features = ["alloc"] }
#indexmap = "1.9.1"
miette = { version = "4.7", features = ["fancy"] }
once_cell = { version = "1.16", features = ["parking_lot"] }
ramhorns = "0.14"
reqwest = { version = "0.11.10", features = [ "json" ] }
serde = { version = "1", features = [ "derive" ] }
reqwest = { version = "0.11.10", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1.19", features = [ "fs", "macros", "process", "rt" ] }
tokio = { version = "1.19", features = ["fs", "macros", "process", "rt"] }
url = "2.2"
windows-sys = { version = "0.42", features = ["Win32_Foundation", "Win32_System_Registry", "Win32_System_WindowsProgramming"] }

View File

@ -6,26 +6,52 @@ use tokio::io::AsyncWriteExt;
use crate::Context;
pub fn get_username() -> Result<String> {
const CAPACITY: u32 = 32;
let mut len: u32 = CAPACITY;
const LAYOUT: std::alloc::Layout =
unsafe { std::alloc::Layout::from_size_align_unchecked(CAPACITY as usize, 1) };
let ptr = unsafe { std::alloc::alloc(LAYOUT) };
ensure!(!ptr.is_null(), "Buffer allocation failed");
let success = unsafe {
windows_sys::Win32::System::WindowsProgramming::GetUserNameA(ptr, &mut len as *mut u32) == 1
};
ensure!(success, "GetUserNameA failed");
assert!(len <= CAPACITY, "Buffer overflow caught");
String::from_utf8(unsafe { Vec::from_raw_parts(ptr, len as usize, CAPACITY as usize) })
.into_diagnostic()
}
/// A sequential pipeline of [`Step`]s.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Pipeline<'a> {
name: &'a str,
steps: Vec<Step<'a>>,
pub name: Option<&'a str>,
pub steps: &'a [Step<'a>],
}
impl<'a> Pipeline<'a> {
#[inline(always)]
pub fn new(name: &'a str, steps: impl Into<Vec<Step<'a>>>) -> Self {
Self {
name,
steps: steps.into(),
}
pub fn new(name: &'a str, steps: &'a [Step<'a>]) -> Self {
Self::of(steps).named(name)
}
#[inline(always)]
pub fn of(steps: &'a [Step<'a>]) -> Self {
Self { name: None, steps }
}
#[inline(always)]
pub fn named(mut self, name: &'a str) -> Self {
self.name = Some(name);
self
}
pub async fn invoke(&self, ctx: &Context) -> Result<()> {
println!("Invoking {}...", self.name);
if let Some(name) = self.name {
println!("Invoking {name}...");
}
for step in self.steps.iter() {
for step in self.steps.into_iter() {
step.invoke(ctx).await?;
}
@ -33,7 +59,7 @@ impl<'a> Pipeline<'a> {
}
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub enum Step<'a> {
DownloadFile {
/// Remote resourcee to download from.
@ -56,6 +82,18 @@ pub enum Step<'a> {
args: &'a [&'a str],
},
InstallMsi {
file: Cow<'a, Path>,
props: Cow<'a, [Cow<'a, str>]>,
},
CreateDirectory {
target: Cow<'a, Path>,
parents: bool,
},
CreateShortcut {
/// Target of the shortcut (i.e. what is points to).
target: ShortcutTarget<'a>,
@ -63,11 +101,64 @@ pub enum Step<'a> {
/// Path of the created shortcut file.
file: Cow<'a, Path>,
},
/// Nukes a publicly accessible directory and locks it from further modification.
Nuke { target: Cow<'a, Path> },
/// Executes the steps concurrently.
Concurrent(&'a [Pipeline<'a>]),
/// Appends the path to the user-wide PATH environment variable.
AppendPath(Cow<'a, Path>),
}
impl<'a> Clone for Step<'a> {
#[inline]
fn clone(&self) -> Self {
match self {
Self::Concurrent(pipelines) => Self::Concurrent(pipelines.clone()),
Self::DownloadFile { res, file } => Self::DownloadFile {
res: res.clone(),
file: file.clone(),
},
Self::ExtractFile { file, dest } => Self::ExtractFile {
file: file.clone(),
dest: dest.clone(),
},
Self::ExecuteCommand { file, args } => Self::ExecuteCommand {
file: file.clone(),
args: args.clone(),
},
Self::InstallMsi { file, props } => Self::InstallMsi {
file: file.clone(),
props: props.clone(),
},
Self::CreateDirectory { target, parents } => Self::CreateDirectory {
target: target.clone(),
parents: *parents,
},
Self::CreateShortcut { target, file } => Self::CreateShortcut {
target: target.clone(),
file: file.clone(),
},
Self::Nuke { target } => Self::Nuke {
target: target.clone(),
},
Self::AppendPath(path) => Self::AppendPath(path.clone()),
}
}
}
impl<'a> Step<'a> {
#[inline]
pub async fn invoke(&self, ctx: &Context) -> Result<()> {
match self {
Self::Concurrent(sequences) => {
println!("Executing concurrent steps...");
if let Err(e) = invoke_parallel(ctx, sequences).await {
return Err(e);
}
}
Self::DownloadFile { res, file } => {
if file.exists() {
println!(
@ -186,6 +277,38 @@ impl<'a> Step<'a> {
.wrap_err(EXECUTE_COMMAND_ERROR_MSG)?;
ensure!(status.success(), EXECUTE_COMMAND_ERROR_MSG);
}
Self::InstallMsi { file, props } => {
println!(
"Installing MSI {file} with props {props:?}`...",
file = file.to_str().unwrap_or("<NON UTF-8>"),
props = props.iter().map(|s| s.as_ref()).collect::<Vec<_>>(),
);
const ERROR_MSG: &'static str = "Installing MSI failed.";
let status = tokio::process::Command::new("msiexec.exe")
.args(["/qn", "/i"])
.arg(file.as_os_str())
.args(props.into_iter().map(|s| s.as_ref()))
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.await
.into_diagnostic()
.wrap_err(ERROR_MSG)?;
ensure!(status.success(), ERROR_MSG);
}
Self::CreateDirectory { target, parents } => {
if target.is_dir() {
println!("Directory {target:?} already created.");
} else {
if *parents {
std::fs::create_dir_all(target)
} else {
std::fs::create_dir(target)
}
.into_diagnostic()
.wrap_err("Create directory failed.")?;
}
}
Self::CreateShortcut { target, file } => {
println!(
"Creating shortcut to {target:?} at {file}...",
@ -211,7 +334,6 @@ impl<'a> Step<'a> {
.wrap_err(CREATE_SHORTCUT_ERROR_MSG)?
}
ShortcutTarget::Executable { file: exec_file, args } => {
use std::fmt::Write;
tokio::process::Command::new("powershell")
.arg("-Command")
.arg(format!(r#"$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut({file:?}); $shortcut.TargetPath = {exec_file:?}; $shortcut.Arguments = {args:?}; $shortcut.Save()"#))
@ -225,6 +347,152 @@ impl<'a> Step<'a> {
};
ensure!(status.success(), CREATE_SHORTCUT_ERROR_MSG);
}
Self::Nuke { target } => {
println!(
"Nuking {target}...",
target = target.to_str().unwrap_or("<NON UTF-8>"),
);
// first delete
if target.is_dir() {
std::fs::remove_dir_all(target)
} else {
std::fs::remove_file(target)
}
.into_diagnostic()
.wrap_err("Nuke failed: Could not remove target")?;
// then make new
std::fs::create_dir_all(target)
.into_diagnostic()
.wrap_err("Nuke failed: Could not create directory")?;
let mut grant = get_username()?;
grant.push_str(":F");
let status = tokio::process::Command::new("cacls")
.arg(target.as_ref())
.arg("/T")
.arg("/G")
.arg(grant)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.await
.into_diagnostic()
.wrap_err("Nuke failed: could not set permissions")?;
ensure!(status.success(), "Nuke failed: could not set permissions");
}
Self::AppendPath(path) => {
static LOCK: once_cell::sync::Lazy<tokio::sync::Mutex<()>> =
once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new(()));
const HKEY: windows_sys::Win32::System::Registry::HKEY =
windows_sys::Win32::System::Registry::HKEY_CURRENT_USER;
const SUBKEY: &str = "Environment";
const VALUE: &str = "PATH";
const TYPE: windows_sys::Win32::System::Registry::RRF_RT =
windows_sys::Win32::System::Registry::RRF_RT_REG_SZ;
const CAPACITY: usize = 1024;
println!(
"Appending {path} to the PATH environment variable...",
path = path.to_str().unwrap_or("<NON UTF-8>"),
);
let mut buffer: [std::mem::MaybeUninit<u8>; CAPACITY] =
std::mem::MaybeUninit::uninit_array();
let mut len: u32 = CAPACITY as u32;
// this lock will be held until the end of the scope. We do this to prevent
// concurrent access to the registry from interfering with (i.e. overwriting)
// eachother.
let _lock = LOCK.lock().await;
let err = unsafe {
windows_sys::Win32::System::Registry::RegGetValueA(
HKEY,
SUBKEY.as_ptr(),
VALUE.as_ptr(),
TYPE,
std::ptr::null_mut(),
&mut buffer as *mut _ as *mut _,
&mut len as *mut _,
)
};
ensure!(
err == windows_sys::Win32::Foundation::ERROR_SUCCESS,
"RegGetValueA failed"
);
assert!(len <= CAPACITY as u32, "Buffer overflow caught");
let buffer: &mut [u8] = unsafe {
std::mem::MaybeUninit::slice_assume_init_mut(&mut buffer[..len as usize])
};
let path = path
.as_os_str()
.to_str()
.ok_or_else(|| miette!("Path is not ASCII"))?;
ensure!(path.is_ascii(), "Path is not ASCII");
let path_b = path.as_bytes();
let contains = buffer.split(|b| *b == ';' as u8).any(|item| item == path_b);
if contains {
println!("Not adding {path} because it is already in the PATH");
} else {
let mut buffer = Vec::from(buffer);
if buffer.is_empty() {
buffer.push(';' as u8);
}
buffer.extend(path_b);
let mut hkey = std::mem::MaybeUninit::uninit();
let err = unsafe {
windows_sys::Win32::System::Registry::RegOpenKeyExA(
HKEY,
SUBKEY.as_ptr(),
0,
windows_sys::Win32::System::Registry::KEY_SET_VALUE,
hkey.as_mut_ptr(),
)
};
ensure!(
err == windows_sys::Win32::Foundation::ERROR_SUCCESS,
"RegOpenKeyExA failed"
);
// SAFETY: we just opened the key (which sets the handle) and checked for errors.
let hkey = unsafe { hkey.assume_init() };
ensure!(buffer.len() < u32::MAX as usize, "Buffer is too large");
let err = unsafe {
windows_sys::Win32::System::Registry::RegSetValueExA(
hkey,
VALUE.as_ptr(),
0,
TYPE,
buffer.as_ptr(),
buffer.len() as u32,
)
};
ensure!(
err == windows_sys::Win32::Foundation::ERROR_SUCCESS,
"RegSetValueExA failed"
);
let err = unsafe {
windows_sys::Win32::System::Registry::RegCloseKey(hkey)
};
ensure!(
err == windows_sys::Win32::Foundation::ERROR_SUCCESS,
"RegCloseKey failed"
);
}
}
}
println!("-> Done.");
@ -232,9 +500,47 @@ impl<'a> Step<'a> {
}
}
// this part needed to be isolated to shut work around an async lowering recursion bug.
#[inline]
unsafe fn spawn_parallel<'a, 'b, 'c>(
ctx: &'b Context,
sequences: &'b [Pipeline<'c>],
) -> async_scoped::Scope<'a, Result<()>, async_scoped::Tokio>
where
'b: 'a,
'c: 'a,
{
async_scoped::Scope::scope(|scope| {
for seq in sequences {
scope.spawn(seq.invoke(ctx));
}
})
.0
}
#[inline]
async fn invoke_parallel<'a>(ctx: &Context, sequences: &[Pipeline<'a>]) -> Result<()> {
// SAFETY: we immediately collect and block on it with no possibility of failure.
let mut results = unsafe { spawn_parallel(ctx, sequences) };
let results = tokio::task::block_in_place(|| {
tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(results.collect())
});
let results = results.into_iter().map(|result| {
match result.into_diagnostic().wrap_err("Pipeline fork error.") {
Ok(Ok(t)) => Ok(t),
Ok(Err(e)) => Err(e),
Err(e) => Err(e),
}
});
results.collect::<Result<()>>()
}
#[derive(Debug, Clone, Copy)]
pub enum RemoteResource<'a> {
// I'm not using url::Url here because it makes it impossible to make this const.
Url(&'a str),
GitHubArtifact {
@ -338,7 +644,7 @@ async fn fetch_latest_release<'a, 'b>(
))
.into_diagnostic()
.wrap_err("Invalid GitHub repo for download step.")?;
let mut resp = reqwest
let resp = reqwest
.get(url)
.send()
.await

View File

@ -1,3 +1,6 @@
#![feature(maybe_uninit_slice)]
#![feature(maybe_uninit_uninit_array)]
#[macro_use]
extern crate miette;
@ -21,7 +24,13 @@ pub fn swtools_path() -> &'static Path {
Path::new(SWTOOLS_PATH)
}
pub fn desktop_path_fallible() -> Option<PathBuf> {
macro_rules! swtools_path {
($( $path:literal )/+) => {
Path::new(concat!("C:\\SWTools" $(, "\\", $path )+))
};
}
pub fn desktop_path() -> Option<PathBuf> {
dirs::desktop_dir()
.filter(|dir| dir.exists())
.or_else(|| Some(Path::new("H:\\Profile\\Desktop").to_owned()))
@ -29,10 +38,6 @@ pub fn desktop_path_fallible() -> Option<PathBuf> {
.or_else(|| dirs::home_dir().map(|dir| dir.join("Desktop")))
}
pub fn desktop_path() -> PathBuf {
desktop_path_fallible().expect("Desktop directory should have been resolved by now.")
}
#[derive(Clone)]
pub struct Context {
pub reqwest: reqwest::Client,
@ -50,126 +55,243 @@ async fn main() -> Result<()> {
bail!("Could not find or access {}", SWTOOLS_PATH);
}
if desktop_path_fallible().is_none() {
bail!("Could not find your desktop directory.");
}
let desktop_path = desktop_path()
.ok_or_else(|| miette!("Could not find your desktop directory."))?;
let nppp_zip = swtools_path().join("temp").join("notepad-plus-plus.zip");
let nppp_dir = swtools_path().join("notepad-plus-plus");
let nppp_zip = swtools_path!("temp" / "notepad-plus-plus.zip");
let nppp_dir = swtools_path!("notepad-plus-plus");
let epp_zip = swtools_path().join("temp").join("explorer-plus-plus.zip");
let epp_dir = swtools_path().join("explorer-plus-plus");
let arduino_zip = swtools_path!("temp" / "arduino.zip");
let arduino_dir = swtools_path!("arduino");
let minecraft_dir = swtools_path().join("minecraft");
let jdk_19_zip = swtools_path!("temp" / "jdk-19.zip");
let jdk_19_dir = swtools_path!("jdk-19");
let psiphon_dir = swtools_path().join("psiphon");
let psiphon_bin = psiphon_dir.join("psiphon3.exe");
let epp_zip = swtools_path!("temp" / "explorer-plus-plus.zip");
let epp_dir = swtools_path!("explorer-plus-plus");
let minecraft_dir = swtools_path!("minecraft");
//let psiphon_dir = swtools_path!("psiphon");
let psiphon_bin = swtools_path!("psiphon" / "psiphon3.exe");
let rustup_init = swtools_path!("temp" / "rustup-init.exe");
let deno_zip = swtools_path!("temp" / "deno.zip");
let deno_dir = swtools_path!("deno");
//let deno_exe = swtools_path!("deno" / "deno.exe");
let nppp_pl = [
Step::DownloadFile {
file: nppp_zip.into(),
res: RemoteResource::GitHubArtifact {
repo: "notepad-plus-plus/notepad-plus-plus",
pattern: "npp.{{tag_name_strip_prefix}}.portable.x64.zip",
},
},
Step::ExtractFile {
file: nppp_zip.into(),
dest: nppp_dir.into(),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: nppp_dir.join("notepad++.exe").into(),
args: "",
},
file: desktop_path.join("Notepad++.lnk").into(),
},
];
let epp_pl = [
Step::DownloadFile {
file: epp_zip.into(),
res: RemoteResource::Url(
"https://explorerplusplus.com/software/explorer++_1.3.5_x64.zip",
),
},
Step::ExtractFile {
file: epp_zip.into(),
dest: epp_dir.into(),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: epp_dir.join("Explorer++.exe").into(),
args: "",
},
file: desktop_path.join("Explorer++.lnk").into(),
},
];
let psiphon_pl = [
Step::DownloadFile {
file: psiphon_bin.into(),
res: RemoteResource::Url("https://s3.amazonaws.com/f58p-mqce-k1yj/psiphon3.exe"),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: psiphon_bin.into(),
args: "",
},
file: desktop_path.join("Psiphon3.lnk").into(),
},
];
let minecraft_pl = [
Step::DownloadFile {
file: minecraft_dir.join("minecraft.exe").into(),
res: RemoteResource::Url("https://launcher.mojang.com/download/Minecraft.exe"),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: minecraft_dir.join("minecraft.exe").into(),
args: "",
},
file: desktop_path.join("Minecraft.lnk").into(),
},
];
let c_drive_lnk_pl = [Step::CreateShortcut {
target: ShortcutTarget::Path {
path: Path::new("C:\\").into(),
},
file: desktop_path.join("OSDisk (C).lnk").into(),
}];
let pwsh_pl = [Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: Path::new("C:\\WINDOWS\\system32\\cmd.exe").into(),
args: "/C powershell",
},
file: desktop_path.join("PowerShell.lnk").into(),
}];
let arduino_pl = [
Step::DownloadFile {
file: arduino_zip.into(),
res: RemoteResource::GitHubArtifact {
repo: "arduino/arduino-ide",
pattern: "arduino-ide_{{tag_name}}_Windows_64bit.zip",
},
},
Step::ExtractFile {
file: arduino_zip.into(),
dest: arduino_dir.into(),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: arduino_dir.join("Arduino IDE.exe").into(),
args: "",
},
file: desktop_path.join("Arduino IDE.lnk").into(),
},
// see https://docs.arduino.cc/software/ide-v1/tutorials/PortableIDE
// TODO: assess if this is actually necessary
//Step::CreateDirectory {
// target: arduino_dir.join("portable").into(),
// parents: false,
//}
];
let jdk_19_pl = [
Step::DownloadFile {
file: jdk_19_zip.into(),
// TODO: expand GitHubArtifact templating support and replace the hardcoded
// url with that.
//res: RemoteResource::GitHubArtifact {
// repo: "adoptium/temurin19-binaries",
// pattern: "OpenJDK19U-jdk_x64_windows_hotspot_19.0.1_10.zip arduino-ide_{{tag_name}}_Windows_64bit.zip",
//},
res: RemoteResource::Url(
"https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.1%2B10/OpenJDK19U-jdk_x64_windows_hotspot_19.0.1_10.zip",
),
},
Step::ExtractFile {
file: jdk_19_zip.into(),
dest: jdk_19_dir.into(),
},
Step::AppendPath(jdk_19_dir.join("bin").into()),
];
let rust_pl = [
Step::DownloadFile {
file: rustup_init.into(),
res: RemoteResource::Url(
"https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe",
),
},
Step::ExecuteCommand {
file: rustup_init.into(),
args: &[
"--default-host",
"x86_64-pc-windows-gnu",
"--default-toolchain",
"nightly",
"--profile",
"default",
],
},
];
let deno_pl = [
Step::CreateDirectory {
target: deno_dir.into(),
parents: true,
},
Step::DownloadFile {
file: deno_zip.into(),
res: RemoteResource::Url(
"https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip"
),
},
Step::ExtractFile {
file: deno_zip.into(),
dest: deno_dir.into(),
},
Step::AppendPath(deno_dir.into()),
];
let pipelines = [
Pipeline::new(
"Install Notepad++",
vec![
Step::DownloadFile {
file: nppp_zip.clone().into(),
res: RemoteResource::GitHubArtifact {
repo: "notepad-plus-plus/notepad-plus-plus",
pattern: "npp.{{tag_name_strip_prefix}}.portable.x64.zip",
},
},
Step::ExtractFile {
file: nppp_zip.clone().into(),
dest: nppp_dir.clone().into(),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: nppp_dir.join("notepad++.exe").into(),
args: "",
},
file: desktop_path().join("Notepad++.lnk").into(),
},
],
),
Pipeline::new(
"Install Explorer++",
vec![
Step::DownloadFile {
file: epp_zip.clone().into(),
res: RemoteResource::Url(
"https://explorerplusplus.com/software/explorer++_1.3.5_x64.zip",
),
},
Step::ExtractFile {
file: epp_zip.clone().into(),
dest: epp_dir.clone().into(),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: epp_dir.join("Explorer++.exe").into(),
args: "",
},
file: desktop_path().join("Explorer++.lnk").into(),
},
],
),
Pipeline::new(
"Install Psiphon VPN",
vec![
Step::DownloadFile {
file: psiphon_bin.clone().into(),
res: RemoteResource::Url(
"https://s3.amazonaws.com/f58p-mqce-k1yj/psiphon3.exe",
),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: psiphon_bin.clone().into(),
args: "",
},
file: desktop_path().join("Psiphon3.lnk").into(),
},
],
),
Pipeline::new(
"Install Minecraft (Java Edition)",
vec![
Step::DownloadFile {
file: minecraft_dir.join("Minecraft.exe").into(),
res: RemoteResource::Url("https://launcher.mojang.com/download/Minecraft.exe"),
},
Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: minecraft_dir.join("Minecraft.exe").into(),
args: "",
},
file: desktop_path().join("Minecraft.lnk").into(),
},
],
),
Pipeline::new(
"Create C:\\ Shortcut",
vec![Step::CreateShortcut {
target: ShortcutTarget::Path {
path: Path::new("C:\\").into(),
},
file: desktop_path().join("OSDisk (C).lnk").into(),
}],
),
Pipeline::new(
"Create PowerShell Shortcut",
vec![Step::CreateShortcut {
target: ShortcutTarget::Executable {
file: Path::new("C:\\WINDOWS\\system32\\cmd.exe").into(),
args: "/C powershell",
},
file: desktop_path().join("PowerShell.lnk").into(),
}],
),
Pipeline::new("Install Notepad++", nppp_pl.as_slice()),
Pipeline::new("Install Explorer++", epp_pl.as_slice()),
Pipeline::new("Install Psiphon VPN", psiphon_pl.as_slice()),
Pipeline::new("Install Minecraft (Java Edition)", minecraft_pl.as_slice()),
Pipeline::new("Create C:\\ Shortcut", c_drive_lnk_pl.as_slice()),
Pipeline::new("Create PowerShell Shortcut", pwsh_pl.as_slice()),
Pipeline::new("Install Arduino IDE v2", arduino_pl.as_slice()),
Pipeline::new("Install Java (19/temurin)", jdk_19_pl.as_slice()),
Pipeline::new("Install Rust (nightly)", rust_pl.as_slice()),
// TODO: add an option to install from src
Pipeline::new("Install Deno (pre-compiled)", deno_pl.as_slice()),
];
let mut args = std::env::args_os();
let _ = args.next();
if let Some(arg) = args.next() {
match arg.to_str() {
Some("list") => {
println!("Pipelines:");
for pipeline in pipelines {
let name = pipeline.name.unwrap_or("Unnamed");
println!("\t{name}");
}
return Ok(());
}
Some("run") => {}
Some(cmd) => {
println!("Unrecognized command: {cmd}");
}
None => {
println!("Unrecognized non-utf8 command");
}
}
}
let ctx = Context {
reqwest: reqwest::Client::builder()
.user_agent(USER_AGENT_STR)
@ -178,24 +300,21 @@ async fn main() -> Result<()> {
.wrap_err("Could not initialize HTTP client.")?,
};
let results = futures_util::future::join_all(
pipelines
.into_iter()
.map(|pipeline| tokio::spawn({
let ctx = ctx.clone();
async move {
pipeline.invoke(&ctx).await
}
})),
)
.await;
let results = results
.into_iter()
.map(|result| match result.into_diagnostic().wrap_err("Task error.") {
Ok(Ok(t)) => Ok(t),
Ok(Err(e)) => Err(e),
Err(e) => Err(e),
let ctx = &ctx;
let (_, results) =
async_scoped::Scope::<'_, _, async_scoped::Tokio>::scope_and_block(move |scope| {
pipelines
.into_iter()
.for_each(|pipeline| scope.spawn(async move { pipeline.invoke(ctx).await }))
});
let results =
results.into_iter().map(
|result| match result.into_diagnostic().wrap_err("Task error.") {
Ok(Ok(t)) => Ok(t),
Ok(Err(e)) => Err(e),
Err(e) => Err(e),
},
);
let mut had_error = false;
for result in results {