Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"cargo": {
"args": [
"build",
"--manifest-path=mbf-agent/Cargo.toml"
"--manifest-path=mbf-agent-runnable/Cargo.toml"
]
},
"args": []
Expand Down
15 changes: 13 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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."*"]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion build_agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down
2 changes: 0 additions & 2 deletions mbf-agent/.gitignore → mbf-agent-core/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,3 @@
*.diff
*.xml
index.json

mbf-agent
File renamed without changes.
2 changes: 1 addition & 1 deletion mbf-agent/Cargo.toml → mbf-agent-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "mbf-agent"
name = "mbf-agent-core"
version = "0.1.0"
edition = "2021"

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
88 changes: 88 additions & 0 deletions mbf-agent-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<DownloadConfig> = 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<ResCache<'static>> {
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<Option<String>> {
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<String>,
modloader_name: String,
modloader_version: Option<String>,
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
7 changes: 7 additions & 0 deletions mbf-agent-runnable/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/target
*.apk
*.so
*.obb
*.diff
*.xml
index.json
5 changes: 5 additions & 0 deletions mbf-agent-runnable/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rust-analyzer.linkedProjects": [
".\\Cargo.toml",
]
}
20 changes: 20 additions & 0 deletions mbf-agent-runnable/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
94 changes: 94 additions & 0 deletions mbf-agent-runnable/src/main.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Loading