Child process manager api (#64)

* child process api

* added hook to js

* process API + restructured process state storage

* formatting

* added path-pid check and fixed probs

* prettier

* added profile checking function

---------

Co-authored-by: Wyatt <wyatt@modrinth.com>
This commit is contained in:
Wyatt Verchere
2023-04-05 10:07:59 -07:00
committed by GitHub
parent 8169d3ad49
commit d5505d3298
14 changed files with 574 additions and 97 deletions

View File

@@ -1,9 +1,10 @@
//! API for interacting with Theseus
pub mod auth;
pub mod process;
pub mod profile;
pub mod profile_create;
pub mod tags;
pub mod settings;
pub mod tags;
pub mod data {
pub use crate::state::{
@@ -16,6 +17,7 @@ pub mod prelude {
pub use crate::{
auth::{self, Credentials},
data::*,
process,
profile::{self, Profile},
profile_create, settings, State,
};

161
theseus/src/api/process.rs Normal file
View File

@@ -0,0 +1,161 @@
//! Theseus process management interface
use std::path::{Path, PathBuf};
use crate::state::MinecraftChild;
pub use crate::{
state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
},
State,
};
// Gets whether a child process stored in the state by PID has finished
#[tracing::instrument]
pub async fn has_finished_by_pid(pid: u32) -> crate::Result<bool> {
Ok(get_exit_status_by_pid(pid).await?.is_some())
}
// Gets the exit status of a child process stored in the state by PID
#[tracing::instrument]
pub async fn get_exit_status_by_pid(pid: u32) -> crate::Result<Option<i32>> {
let state = State::get().await?;
let children = state.children.read().await;
Ok(children.exit_status(&pid).await?.and_then(|f| f.code()))
}
// Gets the PID of each stored process in the state
#[tracing::instrument]
pub async fn get_all_pids() -> crate::Result<Vec<u32>> {
let state = State::get().await?;
let children = state.children.read().await;
Ok(children.keys())
}
// Gets the PID of each *running* stored process in the state
#[tracing::instrument]
pub async fn get_all_running_pids() -> crate::Result<Vec<u32>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_keys().await
}
// Gets the Profile paths of each *running* stored process in the state
#[tracing::instrument]
pub async fn get_all_running_profile_paths() -> crate::Result<Vec<PathBuf>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_profile_paths().await
}
// Gets the Profiles (cloned) of each *running* stored process in the state
#[tracing::instrument]
pub async fn get_all_running_profiles() -> crate::Result<Vec<Profile>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_profiles().await
}
// Gets the PID of each stored process in the state by profile path
#[tracing::instrument]
pub async fn get_pids_by_profile_path(
profile_path: &Path,
) -> crate::Result<Vec<u32>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_keys_with_profile(profile_path).await
}
// Gets stdout of a child process stored in the state by PID, as a string
#[tracing::instrument]
pub async fn get_stdout_by_pid(pid: u32) -> crate::Result<String> {
let state = State::get().await?;
// Get stdout from child
let children = state.children.read().await;
// Extract child or return crate::Error
if let Some(child) = children.get(&pid) {
let child = child.read().await;
Ok(child.stdout.get_output().await?)
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No child process with PID {}",
pid
))
.as_error())
}
}
// Gets stderr of a child process stored in the state by PID, as a string
#[tracing::instrument]
pub async fn get_stderr_by_pid(pid: u32) -> crate::Result<String> {
let state = State::get().await?;
// Get stdout from child
let children = state.children.read().await;
// Extract child or return crate::Error
if let Some(child) = children.get(&pid) {
let child = child.read().await;
Ok(child.stderr.get_output().await?)
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No child process with PID {}",
pid
))
.as_error())
}
}
// Kill a child process stored in the state by PID, as a string
#[tracing::instrument]
pub async fn kill_by_pid(pid: u32) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
if let Some(mchild) = children.get(&pid) {
let mut mchild = mchild.write().await;
kill(&mut mchild).await
} else {
// No error returned for already finished process
Ok(())
}
}
// Wait for a child process stored in the state by PID
#[tracing::instrument]
pub async fn wait_for_by_pid(pid: u32) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
// No error returned for already killed process
if let Some(mchild) = children.get(&pid) {
let mut mchild = mchild.write().await;
wait_for(&mut mchild).await
} else {
// No error returned for already finished process
Ok(())
}
}
// Kill a running child process directly, and wait for it to be killed
#[tracing::instrument]
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
running.child.kill().await?;
wait_for(running).await
}
// Await on the completion of a child process directly
#[tracing::instrument]
pub async fn wait_for(running: &mut MinecraftChild) -> crate::Result<()> {
let result = running.child.wait().await.map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running minecraft: {err}"
))
})?;
match result.success() {
false => Err(crate::ErrorKind::LauncherError(format!(
"Minecraft exited with non-zero code {}",
result.code().unwrap_or(-1)
))
.as_error()),
true => Ok(()),
}
}

