Add new setup/update (Rust)

This commit is contained in:
Caelan Sayler
2023-12-13 15:40:04 +00:00
parent 4e0d11fbe9
commit 0c87c94b45
23 changed files with 5382 additions and 0 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# Rust
target/
#################
## Eclipse
#################

View File

@@ -0,0 +1,11 @@
# Windows 64 bit programs
[target.x86_64-pc-windows-msvc]
rustflags = [
"-C", "link-arg=libvcruntime.lib"
]
# Windows 32 bit programs
[target.i686-pc-windows-msvc]
rustflags = [
"-C", "link-arg=libvcruntime.lib"
]

85
src/Rust/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,85 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'setup'",
"cargo": {
"args": [
"build",
"--bin=setup",
"--package=clowd_squirrel"
],
"filter": {
"name": "setup",
"kind": "bin"
}
},
"args": [
"--debug", "C:\\Source\\rust setup testing\\Clowd-3.4.439-full.nupkg", "--installto", "C:\\Source\\rust setup testing\\install"
],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'setup'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=setup",
"--package=clowd_squirrel"
],
"filter": {
"name": "setup",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'update'",
"cargo": {
"args": [
"build",
"--bin=update",
"--package=clowd_squirrel"
],
"filter": {
"name": "update",
"kind": "bin"
}
},
"args": ["start", "hello"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'update'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=update",
"--package=clowd_squirrel"
],
"filter": {
"name": "update",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

1611
src/Rust/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

89
src/Rust/Cargo.toml Normal file
View File

@@ -0,0 +1,89 @@
[package]
name = "clowd_squirrel"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "setup"
path = "src/setup.rs"
[[bin]]
name = "update"
path = "src/update.rs"
[profile.release]
opt-level = "z" # optimize for size
lto = true # link-time optimization
debug = false # disable debug info
debug-assertions = false # disable debug assertions
overflow-checks = false # disable overflow checks
panic = "abort" # abort on panic
incremental = false # disable incremental compilation
codegen-units = 1 # compile all code into a single unit
rpath = false # disable rpath
[dependencies]
anyhow = "1.0"
memmap2 = "0.9"
pretty-bytes-rust = "0.3"
xml = "0.8"
os_info = { git = "https://github.com/stanislav-tkach/os_info.git", branch = "master", default-features = false } # public releases don't yet have processor arch info
winsafe = { git = "https://github.com/caesay/winsafe.git", branch = "cs/persistfile-and-lnk", features = [
"kernel",
"version",
"user",
"shell",
"comctl",
"gui",
"ole",
] }
zip = { version = "0.6", default-features = false, features = ["deflate"] }
regex = "1.10"
rand = "0.8"
log = "0.4"
simplelog = "0.12"
clap = "4.4"
image = { version = "0.24", default-features = false, features = [
"gif",
"jpeg",
"png",
] }
fs_extra = "1.2"
windows = { version = "0.52", default-features = false, features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_Threading",
] }
windows-sys = { version = "0.52", default-features = false, features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Storage",
"Win32_Storage_FileSystem",
"Win32_System_Kernel",
"Win32_System_Threading",
"Win32_System_WindowsProgramming",
"Wdk",
"Wdk_System",
"Wdk_System_Threading",
] }
semver = "1.0"
chrono = "0.4"
wait-timeout = "0.2"
lazy_static = "1.4"
strum = { version = "0.25", features = ["derive"] }
ureq = { version = "2.9", default-features = false, features = [
"native-tls",
"gzip",
] }
native-tls = "0.2"
file-rotate = "0.7"
derivative = "2.2"
remove_dir_all = { git = "https://github.com/caesay/remove_dir_all.git", features = ["log"] }
glob = "0.3"
normpath = "1.0.1"
codesign-verify = { git = "https://github.com/caesay/codesign-verify-rs.git" }
[build-dependencies]
winres = "0.1"
semver = "1.0"
cc = "1.0"

42
src/Rust/app.manifest Normal file
View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<!-- Indicate our support for newer versions so windows stops lying to us -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
</application>
</compatibility>
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<!-- Disable legacy dpi / bitmap scaling -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

27
src/Rust/build.rs Normal file
View File

@@ -0,0 +1,27 @@
use semver;
extern crate winres;
fn main() {
cc::Build::new()
.cpp(true)
.file("src/platform/windows/shortcuts.cpp")
.define("UNICODE", None)
.define("_UNICODE", None)
.compile("lib_shortcuts");
println!("cargo:rerun-if-changed=src/platform/windows/shortcuts.cpp");
let ver = env!("CARGO_PKG_VERSION");
let ver = semver::Version::parse(&ver).unwrap();
let ver: u64 = ver.major << 48 | ver.minor << 32 | ver.patch << 16;
let _ = winres::WindowsResource::new()
.set_manifest_file("app.manifest")
.set_version_info(winres::VersionInfo::PRODUCTVERSION, ver)
.set_version_info(winres::VersionInfo::FILEVERSION, ver)
.set("CompanyName", "Clowd.Squirrel")
.set("ProductName", "Clowd.Squirrel")
.set("FileDescription", "Clowd.Squirrel")
.set("LegalCopyright", "Caelan Sayler (c) 2023")
.compile()
.unwrap();
}

5
src/Rust/rustfmt.toml Normal file
View File

@@ -0,0 +1,5 @@
max_width = 160
use_small_heuristics = "Max"
indent_style = "Visual"
unstable_features = true
format_strings = true

566
src/Rust/src/bundle.rs Normal file
View File

@@ -0,0 +1,566 @@
use anyhow::{anyhow, bail, Result};
use chrono::{Datelike, Local as DateTime};
use memmap2::Mmap;
use regex::Regex;
use semver::Version;
use std::{
cell::RefCell,
fs::{self, File},
io::{Cursor, Read, Seek},
path::{Path, PathBuf},
rc::Rc,
};
use winsafe::{self as w, co, prelude::*};
use xml::reader::{EventReader, XmlEvent};
use zip::ZipArchive;
use crate::util;
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
static BUNDLE_PLACEHOLDER: [u8; 48] = [
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package offset
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package length
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29, // 32 bytes for bundle signature
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4, //
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5, //
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d, //
];
pub fn header_offset_and_length() -> (i64, i64) {
let offset = i64::from_ne_bytes(BUNDLE_PLACEHOLDER[0..8].try_into().unwrap());
let length = i64::from_ne_bytes(BUNDLE_PLACEHOLDER[8..16].try_into().unwrap());
(offset, length)
}
#[derive(Clone)]
pub struct BundleInfo<'a> {
zip: Rc<RefCell<ZipArchive<Box<dyn ReadSeek + 'a>>>>,
zip_from_file: bool,
zip_range: Option<&'a [u8]>,
file_path: Option<PathBuf>,
}
pub fn load_bundle_from_file<'a>(file_name: &PathBuf) -> Result<BundleInfo<'a>> {
debug!("Loading bundle from file '{}'...", file_name.display());
let file = util::retry_io(|| File::open(&file_name))?;
let cursor: Box<dyn ReadSeek> = Box::new(file);
let zip = ZipArchive::new(cursor)?;
return Ok(BundleInfo { zip: Rc::new(RefCell::new(zip)), zip_from_file: true, file_path: Some(file_name.to_owned()), zip_range: None });
}
pub fn load_bundle_from_mmap<'a>(mmap: &'a Mmap, debug_pkg: &Option<&PathBuf>) -> Result<BundleInfo<'a>> {
info!("Reading bundle header...");
let (offset, length) = header_offset_and_length();
info!("Bundle offset = {}, length = {}", offset, length);
let zip_range: &'a [u8] = &mmap[offset as usize..(offset + length) as usize];
// try to load the bundle from embedded zip
if offset > 0 && length > 0 {
info!("Loading bundle from embedded zip...");
let cursor: Box<dyn ReadSeek> = Box::new(Cursor::new(zip_range));
let zip = ZipArchive::new(cursor).map_err(|e| anyhow::Error::new(e))?;
return Ok(BundleInfo { zip: Rc::new(RefCell::new(zip)), zip_from_file: false, zip_range: Some(zip_range), file_path: None });
}
// in debug mode only, allow a nupkg to be passed in as the first argument
if cfg!(debug_assertions) {
if let Some(pkg) = debug_pkg {
info!("Loading bundle from debug nupkg file...");
return load_bundle_from_file(pkg.to_owned());
}
}
bail!("Could not find embedded zip file. Please contact the application author.");
}
impl BundleInfo<'_> {
pub fn calculate_size(&self) -> (u64, u64) {
let mut total_uncompressed_size = 0u64;
let mut total_compressed_size = 0u64;
let mut archive = self.zip.borrow_mut();
for i in 0..archive.len() {
let file = archive.by_index(i);
if file.is_ok() {
let file = file.unwrap();
total_uncompressed_size += file.size();
total_compressed_size += file.compressed_size();
}
}
(total_compressed_size, total_uncompressed_size)
}
pub fn get_splash_bytes(&self) -> Option<Vec<u8>> {
let splash_idx = self.find_zip_file(|name| name.contains("splashimage"));
if splash_idx.is_none() {
warn!("Could not find splash image in bundle.");
return None;
}
let mut archive = self.zip.borrow_mut();
let sf = archive.by_index(splash_idx.unwrap());
if sf.is_err() {
warn!("Could not find splash image in bundle.");
return None;
}
let res: Result<Vec<u8>, _> = sf.unwrap().bytes().collect();
if res.is_err() {
warn!("Could not find splash image in bundle.");
return None;
}
let bytes = res.unwrap();
if bytes.is_empty() {
warn!("Could not find splash image in bundle.");
return None;
}
Some(bytes)
}
pub fn find_zip_file<F>(&self, predicate: F) -> Option<usize>
where
F: Fn(&str) -> bool,
{
let mut archive = self.zip.borrow_mut();
for i in 0..archive.len() {
if let Ok(file) = archive.by_index(i) {
let name = file.name();
if predicate(name) {
return Some(i);
}
}
}
None
}
pub fn extract_zip_idx_to_path<T: AsRef<str>>(&self, index: usize, path: T) -> Result<()> {
let path = path.as_ref();
debug!("Extracting zip file to path: {}", path);
let p = PathBuf::from(path);
let parent = p.parent().unwrap();
if !parent.exists() {
debug!("Creating parent directory: {:?}", parent);
util::retry_io(|| fs::create_dir_all(parent))?;
}
let mut archive = self.zip.borrow_mut();
let mut file = archive.by_index(index)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
debug!("Writing file to disk: {:?}", path);
util::retry_io(|| fs::write(path, &buffer))?;
Ok(())
}
pub fn extract_zip_predicate_to_path<F, T: AsRef<str>>(&self, predicate: F, path: T) -> Result<usize>
where
F: Fn(&str) -> bool,
{
let idx = self.find_zip_file(predicate);
if idx.is_none() {
bail!("Could not find file in bundle.");
}
let idx = idx.unwrap();
self.extract_zip_idx_to_path(idx, path)?;
Ok(idx)
}
pub fn read_manifest(&self) -> Result<Manifest> {
let nuspec_idx = self
.find_zip_file(|name| name.ends_with(".nuspec"))
.ok_or_else(|| anyhow!("This installer is missing a package manifest (.nuspec). Please contact the application author."))?;
let mut contents = String::new();
let mut archive = self.zip.borrow_mut();
archive.by_index(nuspec_idx)?.read_to_string(&mut contents)?;
let app = read_manifest_from_string(&contents)?;
Ok(app)
}
pub fn copy_bundle_to_file<T: AsRef<str>>(&self, nupkg_path: T) -> Result<()> {
let nupkg_path = nupkg_path.as_ref();
if self.zip_from_file {
util::retry_io(|| fs::copy(self.file_path.clone().unwrap(), nupkg_path))?;
} else {
util::retry_io(|| fs::write(nupkg_path, self.zip_range.unwrap()))?;
}
Ok(())
}
pub fn len(&self) -> usize {
let archive = self.zip.borrow();
archive.len()
}
pub fn get_file_names(&self) -> Result<Vec<String>> {
let mut files: Vec<String> = Vec::new();
let mut archive = self.zip.borrow_mut();
for i in 0..archive.len() {
let file = archive.by_index(i)?;
let key = file.name();
files.push(key.to_string());
}
Ok(files)
}
}
#[derive(Debug, Default, Clone)]
pub struct Manifest {
pub id: String,
pub version: String,
pub title: String,
pub authors: String,
pub description: String,
pub machine_architecture: String,
pub runtime_dependencies: String,
pub main_exe: String,
pub os: String,
pub os_min_version: String,
}
#[cfg(target_os = "windows")]
impl Manifest {
const UNINST_STR: &'static str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
pub fn get_update_path(&self, root_path: &PathBuf) -> String {
root_path.join("Update.exe").to_string_lossy().to_string()
}
pub fn get_main_exe_path(&self, root_path: &PathBuf) -> String {
root_path.join("current").join(&self.main_exe).to_string_lossy().to_string()
}
pub fn get_packages_path(&self, root_path: &PathBuf) -> String {
root_path.join("packages").to_string_lossy().to_string()
}
pub fn get_current_path(&self, root_path: &PathBuf) -> String {
root_path.join("current").to_string_lossy().to_string()
}
pub fn get_nuspec_path(&self, root_path: &PathBuf) -> String {
root_path.join("current").join("sq.version").to_string_lossy().to_string()
}
pub fn get_target_nupkg_path(&self, root_path: &PathBuf) -> String {
root_path.join("packages").join(format!("{}-{}-full.nupkg", self.id, self.version)).to_string_lossy().to_string()
}
pub fn write_uninstall_entry(&self, root_path: &PathBuf) -> Result<()> {
info!("Writing uninstall registry key...");
let root_path_str = root_path.to_string_lossy().to_string();
let main_exe_path = self.get_main_exe_path(root_path);
let updater_path = self.get_update_path(root_path);
let folder_size = fs_extra::dir::get_size(&root_path).unwrap();
let sver = semver::Version::parse(&self.version)?;
let sver_str = format!("{}.{}.{}", sver.major, sver.minor, sver.patch);
let now = DateTime::now();
let formatted_date = format!("{}{:02}{:02}", now.year(), now.month(), now.day());
let uninstall_cmd = format!("\"{}\" --uninstall", updater_path);
let uninstall_quiet = format!("\"{}\" --uninstall --silent", updater_path);
let reg_uninstall = w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
let reg_app = reg_uninstall.RegCreateKeyEx(&self.id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0;
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(self.title.to_owned()))?;
reg_app.RegSetKeyValue(None, Some("DisplayVersion"), w::RegistryValue::Sz(sver_str))?;
reg_app.RegSetKeyValue(None, Some("InstallDate"), w::RegistryValue::Sz(formatted_date))?;
reg_app.RegSetKeyValue(None, Some("InstallLocation"), w::RegistryValue::Sz(root_path_str.to_owned()))?;
reg_app.RegSetKeyValue(None, Some("Publisher"), w::RegistryValue::Sz(self.authors.to_owned()))?;
reg_app.RegSetKeyValue(None, Some("QuietUninstallString"), w::RegistryValue::Sz(uninstall_quiet))?;
reg_app.RegSetKeyValue(None, Some("UninstallString"), w::RegistryValue::Sz(uninstall_cmd))?;
reg_app.RegSetKeyValue(None, Some("EstimatedSize"), w::RegistryValue::Dword((folder_size / 1024).try_into()?))?;
reg_app.RegSetKeyValue(None, Some("NoModify"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("NoRepair"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("Language"), w::RegistryValue::Dword(0x0409))?;
Ok(())
}
pub fn remove_uninstall_entry(&self) -> Result<()> {
info!("Removing uninstall registry keys...");
let reg_uninstall = w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
reg_uninstall.RegDeleteKey(&self.id)?;
Ok(())
}
}
pub fn read_manifest_from_string(xml: &str) -> Result<Manifest> {
let mut obj: Manifest = Default::default();
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut vec: Vec<String> = Vec::new();
for e in parser {
match e {
Ok(XmlEvent::StartElement { name, .. }) => {
vec.push(name.local_name);
}
Ok(XmlEvent::Characters(text)) => {
if vec.is_empty() {
continue;
}
let el_name = vec.last().unwrap();
if el_name == "id" {
obj.id = text;
} else if el_name == "version" {
obj.version = text;
} else if el_name == "title" {
obj.title = text;
} else if el_name == "authors" {
obj.authors = text;
} else if el_name == "description" {
obj.description = text;
} else if el_name == "machineArchitecture" {
obj.machine_architecture = text;
} else if el_name == "runtimeDependencies" {
obj.runtime_dependencies = text;
} else if el_name == "mainExe" {
obj.main_exe = text;
} else if el_name == "os" {
obj.os = text;
} else if el_name == "osMinVersion" {
obj.os_min_version = text;
}
}
Ok(XmlEvent::EndElement { .. }) => {
vec.pop();
}
Err(e) => {
error!("Error: {e}");
break;
}
// There's more: https://docs.rs/xml-rs/latest/xml/reader/enum.XmlEvent.html
_ => {}
}
}
if obj.id.is_empty() {
bail!("Missing 'id' in package manifest. Please contact the application author.");
}
if !obj.os.is_empty() && obj.os != "win" {
bail!("Unsupported 'os' in package manifest ({}). Please contact the application author.", obj.os);
}
if obj.version.is_empty() {
bail!("Missing 'version' in package manifest. Please contact the application author.");
}
if obj.main_exe.is_empty() {
bail!("Missing 'mainExe' in package manifest. Please contact the application author.");
}
if obj.title.is_empty() {
obj.title = obj.id.clone();
}
Ok(obj)
}
#[derive(Debug, Clone, derivative::Derivative)]
#[derivative(Default)]
pub struct EntryNameInfo {
pub name: String,
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
pub version: Version,
pub is_delta: bool,
pub file_path: String,
pub os: Option<String>,
pub os_min_ver: Option<String>,
pub os_arch: Option<String>,
}
impl EntryNameInfo {
pub fn load_manifest(&self) -> Result<Manifest> {
let path = Path::new(&self.file_path).to_path_buf();
let bundle = load_bundle_from_file(&path)?;
bundle.read_manifest()
}
}
lazy_static! {
static ref ENTRY_SUFFIX_FULL: Regex = Regex::new(r"(?i)-full.nupkg$").unwrap();
static ref ENTRY_SUFFIX_DELTA: Regex = Regex::new(r"(?i)-delta.nupkg$").unwrap();
static ref ENTRY_VERSION_START: Regex = Regex::new(r"[\.-](0|[1-9]\d*)\.(0|[1-9]\d*)($|[^\d])").unwrap();
static ref ENTRY_RID: Regex = Regex::new(r"(?i)(-(?<os>osx|win)\.?(?<ver>[\d\.]+)?)?(?:-(?<arch>x64|x86|arm64))?$").unwrap();
}
pub fn parse_package_file_path(path: PathBuf) -> Option<EntryNameInfo> {
let name = path.file_name()?.to_string_lossy().to_string();
let m = parse_package_file_name(name);
if m.is_some() {
let mut m = m.unwrap();
m.file_path = path.to_string_lossy().to_string();
return Some(m);
}
m
}
fn parse_package_file_name<T: AsRef<str>>(name: T) -> Option<EntryNameInfo> {
let name = name.as_ref();
let full = ENTRY_SUFFIX_FULL.is_match(name);
let delta = ENTRY_SUFFIX_DELTA.is_match(name);
if !full && !delta {
return None;
}
let mut entry = EntryNameInfo::default();
entry.is_delta = delta;
let name_and_ver = if full { ENTRY_SUFFIX_FULL.replace(name, "") } else { ENTRY_SUFFIX_DELTA.replace(name, "") };
let ver_idx = ENTRY_VERSION_START.find(&name_and_ver);
if ver_idx.is_none() {
return None;
}
let ver_idx = ver_idx.unwrap().start();
entry.name = name_and_ver[0..ver_idx].to_string();
let ver_idx = ver_idx + 1;
let version = name_and_ver[ver_idx..].to_string();
let rid_idx = ENTRY_RID.find(&version);
if rid_idx.is_none() {
let sv = Version::parse(&version);
if sv.is_err() {
return None;
}
entry.version = sv.unwrap();
return Some(entry);
}
let rid_idx = rid_idx.unwrap().start();
let caps = ENTRY_RID.captures(&version).unwrap();
let version = version[0..rid_idx].to_string();
let sv = Version::parse(&version);
if sv.is_err() {
return None;
}
entry.version = sv.unwrap();
entry.os = caps.name("os").map(|m| m.as_str().to_string());
entry.os_min_ver = caps.name("ver").map(|m| m.as_str().to_string());
entry.os_arch = caps.name("arch").map(|m| m.as_str().to_string());
Some(entry)
}
#[test]
fn test_parse_package_file_name() {
// test no rid
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, None);
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, None);
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-delta.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, true);
assert_eq!(entry.os, None);
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, None);
let entry = parse_package_file_name("My.Cool-App-1.1.0-full.nupkg").unwrap();
assert_eq!(entry.name, "My.Cool-App");
assert_eq!(entry.version, Version::parse("1.1.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, None);
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, None);
// test with rid individual components
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-osx-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, Some("osx".to_string()));
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, None);
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-win-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, Some("win".to_string()));
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, None);
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-x86-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, None);
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, Some("x86".to_string()));
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-x64-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, None);
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, Some("x64".to_string()));
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-arm64-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, None);
assert_eq!(entry.os_min_ver, None);
assert_eq!(entry.os_arch, Some("arm64".to_string()));
// test with full rid
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-win10-x64-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, Some("win".to_string()));
assert_eq!(entry.os_min_ver, Some("10".to_string()));
assert_eq!(entry.os_arch, Some("x64".to_string()));
let entry = parse_package_file_name("Clowd.Squirrel-1.0.0-win10-arm64-full.nupkg").unwrap();
assert_eq!(entry.name, "Clowd.Squirrel");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, Some("win".to_string()));
assert_eq!(entry.os_min_ver, Some("10".to_string()));
assert_eq!(entry.os_arch, Some("arm64".to_string()));
// test with version extras
let entry = parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.nupkg").unwrap();
assert_eq!(entry.name, "MyCoolApp");
assert_eq!(entry.version, Version::parse("1.2.3-beta1").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, Some("win".to_string()));
assert_eq!(entry.os_min_ver, Some("7".to_string()));
assert_eq!(entry.os_arch, Some("x64".to_string()));
let entry = parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-delta.nupkg").unwrap();
assert_eq!(entry.name, "MyCoolApp");
assert_eq!(entry.version, Version::parse("1.2.3-beta1").unwrap());
assert_eq!(entry.is_delta, true);
assert_eq!(entry.os, Some("win".to_string()));
assert_eq!(entry.os_min_ver, Some("7".to_string()));
assert_eq!(entry.os_arch, Some("x64".to_string()));
let entry = parse_package_file_name("MyCoolApp-1.2.3-beta.22.44-win7-x64-full.nupkg").unwrap();
assert_eq!(entry.name, "MyCoolApp");
assert_eq!(entry.version, Version::parse("1.2.3-beta.22.44").unwrap());
assert_eq!(entry.is_delta, false);
assert_eq!(entry.os, Some("win".to_string()));
assert_eq!(entry.os_min_ver, Some("7".to_string()));
assert_eq!(entry.os_arch, Some("x64".to_string()));
// test invalid names
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.nupkg.zip").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.zip").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2.3.nupkg").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2-full.nupkg").is_none());
}

