Skip to content

Commit

Permalink
Implement automatic Rokit installation when launching from Windows Ex…
Browse files Browse the repository at this point in the history
…plorer
  • Loading branch information
filiptibell committed Jul 17, 2024
1 parent 8bae269 commit c1155e7
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 10 deletions.
2 changes: 2 additions & 0 deletions lib/system/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod current;
mod env;
mod process;
mod runner;

pub use self::current::{current_dir, current_exe, current_exe_contents, current_exe_name};
pub use self::env::{add_to_path, exists_in_path};
pub use self::process::{Launcher as ProcessLauncher, Parent as ProcessParent};
pub use self::runner::run_interruptible;
75 changes: 75 additions & 0 deletions lib/system/process/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#![allow(clippy::unused_async)]

use std::io::{stderr, stdout, IsTerminal};

#[cfg(unix)]
mod unix;

#[cfg(windows)]
mod windows;

#[cfg(unix)]
use self::unix as platform;

#[cfg(windows)]
use self::windows as platform;

/**
Enum representing possible sources that may have launched Rokit.
Note that this in non-exhaustive, and may be extended in the future.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Launcher {
WindowsExplorer,
MacOsFinder,
}

/**
Enum representing the detected kind of parent process of Rokit.
Note that this in non-exhaustive, and may be extended in the future.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Parent {
Launcher(Launcher),
Terminal,
}

impl Parent {
/**
Returns `true` if the parent is a launcher.
*/
#[must_use]
pub const fn is_launcher(self) -> bool {
matches!(self, Self::Launcher(_))
}

/**
Returns `true` if the parent is a terminal.
*/
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Terminal)
}

/**
Tries to detect the parent process of Rokit.
Returns `None` if the parent process could not be detected.
*/
pub async fn get() -> Option<Self> {
platform::try_detect_launcher()
.await
.map(Self::Launcher)
.or_else(|| {
if stdout().is_terminal() || stderr().is_terminal() {
Some(Self::Terminal)
} else {
None
}
})
}
}
4 changes: 4 additions & 0 deletions lib/system/process/unix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub async fn try_detect_launcher() -> Option<super::Launcher> {
// FUTURE: Try to detect if this process was launched from Finder / Linux equivalent
None
}
36 changes: 36 additions & 0 deletions lib/system/process/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use winapi::um::processthreadsapi::GetCurrentProcessId;
use winapi::um::wincon::GetConsoleProcessList;

use super::Launcher;

pub async fn try_detect_launcher() -> Option<super::Launcher> {
tracing::debug!("trying to detect launcher using Windows API");

/*
Allocate a buffer for process IDs - we need space for at
least one ID, which will hopefully be our own process ID
*/
let mut process_list = [0u32; 1];
let process_id = unsafe { GetCurrentProcessId() };
let process_count = unsafe { GetConsoleProcessList(process_list.as_mut_ptr(), 1) };

tracing::debug!(
id = %process_id,
count = %process_count,
"got process id and count"
);

/*
If there's only one process (our process), the console will be destroyed on exit,
this very likely means it was launched from Explorer or a similar environment.
A similar environment could be the download folder in a web browser,
launching the program directly using the "Run" dialog, ..., but for
simplicity we'll just assume it was launched from Explorer.
*/
if process_count == 1 && process_list[0] == process_id {
Some(Launcher::WindowsExplorer)
} else {
None
}
}
50 changes: 40 additions & 10 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use anyhow::{Context, Result};
use clap::{ArgAction, Parser};
use clap::{ArgAction, CommandFactory, Parser};
use tokio::time::Instant;
use tracing::level_filters::LevelFilter;

use rokit::storage::Home;
use rokit::system::ProcessParent;

use crate::util::init_tracing;

Expand Down Expand Up @@ -33,17 +34,32 @@ use self::update::UpdateSubcommand;
#[clap(author, version, about)]
pub struct Cli {
#[clap(subcommand)]
pub subcommand: Subcommand,
pub subcommand: Option<Subcommand>,
#[clap(flatten)]
pub options: GlobalOptions,
}

impl Cli {
pub async fn run(self) -> Result<()> {
// 1. Enable the appropriate level of tracing / logging
// Enable the appropriate level of tracing / logging
init_tracing(self.options.tracing_level_filter());

// 2. Load Rokit data structures
// If we didn't get a subcommand, we should either print the help,
// or automatically run self-install if launched from the explorer
let (auto_self_install, command) = if let Some(subcommand) = self.subcommand {
(false, subcommand)
} else if ProcessParent::get()
.await
.is_some_and(ProcessParent::is_launcher)
{
let subcommand = Subcommand::SelfInstall(SelfInstallSubcommand {});
(true, subcommand)
} else {
Cli::command().print_help()?;
std::process::exit(0);
};

// Load Rokit data structures
let start_home = Instant::now();
let home = Home::load_from_env().await.context(
"Failed to load Rokit home!\
Expand All @@ -54,18 +70,18 @@ impl Cli {
"Rokit loaded"
);

// 3. Run the subcommand and capture the result - note that we
// do not (!!!) use the question mark operator here, because we
// want to save our data below even if the subcommand fails.
// Run the subcommand and capture the result - note that we
// do not (!!!) use the question mark operator here, because
// we want to save our data below even if the subcommand fails.
let start_command = Instant::now();
let result = self.subcommand.run(&home).await;
let result = command.run(&home).await;
tracing::trace!(
elapsed = ?start_command.elapsed(),
success = result.is_ok(),
"Rokit ran",
);

// 4. Save Rokit data structures to disk
// Save Rokit data structures to disk
let start_save = Instant::now();
home.save().await.context(
"Failed to save Rokit data!\
Expand All @@ -76,7 +92,21 @@ impl Cli {
"Rokit saved"
);

// 5. Return the result of the subcommand
// Wait for user input if we automatically ran the
// self-install from clicking Rokit in the explorer,
// so that the window doesn't immediately close.
if auto_self_install {
dialoguer::Input::new()
.with_prompt("Press Enter to continue")
.show_default(false)
.allow_empty(true)
.report(false)
.default(true)
.interact()
.ok();
}

// Return the result of the subcommand
result
}
}
Expand Down

0 comments on commit c1155e7

Please sign in to comment.