diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index d565ad84..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "rust-lang.rust-analyzer", - "jnoortheen.nix-ide", - "mkhl.direnv" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b616542..b732d08e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,7 @@ { - "editor.formatOnSave": true, - "files.autoSave": "onFocusChange", - "rust-analyzer.linkedProjects": [ - "./codchi/cli/Cargo.toml" - ], - "rust-analyzer.showUnlinkedFileNotification": false, -} \ No newline at end of file + "editor.formatOnSave": true, + "files.autoSave": "onFocusChange", + "rust-analyzer.showUnlinkedFileNotification": false, + "nix.enableLanguageServer": true, + "nix.serverPath": "nil" +} diff --git a/codchi/shell.nix b/codchi/shell.nix index fd0bfb95..1e4979a9 100644 --- a/codchi/shell.nix +++ b/codchi/shell.nix @@ -7,7 +7,7 @@ , codchi -, nixd +, nil , nixpkgs-fmt , strace , gdb @@ -50,7 +50,7 @@ mkShell (lib.recursiveUpdate inputsFrom = [ codchi ]; packages = [ - nixd + nil nixpkgs-fmt codchi.passthru.rust diff --git a/codchi/src/cli.rs b/codchi/src/cli.rs index 527b0c7f..53f58044 100644 --- a/codchi/src/cli.rs +++ b/codchi/src/cli.rs @@ -7,6 +7,7 @@ use git_url_parse::{GitUrl, Scheme}; use lazy_regex::regex_is_match; use log::Level; use std::{ + path::PathBuf, str::FromStr, sync::{LazyLock, OnceLock}, }; @@ -450,6 +451,18 @@ See the following docs on how to register the completions with your shell: /// Start the codchi tray if not running. #[clap(hide = true)] Tray {}, + + #[clap(about = "Export the file system of a code machine.")] + Tar { + /// Name of the code machine + name: String, + + /// Path to export to. + target_file: PathBuf, + }, + + #[clap(about = "Open a shell inside `codchistore`.")] + DebugStore, } mod module { diff --git a/codchi/src/logging/output.rs b/codchi/src/logging/output.rs index c059e0f4..433e6be2 100644 --- a/codchi/src/logging/output.rs +++ b/codchi/src/logging/output.rs @@ -1,8 +1,8 @@ use std::{fmt::Display, io::stdout}; +use crate::config::{MachineModules, MachineStatus, Mod, ModLsOutput, StatusOutput}; use itertools::Itertools; use serde::Serialize; -use crate::config::{MachineModules, MachineStatus, Mod, ModLsOutput, StatusOutput}; use crate::platform::{ConfigStatus, Machine}; diff --git a/codchi/src/logging/progress.rs b/codchi/src/logging/progress.rs index 352f3923..56ed49f7 100644 --- a/codchi/src/logging/progress.rs +++ b/codchi/src/logging/progress.rs @@ -1,10 +1,10 @@ use super::nix::{self, Activity, ActivityType, LogItem, LogResult}; +use crate::util::store_path_base; use console::style; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use itertools::Itertools; use log::Level; use number_prefix::NumberPrefix; -use crate::util::store_path_base; use std::{borrow::Cow, collections::HashMap, sync::LazyLock, time::Duration}; use throttle::Throttle; diff --git a/codchi/src/main.rs b/codchi/src/main.rs index af65713b..8632682c 100644 --- a/codchi/src/main.rs +++ b/codchi/src/main.rs @@ -8,8 +8,8 @@ use crate::{ }; use clap::{CommandFactory, Parser}; use config::{git_url::GitUrl, CodchiConfig, MachineConfig}; -use logging::CodchiOutput; -use platform::{ConfigStatus, Host}; +use logging::{set_progress_status, CodchiOutput}; +use platform::{ConfigStatus, Host, MachineDriver}; use std::{env, process::exit}; use util::{ResultExt, UtilExt}; @@ -27,6 +27,7 @@ fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + // process immediate commands if let Some(Cmd::Completion { shell }) = &cli.command { shell.generate(&mut Cli::command(), &mut std::io::stdout()); exit(0); @@ -39,11 +40,24 @@ fn main() -> anyhow::Result<()> { // preload config let cfg = CodchiConfig::get(); - if !matches!(cli.command, Some(Cmd::Tray {})) && cfg.tray.autostart { - Driver::host() - .start_tray(false) - .trace_err("Failed starting codchi's tray") - .ignore(); + // process commands without the store commands + match &cli.command { + Some(Cmd::Tray {}) if cfg.tray.autostart => { + Driver::host() + .start_tray(false) + .trace_err("Failed starting codchi's tray") + .ignore(); + exit(0); + } + Some(Cmd::Tar { name, target_file }) => { + progress_scope! { + set_progress_status(format!("Exported files of {name} to {target_file:?}...")); + Machine::by_name(name, false)?.tar(target_file)?; + log::info!("Success! Exported file system of machine {name} to {target_file:?}"); + } + exit(0); + } + _ => {} } CLI_ARGS @@ -52,8 +66,9 @@ fn main() -> anyhow::Result<()> { let _ = Driver::store(); + // all other commands match &cli.command.unwrap_or(Cmd::Status {}) { - Cmd::Status {} => Machine::list()?.print(cli.json), + Cmd::Status {} => Machine::list(true)?.print(cli.json), Cmd::Init { machine_name, url, @@ -99,12 +114,15 @@ fn main() -> anyhow::Result<()> { "Machine '{machine_name}' is ready! Use `codchi exec {machine_name}` to start it." ); } - Cmd::Rebuild { no_update, name } => Machine::by_name(name)?.build(*no_update)?, - Cmd::Exec { name, cmd } => Machine::by_name(name)?.exec(cmd)?, + Cmd::Rebuild { no_update, name } => { + Machine::by_name(name, true)?.build(*no_update)?; + log::info!("Machine {name} rebuilt successfully!"); + } + Cmd::Exec { name, cmd } => Machine::by_name(name, true)?.exec(cmd)?, Cmd::Delete { name, i_am_really_sure, - } => Machine::by_name(name)?.delete(*i_am_really_sure)?, + } => Machine::by_name(name, true)?.delete(*i_am_really_sure)?, Cmd::Module(cmd) => match cmd { cli::ModuleCmd::List { name } => { let json = cli.json; @@ -157,6 +175,8 @@ fn main() -> anyhow::Result<()> { } => Driver::store().gc(delete_old.map(|x| x.unwrap_or_default()), *all, machines)?, Cmd::Tray {} => tray::run()?, Cmd::Completion { .. } => unreachable!(), + Cmd::Tar { .. } => unreachable!(), + Cmd::DebugStore => unreachable!(), } if CodchiConfig::get().tray.autostart { Driver::host() diff --git a/codchi/src/module.rs b/codchi/src/module.rs index 782fd50f..2e329e89 100644 --- a/codchi/src/module.rs +++ b/codchi/src/module.rs @@ -94,7 +94,7 @@ pub fn add( } cfg.write(lock)?; - let machine = Machine::by_name(machine_name)?; + let machine = Machine::by_name(machine_name, true)?; machine.write_flake()?; machine.update_status() } @@ -244,7 +244,7 @@ pub fn set( } cfg.write(lock)?; - let machine = Machine::by_name(machine_name)?; + let machine = Machine::by_name(machine_name, true)?; machine.write_flake()?; machine.update_status() } @@ -269,7 +269,7 @@ pub fn delete(machine_name: &str, module_name: &ModuleName) -> Result { } cfg.write(lock)?; - let machine = Machine::by_name(machine_name)?; + let machine = Machine::by_name(machine_name, true)?; machine.write_flake()?; machine.update_status() } diff --git a/codchi/src/platform/cmd/mod.rs b/codchi/src/platform/cmd/mod.rs index 88fa3cbe..81f5f412 100644 --- a/codchi/src/platform/cmd/mod.rs +++ b/codchi/src/platform/cmd/mod.rs @@ -1,7 +1,7 @@ use super::*; +use crate::util::UtilExt; use anyhow::anyhow; use serde::Deserialize; -use crate::util::UtilExt; use std::io::{BufRead, BufReader, Read}; use std::process::{Child, Stdio}; use std::sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender}; diff --git a/codchi/src/platform/linux/lxd.rs b/codchi/src/platform/linux/lxd.rs index 12ef247a..05ceae69 100644 --- a/codchi/src/platform/linux/lxd.rs +++ b/codchi/src/platform/linux/lxd.rs @@ -66,6 +66,19 @@ pub mod container { Ok(()) } + pub fn export(name: &str, target_path: &str) -> Result<()> { + let mut cmd = lxc_command(&[ + "export", + "--instance-only", + "--compression", + "none", + name, + target_path, + ]); + cmd.wait_ok()?; + Ok(()) + } + pub fn get_info(name: &str) -> Result> { let info = lxc_command(&["list", "--format", "json"]) .output_json::>()? diff --git a/codchi/src/platform/linux/mod.rs b/codchi/src/platform/linux/mod.rs index 52c3ffbd..3159dab9 100644 --- a/codchi/src/platform/linux/mod.rs +++ b/codchi/src/platform/linux/mod.rs @@ -5,14 +5,15 @@ use super::{Driver, LinuxCommandTarget, LinuxUser, NixDriver, Store}; use crate::{ cli::DEBUG, consts::{self, machine::machine_name, store, user, ToPath}, - logging::{log_progress, set_progress_status}, + logging::{log_progress, set_progress_status, with_suspended_progress}, platform::{ platform::lxd::container::LxdDevice, CommandExt, Machine, MachineDriver, PlatformStatus, }, util::{with_tmp_file, LinuxPath, PathExt, ResultExt, UtilExt}, }; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; pub use host::*; +use inquire::Confirm; use log::*; use std::{ collections::HashMap, @@ -20,6 +21,7 @@ use std::{ fs::{self, File}, io::Write, path::PathBuf, + process::Command, sync::mpsc::channel, thread, }; @@ -325,6 +327,101 @@ tail -f "{log_file}" cmd.with_user(LinuxUser::Root) } + + fn tar(&self, target_file: &std::path::Path) -> Result<()> { + fn command_with_privileges(reason: &str, command: &[&str]) -> Result { + let sudo_path = which::which("sudo").ok(); + let doas_path = which::which("doas").ok(); + + let (sudo_name, sudo) = if let Some(path) = sudo_path { + ("sudo", path) + } else if let Some(path) = doas_path { + ("doas", path) + } else { + bail!( + "Neither sudo nor doas was found on this system. \ +This is needed in order to {reason}." + ); + }; + log::debug!("Found {sudo_name} at {sudo:?}"); + + let message = format!( + "Codchi needs to invoke `{}` as root in order to {reason}. Is it okay to use {sudo_name}?", + command.join(" ") + ); + let user_confirmed = Confirm::new(&message).with_default(true).prompt()?; + + if user_confirmed { + let mut cmd = Command::new(sudo); + cmd.args(command); + Ok(cmd) + } else { + bail!("Operation was canceled by the user"); + } + } + + with_tmp_file(&format!("codchi-backup-{}", self.config.name), |tmp_dir| { + fs::create_dir_all(tmp_dir)?; + let lxc_export = tmp_dir.join("lxc_export.tar").display().to_string(); + let target_file = target_file.display().to_string(); + lxd::container::export( + &consts::machine::machine_name(&self.config.name), + &lxc_export, + )?; + Command::new("tar") + .args(["-C", &tmp_dir.display().to_string(), "-xf", &lxc_export]) + .wait_ok()?; + with_suspended_progress(|| { + command_with_privileges( + "export the code machine root file system", + &[ + "tar", + "-C", + &tmp_dir + .join("backup/container/rootfs") + .display() + .to_string(), + "-cf", + &target_file, + ".", + ], + )? + .wait_ok()?; + + // add home dir + command_with_privileges( + "export the code machine home directory", + &[ + "tar", + "--append", + "-f", + &target_file, + "-C", + &consts::host::DIR_DATA + .join_machine(&self.config.name) + .display() + .to_string(), + "--transform", + "s|^./|./home/codchi/|", + "--owner=1000", + "--group=100", + "--numeric-owner", + ".", + ], + )? + .wait_ok()?; + + command_with_privileges( + "cleanup temporary files", + &["rm", "-rf", &tmp_dir.display().to_string()], + )? + .wait_ok()?; + Ok(()) + }) + })?; + + Ok(()) + } } #[derive(Debug, Clone)] diff --git a/codchi/src/platform/machine.rs b/codchi/src/platform/machine.rs index a9ebd767..db5b7483 100644 --- a/codchi/src/platform/machine.rs +++ b/codchi/src/platform/machine.rs @@ -33,6 +33,9 @@ pub trait MachineDriver: Sized { fn delete_container(&self) -> Result<()>; fn create_exec_cmd(&self, cmd: &[&str]) -> LinuxCommandBuilder; + + /// Export file system of a machine to a tar WITHOUT starting the store or the machine. + fn tar(&self, target_file: &std::path::Path) -> Result<()>; } #[derive(Debug, Clone)] @@ -97,23 +100,30 @@ fi }; Ok(self) } - pub fn read(config: MachineConfig) -> Result { - Self { + pub fn read(config: MachineConfig, update_status: bool) -> Result { + let machine = Self { config, config_status: ConfigStatus::NotInstalled, platform_status: PlatformStatus::NotInstalled, + }; + if update_status { + machine.update_status() + } else { + Ok(machine) } - .update_status() } /// Returns Err if machine doesn't exist - pub fn by_name(name: &str) -> Result { + pub fn by_name(name: &str, update_status: bool) -> Result { let (_, cfg) = MachineConfig::open_existing(name, false)?; - Self::read(cfg) + Self::read(cfg, update_status) } - pub fn list() -> Result> { - MachineConfig::list()?.into_iter().map(Self::read).collect() + pub fn list(update_status: bool) -> Result> { + MachineConfig::list()? + .into_iter() + .map(|cfg| Self::read(cfg, update_status)) + .collect() } pub fn write_flake(&self) -> Result<()> { diff --git a/codchi/src/platform/windows/mod.rs b/codchi/src/platform/windows/mod.rs index 0c584a66..2aa636bf 100644 --- a/codchi/src/platform/windows/mod.rs +++ b/codchi/src/platform/windows/mod.rs @@ -285,6 +285,49 @@ tail -f "{log_file}" cmd.with_cwd(consts::user::DEFAULT_HOME.clone()) .with_user(LinuxUser::Default) } + + fn tar(&self, target_file: &std::path::Path) -> Result<()> { + let target_absolute = if target_file.is_absolute() { + target_file.to_path_buf() + } else { + env::current_dir()?.join(target_file) + }; + let wsl_path = wsl_command() + .args([ + "-d", + &consts::machine::machine_name(&self.config.name), + "--system", + "--user", + "root", + ]) + .args([ + "wslpath", + "-u", + &target_absolute.display().to_string().replace("\\", "/"), + ]) + .output_utf8_ok() + .map(|path| path.trim().to_owned()) + .with_context(|| format!("Failed to run 'wslpath' with path {target_absolute:?}."))?; + wsl_command() + .args([ + "-d", + &consts::machine::machine_name(&self.config.name), + "--system", + "--user", + "root", + ]) + .args([ + "/mnt/wslg/distro/bin/tar", + "-C", + "/mnt/wslg/distro", + "-cf", + &wsl_path, + ".", + ]) + .wait_ok()?; + + Ok(()) + } } #[derive(Debug, Clone)] diff --git a/codchi/src/tray/mod.rs b/codchi/src/tray/mod.rs index 5ef45126..88ee2e15 100644 --- a/codchi/src/tray/mod.rs +++ b/codchi/src/tray/mod.rs @@ -41,7 +41,7 @@ impl App { fn new() -> Result { Ok(Self { config: CodchiConfig::get(), - machines: Machine::list()?, + machines: Machine::list(true)?, }) } } diff --git a/configuration.nix b/configuration.nix new file mode 100644 index 00000000..0b1617f9 --- /dev/null +++ b/configuration.nix @@ -0,0 +1,18 @@ +{ pkgs, ... }: +{ + environment.systemPackages = [ + (pkgs.vscode-with-extensions.override { + vscode = pkgs.vscodium; + vscodeExtensions = with pkgs.vscode-extensions; [ + rust-lang.rust-analyzer + jnoortheen.nix-ide + mkhl.direnv + asvetliakov.vscode-neovim + ]; + }) + ]; + programs.direnv = { + enable = true; + nix-direnv.enable = true; + }; +} diff --git a/docs/content/4.contrib/roadmap.md b/docs/content/4.contrib/roadmap.md index 44efd168..7eff69ab 100644 --- a/docs/content/4.contrib/roadmap.md +++ b/docs/content/4.contrib/roadmap.md @@ -1,20 +1,13 @@ # Roadmap -- Docs - - [ ] Architecture - - [ ] Landing Page - - Demo Video - - Bugs - [ ] shortcuts / terminal fragments win - update path on every update => autostart codchi tray => run "migration" - Future - - Announcement Post (Discourse, HN?, Product Hunt?) - - [ ] GPU + - Announcement Post (Discourse, Reddit) - Features - - [ ] codchiw.exe - [ ] `codchi status` on windows is slow => move wsl status checking / store container starting to scheduled / time based task - [ ] `codchi recover` => fs tar export @@ -34,7 +27,6 @@ - tray icon - ip kopieren - - open questions: - WSL - what happens if store stops and machines runs? diff --git a/flake.nix b/flake.nix index 0dde19e4..a1bdf886 100644 --- a/flake.nix +++ b/flake.nix @@ -71,19 +71,9 @@ inherit lib; nixosModules.default = import ./nix/nixos; - nixosModules.codchi = { pkgs, ... }: { - nixpkgs.config.allowUnfree = true; - environment.systemPackages = self.devShells.${system}.default.nativeBuildInputs ++ [ - pkgs.vscodium - ]; - # programs.neovim = { - # enable = true; - # package = pkgs.nixvim.makeNixvim (import ./editor.nix); - # }; - programs.direnv = { - enable = true; - nix-direnv.enable = true; - }; + nixosModules.codchi = { + imports = [ ./configuration.nix ]; + environment.systemPackages = self.devShells.${system}.default.nativeBuildInputs; }; packages.${system} = {