82
src/Rust/src/download.rs Normal file
View File

@@ -0,0 +1,82 @@
use anyhow::Result;
use std::fs::File;
use std::io::{Read, Write};
use crate::util;
pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> Result<()>
where
A: FnMut(i16),
{
// let url = "https://proof.ovh.net/files/10Mb.dat";
let tls_connector = native_tls::TlsConnector::new()?;
let agent: ureq::Agent = ureq::AgentBuilder::new().tls_connector(tls_connector.into()).build();
let response = agent.get(url).call()?;
let total_size = response.header("Content-Length").and_then(|s| s.parse::<u64>().ok());
let mut file = util::retry_io(|| File::create(file_path))?;
const CHUNK_SIZE: usize = 2 * 1024 * 1024; // 2MB
let mut downloaded: u64 = 0;
let mut buffer = vec![0; CHUNK_SIZE];
let mut reader = response.into_reader();
let mut last_progress = 0;
while let Ok(size) = reader.read(&mut buffer) {
if size == 0 {
break; // End of stream
}
file.write_all(&buffer[..size])?;
downloaded += size as u64;
if total_size.is_some() {
// floor to nearest 5% to reduce message spam
let new_progress = (downloaded as f64 / total_size.unwrap() as f64 * 20.0).floor() as i16 * 5;
if new_progress > last_progress {
last_progress = new_progress;
progress(last_progress);
}
}
}
Ok(())
}
pub fn download_url_as_string(url: &str) -> Result<String> {
let tls_connector = native_tls::TlsConnector::new()?;
let agent: ureq::Agent = ureq::AgentBuilder::new().tls_connector(tls_connector.into()).build();
let r = agent.get(url).call()?.into_string()?;
Ok(r)
}
#[test]
fn test_download_uses_tls_and_encoding_correctly() {
assert_eq!(download_url_as_string("https://dotnetcli.blob.core.windows.net/dotnet/WindowsDesktop/5.0/latest.version").unwrap(), "5.0.17");
}
#[test]
fn test_download_file_reports_progress() {
// https://www.ip-toolbox.com/speedtest-files/
let test_file = "https://proof.ovh.net/files/10Mb.dat";
let mut prog_count = 0;
let mut last_prog = 0;
download_url_to_file(test_file, "test_download_file_reports_progress.txt", |p| {
assert!(p >= last_prog);
prog_count += 1;
last_prog = p;
})
.unwrap();
assert!(prog_count >= 4);
assert!(prog_count <= 20);
assert_eq!(last_prog, 100);
let p = std::path::Path::new("test_download_file_reports_progress.txt");
let meta = p.metadata().unwrap();
let len = meta.len();
assert_eq!(len, 10 * 1024 * 1024);
std::fs::remove_file(p).unwrap();
}

View File

@@ -0,0 +1,11 @@
#[cfg(target_family = "windows")]
mod windows;
#[cfg(target_family = "unix")]
mod unix;
#[cfg(target_family = "windows")]
pub use windows::*;
#[cfg(target_family = "unix")]
pub use unix::*;

View File

@@ -0,0 +1,8 @@
use anyhow::{anyhow, bail, Result};
pub fn wait_for_parent_to_exit() -> Result<()> {
let id = std::os::unix::process::parent_id();
if id >= 1 {
waitpid(id);
}
}

View File

@@ -0,0 +1,159 @@
use crate::bundle::Manifest;
use std::{
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
};
use winsafe::{self as w, co, prelude::*, WString};
static SILENT: AtomicBool = AtomicBool::new(false);
pub fn set_silent(silent: bool) {
SILENT.store(silent, Ordering::Relaxed);
}
pub fn get_silent() -> bool {
SILENT.load(Ordering::Relaxed)
}
pub fn show_error<T: AsRef<str>, T2: AsRef<str>>(err: T, title: T2) {
if get_silent() {
return;
}
let err = err.as_ref();
let title = title.as_ref();
let _ = w::HWND::GetDesktopWindow().MessageBox(err, title, co::MB::ICONERROR);
}
pub fn show_info<T: AsRef<str>, T2: AsRef<str>>(info: T, title: T2) {
if get_silent() {
return;
}
let info = info.as_ref();
let title = title.as_ref();
let _ = w::HWND::GetDesktopWindow().MessageBox(info, title, co::MB::ICONINFORMATION);
}
pub fn show_warning<T: AsRef<str>, T2: AsRef<str>>(warning: T, title: T2) {
if get_silent() {
return;
}
let warning = warning.as_ref();
let title = title.as_ref();
let _ = w::HWND::GetDesktopWindow().MessageBox(warning, title, co::MB::ICONWARNING);
}
pub fn show_restart_required(app: &Manifest) {
if get_silent() {
return;
}
let hwnd = w::HWND::GetDesktopWindow();
let _ = w::task_dlg::warn(
&hwnd,
format!("{} Setup {}", app.title, app.version).as_str(),
Some("Restart Required"),
"A restart is required before Setup can continue. Please restart your computer and try again.",
);
}
pub fn show_missing_dependencies_dialog(app: &Manifest, depedency_string: &str) -> bool {
if get_silent() {
return true;
}
let hwnd = w::HWND::GetDesktopWindow();
w::task_dlg::ok_cancel(
&hwnd,
format!("{} Setup {}", app.title, app.version).as_str(),
Some(format!("{} has missing system dependencies.", app.title).as_str()),
format!("{} requires the following packages to be installed: {}, would you like to continue?", app.title, depedency_string).as_str(),
Some("Install"),
)
.unwrap_or(false)
}
pub fn show_uninstall_complete_with_errors_dialog(app: &Manifest, log_path: &PathBuf) {
if get_silent() {
return;
}
let mut setup_name = WString::from_str(format!("{} Uninstall", app.title));
let mut instruction = WString::from_str(format!("{} uninstall has completed with errors.", app.title));
let mut content = WString::from_str(
"There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again.",
);
let mut footer = WString::from_str(format!("Log file: '<A HREF=\"na\">{}</A>'", log_path.display()));
let mut config: w::TASKDIALOGCONFIG = Default::default();
config.dwFlags = co::TDF::ENABLE_HYPERLINKS | co::TDF::SIZE_TO_CONTENT;
config.dwCommonButtons = co::TDCBF::OK;
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::WARNING));
config.set_pszWindowTitle(Some(&mut setup_name));
config.set_pszMainInstruction(Some(&mut instruction));
config.set_pszContent(Some(&mut content));
if log_path.exists() {
config.set_pszFooterIcon(w::IconId::Id(co::TD_ICON::INFORMATION.into()));
config.set_pszFooter(Some(&mut footer));
}
config.lpCallbackData = log_path as *const PathBuf as usize;
config.pfCallback = Some(task_dialog_callback);
let _ = w::TaskDialogIndirect(&config, None);
}
pub fn show_overwrite_repair_dialog(app: &Manifest, root_path: &PathBuf, root_is_default: bool) -> bool {
if get_silent() {
return true;
}
let mut setup_name = WString::from_str(format!("{} Setup {}", app.title, app.version));
let mut instruction = WString::from_str(format!("{} is already installed.", app.title));
let mut content = WString::from_str("This application is installed on your computer. You can attempt to repair the install, this may end up installing an older version of the application and may delete any saved settings/preferences.");
let mut footer = if root_is_default {
WString::from_str(format!("The install directory is '<A HREF=\"na\">%LocalAppData%\\{}</A>'", app.id))
} else {
WString::from_str(format!("The install directory is '<A HREF=\"na\">{}</A>'", root_path.display()))
};
let mut btn_yes_txt = WString::from_str(format!("Repair\nErase the application and install version {}.", app.version));
let mut btn_yes = w::TASKDIALOG_BUTTON::default();
btn_yes.set_nButtonID(co::DLGID::YES.into());
btn_yes.set_pszButtonText(Some(&mut btn_yes_txt));
let mut btn_cancel_txt = WString::from_str("Cancel\nBackup or save your work first.");
let mut btn_cancel = w::TASKDIALOG_BUTTON::default();
btn_cancel.set_nButtonID(co::DLGID::CANCEL.into());
btn_cancel.set_pszButtonText(Some(&mut btn_cancel_txt));
let mut custom_btns = Vec::with_capacity(2);
custom_btns.push(btn_yes);
custom_btns.push(btn_cancel);
let mut config: w::TASKDIALOGCONFIG = Default::default();
config.dwFlags = co::TDF::ENABLE_HYPERLINKS | co::TDF::USE_COMMAND_LINKS | co::TDF::SIZE_TO_CONTENT;
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::WARNING));
config.set_pButtons(Some(&mut custom_btns));
config.set_pszWindowTitle(Some(&mut setup_name));
config.set_pszMainInstruction(Some(&mut instruction));
config.set_pszContent(Some(&mut content));
config.set_pszFooterIcon(w::IconId::Id(co::TD_ICON::INFORMATION.into()));
config.set_pszFooter(Some(&mut footer));
config.lpCallbackData = root_path as *const PathBuf as usize;
config.pfCallback = Some(task_dialog_callback);
let (btn, _) = w::TaskDialogIndirect(&config, None).ok().unwrap_or_else(|| (co::DLGID::YES, 0));
return btn == co::DLGID::YES;
}
extern "system" fn task_dialog_callback(_: w::HWND, msg: co::TDN, _: usize, _: isize, lp_ref_data: usize) -> co::HRESULT {
if msg == co::TDN::HYPERLINK_CLICKED {
let raw = lp_ref_data as *const PathBuf;
let path: &PathBuf = unsafe { &*raw };
let dir = path.to_str().unwrap();
w::HWND::GetDesktopWindow().ShellExecute("open", &dir, None, None, co::SW::SHOWDEFAULT).ok();
return co::HRESULT::S_FALSE; // do not close dialog
}
return co::HRESULT::S_OK; // close dialog on button press
}

View File

@@ -0,0 +1,9 @@
mod dialogs;
mod self_delete;
mod shortcuts;
mod util;
pub use dialogs::*;
pub use self_delete::*;
pub use shortcuts::*;
pub use util::*;

View File

