diff --git a/.vscode/launch.json b/.vscode/launch.json index c7fe3f95..9b074f96 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "cargo": { "args": [ "build", - "--manifest-path=mbf-agent/Cargo.toml" + "--manifest-path=mbf-agent-runnable/Cargo.toml" ] }, "args": [] diff --git a/Cargo.lock b/Cargo.lock index 049e65ac..5952a644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -782,7 +782,7 @@ dependencies = [ ] [[package]] -name = "mbf-agent" +name = "mbf-agent-core" version = "0.1.0" dependencies = [ "anyhow", @@ -801,6 +801,17 @@ dependencies = [ "xml", ] +[[package]] +name = "mbf-agent-runnable" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "mbf-agent-core", + "serde", + "serde_json", +] + [[package]] name = "mbf-res-man" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2d7b8db5..682399a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [workspace] resolver = "2" -members = [ +members = [ "mbf-adb-killer", - "mbf-agent", + "mbf-agent-core", + "mbf-agent-runnable", "mbf-res-man", - "mbf-zip", + "mbf-zip", ] [profile.dev.package."*"] diff --git a/README.md b/README.md index 27eeb38c..76304f7a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ MBF has some query parameters which can be passed with the URL. These are useful ## Project Structure -- `./mbf-agent` contains the agent, which is an executable written in Rust that is executed by the frontend via ADB. This agent does pretty much all the work, including installing mods and patching the game. +- `./mbf-agent-core` contains the agent core written in Rust. This agent does pretty much all the work, including installing mods and patching the game. +- `./mbf-agent-runnable` contains the runnable agent binary, installed on the Quest, which is an executable that is executed by the frontend via ADB. Handles almost all work required through stdin/stdout. - `./mbf-agent-wrapper` is a Python script that can be used to invoke the MBF backend with a command-line-interface, handy for developers or Chromium-haters. - `./mbf-adb-killer` is a development utility that kills any running ADB server when the frontend tries to connect to your Quest, thus avoiding conflicts between MBF and other apps *during development only.*. - `./mbf-res-man` contains the MBF resource management project, which contains code used by MBF to access external resources e.g. core mods, but also for updating its own resource repositories, e.g. [MBF Diffs](https://github.com/Lauriethefish/mbf-diffs/releases) whenever a new version of Beat Saber is released. diff --git a/build_agent.ps1 b/build_agent.ps1 index f3389aa6..c41a5a88 100644 --- a/build_agent.ps1 +++ b/build_agent.ps1 @@ -10,7 +10,7 @@ $TARGET = "aarch64-linux-android" $OutputDirectory = "$PSScriptRoot/mbf-site/public" $AgentDetailsOutputPath = "$PSScriptRoot/mbf-site/src/agent_manifest.ts" $OutputPath = "$OutputDirectory/mbf-agent" -$CargoManifestPath = "$PSScriptRoot/mbf-agent/Cargo.toml" +$CargoManifestPath = "$PSScriptRoot/mbf-agent-runnable/Cargo.toml" $Command = "cargo build --manifest-path `"$CargoManifestPath`" --target $TARGET" if ( $release -eq $true ) diff --git a/mbf-agent/.gitignore b/mbf-agent-core/.gitignore similarity index 81% rename from mbf-agent/.gitignore rename to mbf-agent-core/.gitignore index 3b64e949..ceaae1b4 100644 --- a/mbf-agent/.gitignore +++ b/mbf-agent-core/.gitignore @@ -5,5 +5,3 @@ *.diff *.xml index.json - -mbf-agent diff --git a/mbf-agent/.vscode/settings.json b/mbf-agent-core/.vscode/settings.json similarity index 100% rename from mbf-agent/.vscode/settings.json rename to mbf-agent-core/.vscode/settings.json diff --git a/mbf-agent/Cargo.toml b/mbf-agent-core/Cargo.toml similarity index 95% rename from mbf-agent/Cargo.toml rename to mbf-agent-core/Cargo.toml index 7c317e81..897253a7 100644 --- a/mbf-agent/Cargo.toml +++ b/mbf-agent-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "mbf-agent" +name = "mbf-agent-core" version = "0.1.0" edition = "2021" diff --git a/mbf-agent/build.rs b/mbf-agent-core/build.rs similarity index 100% rename from mbf-agent/build.rs rename to mbf-agent-core/build.rs diff --git a/mbf-agent/src/axml/axml2xml.rs b/mbf-agent-core/src/axml/axml2xml.rs similarity index 100% rename from mbf-agent/src/axml/axml2xml.rs rename to mbf-agent-core/src/axml/axml2xml.rs diff --git a/mbf-agent/src/axml/mod.rs b/mbf-agent-core/src/axml/mod.rs similarity index 100% rename from mbf-agent/src/axml/mod.rs rename to mbf-agent-core/src/axml/mod.rs diff --git a/mbf-agent/src/axml/reader.rs b/mbf-agent-core/src/axml/reader.rs similarity index 100% rename from mbf-agent/src/axml/reader.rs rename to mbf-agent-core/src/axml/reader.rs diff --git a/mbf-agent/src/axml/res_ids.rs b/mbf-agent-core/src/axml/res_ids.rs similarity index 100% rename from mbf-agent/src/axml/res_ids.rs rename to mbf-agent-core/src/axml/res_ids.rs diff --git a/mbf-agent/src/axml/resourceIds.bin b/mbf-agent-core/src/axml/resourceIds.bin similarity index 100% rename from mbf-agent/src/axml/resourceIds.bin rename to mbf-agent-core/src/axml/resourceIds.bin diff --git a/mbf-agent/src/axml/writer.rs b/mbf-agent-core/src/axml/writer.rs similarity index 100% rename from mbf-agent/src/axml/writer.rs rename to mbf-agent-core/src/axml/writer.rs diff --git a/mbf-agent/src/data_fix.rs b/mbf-agent-core/src/data_fix.rs similarity index 100% rename from mbf-agent/src/data_fix.rs rename to mbf-agent-core/src/data_fix.rs diff --git a/mbf-agent/src/debug_cert.pem b/mbf-agent-core/src/debug_cert.pem similarity index 100% rename from mbf-agent/src/debug_cert.pem rename to mbf-agent-core/src/debug_cert.pem diff --git a/mbf-agent/src/downgrading.rs b/mbf-agent-core/src/downgrading.rs similarity index 100% rename from mbf-agent/src/downgrading.rs rename to mbf-agent-core/src/downgrading.rs diff --git a/mbf-agent/src/downloads.rs b/mbf-agent-core/src/downloads.rs similarity index 100% rename from mbf-agent/src/downloads.rs rename to mbf-agent-core/src/downloads.rs diff --git a/mbf-agent/src/handlers/import.rs b/mbf-agent-core/src/handlers/import.rs similarity index 100% rename from mbf-agent/src/handlers/import.rs rename to mbf-agent-core/src/handlers/import.rs diff --git a/mbf-agent/src/handlers/mod.rs b/mbf-agent-core/src/handlers/mod.rs similarity index 100% rename from mbf-agent/src/handlers/mod.rs rename to mbf-agent-core/src/handlers/mod.rs diff --git a/mbf-agent/src/handlers/mod_management.rs b/mbf-agent-core/src/handlers/mod_management.rs similarity index 100% rename from mbf-agent/src/handlers/mod_management.rs rename to mbf-agent-core/src/handlers/mod_management.rs diff --git a/mbf-agent/src/handlers/mod_status.rs b/mbf-agent-core/src/handlers/mod_status.rs similarity index 100% rename from mbf-agent/src/handlers/mod_status.rs rename to mbf-agent-core/src/handlers/mod_status.rs diff --git a/mbf-agent/src/handlers/patching.rs b/mbf-agent-core/src/handlers/patching.rs similarity index 100% rename from mbf-agent/src/handlers/patching.rs rename to mbf-agent-core/src/handlers/patching.rs diff --git a/mbf-agent/src/handlers/utility.rs b/mbf-agent-core/src/handlers/utility.rs similarity index 100% rename from mbf-agent/src/handlers/utility.rs rename to mbf-agent-core/src/handlers/utility.rs diff --git a/mbf-agent-core/src/lib.rs b/mbf-agent-core/src/lib.rs new file mode 100644 index 00000000..12df48d2 --- /dev/null +++ b/mbf-agent-core/src/lib.rs @@ -0,0 +1,88 @@ +pub mod axml; +pub mod data_fix; +pub mod downgrading; +pub mod downloads; +pub mod handlers; +pub mod manifest; +pub mod mod_man; +pub mod models; +pub mod parameters; +pub mod patching; + +use anyhow::{Context, Result}; +use downloads::DownloadConfig; +use log::{debug, warn}; +use mbf_res_man::res_cache::ResCache; +use parameters::PARAMETERS; +use serde::{Deserialize, Serialize}; +use std::{path::Path, process::Command, sync}; + +#[cfg(feature = "request_timing")] +use log::info; +#[cfg(feature = "request_timing")] +use std::time::Instant; + +/// Attempts to delete legacy directories no longer used by MBF to free up space +/// Logs on failure +pub fn try_delete_legacy_dirs() { + for dir in &PARAMETERS.legacy_dirs { + if Path::new(dir).exists() { + match std::fs::remove_dir_all(dir) { + Ok(_) => debug!("Successfully removed legacy dir {dir}"), + Err(err) => warn!("Failed to remove legacy dir {dir}: {err}"), + } + } + } +} + +static DOWNLOAD_CFG: sync::OnceLock = sync::OnceLock::new(); + +/// Gets the default config used for downloads in MBF +pub fn get_dl_cfg() -> &'static DownloadConfig<'static> { + DOWNLOAD_CFG.get_or_init(|| { + DownloadConfig { + max_disconnections: 10, + // If downloads data successfully for 10 seconds, reset disconnection attempts + disconnection_reset_time: Some(std::time::Duration::from_secs_f32(10.0)), + disconnect_wait_time: std::time::Duration::from_secs_f32(5.0), + progress_update_interval: Some(std::time::Duration::from_secs_f32(2.0)), + ureq_agent: mbf_res_man::default_agent::get_agent(), + } + }) +} + +/// Creates a ResCache for downloading files using mbf_res_man +/// This should be reused where possible. +pub fn load_res_cache() -> Result> { + std::fs::create_dir_all(&PARAMETERS.res_cache).expect("Failed to create resource cache folder"); + Ok(ResCache::new( + (&PARAMETERS.res_cache).into(), + mbf_res_man::default_agent::get_agent(), + )) +} + +pub fn get_apk_path() -> Result> { + let pm_output = Command::new("pm") + .args(["path", &PARAMETERS.apk_id]) + .output() + .context("Working out APK path")?; + if 8 > pm_output.stdout.len() { + // App not installed + Ok(None) + } else { + Ok(Some( + std::str::from_utf8(pm_output.stdout.split_at(8).1)? + .trim_end() + .to_owned(), + )) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +struct ModTag { + patcher_name: String, + patcher_version: Option, + modloader_name: String, + modloader_version: Option, +} diff --git a/mbf-agent/src/manifest.rs b/mbf-agent-core/src/manifest.rs similarity index 100% rename from mbf-agent/src/manifest.rs rename to mbf-agent-core/src/manifest.rs diff --git a/mbf-agent/src/mod_man/loaded_mod.rs b/mbf-agent-core/src/mod_man/loaded_mod.rs similarity index 100% rename from mbf-agent/src/mod_man/loaded_mod.rs rename to mbf-agent-core/src/mod_man/loaded_mod.rs diff --git a/mbf-agent/src/mod_man/manifest.rs b/mbf-agent-core/src/mod_man/manifest.rs similarity index 100% rename from mbf-agent/src/mod_man/manifest.rs rename to mbf-agent-core/src/mod_man/manifest.rs diff --git a/mbf-agent/src/mod_man/mod.rs b/mbf-agent-core/src/mod_man/mod.rs similarity index 100% rename from mbf-agent/src/mod_man/mod.rs rename to mbf-agent-core/src/mod_man/mod.rs diff --git a/mbf-agent/src/mod_man/qmod_schema.json b/mbf-agent-core/src/mod_man/qmod_schema.json similarity index 100% rename from mbf-agent/src/mod_man/qmod_schema.json rename to mbf-agent-core/src/mod_man/qmod_schema.json diff --git a/mbf-agent/src/mod_man/util.rs b/mbf-agent-core/src/mod_man/util.rs similarity index 100% rename from mbf-agent/src/mod_man/util.rs rename to mbf-agent-core/src/mod_man/util.rs diff --git a/mbf-agent/src/models/mod.rs b/mbf-agent-core/src/models/mod.rs similarity index 100% rename from mbf-agent/src/models/mod.rs rename to mbf-agent-core/src/models/mod.rs diff --git a/mbf-agent/src/models/request.rs b/mbf-agent-core/src/models/request.rs similarity index 100% rename from mbf-agent/src/models/request.rs rename to mbf-agent-core/src/models/request.rs diff --git a/mbf-agent/src/models/response.rs b/mbf-agent-core/src/models/response.rs similarity index 100% rename from mbf-agent/src/models/response.rs rename to mbf-agent-core/src/models/response.rs diff --git a/mbf-agent/src/parameters.rs b/mbf-agent-core/src/parameters.rs similarity index 100% rename from mbf-agent/src/parameters.rs rename to mbf-agent-core/src/parameters.rs diff --git a/mbf-agent/src/patching.rs b/mbf-agent-core/src/patching.rs similarity index 100% rename from mbf-agent/src/patching.rs rename to mbf-agent-core/src/patching.rs diff --git a/mbf-agent/src/requests.rs b/mbf-agent-core/src/requests.rs similarity index 100% rename from mbf-agent/src/requests.rs rename to mbf-agent-core/src/requests.rs diff --git a/mbf-agent-runnable/.gitignore b/mbf-agent-runnable/.gitignore new file mode 100644 index 00000000..ceaae1b4 --- /dev/null +++ b/mbf-agent-runnable/.gitignore @@ -0,0 +1,7 @@ +/target +*.apk +*.so +*.obb +*.diff +*.xml +index.json diff --git a/mbf-agent-runnable/.vscode/settings.json b/mbf-agent-runnable/.vscode/settings.json new file mode 100644 index 00000000..93d4afc3 --- /dev/null +++ b/mbf-agent-runnable/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + ".\\Cargo.toml", + ] +} \ No newline at end of file diff --git a/mbf-agent-runnable/Cargo.toml b/mbf-agent-runnable/Cargo.toml new file mode 100644 index 00000000..b8370299 --- /dev/null +++ b/mbf-agent-runnable/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mbf-agent-runnable" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "mbf-agent" +path = "src/main.rs" + +[dependencies] +mbf-agent-core = { path = "../mbf-agent-core" } +log = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" + + + +[features] +request_timing = ["mbf-agent-core/request_timing"] \ No newline at end of file diff --git a/mbf-agent-runnable/src/main.rs b/mbf-agent-runnable/src/main.rs new file mode 100644 index 00000000..27b1d9e1 --- /dev/null +++ b/mbf-agent-runnable/src/main.rs @@ -0,0 +1,94 @@ +use std::{io::{BufRead, BufReader, Write}, panic}; + +use anyhow::{Context, Result}; +use log::{error, Level}; +use mbf_agent_core::{handlers, models::{request, response}, parameters::init_parameters}; + +static LOGGER: ResponseLogger = ResponseLogger {}; + +struct ResponseLogger {} + +impl log::Log for ResponseLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= Level::Debug + } + + fn log(&self, record: &log::Record) { + // Skip logs that are not from mbf_agent, mbf_zip, etc. + // ...as these are spammy logs from ureq or rustls, and we do nto want them. + match record.module_path() { + Some(module_path) => { + if !module_path.starts_with("mbf") { + return; + } + } + None => return, + } + + // Ignore errors, logging should be infallible and we don't want to panic + let _result = write_response(response::Response::LogMsg { + message: format!("{}", record.args()), + level: match record.level() { + Level::Debug => response::LogLevel::Debug, + Level::Info => response::LogLevel::Info, + Level::Warn => response::LogLevel::Warn, + Level::Error => response::LogLevel::Error, + Level::Trace => response::LogLevel::Trace, + }, + }); + } + + fn flush(&self) { + let _ = std::io::stdout().flush(); + } +} + +fn write_response(response: response::Response) -> Result<()> { + let mut lock = std::io::stdout().lock(); + serde_json::to_writer(&mut lock, &response).context("Serializing JSON response")?; + writeln!(lock)?; + Ok(()) +} + +fn main() -> Result<()> { + #[cfg(feature = "request_timing")] + let start_time = Instant::now(); + + log::set_logger(&LOGGER).expect("Failed to set up logging"); + log::set_max_level(log::LevelFilter::Debug); + + let mut reader = BufReader::new(std::io::stdin()); + let mut line = String::new(); + reader.read_line(&mut line)?; + let req: request::Request = serde_json::from_str(&line)?; + + // Set the parameters for this instance of the agent + init_parameters( + &req.agent_parameters.game_id, + req.agent_parameters.ignore_package_id, + ); + + // Set a panic hook that writes the panic as a JSON Log + // (we don't do this in catch_unwind as we get an `Any` there, which doesn't implement Display) + panic::set_hook(Box::new(|info| { + error!("Request failed due to a panic!: {info}") + })); + + match std::panic::catch_unwind(|| handlers::handle_request(req)) { + Ok(resp) => match resp { + Ok(resp) => { + #[cfg(feature = "request_timing")] + { + let req_time = Instant::now() - start_time; + info!("Request complete in {}ms", req_time.as_millis()); + } + + write_response(resp)?; + } + Err(err) => error!("{err:?}"), + }, + Err(_) => {} // Panic will be outputted above + }; + + Ok(()) +} diff --git a/mbf-agent/src/main.rs b/mbf-agent/src/main.rs deleted file mode 100644 index 2415af76..00000000 --- a/mbf-agent/src/main.rs +++ /dev/null @@ -1,181 +0,0 @@ -mod axml; -mod data_fix; -mod downloads; -mod handlers; -mod manifest; -mod mod_man; -mod models; -mod patching; -mod parameters; -mod downgrading; - -use anyhow::{Context, Result}; -use downloads::DownloadConfig; -use log::{debug, error, warn, Level}; -use mbf_res_man::res_cache::ResCache; -use models::{request, response}; -use parameters::{init_parameters, PARAMETERS}; -use serde::{Deserialize, Serialize}; -use std::{ - io::{BufRead, BufReader, Write}, - panic, - path::Path, - process::Command, - sync, -}; - -#[cfg(feature = "request_timing")] -use log::info; -#[cfg(feature = "request_timing")] -use std::time::Instant; - -/// Attempts to delete legacy directories no longer used by MBF to free up space -/// Logs on failure -pub fn try_delete_legacy_dirs() { - for dir in &PARAMETERS.legacy_dirs { - if Path::new(dir).exists() { - match std::fs::remove_dir_all(dir) { - Ok(_) => debug!("Successfully removed legacy dir {dir}"), - Err(err) => warn!("Failed to remove legacy dir {dir}: {err}"), - } - } - } -} - -static DOWNLOAD_CFG: sync::OnceLock = sync::OnceLock::new(); - -/// Gets the default config used for downloads in MBF -pub fn get_dl_cfg() -> &'static DownloadConfig<'static> { - DOWNLOAD_CFG.get_or_init(|| { - DownloadConfig { - max_disconnections: 10, - // If downloads data successfully for 10 seconds, reset disconnection attempts - disconnection_reset_time: Some(std::time::Duration::from_secs_f32(10.0)), - disconnect_wait_time: std::time::Duration::from_secs_f32(5.0), - progress_update_interval: Some(std::time::Duration::from_secs_f32(2.0)), - ureq_agent: mbf_res_man::default_agent::get_agent(), - } - }) -} - -/// Creates a ResCache for downloading files using mbf_res_man -/// This should be reused where possible. -pub fn load_res_cache() -> Result> { - std::fs::create_dir_all(&PARAMETERS.res_cache).expect("Failed to create resource cache folder"); - Ok(ResCache::new( - (&PARAMETERS.res_cache).into(), - mbf_res_man::default_agent::get_agent(), - )) -} - -pub fn get_apk_path() -> Result> { - let pm_output = Command::new("pm") - .args(["path", &PARAMETERS.apk_id]) - .output() - .context("Working out APK path")?; - if 8 > pm_output.stdout.len() { - // App not installed - Ok(None) - } else { - Ok(Some( - std::str::from_utf8(pm_output.stdout.split_at(8).1)? - .trim_end() - .to_owned(), - )) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -struct ModTag { - patcher_name: String, - patcher_version: Option, - modloader_name: String, - modloader_version: Option, -} - -struct ResponseLogger {} - -impl log::Log for ResponseLogger { - fn enabled(&self, metadata: &log::Metadata) -> bool { - metadata.level() <= Level::Debug - } - - fn log(&self, record: &log::Record) { - // Skip logs that are not from mbf_agent, mbf_zip, etc. - // ...as these are spammy logs from ureq or rustls, and we do nto want them. - match record.module_path() { - Some(module_path) => { - if !module_path.starts_with("mbf") { - return; - } - } - None => return, - } - - // Ignore errors, logging should be infallible and we don't want to panic - let _result = write_response(response::Response::LogMsg { - message: format!("{}", record.args()), - level: match record.level() { - Level::Debug => response::LogLevel::Debug, - Level::Info => response::LogLevel::Info, - Level::Warn => response::LogLevel::Warn, - Level::Error => response::LogLevel::Error, - Level::Trace => response::LogLevel::Trace, - }, - }); - } - - fn flush(&self) { - let _ = std::io::stdout().flush(); - } -} - -fn write_response(response: response::Response) -> Result<()> { - let mut lock = std::io::stdout().lock(); - serde_json::to_writer(&mut lock, &response).context("Serializing JSON response")?; - writeln!(lock)?; - Ok(()) -} - -static LOGGER: ResponseLogger = ResponseLogger {}; - -fn main() -> Result<()> { - #[cfg(feature = "request_timing")] - let start_time = Instant::now(); - - log::set_logger(&LOGGER).expect("Failed to set up logging"); - log::set_max_level(log::LevelFilter::Debug); - - let mut reader = BufReader::new(std::io::stdin()); - let mut line = String::new(); - reader.read_line(&mut line)?; - let req: request::Request = serde_json::from_str(&line)?; - - // Set the parameters for this instance of the agent - init_parameters(&req.agent_parameters.game_id, req.agent_parameters.ignore_package_id); - - // Set a panic hook that writes the panic as a JSON Log - // (we don't do this in catch_unwind as we get an `Any` there, which doesn't implement Display) - panic::set_hook(Box::new(|info| { - error!("Request failed due to a panic!: {info}") - })); - - match std::panic::catch_unwind(|| handlers::handle_request(req)) { - Ok(resp) => match resp { - Ok(resp) => { - #[cfg(feature = "request_timing")] - { - let req_time = Instant::now() - start_time; - info!("Request complete in {}ms", req_time.as_millis()); - } - - write_response(resp)?; - } - Err(err) => error!("{err:?}"), - }, - Err(_) => {} // Panic will be outputted above - }; - - Ok(()) -} diff --git a/watch_agent.ps1 b/watch_agent.ps1 index c97523d1..890e77d4 100644 --- a/watch_agent.ps1 +++ b/watch_agent.ps1 @@ -2,4 +2,4 @@ # To install it, run `cargo install cargo-watch` Write-Output "Waiting for agent modifications" -cargo watch -w "$PSScriptRoot\mbf-agent\src\" -w "$PSScriptRoot\mbf-res-man\src\" -s "pwsh $PSScriptRoot/build_agent.ps1" \ No newline at end of file +cargo watch -w "$PSScriptRoot\mbf-agent-runnable\src\" -w "$PSScriptRoot\mbf-res-man\src\" -s "pwsh $PSScriptRoot/build_agent.ps1" \ No newline at end of file