From 47970d932bb705d94b37e4e3dbc2c8b8de7c3697 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 26 Jun 2023 14:29:53 -0700 Subject: [PATCH] Exports (#135) * Initial bug fixes * fix compile error on non-mac * Fix even more bugs * Fix more * fix more * fix build * fix build * working basic * removed zip * working functions * merge fixes * fixed loadintg bar bug * changed to one layer deep * forge version numbers * overrides dont include mrpack * merge * fixes * fixes * fixed deletion * merge errors * force sync before export * removed testing * missed line * removed console log * mac error reverted --------- Co-authored-by: Jai A --- Cargo.lock | 1 + theseus/Cargo.toml | 1 + theseus/src/api/pack.rs | 14 +- theseus/src/api/profile.rs | 45 +++ theseus/src/error.rs | 9 +- theseus/src/event/mod.rs | 6 +- theseus/src/state/profiles.rs | 75 ++-- theseus/src/util/export.rs | 335 ++++++++++++++++++ theseus/src/util/mod.rs | 1 + theseus_gui/src-tauri/src/api/profile.rs | 36 ++ theseus_gui/src-tauri/src/api/utils.rs | 7 +- theseus_gui/src-tauri/src/main.rs | 8 +- .../src/components/ui/RunningAppBar.vue | 6 +- theseus_gui/src/helpers/profile.js | 24 ++ 14 files changed, 509 insertions(+), 59 deletions(-) create mode 100644 theseus/src/util/export.rs diff --git a/Cargo.lock b/Cargo.lock index 8d1d34b66..9432370b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4569,6 +4569,7 @@ dependencies = [ name = "theseus" version = "0.2.2" dependencies = [ + "async-recursion", "async-tungstenite", "async_zip", "bytes", diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index c50c29946..98de7da26 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -42,6 +42,7 @@ futures = "0.3" reqwest = { version = "0.11", features = ["json", "stream"] } tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1", features = ["fs"] } +async-recursion = "1.0.4" notify = { version = "5.1.0", default-features = false } notify-debouncer-mini = { version = "0.2.1", default-features = false } diff --git a/theseus/src/api/pack.rs b/theseus/src/api/pack.rs index 4e9860e0b..a9152ca5d 100644 --- a/theseus/src/api/pack.rs +++ b/theseus/src/api/pack.rs @@ -22,7 +22,7 @@ use tokio::fs; #[derive(Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] -struct PackFormat { +pub struct PackFormat { pub game: String, pub format_version: i32, pub version_id: String, @@ -34,7 +34,7 @@ struct PackFormat { #[derive(Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] -struct PackFile { +pub struct PackFile { pub path: String, pub hashes: HashMap, pub env: Option>, @@ -44,7 +44,7 @@ struct PackFile { #[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(rename_all = "camelCase", from = "String")] -enum PackFileHash { +pub enum PackFileHash { Sha1, Sha512, Unknown(String), @@ -62,14 +62,14 @@ impl From for PackFileHash { #[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(rename_all = "camelCase")] -enum EnvType { +pub enum EnvType { Client, Server, } #[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] -enum PackDependency { +pub enum PackDependency { Forge, FabricLoader, QuiltLoader, @@ -299,7 +299,9 @@ async fn install_pack( mod_loader = Some(ModLoader::Quilt); loader_version = Some(value); } - PackDependency::Minecraft => game_version = Some(value), + PackDependency::Minecraft => { + game_version = Some(value.clone()) + } } } diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index 0612a49b0..196f0b636 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -3,6 +3,7 @@ use crate::event::emit::{init_loading, loading_try_for_each_concurrent}; use crate::event::LoadingBarType; use crate::prelude::JavaVersion; use crate::state::ProjectMetadata; +use crate::util::export; use crate::{ auth::{self, refresh}, event::{emit::emit_profile, ProfilePayloadType}, @@ -490,6 +491,50 @@ pub async fn remove_project( } } +/// Exports the profile to a Modrinth-formatted .mrpack file +// Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) +#[tracing::instrument(skip_all)] +pub async fn export_mrpack( + profile_path: &Path, + export_path: PathBuf, + included_overrides: Vec, // which folders to include in the overrides + version_id: Option, +) -> crate::Result<()> { + let state = State::get().await?; + let io_semaphore = state.io_semaphore.0.read().await; + let permit: tokio::sync::SemaphorePermit = io_semaphore.acquire().await?; + let profile = get(profile_path, None).await?.ok_or_else(|| { + crate::ErrorKind::OtherError(format!( + "Tried to export a nonexistent or unloaded profile at path {}!", + profile_path.display() + )) + })?; + export::export_mrpack( + &profile, + &export_path, + version_id.unwrap_or("1.0.0".to_string()), + included_overrides, + true, + &permit, + ) + .await?; + Ok(()) +} + +// Given a folder path, populate a Vec of all the subfolders +// Intended to be used for finding potential override folders +// profile +// -- folder1 +// -- folder2 +// -- file1 +// => [folder1, folder2] +#[tracing::instrument] +pub async fn get_potential_override_folders( + profile_path: PathBuf, +) -> crate::Result> { + export::get_potential_override_folders(profile_path).await +} + /// Run Minecraft using a profile and the default credentials, logged in credentials, /// failing with an error if no credentials are available #[tracing::instrument] diff --git a/theseus/src/error.rs b/theseus/src/error.rs index cfd624750..d228ae3c5 100644 --- a/theseus/src/error.rs +++ b/theseus/src/error.rs @@ -82,11 +82,14 @@ pub enum ErrorKind { #[error("Zip error: {0}")] ZipError(#[from] async_zip::error::ZipError), - #[error("Error: {0}")] - OtherError(String), - #[error("File watching error: {0}")] NotifyError(#[from] notify::Error), + + #[error("Error stripping prefix: {0}")] + StripPrefixError(#[from] std::path::StripPrefixError), + + #[error("Error: {0}")] + OtherError(String), } #[derive(Debug)] diff --git a/theseus/src/event/mod.rs b/theseus/src/event/mod.rs index a3d550aa0..08879368a 100644 --- a/theseus/src/event/mod.rs +++ b/theseus/src/event/mod.rs @@ -115,7 +115,7 @@ impl Drop for LoadingBarId { loader_uuid, }, ); - tracing::debug!( + tracing::trace!( "Exited at {fraction} for loading bar: {:?}", loader_uuid ); @@ -165,6 +165,10 @@ pub enum LoadingBarType { profile_path: PathBuf, profile_name: String, }, + ZipExtract { + profile_path: PathBuf, + profile_name: String, + }, } #[derive(Serialize, Clone)] diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 337760e10..6136c4f30 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -224,44 +224,7 @@ impl Profile { pub fn sync_projects_task(path: PathBuf) { tokio::task::spawn(async move { - let res = async { - let state = State::get().await?; - let profile = crate::api::profile::get(&path, None).await?; - - if let Some(profile) = profile { - let paths = profile.get_profile_project_paths()?; - - let projects = crate::state::infer_data_from_files( - profile.clone(), - paths, - state.directories.caches_dir(), - &state.io_semaphore, - &state.fetch_semaphore, - ) - .await?; - - let mut new_profiles = state.profiles.write().await; - if let Some(profile) = new_profiles.0.get_mut(&path) { - profile.projects = projects; - } - - emit_profile( - profile.uuid, - profile.path, - &profile.metadata.name, - ProfilePayloadType::Synced, - ) - .await?; - } else { - tracing::warn!( - "Unable to fetch single profile projects: path {path:?} invalid", - ); - } - - Ok::<(), crate::Error>(()) - } - .await; - + let res = Self::sync_projects_inner(path).await; match res { Ok(()) => {} Err(err) => { @@ -273,6 +236,42 @@ impl Profile { }); } + pub async fn sync_projects_inner(path: PathBuf) -> crate::Result<()> { + let state = State::get().await?; + let profile = crate::api::profile::get(&path, None).await?; + + if let Some(profile) = profile { + let paths = profile.get_profile_project_paths()?; + + let projects = crate::state::infer_data_from_files( + profile.clone(), + paths, + state.directories.caches_dir(), + &state.io_semaphore, + &state.fetch_semaphore, + ) + .await?; + + let mut new_profiles = state.profiles.write().await; + if let Some(profile) = new_profiles.0.get_mut(&path) { + profile.projects = projects; + } + + emit_profile( + profile.uuid, + profile.path, + &profile.metadata.name, + ProfilePayloadType::Synced, + ) + .await?; + } else { + tracing::warn!( + "Unable to fetch single profile projects: path {path:?} invalid", + ); + } + Ok::<(), crate::Error>(()) + } + pub fn get_profile_project_paths(&self) -> crate::Result> { let mut files = Vec::new(); let mut read_paths = |path: &str| { diff --git a/theseus/src/util/export.rs b/theseus/src/util/export.rs new file mode 100644 index 000000000..7fc0a6cde --- /dev/null +++ b/theseus/src/util/export.rs @@ -0,0 +1,335 @@ +//! Functions for fetching infromation from the Internet +use crate::event::emit::{emit_loading, init_loading}; +use crate::pack::{ + EnvType, PackDependency, PackFile, PackFileHash, PackFormat, +}; +use crate::process::Profile; +use crate::profile::get; +use crate::LoadingBarType; +use async_zip::tokio::write::ZipFileWriter; +use async_zip::{Compression, ZipEntryBuilder}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use tokio::fs::{self, File}; +use tokio::io::AsyncReadExt; +use tokio::sync::SemaphorePermit; + +/// Creates a .mrpack (Modrinth zip file) for a given modpack +// Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) +#[tracing::instrument(skip_all)] +#[theseus_macros::debug_pin] +pub async fn export_mrpack( + profile: &Profile, + export_location: &Path, + version_id: String, + included_overrides: Vec, // which folders to include in the overrides + loading_bar: bool, + _semaphore: &SemaphorePermit<'_>, +) -> crate::Result<()> { + let profile_base_path = &profile.path; + + let mut file = File::create(export_location).await?; + let mut writer = ZipFileWriter::new(&mut file); + + // Create mrpack json configuration file + let packfile = create_mrpack_json(profile, version_id)?; + let modrinth_path_list = get_modrinth_pack_list(&packfile); + + // Build vec of all files in the folder + let mut path_list = Vec::new(); + build_folder(profile_base_path, &mut path_list).await?; + + // Initialize loading bar + let loading_bar = if loading_bar { + Some( + init_loading( + LoadingBarType::ZipExtract { + profile_path: profile.path.to_path_buf(), + profile_name: profile.metadata.name.clone(), + }, + path_list.len() as f64, + "Exporting profile to .mrpack", + ) + .await?, + ) + } else { + None + }; + + // Iterate over every file in the folder + // Every file that is NOT in the config file is added to the zip, in overrides + for path in path_list { + if let Some(ref loading_bar) = loading_bar { + emit_loading(loading_bar, 1.0, None).await?; + } + + // Get local path of file, relative to profile folder + let relative_path = path.strip_prefix(profile_base_path)?; + + // Get highest level folder pair ('a/b' in 'a/b/c', 'a' in 'a') + // We only go one layer deep for the sake of not having a huge list of overrides + let topmost_two = relative_path + .iter() + .take(2) + .map(|os| os.to_string_lossy().to_string()) + .collect::>(); + + // a,b => a/b + // a => a + let topmost = match topmost_two.len() { + 2 => topmost_two.join("/"), + 1 => topmost_two[0].clone(), + _ => { + return Err(crate::ErrorKind::OtherError( + "No topmost folder found".to_string(), + ) + .into()) + } + }; + + if !included_overrides.contains(&topmost) { + continue; + } + + let relative_path: std::borrow::Cow = + relative_path.to_string_lossy(); + let relative_path = relative_path.replace('\\', "/"); + let relative_path = relative_path.trim_start_matches('/').to_string(); + + if modrinth_path_list.contains(&relative_path) { + continue; + } + + // File is not in the config file, add it to the .mrpack zip + if path.is_file() { + let mut file = File::open(&path).await?; + let mut data = Vec::new(); + file.read_to_end(&mut data).await?; + let builder = ZipEntryBuilder::new( + format!("overrides/{relative_path}"), + Compression::Deflate, + ); + writer.write_entry_whole(builder, &data).await?; + } + } + + // Add modrinth json to the zip + let data = serde_json::to_vec_pretty(&packfile)?; + let builder = ZipEntryBuilder::new( + "modrinth.index.json".to_string(), + Compression::Deflate, + ); + writer.write_entry_whole(builder, &data).await?; + + writer.close().await?; + Ok(()) +} + +fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec { + packfile + .files + .iter() + .map(|f| { + let path = PathBuf::from(f.path.clone()); + let name = path.to_string_lossy(); + let name = name.replace('\\', "/"); + name.trim_start_matches('/').to_string() + }) + .collect::>() +} + +/// Creates a json configuration for a .mrpack zipped file +// Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) +#[tracing::instrument(skip_all)] +pub fn create_mrpack_json( + profile: &Profile, + version_id: String, +) -> crate::Result { + // Add loader version to dependencies + let mut dependencies = HashMap::new(); + match ( + profile.metadata.loader, + profile.metadata.loader_version.clone(), + ) { + (crate::prelude::ModLoader::Forge, Some(v)) => { + dependencies.insert(PackDependency::Forge, v.id) + } + (crate::prelude::ModLoader::Fabric, Some(v)) => { + dependencies.insert(PackDependency::FabricLoader, v.id) + } + (crate::prelude::ModLoader::Quilt, Some(v)) => { + dependencies.insert(PackDependency::QuiltLoader, v.id) + } + (crate::prelude::ModLoader::Vanilla, _) => None, + _ => { + return Err(crate::ErrorKind::OtherError( + "Loader version mismatch".to_string(), + ) + .into()) + } + }; + dependencies.insert( + PackDependency::Minecraft, + profile.metadata.game_version.clone(), + ); + + // Converts a HashMap to a HashMap + // But the values are sanitized to only include the version number + let dependencies = dependencies + .into_iter() + .map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string())) + .collect::>(); + + let base_path = &profile.path; + let files: Result, crate::ErrorKind> = profile + .projects + .iter() + .filter_map(|(mod_path, project)| { + let path = match mod_path.strip_prefix(base_path) { + Ok(path) => path.to_string_lossy().to_string(), + Err(e) => { + return Some(Err(e.into())); + } + }; + + // Only Modrinth projects have a modrinth metadata field for the modrinth.json + Some(Ok(match project.metadata { + crate::prelude::ProjectMetadata::Modrinth { + ref project, + ref version, + .. + } => { + let mut env = HashMap::new(); + env.insert(EnvType::Client, project.client_side.clone()); + env.insert(EnvType::Server, project.server_side.clone()); + + let primary_file = if let Some(primary_file) = + version.files.first() + { + primary_file + } else { + return Some(Err(crate::ErrorKind::OtherError( + format!("No primary file found for mod at: {path}"), + ))); + }; + + let file_size = primary_file.size; + let downloads = vec![primary_file.url.clone()]; + let hashes = primary_file + .hashes + .clone() + .into_iter() + .map(|(h1, h2)| (PackFileHash::from(h1), h2)) + .collect(); + + PackFile { + path, + hashes, + env: Some(env), + downloads, + file_size, + } + } + // Inferred files are skipped for the modrinth.json + crate::prelude::ProjectMetadata::Inferred { .. } => { + return None + } + // Unknown projects are skipped for the modrinth.json + crate::prelude::ProjectMetadata::Unknown => return None, + })) + }) + .collect(); + let files = files?; + + Ok(PackFormat { + game: "minecraft".to_string(), + format_version: 1, + version_id, + name: profile.metadata.name.clone(), + summary: None, + files, + dependencies, + }) +} + +fn sanitize_loader_version_string(s: &str) -> &str { + // Split on '-' + // If two or more, take the second + // If one, take the first + // If none, take the whole thing + let mut split: std::str::Split<'_, char> = s.split('-'); + match split.next() { + Some(first) => match split.next() { + Some(second) => second, + None => first, + }, + None => s, + } +} + +// Given a folder path, populate a Vec of all the files in the folder, recursively +#[async_recursion::async_recursion] +pub async fn build_folder( + path: &Path, + path_list: &mut Vec, +) -> crate::Result<()> { + let mut read_dir = fs::read_dir(path).await?; + while let Some(entry) = read_dir.next_entry().await? { + let path = entry.path(); + if path.is_dir() { + build_folder(&path, path_list).await?; + } else { + path_list.push(path); + } + } + Ok(()) +} + +// Given a folder path, populate a Vec of all the subfolders +// Intended to be used for finding potential override folders +// profile +// -- folder1 +// -- folder2 +// ----- file2 +// ----- folder3 +// ------- folder4 +// -- file1 +// => [folder1, folder2, fil2, folder3, file1] +pub async fn get_potential_override_folders( + profile_path: PathBuf, +) -> crate::Result> { + // First, get a dummy mrpack json for the files within + let profile: Profile = + get(&profile_path, None).await?.ok_or_else(|| { + crate::ErrorKind::OtherError(format!( + "Tried to export a nonexistent or unloaded profile at path {}!", + profile_path.display() + )) + })?; + let mrpack = create_mrpack_json(&profile, "0".to_string())?; + let mrpack_files = get_modrinth_pack_list(&mrpack); + + let mut path_list: Vec = Vec::new(); + let mut read_dir = fs::read_dir(&profile_path).await?; + while let Some(entry) = read_dir.next_entry().await? { + let path: PathBuf = entry.path(); + if path.is_dir() { + // Two layers of files/folders if its a folder + let mut read_dir = fs::read_dir(&path).await?; + while let Some(entry) = read_dir.next_entry().await? { + let path: PathBuf = entry.path(); + let name = path.strip_prefix(&profile_path)?.to_path_buf(); + if !mrpack_files.contains(&name.to_string_lossy().to_string()) { + path_list.push(name); + } + } + } else { + // One layer of files/folders if its a file + let name = path.strip_prefix(&profile_path)?.to_path_buf(); + if !mrpack_files.contains(&name.to_string_lossy().to_string()) { + path_list.push(name); + } + } + } + Ok(path_list) +} diff --git a/theseus/src/util/mod.rs b/theseus/src/util/mod.rs index a3a4c7937..dfc405165 100644 --- a/theseus/src/util/mod.rs +++ b/theseus/src/util/mod.rs @@ -1,4 +1,5 @@ //! Theseus utility functions +pub mod export; pub mod fetch; pub mod jre; pub mod platform; diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index b7b76b252..864bfcf78 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -134,6 +134,42 @@ pub async fn profile_remove_project( profile::remove_project(path, project_path).await?; Ok(()) } + +// Exports a profile to a .mrpack file (export_location should end in .mrpack) +// invoke('profile_export_mrpack') +#[tauri::command] +pub async fn profile_export_mrpack( + path: &Path, + export_location: PathBuf, + included_overrides: Vec, + version_id: Option, +) -> Result<()> { + profile::export_mrpack( + path, + export_location, + included_overrides, + version_id, + ) + .await?; + Ok(()) +} + +// Given a folder path, populate a Vec of all the subfolders +// Intended to be used for finding potential override folders +// profile +// -- folder1 +// -- folder2 +// -- file1 +// => [folder1, folder2] +#[tauri::command] +pub async fn profile_get_potential_override_folders( + profile_path: PathBuf, +) -> Result> { + let overrides = + profile::get_potential_override_folders(profile_path).await?; + Ok(overrides) +} + // Run minecraft using a profile using the default credentials // Returns the UUID, which can be used to poll // for the actual Child in the state. diff --git a/theseus_gui/src-tauri/src/api/utils.rs b/theseus_gui/src-tauri/src/api/utils.rs index 6d9683bc5..68bd47fb8 100644 --- a/theseus_gui/src-tauri/src/api/utils.rs +++ b/theseus_gui/src-tauri/src/api/utils.rs @@ -34,16 +34,15 @@ pub fn show_in_folder(path: String) -> Result<()> { #[cfg(target_os = "linux")] { - use std::fs; use std::fs::metadata; use std::path::PathBuf; - if path.contains(",") { + if path.contains(',') { // see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76 let new_path = match metadata(&path)?.is_dir() { - true => path.clone(), + true => path, false => { - let mut path2 = PathBuf::from(path.clone()); + let mut path2 = PathBuf::from(path); path2.pop(); path2.to_string_lossy().to_string() } diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index d7c8cf51f..d9d0735a4 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -24,11 +24,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { #[tauri::command] fn is_dev() -> bool { - if cfg!(debug_assertions) { - true - } else { - false - } + cfg!(debug_assertions) } use tracing_subscriber::prelude::*; @@ -167,6 +163,8 @@ fn main() { api::jre::jre_get_jre, api::jre::jre_auto_install_java, api::jre::jre_get_max_memory, + api::profile::profile_export_mrpack, + api::profile::profile_get_potential_override_folders, api::process::process_get_all_uuids, api::process::process_get_all_running_uuids, api::process::process_get_uuids_by_profile_path, diff --git a/theseus_gui/src/components/ui/RunningAppBar.vue b/theseus_gui/src/components/ui/RunningAppBar.vue index 350097476..d929aaee3 100644 --- a/theseus_gui/src/components/ui/RunningAppBar.vue +++ b/theseus_gui/src/components/ui/RunningAppBar.vue @@ -49,8 +49,10 @@

{{ loadingBar.title }}

- -
{{ Math.floor(loadingBar.current) }}% {{ loadingBar.message }}
+ +
+ {{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% {{ loadingBar.message }} +
diff --git a/theseus_gui/src/helpers/profile.js b/theseus_gui/src/helpers/profile.js index 85358eaa1..63ba1a6b6 100644 --- a/theseus_gui/src/helpers/profile.js +++ b/theseus_gui/src/helpers/profile.js @@ -89,6 +89,30 @@ export async function remove_project(path, projectPath) { return await invoke('profile_remove_project', { path, projectPath }) } +// Export a profile to .mrpack +/// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs') +// Version id is optional (ie: 1.1.5) +export async function export_profile_mrpack(path, exportLocation, includedOverrides, versionId) { + return await invoke('profile_export_mrpack', { + path, + exportLocation, + includedOverrides, + versionId, + }) +} + +// Given a folder path, populate an array of all the subfolders +// Intended to be used for finding potential override folders +// profile +// -- mods +// -- resourcepacks +// -- file1 +// => [mods, resourcepacks] +// allows selection for 'included_overrides' in export_profile_mrpack +export async function get_potential_override_folders(profilePath) { + return await invoke('profile_get_potential_override_folders', { profilePath }) +} + // Run Minecraft using a pathed profile // Returns PID of child export async function run(path) {