@@ -0,0 +1,57 @@
use anyhow::{anyhow, Result};
use std::{
env,
ffi::{OsStr, OsString},
mem::size_of,
os::windows::ffi::OsStrExt,
path::Path,
};
use windows::{
core::{PCWSTR, PWSTR},
Win32::System::Threading::{CreateProcessW, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, STARTUPINFOW},
};
pub fn register_intent_to_delete_self(delay_seconds: usize, current_directory: &Path) -> Result<()> {
info!("Deleting self...");
let my_self = env::current_exe()?.to_string_lossy().to_string();
let command = format!("cmd.exe /C choice /C Y /N /D Y /T {} & Del \"{}\"", delay_seconds, my_self);
info!("Running: {}", command);
const CREATE_NO_WINDOW: u32 = 0x08000000;
new_process(&OsString::from(command), false, Some(current_directory), CREATE_NO_WINDOW)?;
Ok(())
}
fn new_process(command: &OsStr, inherit_handles: bool, current_directory: Option<&Path>, process_creation_flags: u32) -> Result<PROCESS_INFORMATION> {
let mut startup_info = STARTUPINFOW::default();
let mut process_info = PROCESS_INFORMATION::default();
startup_info.cb = size_of::<STARTUPINFOW>() as u32;
let process_creation_flags = PROCESS_CREATION_FLAGS(process_creation_flags);
let current_directory_ptr =
current_directory.map(|path| path.as_os_str().encode_wide().collect::<Vec<_>>()).map(|wide_path| wide_path.as_ptr()).unwrap_or(std::ptr::null_mut());
let mut command = command.encode_wide().collect::<Vec<_>>();
let res = unsafe {
CreateProcessW(
PCWSTR::null(),
PWSTR(command.as_mut_ptr()),
None,
None,
inherit_handles,
process_creation_flags,
None,
PCWSTR(current_directory_ptr),
&startup_info,
&mut process_info,
)
};
if res.is_ok() {
Ok(process_info)
} else {
Err(anyhow!("Failed to create process."))
}
}

View File

@@ -0,0 +1,65 @@
#include "windows.h"
#include "winnls.h"
#include "shobjidl.h"
#include "objbase.h"
#include "objidl.h"
#include "shlguid.h"
#include "strsafe.h"
// all ripped from the following link and then modified
// https://learn.microsoft.com/en-us/windows/win32/shell/links
extern "C" HRESULT CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink, LPCWSTR lpszWorkDir)
{
HRESULT hres;
IShellLink* psl;
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
if (SUCCEEDED(hres)) {
IPersistFile* ppf;
psl->SetPath(lpszPathObj);
psl->SetWorkingDirectory(lpszWorkDir);
hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
if (SUCCEEDED(hres)) {
hres = ppf->Save(lpszPathLink, TRUE);
ppf->Release();
}
psl->Release();
}
return hres;
}
extern "C" HRESULT ResolveLink(LPWSTR lpszLinkFile, LPWSTR lpszPath, int iPathBufferSize, LPWSTR lpszWorkDir, int iWorkDirBufferSize)
{
HRESULT hres;
IShellLink* psl;
WCHAR szGotPath[MAX_PATH];
WCHAR szWorkDir[MAX_PATH];
WIN32_FIND_DATA wfd;
*lpszPath = 0; // Assume failure
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
if (SUCCEEDED(hres)) {
IPersistFile* ppf;
hres = psl->QueryInterface(IID_IPersistFile, (void**)&ppf);
if (SUCCEEDED(hres)) {
hres = ppf->Load(lpszLinkFile, STGM_READ);
if (SUCCEEDED(hres)) {
hres = psl->Resolve(0, 0x2 | 0x1 | (1 << 16));
if (SUCCEEDED(hres)) {
hres = psl->GetPath(szGotPath, MAX_PATH, (WIN32_FIND_DATA*)&wfd, SLGP_UNCPRIORITY);
if (SUCCEEDED(hres)) {
hres = psl->GetWorkingDirectory(szWorkDir, MAX_PATH);
if (SUCCEEDED(hres)) {
hres = StringCbCopy(lpszPath, iPathBufferSize, szGotPath);
if (SUCCEEDED(hres)) {
hres = StringCbCopy(lpszWorkDir, iWorkDirBufferSize, szWorkDir);
}
}
}
}
}
ppf->Release();
}
psl->Release();
}
return hres;
}

View File

@@ -0,0 +1,115 @@
use anyhow::Result;
use glob::glob;
use std::path::Path;
use winsafe::{self as w, co, HrResult, WString};
use crate::platform;
use crate::util;
type PCSTR = *const u16;
extern "C" {
fn CreateLink(lpszPathObj: PCSTR, lpszPathLink: PCSTR, lpszWorkDir: PCSTR) -> u32;
fn ResolveLink(lpszLinkFile: PCSTR, lpszPath: PCSTR, iPathBufferSize: i32, lpszWorkDir: PCSTR, iWorkDirBufferSize: i32) -> u32;
}
pub fn resolve_lnk(link_path: &str) -> HrResult<(String, String)> {
let _comguard = w::CoInitializeEx(co::COINIT::APARTMENTTHREADED)?;
_resolve_lnk(link_path)
}
pub fn create_lnk(output: &str, target: &str, work_dir: &str) -> HrResult<()> {
let _comguard = w::CoInitializeEx(co::COINIT::APARTMENTTHREADED)?;
_create_lnk(output, target, work_dir)
}
fn _create_lnk(output: &str, target: &str, work_dir: &str) -> HrResult<()> {
let _comguard = w::CoInitializeEx(co::COINIT::APARTMENTTHREADED)?;
let hr = unsafe { CreateLink(WString::from_str(target).as_ptr(), WString::from_str(output).as_ptr(), WString::from_str(work_dir).as_ptr()) };
match unsafe { co::HRESULT::from_raw(hr) } {
co::HRESULT::S_OK => Ok(()),
hr => Err(hr),
}
}
fn _resolve_lnk(link_path: &str) -> HrResult<(String, String)> {
let mut path_buf = WString::new_alloc_buf(1024);
let mut work_dir_buf = WString::new_alloc_buf(1024);
let hr = unsafe {
ResolveLink(
WString::from_str(link_path).as_ptr(),
path_buf.as_mut_ptr(),
path_buf.buf_len() as _,
work_dir_buf.as_mut_ptr(),
work_dir_buf.buf_len() as _,
)
};
match unsafe { co::HRESULT::from_raw(hr) } {
co::HRESULT::S_OK => Ok((path_buf.to_string(), work_dir_buf.to_string())),
hr => Err(hr),
}
}
pub fn remove_all_for_root_dir<P: AsRef<Path>>(root_dir: P) -> Result<()> {
let _comguard = w::CoInitializeEx(co::COINIT::APARTMENTTHREADED)?;
let root_dir = root_dir.as_ref();
info!("Searching for shortcuts containing root: '{}'", root_dir.to_string_lossy());
let mut search_paths = vec![
w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Desktop, co::KF::DONT_UNEXPAND, None)?,
w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Startup, co::KF::DONT_UNEXPAND, None)?,
w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None)?,
];
let pinned_str = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::RoamingAppData, co::KF::DONT_UNEXPAND, None)?;
let pinned_path = Path::new(&pinned_str).join("Microsoft\\Internet Explorer\\Quick Launch\\User Pinned");
search_paths.push(pinned_path.to_string_lossy().to_string());
for search_root in search_paths {
let g = format!("{}/**/*.lnk", search_root);
info!("Searching for shortcuts in: '{}'", g);
if let Ok(paths) = glob(&g) {
for path in paths {
if let Ok(path) = path {
trace!("Checking shortcut: '{}'", path.to_string_lossy());
let res = _resolve_lnk(&path.to_string_lossy());
if let Ok((target, work_dir)) = res {
let target_match = platform::is_sub_path(&target, root_dir).unwrap_or(false);
let work_dir_match = platform::is_sub_path(&work_dir, root_dir).unwrap_or(false);
if target_match || work_dir_match {
let mstr = if target_match && work_dir_match {
format!("both target ({}) and work dir ({})", target, work_dir)
} else if target_match {
format!("target ({})", target)
} else {
format!("work dir ({})", work_dir)
};
warn!("Removing shortcut '{}' because {} matched.", path.to_string_lossy(), mstr);
util::retry_io(|| std::fs::remove_file(&path))?;
}
}
}
}
}
}
Ok(())
}
#[test]
fn shortcut_full_integration_test() {
let desktop = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Desktop, co::KF::DONT_UNEXPAND, None).unwrap();
let link_location = Path::new(&desktop).join("testclowd123hi.lnk");
let target = r"C:\Users\Caelan\AppData\Local\NonExistingAppHello123\current\HelloWorld.exe";
let work = r"C:\Users\Caelan/appData\Local/NonExistingAppHello123\current";
let root = r"C:\Users\Caelan/appData\Local/NonExistingAppHello123";
create_lnk(&link_location.to_string_lossy(), target, work).unwrap();
assert!(link_location.exists());
let (target_out, work_out) = resolve_lnk(&link_location.to_string_lossy()).unwrap();
assert_eq!(target_out, target);
assert_eq!(work_out, work);
remove_all_for_root_dir(root).unwrap();
assert!(!link_location.exists());
}

View File

