diff --git a/scripts/install_quickemu b/scripts/install_quickemu new file mode 100644 index 0000000..efaba95 --- /dev/null +++ b/scripts/install_quickemu @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +import platform +import os +import shutil +import sys + + +def _(c: str): + """Execute the command `c` and print it""" + print("> " + c) + os.system(c) + + +def clone_repo(): + if os.path.exists(os.path.expanduser("~/.local/share/quickemu")): + print("📦 quickemu is already installed. Updating...") + update_quickemu() + return + + print("📦 Cloning quickemu...") + + _("git clone --filter=blob:none https://github.com/quickemu-project/quickemu ~/.local/share/quickemu") + _("mkdir -p ~/.local/bin") + _("ln -s ~/.local/share/quickemu/quickemu ~/.local/bin/quickemu") + _("ln -s ~/.local/share/quickemu/macrecovery ~/.local/bin/macrecovery") + _("ln -s ~/.local/share/quickemu/quickget ~/.local/bin/quickget") + _("ln -s ~/.local/share/quickemu/windowskey ~/.local/bin/windowskey") + + print("Installation complete.") + print("⚠️ Make sure ~/.local/bin is in your PATH.") + + +def update_quickemu(): + print("📦 Updating quickemu...") + + _("cd ~/.local/share/quickemu") + _("git pull") + + print("Update complete.") + print("⚠️ Make sure ~/.local/bin is in your PATH.") + + +def install_fedora(): + print("📦 Installing dependencies...") + + _("sudo dnf install qemu bash coreutils edk2-tools grep jq lsb procps python3 genisoimage usbutils" + + " util-linux sed spice-gtk-tools swtpm wget xdg-user-dirs xrandr unzip socat -y") + + clone_repo() + + sys.exit(0) + + +def install_deb(): + print("📦 Installing dependencies...") + + _("sudo apt update") + _("sudo apt install qemu bash coreutils ovmf grep jq lsb-base procps python3 genisoimage usbutils" + + " util-linux sed spice-client-gtk libtss2-tcti-swtpm0 wget xdg-user-dirs zsync unzip socat -y") + + clone_repo() + + sys.exit(0) + + +def install_ubuntu(): + print("⚠️ Adding ppa...") + + _("sudo apt-add-repository ppa:flexiondotorg/quickemu") + _("sudo apt update") + _("sudo apt install quickemu -y") + + sys.exit(0) + + +if __name__ == "__main__": + print("⚠️ This script requires elevated privileges (sudo). You will be asked for your password.") + + os_release = platform.freedesktop_os_release() + + distro_id = os_release.get("ID_LIKE") + distro_id_like = os_release.get("ID") + + if not distro_id and not distro_id_like: + print("❌ Couldn't fetch distro, is os-release installed?") + + if distro_id == "ubuntu" \ + or distro_id_like == "ubuntu": + install_ubuntu() + elif distro_id == "debian" \ + or distro_id_like == "debian" \ + or shutil.which("apt"): + install_deb() + elif distro_id == "fedora" \ + or distro_id_like == "fedora" \ + or shutil.which("dnf"): + install_fedora() + else: + if distro_id: + print("❌ Unsupported distro: ", distro_id) + elif distro_id_like: + print("❌ Unsupported distro: ", distro_id_like) + else: + print("❌ Unsupported distro. Couldn't fetch data from os-release and couldn't find dnf or apt on PATH.") + + sys.exit(1) + + print("❌ Unsupported platform.") + sys.exit(1) diff --git a/winapps-cli/src/main.rs b/winapps-cli/src/main.rs index b82f71d..d880054 100644 --- a/winapps-cli/src/main.rs +++ b/winapps-cli/src/main.rs @@ -1,6 +1,7 @@ use clap::Command; -use winapps::RemoteClient; use winapps::freerdp::freerdp_back::Freerdp; +use winapps::quickemu::{create_vm, kill_vm, start_vm}; +use winapps::RemoteClient; fn cli() -> Command { Command::new("winapps-cli") @@ -10,27 +11,60 @@ fn cli() -> Command { .allow_external_subcommands(true) .subcommand(Command::new("check").about("Checks remote connection")) .subcommand(Command::new("connect").about("Connects to remote")) + .subcommand( + Command::new("vm") + .about("Manage a windows 10 vm using quickemu") + .subcommand_required(true) + .arg_required_else_help(true) + .allow_external_subcommands(true) + .subcommand(Command::new("create").about("Create a windows 10 vm using quickget")) + .subcommand(Command::new("start").about("Start the vm")) + .subcommand(Command::new("kill").about("Kill the running VM")), + ) } fn main() { let cli = cli(); let matches = cli.clone().get_matches(); - let client: &dyn RemoteClient = &Freerdp{}; + let config = winapps::load_config(None); + let client: &dyn RemoteClient = &Freerdp {}; match matches.subcommand() { Some(("check", _)) => { println!("Checking remote connection"); - - let config = winapps::load_config(None); client.check_depends(config); } Some(("connect", _)) => { println!("Connecting to remote"); - - let config = winapps::load_config(None); client.run_app(config, "explorer"); } + + Some(("vm", command)) => { + match command.subcommand() { + Some(("create", _)) => { + println!("Creating windows 10 vm.."); + create_vm(config); + } + Some(("start", _)) => { + println!("Starting vm.."); + start_vm(config); + } + + Some(("kill", _)) => { + println!("Killing vm.."); + kill_vm(config); + } + + Some((_, _)) => { + cli.about("Command not found, try existing ones!") + .print_help() + .expect("Couldn't print help"); + } + _ => unreachable!(), + }; + } + Some((_, _)) => { cli.about("Command not found, try existing ones!") .print_help() diff --git a/winapps/Cargo.toml b/winapps/Cargo.toml index 629eb36..abd213a 100644 --- a/winapps/Cargo.toml +++ b/winapps/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" [dependencies] derive-new = "0.5.9" +home = "0.5.5" serde = { version = "1.0.171", features = ["derive"] } toml = "0.7.6" diff --git a/winapps/src/freerdp.rs b/winapps/src/freerdp.rs index 1150320..8da682a 100644 --- a/winapps/src/freerdp.rs +++ b/winapps/src/freerdp.rs @@ -1,23 +1,24 @@ pub mod freerdp_back { use std::process::{Command, Stdio}; - use crate::{RemoteClient, Config}; + use crate::{Config, RemoteClient}; pub struct Freerdp {} impl RemoteClient for Freerdp { fn check_depends(&self, _config: Config) { - let mut xfreerdp = Command::new("xfreerdp"); xfreerdp.stdout(Stdio::null()); xfreerdp.args(["-h"]); - xfreerdp.spawn().expect("Freerdp execution failed! It needs to be installed!"); + xfreerdp + .spawn() + .expect("Freerdp execution failed! It needs to be installed!"); println!("Freerdp found!"); - + println!("Checks success!"); } - fn run_app(&self, config: Config, _app: &str) { + fn run_app(&self, _config: Config, _app: &str) { todo!() } } diff --git a/winapps/src/lib.rs b/winapps/src/lib.rs index 9d17961..41347d6 100644 --- a/winapps/src/lib.rs +++ b/winapps/src/lib.rs @@ -1,6 +1,10 @@ +pub mod quickemu; + use derive_new::new; +use home::home_dir; use serde::{Deserialize, Serialize}; use std::io::Write; +use std::path::PathBuf; use std::{ env, fs::{self, File}, @@ -21,6 +25,16 @@ pub struct Config { host: HostConfig, #[new(value = "RemoteConfig::new()")] rdp: RemoteConfig, + #[new(value = "VmConfig::new()")] + vm: VmConfig, +} + +#[derive(new, Debug, Deserialize, Serialize)] +pub struct VmConfig { + #[new(value = "\"windows-10\".to_string()")] + short_name: String, + #[new(value = "\"windows-10-22H2\".to_string()")] + name: String, } #[derive(new, Debug, Deserialize, Serialize)] @@ -41,35 +55,86 @@ pub struct RemoteConfig { password: String, } -pub fn load_config(path: Option<&str>) -> Config { - let config = env::var("XDG_CONFIG_HOME").expect("Could not find the home path!"); - let default = &format!("{}{}", config, "/winapps/"); - let path = Path::new(path.unwrap_or(default)); - let config = Config::new(); +pub fn get_config_file(path: Option<&str>) -> PathBuf { + let default = match env::var("XDG_CONFIG_HOME") { + Ok(dir) => PathBuf::from(dir).join("winapps"), + Err(_) => { + println!("Couldn't read XDG_CONFIG_HOME, falling back to ~/.config"); + home_dir() + .expect("Could not find the home path!") + .join(".config/winapps") + } + }; + + let path = Path::new(path.unwrap_or(default.to_str().unwrap())); if !path.exists() { - println!("{:?} does not exist! Creating...", path.to_str()); + println!("{:?} does not exist! Creating...", path); fs::create_dir_all(path).expect("Failed to create directory"); } - let config_file = path.join("config.toml"); + if !path.is_dir() { + panic!("Config directory {:?} is not a directory!", path); + } + + path.join("config.toml") +} - if !config_file.exists() { - let mut config_file = - File::create(&config_file).expect("Failed to create configuration file"); +pub fn load_config(path: Option<&str>) -> Config { + let config = Config::new(); + let config_path = get_config_file(path); - let gen_config = - toml::to_string(&config).expect("Failed to generate default configuration"); - write!(config_file, "{}", gen_config).expect("Failed to write configuration file"); + if !config_path.exists() { + save_config(&config, path).expect("Failed to write default configuration"); + return config; } - let config_file = fs::read_to_string(config_file).expect("Failed to read configuration file"); + let config_file = fs::read_to_string(config_path).expect("Failed to read configuration file"); let config: Config = toml::from_str(config_file.as_str()).expect("Failed to parse the configuration"); config } +pub fn save_config(config: &Config, path: Option<&str>) -> std::io::Result<()> { + let config_path = get_config_file(path); + let serialized_config = toml::to_string(&config).expect("Failed to serialize configuration"); + + let mut config_file = match config_path.exists() { + true => File::open(&config_path).expect("Failed to open configuration file"), + false => File::create(&config_path).expect("Failed to create configuration file"), + }; + + write!(config_file, "{}", serialized_config) +} + +pub fn get_data_dir() -> PathBuf { + let data_dir = match env::var("XDG_DATA_HOME") { + Ok(dir) => PathBuf::from(dir).join("winapps"), + Err(_) => { + println!("Couldn't read XDG_DATA_HOME, falling back to ~/.local/share"); + home_dir() + .expect("Could not find the home path!") + .join(".local/share/winapps") + } + }; + + if !data_dir.exists() { + let dir = data_dir.clone(); + println!( + "Data directory {:?} does not exist! Creating...", + dir.to_str() + ); + fs::create_dir_all(dir).expect("Failed to create directory"); + } + + if !data_dir.is_dir() { + panic!("Data directory {:?} is not a directory!", data_dir); + } + + data_dir +} + pub fn add(left: usize, right: usize) -> usize { left + right } diff --git a/winapps/src/quickemu.rs b/winapps/src/quickemu.rs new file mode 100644 index 0000000..73b3300 --- /dev/null +++ b/winapps/src/quickemu.rs @@ -0,0 +1,89 @@ +use crate::{get_data_dir, save_config, Config}; +use std::fs; +use std::process::exit; +use std::process::Command; + +pub fn create_vm(mut config: Config) { + let data_dir = get_data_dir(); + + let output = match Command::new("quickget") + .current_dir(data_dir) + .arg("windows") + .arg("10") + .output() + { + Ok(o) => o, + Err(e) => { + println!("Failed to execute quickget: {}", e); + println!("Please make sure quickget is installed and in your PATH"); + exit(1); + } + }; + + config.vm.name = "windows-10-22H2".to_string(); + config.vm.short_name = "windows-10".to_string(); + + save_config(&config, None).expect("Failed to save config, VM will not start properly"); + + println!("{}", String::from_utf8_lossy(&output.stdout)); +} + +pub fn start_vm(config: Config) { + let data_dir = get_data_dir(); + + let command = match Command::new("quickemu") + .current_dir(data_dir) + .args([ + "--ignore-msrs-always", + "--vm", + &format!("{}.conf", config.vm.name), + "--display", + "none", + ]) + .spawn() + { + Ok(c) => c, + Err(e) => { + println!("Failed to execute quickemu: {}", e); + println!("Please make sure quickemu is installed and in your PATH"); + exit(1); + } + }; + + let output = match command.wait_with_output() { + Ok(o) => o, + Err(e) => { + println!("Failed to gather output from quickemu: {}", e); + println!("Please make sure quickemu is installed and in your PATH"); + exit(1); + } + }; + + println!("{}", String::from_utf8_lossy(&output.stdout)); +} + +pub fn kill_vm(config: Config) { + let data_dir = get_data_dir(); + + match fs::read_to_string( + data_dir.join(format!("{}/{}.pid", config.vm.short_name, config.vm.name)), + ) { + Ok(pid) => { + let pid = pid.trim(); + + println!("Killing VM with PID {}", pid); + + match Command::new("kill").arg(pid).spawn() { + Ok(_) => (), + Err(e) => { + println!("Failed to kill VM: {}", e); + exit(1); + } + } + } + Err(e) => { + println!("Failed to read PID file: {}", e); + exit(1); + } + } +}