mod loglevel;
#[cfg(windows)]
use anyhow::anyhow;
#[cfg(windows)]
use anyhow::Result;
#[cfg(windows)]
use indicatif::{HumanDuration, ProgressBar, ProgressStyle};
#[cfg(windows)]
use log::{debug, error, info, warn};
#[cfg(windows)]
use loglevel::LogLevel;
#[cfg(windows)]
use std::io::Write;
#[cfg(windows)]
use std::{
env::temp_dir,
path::{Path, PathBuf},
time::Instant,
};
#[cfg(windows)]
use structopt::StructOpt;
#[cfg(windows)]
use tokio::{fs, io::AsyncWriteExt};
#[cfg(windows)]
use winreg::{
enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE},
RegKey, RegValue,
};
#[cfg(windows)]
#[derive(Debug, StructOpt)]
#[structopt(name = "jvc-init")]
pub struct Cli {
#[structopt(long = "release", short = "r", default_value = "latest")]
pub release: String,
#[structopt(long = "install-dir", short = "d")]
pub install_dir: Option<PathBuf>,
}
#[cfg(windows)]
fn init_logging(log_level: &LogLevel) {
env_logger::Builder::new()
.format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args()))
.filter(Some("jvc"), log_level.into())
.filter(Some("jvc-init"), log_level.into())
.init();
}
#[cfg(windows)]
async fn install_jvc(release_tag: Option<String>, install_dir: PathBuf) -> Result<()> {
let filename = "jvc-win-amd64.exe".to_owned();
let download_url = if let Some(tag) = release_tag {
format!(
"https://github.com/neculai-stanciu/jvc/releases/{}/download/{}",
tag, filename
)
} else {
format!(
"https://github.com/neculai-stanciu/jvc/releases/latest/download/{}",
filename
)
};
let download_dir = temp_dir();
download_binary(
download_url.as_ref(),
download_dir.as_path(),
filename.as_ref(),
)
.await?;
let download_binary_path = download_dir.join(filename.clone());
let installation_dir = install_dir.join("jvc.exe");
move_to_install_dir(download_binary_path, installation_dir.as_path()).await?;
add_to_path(installation_dir).await?;
Ok(())
}
#[cfg(windows)]
async fn add_to_path(binary_path: PathBuf) -> Result<()> {
let value = binary_path
.as_os_str()
.to_str()
.ok_or(anyhow!("Cannot transform os path to string"))?;
set_persistent_path(value).await?;
Ok(())
}
#[cfg(windows)]
async fn move_to_install_dir(from: PathBuf, install_dir: &Path) -> Result<u64> {
debug!(
"Try to copy from {:?} to {:?}",
from.as_path().as_os_str(),
install_dir.as_os_str()
);
std::fs::copy(from, install_dir)
.map_err(|e| anyhow!("Cannot copy binary to installation dir \n {:?}", e))
}
#[cfg(windows)]
async fn download_binary(
download_url: &str,
download_path: &Path,
binary_name: &str,
) -> Result<()> {
info!("Starting download binary: {}", download_url);
let file_name = download_path.join(binary_name);
let mut response = reqwest::get(download_url).await?;
debug!(
"Try to write in temp dir: {} - response status {}",
file_name.to_str().expect("cannot get dir path"),
&response.status()
);
if response.status().as_u16() != 200u16 {
error!("Binary path has changed. Open in issue on https://github.com/neculai-stanciu/jvc/issues for this.");
}
let binary_size = response
.content_length()
.ok_or(anyhow!("Cannot find binary size"))?;
let pb = ProgressBar::new(binary_size);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.progress_chars("#>-"));
if file_name.exists() {
fs::remove_file(&file_name).await?;
}
let mut dest = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&file_name)
.await?;
while let Some(chunk) = response.chunk().await? {
dest.write_all(&chunk).await?;
pb.inc(chunk.len() as u64);
}
Ok(())
}
#[cfg(windows)]
pub fn string_to_winreg_bytes(s: &str) -> Vec<u8> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
let v: Vec<u16> = OsStr::new(s).encode_wide().chain(Some(0)).collect();
unsafe { std::slice::from_raw_parts(v.as_ptr().cast::<u8>(), v.len() * 2).to_vec() }
}
#[cfg(windows)]
pub fn string_from_winreg_value(val: &winreg::RegValue) -> Option<String> {
use std::slice;
match val.vtype {
RegType::REG_SZ | RegType::REG_EXPAND_SZ => {
let words = unsafe {
#[allow(clippy::cast_ptr_alignment)]
slice::from_raw_parts(val.bytes.as_ptr().cast::<u16>(), val.bytes.len() / 2)
};
String::from_utf16(words).ok().map(|mut s| {
while s.ends_with('\u{0}') {
s.pop();
}
s
})
}
_ => None,
}
}
#[cfg(windows)]
fn get_windows_path_var() -> Result<Option<String>> {
use std::io;
let root = RegKey::predef(HKEY_CURRENT_USER);
let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
let reg_value = environment.get_raw_value("PATH");
match reg_value {
Ok(val) => {
if let Some(s) = string_from_winreg_value(&val) {
Ok(Some(s))
} else {
warn!("the registry key HKEY_CURRENT_USER\\Environment\\PATH does not contain valid Unicode. \
Not modifying the PATH variable");
Ok(None)
}
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(Some(String::new())),
Err(_e) => Err(anyhow!("Cannot read path")),
}
}
#[cfg(windows)]
async fn set_persistent_path(value: &str) -> Result<()> {
debug!("Extend path with value: {}", value);
let root = RegKey::predef(HKEY_CURRENT_USER);
let current_path_value = get_windows_path_var()?;
if let Some(path_val) = current_path_value {
if path_val.contains(value) {
info!("Ignore adding value because is already configured");
return Ok(());
}
debug!("Path values: \n {}", path_val);
let final_path = format!("{};{}", value, path_val);
debug!("End value: \n {}", final_path);
let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
let reg_value = RegValue {
bytes: string_to_winreg_bytes(final_path.as_ref()),
vtype: RegType::REG_EXPAND_SZ,
};
environment.set_raw_value("PATH", ®_value)?;
}
info!(
"jvc installed successfully, please close all your terminal windows to refresh env paths"
);
Ok(())
}
#[cfg(windows)]
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let start_time = Instant::now();
let args: Cli = Cli::from_args();
init_logging(&LogLevel::Info);
let install_dir = args.install_dir.unwrap_or(
dirs::home_dir()
.ok_or(anyhow!(
"Cannot get home dir. Please specify your installation dir option."
))?
.join(".jvc"),
);
install_jvc(Some(args.release), install_dir).await?;
let end = start_time.elapsed();
debug!("Time elapsed is: {}", HumanDuration(end));
Ok(())
}
#[cfg(unix)]
fn main() {
println!("Bin jvc-init is intended only for Windows operating system.");
}