@@ -0,0 +1,502 @@
use anyhow::{anyhow, bail, Result};
use normpath::PathExt;
use std::{
collections::HashMap,
ffi::OsStr,
io::Read,
os::windows::process::CommandExt,
path::{Path, PathBuf},
process::Command as Process,
time::Duration,
};
use wait_timeout::ChildExt;
use windows::{
core::PCWSTR,
Win32::{
Foundation::{self, GetLastError},
System::Threading::CreateMutexW,
},
};
use winsafe::{self as w, co, prelude::*};
use crate::util;
pub fn wait_for_parent_to_exit(ms_to_wait: u32) -> Result<()> {
info!("Reading parent process information.");
let basic_info = windows_sys::Wdk::System::Threading::ProcessBasicInformation;
let handle = unsafe { windows_sys::Win32::System::Threading::GetCurrentProcess() };
let mut return_length: u32 = 0;
let return_length_ptr: *mut u32 = &mut return_length as *mut u32;
let mut info = windows_sys::Win32::System::Threading::PROCESS_BASIC_INFORMATION {
AffinityMask: 0,
BasePriority: 0,
ExitStatus: 0,
InheritedFromUniqueProcessId: 0,
PebBaseAddress: std::ptr::null_mut(),
UniqueProcessId: 0,
};
let info_ptr: *mut ::core::ffi::c_void = &mut info as *mut _ as *mut ::core::ffi::c_void;
let info_size = std::mem::size_of::<windows_sys::Win32::System::Threading::PROCESS_BASIC_INFORMATION>() as u32;
let hr = unsafe { windows_sys::Wdk::System::Threading::NtQueryInformationProcess(handle, basic_info, info_ptr, info_size, return_length_ptr) };
if hr != 0 {
return Err(anyhow!("Failed to query process information: {}", hr));
}
if info.InheritedFromUniqueProcessId <= 1 {
// the parent process has exited
info!("The parent process ({}) has already exited", info.InheritedFromUniqueProcessId);
return Ok(());
}
fn get_pid_start_time(process: w::HPROCESS) -> Result<u64> {
let mut creation = w::FILETIME::default();
let mut exit = w::FILETIME::default();
let mut kernel = w::FILETIME::default();
let mut user = w::FILETIME::default();
process.GetProcessTimes(&mut creation, &mut exit, &mut kernel, &mut user)?;
Ok(((creation.dwHighDateTime as u64) << 32) | creation.dwLowDateTime as u64)
}
let parent_handle = w::HPROCESS::OpenProcess(co::PROCESS::QUERY_LIMITED_INFORMATION, false, info.InheritedFromUniqueProcessId as u32)?;
let parent_start_time = get_pid_start_time(unsafe { parent_handle.raw_copy() })?;
let myself_start_time = get_pid_start_time(w::HPROCESS::GetCurrentProcess())?;
if parent_start_time > myself_start_time {
// the parent process has exited and the id has been re-used
info!(
"The parent process ({}) has already exited. parent_start={}, my_start={}",
info.InheritedFromUniqueProcessId, parent_start_time, myself_start_time
);
return Ok(());
}
info!("Waiting {}ms for parent process ({}) to exit.", ms_to_wait, info.InheritedFromUniqueProcessId);
match parent_handle.WaitForSingleObject(Some(ms_to_wait)) {
Ok(co::WAIT::OBJECT_0) => Ok(()),
// Ok(co::WAIT::TIMEOUT) => Ok(()),
_ => Err(anyhow!("Failed to wait for parent process to exit.")),
}
}
pub fn create_global_mutex(name: &str) -> Result<Foundation::HANDLE> {
let encoded = name.encode_utf16().chain([0u16]).collect::<Vec<u16>>();
let pw = PCWSTR(encoded.as_ptr());
let mutex = unsafe { CreateMutexW(None, true, pw) }?;
match unsafe { GetLastError() } {
Ok(_) => Ok(mutex),
Err(err) => {
if err == Foundation::ERROR_ALREADY_EXISTS.into() {
bail!("Another installer or updater for this application is running, quit that process and try again.");
} else {
bail!("Unable to create global mutex: {}", err);
}
}
}
}
pub fn is_sub_path<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, parent: P2) -> Result<bool> {
let path = path.as_ref().to_string_lossy().to_lowercase();
let parent = parent.as_ref().to_string_lossy().to_lowercase();
// some quick bails before we do the more expensive path normalization
if path.is_empty() || parent.is_empty() {
return Ok(false);
}
if path.len() < parent.len() {
return Ok(false);
}
if path.starts_with(&parent) {
return Ok(true);
}
let path = w::ExpandEnvironmentStrings(&path)?;
let parent = w::ExpandEnvironmentStrings(&parent)?;
let path = Path::new(&path);
let parent = Path::new(&parent);
// we just bail if paths are not absolute. in the cases where we use this function,
// we should have absolute paths from the file system (eg. iterating running processes, reading shortcuts)
// if we receive a relative path, it's likely coming from a shortcut target/working directory
// that we can't resolve with ExpandEnvironmentStrings
if !path.is_absolute() || !parent.is_absolute() {
return Ok(false);
}
// calls GetFullPathNameW
let path = path.normalize_virtually()?.as_path().to_string_lossy().to_lowercase();
let parent = parent.normalize_virtually()?.as_path().to_string_lossy().to_lowercase();
let path = PathBuf::from(path);
let parent = PathBuf::from(parent);
// use path.starts_with instead of string.starts_with because it compares by path component
Ok(path.starts_with(parent))
}
#[test]
fn test_is_sub_path_works_with_existing_paths() {
let path = PathBuf::from(r"C:\Windows\System32/dxdiag.exe");
let parent = PathBuf::from(r"c:\windows/system32\");
assert!(is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from(r"C:\Windows\System32/dxdiag.exe");
let parent = PathBuf::from(r"c:\windows/");
assert!(is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from(r"C:\Windows\System32/dxdiag.exe");
let parent = PathBuf::from(r"c:\windows\");
assert!(is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from(r"C:\Windows\System32/dxdiag.exe");
let parent = PathBuf::from(r"c:\windows");
assert!(is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from(r"C:\Windows\System32/dxdiag.exe");
let parent = PathBuf::from(r"c:/");
assert!(is_sub_path(&path, &parent).unwrap());
}
#[test]
fn test_is_sub_path_works_with_non_existing_paths() {
let path = PathBuf::from(r"C:\Some/Non-existing\Path/Whatever.exe");
let parent = PathBuf::from(r"c:\some\non-existing/path\");
assert!(is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from(r"C:\Some/Non-existing\Path/Whatever.exe");
let parent = PathBuf::from(r"c:\some\non-existing/path/");
assert!(is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from(r"C:\Some/Non-existing\Path/Whatever.exe");
let parent = PathBuf::from(r"c:\some/non-existing/");
assert!(is_sub_path(&path, &parent).unwrap());
}
#[test]
fn test_is_sub_path_works_with_env_var_paths_and_avoids_current_dir() {
let path = PathBuf::from(r"C:\Windows\System32\cmd.exe");
let parent = PathBuf::from(r"%windir%");
assert!(is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from(r"C:\Source\rust setup testing\install");
let parent = PathBuf::from(r"%windir%\system32");
assert!(!is_sub_path(&path, &parent).unwrap());
let path = r"%windir%\system32";
let parent = std::env::current_dir().unwrap().to_string_lossy().to_string();
assert!(!is_sub_path(&path, &parent).unwrap());
assert!(!is_sub_path(&parent, &path).unwrap());
}
#[test]
fn test_is_sub_path_works_with_empty_paths() {
let path = PathBuf::from(r"C:\Windows\Path.exe");
let parent = PathBuf::from("");
assert!(!is_sub_path(&path, &parent).unwrap());
let path = PathBuf::from("");
let parent = PathBuf::from(r"c:\some\non-existing/path/");
assert!(!is_sub_path(&path, &parent).unwrap());
}
pub fn is_os_version_or_greater(version: &str) -> Result<bool> {
let (mut major, mut minor, mut build, _) = util::parse_version(version)?;
if major < 8 {
return Ok(w::IsWindows7OrGreater()?);
}
if major == 8 {
return Ok(if minor >= 1 { w::IsWindows8Point1OrGreater()? } else { w::IsWindows8OrGreater()? });
}
// https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions
if major == 11 {
if build < 22000 {
build = 22000;
}
major = 10;
minor = 0;
}
if major == 10 && build <= 0 {
return Ok(w::IsWindows10OrGreater()?);
}
let mut mask: u64 = 0;
mask = w::VerSetConditionMask(mask, co::VER_MASK::MAJORVERSION, co::VER_COND::GREATER_EQUAL);
mask = w::VerSetConditionMask(mask, co::VER_MASK::MINORVERSION, co::VER_COND::GREATER_EQUAL);
mask = w::VerSetConditionMask(mask, co::VER_MASK::BUILDNUMBER, co::VER_COND::GREATER_EQUAL);
let mut osvi: w::OSVERSIONINFOEX = Default::default();
osvi.dwMajorVersion = major;
osvi.dwMinorVersion = minor;
osvi.dwBuildNumber = build;
return Ok(w::VerifyVersionInfo(&mut osvi, co::VER_MASK::MAJORVERSION | co::VER_MASK::MINORVERSION | co::VER_MASK::BUILDNUMBER, mask)?);
}
#[test]
pub fn test_os_returns_true_for_everything_on_windows_11_and_below() {
assert!(is_os_version_or_greater("6").unwrap());
assert!(is_os_version_or_greater("7").unwrap());
assert!(is_os_version_or_greater("8").unwrap());
assert!(is_os_version_or_greater("8.1").unwrap());
assert!(is_os_version_or_greater("10").unwrap());
assert!(is_os_version_or_greater("10.0.20000").unwrap());
assert!(is_os_version_or_greater("11").unwrap());
assert!(!is_os_version_or_greater("12").unwrap());
}
pub fn get_processes_running_in_directory(dir: &PathBuf) -> Result<HashMap<u32, PathBuf>> {
let mut oup = HashMap::new();
let mut hpl = w::HPROCESSLIST::CreateToolhelp32Snapshot(co::TH32CS::SNAPPROCESS, None)?;
for proc_entry in hpl.iter_processes() {
if let Ok(proc) = proc_entry {
let process = w::HPROCESS::OpenProcess(co::PROCESS::QUERY_LIMITED_INFORMATION, false, proc.th32ProcessID);
if process.is_err() {
continue;
}
let process = process.unwrap();
let full_path = process.QueryFullProcessImageName(co::PROCESS_NAME::WIN32);
if full_path.is_err() {
continue;
}
let full_path = full_path.unwrap();
let full_path = Path::new(&full_path);
if let Ok(is_subpath) = is_sub_path(full_path, dir) {
if is_subpath {
oup.insert(proc.th32ProcessID, full_path.to_path_buf());
}
}
}
}
Ok(oup)
}
pub fn kill_pid(pid: u32) -> Result<()> {
let process = w::HPROCESS::OpenProcess(co::PROCESS::TERMINATE, false, pid)?;
process.TerminateProcess(1)?;
Ok(())
}
pub fn kill_processes_in_directory(dir: &PathBuf) -> Result<()> {
info!("Checking for running processes in: {}", dir.display());
let processes = get_processes_running_in_directory(dir)?;
let my_pid = std::process::id();
for (pid, exe) in processes.iter() {
if *pid == my_pid {
warn!("Skipping killing self: {} ({})", exe.display(), pid);
continue;
}
warn!("Killing process: {} ({})", exe.display(), pid);
kill_pid(*pid)?;
}
Ok(())
}
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub fn run_process_no_console_and_wait<S, P>(exe: S, args: Vec<&str>, work_dir: P, timeout: Option<Duration>) -> Result<String>
where
S: AsRef<OsStr>,
P: AsRef<Path>,
{
let mut cmd = Process::new(exe)
.args(args)
.current_dir(work_dir)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.creation_flags(CREATE_NO_WINDOW)
.spawn()?;
fn check_process_status_and_output(status: std::process::ExitStatus, mut cmd: std::process::Child) -> Result<String> {
let mut stdout = cmd.stdout.take().unwrap();
let mut stderr = cmd.stderr.take().unwrap();
let mut stdout_buf = Vec::new();
stdout.read_to_end(&mut stdout_buf)?;
stderr.read_to_end(&mut stdout_buf)?;
if !status.success() {
warn!("Process exited with non-zero exit code: {}", status.code().unwrap_or(0));
if stdout_buf.len() > 0 {
warn!(" Output:\n{}", String::from_utf8_lossy(&stdout_buf));
}
return Err(anyhow!("Process exited with non-zero exit code: {}", status.code().unwrap_or(0)));
}
Ok(String::from_utf8_lossy(&stdout_buf).to_string())
}
if let Some(t) = timeout {
match cmd.wait_timeout(t) {
Ok(Some(status)) => check_process_status_and_output(status, cmd),
Ok(None) => {
cmd.kill()?;
return Err(anyhow!("Process timed out after {:?}", t));
}
Err(e) => return Err(e.into()),
}
} else {
let status = cmd.wait()?;
check_process_status_and_output(status, cmd)
}
}
pub fn run_process_no_console<S, P>(exe: S, args: Vec<&str>, work_dir: P) -> Result<()>
where
S: AsRef<OsStr>,
P: AsRef<Path>,
{
Process::new(exe).args(args).current_dir(work_dir).creation_flags(CREATE_NO_WINDOW).spawn()?;
Ok(())
}
pub fn run_process_no_console_raw_args<S, P>(exe: S, args: &str, work_dir: P) -> Result<()>
where
S: AsRef<OsStr>,
P: AsRef<Path>,
{
Process::new(exe).raw_arg(args).current_dir(work_dir).creation_flags(CREATE_NO_WINDOW).spawn()?;
Ok(())
}
pub fn run_process<S, P>(exe: S, args: Vec<&str>, work_dir: P) -> Result<()>
where
S: AsRef<OsStr>,
P: AsRef<Path>,
{
Process::new(exe).args(args).current_dir(work_dir).spawn()?;
Ok(())
}
pub fn run_process_raw_args<S, P>(exe: S, args: &str, work_dir: P) -> Result<()>
where
S: AsRef<OsStr>,
P: AsRef<Path>,
{
Process::new(exe).raw_arg(args).current_dir(work_dir).spawn()?;
Ok(())
}
#[test]
fn test_get_running_processes_finds_cargo() {
let profile = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Profile, co::KF::DONT_UNEXPAND, None).unwrap();
let path = Path::new(&profile);
let rustup = path.join(".rustup");
let processes = get_processes_running_in_directory(&rustup).unwrap();
assert!(processes.len() > 0);
let mut found = false;
for (_pid, exe) in processes.iter() {
if exe.ends_with("cargo.exe") {
found = true;
}
}
assert!(found);
}
pub fn is_cpu_architecture_supported(architecture: &str) -> Result<bool> {
let info = os_info::get();
let machine = info.architecture();
if machine.is_none() {
return Ok(true); // we can't detect current arch so try installing anyway.
}
let mut machine = machine.unwrap();
let is_win_11 = is_os_version_or_greater("11")?;
if machine.is_empty() || architecture.is_empty() {
return Ok(true);
}
// https://github.com/stanislav-tkach/os_info/blob/master/os_info/src/windows/winapi.rs#L82
if machine == "x86_64" {
machine = "x64";
} else if machine == "i386" {
machine = "x86";
} else if machine == "aarch64" {
machine = "arm64";
}
if machine == "x86" {
// windows x86 only supports x86
Ok(architecture == "x86")
} else if machine == "x64" {
// windows x64 only supports x86 and x64
Ok(architecture == "x86" || architecture == "x64")
} else if machine == "arm64" {
// windows arm64 supports x86, and arm64, and only on windows 11 does it support x64
Ok(architecture == "x86" || (architecture == "x64" && is_win_11) || architecture == "arm64")
} else {
// we don't know what this is, so try installing anyway
Ok(true)
}
}
#[test]
pub fn test_x64_and_x86_is_supported_but_not_arm64_or_invalid() {
assert!(!is_cpu_architecture_supported("arm64").unwrap());
assert!(!is_cpu_architecture_supported("invalid").unwrap());
assert!(is_cpu_architecture_supported("x64").unwrap());
assert!(is_cpu_architecture_supported("x86").unwrap());
}
pub fn check_authenticode_signature<P: AsRef<Path>>(path: P) -> Result<bool> {
let path = path.as_ref();
let v = codesign_verify::CodeSignVerifier::for_file(path)
.map_err(|e| anyhow!("Unable to open authenticode verifier for '{}' ({:?})", path.to_string_lossy(), e))?;
let sig = v.verify().map_err(|e| anyhow!("Unable to verify binary signature '{}' ({:?})", path.to_string_lossy(), e))?;
info!("Code signature for '{}' is valid", path.to_string_lossy());
debug!("Subject Name: {:?}", sig.subject_name());
debug!("Issuer Name: {:?}", sig.issuer_name());
debug!("SHA1 Thumbprint: {}", sig.sha1_thumbprint());
debug!("Serial: {:?}", sig.serial());
Ok(true)
}
pub fn assert_can_run_binary_authenticode<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
info!("Verifying authenticode signatures of prospective launch binary...");
let target = check_authenticode_signature(path).unwrap_or(false);
let myself = check_authenticode_signature(std::env::current_exe()?).unwrap_or(false);
debug!("Target ({}) Signature = {}", path.to_string_lossy(), if target { "PASS" } else { "FAIL" });
debug!("My Signature = {}", if target { "PASS" } else { "FAIL" });
if myself && !target {
bail!("This binary is signed, and the target binary is not. Refusing to run.")
}
Ok(())
}
#[test]
#[ignore]
fn test_authenticode() {
crate::util::trace_logger();
assert!(verify_authenticode_against_powershell(r"C:\Windows\System32\notepad.exe"));
assert!(verify_authenticode_against_powershell(r"C:\Windows\System32\cmd.exe"));
assert!(verify_authenticode_against_powershell(r"C:\Users\Caelan\AppData\Local\Programs\Microsoft VS Code\Code.exe"));
assert!(!verify_authenticode_against_powershell(r"C:\Users\Caelan\AppData\Local\Clowd\Update.exe"));
assert!(!verify_authenticode_against_powershell(r"C:\Users\Caelan\.cargo\bin\cargo.exe"));
}
fn verify_authenticode_against_powershell(path: &str) -> bool {
let command = format!("Get-AuthenticodeSignature \"{}\" | select Status -expandproperty Status", path);
let args = command.split_whitespace().collect();
let ps_output = super::run_process_no_console_and_wait("powershell", args, std::env::current_dir().unwrap(), None).unwrap();
let ps_result = ps_output.trim() == "Valid";
let my_result = check_authenticode_signature(path).unwrap_or(false);
assert!(ps_result == my_result);
return my_result;
}

694
src/Rust/src/runtimes.rs Normal file
View File

@@ -0,0 +1,694 @@
use anyhow::{anyhow, bail, Result};
use regex::Regex;
use std::process::Command as Process;
use std::{collections::HashMap, env, fs, path::Path};
use winsafe::{self as w, co, prelude::*};
use crate::{util, download};
const REDIST_2015_2022_X86: &str = "https://aka.ms/vs/17/release/vc_redist.x86.exe";
const REDIST_2015_2022_X64: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
const REDIST_2015_2022_ARM64: &str = "https://aka.ms/vs/17/release/vc_redist.arm64.exe";
const NDP_REG_KEY: &str = "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full";
const UNINSTALL_REG_KEY: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
#[rustfmt::skip]
lazy_static! {
static ref HM_NET_FX: HashMap<String, FullFrameworkInfo> = {
let mut net_fx: HashMap<String, FullFrameworkInfo> = HashMap::new();
// https://learn.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed#detect-net-framework-45-and-later-versions
net_fx.insert("net45".to_owned(), FullFrameworkInfo::new(".NET Framework 4.5", "http://go.microsoft.com/fwlink/?LinkId=397707", 378389));
net_fx.insert("net451".to_owned(), FullFrameworkInfo::new(".NET Framework 4.5.1", "http://go.microsoft.com/fwlink/?LinkId=397707", 378675));
net_fx.insert("net452".to_owned(), FullFrameworkInfo::new(".NET Framework 4.5.2", "http://go.microsoft.com/fwlink/?LinkId=397707", 379893));
net_fx.insert("net46".to_owned(), FullFrameworkInfo::new(".NET Framework 4.6", "http://go.microsoft.com/fwlink/?LinkId=780596", 393295));
net_fx.insert("net461".to_owned(), FullFrameworkInfo::new(".NET Framework 4.6.1", "http://go.microsoft.com/fwlink/?LinkId=780596", 394254));
net_fx.insert("net462".to_owned(), FullFrameworkInfo::new(".NET Framework 4.6.2", "http://go.microsoft.com/fwlink/?LinkId=780596", 394802));
net_fx.insert("net47".to_owned(), FullFrameworkInfo::new(".NET Framework 4.7", "http://go.microsoft.com/fwlink/?LinkId=863262", 460798));
net_fx.insert("net471".to_owned(), FullFrameworkInfo::new(".NET Framework 4.7.1", "http://go.microsoft.com/fwlink/?LinkId=863262", 461308));
net_fx.insert("net472".to_owned(), FullFrameworkInfo::new(".NET Framework 4.7.2", "http://go.microsoft.com/fwlink/?LinkId=863262", 461808));
net_fx.insert("net48".to_owned(), FullFrameworkInfo::new(".NET Framework 4.8", "http://go.microsoft.com/fwlink/?LinkId=2085155", 528040));
net_fx.insert("net481".to_owned(), FullFrameworkInfo::new(".NET Framework 4.8.1", "http://go.microsoft.com/fwlink/?LinkId=2203304", 533320));
net_fx
};
static ref HM_VCREDIST: HashMap<String, VCRedistInfo> = {
let mut vcredist: HashMap<String, VCRedistInfo> = HashMap::new();
vcredist.insert("vcredist100-x86".to_owned(), VCRedistInfo::new("Visual C++ 2010 Redist (x86)", "10.00.40219", RuntimeArch::X86, "https://download.microsoft.com/download/C/6/D/C6D0FD4E-9E53-4897-9B91-836EBA2AACD3/vcredist_x86.exe"));
vcredist.insert("vcredist100-x64".to_owned(), VCRedistInfo::new("Visual C++ 2010 Redist (x64)", "10.00.40219", RuntimeArch::X64, "https://download.microsoft.com/download/A/8/0/A80747C3-41BD-45DF-B505-E9710D2744E0/vcredist_x64.exe"));
vcredist.insert("vcredist110-x86".to_owned(), VCRedistInfo::new("Visual C++ 2012 Redist (x86)", "11.00.61030", RuntimeArch::X86, "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe"));
vcredist.insert("vcredist110-x64".to_owned(), VCRedistInfo::new("Visual C++ 2012 Redist (x64)", "11.00.61030", RuntimeArch::X64, "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe"));
vcredist.insert("vcredist120-x86".to_owned(), VCRedistInfo::new("Visual C++ 2013 Redist (x86)", "12.00.40664", RuntimeArch::X86, "https://aka.ms/highdpimfc2013x86enu"));
vcredist.insert("vcredist120-x64".to_owned(), VCRedistInfo::new("Visual C++ 2013 Redist (x64)", "12.00.40664", RuntimeArch::X64, "https://aka.ms/highdpimfc2013x64enu"));
// from 2015-2022, the binaries are all compatible, so we can always just install the latest version
// https://docs.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022
// https://docs.microsoft.com/en-us/cpp/porting/binary-compat-2015-2017?view=msvc-170
vcredist.insert("vcredist140-x86".to_owned(), VCRedistInfo::new("Visual C++ 2015 Redist (x86)", "14.00.23506", RuntimeArch::X86, REDIST_2015_2022_X86));
vcredist.insert("vcredist140-x64".to_owned(), VCRedistInfo::new("Visual C++ 2015 Redist (x64)", "14.00.23506", RuntimeArch::X64, REDIST_2015_2022_X64));
vcredist.insert("vcredist141-x86".to_owned(), VCRedistInfo::new("Visual C++ 2017 Redist (x86)", "14.15.26706", RuntimeArch::X86, REDIST_2015_2022_X86));
vcredist.insert("vcredist141-x64".to_owned(), VCRedistInfo::new("Visual C++ 2017 Redist (x64)", "14.15.26706", RuntimeArch::X64, REDIST_2015_2022_X64));
vcredist.insert("vcredist142-x86".to_owned(), VCRedistInfo::new("Visual C++ 2019 Redist (x86)", "14.20.27508", RuntimeArch::X86, REDIST_2015_2022_X86));
vcredist.insert("vcredist142-x64".to_owned(), VCRedistInfo::new("Visual C++ 2019 Redist (x64)", "14.20.27508", RuntimeArch::X64, REDIST_2015_2022_X64));
vcredist.insert("vcredist143-x86".to_owned(), VCRedistInfo::new("Visual C++ 2022 Redist (x86)", "14.30.30704", RuntimeArch::X86, REDIST_2015_2022_X86));
vcredist.insert("vcredist143-x64".to_owned(), VCRedistInfo::new("Visual C++ 2022 Redist (x64)", "14.30.30704", RuntimeArch::X64, REDIST_2015_2022_X64));
vcredist.insert("vcredist143-arm64".to_owned(), VCRedistInfo::new("Visual C++ 2022 Redist (arm64)", "14.30.30704", RuntimeArch::Arm64, REDIST_2015_2022_ARM64));
vcredist
};
}
#[derive(PartialEq, Debug, Clone, strum::IntoStaticStr)]
pub enum RuntimeArch {
X86,
X64,
Arm64,
}
impl RuntimeArch {
pub fn from_str(arch_str: &str) -> Option<Self> {
match arch_str.to_lowercase().as_str() {
"x86" => Some(RuntimeArch::X86),
"x64" => Some(RuntimeArch::X64),
"arm64" => Some(RuntimeArch::Arm64),
_ => None,
}
}
pub fn from_current_system() -> Option<Self> {
let info = os_info::get();
let machine = info.architecture();
if machine.is_none() {
return None;
}
let mut machine = machine.unwrap();
if machine.is_empty() {
return None;
}
if machine == "x86_64" {
machine = "x64";
} else if machine == "i386" {
machine = "x86";
} else if machine == "aarch64" {
machine = "arm64";
}
Self::from_str(machine)
}
}
#[test]
fn test_cpu_arch_from_str() {
assert_eq!(RuntimeArch::from_str("x86"), Some(RuntimeArch::X86));
assert_eq!(RuntimeArch::from_str("x64"), Some(RuntimeArch::X64));
assert_eq!(RuntimeArch::from_str("arm64"), Some(RuntimeArch::Arm64));
assert_eq!(RuntimeArch::from_str("foo"), None);
assert_eq!(RuntimeArch::from_str("X86"), Some(RuntimeArch::X86));
assert_eq!(RuntimeArch::from_str("X64"), Some(RuntimeArch::X64));
assert_eq!(RuntimeArch::from_str("ARM64"), Some(RuntimeArch::Arm64));
}
#[derive(Debug, PartialEq, Clone, strum::IntoStaticStr)]
pub enum RuntimeInstallResult {
InstallSuccess,
RestartRequired,
}
fn def_installer_routine(installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
let mut args = Vec::new();
args.push("/norestart");
if quiet {
args.push("/q");
} else {
args.push("/passive");
args.push("/showrmui");
}
info!("Running installer: '{}', args={:?}", installer_path, args);
let mut cmd = Process::new(installer_path).args(&args).spawn()?;
let result: i32 = cmd.wait()?.code().ok_or_else(|| anyhow!("Unable to get installer exit code."))?;
// https://johnkoerner.com/install/windows-installer-error-codes/
match result {
0 => Ok(RuntimeInstallResult::InstallSuccess), // success
1602 => Err(anyhow!("User cancelled installation.")),
1618 => Err(anyhow!("Another installation is already in progress.")),
3010 => Ok(RuntimeInstallResult::RestartRequired), // success, restart required
5100 => Err(anyhow!("System does not meet runtime requirements.")),
1638 => Ok(RuntimeInstallResult::InstallSuccess), // a newer compatible version is already installed
1641 => Ok(RuntimeInstallResult::RestartRequired), // installer initiated a restart
_ => Err(anyhow!("Installer failed with exit code: {}", result)),
}
}
pub trait RuntimeInfo {
fn get_exe_name(&self) -> String;
fn display_name(&self) -> &str;
fn is_installed(&self) -> bool;
fn get_download_url(&self) -> Result<String>;
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult>;
}
#[derive(Clone, Debug)]
pub struct FullFrameworkInfo {
display_name: String,
download_url: String,
release_version: u32,
}
impl FullFrameworkInfo {
pub fn new(display_name: &str, download_url: &str, release_version: u32) -> Self {
FullFrameworkInfo { display_name: display_name.to_string(), download_url: download_url.to_string(), release_version }
}
}
impl RuntimeInfo for FullFrameworkInfo {
fn get_exe_name(&self) -> String {
format!("inst-netfx-{}.exe", self.release_version)
}
fn display_name(&self) -> &str {
&self.display_name
}
fn get_download_url(&self) -> Result<String> {
Ok(self.download_url.to_owned())
}
fn is_installed(&self) -> bool {
let lm = w::HKEY::LOCAL_MACHINE;
let key = lm.RegOpenKeyEx(Some(NDP_REG_KEY), co::REG_OPTION::NoValue, co::KEY::READ);
if key.is_err() {
// key doesn't exist, so .net framework not installed
return false;
}
let release = key.unwrap().RegGetValue(None, Some("Release"));
if release.is_err() {
// key doesn't exist, so .net framework not installed
return false;
}
match release.unwrap() {
w::RegistryValue::Dword(v) => return v >= self.release_version,
_ => return false,
}
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
def_installer_routine(installer_path, quiet)
}
}
#[derive(Clone, Debug)]
pub struct VCRedistInfo {
display_name: String,
download_url: String,
min_version: String,
architecture: RuntimeArch,
}
impl VCRedistInfo {
pub fn new(display_name: &str, min_version: &str, architecture: RuntimeArch, download_url: &str) -> Self {
VCRedistInfo { display_name: display_name.to_string(), min_version: min_version.to_string(), architecture, download_url: download_url.to_string() }
}
}
impl RuntimeInfo for VCRedistInfo {
fn get_exe_name(&self) -> String {
let my_arch: &'static str = self.clone().architecture.into();
format!("inst-vcredist-{}-{}.exe", self.min_version, my_arch)
}
fn display_name(&self) -> &str {
&self.display_name
}
fn get_download_url(&self) -> Result<String> {
Ok(self.download_url.to_owned())
}
fn is_installed(&self) -> bool {
let mut installed_programs = HashMap::new();
get_installed_programs(&mut installed_programs, co::KEY::READ | co::KEY::WOW64_32KEY);
get_installed_programs(&mut installed_programs, co::KEY::READ | co::KEY::WOW64_64KEY);
let (my_major, my_minor, my_build, _) = util::parse_version(&self.min_version).unwrap();
let reg = Regex::new(r"(?i)Microsoft Visual C\+\+(.*)Redistributable").unwrap();
for (k, v) in installed_programs {
if reg.is_match(&k) {
let lower_name = k.to_lowercase();
let my_arch: &'static str = self.clone().architecture.into();
let my_arch = my_arch.to_lowercase();
if lower_name.contains(&my_arch) {
// this is a vcredist of the same processor architecture. check if version satisfies our requirement
let (major, minor, build, _) = util::parse_version(&v).unwrap();
if my_major == major && minor >= my_minor && build >= my_build {
return true;
}
}
}
}
false
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
def_installer_routine(installer_path, quiet)
}
}
fn get_installed_programs(map: &mut HashMap<String, String>, access_rights: co::KEY) {
let key = w::HKEY::LOCAL_MACHINE.RegOpenKeyEx(Some(UNINSTALL_REG_KEY), co::REG_OPTION::NoValue, access_rights);
if let Ok(view) = key {
if let Ok(iter) = view.RegEnumKeyEx() {
for key_result in iter {
if let Ok(key_name) = key_result {
let subkey = view.RegOpenKeyEx(Some(&key_name), co::REG_OPTION::NoValue, access_rights);
if subkey.is_err() {
continue;
}
let subkey = subkey.unwrap();
let name = subkey.RegQueryValueEx(Some("DisplayName"));
let version = subkey.RegQueryValueEx(Some("DisplayVersion"));
if name.is_ok() && version.is_ok() {
if let w::RegistryValue::Sz(display_name) = name.unwrap() {
if let w::RegistryValue::Sz(display_version) = version.unwrap() {
map.insert(display_name, display_version);
}
}
}
}
}
}
}
}
#[test]
fn test_get_installed_programs_returns_visual_studio() {
let mut map = HashMap::new();
get_installed_programs(&mut map, co::KEY::READ | co::KEY::WOW64_64KEY);
assert!(map.contains_key("Microsoft Visual Studio Installer"));
}
#[test]
fn test_vcredist_is_installed_finds_vcredist143_but_not_arm64() {
let vc143 = HM_VCREDIST.get("vcredist143-x64").unwrap();
assert!(vc143.is_installed());
let vc143 = HM_VCREDIST.get("vcredist143-arm64").unwrap();
assert!(!vc143.is_installed());
}
#[derive(PartialEq, Debug, Clone, strum::IntoStaticStr)]
pub enum DotnetRuntimeType {
Runtime,
AspNetCore,
WindowsDesktop,
}
impl DotnetRuntimeType {
pub fn from_str(runtime_str: &str) -> Option<Self> {
match runtime_str.to_lowercase().as_str() {
"runtime" => Some(DotnetRuntimeType::Runtime),
"aspnetcore" => Some(DotnetRuntimeType::AspNetCore),
"asp" => Some(DotnetRuntimeType::AspNetCore),
"aspcore" => Some(DotnetRuntimeType::AspNetCore),
"windowsdesktop" => Some(DotnetRuntimeType::WindowsDesktop),
"desktop" => Some(DotnetRuntimeType::WindowsDesktop),
_ => None,
}
}
}
#[test]
fn test_dotnet_runtime_type_from_str() {
assert_eq!(DotnetRuntimeType::from_str("runtime"), Some(DotnetRuntimeType::Runtime));
assert_eq!(DotnetRuntimeType::from_str("aspnetcore"), Some(DotnetRuntimeType::AspNetCore));
assert_eq!(DotnetRuntimeType::from_str("asp"), Some(DotnetRuntimeType::AspNetCore));
assert_eq!(DotnetRuntimeType::from_str("aspcore"), Some(DotnetRuntimeType::AspNetCore));
assert_eq!(DotnetRuntimeType::from_str("windowsdesktop"), Some(DotnetRuntimeType::WindowsDesktop));
assert_eq!(DotnetRuntimeType::from_str("desktop"), Some(DotnetRuntimeType::WindowsDesktop));
assert_eq!(DotnetRuntimeType::from_str("foo"), None);
assert_eq!(DotnetRuntimeType::from_str("RUNTIME"), Some(DotnetRuntimeType::Runtime));
assert_eq!(DotnetRuntimeType::from_str("ASPNETCORE"), Some(DotnetRuntimeType::AspNetCore));
assert_eq!(DotnetRuntimeType::from_str("ASP"), Some(DotnetRuntimeType::AspNetCore));
assert_eq!(DotnetRuntimeType::from_str("ASPCORE"), Some(DotnetRuntimeType::AspNetCore));
assert_eq!(DotnetRuntimeType::from_str("WINDOWSDESKTOP"), Some(DotnetRuntimeType::WindowsDesktop));
assert_eq!(DotnetRuntimeType::from_str("DESKTOP"), Some(DotnetRuntimeType::WindowsDesktop));
}
#[derive(Clone, Debug)]
pub struct DotnetInfo {
display_name: String,
version: String,
architecture: RuntimeArch,
runtime_type: DotnetRuntimeType,
}
fn get_dotnet_base_path(runtime_arch: RuntimeArch, runtime_type: DotnetRuntimeType) -> Result<String> {
let system = RuntimeArch::from_current_system();
if system.is_none() {
bail!("Unable to determine system architecture.");
}
let system = system.unwrap();
let dotnet_path = match runtime_type {
DotnetRuntimeType::Runtime => "shared\\Microsoft.NETCore.App",
DotnetRuntimeType::AspNetCore => "shared\\Microsoft.AspNetCore.App",
DotnetRuntimeType::WindowsDesktop => "shared\\Microsoft.WindowsDesktop.App",
};
// it's easy to check if we're looking for x86 dotnet because it's always in the same place.
if runtime_arch == RuntimeArch::X86 {
let pf32 = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::ProgramFilesX86, co::KF::DONT_UNEXPAND, None)?;
let join = Path::new(&pf32).join("dotnet").join(dotnet_path);
let result = join.to_str().ok_or_else(|| anyhow!("Unable to convert path to string."))?;
return Ok(result.to_string());
}
// this only works in a 64 bit process, otherwise it points to ProgramFilesX86
let mut pf64 = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::ProgramFilesX64, co::KF::DONT_UNEXPAND, None)?;
// try to get the real 64 bit program files directory via ProgramW6432
let pf64compat = env::var("ProgramW6432");
if pf64compat.is_ok() {
let puw = pf64compat.unwrap();
if Path::new(&puw).exists() {
pf64 = puw;
}
}
// looking for x64 on an x64 system will always be in pf64.
// it's the same when looking for arm64 on an arm64 system
// if looking for x64 on an arm64 system, it will be in a sub-directory
if runtime_arch == system {
let join = Path::new(&pf64).join("dotnet").join(dotnet_path);
let result = join.to_str().ok_or_else(|| anyhow!("Unable to convert path to string."))?;
return Ok(result.to_string());
} else if runtime_arch == RuntimeArch::X64 && system == RuntimeArch::Arm64 {
let join = Path::new(&pf64).join("dotnet").join("x64").join(dotnet_path);
let result = join.to_str().ok_or_else(|| anyhow!("Unable to convert path to string."))?;
return Ok(result.to_string());
} else {
bail!("Unable to determine dotnet base path.");
}
}
fn list_subfolders(path: &str) -> Vec<String> {
let mut folders = Vec::new();
if let Ok(entries) = fs::read_dir(path) {
for entry in entries {
if let Ok(entry) = entry {
if let Ok(metadata) = entry.metadata() {
if metadata.is_dir() {
if let Some(folder_name) = entry.path().file_name() {
if let Some(folder_name_str) = folder_name.to_str() {
folders.push(folder_name_str.to_string());
}
}
}
}
}
}
}
folders
}
#[test]
fn test_get_dotnet_base_path() {
let path = get_dotnet_base_path(RuntimeArch::X86, DotnetRuntimeType::Runtime).unwrap();
assert_eq!(path, "C:\\Program Files (x86)\\dotnet\\shared\\Microsoft.NETCore.App");
let path = get_dotnet_base_path(RuntimeArch::X64, DotnetRuntimeType::Runtime).unwrap();
assert_eq!(path, "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App");
let path = get_dotnet_base_path(RuntimeArch::X86, DotnetRuntimeType::AspNetCore).unwrap();
assert_eq!(path, "C:\\Program Files (x86)\\dotnet\\shared\\Microsoft.AspNetCore.App");
let path = get_dotnet_base_path(RuntimeArch::X64, DotnetRuntimeType::AspNetCore).unwrap();
assert_eq!(path, "C:\\Program Files\\dotnet\\shared\\Microsoft.AspNetCore.App");
let path = get_dotnet_base_path(RuntimeArch::X86, DotnetRuntimeType::WindowsDesktop).unwrap();
assert_eq!(path, "C:\\Program Files (x86)\\dotnet\\shared\\Microsoft.WindowsDesktop.App");
let path = get_dotnet_base_path(RuntimeArch::X64, DotnetRuntimeType::WindowsDesktop).unwrap();
assert_eq!(path, "C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App");
}
impl RuntimeInfo for DotnetInfo {
fn get_exe_name(&self) -> String {
let my_arch: &'static str = self.clone().architecture.into();
let my_type: &'static str = self.clone().runtime_type.into();
format!("inst-dotnet-{}-{}-{}.exe", self.version, my_arch, my_type)
}
fn display_name(&self) -> &str {
&self.display_name
}
fn get_download_url(&self) -> Result<String> {
let uncached_feed = "https://dotnetcli.blob.core.windows.net/dotnet";
let dotnet_feed = "https://dotnetcli.azureedge.net/dotnet";
let (major, minor, _, _) = util::parse_version(&self.version)?;
let runtime_str = match self.runtime_type {
DotnetRuntimeType::Runtime => "dotnet",
DotnetRuntimeType::AspNetCore => "aspnetcore",
DotnetRuntimeType::WindowsDesktop => "WindowsDesktop",
};
let get_latest_url = format!("{uncached_feed}/{runtime_str}/{major}.{minor}/latest.version");
let version = download::download_url_as_string(&get_latest_url)?;
let cpu_arch_str = match self.architecture {
RuntimeArch::X86 => "x86",
RuntimeArch::X64 => "x64",
RuntimeArch::Arm64 => "arm64",
};
let download_url = match self.runtime_type {
DotnetRuntimeType::Runtime => format!("{}/Runtime/{}/dotnet-runtime-{}-win-{}.exe", dotnet_feed, version, version, cpu_arch_str),
DotnetRuntimeType::AspNetCore => format!("{}/aspnetcore/Runtime/{}/aspnetcore-runtime-{}-win-{}.exe", dotnet_feed, version, version, cpu_arch_str),
DotnetRuntimeType::WindowsDesktop => {
format!("{}/WindowsDesktop/{}/windowsdesktop-runtime-{}-win-{}.exe", dotnet_feed, version, version, cpu_arch_str)
}
};
Ok(download_url)
}
fn is_installed(&self) -> bool {
let base_path = get_dotnet_base_path(self.architecture.clone(), self.runtime_type.clone());
if base_path.is_err() {
return false;
}
let base_path = base_path.unwrap();
let installed_versions = list_subfolders(&base_path);
let (my_major, my_minor, my_build, _) = util::parse_version(&self.version).unwrap();
for i in 0..installed_versions.len() {
let v = installed_versions.get(i).unwrap();
let pv = util::parse_version(v);
let (major, minor, build, _) = match pv {
Ok(v) => v,
Err(_) => continue,
};
if my_major == major && my_minor == minor && build >= my_build {
return true;
}
}
false
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
def_installer_routine(installer_path, quiet)
}
}
#[test]
fn test_dotnet_resolves_latest_version() {
// dotnet 5.0 is EOL so 5.0.17 should always be the latest.
assert_eq!(
parse_dotnet_version("net5.0").unwrap().get_download_url().unwrap(),
"https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/5.0.17/windowsdesktop-runtime-5.0.17-win-x64.exe"
);
}
#[test]
fn test_dotnet_detects_installed_versions() {
assert!(parse_dotnet_version("net6-runtime").unwrap().is_installed());
assert!(parse_dotnet_version("net6-desktop").unwrap().is_installed());
assert!(parse_dotnet_version("net6-asp").unwrap().is_installed());
assert!(!parse_dotnet_version("net9").unwrap().is_installed());
}
lazy_static! {
static ref REGEX_DOTNET: Regex =
Regex::new(r"^net(?:coreapp)?(?<version>(?P<major>\d+)(\.(?P<minor>\d+))?(\.(?P<build>\d+))?)(?:-(?<arch>[a-zA-Z]+\d\d))?(?:-(?<type>[a-zA-Z]+))?$")
.unwrap();
}
fn parse_dotnet_version(version: &str) -> Result<DotnetInfo> {
let caps = REGEX_DOTNET.captures(version).ok_or_else(|| anyhow!("Invalid dotnet version string: '{}'", version))?;
let version_str = caps.name("version").ok_or_else(|| anyhow!("Invalid dotnet version string: '{}'", version))?.as_str();
let architecture_str = caps.name("arch").map(|m| m.as_str()).unwrap_or("x64");
let runtime_type_str = caps.name("type").map(|m| m.as_str()).unwrap_or("desktop");
let (major, minor, build, revision) = util::parse_version(version_str)?; // validate it's valid version string
if major < 5 {
bail!("Only dotnet 5 and greater is supported.");
}
if revision != 0 {
bail!("Invalid dotnet version string: '{}'", version);
}
let architecture = RuntimeArch::from_str(architecture_str).ok_or_else(|| anyhow!("Invalid dotnet version string: '{}'", version))?;
let runtime_type = DotnetRuntimeType::from_str(runtime_type_str).ok_or_else(|| anyhow!("Invalid dotnet version string: '{}'", version))?;
let version_str = format!("{}.{}.{}", major, minor, build);
let display_name = format!(".NET {} {:?} {:?}", version_str, architecture, runtime_type);
Ok(DotnetInfo { display_name, version: version_str, architecture, runtime_type })
}
pub fn parse_dependency_list(list: &str) -> Vec<Box<dyn RuntimeInfo>> {
let mut vec: Vec<Box<dyn RuntimeInfo>> = Vec::new();
for dep in list.split(',') {
let dep = dep.trim();
if dep.is_empty() {
continue;
}
if let Some(info) = HM_NET_FX.get(dep) {
vec.push(Box::new(info.clone()));
} else if let Some(info) = HM_VCREDIST.get(dep) {
vec.push(Box::new(info.clone()));
} else if let Ok(info) = parse_dotnet_version(dep) {
vec.push(Box::new(info));
}
}
vec
}
#[test]
fn test_parse_dotnet_display_name() {
let info = parse_dotnet_version("net5.0").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 X64 WindowsDesktop");
let info = parse_dotnet_version("net5.0-x64").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 X64 WindowsDesktop");
let info = parse_dotnet_version("net5.0-x86").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 X86 WindowsDesktop");
let info = parse_dotnet_version("net5.0-arm64").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 Arm64 WindowsDesktop");
let info = parse_dotnet_version("net5.0-desktop").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 X64 WindowsDesktop");
let info = parse_dotnet_version("net5.0-runtime").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 X64 Runtime");
let info = parse_dotnet_version("net5.0-x86-runtime").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 X86 Runtime");
let info = parse_dotnet_version("net5.0-arm64-runtime").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 Arm64 Runtime");
let info = parse_dotnet_version("net5.0-asp").unwrap();
assert_eq!(info.display_name, ".NET 5.0.0 X64 AspNetCore");
let info = parse_dotnet_version("net6.0.2").unwrap();
assert_eq!(info.display_name, ".NET 6.0.2 X64 WindowsDesktop");
}
#[test]
fn test_parse_dotnet_version() {
let info = parse_dotnet_version("net5.0").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net5.0-x64").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net5.0-x86").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X86);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net5.0-arm64").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::Arm64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net5.0-desktop").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net5.0-runtime").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::Runtime);
let info = parse_dotnet_version("net5.0-x86-runtime").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X86);
assert_eq!(info.runtime_type, DotnetRuntimeType::Runtime);
let info = parse_dotnet_version("net5.0-arm64-runtime").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::Arm64);
assert_eq!(info.runtime_type, DotnetRuntimeType::Runtime);
let info = parse_dotnet_version("net5.0-asp").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::AspNetCore);
let info = parse_dotnet_version("net5.0-x86-asp").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::X86);
assert_eq!(info.runtime_type, DotnetRuntimeType::AspNetCore);
let info = parse_dotnet_version("net5.0-arm64-asp").unwrap();
assert_eq!(info.version, "5.0.0");
assert_eq!(info.architecture, RuntimeArch::Arm64);
assert_eq!(info.runtime_type, DotnetRuntimeType::AspNetCore);
let info = parse_dotnet_version("net7.0").unwrap();
assert_eq!(info.version, "7.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net7").unwrap();
assert_eq!(info.version, "7.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net8").unwrap();
assert_eq!(info.version, "8.0.0");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
let info = parse_dotnet_version("net321.321.321").unwrap();
assert_eq!(info.version, "321.321.321");
assert_eq!(info.architecture, RuntimeArch::X64);
assert_eq!(info.runtime_type, DotnetRuntimeType::WindowsDesktop);
}
#[test]
fn test_parse_dotnet_version_returns_error_on_invalid_strings() {
assert!(parse_dotnet_version("net4.0").is_err());
assert!(parse_dotnet_version("net4.0-x86").is_err());
assert!(parse_dotnet_version("net4.0-windowsdesktop").is_err());
assert!(parse_dotnet_version("net4.0-windowsdesktop-x86").is_err());
assert!(parse_dotnet_version("net4.0-aspnetcore").is_err());
assert!(parse_dotnet_version("net4.0-aspnetcore-x86").is_err());
assert!(parse_dotnet_version("net4.0-asp").is_err());
assert!(parse_dotnet_version("net4.0-asp-x86").is_err());
assert!(parse_dotnet_version("net5.0-aspnetcore-x86").is_err());
assert!(parse_dotnet_version("net5.0-asp-x86").is_err());
assert!(parse_dotnet_version("netcoreapp4.8").is_err());
assert!(parse_dotnet_version("net4.8").is_err());
assert!(parse_dotnet_version("net2.5").is_err());
assert!(parse_dotnet_version("asd").is_err());
assert!(parse_dotnet_version("net7.0-x64-base").is_err());
assert!(parse_dotnet_version("net6.0.0.4").is_err());
assert!(parse_dotnet_version("net4.9").is_err());
assert!(parse_dotnet_version("net6-basd").is_err());
assert!(parse_dotnet_version("net6-x64-aakak").is_err());
assert!(parse_dotnet_version("").is_err());
}

