From e7a3ce4976c7e1b4746c2adabe7c3a7e103f7ef8 Mon Sep 17 00:00:00 2001 From: Michael Pfaff Date: Tue, 8 Nov 2022 08:40:01 -0500 Subject: [PATCH] A bunch of stuff --- Cargo.lock | 201 +++++++++++++++++++++++-- Cargo.toml | 14 +- src/install.rs | 334 ++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 387 ++++++++++++++++++++++++++++++++----------------- 4 files changed, 770 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a4f95b..2800fc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index d744e04..9e2f12b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/install.rs b/src/install.rs index 456f719..ae20765 100644 --- a/src/install.rs +++ b/src/install.rs @@ -6,26 +6,52 @@ use tokio::io::AsyncWriteExt; use crate::Context; +pub fn get_username() -> Result { + 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>, + 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>>) -> 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(""), + props = props.iter().map(|s| s.as_ref()).collect::>(), + ); + 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(""), + ); + // 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> = + 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(""), + ); + + let mut buffer: [std::mem::MaybeUninit; 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::>() +} + #[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 diff --git a/src/main.rs b/src/main.rs index e2cc5d5..d66b9c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { +macro_rules! swtools_path { + ($( $path:literal )/+) => { + Path::new(concat!("C:\\SWTools" $(, "\\", $path )+)) + }; +} + +pub fn desktop_path() -> Option { 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 { .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 {