diff --git a/.editorconfig b/.editorconfig index 524c263..6245392 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,11 @@ root = true charset = utf-8 -insert_final_newline = true end_of_line = lf +insert_final_newline = true [*.{json,js,ts,tsx,rs,codegen}] indent_size = 4 indent_style = tab + +[version] +insert_final_newline = false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a81ff3b..9f47c1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: - name: Download Decky CLI run: | mkdir /tmp/decky-cli - curl -L -o /tmp/decky-cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/download/0.0.1-alpha.12/decky" + curl -L -o /tmp/decky-cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/download/0.0.2/decky-linux-x86_64" chmod +x /tmp/decky-cli/decky echo "/tmp/decky-cli" >> $GITHUB_PATH diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 0bd16ca..e568fe6 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -269,6 +269,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + [[package]] name = "async-trait" version = "0.1.68" @@ -303,6 +309,7 @@ version = "0.0.0" dependencies = [ "actix-cors", "actix-web", + "anyhow", "async-trait", "chrono", "criterion", @@ -310,16 +317,19 @@ dependencies = [ "futures", "glob", "keyvalues-serde", - "log", + "lazy_static", "once_cell", "semver", "serde", "serde_alias", "serde_json", - "simplelog", "slotmap", "tokio", "tokio-stream", + "toml", + "tracing", + "tracing-appender", + "tracing-subscriber", ] [[package]] @@ -477,7 +487,7 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "bitflags 1.3.2", "clap_lex", - "indexmap", + "indexmap 1.9.3", "textwrap", ] @@ -567,6 +577,15 @@ dependencies = [ "itertools", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.3" @@ -593,12 +612,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -648,6 +664,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "flate2" version = "1.0.27" @@ -807,7 +829,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -826,6 +848,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -907,7 +935,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", ] [[package]] @@ -982,9 +1020,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.146" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "local-channel" @@ -1062,6 +1100,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1081,15 +1129,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "object" version = "0.32.0" @@ -1117,6 +1156,12 @@ version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1467,6 +1512,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1502,23 +1556,21 @@ dependencies = [ ] [[package]] -name = "signal-hook-registry" -version = "1.4.1" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "libc", + "lazy_static", ] [[package]] -name = "simplelog" -version = "0.12.1" +name = "signal-hook-registry" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ - "log", - "termcolor", - "time 0.3.22", + "libc", ] [[package]] @@ -1588,15 +1640,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "textwrap" version = "0.16.0" @@ -1623,6 +1666,16 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.1.45" @@ -1641,8 +1694,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", - "libc", - "num_threads", "serde", "time-core", "time-macros", @@ -1744,25 +1795,121 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time 0.3.22", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ + "log", "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -1809,6 +1956,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" @@ -2007,6 +2160,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +dependencies = [ + "memchr", +] + [[package]] name = "zstd" version = "0.12.4" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e047738..2c691fb 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -5,13 +5,9 @@ edition = "2021" license = "GPL-2.0" authors = ["Christopher-Robin "] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] actix-cors = "0.6.4" actix-web = "4.4.0" -log = "0.4" -simplelog = "0.12" once_cell = "1.18.0" chrono = "0.4.24" serde_json = "1.0" @@ -26,6 +22,12 @@ slotmap = { version = "1.0.6", features = ["serde"] } glob = "0.3.1" semver = { version = "1.0.20", features = ["serde"] } either = "1.9.0" +toml = "0.8.8" +anyhow = "1.0.79" +lazy_static = "1.4.0" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["json"] } +tracing-appender = "0.2.3" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } diff --git a/backend/Dockerfile b/backend/Dockerfile index 3219990..05ca0a0 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,4 @@ FROM ghcr.io/steamdeckhomebrew/holo-toolchain-rust:latest RUN pacman -S --noconfirm cmake make clang git -# Updates the Crates.io index -RUN cargo search --limit 0 - ENTRYPOINT [ "/backend/entrypoint.sh" ] diff --git a/backend/build-docker.sh b/backend/build-docker.sh index 84dae41..2676cb3 100644 --- a/backend/build-docker.sh +++ b/backend/build-docker.sh @@ -9,6 +9,7 @@ mkdir -p out echo "--- Building plugin backend ---" cargo build --profile docker +BUILD_EXIT=$? mkdir -p out mv target/docker/backend out/backend @@ -18,3 +19,4 @@ echo " --- Cleaning up ---" cargo clean # remove newly-cloned git repo and artifacts rm -rf ./ryzenadj +exit $BUILD_EXIT \ No newline at end of file diff --git a/backend/src/api.rs b/backend/src/api.rs index 5f4e144..9e28919 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -7,17 +7,15 @@ use crate::{ sdcard::{get_card_cid, is_card_inserted}, }; use actix_web::{ - delete, get, - http::StatusCode, - post, - web, - Either, HttpResponse, HttpResponseBuilder, Responder, Result, + delete, get, http::StatusCode, post, web, Either, HttpResponse, HttpResponseBuilder, Responder, + Result, }; use futures::StreamExt; use serde::Deserialize; use std::{ops::Deref, sync::Arc}; use tokio::sync::broadcast::Sender; use tokio_stream::wrappers::BroadcastStream; +use tracing::{instrument, trace}; pub(crate) fn config(cfg: &mut web::ServiceConfig) { cfg // @@ -49,16 +47,20 @@ pub(crate) fn config(cfg: &mut web::ServiceConfig) { } #[get("/version")] +#[instrument] pub(crate) async fn version() -> impl Responder { HttpResponse::Ok().body(PACKAGE_VERSION) } +#[allow(clippy::async_yields_async)] #[get("/health")] +#[instrument] pub(crate) async fn health() -> impl Responder { HttpResponse::Ok() } #[get("/listen")] +#[instrument] pub(crate) async fn listen(sender: web::Data>) -> Result { let event_stream = BroadcastStream::new(sender.subscribe()).map(|res| match res { Err(_) => Err(Error::from_str("Subscriber Closed")), @@ -70,11 +72,13 @@ pub(crate) async fn listen(sender: web::Data>) -> Result>) -> impl Responder { web::Json(datastore.list_cards_with_games()) } #[get("/list/games/{card_id}")] +#[instrument] pub(crate) async fn list_games_for_card( card_id: web::Path, datastore: web::Data>, @@ -86,6 +90,7 @@ pub(crate) async fn list_games_for_card( } #[get("/list/cards/{game_id}")] +#[instrument] pub(crate) async fn list_cards_for_game( game_id: web::Path, datastore: web::Data>, @@ -97,6 +102,7 @@ pub(crate) async fn list_cards_for_game( } #[get("/current")] +#[instrument] pub(crate) async fn get_current_card_and_games( datastore: web::Data>, ) -> Result> { @@ -119,6 +125,7 @@ pub(crate) async fn get_current_card_and_games( } #[get("/current/card")] +#[instrument] pub(crate) async fn get_current_card(datastore: web::Data>) -> Result { if !is_card_inserted() { return Err(Error::from_str("No card is inserted").into()); @@ -130,6 +137,7 @@ pub(crate) async fn get_current_card(datastore: web::Data>) -> Result } #[get("/current/id")] +#[instrument] pub(crate) async fn get_current_card_id() -> Result { if !is_card_inserted() { return Err(Error::from_str("No card is inserted").into()); @@ -139,6 +147,7 @@ pub(crate) async fn get_current_card_id() -> Result { } #[get("/current/games")] +#[instrument] pub(crate) async fn get_games_on_current_card( datastore: web::Data>, ) -> Result { @@ -155,6 +164,7 @@ pub(crate) async fn get_games_on_current_card( } #[post("/card/{id}")] +#[instrument] pub(crate) async fn create_card( id: web::Path, body: web::Json, @@ -175,11 +185,13 @@ pub(crate) async fn create_card( false => datastore.add_card(id.into_inner(), body.into_inner()), } + trace!("Sending Updated event"); _ = sender.send(CardEvent::Updated); Ok(HttpResponse::Ok()) } #[delete("/card/{id}")] +#[instrument] pub(crate) async fn delete_card( id: web::Path, datastore: web::Data>, @@ -187,11 +199,13 @@ pub(crate) async fn delete_card( ) -> Result { datastore.remove_element(&id)?; + trace!("Sending Updated event"); _ = sender.send(CardEvent::Updated); Ok(HttpResponse::Ok()) } #[get("/card/{id}")] +#[instrument] pub(crate) async fn get_card( id: web::Path, datastore: web::Data>, @@ -200,6 +214,7 @@ pub(crate) async fn get_card( } #[post("/cards")] +#[instrument] pub(crate) async fn update_cards( body: web::Json>, datastore: web::Data>, @@ -219,16 +234,19 @@ pub(crate) async fn update_cards( } } + trace!("Sending Updated event"); _ = sender.send(CardEvent::Updated); Ok(HttpResponse::Ok()) } #[get("/cards")] +#[instrument] pub(crate) async fn list_cards(datastore: web::Data>) -> impl Responder { web::Json(datastore.list_cards()) } #[post("/game/{id}")] +#[instrument] pub(crate) async fn create_game( id: web::Path, body: web::Json, @@ -249,6 +267,7 @@ pub(crate) async fn create_game( } #[delete("/game/{id}")] +#[instrument] pub(crate) async fn delete_game( id: web::Path, datastore: web::Data>, @@ -259,6 +278,7 @@ pub(crate) async fn delete_game( } #[get("/game/{id}")] +#[instrument] pub(crate) async fn get_game( id: web::Path, datastore: web::Data>, @@ -267,11 +287,14 @@ pub(crate) async fn get_game( } #[get("/games")] +#[instrument] pub(crate) async fn list_games(datastore: web::Data>) -> impl Responder { web::Json(datastore.list_games()) } +#[allow(clippy::async_yields_async)] #[post("/games")] +#[instrument] pub(crate) async fn create_games( body: web::Json>, datastore: web::Data>, @@ -289,19 +312,20 @@ pub(crate) async fn create_games( HttpResponse::Ok() } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct LinkBody { card_id: String, game_id: String, } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct ManyLinkBody { card_id: String, game_ids: Vec, } #[post("/link")] +#[instrument] pub(crate) async fn create_link( body: web::Json, datastore: web::Data>, @@ -309,11 +333,13 @@ pub(crate) async fn create_link( ) -> Result { datastore.link(&body.game_id, &body.card_id)?; + trace!("Sending Updated event"); _ = sender.send(CardEvent::Updated); Ok(HttpResponse::Ok()) } #[post("/linkmany")] +#[instrument] pub(crate) async fn create_links( body: web::Json, datastore: web::Data>, @@ -321,14 +347,16 @@ pub(crate) async fn create_links( ) -> Result { let data = body.into_inner(); for game_id in data.game_ids.iter() { - datastore.link(&game_id, &data.card_id)?; + datastore.link(game_id, &data.card_id)?; } + trace!("Sending Updated event"); _ = sender.send(CardEvent::Updated); Ok(HttpResponse::Ok()) } #[post("/unlink")] +#[instrument] pub(crate) async fn delete_link( body: web::Json, datastore: web::Data>, @@ -336,11 +364,13 @@ pub(crate) async fn delete_link( ) -> Result { datastore.unlink(&body.game_id, &body.card_id)?; + trace!("Sending Updated event"); _ = sender.send(CardEvent::Updated); Ok(HttpResponse::Ok()) } #[post("/unlinkmany")] +#[instrument] pub(crate) async fn delete_links( body: web::Json, datastore: web::Data>, @@ -348,14 +378,16 @@ pub(crate) async fn delete_links( ) -> Result { let data = body.into_inner(); for game_id in data.game_ids.iter() { - datastore.unlink(&game_id, &data.card_id)?; + datastore.unlink(game_id, &data.card_id)?; } + trace!("Sending Updated event"); _ = sender.send(CardEvent::Updated); Ok(HttpResponse::Ok()) } #[post("/save")] +#[instrument] pub(crate) async fn save(datastore: web::Data>) -> Result { datastore.write_to_file()?; diff --git a/backend/src/cfg.rs b/backend/src/cfg.rs new file mode 100644 index 0000000..6c81568 --- /dev/null +++ b/backend/src/cfg.rs @@ -0,0 +1,76 @@ +use anyhow::Result; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::{ + fs::{self, File}, + io::Write, + path::PathBuf, +}; +use tracing::Level; + +use crate::DATA_DIR; + +lazy_static! { + pub static ref CONFIG_PATH: PathBuf = DATA_DIR.join("config.toml"); + pub static ref CONFIG: Config = Config::load().unwrap_or_else(|| { + let result = Config::new(); + result.write().expect("Write to succeed"); + result + }); +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Serialize, Deserialize)] +#[serde(remote = "Level")] +pub enum LogLevel { + TRACE = 0, + DEBUG = 1, + INFO = 2, + WARN = 3, + ERROR = 4, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub port: u16, + pub scan_interval: u64, + pub store_file: PathBuf, + pub log_file: PathBuf, + #[serde(with = "LogLevel")] + pub log_level: Level, +} + +impl Config { + pub fn new() -> Self { + Config { + port: 12412, + scan_interval: 5000, + log_file: "microsdeck.log".into(), + store_file: "store".into(), + log_level: Level::INFO, + } + } + pub fn write(&self) -> Result<()> { + self.write_to_file(&CONFIG_PATH) + } + pub fn write_to_file(&self, path: &'_ PathBuf) -> Result<()> { + fs::create_dir_all(path.parent().expect("The file to have a parent directory"))?; + let mut file = File::create(path)?; + Ok(file.write_all(Self::write_to_str(self)?.as_bytes())?) + } + pub fn write_to_str(&self) -> Result { + Ok(toml::ser::to_string(self)?) + } + + pub fn load() -> Option { + Self::load_from_file(&CONFIG_PATH) + } + pub fn load_from_file(path: &'_ PathBuf) -> Option { + fs::read_to_string(path) + .ok() + .and_then(|val| Self::load_from_str(&val).ok()) + } + pub fn load_from_str(content: &'_ str) -> Result { + Ok(toml::de::from_str::(content)?) + } +} diff --git a/backend/src/ds.rs b/backend/src/ds.rs index 8b7d92d..476cbd0 100644 --- a/backend/src/ds.rs +++ b/backend/src/ds.rs @@ -4,7 +4,6 @@ use crate::{ err::Error, sdcard::get_steam_acf_files, }; -use log::error; use semver::Version; use serde::{Deserialize, Serialize}; use slotmap::{DefaultKey, SlotMap}; @@ -16,6 +15,7 @@ use std::{ path::PathBuf, sync::RwLock, }; +use tracing::{error, instrument}; #[derive(Serialize, Deserialize, Debug, Clone)] pub(crate) enum StoreElement { @@ -66,7 +66,7 @@ fn default_version() -> Version { #[derive(Serialize, Deserialize, Debug)] pub struct StoreData { - #[serde(default="default_version")] + #[serde(default = "default_version")] version: Version, nodes: SlotMap, node_ids: HashMap, @@ -75,18 +75,21 @@ pub struct StoreData { } impl StoreData { + #[instrument(skip(self))] pub fn add_card(&mut self, id: String, card: MicroSDCard) { self.node_ids .entry(id) .or_insert_with(|| self.nodes.insert(Node::from_card(card))); } + #[instrument(skip(self))] pub fn add_game(&mut self, id: String, game: Game) { self.node_ids .entry(id) .or_insert_with(|| self.nodes.insert(Node::from_game(game))); } + #[instrument(skip(self, func))] pub fn update_card(&mut self, card_id: &str, mut func: F) -> Result<(), Error> where F: FnMut(&mut MicroSDCard) -> Result<(), Error>, @@ -106,6 +109,7 @@ impl StoreData { Ok(()) } + #[instrument(skip(self))] pub fn link(&mut self, a_id: &str, b_id: &str) -> Result<(), Error> { let a_key = self.node_ids.get(a_id); let b_key = self.node_ids.get(b_id); @@ -119,6 +123,7 @@ impl StoreData { Ok(()) } + #[instrument(skip(self))] pub fn unlink(&mut self, a_id: &str, b_id: &str) -> Result<(), Error> { let game_key = self.node_ids.get(a_id); let card_key = self.node_ids.get(b_id); @@ -132,6 +137,7 @@ impl StoreData { Ok(()) } + #[instrument(skip(self))] pub fn remove_item(&mut self, id: &str) -> Result<(), Error> { let element_key = self .node_ids @@ -145,10 +151,12 @@ impl StoreData { Ok(()) } + #[instrument(skip(self))] pub fn contains_element(&self, card_id: &str) -> bool { self.node_ids.contains_key(card_id) } + #[instrument(skip(self))] pub fn get_card(&self, card_id: &str) -> Result { self.node_ids .get(card_id) @@ -160,6 +168,7 @@ impl StoreData { }) } + #[instrument(skip(self))] pub fn get_game(&self, game_id: &str) -> Result { self.node_ids .get(game_id) @@ -171,6 +180,7 @@ impl StoreData { }) } + #[instrument(skip(self))] pub fn get_card_and_games(&self, card_id: &str) -> Result<(MicroSDCard, Vec), Error> { let card_key = self .node_ids @@ -192,6 +202,7 @@ impl StoreData { Ok((card, games)) } + #[instrument(skip(self))] pub fn get_games_on_card(&self, card_id: &str) -> Result, Error> { let card_key = self .node_ids @@ -207,6 +218,7 @@ impl StoreData { Ok(games) } + #[instrument(skip(self))] pub fn get_cards_for_game(&self, game_id: &str) -> Result, Error> { let game_key = self .node_ids @@ -222,6 +234,7 @@ impl StoreData { Ok(cards) } + #[instrument(skip(self))] pub fn list_cards(&self) -> Vec { self.nodes .iter() @@ -229,6 +242,7 @@ impl StoreData { .collect() } + #[instrument(skip(self))] pub fn list_games(&self) -> Vec { self.nodes .iter() @@ -236,6 +250,7 @@ impl StoreData { .collect() } + #[instrument(skip(self))] pub fn list_cards_with_games(&self) -> Vec<(MicroSDCard, Vec)> { self.nodes .iter() @@ -318,8 +333,8 @@ impl Store { } pub fn read_from_file(file: PathBuf) -> Result { - let contents = read_to_string(&file).map_err(|e| Error::from(e))?; - let store_data: StoreData = serde_json::from_str(&contents).map_err(|e| Error::from(e))?; + let contents = read_to_string(&file).map_err(Error::from)?; + let store_data: StoreData = serde_json::from_str(&contents).map_err(Error::from)?; Ok(Store { data: RwLock::new(store_data), file: Some(file), @@ -347,7 +362,7 @@ impl Store { } if let Err(err) = self.write_to_file() { - error!("Unable to write datastore to file: {}", err); + error!(%err, "Unable to write datastore to file \"{}\"", err); } } @@ -365,15 +380,16 @@ impl Store { } } - if dead_node_ids.len() > 0 { + if !dead_node_ids.is_empty() { result &= false; - error!("Found dead node_ids: {:?}", dead_node_ids); + error!(?dead_node_ids, "Found dead node_ids"); } } - return result; + result } + /// Removes any whitespace from the card uid pub fn clean_up(&self) { let mut data = self.data.write().unwrap(); @@ -427,7 +443,7 @@ impl Store { } pub fn remove_element(&self, id: &str) -> Result<(), Error> { - // these two operations have to happen within a single scope otherwise the try_write_to_file causes a deadlock + // these two operations have to happen within a single lock otherwise the try_write_to_file causes a deadlock { let mut lock = self.data.write().unwrap(); lock.remove_item(id)?; diff --git a/backend/src/dto.rs b/backend/src/dto.rs index f80200b..2239f6f 100644 --- a/backend/src/dto.rs +++ b/backend/src/dto.rs @@ -5,17 +5,17 @@ use serde::{Deserialize, Serialize}; pub enum CardEvent { Inserted, Removed, - Updated + Updated, } impl EventTrait for CardEvent { - fn get_event(&self) -> Option<&'static str> { - Some(match self { + fn get_event(&self) -> Option<&'static str> { + Some(match self { CardEvent::Inserted => "insert", CardEvent::Removed => "remove", CardEvent::Updated => "update", }) - } + } // fn get_data(&self) -> Option<&'static str> { // match self { // CardEvent::Inserted => None, @@ -33,7 +33,7 @@ fn default_true() -> bool { pub struct MicroSDCard { pub uid: String, pub libid: String, - + #[serde(default)] pub mount: Option, diff --git a/backend/src/env.rs b/backend/src/env.rs index 5cb11c1..ff440b0 100644 --- a/backend/src/env.rs +++ b/backend/src/env.rs @@ -1,51 +1,38 @@ -use log::warn; -use std::path::Path; - -pub const PORT: u16 = 12412; // TODO replace with something unique - -pub const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME"); -pub const PACKAGE_VERSION: &'static str = include_str!("../version"); -pub const PACKAGE_AUTHORS: &'static str = env!("CARGO_PKG_AUTHORS"); - -const TEMPDIR: &'static str = "/tmp/MicroSDeck"; - -pub fn get_data_dir() -> String { - return match std::env::var("DECKY_PLUGIN_RUNTIME_DIR") { - Ok(loc) => loc.to_string(), - Err(_) => { - warn!("Unable to find \"DECKY_PLUGIN_RUNTIME_DIR\" in env. Assuming Dev mode & using temporary directory"); - TEMPDIR.to_string() + "/data" +use lazy_static::*; +use std::path::PathBuf; + +pub const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); +pub const PACKAGE_VERSION: &str = include_str!("../version"); +pub const PACKAGE_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); + +const TEMPDIR: &str = "/tmp/MicroSDeck"; + +lazy_static! { + pub static ref DATA_DIR: PathBuf = PathBuf::from( + match std::env::var("DECKY_PLUGIN_RUNTIME_DIR") { + Ok(loc) => loc, + Err(_) => { + println!("Unable to find \"DECKY_PLUGIN_RUNTIME_DIR\" in env. Assuming Dev mode & using temporary directory"); + TEMPDIR.to_string() + "/data" + } } - }; -} -pub fn get_log_dir() -> String { - return match std::env::var("DECKY_PLUGIN_LOG_DIR") { - Ok(loc) => loc.to_string(), + ); + pub static ref LOG_DIR: PathBuf = PathBuf::from(match std::env::var("DECKY_PLUGIN_LOG_DIR") { + Ok(loc) => loc, Err(_) => { - warn!("Unable to find \"DECKY_PLUGIN_LOG_DIR\" in env. Assuming Dev mode & using temporary directory"); + println!("Unable to find \"DECKY_PLUGIN_LOG_DIR\" in env. Assuming Dev mode & using temporary directory"); TEMPDIR.to_string() + "/log" } - }; -} - -pub fn get_file_path(file_name: &str, get_base_dir: &dyn Fn() -> String) -> Option { - let dir = get_base_dir(); - - Path::new(dir.as_str()) - .join(file_name) - .to_str() - .map(|v| v.to_string()) + }); } pub fn get_file_path_and_create_directory( - file_name: &str, - get_base_dir: &dyn Fn() -> String, -) -> Option { - let dir = get_base_dir(); - - if let Err(_) = std::fs::create_dir_all(Path::new(dir.as_str())) { + file_name: &PathBuf, + base_dir: &PathBuf, +) -> Option { + if std::fs::create_dir_all(base_dir).is_err() { return None; } - get_file_path(file_name, get_base_dir) + Some(base_dir.join(file_name)) } diff --git a/backend/src/err.rs b/backend/src/err.rs index ce3aeaf..502d9ec 100644 --- a/backend/src/err.rs +++ b/backend/src/err.rs @@ -1,18 +1,16 @@ #![allow(dead_code)] - -use std::fmt; - use actix_web::ResponseError; +use std::fmt; #[derive(Debug)] -struct StdErr; +struct StdErr(String); -impl std::error::Error for StdErr{} +impl std::error::Error for StdErr {} impl fmt::Display for StdErr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "StdErr") + write!(f, "{}", self) } } @@ -35,14 +33,14 @@ impl Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Error: {}", &self.0) + write!(f, "{}", &self.0) } } -impl Into> for Error { - fn into(self) -> Box { - Box::new(StdErr) - } +impl From for Box { + fn from(err: Error) -> Self { + Box::new(StdErr(err.to_string())) + } } impl From for Error { diff --git a/backend/src/event.rs b/backend/src/event.rs index 8fb52af..061364b 100644 --- a/backend/src/event.rs +++ b/backend/src/event.rs @@ -1,8 +1,6 @@ use actix_web::web::Bytes; -pub(crate) struct Event { - val: T -} +pub(crate) struct Event(T); pub(crate) trait EventTrait { fn get_id(&self) -> Option<&'static str> { @@ -18,42 +16,42 @@ pub(crate) trait EventTrait { impl Event { pub fn new(val: T) -> Self { - Event { val } + Event(val) } } -impl ToString for Event { - fn to_string(&self) -> String { - let mut output = "".to_string(); +impl ToString for Event { + fn to_string(&self) -> String { + let mut output = "".to_string(); - if let Some(value) = self.val.get_id() { + if let Some(value) = self.0.get_id() { output += &format!("id: {}\n", value); } - if let Some(value) = self.val.get_event() { - output += &format!("event: {}\n", value); + if let Some(value) = self.0.get_event() { + output += &format!("event: {}\n", value); } - if let Some(value) = self.val.get_data() { - output += &format!("data: {}\n", value); + if let Some(value) = self.0.get_data() { + output += &format!("data: {}\n", value); } - if output != "" { + if !output.is_empty() { output += "\n"; } - return output; + output } } -impl Into for Event { - fn into(self) -> Bytes { - Bytes::from(self.to_string()) - } +impl From> for Bytes { + fn from(val: Event) -> Self { + Bytes::from(val.to_string()) + } } impl From for Event { - fn from(value: T) -> Self { - Event { val: value } - } + fn from(value: T) -> Self { + Event(value) + } } pub(crate) struct EventBuilder { @@ -62,9 +60,14 @@ pub(crate) struct EventBuilder { data: Option<&'static str>, } +#[allow(dead_code)] impl EventBuilder { pub fn new() -> Self { - EventBuilder { id: None, event: None, data: None } + EventBuilder { + id: None, + event: None, + data: None, + } } pub fn with_id(mut self, id: &'static str) -> Self { self.id = Some(id); @@ -90,4 +93,4 @@ impl EventTrait for EventBuilder { fn get_id(&self) -> Option<&'static str> { self.id } -} \ No newline at end of file +} diff --git a/backend/src/log.rs b/backend/src/log.rs index 2f30982..b282200 100644 --- a/backend/src/log.rs +++ b/backend/src/log.rs @@ -1,82 +1,33 @@ -use chrono; -use log::{Level, Metadata, Record}; -use std::env; -use std::fs::{File, OpenOptions}; -use std::io::prelude::*; -use std::str::FromStr; - -use crate::env::{get_file_path_and_create_directory, get_log_dir}; -use crate::err::Error; - -pub struct Logger { - file: File, - max_level: Level, -} - -impl Logger { - pub fn to_file(&self) -> &File { - &self.file - } - - pub fn new() -> Option { - let file_path = get_file_path_and_create_directory("backend.log", &get_log_dir) - .expect("to retrieve the log file path"); - - let file = OpenOptions::new() - .write(true) - .append(true) - .create(true) - .open(&file_path) - .ok()?; - - let max_level = env::var("LOG_LEVEL") - .map_err(Error::from) - .and_then(|v| Level::from_str(&v).map_err(Error::from)) - .unwrap_or({ - if cfg!(debug_assertions) { - Level::Debug - } else { - Level::Info - } - }); - - println!("Logging enabled to {file_path} with level {max_level}"); - - Some(Logger { file, max_level }) +use crate::cfg::CONFIG; +use crate::{get_file_path_and_create_directory, LOG_DIR}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::Layer; + +pub fn create_subscriber() { + let log_file_path = get_file_path_and_create_directory(&CONFIG.log_file, &LOG_DIR) + .expect("Log file to be created"); + + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(log_file_path) + .expect("Log file to be created"); + + let file_writer = tracing_subscriber::fmt::layer() + .json() + .with_writer(file) + .with_filter(tracing_subscriber::filter::LevelFilter::from_level( + CONFIG.log_level, + )); + + let subscriber = tracing_subscriber::registry().with(file_writer); + + if cfg!(debug_assertions) { + subscriber + .with(tracing_subscriber::fmt::layer().pretty()) + .init(); + } else { + subscriber.init(); } } - -impl log::Log for Logger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= self.max_level // && metadata.target() != "tracing::span" - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - let current_time = chrono::offset::Local::now(); - - println!( - "{} {}: {}", - current_time.format("%H:%M:%S"), - record.level(), - record.args() - ); - - let message = format!( - "{} {} @ {}:{} {} \"{}\"", - current_time.naive_utc(), - record.level(), - record.file().unwrap_or("UNKNOWN"), - record.line().unwrap_or(0), - record.metadata().target(), - record.args() - ); - - if let Err(e) = writeln!(self.to_file(), "{message}") { - eprintln!("Couldn't write to file: {}", e); - } - } - } - - fn flush(&self) {} -} diff --git a/backend/src/main.rs b/backend/src/main.rs index 52bf68e..aff854f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,4 +1,5 @@ mod api; +mod cfg; mod ds; mod dto; mod env; @@ -8,35 +9,29 @@ mod log; mod sdcard; mod steam; mod watch; - -use crate::{api::config, dto::CardEvent}; +use crate::cfg::CONFIG; use crate::ds::Store; use crate::env::*; -use crate::log::Logger; use crate::watch::start_watch; -use ::log::{debug, error, info}; +use crate::{api::config, dto::CardEvent}; use actix_cors::Cors; use actix_web::{web, App, HttpServer}; -use env::get_data_dir; use err::Error; use futures::{pin_mut, select, FutureExt}; -use once_cell::sync::Lazy; -use simplelog::LevelFilter; +use log::create_subscriber; use std::path::PathBuf; use std::process::exit; use std::sync::Arc; use tokio::sync::broadcast::{self, Sender}; +use tracing::{debug, error, info}; -static LOGGER: Lazy = Lazy::new(|| Logger::new().expect("Logger to be created")); - -pub fn init() -> Result<(), ::log::SetLoggerError> { - ::log::set_logger(&*LOGGER).map(|()| ::log::set_max_level(LevelFilter::Trace)) +pub fn init() { + create_subscriber(); } type MainResult = Result<(), Error>; -async fn run_server(datastore: Arc, sender: Sender) -> MainResult { - +async fn run_web_server(datastore: Arc, sender: Sender) -> MainResult { info!("Starting HTTP server..."); HttpServer::new(move || { @@ -54,7 +49,7 @@ async fn run_server(datastore: Arc, sender: Sender) -> MainRes .configure(config) }) .workers(2) - .bind(("0.0.0.0", PORT))? + .bind(("0.0.0.0", CONFIG.port))? .run() .await .map_err(|err| err.into()) @@ -66,27 +61,21 @@ async fn main() { std::env::set_var("RUST_BACKTRACE", "1"); } - match init() { - Err(err) => { - error!("Unable to Initialize:\n{}", err); - return; - } - Ok(()) => debug!("Initialized..."), - } - + init(); + info!( - "{}@{} by {}", - PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_AUTHORS + version = PACKAGE_VERSION, + "{}@{} by {}", PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_AUTHORS ); let store_path = PathBuf::from( - &std::env::var("STORE_PATH").unwrap_or( - get_file_path_and_create_directory("store", &get_data_dir) + &std::env::var("STORE_PATH").map(PathBuf::from).unwrap_or( + get_file_path_and_create_directory(&CONFIG.store_file, &DATA_DIR) .expect("should retrieve data directory"), ), ); - debug!("Loading from store {:?}", store_path); + debug!(store_path = store_path.to_str(), "Loading from store"); let store: Arc = Arc::new(Store::read_from_file(store_path.clone()).unwrap_or(Store::new(Some(store_path)))); @@ -102,7 +91,7 @@ async fn main() { let (txtx, _) = broadcast::channel::(1); - let server_future = run_server(store.clone(), txtx.clone()).fuse(); + let server_future = run_web_server(store.clone(), txtx.clone()).fuse(); let watch_future = start_watch(store.clone(), txtx.clone()).fuse(); @@ -111,17 +100,17 @@ async fn main() { select! { result = server_future => match result { Ok(_) => info!("Server ran to completion..."), - Err(err) => error!("Server exited with error: {err}") + Err(err) => error!(%err, "Server exited with error") }, result = watch_future => match result { Ok(_) => info!("Watch ran to completion.."), - Err(err) => error!("Watch exited with error: {err}"), + Err(err) => error!(%err, "Watch exited with error"), }, }; info!("Saving Database"); if let Err(err) = store.write_to_file() { - error!("Failed to write datastore to file {err}"); + error!(%err, "Failed to write datastore to file"); } info!("Exiting..."); diff --git a/backend/src/sdcard.rs b/backend/src/sdcard.rs index 483c945..43fef44 100644 --- a/backend/src/sdcard.rs +++ b/backend/src/sdcard.rs @@ -3,6 +3,9 @@ use std::{ io, }; +pub const DEFAULT_MOUNT: &str = "mmcblk0p1"; +pub const LIBRARY_FOLDER_FILE: &str = "libraryfolder.vdf"; + use crate::err::Error; pub fn is_card_inserted() -> bool { @@ -18,16 +21,18 @@ pub fn get_card_cid() -> Option { pub fn has_libraryfolder(mount: &Option) -> bool { std::fs::metadata(format!( - "/run/media/{}/libraryfolder.vdf", - mount.clone().unwrap_or("mmcblk0p1".into()) + "/run/media/{}/{}", + mount.clone().unwrap_or(DEFAULT_MOUNT.into()), + LIBRARY_FOLDER_FILE )) .is_ok() } pub fn read_libraryfolder(mount: &Option) -> io::Result { std::fs::read_to_string(format!( - "/run/media/{}/libraryfolder.vdf", - mount.clone().unwrap_or("mmcblk0p1".into()) + "/run/media/{}/{}", + mount.clone().unwrap_or(DEFAULT_MOUNT.into()), + LIBRARY_FOLDER_FILE )) } @@ -36,9 +41,8 @@ pub fn get_steam_acf_files( ) -> Result, Error> { Ok(fs::read_dir(format!( "/run/media/{}/steamapps/", - mount.clone().unwrap_or("mmcblk0p1".into()) + mount.clone().unwrap_or(DEFAULT_MOUNT.into()) ))? - .into_iter() .filter_map(Result::ok) .filter(|f| f.path().extension().unwrap_or_default().eq("acf"))) } diff --git a/backend/src/steam.rs b/backend/src/steam.rs index 722a3ca..460ec71 100644 --- a/backend/src/steam.rs +++ b/backend/src/steam.rs @@ -9,12 +9,7 @@ pub struct LibraryFolder { pub label: String, } -#[serde_alias( - CamelCase, - PascalCase, - LowerCase, - SnakeCase -)] +#[serde_alias(CamelCase, PascalCase, LowerCase, SnakeCase)] #[derive(Deserialize)] pub struct AppState { pub appid: String, diff --git a/backend/src/watch.rs b/backend/src/watch.rs index 8b1714d..fa632d6 100644 --- a/backend/src/watch.rs +++ b/backend/src/watch.rs @@ -1,26 +1,49 @@ +use crate::cfg::CONFIG; use crate::{ds::Store, dto::*, err::Error, sdcard::*, steam::*}; -use log::{debug, error, info, trace}; use std::borrow::Borrow; use std::path::{Path, PathBuf}; use std::{fs, sync::Arc, time::Duration}; use tokio::sync::broadcast::Sender; use tokio::time::interval; +use tracing::{debug, error, info, span, trace, warn}; -fn read_msd_directory(datastore: &Store, mount: &Option) -> Result<(), Error> { +fn read_microsd_steam_dir(datastore: &Store, mount: &Option) -> Result<(), Error> { let cid = get_card_cid().ok_or(Error::from_str("Unable to retrieve CID from MicroSD card"))?; let library: LibraryFolder = keyvalues_serde::from_str(&read_libraryfolder(mount)?)?; - trace!("contentid: {}", library.contentid); + debug!( + ?library, + "Read & deserialized library from {}", LIBRARY_FOLDER_FILE + ); let games: Vec = get_steam_acf_files(mount)? - .filter_map(|f| fs::read_to_string(f.path()).ok()) - .filter_map(|s| keyvalues_serde::from_str(s.as_str()).ok()) + .filter_map(|f| match fs::read_to_string(f.path()) { + Ok(value) => Some(value), + Err(err) => { + error!(%err, path=?f.path(), "Unable to read Steam ACF file {:?}", f.path()); + None + } + }) + .filter_map(|s| match keyvalues_serde::from_str(s.as_str()) { + Ok(value) => Some(value), + Err(err) => { + error!(%err, contents=s.as_str(), "Unable to Deserialize Steam ACF file"); + None + } + }) .collect(); - trace!("Retrieved {} Games: {:?}", games.len(), games); + debug!( + game_count = games.len(), + ?games, + "Retrieved {} Games from acf files", + games.len() + ); if !datastore.contains_element(&cid) { + debug!(cid, "No MicroSD card found, creating new card"); + datastore.add_card( cid.clone(), MicroSDCard { @@ -36,15 +59,22 @@ fn read_msd_directory(datastore: &Store, mount: &Option) -> Result<(), E // Remove any games that are linked to the card in the database but on the card let current_games = datastore.get_games_on_card(&cid)?; + debug!( + ?current_games, + "Retrieved {} Games from database", + current_games.len() + ); for deleted_game in current_games .iter() .filter(|v| v.is_steam && !games.iter().any(|g| g.appid == v.uid)) { + debug!(game = ?deleted_game, cid, "Game was removed from MicroSD card"); datastore.unlink(&deleted_game.uid, &cid)? } for game in games.iter() { if !datastore.contains_element(&game.appid) { + debug!(?game, "Game not found in database. Adding game"); datastore.add_game( game.appid.clone(), Game { @@ -56,29 +86,50 @@ fn read_msd_directory(datastore: &Store, mount: &Option) -> Result<(), E ); } + debug!(?game, cid, "Linking game to MicroSD card"); datastore.link(&game.appid, &cid).expect("game to be added") } Ok(()) } +fn find_mount_name() -> Result, Error> { + for entry in Path::new("/dev/disk/by-label") + .read_dir()? + .filter_map(|dir| dir.ok()) + { + trace!(path = ?entry.path().canonicalize()?, "testing label for mount point of MicroSD Card"); + if entry.path().canonicalize()? == PathBuf::from("/dev/mmcblk0p1") { + let label = entry.file_name(); + info!(label = ?label, "Found MicroSD Card label"); + return Ok(Some(label.to_string_lossy().to_string())); + } + } + + Ok(None) +} + pub async fn start_watch(datastore: Arc, sender: Sender) -> Result<(), Error> { - let mut interval = interval(Duration::from_secs(1)); + let mut interval = interval(Duration::from_millis(CONFIG.scan_interval)); let mut card_inserted = false; info!("Starting Watcher..."); + // Small cache for optimization purposes let mut mount: Option = None; loop { interval.tick().await; + let _ = span!(tracing::Level::INFO, "watch cycle", mount).entered(); + // No card no worries. if !is_card_inserted() { // The card has been removed since the last check if card_inserted { debug!("Card was removed"); + trace!("Sending Removed event"); let _ = sender.send(CardEvent::Removed); } card_inserted = false; @@ -89,6 +140,7 @@ pub async fn start_watch(datastore: Arc, sender: Sender) -> Re if !card_inserted { debug!("Card was inserted"); + trace!("Sending Inserted event"); let _ = sender.send(CardEvent::Inserted); mount = None; } @@ -96,73 +148,89 @@ pub async fn start_watch(datastore: Arc, sender: Sender) -> Re card_inserted = true; let cid = match get_card_cid() { - Some(v) => v, + Some(v) => { + trace!(card_id = v, "Loaded card CID: {}", v); + v + } None => { error!("Unable to read Card ID"); continue; } }; + // If we have a mount point and it does not resolve to the library folder, we need to determine the mount point if !has_libraryfolder(&mount) { debug!( - "could not find library folder under mount {}", - mount.clone().unwrap_or("mmcblk0".into()) + mount = mount.clone().unwrap_or(DEFAULT_MOUNT.into()), + "could not find library folder under existing mount", ); - debug!("trying to automatically determine label"); - - if mount == None { - if let Some(card) = datastore.get_card(&cid).ok() { - if card.mount != None { - debug!("MicroSD card had preexisting mount saved. Reusing that."); + debug!("trying to automatically determine mount point"); + + if mount.is_none() { + // Try and retrieve the mount from the database + if let Ok(card) = datastore.get_card(&cid) { + if card.mount.is_some() { + debug!( + mount = card.mount, + "MicroSD card had preexisting mount saved. Reusing that." + ); } mount = card.mount } } // Whatever we loaded did not work. - if mount != None && !has_libraryfolder(&mount) { - debug!("mount {mount:?} does not resolve library. Removing it"); + if mount.is_some() && !has_libraryfolder(&mount) { + warn!( + mount = mount, + "loaded mount does not resolve library. Resetting mount" + ); mount = None; } - if mount == None { - for entry in Path::new("/dev/disk/by-label") - .read_dir()? - .filter_map(|dir| dir.ok()) - { - if entry.path().canonicalize()? == PathBuf::from("/dev/mmcblk0p1") { - let label = entry.file_name(); - info!("Found MicroSD Card label {label:?}"); - mount = Some(label.to_string_lossy().to_string()); - } - } + // We cannot find the library & the mount in the database (if it even exists) is wrong. + // Time to use udev to determine what mount name the microsd card uses + if mount.is_none() { + trace!("No mount found. Trying to determine mount point"); + mount = find_mount_name()?; } + debug!(mount = mount, "Updating card's mount point"); let _ = datastore.update_card(&cid, |card| { card.mount = mount.clone(); Ok(()) }); - continue; + // All has failed. We have no clue how to get to the libary of this MicroSD card. + // Lets hope it somehow magically fixes itself the next time around + if !has_libraryfolder(&mount) { + error!("Unable to determine the mount point for the MicroSD card"); + continue; + } } // Do we have changes in the steam directory. This should only occur when something has been added/deleted let hash = match datastore.is_hash_changed(&cid, &mount) { - None => continue, + None => { + debug!("No hash found. Skipping iteration"); + continue; + } Some(v) => v, }; - info!("Watcher Detected update"); + info!(hash = hash, "Watcher Detected update"); // Something went wrong during parsing. Not great - if let Err(err) = read_msd_directory(datastore.borrow(), &mount) { - error!("Problem reading MicroSD Card: \"{err}\""); + if let Err(err) = read_microsd_steam_dir(datastore.borrow(), &mount) { + error!(%err, "Failed to read MicroSD card library data, Reason: \"{}\"", err); continue; } // commit update + trace!(hash, "Updating hash in database"); datastore.update_hash(&cid, hash); + trace!("Sending Updated event"); let _ = sender.send(CardEvent::Updated); } } diff --git a/backend/version b/backend/version index 71172b4..42624f3 100644 --- a/backend/version +++ b/backend/version @@ -1 +1 @@ -0.10.1 \ No newline at end of file +0.10.2 \ No newline at end of file diff --git a/lib/src/MicoSDeck.ts b/lib/src/MicoSDeck.ts index a520b33..405657c 100644 --- a/lib/src/MicoSDeck.ts +++ b/lib/src/MicoSDeck.ts @@ -32,7 +32,7 @@ export { version }; * Matches the current MicroSDeck version against the version provided. * Returns true if the semver matches and compatibility is guaranteed. * @export - * @param {string} version1 his should be in valid semver notation of x.y.z + * @param {string} version1 This should be in valid semver notation of x.y.z * @param {string} version2 This should be in valid semver notation of x.y.z */ export function IsMatchingSemver(version1: string, version2: string, strict: boolean = false): boolean { diff --git a/main.py b/main.py index 64c2e0a..0bab63c 100644 --- a/main.py +++ b/main.py @@ -38,4 +38,7 @@ async def _main(self): case _code: print("Program exited with exit code: " + _code) break; - \ No newline at end of file + # Function used to clean up a plugin when it's told to unload by Decky-Loader + + async def _unload(self): + self.backend_proc.kill(); \ No newline at end of file diff --git a/util/versioning.mjs b/util/versioning.mjs index bfb8b4d..eb95b9c 100644 --- a/util/versioning.mjs +++ b/util/versioning.mjs @@ -4,7 +4,7 @@ import { fileURLToPath } from "url"; export const Version = ReadPackageVersion(); function ReadPackageVersion() { - return readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../backend/version"), { encoding: "utf8" }); + return readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../backend/version"), { encoding: "utf8" }).trim(); } function WriteVersionToPackage(file, version) { diff --git a/version b/version new file mode 120000 index 0000000..41fd75c --- /dev/null +++ b/version @@ -0,0 +1 @@ +backend/version \ No newline at end of file