353
src/Rust/src/setup.rs Normal file
View File

@@ -0,0 +1,353 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#![allow(dead_code)]
mod bundle;
mod download;
mod runtimes;
mod splash;
mod util;
mod platform;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate simplelog;
use anyhow::{anyhow, bail, Result};
use clap::{arg, value_parser, Command};
use memmap2::Mmap;
use pretty_bytes_rust::pretty_bytes;
use regex::Regex;
use std::{
env,
fs::{self, File},
path::{Path, PathBuf},
time::Duration,
};
use winsafe::{self as w, co};
fn main() -> Result<()> {
let mut arg_config = Command::new("Setup")
.about(format!("Clowd.Squirrel Setup ({}) installs Squirrel applications.\nhttps://github.com/clowd/Clowd.Squirrel", env!("CARGO_PKG_VERSION")))
.arg(arg!(-s --silent "Hides all dialogs and answers 'yes' to all prompts"))
.arg(arg!(-v --verbose "Print debug messages to console"))
.arg(arg!(-l --log <FILE> "Enable file logging and set location").required(false).value_parser(value_parser!(PathBuf)))
.arg(arg!(-t --installto <DIR> "Installation directory to install the application").required(false).value_parser(value_parser!(PathBuf)));
if cfg!(debug_assertions) {
arg_config = arg_config.arg(arg!(-d --debug <FILE> "Debug mode, install from a nupkg file").required(false).value_parser(value_parser!(PathBuf)));
}
let matches = arg_config.get_matches();
let silent = matches.get_flag("silent");
let verbose = matches.get_flag("verbose");
let debug = matches.get_one::<PathBuf>("debug");
let logfile = matches.get_one::<PathBuf>("log");
let installto = matches.get_one::<PathBuf>("installto");
platform::set_silent(silent);
util::setup_logging(logfile, true, verbose)?;
info!("Starting Clowd.Squirrel Setup ({})", env!("CARGO_PKG_VERSION"));
info!(" Location: {:?}", std::env::current_exe()?);
info!(" Silent: {}", silent);
info!(" Verbose: {}", verbose);
info!(" Log: {:?}", logfile);
info!(" Install To: {:?}", installto);
if cfg!(debug_assertions) {
info!(" Debug: {:?}", debug);
}
let res = run(&debug, &installto);
if let Err(e) = &res {
error!("An error has occurred: {}", e);
platform::show_error(format!("An error has occurred: {}", e), "Setup Error".to_string());
}
res?;
Ok(())
}
fn run(debug_pkg: &Option<&PathBuf>, install_to: &Option<&PathBuf>) -> Result<()> {
let osinfo = os_info::get();
info!("OS: {}, Arch={}", osinfo, osinfo.architecture().unwrap_or("unknown"));
if !w::IsWindowsVersionOrGreater(6, 1, 1)? {
bail!("This installer requires Windows 7 SP1 or later and cannot run.");
}
let file = File::open(env::current_exe()?)?;
let mmap = unsafe { Mmap::map(&file)? };
let pkg = bundle::load_bundle_from_mmap(&mmap, debug_pkg)?;
info!("Bundle loaded successfully.");
// find and parse nuspec
info!("Reading package manifest...");
let app = pkg.read_manifest()?;
info!("Package manifest loaded successfully.");
info!(" Package ID: {}", &app.id);
info!(" Package Version: {}", &app.version);
info!(" Package Title: {}", &app.title);
info!(" Package Authors: {}", &app.authors);
info!(" Package Description: {}", &app.description);
info!(" Package Machine Architecture: {}", &app.machine_architecture);
info!(" Package Runtime Dependencies: {}", &app.runtime_dependencies);
let mutex_name = format!("clowdsquirrel-{}", &app.id);
info!("Attempting to open global system mutex: '{}'", &mutex_name);
let _mutex = platform::create_global_mutex(&mutex_name)?;
info!("Checking application pre-requisites...");
let dependencies = runtimes::parse_dependency_list(&app.runtime_dependencies);
let mut missing: Vec<&Box<dyn runtimes::RuntimeInfo>> = Vec::new();
let mut missing_str = String::new();
for i in 0..dependencies.len() {
let dep = &dependencies[i];
if dep.is_installed() {
info!(" {} is already installed.", dep.display_name());
continue;
}
info!(" {} is missing.", dep.display_name());
if !missing.is_empty() {
missing_str += ", ";
}
missing.push(dep);
missing_str += dep.display_name();
}
let splash_bytes = pkg.get_splash_bytes();
if !missing.is_empty() {
if !platform::show_missing_dependencies_dialog(&app, &missing_str) {
error!("User cancelled pre-requisite installation.");
return Ok(());
}
let downloads = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Downloads, co::KF::DONT_UNEXPAND, None)?;
let downloads = Path::new(downloads.as_str());
info!("Downloading {} missing pre-requisites...", missing.len());
let quiet = platform::get_silent();
for i in 0..missing.len() {
let dep = &missing[i];
let url = dep.get_download_url()?;
let exe_name = downloads.join(dep.get_exe_name());
if !exe_name.exists() {
let tx = splash::show_splash_in_new_thread(app.title.to_owned(), splash_bytes.clone(), true);
info!(" Downloading {}...", dep.display_name());
let result = download::download_url_to_file(&url, &exe_name.to_str().unwrap(), |p| {
let _ = tx.send(p);
});
let _ = tx.send(splash::MSG_CLOSE);
result?;
}
info!(" Installing {}...", dep.display_name());
let result = dep.install(exe_name.to_str().unwrap(), quiet)?;
if result == runtimes::RuntimeInstallResult::RestartRequired {
warn!("A restart is required to complete the installation of {}.", dep.display_name());
platform::show_restart_required(&app);
return Ok(());
}
}
}
info!("Determining install directory...");
let (root_path, root_is_default) = if install_to.is_some() {
(install_to.unwrap().clone(), false)
} else {
let appdata = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::LocalAppData, co::KF::DONT_UNEXPAND, None)?;
(Path::new(&appdata).join(&app.id), true)
};
// path needs to exist for future operations (disk space etc)
if !root_path.exists() {
util::retry_io(|| fs::create_dir_all(&root_path))?;
}
let root_path_str = root_path.to_str().unwrap();
info!("Installation Directory: {:?}", root_path_str);
// do we have enough disk space?
let (compressed_size, extracted_size) = pkg.calculate_size();
let required_space = compressed_size + extracted_size + (50 * 1000 * 1000); // archive + squirrel overhead
let mut free_space: u64 = 0;
w::GetDiskFreeSpaceEx(Some(&root_path_str), None, None, Some(&mut free_space))?;
if free_space < required_space {
bail!(
"{} requires at least {} disk space to be installed. There is only {} available.",
&app.title,
pretty_bytes(required_space, None),
pretty_bytes(free_space, None)
);
}
info!("There is {} free space available at destination, this package requires {}.", pretty_bytes(free_space, None), pretty_bytes(required_space, None));
// does this app support this OS / architecture?
if !app.os_min_version.is_empty() && !platform::is_os_version_or_greater(&app.os_min_version)? {
bail!("This application requires Windows {} or later.", &app.os_min_version);
}
if !app.machine_architecture.is_empty() && !platform::is_cpu_architecture_supported(&app.machine_architecture)? {
bail!("This application ({}) does not support your CPU architecture.", &app.machine_architecture);
}
let mut root_path_renamed = String::new();
// does the target directory exist and have files? (eg. already installed)
if !util::is_dir_empty(&root_path) {
// the target directory is not empty, and not dead
if !platform::show_overwrite_repair_dialog(&app, &root_path, root_is_default) {
// user cancelled overwrite prompt
error!("Directory exists, and user cancelled overwrite.");
return Ok(());
}
platform::kill_processes_in_directory(&root_path)
.map_err(|z| anyhow!("Failed to stop application ({}), please close the application and try running the installer again.", z))?;
root_path_renamed = format!("{}_{}", root_path_str, util::random_string(8));
info!("Renaming existing directory to '{}' to allow rollback...", root_path_renamed);
util::retry_io(|| fs::rename(&root_path, &root_path_renamed)).map_err(|_| {
anyhow!(
"Failed to remove existing application directory, please close the application and try running the installer again. \
If the issue persists, try uninstalling first via Programs & Features, or restarting your computer."
)
})?;
}
info!("Preparing and cleaning installation directory...");
remove_dir_all::ensure_empty_dir(&root_path)?;
let tx = splash::show_splash_in_new_thread(app.title.to_owned(), splash_bytes.clone(), true);
let install_result = install_app(&pkg, &root_path, &tx);
let _ = tx.send(splash::MSG_CLOSE);
if install_result.is_ok() {
info!("Installation completed successfully!");
if !root_path_renamed.is_empty() {
info!("Removing rollback directory...");
let _ = util::retry_io(|| fs::remove_dir_all(&root_path_renamed));
}
} else {
error!("Installation failed!");
if !root_path_renamed.is_empty() {
info!("Rolling back installation...");
let _ = platform::kill_processes_in_directory(&root_path);
let _ = util::retry_io(|| fs::remove_dir_all(&root_path));
let _ = util::retry_io(|| fs::rename(&root_path_renamed, &root_path));
}
install_result?;
}
Ok(())
}
fn install_app(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::mpsc::Sender<i16>) -> Result<()> {
info!("Starting installation!");
let app = pkg.read_manifest()?;
// all application paths
let updater_path = app.get_update_path(root_path);
let packages_path = app.get_packages_path(root_path);
let current_path = app.get_current_path(root_path);
let nuspec_path = app.get_nuspec_path(root_path);
let nupkg_path = app.get_target_nupkg_path(root_path);
let main_exe_path = app.get_main_exe_path(root_path);
info!("Extracting Update.exe...");
let updater_idx = pkg
.extract_zip_predicate_to_path(|name| name.ends_with("Squirrel.exe"), updater_path)
.map_err(|_| anyhow!("This installer is missing a critical binary (Update.exe). Please contact the application author."))?;
let _ = tx.send(5);
info!("Extracting bundle manifest...");
let _ = pkg
.extract_zip_predicate_to_path(|name| name.ends_with(".nuspec"), nuspec_path)
.map_err(|_| anyhow!("This installer is missing a nuspec. Please contact the application author."))?;
let _ = tx.send(7);
info!("Copying nupkg to packages directory...");
util::retry_io(|| fs::create_dir_all(&packages_path))?;
pkg.copy_bundle_to_file(&nupkg_path)?;
let _ = tx.send(10);
info!("Extracting {} app files to current directory...", pkg.len());
let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap();
let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap();
let files = pkg.get_file_names()?;
let num_files = files.len();
for (i, key) in files.iter().enumerate() {
if i == updater_idx || !re.is_match(key) || key.ends_with("/") || key.ends_with("\\") {
info!(" {} Skipped '{}'", i, key);
continue;
}
let file_path_in_zip = re.replace(key, "").to_string();
let file_path_on_disk = Path::new(&current_path).join(&file_path_in_zip);
if stub_regex.is_match(&file_path_in_zip) {
// let stub_key = stub_regex.replace(&file_path_in_zip, ".exe").to_string();
// file_path_on_disk = root_path.join(&stub_key);
info!(" {} Skipped Stub (obsolete) '{}'", i, key);
continue;
}
let final_path = file_path_on_disk.to_str().unwrap().replace("/", "\\");
info!(" {} Extracting '{}' to '{}'", i, key, final_path);
pkg.extract_zip_idx_to_path(i, &final_path)?;
let progress = ((i as f32 / num_files as f32) * 80.0) as i16 + 10;
let _ = tx.send(progress);
}
let _ = tx.send(90);
// let folder_size = fs_extra::dir::get_size(&root_path).unwrap();
// info!("{} extracted to {}", pretty_bytes(folder_size, None), root_path_str);
if !Path::new(&main_exe_path).exists() {
bail!("The main executable could not be found in the package. Please contact the application author.");
}
info!("Creating start menu shortcut...");
let _comguard = w::CoInitializeEx(co::COINIT::APARTMENTTHREADED)?;
let startmenu = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None)?;
let lnk_path = Path::new(&startmenu).join("Programs").join(format!("{}.lnk", &app.title));
platform::create_lnk(&lnk_path.to_string_lossy(), &main_exe_path, &current_path)?;
info!("Starting process install hook: \"{}\" --squirrel-install {}", main_exe_path, &app.version);
let args = vec!["--squirrel-install", &app.version];
if let Err(e) = platform::run_process_no_console_and_wait(&main_exe_path, args, &current_path, Some(Duration::from_secs(30))) {
let setup_name = format!("{} Setup {}", app.title, app.version);
error!("Process install hook failed: {}", e);
let _ = tx.send(splash::MSG_CLOSE);
platform::show_warning(
format!("Installation has completed, but the application install hook failed ({}). It may not have installed correctly.", e),
setup_name,
);
}
let _ = tx.send(100);
app.write_uninstall_entry(root_path)?;
if !platform::get_silent() {
info!("Starting app: \"{}\" --squirrel-firstrun", main_exe_path);
let args = vec!["--squirrel-firstrun"];
let _ = platform::run_process(&main_exe_path, args, &current_path);
}
Ok(())
}