View File

@@ -1,4 +1,5 @@
//! Theseus profile management interface
use crate::state::MinecraftChild;
pub use crate::{
state::{JavaSettings, Profile},
State,
@@ -9,10 +10,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use tokio::{
process::{Child, Command},
sync::RwLock,
};
use tokio::{process::Command, sync::RwLock};
/// Add a profile to the in-memory state
#[tracing::instrument]
@@ -114,7 +112,7 @@ pub async fn list(
pub async fn run(
path: &Path,
credentials: &crate::auth::Credentials,
) -> crate::Result<Arc<RwLock<Child>>> {
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await.unwrap();
let settings = state.settings.read().await;
let profile = get(path).await?.ok_or_else(|| {
@@ -227,31 +225,8 @@ pub async fn run(
"Process failed to stay open.".to_string(),
)
})?;
let child_arc = state_children.insert(pid, mc_process);
let mchild_arc =
state_children.insert_process(pid, path.to_path_buf(), mc_process);
Ok(child_arc)
}
#[tracing::instrument]
pub async fn kill(running: &mut Child) -> crate::Result<()> {
running.kill().await?;
wait_for(running).await
}
#[tracing::instrument]
pub async fn wait_for(running: &mut Child) -> crate::Result<()> {
let result = running.wait().await.map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running minecraft: {err}"
))
})?;
match result.success() {
false => Err(crate::ErrorKind::LauncherError(format!(
"Minecraft exited with non-zero code {}",
result.code().unwrap_or(-1)
))
.as_error()),
true => Ok(()),
}
Ok(mchild_arc)
}

View File

@@ -21,4 +21,4 @@ pub async fn set(settings: Settings) -> crate::Result<()> {
// Replaces the settings struct in the RwLock with the passed argument
*state.settings.write().await = settings;
Ok(())
}
}

View File

