From 4eaf6fae2bba0219a316d37499ee6e1549645862 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 6 Jan 2024 13:26:35 -0500 Subject: [PATCH] Add all back-end functionality for interacting with variants on the front-end --- backend/Cargo.lock | 16 +- backend/Cargo.toml | 4 +- .../src/v1/metadata.rs | 3 +- .../src/api/get_game.rs | 121 +++++++++++++++ .../src/api/get_setting.rs | 5 +- backend/community_settings_srv/src/api/mod.rs | 2 + .../src/api/save_setting.rs | 64 +++++++- .../community_settings_srv/src/file_util.rs | 54 ++++++- backend/community_settings_srv/src/main.rs | 5 + backend/src/api/api_types.rs | 6 + backend/src/api/general.rs | 58 +++++++ backend/src/api/handler.rs | 14 +- backend/src/api/mod.rs | 1 + backend/src/api/web.rs | 142 ++++++++++++++++++ backend/src/main.rs | 18 ++- backend/src/persist/file.rs | 17 ++- backend/src/persist/mod.rs | 2 + backend/src/settings/general.rs | 42 +++++- backend/src/settings/traits.rs | 12 +- src/backend.ts | 31 ++++ 20 files changed, 590 insertions(+), 27 deletions(-) create mode 100644 backend/community_settings_srv/src/api/get_game.rs create mode 100644 backend/src/api/web.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 719de2a..0daaacc 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -289,6 +289,13 @@ dependencies = [ "cc", ] +[[package]] +name = "community_settings_core" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -778,7 +785,9 @@ dependencies = [ [[package]] name = "libryzenadj" -version = "0.14.0" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5bccdf07c3234c06c435648a53d8cb369f76d20e03bb8d2f8c24fb2330efc32" dependencies = [ "errno", "libryzenadj-sys", @@ -788,7 +797,9 @@ dependencies = [ [[package]] name = "libryzenadj-sys" -version = "0.14.0" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de3621be974e892e12d4a07a6a2e32b6a05950759b062d94f5b54f78fabc3a" dependencies = [ "bindgen", "cmake", @@ -1055,6 +1066,7 @@ name = "powertools" version = "1.5.0-ng1" dependencies = [ "async-trait", + "community_settings_core", "libryzenadj", "limits_core", "log", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 660416e..15a72f7 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -28,9 +28,11 @@ simplelog = "0.12" # limits & driver functionality limits_core = { version = "3", path = "./limits_core" } +community_settings_core = { version = "0.1", path = "./community_settings_core" } regex = "1" smokepatio = { version = "*", path = "../../smokepatio" } -libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } +#libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } +libryzenadj = { version = "0.13" } # ureq's tls feature does not like musl targets ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } diff --git a/backend/community_settings_core/src/v1/metadata.rs b/backend/community_settings_core/src/v1/metadata.rs index 0208658..009baa6 100644 --- a/backend/community_settings_core/src/v1/metadata.rs +++ b/backend/community_settings_core/src/v1/metadata.rs @@ -5,7 +5,8 @@ pub struct Metadata { pub name: String, pub steam_app_id: u32, pub steam_user_id: u64, - pub stream_username: String, + pub steam_username: String, + pub tags: Vec, /// Should always be a valid u128, but some parsers do not support that pub id: String, pub config: super::Config, diff --git a/backend/community_settings_srv/src/api/get_game.rs b/backend/community_settings_srv/src/api/get_game.rs new file mode 100644 index 0000000..c7d1966 --- /dev/null +++ b/backend/community_settings_srv/src/api/get_game.rs @@ -0,0 +1,121 @@ +use actix_web::{get, web, Responder, http::header}; + +use crate::cli::Cli; +use crate::file_util; + +const MAX_RESULTS: usize = 50; + +fn special_settings() -> Vec { + vec![ + community_settings_core::v1::Metadata { + name: "Zeroth the Least".to_owned(), + steam_app_id: 0, + steam_user_id: 76561198116690523, + steam_username: "NGnius".to_owned(), + tags: vec!["0".to_owned(), "gr8".to_owned()], + id: 0.to_string(), + config: community_settings_core::v1::Config { + cpus: vec![ + community_settings_core::v1::Cpu { + online: true, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Michaƫlle Jean".to_owned(), + }, + community_settings_core::v1::Cpu { + online: false, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Adrienne Clarkson".to_owned(), + }, + community_settings_core::v1::Cpu { + online: true, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Michael Collins".to_owned(), + } + ], + gpu: community_settings_core::v1::Gpu { + fast_ppt: Some(1), + slow_ppt: Some(1), + tdp: None, + tdp_boost: None, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + slow_memory: false, + }, + battery: community_settings_core::v1::Battery { + charge_rate: Some(42), + charge_mode: Some("nuclear fusion".to_owned()), + events: vec![ + community_settings_core::v1::BatteryEvent { + trigger: "anything but one on a gun".to_owned(), + charge_rate: Some(42), + charge_mode: Some("neutral".to_owned()), + } + ], + } + } + } + ] +} + +fn get_some_settings_by_app_id(steam_app_id: u32, cli: &'static Cli) -> std::io::Result> { + let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, steam_app_id); + let mut files: Vec<_> = app_id_folder.read_dir()? + .filter_map(|res| res.ok()) + .filter(|f| f.path().extension().map(|ext| ext == file_util::RON_EXTENSION).unwrap_or(false)) + .filter_map(|f| f.metadata().ok().map(|meta| (f, meta))) + .filter_map(|(f, meta)| meta.created().ok().map(|time| (f, meta, time))) + .collect(); + files.sort_by(|(_, _, a_created), (_, _, b_created)| a_created.cmp(b_created)); + + let mut results = Vec::with_capacity(MAX_RESULTS); + for (_, (f, _, _)) in files.into_iter().enumerate().take_while(|(i, _)| *i < MAX_RESULTS) { + let reader = std::io::BufReader::new(std::fs::File::open(f.path())?); + let setting = match ron::de::from_reader(reader) { + Ok(x) => x, + Err(e) => { + let e_msg = format!("{}", e); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + }; + results.push(setting); + } + Ok(results) +} + +#[get("/api/setting/by_app_id/{id}")] +pub async fn get_setting_by_app_id_handler( + id: web::Path, + accept: web::Header, + cli: web::Data<&'static Cli>, +) -> std::io::Result { + let id: u32 = *id; + println!("Accept: {}", accept.to_string()); + let preferred = accept.preference(); + if super::is_mime_type_ron_capable(&preferred) { + // Send RON + let ron = if id != 0 { + get_some_settings_by_app_id(id, &*cli)? + } else { + special_settings() + }; + // TODO don't dump to string + let result_body = ron::ser::to_string(&ron).unwrap(); + Ok(actix_web::HttpResponse::Ok() + //.insert_header(header::ContentType("application/ron".parse().unwrap())) + .insert_header(header::ContentType(mime::STAR_STAR)) + .body(actix_web::body::BoxBody::new(result_body)) + ) + } else { + // Send JSON (fallback) + let json = if id != 0 { + get_some_settings_by_app_id(id, &*cli)? + } else { + special_settings() + }; + // TODO don't dump to string + let result_body = serde_json::to_string(&json).unwrap(); + Ok(actix_web::HttpResponse::Ok() + .insert_header(header::ContentType::json()) + .body(actix_web::body::BoxBody::new(result_body)) + ) + } +} diff --git a/backend/community_settings_srv/src/api/get_setting.rs b/backend/community_settings_srv/src/api/get_setting.rs index 9ee986d..47f9cc4 100644 --- a/backend/community_settings_srv/src/api/get_setting.rs +++ b/backend/community_settings_srv/src/api/get_setting.rs @@ -8,7 +8,8 @@ fn special_settings() -> community_settings_core::v1::Metadata { name: "Zeroth the Least".to_owned(), steam_app_id: 1675200, steam_user_id: 76561198116690523, - stream_username: "NGnius".to_owned(), + steam_username: "NGnius".to_owned(), + tags: vec!["0".to_owned(), "gr8".to_owned()], id: 0.to_string(), config: community_settings_core::v1::Config { cpus: vec![ @@ -51,7 +52,7 @@ fn special_settings() -> community_settings_core::v1::Metadata { } } -#[get("/api/setting/{id}")] +#[get("/api/setting/by_id/{id}")] pub async fn get_setting_handler( id: web::Path, accept: web::Header, diff --git a/backend/community_settings_srv/src/api/mod.rs b/backend/community_settings_srv/src/api/mod.rs index 7581e25..ca947e3 100644 --- a/backend/community_settings_srv/src/api/mod.rs +++ b/backend/community_settings_srv/src/api/mod.rs @@ -1,6 +1,8 @@ +mod get_game; mod get_setting; mod save_setting; +pub use get_game::get_setting_by_app_id_handler as get_setting_by_steam_app_id; pub use get_setting::get_setting_handler as get_setting_by_id; pub use save_setting::save_setting_handler as save_setting_with_new_id; diff --git a/backend/community_settings_srv/src/api/save_setting.rs b/backend/community_settings_srv/src/api/save_setting.rs index 5e84d8e..39d610e 100644 --- a/backend/community_settings_srv/src/api/save_setting.rs +++ b/backend/community_settings_srv/src/api/save_setting.rs @@ -18,7 +18,7 @@ pub async fn save_setting_handler( Err(_e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "too many bytes in payload")), }; let next_id = file_util::next_setting_id(&cli.folder); - let parsed_data = if super::is_mime_type_ron_capable(&content_type) { + let parsed_data: community_settings_core::v1::Metadata = if super::is_mime_type_ron_capable(&content_type) { // Parse as RON match ron::de::from_reader(bytes.as_ref()) { Ok(x) => x, @@ -44,15 +44,15 @@ pub async fn save_setting_handler( }; // TODO validate user and app id // Reject blocked users and apps - let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION); - let writer = std::io::BufWriter::new(std::fs::File::create(path)?); + let path_ron = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION); + let writer = std::io::BufWriter::new(std::fs::File::create(&path_ron)?); if let Err(e) = ron::ser::to_writer(writer, &parsed_data) { let e_msg = format!("{}", e); return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); } - let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION); - let writer = std::io::BufWriter::new(std::fs::File::create(path)?); + let path_json = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION); + let writer = std::io::BufWriter::new(std::fs::File::create(&path_json)?); if let Err(e) = serde_json::to_writer(writer, &parsed_data) { let e_msg = format!("{}", e); if let Some(io_e) = e.io_error_kind() { @@ -62,7 +62,59 @@ pub async fn save_setting_handler( } } - // TODO create symlinks for other ways of looking up these settings files + // create symlinks for other ways of looking up these settings files + let filename_ron = file_util::filename(next_id, file_util::RON_EXTENSION); + let filename_json = file_util::filename(next_id, file_util::JSON_EXTENSION); + + // create symlinks to app id folder + let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, parsed_data.steam_app_id); + if !app_id_folder.exists() { + std::fs::create_dir(&app_id_folder)?; + } + #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained + { + std::os::windows::fs::symlink_file(&path_ron, app_id_folder.join(&filename_ron))?; + std::os::windows::fs::symlink_file(&path_json, app_id_folder.join(&filename_json))?; + } + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&path_ron, app_id_folder.join(&filename_ron))?; + std::os::unix::fs::symlink(&path_json, app_id_folder.join(&filename_json))?; + } + + // create symlinks for user id folder + let user_id_folder = file_util::setting_folder_by_user_id(&cli.folder, parsed_data.steam_user_id); + if !user_id_folder.exists() { + std::fs::create_dir(&user_id_folder)?; + } + #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained + { + std::os::windows::fs::symlink_file(&path_ron, user_id_folder.join(&filename_ron))?; + std::os::windows::fs::symlink_file(&path_json, user_id_folder.join(&filename_json))?; + } + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&path_ron, user_id_folder.join(&filename_ron))?; + std::os::unix::fs::symlink(&path_json, user_id_folder.join(&filename_json))?; + } + + // create symlinks for each tag + for tag in parsed_data.tags.iter() { + let tag_folder = file_util::setting_folder_by_tag(&cli.folder, tag); + if !tag_folder.exists() { + std::fs::create_dir(&tag_folder)?; + } + #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained + { + std::os::windows::fs::symlink_file(&path_ron, tag_folder.join(&filename_ron))?; + std::os::windows::fs::symlink_file(&path_json, tag_folder.join(&filename_json))?; + } + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&path_ron, tag_folder.join(&filename_ron))?; + std::os::unix::fs::symlink(&path_json, tag_folder.join(&filename_json))?; + } + } Ok(actix_web::HttpResponse::NoContent()) } diff --git a/backend/community_settings_srv/src/file_util.rs b/backend/community_settings_srv/src/file_util.rs index 467e304..670c608 100644 --- a/backend/community_settings_srv/src/file_util.rs +++ b/backend/community_settings_srv/src/file_util.rs @@ -6,14 +6,66 @@ pub const JSON_EXTENSION: &'static str = "json"; const SETTING_FOLDER: &'static str = "settings"; const ID_FOLDER: &'static str = "by_id"; +const APP_ID_FOLDER: &'static str = "by_app_id"; +const USER_ID_FOLDER: &'static str = "by_user_id"; +const TAG_FOLDER: &'static str = "by_tag"; static LAST_SETTING_ID: Mutex = Mutex::new(0); +pub fn build_folder_layout(root: impl AsRef) -> std::io::Result<()> { + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(ID_FOLDER) + )?; + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(APP_ID_FOLDER) + )?; + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(USER_ID_FOLDER) + )?; + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(TAG_FOLDER) + )?; + Ok(()) +} + +pub fn filename(id: u128, ext: &str) -> String { + format!("{}.{}", id, ext) +} + pub fn setting_path_by_id(root: impl AsRef, id: u128, ext: &str) -> PathBuf { root.as_ref() .join(SETTING_FOLDER) .join(ID_FOLDER) - .join(format!("{}.{}", id, ext)) + .join(filename(id, ext)) +} + +pub fn setting_folder_by_app_id(root: impl AsRef, steam_app_id: u32) -> PathBuf { + root.as_ref() + .join(SETTING_FOLDER) + .join(APP_ID_FOLDER) + .join(steam_app_id.to_string()) +} + +pub fn setting_folder_by_user_id(root: impl AsRef, steam_user_id: u64) -> PathBuf { + root.as_ref() + .join(SETTING_FOLDER) + .join(USER_ID_FOLDER) + .join(steam_user_id.to_string()) +} + +pub fn setting_folder_by_tag(root: impl AsRef, tag: &str) -> PathBuf { + root.as_ref() + .join(SETTING_FOLDER) + .join(TAG_FOLDER) + .join(tag) } pub fn next_setting_id(root: impl AsRef) -> u128 { diff --git a/backend/community_settings_srv/src/main.rs b/backend/community_settings_srv/src/main.rs index 70ba717..2753bfa 100644 --- a/backend/community_settings_srv/src/main.rs +++ b/backend/community_settings_srv/src/main.rs @@ -25,6 +25,10 @@ async fn main() -> std::io::Result<()> { .unwrap(); log::debug!("Logging to: {}", args.log.display()); + // setup + log::debug!("Building folder layout (if not exists) at: {}", &args.folder.display()); + file_util::build_folder_layout(&args.folder)?; + let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args)); HttpServer::new(move || { App::new() @@ -32,6 +36,7 @@ async fn main() -> std::io::Result<()> { //.app_data(web::Data::new(IndexPage::load("dist/index.html").unwrap())) //.app_data(basic::Config::default().realm("Restricted area")) .service(api::get_setting_by_id) + .service(api::get_setting_by_steam_app_id) .service(api::save_setting_with_new_id) }) .bind(("0.0.0.0", leaked_args.port))? diff --git a/backend/src/api/api_types.rs b/backend/src/api/api_types.rs index 8d72107..cf889d3 100644 --- a/backend/src/api/api_types.rs +++ b/backend/src/api/api_types.rs @@ -61,3 +61,9 @@ pub struct GpuLimits { pub clock_step: u64, pub memory_control_capable: bool, } + +#[derive(Serialize, Deserialize)] +pub struct VariantInfo { + pub id: String, + pub name: String, +} diff --git a/backend/src/api/general.rs b/backend/src/api/general.rs index 73cb706..a534d22 100644 --- a/backend/src/api/general.rs +++ b/backend/src/api/general.rs @@ -391,3 +391,61 @@ fn wait_for_response(sender: &Sender, rx: mpsc::Receiver, api_ sender.send(api_msg).expect(&format!("{} send failed", op)); rx.recv().expect(&format!("{} callback recv failed", op)) } + +/// Generate get variants +pub fn get_all_variants(sender: Sender) -> impl AsyncCallable { + let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety + let getter = move || { + let sender2 = sender.clone(); + move || { + let (tx, rx) = mpsc::channel(); + let callback = + move |variants: Vec| tx.send(variants).expect("get_all_variants callback send failed"); + sender2 + .lock() + .unwrap() + .send(ApiMessage::General(GeneralMessage::GetAllVariants( + Box::new(callback), + ))) + .expect("get_all_variants send failed"); + rx.recv().expect("get_all_variants callback recv failed") + } + }; + super::async_utils::AsyncIshGetter { + set_get: getter, + trans_getter: |result| { + let mut output = Vec::with_capacity(result.len()); + for status in result.iter() { + output.push(Primitive::Json(serde_json::to_string(status).expect("Failed to serialize variant info to JSON"))); + } + output + }, + } +} + +/// Generate get current variant +pub fn get_current_variant(sender: Sender) -> impl AsyncCallable { + let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety + let getter = move || { + let sender2 = sender.clone(); + move || { + let (tx, rx) = mpsc::channel(); + let callback = + move |variant: super::VariantInfo| tx.send(variant).expect("get_all_variants callback send failed"); + sender2 + .lock() + .unwrap() + .send(ApiMessage::General(GeneralMessage::GetCurrentVariant( + Box::new(callback), + ))) + .expect("get_current_variant send failed"); + rx.recv().expect("get_current_variant callback recv failed") + } + }; + super::async_utils::AsyncIshGetter { + set_get: getter, + trans_getter: |result| { + vec![Primitive::Json(serde_json::to_string(&result).expect("Failed to serialize variant info to JSON"))] + }, + } +} diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index 69234c6..a00507e 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -227,6 +227,9 @@ pub enum GeneralMessage { GetPersistent(Callback), GetCurrentProfileName(Callback), GetPath(Callback), + GetCurrentVariant(Callback), + GetAllVariants(Callback>), + AddVariant(crate::persist::SettingsJson, Callback>), ApplyNow, } @@ -238,6 +241,15 @@ impl GeneralMessage { Self::GetPersistent(cb) => cb(*settings.persistent()), Self::GetCurrentProfileName(cb) => cb(settings.get_name().to_owned()), Self::GetPath(cb) => cb(settings.get_path().to_owned()), + Self::GetCurrentVariant(cb) => cb(settings.get_variant_info()), + Self::GetAllVariants(cb) => cb(settings.get_variants()), + Self::AddVariant(variant, cb) => match settings.add_variant(variant) { + Ok(variants) => cb(variants), + Err(e) => { + print_errors("GeneralMessage::AddVariant => TGeneral::add_variant", vec![e]); + cb(Vec::with_capacity(0)) + }, + }, Self::ApplyNow => {} } dirty @@ -389,7 +401,7 @@ impl ApiMessageHandler { true } ApiMessage::LoadSystemSettings => { - settings.load_system_default(settings.general.get_name().to_owned(), settings.general.get_variant_id(), settings.general.get_variant_name().to_owned()); + settings.load_system_default(settings.general.get_name().to_owned(), settings.general.get_variant_id(), settings.general.get_variant_info().name); true } ApiMessage::GetLimits(cb) => { diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index 8dc8add..488faf3 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -7,6 +7,7 @@ pub mod gpu; pub mod handler; pub mod message; mod utility; +pub mod web; pub(super) type ApiParameterType = Vec; diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs new file mode 100644 index 0000000..a7b5f0d --- /dev/null +++ b/backend/src/api/web.rs @@ -0,0 +1,142 @@ +use std::sync::mpsc::{self, Sender}; +use std::sync::{Arc, Mutex}; +use usdpl_back::core::serdes::Primitive; +use usdpl_back::AsyncCallable; + +use super::handler::{ApiMessage, GeneralMessage}; + +const BASE_URL: &'static str = "http://powertools.ngni.us"; + +/// Get search results web method +pub fn search_by_app_id() -> impl AsyncCallable { + let getter = move || { + move |steam_app_id: u32| { + let req_url = format!("{}/api/setting/by_app_id/{}", BASE_URL, steam_app_id); + match ureq::get(&req_url).call() { + Ok(response) => { + let json_res: std::io::Result> = response.into_json(); + match json_res { + Ok(search_results) => { + // search results may be quite large, so let's do the JSON string conversion in the background (blocking) thread + match serde_json::to_string(&search_results) { + Err(e) => log::error!("Cannot convert search results from `{}` to JSON: {}", req_url, e), + Ok(s) => return s, + } + } + Err(e) => { + log::error!("Cannot parse response from `{}`: {}", req_url, e) + } + } + } + Err(e) => log::warn!("Cannot get search results from `{}`: {}", req_url, e), + } + "[]".to_owned() + } + }; + super::async_utils::AsyncIsh { + trans_setter: |params| { + if let Some(Primitive::F64(app_id)) = params.get(0) { + Ok(*app_id as u32) + } else { + Err("search_by_app_id missing/invalid parameter 0".to_owned()) + } + }, + set_get: getter, + trans_getter: |result| vec![Primitive::Json(result)], + } +} + +fn web_config_to_settings_json(meta: community_settings_core::v1::Metadata) -> crate::persist::SettingsJson { + crate::persist::SettingsJson { + version: crate::persist::LATEST_VERSION, + name: meta.name, + variant: u64::MAX, // TODO maybe change this to use the 64 low bits of id (u64::MAX will cause it to generate a new id when added to file variant map + persistent: true, + cpus: meta.config.cpus.into_iter().map(|cpu| crate::persist::CpuJson { + online: cpu.online, + clock_limits: cpu.clock_limits.map(|lim| crate::persist::MinMaxJson { + min: lim.min, + max: lim.max, + }), + governor: cpu.governor, + root: None, + }).collect(), + gpu: crate::persist::GpuJson { + fast_ppt: meta.config.gpu.fast_ppt, + slow_ppt: meta.config.gpu.slow_ppt, + tdp: meta.config.gpu.tdp, + tdp_boost: meta.config.gpu.tdp_boost, + clock_limits: meta.config.gpu.clock_limits.map(|lim| crate::persist::MinMaxJson { + min: lim.min, + max: lim.max, + }), + slow_memory: meta.config.gpu.slow_memory, + root: None, + }, + battery: crate::persist::BatteryJson { + charge_rate: meta.config.battery.charge_rate, + charge_mode: meta.config.battery.charge_mode, + events: meta.config.battery.events.into_iter().map(|be| crate::persist::BatteryEventJson { + charge_rate: be.charge_rate, + charge_mode: be.charge_mode, + trigger: be.trigger, + }).collect(), + root: None, + }, + provider: Some(crate::persist::DriverJson::AutoDetect), + } +} + +/// Download config web method +pub fn download_new_config(sender: Sender) -> impl AsyncCallable { + let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety + let getter = move || { + let sender2 = sender.clone(); + move |id: u128| { + let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id); + match ureq::get(&req_url).call() { + Ok(response) => { + let json_res: std::io::Result = response.into_json(); + match json_res { + Ok(meta) => { + let (tx, rx) = mpsc::channel(); + let callback = + move |values: Vec| tx.send(values).expect("download_new_config callback send failed"); + sender2 + .lock() + .unwrap() + .send(ApiMessage::General(GeneralMessage::AddVariant(web_config_to_settings_json(meta), Box::new(callback)))) + .expect("download_new_config send failed"); + return rx.recv().expect("download_new_config callback recv failed"); + } + Err(e) => { + log::error!("Cannot parse response from `{}`: {}", req_url, e) + } + } + } + Err(e) => log::warn!("Cannot get setting result from `{}`: {}", req_url, e), + } + vec![] + } + }; + super::async_utils::AsyncIsh { + trans_setter: |params| { + if let Some(Primitive::String(id)) = params.get(0) { + match id.parse::() { + Ok(id) => Ok(id), + Err(e) => Err(format!("download_new_config non-u128 string parameter 0: {} (got `{}`)", e, id)) + } + } else { + Err("download_new_config missing/invalid parameter 0".to_owned()) + } + }, + set_get: getter, + trans_getter: |result| { + let mut output = Vec::with_capacity(result.len()); + for status in result.iter() { + output.push(Primitive::Json(serde_json::to_string(status).expect("Failed to serialize variant info to JSON"))); + } + output + }, + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index a07b4d4..e62a4a6 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -299,8 +299,24 @@ fn main() -> Result<(), ()> { "GENERAL_get_periodicals", api::general::get_periodicals(api_sender.clone()) ) + .register_async( + "GENERAL_get_all_variants", + api::general::get_all_variants(api_sender.clone()) + ) + .register_async( + "GENERAL_get_current_variant", + api::general::get_current_variant(api_sender.clone()) + ) .register_async("MESSAGE_get", message_getter) - .register_async("MESSAGE_dismiss", message_dismisser); + .register_async("MESSAGE_dismiss", message_dismisser) + .register_async( + "WEB_search_by_app", + api::web::search_by_app_id() + ) + .register_async( + "WEB_download_new", + api::web::download_new_config(api_sender.clone()) + ); if let Err(e) = loaded_settings.on_set() { e.iter() diff --git a/backend/src/persist/file.rs b/backend/src/persist/file.rs index 2750996..b6cdee2 100644 --- a/backend/src/persist/file.rs +++ b/backend/src/persist/file.rs @@ -37,14 +37,24 @@ impl FileJson { ron::de::from_reader(&mut file).map_err(|e| SerdeError::Serde(e.into())) } - pub fn update_variant_or_create>(path: P, setting: SettingsJson, given_name: String) -> Result<(), SerdeError> { + fn next_available_id(&self) -> u64 { + self.variants.keys() + .max() + .map(|k| k+1) + .unwrap_or(0) + } + + pub fn update_variant_or_create>(path: P, mut setting: SettingsJson, given_name: String) -> Result { if !setting.persistent { - return Ok(()) + return Self::open(path) } let path = path.as_ref(); let file = if path.exists() { let mut file = Self::open(path)?; + if setting.variant == u64::MAX { + setting.variant = file.next_available_id(); + } file.variants.insert(setting.variant, setting); file } else { @@ -57,6 +67,7 @@ impl FileJson { } }; - file.save(path) + file.save(path)?; + Ok(file) } } diff --git a/backend/src/persist/mod.rs b/backend/src/persist/mod.rs index 4c9a31b..809890d 100644 --- a/backend/src/persist/mod.rs +++ b/backend/src/persist/mod.rs @@ -14,3 +14,5 @@ pub use general::{MinMaxJson, SettingsJson}; pub use gpu::GpuJson; pub use error::{SerdeError, RonError}; + +pub const LATEST_VERSION: u64 = 0; diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index b1d5cf8..d2c6b8d 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -89,14 +89,45 @@ impl TGeneral for General { self.variant_id = id; } - fn get_variant_name(&self) -> &'_ str { - &self.variant_name - } - fn variant_name(&mut self, name: String) { self.variant_name = name; } + fn get_variants(&self) -> Vec { + if let Ok(file) = crate::persist::FileJson::open(self.get_path()) { + file.variants.into_iter() + .map(|(id, conf)| crate::api::VariantInfo { + id: id.to_string(), + name: conf.name, + }) + .collect() + } else { + vec![self.get_variant_info()] + } + } + + fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result, SettingError> { + let variant_name = variant.name.clone(); + crate::persist::FileJson::update_variant_or_create(self.get_path(), variant, variant_name) + .map_err(|e| SettingError { + msg: format!("failed to add variant: {}", e), + setting: SettingVariant::General, + }) + .map(|file| file.variants.into_iter() + .map(|(id, conf)| crate::api::VariantInfo { + id: id.to_string(), + name: conf.name, + }) + .collect()) + } + + fn get_variant_info(&self) -> crate::api::VariantInfo { + crate::api::VariantInfo { + id: self.variant_id.to_string(), + name: self.variant_name.clone(), + } + } + fn provider(&self) -> crate::persist::DriverJson { self.driver.clone() } @@ -265,9 +296,10 @@ impl Settings { }*/ pub fn json(&self) -> SettingsJson { + let variant_info = self.general.get_variant_info(); SettingsJson { version: LATEST_VERSION, - name: self.general.get_variant_name().to_owned(), + name: variant_info.name, variant: self.general.get_variant_id(), persistent: self.general.get_persistent(), cpus: self.cpus.json(), diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index 8d93c55..cacff0a 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -109,14 +109,18 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send { fn name(&mut self, name: String); - fn get_variant_id(&self) -> u64; - fn variant_id(&mut self, id: u64); - fn get_variant_name(&self) -> &'_ str; - fn variant_name(&mut self, name: String); + fn get_variant_id(&self) -> u64; + + fn get_variants(&self) -> Vec; + + fn get_variant_info(&self) -> crate::api::VariantInfo; + + fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result, SettingError>; + fn provider(&self) -> crate::persist::DriverJson; } diff --git a/src/backend.ts b/src/backend.ts index 1d8d1f0..b172690 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -349,3 +349,34 @@ export async function getPeriodicals(): Promise { settings_path: result[4], }; } + +export type StoreMetadata = { + name: string, + steam_app_id: number, + steam_user_id: number, + steam_username: string, + tags: string[], + id: string, + //config: any, +} + +export async function searchStoreByAppId(id: number): Promise { + return (await call_backend("WEB_search_by_app", [id]))[0]; +} + +export type VariantInfo = { + id: string, + name: string, +} + +export async function storeDownloadById(id: string): Promise { + return (await call_backend("WEB_download_new", [id])); +} + +export async function getAllSettingVariants(): Promise { + return (await call_backend("GENERAL_get_all_variants", [])); +} + +export async function getCurrentSettingVariant(): Promise { + return (await call_backend("GENERAL_get_current_variant", []))[0]; +}