346
src/Rust/src/splash.rs Normal file
View File

@@ -0,0 +1,346 @@
use anyhow::{bail, Result};
use image::{codecs::gif::GifDecoder, io::Reader as ImageReader, AnimationDecoder, DynamicImage, ImageFormat};
use std::{
cell::RefCell,
io::Cursor,
rc::Rc,
sync::mpsc::{self, Receiver, Sender},
thread,
time::Duration,
};
use w::WString;
use winsafe::{self as w, co, guard::DeleteObjectGuard, gui, prelude::*};
const TMR_GIF: usize = 1;
const MSG_NOMESSAGE: i16 = -99;
pub const MSG_CLOSE: i16 = -1;
// pub const MSG_INDEFINITE: i16 = -2;
pub fn show_splash_in_new_thread(app_name: String, imgstream: Option<Vec<u8>>, delay: bool) -> Sender<i16> {
let (tx, rx) = mpsc::channel::<i16>();
let tx2 = tx.clone();
thread::spawn(move || {
if delay {
info!("Showing splash screen with 3 second delay...");
// wait a bit, if the MSG_CLOSE message is sent within 3 seconds, we won't bother showing it.
thread::sleep(Duration::from_millis(3000));
// read all messages, checking for MSG_CLOSE, and then send the last progress message we received.
let mut last_progress = 0;
let mut closed = false;
loop {
let msg = rx.try_recv().unwrap_or(MSG_NOMESSAGE);
if msg == MSG_CLOSE {
closed = true;
break;
} else if msg >= 0 {
last_progress = msg;
} else if msg == MSG_NOMESSAGE {
break;
}
}
if closed {
info!("Splash screen received MSG_CLOSE before delay ended, so it wasn't shown.");
return;
}
tx2.send(last_progress).unwrap();
info!("Splash screen delay ended, showing splash window...");
} else {
info!("Showing splash screen immediately...");
}
if imgstream.is_some() {
let _ = SplashWindow::new(app_name, imgstream.unwrap(), rx).and_then(|w| {
w.run()?;
Ok(())
});
} else {
show_com_ctl_progress_dialog(app_name, rx);
}
});
tx
}
#[derive(Clone)]
pub struct SplashWindow {
wnd: gui::WindowMain,
frames: Rc<Vec<DeleteObjectGuard<w::HBITMAP>>>,
rx: Rc<Receiver<i16>>,
delay: u16,
progress: Rc<RefCell<i16>>,
frame_idx: Rc<RefCell<usize>>,
w: u16,
h: u16,
}
fn average(numbers: &[u16]) -> u16 {
let sum: u16 = numbers.iter().sum();
let count = numbers.len() as u16;
sum / count
}
fn convert_rgba_to_bgra(image_data: &mut Vec<u8>) {
for chunk in image_data.chunks_mut(4) {
// Swap red and blue channels
let tmp = chunk[0];
chunk[0] = chunk[2];
chunk[2] = tmp;
}
}
impl SplashWindow {
pub fn new(app_name: String, img_stream: Vec<u8>, rx: Receiver<i16>) -> Result<Self> {
let mut delays = Vec::new();
let mut frames = Vec::new();
let fmt_cursor = Cursor::new(&img_stream);
let fmt_reader = ImageReader::new(fmt_cursor).with_guessed_format()?;
let fmt = fmt_reader.format();
let dims = &fmt_reader.into_dimensions()?;
let w: u16 = u16::try_from(dims.0)?;
let h: u16 = u16::try_from(dims.1)?;
if Some(ImageFormat::Gif) == fmt {
info!("Image is animated GIF ({}x{}), loading frames...", w, h);
let gif_cursor = Cursor::new(&img_stream);
let decoder = GifDecoder::new(gif_cursor)?;
let dec_frames = decoder.into_frames();
for frame in dec_frames.into_iter() {
let frame = frame?;
let (num, dem) = frame.delay().numer_denom_ms();
delays.push((num / dem) as u16);
let dynamic = DynamicImage::from(frame.buffer().to_owned());
let mut vec = dynamic.to_rgba8().to_vec();
convert_rgba_to_bgra(&mut vec);
let bitmap = w::HBITMAP::CreateBitmap(winsafe::SIZE { cx: w.into(), cy: h.into() }, 1, 32, vec.as_mut_ptr() as *mut u8)?;
frames.push(bitmap);
}
info!("Successfully loaded {} frames.", frames.len());
} else {
info!("Loading static image (detected {:?})...", fmt);
delays.push(16); // 60 fps
let img_cursor = Cursor::new(&img_stream);
let img_decoder = ImageReader::new(img_cursor).with_guessed_format()?.decode()?;
let mut vec = img_decoder.to_rgba8().to_vec();
convert_rgba_to_bgra(&mut vec);
let bitmap = w::HBITMAP::CreateBitmap(winsafe::SIZE { cx: w.into(), cy: h.into() }, 1, 32, vec.as_mut_ptr() as *mut u8)?;
frames.push(bitmap);
info!("Successfully loaded.");
}
// TODO: only support a fixed frame delay for now. Maybe should
// support a variable frame delay in the future.
let delay = average(&delays);
let wnd = gui::WindowMain::new(gui::WindowMainOpts {
class_icon: gui::Icon::Idi(co::IDI::APPLICATION),
class_cursor: gui::Cursor::Idc(co::IDC::APPSTARTING),
class_style: co::CS::HREDRAW | co::CS::VREDRAW,
class_name: "SquirrelSetupSplashWindow".to_owned(),
title: app_name,
size: (w.into(), h.into()),
ex_style: co::WS_EX::NoValue,
style: co::WS::POPUP,
..Default::default()
});
let frames = Rc::new(frames);
let rx = Rc::new(rx);
let progress = Rc::new(RefCell::new(0));
let frame_idx = Rc::new(RefCell::new(0));
let mut new_self = Self { wnd, frames, delay, frame_idx, w, h, rx, progress };
new_self.events();
Ok(new_self)
}
pub fn run(&self) -> Result<i32> {
let res = self.wnd.run_main(Some(co::SW::SHOWNOACTIVATE));
if res.is_err() {
error!("Error Showing Splash Window: {:?}", res);
bail!("Error Showing Splash Window: {:?}", res);
} else {
info!("Splash Window Closed");
}
Ok(res.unwrap())
}
fn events(&mut self) {
let self2 = self.clone();
self.wnd.on().wm_create(move |_m| {
// will ask Windows to give us a WM_TIMER every `delay` milliseconds
self2.wnd.hwnd().SetTimer(TMR_GIF, self2.delay.into(), None)?;
Ok(0)
});
self.wnd.on().wm_nc_hit_test(|_m| {
Ok(co::HT::CAPTION) // make the window draggable
});
let self2 = self.clone();
self.wnd.on().wm_timer(TMR_GIF, move || {
// handle any incoming messages before painting
loop {
let msg = self2.rx.try_recv().unwrap_or(MSG_NOMESSAGE);
if msg == MSG_NOMESSAGE {
break;
} else if msg == MSG_CLOSE {
self2.wnd.hwnd().SendMessage(w::msg::wm::Close {});
return Ok(());
} else if msg >= 0 {
let mut p = self2.progress.borrow_mut();
*p = msg;
}
}
// trigger a new WM_PAINT
self2.wnd.hwnd().InvalidateRect(None, true)?;
Ok(())
});
let self2 = self.clone();
self.wnd.on().wm_paint(move || {
let mut idx = self2.frame_idx.borrow_mut();
let h_bitmap = unsafe { self2.frames[*idx].raw_copy() };
*idx += 1;
if *idx >= self2.frames.len() {
*idx = 0;
}
let hwnd = self2.wnd.hwnd();
let rect = hwnd.GetClientRect()?;
let hdc = hwnd.BeginPaint()?;
let hdc_mem = hdc.CreateCompatibleDC()?;
let _old_bitmap = hdc_mem.SelectObject(&h_bitmap)?;
let progress = self2.progress.borrow();
let progress_brush = w::HBRUSH::CreateSolidBrush(w::COLORREF::new(0, 255, 0))?;
let progress_width = (rect.right as f32 * (*progress as f32 / 100.0)) as i32;
let progress_rect = w::RECT { left: 0, bottom: rect.bottom, right: progress_width, top: rect.bottom - 10 };
hdc_mem.FillRect(progress_rect, &progress_brush)?;
hdc.SetStretchBltMode(co::STRETCH_MODE::STRETCH_HALFTONE)?;
hdc.StretchBlt(
w::POINT { x: 0, y: 0 },
w::SIZE { cx: rect.right, cy: rect.bottom },
&hdc_mem,
w::POINT { x: 0, y: 0 },
w::SIZE { cx: self2.w.into(), cy: self2.h.into() },
co::ROP::SRCCOPY,
)?;
Ok(())
});
}
}
// pub const TDM_SET_PROGRESS_BAR_MARQUEE: co::WM = unsafe { co::WM::from_raw(1131) };
// pub const TDM_SET_MARQUEE_PROGRESS_BAR: co::WM = unsafe { co::WM::from_raw(1127) };
pub const TDM_SET_PROGRESS_BAR_POS: co::WM = unsafe { co::WM::from_raw(1130) };
// struct MsgSetProgressMarqueeOnOff {
// is_marquee_on: bool,
// }
// unsafe impl MsgSend for MsgSetProgressMarqueeOnOff {
// type RetType = ();
// fn convert_ret(&self, _: isize) -> Self::RetType {
// ()
// }
// fn as_generic_wm(&mut self) -> w::msg::WndMsg {
// let v: usize = if self.is_marquee_on { 1 } else { 0 };
// w::msg::WndMsg { msg_id: TDM_SET_PROGRESS_BAR_MARQUEE, wparam: v, lparam: 0 }
// }
// }
// struct MsgSetProgressMarqueeMode {
// is_marquee_on: bool,
// }
// unsafe impl MsgSend for MsgSetProgressMarqueeMode {
// type RetType = ();
// fn convert_ret(&self, _: isize) -> Self::RetType {
// ()
// }
// fn as_generic_wm(&mut self) -> w::msg::WndMsg {
// let v: usize = if self.is_marquee_on { 1 } else { 0 };
// w::msg::WndMsg { msg_id: TDM_SET_MARQUEE_PROGRESS_BAR, wparam: v, lparam: 0 }
// }
// }
struct MsgSetProgressPos {
pos: usize,
}
unsafe impl MsgSend for MsgSetProgressPos {
type RetType = ();
fn convert_ret(&self, _: isize) -> Self::RetType {
()
}
fn as_generic_wm(&mut self) -> w::msg::WndMsg {
w::msg::WndMsg { msg_id: TDM_SET_PROGRESS_BAR_POS, wparam: self.pos, lparam: 0 }
}
}
#[derive(Clone)]
pub struct ComCtlProgressWindow {
// hwnd: Rc<RefCell<w::HWND>>,
rx: Rc<Receiver<i16>>,
}
fn show_com_ctl_progress_dialog(app_name: String, rx: Receiver<i16>) {
let mut setup_name = WString::from_str(format!("{} Setup", app_name));
let mut content = WString::from_str("Please Wait...");
let mut config: w::TASKDIALOGCONFIG = Default::default();
config.dwFlags = co::TDF::SIZE_TO_CONTENT | co::TDF::SHOW_PROGRESS_BAR | co::TDF::CALLBACK_TIMER;
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
config.set_pszWindowTitle(Some(&mut setup_name));
config.set_pszMainInstruction(Some(&mut content));
// if (_icon != null) {
// config.dwFlags |= TASKDIALOG_FLAGS.TDF_USE_HICON_MAIN;
// config.mainIcon = _icon.Handle;
// }
let me = ComCtlProgressWindow { rx: Rc::new(rx) };
config.lpCallbackData = &me as *const ComCtlProgressWindow as usize;
config.pfCallback = Some(task_dialog_callback);
let _ = w::TaskDialogIndirect(&config, None);
}
extern "system" fn task_dialog_callback(hwnd: w::HWND, msg: co::TDN, _: usize, _: isize, lp_ref_data: usize) -> co::HRESULT {
let raw = lp_ref_data as *const ComCtlProgressWindow;
let me: &ComCtlProgressWindow = unsafe { &*raw };
// if msg == co::TDN::DIALOG_CONSTRUCTED {
// let mut h = me.hwnd.borrow_mut();
// *h = hwnd;
// }
if msg == co::TDN::BUTTON_CLICKED {
return co::HRESULT::S_FALSE; // TODO, support cancellation
}
if msg == co::TDN::TIMER {
let mut progress: i16 = -1;
loop {
let msg = me.rx.try_recv().unwrap_or(MSG_NOMESSAGE);
if msg == MSG_NOMESSAGE {
break;
} else if msg == MSG_CLOSE {
hwnd.SendMessage(w::msg::wm::Close {});
return co::HRESULT::S_OK;
} else if msg >= 0 {
progress = msg;
}
}
if progress > 0 {
hwnd.SendMessage(MsgSetProgressPos { pos: progress as usize });
}
}
return co::HRESULT::S_OK;
}

