knowledge 0.12.1

The launcher and updater for Knowledge.Dev Workbench
Documentation
use crate::cacher::Cacher;
use crate::crates::CratesApi;
use crate::opts::{AppCommand, Opts, UpdateCommand};
use crate::workbench::WorkbenchApi;
use anyhow::{Result, bail};
use dialoguer::Confirm;
use owo_colors::OwoColorize;
#[cfg(not(target_os = "windows"))]
use std::fs;
#[cfg(unix)]
use std::io;
use std::process::{Command, Stdio};

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

#[cfg(target_os = "windows")]
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;

#[cfg(target_os = "windows")]
const DETACHED_PROCESS: u32 = 0x0000_0008;

#[cfg(unix)]
use std::os::unix::process::CommandExt;

#[cfg(unix)]
unsafe extern "C" {
    fn setsid() -> i32;
}

pub struct App {
    opts: Opts,
    cacher: Cacher,
    crates_api: CratesApi,
    workbench_api: WorkbenchApi,
}

impl App {
    pub async fn entrypoint(opts: Opts) -> Result<()> {
        let mut app = Self::init(opts).await?;
        match &app.opts.command {
            None => {
                app.update_workbench(false, false).await?;
                app.command_workbench().await?;
            }
            Some(AppCommand::Update(opts)) => {
                let opts = Some(opts.clone());
                app.command_update(true, false, opts).await?;
            }
            Some(AppCommand::Upgrade(opts)) => {
                let opts = Some(opts.clone());
                app.command_update(true, true, opts).await?;
            }
            Some(AppCommand::Workbench) => {
                app.update_workbench(false, false).await?;
                app.command_workbench().await?;
            }
            Some(AppCommand::Clean) => {
                app.command_clean().await?;
            }
        }
        Ok(())
    }

    async fn init(opts: Opts) -> Result<Self> {
        let mut cacher = Cacher::create().await?;
        cacher.initialize().await?;
        Ok(Self {
            opts,
            cacher,
            crates_api: CratesApi::new(),
            workbench_api: WorkbenchApi::new(),
        })
    }

    async fn update_launcher(&mut self, force_check: bool) -> Result<()> {
        if self.cacher.launcher.is_update_required() || force_check {
            eprintln!("Checking an update for the launcher...");
            let version = self.crates_api.latest_version().await?;

            if self.cacher.launcher.is_outdated(version) {
                eprintln!(
                    "New version of launcher is available. To update run the following command"
                );
                eprintln!("{}", "cargo install knowledge".green());
            }

            self.cacher.launcher.update_check();
            self.cacher.write_state().await?;
        }
        Ok(())
    }

    async fn update_workbench(&mut self, force_check: bool, force_download: bool) -> Result<()> {
        if self.cacher.workbench.is_update_required() || force_check {
            eprintln!("Checking for workbench updates...");
            let latest = self.workbench_api.latest_release().await?;
            let version = latest.version.clone();
            if self.cacher.workbench.is_outdated(version.clone()) || force_download {
                eprintln!("Downloading workbench {}...", version);
                let url = latest.get_asset_url();
                let bin_name = if cfg!(target_os = "windows") {
                    "workbench.exe"
                } else {
                    "workbench"
                };
                let dest = self.cacher.bin_dir().join(bin_name);
                self.workbench_api.download_binary(&url, &dest).await?;
                #[cfg(not(target_os = "windows"))]
                {
                    use std::os::unix::fs::PermissionsExt;
                    let mut perm = fs::metadata(&dest)?.permissions();
                    perm.set_mode(perm.mode() | 0o755);
                    fs::set_permissions(&dest, perm)?;
                }
                self.cacher.workbench.version = Some(version.clone());
                eprintln!("workbench {} is ready.", version);
            }
            self.cacher.workbench.update_check();
            self.cacher.write_state().await?;
        }
        Ok(())
    }

    pub async fn command_update(
        &mut self,
        force_check: bool,
        force_download: bool,
        _opts: Option<UpdateCommand>,
    ) -> Result<()> {
        if let Err(err) = self.update_launcher(force_check).await {
            eprintln!("Launcher updating failed: {}", err.to_string().red());
        }
        if let Err(err) = self.update_workbench(force_check, force_download).await {
            eprintln!("Workbench updating failed: {}", err.to_string().red());
        }
        Ok(())
    }

    pub async fn command_workbench(&mut self) -> Result<()> {
        let mut bin_path = self.cacher.bin_dir().clone();
        let bin_name = if cfg!(target_os = "windows") {
            "workbench.exe"
        } else {
            "workbench"
        };
        bin_path.push(bin_name);

        if !bin_path.exists() {
            bail!("Workbench binary not found. Run 'knowledge update' first.");
        }

        eprintln!("Starting workbench...");

        let mut command = Command::new(&bin_path);
        command
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null());

        detach_command(&mut command);

        command.spawn()?;

        Ok(())
    }

    pub async fn command_clean(self) -> Result<()> {
        if Confirm::new()
            .with_prompt("Do you want to clean the cache?")
            .interact()?
        {
            eprintln!("Cleaning the cache...");
            self.cacher.remove_cache().await?;
        }
        Ok(())
    }
}

#[cfg(unix)]
fn detach_command(command: &mut Command) {
    unsafe {
        command.pre_exec(|| {
            let session_id = setsid();
            if session_id == -1 {
                return Err(io::Error::last_os_error());
            }
            Ok(())
        });
    }
}

#[cfg(target_os = "windows")]
fn detach_command(command: &mut Command) {
    command.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
}

#[cfg(not(any(unix, target_os = "windows")))]
fn detach_command(_command: &mut Command) {}