@@ -1,5 +1,5 @@
//! Logic for launching Minecraft
use crate::state as st;
use crate::{process, state as st};
use daedalus as d;
use dunce::canonicalize;
use std::{path::Path, process::Stdio};
@@ -176,6 +176,18 @@ pub async fn launch_minecraft(
let env_args = Vec::from(env_args);
// Check if profile has a running profile, and reject running the command if it does
// Done late so a quick double call doesn't launch two instances
let existing_processes =
process::get_pids_by_profile_path(instance_path).await?;
if let Some(pid) = existing_processes.first() {
return Err(crate::ErrorKind::LauncherError(format!(
"Profile {} is already running at PID: {pid}",
instance_path.display()
))
.as_error());
}
command
.args(
args::get_jvm_arguments(
@@ -217,8 +229,8 @@ pub async fn launch_minecraft(
.current_dir(instance_path.clone())
.env_clear()
.envs(env_args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.spawn().map_err(|err| {
crate::ErrorKind::LauncherError(format!(

View File

@@ -1,33 +1,166 @@
use std::path::{Path, PathBuf};
use std::{collections::HashMap, sync::Arc};
use tokio::process::Child;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{ChildStderr, ChildStdout};
use tokio::sync::RwLock;
use super::Profile;
// Child processes (instances of Minecraft)
// A wrapper over a Hashmap connecting PID -> Child
// Left open for future functionality re: polling children
pub struct Children(HashMap<u32, Arc<RwLock<Child>>>);
// A wrapper over a Hashmap connecting PID -> MinecraftChild
pub struct Children(HashMap<u32, Arc<RwLock<MinecraftChild>>>);
// Minecraft Child, bundles together the PID, the actual Child, and the easily queryable stdout and stderr streams
#[derive(Debug)]
pub struct MinecraftChild {
pub pid: u32,
pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID
pub child: tokio::process::Child,
pub stdout: SharedOutput,
pub stderr: SharedOutput,
}
impl Children {
pub fn new() -> Children {
Children(HashMap::new())
}
// Inserts and returns a ref to the child
// Unlike a Hashmap, this directly returns the reference to the Child rather than any previously stored Child that may exist
pub fn insert(
// Inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild
// The threads for stdout and stderr are spawned here
// Unlike a Hashmap's 'insert', this directly returns the reference to the Child rather than any previously stored Child that may exist
pub fn insert_process(
&mut self,
pid: u32,
child: tokio::process::Child,
) -> Arc<RwLock<Child>> {
let child = Arc::new(RwLock::new(child));
self.0.insert(pid, child.clone());
child
profile_path: PathBuf,
mut child: tokio::process::Child,
) -> Arc<RwLock<MinecraftChild>> {
// Create std watcher threads for stdout and stderr
let stdout = SharedOutput::new();
if let Some(child_stdout) = child.stdout.take() {
let stdout_clone = stdout.clone();
tokio::spawn(async move {
if let Err(e) = stdout_clone.read_stdout(child_stdout).await {
eprintln!("Stdout process died with error: {}", e);
}
});
}
let stderr = SharedOutput::new();
if let Some(child_stderr) = child.stderr.take() {
let stderr_clone = stderr.clone();
tokio::spawn(async move {
if let Err(e) = stderr_clone.read_stderr(child_stderr).await {
eprintln!("Stderr thread died with error: {}", e);
}
});
}
// Create MinecraftChild
let mchild = MinecraftChild {
pid,
profile_path,
child,
stdout,
stderr,
};
let mchild = Arc::new(RwLock::new(mchild));
self.0.insert(pid, mchild.clone());
mchild
}
// Returns a ref to the child
pub fn get(&self, pid: &u32) -> Option<Arc<RwLock<Child>>> {
pub fn get(&self, pid: &u32) -> Option<Arc<RwLock<MinecraftChild>>> {
self.0.get(pid).cloned()
}
// Gets all PID keys
pub fn keys(&self) -> Vec<u32> {
self.0.keys().cloned().collect()
}
// Get exit status of a child by PID
// Returns None if the child is still running
pub async fn exit_status(
&self,
pid: &u32,
) -> crate::Result<Option<std::process::ExitStatus>> {
if let Some(child) = self.get(pid) {
let child = child.clone();
let mut child = child.write().await;
Ok(child.child.try_wait()?)
} else {
Ok(None)
}
}
// Gets all PID keys of running children
pub async fn running_keys(&self) -> crate::Result<Vec<u32>> {
let mut keys = Vec::new();
for key in self.keys() {
if let Some(child) = self.get(&key) {
let child = child.clone();
let mut child = child.write().await;
if child.child.try_wait()?.is_none() {
keys.push(key);
}
}
}
Ok(keys)
}
// Gets all PID keys of running children with a given profile path
pub async fn running_keys_with_profile(
&self,
profile_path: &Path,
) -> crate::Result<Vec<u32>> {
let running_keys = self.running_keys().await?;
let mut keys = Vec::new();
for key in running_keys {
if let Some(child) = self.get(&key) {
let child = child.clone();
let child = child.read().await;
if child.profile_path == profile_path {
keys.push(key);
}
}
}
Ok(keys)
}
// Gets all profiles of running children
pub async fn running_profile_paths(&self) -> crate::Result<Vec<PathBuf>> {
let mut profiles = Vec::new();
for key in self.keys() {
if let Some(child) = self.get(&key) {
let child = child.clone();
let mut child = child.write().await;
if child.child.try_wait()?.is_none() {
profiles.push(child.profile_path.clone());
}
}
}
Ok(profiles)
}
// Gets all profiles of running children
// Returns clones because it would be serialized anyway
pub async fn running_profiles(&self) -> crate::Result<Vec<Profile>> {
let mut profiles = Vec::new();
for key in self.keys() {
if let Some(child) = self.get(&key) {
let child = child.clone();
let mut child = child.write().await;
if child.child.try_wait()?.is_none() {
if let Some(prof) =
crate::api::profile::get(&child.profile_path.clone())
.await?
{
profiles.push(prof);
}
}
}
}
Ok(profiles)
}
}
impl Default for Children {
@@ -35,3 +168,58 @@ impl Default for Children {
Self::new()
}
}
// SharedOutput, a wrapper around a String that can be read from and written to concurrently
// Designed to be used with ChildStdout and ChildStderr in a tokio thread to have a simple String storage for the output of a child process
#[derive(Clone, Debug)]
pub struct SharedOutput {
output: Arc<RwLock<String>>,
}
impl SharedOutput {
fn new() -> Self {
SharedOutput {
output: Arc::new(RwLock::new(String::new())),
}
}
// Main entry function to a created SharedOutput, returns the log as a String
pub async fn get_output(&self) -> crate::Result<String> {
let output = self.output.read().await;
Ok(output.clone())
}
async fn read_stdout(
&self,
child_stdout: ChildStdout,
) -> crate::Result<()> {
let mut buf_reader = BufReader::new(child_stdout);
let mut line = String::new();
while buf_reader.read_line(&mut line).await? > 0 {
{
let mut output = self.output.write().await;
output.push_str(&line);
}
line.clear();
}
Ok(())
}
async fn read_stderr(
&self,
child_stderr: ChildStderr,
) -> crate::Result<()> {
let mut buf_reader = BufReader::new(child_stderr);
let mut line = String::new();
while buf_reader.read_line(&mut line).await? > 0 {
{
let mut output = self.output.write().await;
output.push_str(&line);
}
line.clear();
}
Ok(())
}
}