401
src/Rust/src/update.rs Normal file
View File

@@ -0,0 +1,401 @@
#![allow(dead_code)]
mod bundle;
mod platform;
mod util;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate simplelog;
use anyhow::{anyhow, bail, Result};
use bundle::Manifest;
use clap::{arg, value_parser, ArgMatches, Command};
use std::fs::File;
use std::path::Path;
use std::time::Duration;
use std::{env, fs, path::PathBuf};
#[rustfmt::skip]
fn root_command() -> Command {
Command::new("Update")
.version(env!("CARGO_PKG_VERSION"))
.about(format!("Clowd.Squirrel Updater ({}) manages packages and installs updates for Squirrel applications.\nhttps://github.com/clowd/Clowd.Squirrel", env!("CARGO_PKG_VERSION")))
.subcommand(Command::new("apply")
.about("Applies a staged / prepared update, installing prerequisite runtimes if necessary")
.arg(arg!(-r --restart "Restart the application after the update"))
.arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update"))
// .arg(arg!(-p --pkg <FILE> "Update package to apply").value_parser(value_parser!(PathBuf)))
)
.subcommand(Command::new("start")
.about("Starts the currently installed version of the application")
.arg(arg!(-a --args <ARGS> "Legacy args format").aliases(vec!["processStartArgs", "process-start-args"]).hide(true).allow_hyphen_values(true).num_args(1))
.arg(arg!(-w --wait "Wait for the parent process to terminate before starting the application"))
.arg(arg!([EXE_NAME] "The optional name of the binary to execute"))
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceeded by '--'.").required(false).last(true).num_args(0..))
.long_flag_aliases(vec!["processStart", "processStartAndWait"])
)
.subcommand(Command::new("uninstall")
.about("Remove all app shortcuts, files, and registry entries.")
.long_flag_alias("uninstall")
)
.arg(arg!(--verbose "Print debug messages to console / log"))
.arg(arg!(-s --silent "Don't show any prompts / dialogs"))
.arg(arg!(-l --log <PATH> "Override the default log file location").value_parser(value_parser!(PathBuf)))
.disable_help_subcommand(true)
.flatten_help(true)
}
fn parse_command_line_matches(input_args: Vec<String>) -> ArgMatches {
// Split the arguments manually to handle the legacy `--flag=value` syntax
// Also, replace `--processStartAndWait` with `--processStart --wait`
let args: Vec<String> = input_args
.into_iter()
.flat_map(|arg| if arg.contains('=') { arg.splitn(2, '=').map(String::from).collect::<Vec<_>>() } else { vec![arg] })
.flat_map(|arg| if arg.eq_ignore_ascii_case("--processStartAndWait") { vec!["--processStart".to_string(), "--wait".to_string()] } else { vec![arg] })
.collect();
root_command().get_matches_from(&args)
}
fn main() -> Result<()> {
let matches = parse_command_line_matches(env::args().collect());
let default_log_file = {
let mut my_dir = env::current_exe().unwrap();
my_dir.pop();
my_dir.join("Clowd.Squirrel.log")
};
let verbose = matches.get_flag("verbose");
let silent = matches.get_flag("silent");
let log_file = matches.get_one("log").unwrap_or(&default_log_file);
platform::set_silent(silent);
util::setup_logging(Some(&log_file), true, verbose)?;
info!("Starting Clowd.Squirrel Updater ({})", env!("CARGO_PKG_VERSION"));
info!(" Location: {}", env::current_exe()?.to_string_lossy());
info!(" Verbose: {}", verbose);
info!(" Silent: {}", silent);
info!(" Log File: {}", log_file.to_string_lossy());
let (subcommand, subcommand_matches) = matches.subcommand().ok_or_else(|| anyhow!("No subcommand was used. Try `--help` for more information."))?;
let result = match subcommand {
"uninstall" => uninstall(subcommand_matches, log_file).map_err(|e| anyhow!("Uninstall error: {}", e)),
"start" => start(&subcommand_matches).map_err(|e| anyhow!("Start error: {}", e)),
"apply" => apply(subcommand_matches).map_err(|e| anyhow!("Apply error: {}", e)),
_ => bail!("Unknown subcommand. Try `--help` for more information."),
};
if let Err(e) = result {
error!("{}", e);
return Err(e.into());
}
Ok(())
}
fn get_my_root_dir() -> Result<PathBuf> {
Ok(Path::new(r"C:\Source\rust setup testing\install").to_path_buf())
// let mut my_dir = env::current_exe()?;
// my_dir.pop();
// Ok(my_dir)
}
fn start(matches: &ArgMatches) -> Result<()> {
// handle legacy arg syntax
let legacy_args = matches.get_one::<String>("args");
let wait_for_parent = matches.get_flag("wait");
let exe_name = matches.get_one::<String>("EXE_NAME");
let exe_args: Option<Vec<&str>> = matches.get_many::<String>("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect());
info!("Command: Start");
info!(" Wait: {:?}", wait_for_parent);
info!(" Exe Name: {:?}", exe_name);
info!(" Exe Args: {:?}", exe_args);
if legacy_args.is_some() {
info!(" Legacy Args: {:?}", legacy_args);
warn!("Legacy args format is deprecated and will be removed in a future release. Please update your application to use the new format.");
}
if legacy_args.is_some() && exe_args.is_some() {
bail!("Cannot use both legacy args and new args format.");
}
if wait_for_parent {
platform::wait_for_parent_to_exit(60_000)?; // 1 minute
}
let (root_path, app) = init_root()?;
let current = app.get_current_path(&root_path);
let exe_to_execute = if let Some(exe) = exe_name {
Path::new(&current).join(exe)
} else {
let exe = app.get_main_exe_path(&root_path);
Path::new(&exe).to_path_buf()
};
if !exe_to_execute.exists() {
bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy());
}
platform::assert_can_run_binary_authenticode(&exe_to_execute)?;
info!("About to launch: '{}' in dir '{}'", exe_to_execute.to_string_lossy(), current);
if let Some(args) = exe_args {
platform::run_process(exe_to_execute, args, current)?;
} else if let Some(args) = legacy_args {
platform::run_process_raw_args(exe_to_execute, args, current)?;
} else {
platform::run_process(exe_to_execute, vec![], current)?;
};
Ok(())
}
fn apply(_matches: &ArgMatches) -> Result<()> {
info!("Command: Apply");
let root_path = get_my_root_dir()?;
todo!();
}
fn init_root() -> Result<(PathBuf, Manifest)> {
let root_path = get_my_root_dir()?;
let app = find_manifest_from_root_dir(&root_path)
.map_err(|m| anyhow!("Unable to read application manifest ({}). Is this a properly installed application?", m))?;
info!("Loaded manifest for application: {}", app.id);
info!("Root Directory: {}", root_path.to_string_lossy());
Ok((root_path, app))
}
fn uninstall(_matches: &ArgMatches, log_file: &PathBuf) -> Result<()> {
info!("Command: Uninstall");
let (root_path, app) = init_root()?;
fn _uninstall_impl(app: &Manifest, root_path: &PathBuf) -> bool {
let current_path = app.get_current_path(&root_path);
let main_exe_path = app.get_main_exe_path(&root_path);
// the real app could be running at the moment
let _ = platform::kill_processes_in_directory(&root_path);
let mut finished_with_errors = false;
// run uninstall hook
info!("Running uninstall hook...");
let args = vec!["--squirrel-install", &app.version];
if let Err(e) = platform::run_process_no_console_and_wait(&main_exe_path, args, &current_path, Some(Duration::from_secs(30))) {
error!("Uninstall hook failed: {}", e);
// for now, i'm ignoring hook failures as we stil should be able to clean up all files
// so warning shouldn't show to the user.
// finished_with_errors = true;
}
// in case the uninstall hook left running processes
let _ = platform::kill_processes_in_directory(&root_path);
info!("Removing directory '{}'", root_path.to_string_lossy());
if let Err(e) = util::retry_io(|| remove_dir_all::remove_dir_containing_current_executable()) {
error!("Unable to remove directory, some files may be in use ({}).", e);
finished_with_errors = true;
}
if let Err(e) = platform::remove_all_for_root_dir(&root_path) {
error!("Unable to remove shortcuts ({}).", e);
// finished_with_errors = true;
}
if let Err(e) = app.remove_uninstall_entry() {
error!("Unable to remove uninstall registry entry ({}).", e);
// finished_with_errors = true;
}
!finished_with_errors
}
// if it returns true, it was a success.
// if it returns false, it was completed with errors which the user should be notified of.
let result = _uninstall_impl(&app, &root_path);
if result {
info!("Finished successfully.");
platform::show_info("The application was successfully uninstalled.", format!("{} Uninstall", app.title));
} else {
error!("Finished with errors.");
platform::show_uninstall_complete_with_errors_dialog(&app, &log_file);
}
let dead_path = root_path.join(".dead");
let _ = File::create(dead_path);
platform::register_intent_to_delete_self(5, &root_path)?;
Ok(())
}
fn find_manifest_from_root_dir(root_path: &PathBuf) -> Result<Manifest> {
// default to checking current/sq.version
let cm = find_current_manifest(root_path);
if cm.is_ok() {
return cm;
}
// if that fails, check for latest full package
let latest = find_latest_full_package(root_path);
if let Some(latest) = latest {
let mani = latest.load_manifest()?;
return Ok(mani);
}
bail!("Unable to locate manifest or package.");
}
fn find_current_manifest(root_path: &PathBuf) -> Result<Manifest> {
let m = bundle::Manifest::default();
let nuspec_path = m.get_nuspec_path(root_path);
if Path::new(&nuspec_path).exists() {
if let Ok(nuspec) = util::retry_io(|| fs::read_to_string(&nuspec_path)) {
return Ok(bundle::read_manifest_from_string(&nuspec)?);
}
}
bail!("Unable to read nuspec file in current directory.")
}
fn find_latest_full_package(root_path: &PathBuf) -> Option<bundle::EntryNameInfo> {
let packages = get_all_packages(root_path);
let mut latest: Option<bundle::EntryNameInfo> = None;
for pkg in packages {
if pkg.is_delta {
continue;
}
if latest.is_none() {
latest = Some(pkg);
} else {
let latest_ver = latest.clone().unwrap().version;
if pkg.version > latest_ver {
latest = Some(pkg);
}
}
}
latest
}
fn get_all_packages(root_path: &PathBuf) -> Vec<bundle::EntryNameInfo> {
let m = bundle::Manifest::default();
let packages = m.get_packages_path(root_path);
let mut vec = Vec::new();
debug!("Scanning for packages in {:?}", packages);
if let Ok(entries) = fs::read_dir(packages) {
for entry in entries {
if let Ok(entry) = entry {
if let Some(pkg) = bundle::parse_package_file_path(entry.path()) {
debug!("Found package: {}", entry.path().to_string_lossy());
vec.push(pkg);
}
}
}
}
vec
}
#[test]
fn test_start_command_supports_legacy_commands() {
fn get_start_args(matches: &ArgMatches) -> (bool, Option<&String>, Option<&String>, Option<Vec<&String>>) {
let legacy_args = matches.get_one::<String>("args");
let wait_for_parent = matches.get_flag("wait");
let exe_name = matches.get_one::<String>("EXE_NAME");
let exe_args: Option<Vec<&String>> = matches.get_many::<String>("EXE_ARGS").map(|v| v.collect());
return (wait_for_parent, exe_name, legacy_args, exe_args);
}
fn try_parse_command_line_matches(input_args: Vec<String>) -> Result<ArgMatches> {
// Split the arguments manually to handle the legacy `--flag=value` syntax
// Also, replace `--processStartAndWait` with `--processStart --wait`
let args: Vec<String> = input_args
.into_iter()
.flat_map(|arg| if arg.contains('=') { arg.splitn(2, '=').map(String::from).collect::<Vec<_>>() } else { vec![arg] })
.flat_map(
|arg| if arg.eq_ignore_ascii_case("--processStartAndWait") { vec!["--processStart".to_string(), "--wait".to_string()] } else { vec![arg] },
)
.collect();
root_command().try_get_matches_from(&args).map_err(|e| anyhow!("{}", e))
}
let command = vec!["Update.exe", "--processStart=hello.exe"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, false);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, None);
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStart", "hello.exe"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, false);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, None);
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait=hello.exe"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, true);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, None);
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "-a", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, true);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "-a", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, true);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "--processStartArgs", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, true);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "--process-start-args", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, true);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "-a", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, true);
assert_eq!(exe_name, None);
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "-a", "-- -c \" asda --aasd"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, true);
assert_eq!(exe_name, None);
assert_eq!(legacy_args, Some(&"-- -c \" asda --aasd".to_string()));
assert_eq!(exe_args, None);
}

141
src/Rust/src/util.rs Normal file
View File

@@ -0,0 +1,141 @@
use anyhow::{anyhow, Result};
use rand::distributions::{Alphanumeric, DistString};
use regex::Regex;
use simplelog::*;
use std::{
io::{self},
path::PathBuf,
thread,
time::Duration,
};
pub fn retry_io<F, T>(op: F) -> io::Result<T>
where
F: Fn() -> io::Result<T>,
{
let res = op();
if res.is_ok() {
return Ok(res.unwrap());
}
warn!("Retrying operation in 333ms... (error was: {:?})", res.err());
thread::sleep(Duration::from_millis(333));
let res = op();
if res.is_ok() {
return Ok(res.unwrap());
}
warn!("Retrying operation in 666ms... (error was: {:?})", res.err());
thread::sleep(Duration::from_millis(666));
let res = op();
if res.is_ok() {
return Ok(res.unwrap());
}
warn!("Retrying operation in 1000ms... (error was: {:?})", res.err());
thread::sleep(Duration::from_millis(1000));
let res = op();
if res.is_ok() {
return Ok(res.unwrap());
}
warn!("Last retry in 1000ms... (error was: {:?})", res.err());
thread::sleep(Duration::from_millis(1000));
op()
}
pub fn random_string(len: usize) -> String {
Alphanumeric.sample_string(&mut rand::thread_rng(), len)
}
pub fn is_dir_empty(path: &PathBuf) -> bool {
if !path.exists() {
return true;
}
let is_empty = path.read_dir().map(|mut i| i.next().is_none()).unwrap_or(false);
let is_dead = path.join(".dead").exists();
return is_dead || is_empty;
}
pub fn trace_logger() {
TermLogger::init(LevelFilter::Trace, Config::default(), TerminalMode::Mixed, ColorChoice::Auto).unwrap();
}
pub fn setup_logging(file: Option<&PathBuf>, console: bool, verbose: bool) -> Result<()> {
let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
if console {
let console_level = if verbose { LevelFilter::Debug } else { LevelFilter::Info };
loggers.push(TermLogger::new(console_level, Config::default(), TerminalMode::Mixed, ColorChoice::Auto));
}
if let Some(f) = file {
let file_level = if verbose { LevelFilter::Trace } else { LevelFilter::Info };
let writer = file_rotate::FileRotate::new(
f.clone(),
file_rotate::suffix::AppendCount::new(1), // keep 1 old log file
file_rotate::ContentLimit::Bytes(1 * 1024 * 1024), // 1MB max log file size
file_rotate::compression::Compression::None,
);
loggers.push(WriteLogger::new(file_level, Config::default(), writer));
}
CombinedLogger::init(loggers)?;
Ok(())
}
lazy_static! {
static ref REGEX_VERSION: Regex = Regex::new(r"^(?P<major>\d+)(\.(?P<minor>\d+))?(\.(?P<build>\d+))?(\.(?P<revision>\d+))?$").unwrap();
}
pub fn parse_version(version: &str) -> Result<(u32, u32, u32, u32)> {
let caps = REGEX_VERSION.captures(version).ok_or_else(|| anyhow!("Invalid version string: '{}'", version))?;
let major_str = caps.name("major").ok_or_else(|| anyhow!("Invalid version string: '{}'", version))?.as_str();
let minor_str = caps.name("minor");
let build_str = caps.name("build");
let revision_str = caps.name("revision");
let major = major_str.parse::<u32>()?;
let minor = if minor_str.is_some() { minor_str.unwrap().as_str().parse::<u32>()? } else { 0 };
let build = if build_str.is_some() { build_str.unwrap().as_str().parse::<u32>()? } else { 0 };
let revision = if revision_str.is_some() { revision_str.unwrap().as_str().parse::<u32>()? } else { 0 };
Ok((major, minor, build, revision))
}
#[test]
fn test_parse_version_works_with_short_version() {
let (major, minor, build, _) = parse_version("10").unwrap();
assert_eq!(major, 10);
assert_eq!(minor, 0);
assert_eq!(build, 0);
}
#[test]
fn test_parse_version_works_with_long_version() {
let (major, minor, build, revision) = parse_version("1033.980.03984.14234").unwrap();
assert_eq!(major, 1033);
assert_eq!(minor, 980);
assert_eq!(build, 3984);
assert_eq!(revision, 14234);
}
#[test]
fn test_parse_version_throws_with_invalid_version() {
assert!(parse_version("invalid").is_err());
assert!(parse_version("1.1.1.1.1").is_err());
assert!(parse_version("1.1.1.a").is_err());
}
pub fn utf8_safe_substring(s: &str, start_char_idx: usize, length: usize) -> Option<&str> {
if length <= 0 {
return None;
}
let mut char_iter = s.char_indices();
let start_byte_idx = char_iter.nth(start_char_idx)?.0;
let end_byte_idx = char_iter.nth(length)?.0;
s.get(start_byte_idx..end_byte_idx)
}