Compare commits

..

25 commits
v2.0.0 ... main

Author SHA1 Message Date
NGnius (Graham) bf7b5617b2 Update README 2024-06-13 18:37:46 -04:00
NGnius (Graham) fadf492aac Create PR template 2024-06-13 18:17:42 -04:00
NGnius (Graham) 921f360e9e Add question issue template, update other templates 2024-06-13 18:03:55 -04:00
NGnius (Graham) 1996eb7e16 Merge branch 'dev' 2024-05-08 22:08:05 -04:00
NGnius (Graham) 9a8f1c9d34 Fix incorrect path in log message 2024-05-08 19:11:31 -04:00
NGnius (Graham) 6b3b1a5966 Re-enable dirty echoes to hopefully mitigate #144 2024-05-07 21:22:01 -04:00
NGnius (Graham) 655946036d Update limits file on server too 2024-05-07 21:15:10 -04:00
NGnius (Graham) 6b7ce73a24 Merge branch 'dev' 2024-05-07 20:23:24 -04:00
NGnius (Graham) 2b3fb4ac9a Make Stanto happy #152 2024-05-07 18:46:21 -04:00
NGnius (Graham) 83983a111d Revert "Update battery charge rate to % #156"
This reverts commit dc014ca1c7.
2024-05-07 18:44:30 -04:00
NGnius (Graham) d7489d5d04 Add more logging for #153 2024-05-07 18:40:06 -04:00
NGnius (Graham) dc014ca1c7 Update battery charge rate to % #156 2024-05-07 17:57:48 -04:00
NGnius (Graham) d4377872aa Update to sysfuss v0.3, sort of fix #156 2024-05-07 17:51:44 -04:00
NGnius (Graham) 64dc319223 Merge branch 'dev' 2024-04-25 22:13:24 -04:00
NGnius (Graham) 29211b4762 Fix infinite load on new profile variant 2024-04-25 21:33:14 -04:00
NGnius (Graham) 5f642a33e3 Merge branch 'dev' 2024-04-23 21:15:58 -04:00
NGnius (Graham) c2f834858c Configure for rootless docker 2024-04-23 21:13:45 -04:00
NGnius (Graham) 3f3241ed45 Merge branch 'dev' 2024-04-21 20:51:41 -04:00
NGnius (Graham) 22e916fe12 Add some deduplication and another symlink fixer 2024-04-21 12:34:11 -04:00
NGnius (Graham) 6c28cef9b3 Fix store scrolling 2024-04-19 22:18:20 -04:00
NGnius (Graham) c1064fdf92 Move filesystem fixes and improvements to separate file 2024-04-19 22:18:06 -04:00
NGnius (Graham) 81b9107bf5 Add some double-criteria symlinks (by app id and tag, by user id and tag) 2024-04-13 10:14:02 -04:00
NGnius (Graham) 9d452acc28 Fix ID population 2024-04-12 21:55:29 -04:00
NGnius (Graham) be60be71ac Fix symlink creation again... fix reading file creation->access time 2024-04-12 21:35:10 -04:00
NGnius (Graham) 6f2c2b186f Fix symlinking logic 2024-04-12 17:31:48 -04:00
42 changed files with 903 additions and 170 deletions

View file

@ -20,7 +20,7 @@ body:
id: reproduction id: reproduction
attributes: attributes:
label: Steps To Reproduce label: Steps To Reproduce
description: Steps to reproduce the behavior description: Steps to reproduce the behaviour.
placeholder: | placeholder: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
@ -33,12 +33,12 @@ body:
attributes: attributes:
label: Anything else? label: Anything else?
description: | description: |
Screenshots? Logs? pt_oc.json? limits_overrides.json? Anything that will give more context about the problem! Screenshots? Logs? limits_overrides.ron? limits_cache.ron? Anything that will give more context about the problem!
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
Please include the log (located at `/tmp/powertools.log`) if possible. Please include the log (located at `/tmp/powertools.log`) if possible.
Note: the log is deleted when the device is restarted. Note: the log is deleted when the device is restarted.
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. Tip: You can attach files by clicking this area to highlight it and then dragging them in.
validations: validations:
required: false required: false
- type: input - type: input

View file

@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: NGnius - name: NGnius
url: https://github.com/NGnius url: https://git.ngni.us/sys/website/wiki#contact-information
about: Repository owner about: Repository owner

View file

@ -3,7 +3,7 @@ description: Suggest functionality to add
labels: "enhancement" labels: "enhancement"
body: body:
- type: textarea - type: textarea
id: described id: feature-description
attributes: attributes:
label: Describe what you'd like to be able to do label: Describe what you'd like to be able to do
description: A clear and concise description of what you want. description: A clear and concise description of what you want.
@ -22,7 +22,11 @@ body:
id: extras id: extras
attributes: attributes:
label: Anything else? label: Anything else?
description: Description of how this can be achieved, or other additional context description: |
Description of how this can be achieved, or other additional context.
If this is related to the UI, consider adding a picture.
Tip: You can attach files by clicking this area to highlight it and then dragging them in.
placeholder: This can be accomplished by doing ... placeholder: This can be accomplished by doing ...
validations: validations:
required: false required: false

23
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: "Question"
description: "Ask for more information about PowerTools"
labels: "question"
body:
- type: textarea
id: question-elaboration
attributes:
label: Question
description: |
A clear and concise description of what you'd like to know.
Please check the wiki and closed issues to avoid waiting for an answer when you didn't need to.
validations:
required: true
- type: textarea
id: extras
attributes:
label: Extra Info
description: |
Additional context or information which may be helpful when answering your question.
Tip: You can attach files by clicking this area to highlight it and then dragging them in.
validations:
required: false

13
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,13 @@
## Description
A short description of what the PR changes.
If this is a minor change and the commit message(s) explains it well, feel free to delete this section.
## Motivation
Why did you write this PR and/or why should it be accepted.
## Fixes
Please indicate issues that this fixes or addresses here.
If this is a bugfix with no pre-existing issue, please describe the bug here instead.

1
.gitignore vendored
View file

@ -49,3 +49,4 @@ yalc.lock
# packaging # packaging
/PowerTools /PowerTools
**.zip **.zip
deck.json

View file

@ -1,12 +1,12 @@
# PowerTools # PowerTools
<!-- TODO Update badges for new git repo location --> <!-- TODO Update badges for new git repo location -->
[![Decky store](https://img.shields.io/badge/dynamic/json?color=blue&label=release&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fplugins.deckbrew.xyz%2Fplugins&style=flat-square)](https://plugins.deckbrew.xyz/) [![Decky store](https://img.shields.io/badge/dynamic/json?color=blue&label=release&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fplugins.deckbrew.xyz%2Fplugins&style=flat-square)](https://plugins.deckbrew.xyz/)
[![Custom store](https://img.shields.io/badge/dynamic/json?color=blue&label=preview&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fnot-decky-alpha.ngni.us%2Fplugins&style=flat-square)](https://github.com/NGnius/PowerTools/wiki) [![Custom store](https://img.shields.io/badge/dynamic/json?color=blue&label=preview&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fnot-decky-alpha.ngni.us%2Fplugins&style=flat-square)](https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki)
[![GitHub package.json version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24.version&style=flat-square&label=local&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/src/branch/main/package.json) [![GitHub package.json version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24.version&style=flat-square&label=local&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/src/branch/main/package.json)
[![Liberapay](https://img.shields.io/liberapay/patrons/NGnius?style=flat-square)](https://liberapay.com/NGnius) [![Liberapay](https://img.shields.io/liberapay/patrons/NGnius?style=flat-square)](https://liberapay.com/NGnius)
[![GitHub](https://img.shields.io/badge/GPL--3.0-orange?style=flat-square&label=license&cacheSeconds=600)](https://github.com/NGnius/PowerTools/blob/main/LICENSE) [![GitHub](https://img.shields.io/badge/GPL--3.0-orange?style=flat-square&label=license&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/blob/main/LICENSE)
[![GitHub package.json dependency version (local)](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24..%5B'decky-frontend-lib'%5D&style=flat-square&label=decky-frontend-lib&cacheSeconds=600)](https://github.com/NGnius/PowerTools/blob/main/pnpm-lock.yaml) [![GitHub package.json dependency version (local)](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24..%5B'decky-frontend-lib'%5D&style=flat-square&label=decky-frontend-lib&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/blob/main/pnpm-lock.yaml)
![plugin_demo](./assets/ui.png) ![plugin_demo](./assets/ui.png)
@ -20,24 +20,38 @@ You will need that installed for this plugin to work.
- Enable & disable CPU threads & SMT - Enable & disable CPU threads & SMT
- Set CPU frequencies - Set CPU frequencies
- Set GPU frequencies and power (fastPPT & slowPPT) - Set GPU frequencies and power (fastPPT & slowPPT)
- Cap battery charge rate (when awake) - Cap battery charge level
- Display supplementary battery info - Display supplementary battery info
- Keep settings between restarts (stored in `~/.config/powertools/<gameId>.json`) - Keep settings between restarts (stored in `~/homebrew/settings/PowerTools/<appId>.ron`)
This plugin is tested on Steam Deck, but is designed to work on other Linux devices as well. Unfortunately I am currently unable to test on other devices. This plugin is tested on Steam Deck LCD/OLED, but is designed to work on other Linux devices as well. Unfortunately I am currently unable to test on most other devices.
## Install ## Install
Please use Decky's [built-in store](https://plugins.deckbrew.xyz/) to install official releases. Please use Decky's [built-in store](https://plugins.deckbrew.xyz/) to install official releases.
If you want to test unstable versions, use [my custom store](https://not-decky-alpha.ngni.us/plugins). If you would like to use an in-development version, feel free to build PowerTools yourself. If you want to test unstable versions, use [my custom store](https://not-decky-alpha.ngni.us/plugins). If you would like to use an in-development version, feel free to build PowerTools yourself.
## Build ## Build/Deploy
0. Requirements: a functioning Rust toolchain for x86_64-unknown-linux-gnu (or -musl), pnpm, and some tech literacy 0. Requirements: a functioning Rust toolchain for x86_64-unknown-linux-gnu (or -musl), pnpm, and some tech literacy
1. In a terminal, navigate to the backend directory of this project and run `./build.sh` 1. In a terminal, navigate to the backend directory of this project and run `./build.sh`
2. In the root of this project, run `pnpm run build` 2. In the root of this project, run `pnpm run build`
3. Transfer the project (especially dist/ and bin/) to a folder in your Steam Deck's `~/homebrew/plugins` directory 3. Transfer the project (especially dist/ and bin/) to a folder in your Steam Deck's `~/homebrew/plugins` directory
4. Restart Decky with `sudo systemctl restart plugin_loader.service`
## License ## License
This is licensed under GNU GPLv3. This is licensed under GNU GPLv3.
## Contributing
All contributions are welcome!
Anything from a comment on an issue to a new feature pull request will be appreciated by PowerTools's crack team of one (NGnius).
### Translations
Adding new languages and keeping existing language files up to date makes PowerTools more accessible to the majority of the world which doesn't speak English. Take a look at [this comment](https://git.ngni.us/NG-SD-Plugins/PowerTools/issues/9#issuecomment-345) (and the rest of that issue) to get started.
### Code
To prevent spam, this server does not allow regular users to create/fork repositories. Please open an issue [here](https://git.ngni.us/sys/website) to request permission. There's no pressure to actually do anything with that permission, though it may be revoked when the server is running low on space.

10
backend/Cargo.lock generated
View file

@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "powertools" name = "powertools"
version = "2.0.0" version = "2.0.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@ -1481,9 +1481,9 @@ dependencies = [
[[package]] [[package]]
name = "smokepatio" name = "smokepatio"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626ef8beee78bebc397d841469fa47bf7e370ddb8b8f3e628e69b03bf968d089" checksum = "3416e8c907d171c4334df3933873c32bff97ca5ad7ae0ee93e6268e04e2041ef"
dependencies = [ dependencies = [
"embedded-io", "embedded-io",
"log", "log",
@ -1540,9 +1540,9 @@ dependencies = [
[[package]] [[package]]
name = "sysfuss" name = "sysfuss"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fa4dd5879b3fd41aff63991a59970cdfeced6f0d5920c5da0937279904d9f45" checksum = "f33bae529511a671b5f2ed4cc46ae0b2ccdf8c03ccf7eebe95a5a886ff7914dc"
[[package]] [[package]]
name = "termcolor" name = "termcolor"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "powertools" name = "powertools"
version = "2.0.0" version = "2.0.3"
edition = "2021" edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Backend (superuser) functionality for PowerTools" description = "Backend (superuser) functionality for PowerTools"
@ -16,7 +16,7 @@ usdpl-back = { version = "0.10.1", features = ["blocking", "decky"] }#, path = "
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
ron = "0.8" ron = "0.8"
sysfuss = { version = "0.2", features = ["derive"] }#,path = "../../sysfs-nav"} sysfuss = { version = "0.3", features = ["derive"] }#,path = "../../sysfs-nav"}
# async # async
tokio = { version = "*", features = ["time"] } tokio = { version = "*", features = ["time"] }
@ -31,7 +31,7 @@ limits_core = { version = "3", path = "./limits_core" }
regex = "1" regex = "1"
# steam deck libs # steam deck libs
smokepatio = { version = "0.1", features = [ "std" ] } smokepatio = { version = "0.2", default-features = false }
libc = "0.2" libc = "0.2"
# online settings # online settings

View file

@ -1,27 +1,34 @@
#!/bin/bash #!/bin/bash
echo "--- Rust version info ---" cd /backend
echo "--- Debug info ---"
rustup --version rustup --version
rustc --version rustc --version
cargo --version cargo --version
mkdir -p out id $USER
stat /backend || exit 1
stat /backend/out || exit 1
echo $(uname -a) > /backend/out/version.txt
echo "--- Building ryzenadj lib ---" echo "--- Building ryzenadj lib ---"
git clone https://github.com/FlyGoat/RyzenAdj ryzenadj git clone https://github.com/FlyGoat/RyzenAdj /tmp/ryzenadj
cd ryzenadj cd /tmp/ryzenadj
git checkout -q 160502771054d31d2f4c2fa46ad42c96336f3a74 git checkout -q v0.14.0
mkdir build && cd build mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release .. cmake -DCMAKE_BUILD_TYPE=Release .. || exit 1
make make || exit 1
mv libryzenadj.so ../../out/libryzenadj.so mv libryzenadj.so /backend/out/libryzenadj.so || exit 1
cd ../.. mv ryzenadj /backend/out/ryzenadj || exit 1
cd /backend
echo "--- Building plugin backend ---" echo "--- Building plugin backend ---"
cargo build --profile docker cargo build --profile docker || exit 1
mkdir -p out mkdir -p out || exit 1
mv target/docker/powertools out/backend mv target/docker/powertools out/backend || exit 1
echo " --- Cleaning up ---" echo " --- Cleaning up ---"
# remove root-owned target folder # remove root-owned target folder

View file

@ -3,11 +3,11 @@
#cargo build --release --target x86_64-unknown-linux-musl #cargo build --release --target x86_64-unknown-linux-musl
#cargo build --target x86_64-unknown-linux-musl #cargo build --target x86_64-unknown-linux-musl
#cross build #cross build
cargo build --release #cargo build --release
#cargo build cargo build
mkdir -p ../bin mkdir -p ../bin
#cp --preserve=mode ./target/x86_64-unknown-linux-musl/release/powertools ../bin/backend #cp --preserve=mode ./target/x86_64-unknown-linux-musl/release/powertools ../bin/backend
#cp --preserve=mode ./target/x86_64-unknown-linux-musl/debug/powertools ../bin/backend #cp --preserve=mode ./target/x86_64-unknown-linux-musl/debug/powertools ../bin/backend
cp --preserve=mode ./target/release/powertools ../bin/backend #cp --preserve=mode ./target/release/powertools ../bin/backend
#cp --preserve=mode ./target/debug/powertools ../bin/backend cp --preserve=mode ./target/debug/powertools ../bin/backend

View file

@ -438,7 +438,7 @@ dependencies = [
[[package]] [[package]]
name = "community_settings_srv" name = "community_settings_srv"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"clap", "clap",
@ -450,6 +450,7 @@ dependencies = [
"serde_json", "serde_json",
"simplelog", "simplelog",
"tokio", "tokio",
"walkdir",
] ]
[[package]] [[package]]
@ -1017,6 +1018,15 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -1319,6 +1329,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "community_settings_srv" name = "community_settings_srv"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -16,6 +16,8 @@ tokio = { version = "1", features = ["full"] }
actix-web = { version = "4.4" } actix-web = { version = "4.4" }
mime = { version = "0.3.17" } mime = { version = "0.3.17" }
walkdir = "2"
# logging # logging
log = "0.4" log = "0.4"
simplelog = "0.12" simplelog = "0.12"

View file

@ -0,0 +1,3 @@
#!/bin/bash
cargo build --release --target aarch64-unknown-linux-musl

View file

@ -60,11 +60,12 @@ fn get_some_settings_by_app_id(steam_app_id: u32, cli: &'static Cli) -> std::io:
let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, steam_app_id); 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()? let mut files: Vec<_> = app_id_folder.read_dir()?
.filter_map(|res| res.ok()) .filter_map(|res| res.ok())
.filter(|f| f.path().extension().map(|ext| ext == file_util::RON_EXTENSION).unwrap_or(false)) .filter(|f| f.path().extension().map(|ext| ext == crate::consts::RON_EXTENSION).unwrap_or(false))
.filter_map(|f| f.metadata().ok().map(|meta| (f, meta))) .filter_map(|f| f.metadata().ok().map(|meta| (f, meta)))
.filter_map(|(f, meta)| meta.created().ok().map(|time| (f, meta, time))) .filter_map(|(f, meta)| meta.modified().ok().map(|time| (f, meta, time)))
.collect(); .collect();
files.sort_by(|(_, _, a_created), (_, _, b_created)| a_created.cmp(b_created)); files.sort_by(|(_, _, a_created), (_, _, b_created)| a_created.cmp(b_created));
let files_len = files.len();
let mut results = Vec::with_capacity(MAX_RESULTS); let mut results = Vec::with_capacity(MAX_RESULTS);
for (_, (f, _, _)) in files.into_iter().enumerate().take_while(|(i, _)| *i < MAX_RESULTS) { for (_, (f, _, _)) in files.into_iter().enumerate().take_while(|(i, _)| *i < MAX_RESULTS) {
@ -72,12 +73,14 @@ fn get_some_settings_by_app_id(steam_app_id: u32, cli: &'static Cli) -> std::io:
let setting = match ron::de::from_reader(reader) { let setting = match ron::de::from_reader(reader) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
log::debug!("Error while reading {}: {}", f.path().display(), e);
let e_msg = format!("{}", e); let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
} }
}; };
results.push(setting); results.push(setting);
} }
log::debug!("Got {} results (from {} files) for {}", results.len(), files_len, app_id_folder.display());
Ok(results) Ok(results)
} }
@ -88,7 +91,8 @@ pub async fn get_setting_by_app_id_handler(
cli: web::Data<&'static Cli>, cli: web::Data<&'static Cli>,
) -> std::io::Result<impl Responder> { ) -> std::io::Result<impl Responder> {
let id: u32 = *id; let id: u32 = *id;
println!("Accept: {}", accept.to_string()); #[cfg(debug_assertions)]
log::debug!("Accept: {}", accept.to_string());
let preferred = accept.preference(); let preferred = accept.preference();
if super::is_mime_type_ron_capable(&preferred) { if super::is_mime_type_ron_capable(&preferred) {
// Send RON // Send RON

View file

@ -67,7 +67,7 @@ pub async fn get_setting_handler(
if super::is_mime_type_ron_capable(&preferred) { if super::is_mime_type_ron_capable(&preferred) {
// Send RON // Send RON
let ron = if id != 0 { let ron = if id != 0 {
let path = file_util::setting_path_by_id(&cli.folder, id, file_util::RON_EXTENSION); let path = file_util::setting_path_by_id(&cli.folder, id, crate::consts::RON_EXTENSION);
if !path.exists() { if !path.exists() {
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("setting id {} does not exist", id))); return Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("setting id {} does not exist", id)));
} }
@ -93,7 +93,7 @@ pub async fn get_setting_handler(
} else { } else {
// Send JSON (fallback) // Send JSON (fallback)
let json = if id != 0 { let json = if id != 0 {
let path = file_util::setting_path_by_id(&cli.folder, id, file_util::JSON_EXTENSION); let path = file_util::setting_path_by_id(&cli.folder, id, crate::consts::JSON_EXTENSION);
// TODO? cache this instead of always loading it from file // TODO? cache this instead of always loading it from file
let reader = std::io::BufReader::new(std::fs::File::open(path)?); let reader = std::io::BufReader::new(std::fs::File::open(path)?);
match serde_json::from_reader(reader) { match serde_json::from_reader(reader) {

View file

@ -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")), 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 next_id = file_util::next_setting_id(&cli.folder);
let parsed_data: community_settings_core::v1::Metadata = if super::is_mime_type_ron_capable(&content_type) { let mut parsed_data: community_settings_core::v1::Metadata = if super::is_mime_type_ron_capable(&content_type) {
// Parse as RON // Parse as RON
match ron::de::from_reader(bytes.as_ref()) { match ron::de::from_reader(bytes.as_ref()) {
Ok(x) => x, Ok(x) => x,
@ -42,16 +42,18 @@ pub async fn save_setting_handler(
} }
} }
}; };
// Override the ID with the one used by this server
parsed_data.id = next_id.to_string();
// TODO validate user and app id // TODO validate user and app id
// Reject blocked users and apps // Reject blocked users and apps
let path_ron = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION); let path_ron = file_util::setting_path_by_id(&cli.folder, next_id, crate::consts::RON_EXTENSION);
let writer = std::io::BufWriter::new(std::fs::File::create(&path_ron)?); let writer = std::io::BufWriter::new(std::fs::File::create(&path_ron)?);
if let Err(e) = ron::ser::to_writer(writer, &parsed_data) { if let Err(e) = ron::ser::to_writer(writer, &parsed_data) {
let e_msg = format!("{}", e); let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
} }
let path_json = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION); let path_json = file_util::setting_path_by_id(&cli.folder, next_id, crate::consts::JSON_EXTENSION);
let writer = std::io::BufWriter::new(std::fs::File::create(&path_json)?); let writer = std::io::BufWriter::new(std::fs::File::create(&path_json)?);
if let Err(e) = serde_json::to_writer(writer, &parsed_data) { if let Err(e) = serde_json::to_writer(writer, &parsed_data) {
let e_msg = format!("{}", e); let e_msg = format!("{}", e);
@ -62,57 +64,32 @@ pub async fn save_setting_handler(
} }
} }
// create symlinks for other ways of looking up these settings files log::debug!("Saved to {}, building symlinks", path_ron.display());
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 to_symlink = file_util::symlinks(&cli.folder, &parsed_data)?;
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 path_ron_canon = path_ron.canonicalize()?;
let user_id_folder = file_util::setting_folder_by_user_id(&cli.folder, parsed_data.steam_user_id); let path_json_canon = path_json.canonicalize()?;
if !user_id_folder.exists() { for ron_link in to_symlink.ron {
std::fs::create_dir(&user_id_folder)?; log::debug!("Symlinking {} -> {}", ron_link.display(), path_ron_canon.display());
}
#[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 #[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_ron_canon, &ron_link)?;
std::os::windows::fs::symlink_file(&path_json, tag_folder.join(&filename_json))?;
} }
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
{ {
std::os::unix::fs::symlink(&path_ron, tag_folder.join(&filename_ron))?; std::os::unix::fs::symlink(&path_ron_canon, &ron_link)?;
std::os::unix::fs::symlink(&path_json, tag_folder.join(&filename_json))?; }
}
for json_link in to_symlink.json {
//log::debug!("Symlinking {} -> {}", json_link.display(), path_json_canon.display());
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(&path_json_canon, json_link)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_json_canon, json_link)?;
} }
} }

View file

@ -14,6 +14,14 @@ pub struct Cli {
/// Log file location /// Log file location
#[arg(short, long, default_value = "/tmp/powertools_community_settings_srv.log")] #[arg(short, long, default_value = "/tmp/powertools_community_settings_srv.log")]
pub log: std::path::PathBuf, pub log: std::path::PathBuf,
/// Perform maintenance tasks
#[arg(long)]
pub fix: bool,
/// Keep up to this many duplicate settings (0 will delete everything!!!)
#[arg(long, default_value_t = 2)]
pub duplicates: usize,
} }
impl Cli { impl Cli {

View file

@ -0,0 +1,8 @@
pub const RON_EXTENSION: &'static str = "ron";
pub const JSON_EXTENSION: &'static str = "json";
pub const SETTING_FOLDER: &'static str = "settings";
pub const ID_FOLDER: &'static str = "by_id";
pub const APP_ID_FOLDER: &'static str = "by_app_id";
pub const USER_ID_FOLDER: &'static str = "by_user_id";
pub const TAG_FOLDER: &'static str = "by_tag";

View file

@ -1,41 +1,10 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::Mutex;
pub const RON_EXTENSION: &'static str = "ron"; use crate::consts::*;
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<u128> = Mutex::new(0); static LAST_SETTING_ID: Mutex<u128> = Mutex::new(0);
pub fn build_folder_layout(root: impl AsRef<Path>) -> 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 { pub fn filename(id: u128, ext: &str) -> String {
format!("{}.{}", id, ext) format!("{}.{}", id, ext)
} }
@ -54,6 +23,23 @@ pub fn setting_folder_by_app_id(root: impl AsRef<Path>, steam_app_id: u32) -> Pa
.join(steam_app_id.to_string()) .join(steam_app_id.to_string())
} }
pub fn setting_tag_folder_by_app_id(root: impl AsRef<Path>, steam_app_id: u32) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.join(steam_app_id.to_string())
.join(TAG_FOLDER)
}
pub fn setting_folder_by_app_id_tag(root: impl AsRef<Path>, steam_app_id: u32, tag: &str) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.join(steam_app_id.to_string())
.join(TAG_FOLDER)
.join(tag)
}
pub fn setting_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) -> PathBuf { pub fn setting_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) -> PathBuf {
root.as_ref() root.as_ref()
.join(SETTING_FOLDER) .join(SETTING_FOLDER)
@ -61,6 +47,23 @@ pub fn setting_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) ->
.join(steam_user_id.to_string()) .join(steam_user_id.to_string())
} }
pub fn setting_tag_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.join(steam_user_id.to_string())
.join(TAG_FOLDER)
}
pub fn setting_folder_by_user_id_tag(root: impl AsRef<Path>, steam_user_id: u64, tag: &str) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.join(steam_user_id.to_string())
.join(TAG_FOLDER)
.join(tag)
}
pub fn setting_folder_by_tag(root: impl AsRef<Path>, tag: &str) -> PathBuf { pub fn setting_folder_by_tag(root: impl AsRef<Path>, tag: &str) -> PathBuf {
root.as_ref() root.as_ref()
.join(SETTING_FOLDER) .join(SETTING_FOLDER)
@ -73,14 +76,83 @@ pub fn next_setting_id(root: impl AsRef<Path>) -> u128 {
let mut last_id = *lock; let mut last_id = *lock;
if last_id == 0 { if last_id == 0 {
// needs init // needs init
last_id = 1;
let mut path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION); let mut path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION);
while path.exists() { while path.exists() {
last_id += 1; last_id += 1;
path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION); path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION);
} }
*lock = last_id; *lock = last_id - 1;
println!("setting id initialized to {}", last_id); log::info!("setting id initialized to {}", last_id);
} }
*lock += 1; *lock += 1;
*lock *lock
} }
pub struct ToSymlink {
pub ron: Vec<PathBuf>,
pub json: Vec<PathBuf>,
}
pub fn symlinks(root: impl AsRef<Path>, meta: &community_settings_core::v1::Metadata) -> std::io::Result<ToSymlink> {
let mut symlink_locations = ToSymlink { ron: Vec::new(), json: Vec::new() };
let filename_ron = filename(meta.get_id(), crate::consts::RON_EXTENSION);
let filename_json = filename(meta.get_id(), crate::consts::JSON_EXTENSION);
// build symlinks to app id folder
let app_id_folder = setting_folder_by_app_id(&root, meta.steam_app_id);
log::debug!("App id folder {}", app_id_folder.display());
if !app_id_folder.exists() {
std::fs::create_dir(&app_id_folder)?;
std::fs::create_dir(setting_tag_folder_by_app_id(&root, meta.steam_app_id))?;
}
symlink_locations.ron.push(app_id_folder.join(&filename_ron));
symlink_locations.json.push(app_id_folder.join(&filename_json));
// create symlinks for user id folder
let user_id_folder = setting_folder_by_user_id(&root, meta.steam_user_id);
if !user_id_folder.exists() {
std::fs::create_dir(&user_id_folder)?;
std::fs::create_dir(setting_tag_folder_by_user_id(&root, meta.steam_user_id))?;
}
symlink_locations.ron.push(user_id_folder.join(&filename_ron));
symlink_locations.json.push(user_id_folder.join(&filename_json));
// create symlinks for each tag
for tag in meta.tags.iter() {
if !str_is_alphanumeric_or_space(&tag){
continue;
}
// create symlinks for general tag folder
let tag_folder = setting_folder_by_tag(&root, tag);
if !tag_folder.exists() {
std::fs::create_dir(&tag_folder)?;
}
symlink_locations.ron.push(tag_folder.join(&filename_ron));
symlink_locations.json.push(tag_folder.join(&filename_json));
// create symlinks for app id tag folder
let app_tag_folder = setting_folder_by_app_id_tag(&root, meta.steam_app_id, tag);
if !app_tag_folder.exists() {
std::fs::create_dir(&app_tag_folder)?;
}
symlink_locations.ron.push(app_tag_folder.join(&filename_ron));
symlink_locations.json.push(app_tag_folder.join(&filename_json));
// create symlinks for user id tag folder
let user_tag_folder = setting_folder_by_user_id_tag(&root, meta.steam_user_id, tag);
if !user_tag_folder.exists() {
std::fs::create_dir(&user_tag_folder)?;
}
symlink_locations.ron.push(user_tag_folder.join(&filename_ron));
symlink_locations.json.push(user_tag_folder.join(&filename_json));
}
Ok(symlink_locations)
}
fn str_is_alphanumeric_or_space(s: &str) -> bool {
let mut result = true;
for ch in s.chars() {
result &= ch.is_ascii_alphanumeric() || ch == ' ';
}
result
}

View file

@ -1,6 +1,9 @@
mod api; mod api;
mod cli; mod cli;
mod consts;
mod file_util; mod file_util;
mod tasks;
mod upgrade;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
@ -27,7 +30,22 @@ async fn main() -> std::io::Result<()> {
// setup // setup
log::debug!("Building folder layout (if not exists) at: {}", &args.folder.display()); log::debug!("Building folder layout (if not exists) at: {}", &args.folder.display());
file_util::build_folder_layout(&args.folder)?; upgrade::build_folder_layout(&args.folder)?;
// fix things
if args.fix {
log::info!("Fixing old symlinks");
upgrade::fix_symlinks(&args.folder)?;
log::info!("Creating missing by_tag folders");
upgrade::make_tag_subfolders(&args.folder)?;
log::info!("Resynchronizing file IDs with file name IDs");
upgrade::sync_ids(&args.folder)?;
log::info!("Rebuilding missing symlinks");
upgrade::rebuild_symlinks(&args.folder)?;
return Ok(())
}
tasks::start_tasks(args.clone());
let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args)); let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args));
HttpServer::new(move || { HttpServer::new(move || {

View file

@ -0,0 +1,16 @@
mod task_runner;
mod symlink_cleanup;
mod user_antispam;
pub fn start_tasks(args: crate::cli::Cli) {
task_runner::TaskRunner::new(
args.clone(),
symlink_cleanup::remove_broken_symlinks,
std::time::Duration::from_secs(1 * 60 /* 1 minute */),
).run();
task_runner::TaskRunner::new(
args.clone(),
user_antispam::remove_similar_user_uploads,
std::time::Duration::from_secs(5 * 60 /* 5 minutes */),
).run();
}

View file

@ -0,0 +1,39 @@
//! Realistically this shouldn't be occur unless there's corruption or a settings files is manually deleted
pub fn remove_broken_symlinks(args: &mut crate::cli::Cli) {
log::info!("Starting broken symlink removal task");
if let Err(e) = enforce_working_symlinks(&args.folder) {
log::error!("Error in broken symlink task: {}", e);
}
log::info!("Completed broken symlink removal task");
}
fn enforce_working_symlinks(root: impl AsRef<std::path::Path>) -> std::io::Result<()> {
for dir_entry in walkdir::WalkDir::new(root.as_ref().join(crate::consts::SETTING_FOLDER)) {
let dir_entry = dir_entry?;
match check_symlink(dir_entry.path()) {
Ok(true) => {},
Ok(false) => {
log::info!("Symlink {} seems broken, removing...", dir_entry.path().display());
if let Err(e) = std::fs::remove_file(dir_entry.path()) {
log::warn!("Failed to delete broken symlink {}: {}", dir_entry.path().display(), e);
}
},
Err(symlink_e) => {
log::info!("Thing {} seems broken, removing... err: {}", dir_entry.path().display(), symlink_e);
if let Err(e) = std::fs::remove_file(dir_entry.path()) {
log::warn!("Failed to delete broken thing {}: {}", dir_entry.path().display(), e);
}
}
}
}
Ok(())
}
fn check_symlink(path: impl AsRef<std::path::Path>) -> std::io::Result<bool> {
if path.as_ref().is_symlink() {
Ok(path.as_ref().read_link().is_ok_and(|link| link.exists()))
} else {
Ok(true)
}
}

View file

@ -0,0 +1,47 @@
use std::thread::{spawn, JoinHandle, sleep};
use std::time::{Duration, Instant};
pub struct TaskRunner<C: Send + 'static, F: FnMut(&mut C) + Send + 'static> {
task: F,
context: C,
period: Option<Duration>,
}
impl <C: Send + 'static, F: FnMut(&mut C) + Send + 'static> TaskRunner<C, F> {
pub fn new(c: C, f: F, period: Duration) -> Self {
Self {
task: f,
context: c,
period: Some(period),
}
}
/*
pub fn new_oneshot(c: C, f: F) -> Self {
Self {
task: f,
context: c,
period: None,
}
}*/
pub fn run(mut self) -> JoinHandle<()> {
if let Some(period) = self.period {
spawn(move || {
let mut pre_task;
let mut after_task;
loop {
pre_task = Instant::now();
(self.task)(&mut self.context);
after_task = Instant::now();
sleep(period - (after_task.duration_since(pre_task)));
}
})
} else {
spawn(move || {
(self.task)(&mut self.context);
})
}
}
}

View file

@ -0,0 +1,114 @@
use crate::file_util;
pub fn remove_similar_user_uploads(args: &mut crate::cli::Cli) {
log::info!("Starting user antispam task");
if let Err(e) = enforce_user_dirs(&args.folder, args.duplicates) {
log::error!("Error in user antispam task: {}", e);
}
log::info!("Completed user antispam task");
}
fn enforce_user_dirs(root: impl AsRef<std::path::Path>, keep_duplicates: usize) -> std::io::Result<()> {
let mut to_remove: Vec<community_settings_core::v1::Metadata> = Vec::new();
let mut seen_names = std::collections::HashMap::<String, Vec<community_settings_core::v1::Metadata>>::new();
for dir_entry in root.as_ref()
.join(crate::consts::SETTING_FOLDER)
.join(crate::consts::USER_ID_FOLDER)
.read_dir()? {
match dir_entry {
Ok(dir_entry) => {
log::info!("Scanning {} for user antispam", dir_entry.path().display());
if dir_entry.file_type()?.is_dir() {
seen_names.clear();
to_remove.clear();
for user_entry in dir_entry.path().read_dir()? {
match user_entry {
Ok(user_entry) => {
let f_path = user_entry.path();
if let Some(ext) = f_path.extension() {
if ext == crate::consts::RON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
match ron::de::from_reader::<_, community_settings_core::v1::Metadata>(reader) {
Ok(x) => {
let sani_name = sanitise_name(&x.name);
if let Some(seen_in_ids) = seen_names.get_mut(&sani_name) {
seen_in_ids.push(x);
} else {
seen_names.insert(sani_name, vec![x]);
}
},
Err(e) => log::debug!("Error while reading {}: {}", f_path.display(), e)
}
}
}
},
Err(e) => log::warn!("Skipping file in {} for user antispam task due to error: {}", dir_entry.path().display(), e),
}
}
for seen in seen_names.values_mut() {
seen.sort_by_key(|meta| meta.get_id()); // sort lowest to highest
// keep the highest id (i.e. latest uploaded) settings
for _ in 0..keep_duplicates {
seen.pop();
}
to_remove.append(seen);
}
log::info!("Found {} spammy entries in {}", to_remove.len(), dir_entry.path().display());
// remove settings (and related symlinks) from the filesystem
for meta in to_remove.iter() {
let filename_ron = file_util::filename(meta.get_id(), crate::consts::RON_EXTENSION);
let filename_json = file_util::filename(meta.get_id(), crate::consts::JSON_EXTENSION);
// delete tag symlinks
for tag in meta.tags.iter() {
let app_id_tag_folder = file_util::setting_folder_by_app_id_tag(root.as_ref(), meta.steam_app_id, tag);
let user_id_tag_folder = file_util::setting_folder_by_user_id_tag(root.as_ref(), meta.steam_user_id, tag);
let tag_folder = file_util::setting_folder_by_tag(root.as_ref(), tag);
let paths = [
app_id_tag_folder.join(&filename_json),
app_id_tag_folder.join(&filename_ron),
user_id_tag_folder.join(&filename_json),
user_id_tag_folder.join(&filename_ron),
tag_folder.join(&filename_json),
tag_folder.join(&filename_ron),
];
for path in paths {
if path.exists() {
if let Err(e) = std::fs::remove_file(&path) {
log::warn!("Failed to delete {}: {}", path.display(), e);
}
}
}
}
// delete first-order symlinks and finally the actual files
let app_id_folder = file_util::setting_folder_by_app_id(root.as_ref(), meta.steam_app_id);
let user_id_folder = file_util::setting_folder_by_user_id(root.as_ref(), meta.steam_user_id);
let paths = [
app_id_folder.join(&filename_json),
app_id_folder.join(&filename_ron),
user_id_folder.join(&filename_json),
user_id_folder.join(&filename_ron),
file_util::setting_path_by_id(root.as_ref(), meta.get_id(), crate::consts::JSON_EXTENSION),
file_util::setting_path_by_id(root.as_ref(), meta.get_id(), crate::consts::RON_EXTENSION),
];
for path in paths {
if path.exists() {
if let Err(e) = std::fs::remove_file(&path) {
log::warn!("Failed to delete {}: {}", path.display(), e);
}
}
}
}
} else {
log::info!("Encountered non-folder path in user dir: {}", dir_entry.path().display());
}
},
Err(e) => log::warn!("Skipping file for user antispam task due to error: {}", e),
}
}
Ok(())
}
fn sanitise_name(name: &str) -> String {
name.trim().to_lowercase()
}

View file

@ -0,0 +1,217 @@
use std::path::Path;
use crate::consts::*;
pub fn build_folder_layout(root: impl AsRef<Path>) -> 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 rebuild_symlinks(root: impl AsRef<Path>) -> std::io::Result<()> {
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_file() {
let f_path = dir_entry.path();
if let Some(ext) = f_path.extension() {
if ext == RON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
let setting: community_settings_core::v1::Metadata = match ron::de::from_reader(reader) {
Ok(x) => x,
Err(e) => {
log::debug!("Error while reading {}: {}", f_path.display(), e);
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
};
let to_symlink = crate::file_util::symlinks(&root, &setting)?;
let path_ron_canon = f_path.canonicalize()?;
let path_json_canon = crate::file_util::setting_path_by_id(&root, setting.get_id(), JSON_EXTENSION).canonicalize()?;
for ron_link in to_symlink.ron {
if ron_link.exists() { continue; }
log::info!("Rebuilding symlink {} -> {}", ron_link.display(), path_ron_canon.display());
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(&path_ron_canon, &ron_link)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_ron_canon, &ron_link)?;
}
}
for json_link in to_symlink.json {
if json_link.exists() { continue; }
log::info!("Rebuilding symlink {} -> {}", json_link.display(), path_json_canon.display());
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(&path_json_canon, json_link)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_json_canon, json_link)?;
}
}
}
}
}
}
Ok(())
}
pub fn fix_symlinks(root: impl AsRef<Path>) -> std::io::Result<()> {
log::info!("root setttings folder: {} aka {} (absolute)", root.as_ref().display(), root.as_ref().canonicalize()?.display());
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_dir() {
make_symlinks_absolute_in_dir(root.as_ref(), dir_entry.path())?;
}
}
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_dir() {
make_symlinks_absolute_in_dir(root.as_ref(), dir_entry.path())?;
}
}
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(TAG_FOLDER).read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_dir() {
make_symlinks_absolute_in_dir(root.as_ref(), dir_entry.path())?;
}
}
Ok(())
}
fn make_symlinks_absolute_in_dir(root: impl AsRef<Path>, dir: impl AsRef<Path>) -> std::io::Result<()> {
let abs_root = root.as_ref().canonicalize()?;
assert!(abs_root.is_absolute());
for dir_entry in dir.as_ref()
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_symlink() {
let path = dir_entry.path();
let link_path = path.read_link()?;
if !link_path.is_absolute() {
let new_link = abs_root.join(
link_path.strip_prefix(&root).expect("Symlinked path does not begin with root settings folder")
);
log::info!("Fixing {} -> {} to -> {}", path.display(), link_path.display(), new_link.display());
std::fs::remove_file(&path)?;
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(new_link, &path)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(new_link, &path)?;
}
}else {
log::info!("Found already-absolute symlink {} -> {}", path.display(), link_path.display());
}
} else {
log::info!("Found non-symlink {}: {:?}", dir_entry.path().display(), dir_entry.file_type()?);
}
}
Ok(())
}
pub fn sync_ids(root: impl AsRef<Path>) -> std::io::Result<()> {
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
let f_path = dir_entry.path();
if let Some(ext) = f_path.extension() {
let id = f_path.file_stem().map(|os| os.to_string_lossy().to_string()).unwrap();
if ext == RON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
let mut setting: community_settings_core::v1::Metadata = match ron::de::from_reader(reader) {
Ok(x) => x,
Err(e) => {
log::debug!("Error while reading {}: {}", f_path.display(), e);
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
};
if setting.id != id {
setting.id = id;
ron::ser::to_writer(std::fs::File::create(&f_path)?, &setting).unwrap();
}
} else if ext == JSON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
let mut setting: community_settings_core::v1::Metadata = match serde_json::from_reader(reader) {
Ok(x) => x,
Err(e) => {
log::debug!("Error while reading {}: {}", f_path.display(), e);
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
};
if setting.id != id {
setting.id = id;
serde_json::to_writer(std::fs::File::create(&f_path)?, &setting).unwrap();
}
}
}
}
Ok(())
}
pub fn make_tag_subfolders(root: impl AsRef<Path>) -> std::io::Result<()> {
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.metadata()?.is_dir() {
let tag_folder = dir_entry.path().join(TAG_FOLDER);
if !tag_folder.exists() {
std::fs::create_dir(&tag_folder)?;
}
}
}
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.metadata()?.is_dir() {
let tag_folder = dir_entry.path().join(TAG_FOLDER);
if !tag_folder.exists() {
std::fs::create_dir(&tag_folder)?;
}
}
}
// TODO populate folders
Ok(())
}

View file

@ -5,4 +5,4 @@ echo "Container's IP address: `awk 'END{print $1}' /etc/hosts`"
cd /backend cd /backend
sudo bash build-docker.sh bash build-docker.sh

View file

@ -34,8 +34,8 @@ impl GenericBatteryLimit {
fn default_steam_deck() -> Self { fn default_steam_deck() -> Self {
Self { Self {
charge_rate: Some(RangeLimit { charge_rate: Some(RangeLimit {
min: Some(250), min: Some(0),
max: Some(2500), max: Some(100),
}), }),
charge_modes: vec![ charge_modes: vec![
"normal".to_owned(), "normal".to_owned(),

View file

@ -612,8 +612,8 @@
"provider": "GabeBoy", "provider": "GabeBoy",
"limits": { "limits": {
"charge_rate": { "charge_rate": {
"min": 250, "min": 0,
"max": 2500 "max": 100
}, },
"charge_modes": [ "charge_modes": [
"normal", "normal",
@ -878,8 +878,8 @@
"provider": "GabeBoySP", "provider": "GabeBoySP",
"limits": { "limits": {
"charge_rate": { "charge_rate": {
"min": 250, "min": 0,
"max": 2500 "max": 100
}, },
"charge_modes": [ "charge_modes": [
"normal", "normal",

View file

@ -32,6 +32,30 @@ pub enum ApiMessage {
UploadCurrentVariant(String, String), // SteamID, Steam username UploadCurrentVariant(String, String), // SteamID, Steam username
} }
impl core::fmt::Display for ApiMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Battery(x) => write!(f, "Battery;{}", x),
Self::Cpu(x) => write!(f, "Cpu;{}", x),
Self::Gpu(x) => write!(f, "Gpu;{}", x),
Self::General(x) => write!(f, "General;{}", x),
Self::OnResume => write!(f, "OnResume"),
Self::OnPluggedIn => write!(f, "OnPluggedIn"),
Self::OnUnplugged => write!(f, "OnUnplugged"),
Self::OnChargeChange(x) => write!(f, "OnChargeChange({:?})", x),
Self::PowerVibeCheck => write!(f, "PowerVibeCheck"),
Self::WaitForEmptyQueue(_) => write!(f, "WaitForEmptyQueue"),
Self::LoadSettings(path, name, variant, variant_name) => write!(f, "LoadSettings({}, {}, {}, {})", path, name, variant, variant_name),
Self::LoadVariant(variant, variant_name) => write!(f, "LoadVariant({}, {})", variant, variant_name),
Self::LoadMainSettings => write!(f, "LoadMainSettings"),
Self::LoadSystemSettings => write!(f, "LoadSystemSettings"),
Self::GetLimits(_) => write!(f, "GetLimits"),
Self::GetProvider(s, _) => write!(f, "GetProvider({})", s),
Self::UploadCurrentVariant(id, user) => write!(f, "UploadCurrentVariant(id: {}, user: {})", id, user),
}
}
}
pub enum BatteryMessage { pub enum BatteryMessage {
SetChargeRate(Option<u64>), SetChargeRate(Option<u64>),
GetChargeRate(Callback<Option<u64>>), GetChargeRate(Callback<Option<u64>>),
@ -46,6 +70,24 @@ pub enum BatteryMessage {
GetChargeLimit(Callback<Option<f64>>), GetChargeLimit(Callback<Option<f64>>),
} }
impl core::fmt::Display for BatteryMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetChargeRate(x) => write!(f, "SetChargeRate({:?})", x),
Self::GetChargeRate(_) => write!(f, "GetChargeRate"),
Self::SetChargeMode(x) => write!(f, "SetChargeMode({:?})", x),
Self::GetChargeMode(_) => write!(f, "GetChargeMode"),
Self::ReadChargeFull(_) => write!(f, "ReadChargeFull"),
Self::ReadChargeNow(_) => write!(f, "ReadChargeNow"),
Self::ReadChargeDesign(_) => write!(f, "ReadChargeDesign"),
Self::ReadCurrentNow(_) => write!(f, "ReadCurrentNow"),
Self::ReadChargePower(_) => write!(f, "ReadChargePower"),
Self::SetChargeLimit(x) => write!(f, "SetChargeLimit({:?})", x),
Self::GetChargeLimit(_) => write!(f, "GetChargeLimit"),
}
}
}
impl BatteryMessage { impl BatteryMessage {
fn process(self, settings: &mut dyn TBattery) -> bool { fn process(self, settings: &mut dyn TBattery) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -87,6 +129,23 @@ pub enum CpuMessage {
GetCpusGovernor(Callback<Vec<String>>), GetCpusGovernor(Callback<Vec<String>>),
} }
impl core::fmt::Display for CpuMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetCpuOnline(i, x) => write!(f, "SetCpuOnline({}, {})", i, x),
Self::SetCpusOnline(x) => write!(f, "SetCpusOnline({:?})", x),
Self::SetSmt(x, _) => write!(f, "SetChargeMode({})", x),
Self::GetSmt(_) => write!(f, "GetSmt"),
Self::GetCpusOnline(_) => write!(f, "GetCpusOnline"),
Self::SetClockLimits(x, y) => write!(f, "SetClockLimits({}, {:?})", x, y),
Self::GetClockLimits(x, _) => write!(f, "GetClockLimits({})", x),
Self::SetCpuGovernor(i, x) => write!(f, "SetCpuGovernor({}, {})", i, x),
Self::SetCpusGovernor(x) => write!(f, "SetCpusGovernor({:?})", x),
Self::GetCpusGovernor(_) => write!(f, "GetCpusGovernor"),
}
}
}
impl CpuMessage { impl CpuMessage {
fn process(self, settings: &mut dyn TCpus) -> bool { fn process(self, settings: &mut dyn TCpus) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -206,6 +265,19 @@ pub enum GpuMessage {
GetMemoryClock(Callback<Option<u64>>), GetMemoryClock(Callback<Option<u64>>),
} }
impl core::fmt::Display for GpuMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetPpt(x, y) => write!(f, "SetPpt(fast {:?}, slow {:?})", x, y),
Self::GetPpt(_) => write!(f, "GetPpt"),
Self::SetClockLimits(x) => write!(f, "SetClockLimits({:?})", x),
Self::GetClockLimits(_) => write!(f, "GetClockLimits"),
Self::SetMemoryClock(x) => write!(f, "SetMemoryClock({:?})", x),
Self::GetMemoryClock(_) => write!(f, "GetMemoryClock"),
}
}
}
impl GpuMessage { impl GpuMessage {
fn process(self, settings: &mut dyn TGpu) -> bool { fn process(self, settings: &mut dyn TGpu) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -242,6 +314,21 @@ pub enum GeneralMessage {
ApplyNow, ApplyNow,
} }
impl core::fmt::Display for GeneralMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetPersistent(x) => write!(f, "SetPersistent({})", x),
Self::GetPersistent(_) => write!(f, "GetPersistent"),
Self::GetCurrentProfileName(_) => write!(f, "GetCurrentProfileName"),
Self::GetPath(_) => write!(f, "GetPath"),
Self::GetCurrentVariant(_) => write!(f, "GetCurrentVariant"),
Self::GetAllVariants(_) => write!(f, "GetAllVariants"),
Self::AddVariant(variant, _) => write!(f, "AddVariant(name: `{}` [...])", variant.name),
Self::ApplyNow => write!(f, "ApplyNow"),
}
}
}
impl GeneralMessage { impl GeneralMessage {
fn process(self, settings: &mut dyn TGeneral) -> bool { fn process(self, settings: &mut dyn TGeneral) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -285,20 +372,31 @@ fn print_errors(call_name: &str, errors: Vec<crate::settings::SettingError>) {
log::error!("Settings {}() err:\n{}", call_name, err_list); log::error!("Settings {}() err:\n{}", call_name, err_list);
} }
fn print_messages(msgs: &Vec<String>) {
let mut log_msg = String::new();
for msg in msgs.iter() {
//use core::fmt::Write;
write!(log_msg, "{}, ", msg).unwrap();
}
log::info!("Processed messages: [{}]", log_msg);
}
impl ApiMessageHandler { impl ApiMessageHandler {
pub fn process_forever(&mut self, settings: &mut Settings) { pub fn process_forever(&mut self, settings: &mut Settings) {
crate::utility::ioperm_power_ec(); crate::utility::ioperm_power_ec();
//let mut dirty_echo = true; // set everything twice, to make sure PowerTools wins on race conditions let mut dirty_echo = true; // set everything twice, to make sure PowerTools wins on race conditions
while let Ok(msg) = self.intake.recv() { while let Ok(msg) = self.intake.recv() {
let mut messages = vec![msg.to_string()]; // keep messages for logging
let mut dirty = self.process(settings, msg); let mut dirty = self.process(settings, msg);
while let Ok(msg) = self.intake.try_recv() { while let Ok(msg) = self.intake.try_recv() {
messages.push(msg.to_string());
dirty |= self.process(settings, msg); dirty |= self.process(settings, msg);
} }
if dirty if dirty
/*|| dirty_echo */ || dirty_echo
{ {
//dirty_echo = dirty; // echo only once dirty_echo = dirty; // echo only once
print_messages(&messages);
// run on_set // run on_set
if let Err(e) = settings.on_set() { if let Err(e) = settings.on_set() {
print_errors("on_set", e); print_errors("on_set", e);

View file

@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use sysfuss::capability::attributes; use sysfuss::capability::attributes;
use sysfuss::{ use sysfuss::{
HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, PowerSupplyAttribute, HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, PowerSupplyAttribute,
PowerSupplyPath, SysAttribute, SysEntity, SysEntityAttributesExt, PowerSupplyPath, SysEntity, SysEntityAttributesExt, SysAttributeExt,
}; };
use limits_core::json_v2::GenericBatteryLimit; use limits_core::json_v2::GenericBatteryLimit;
@ -230,12 +230,12 @@ const HWMON_NEEDS: &[HwMonAttribute] = &[
]; ];
const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute =
HwMonAttribute::custom("maximum_battery_charge_rate"); HwMonAttribute::custom("max_battery_charge_rate");
const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute = const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute =
HwMonAttribute::custom("max_battery_charge_level"); HwMonAttribute::custom("max_battery_charge_level");
const MAX_CHARGE_RATE: u64 = 2500; const MAX_CHARGE_RATE: u64 = 100;
const MIN_CHARGE_RATE: u64 = 250; const MIN_CHARGE_RATE: u64 = 0;
impl Battery { impl Battery {
fn find_battery_sysfs(root: Option<impl AsRef<std::path::Path>>) -> PowerSupplyPath { fn find_battery_sysfs(root: Option<impl AsRef<std::path::Path>>) -> PowerSupplyPath {
@ -325,7 +325,7 @@ impl Battery {
self.state.charge_rate_set = true; self.state.charge_rate_set = true;
let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon); let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon);
self.sysfs_hwmon self.sysfs_hwmon
.set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate) .set(MAX_BATTERY_CHARGE_RATE_ATTR, format!("{}\n", charge_rate))
.map_err(|e| SettingError { .map_err(|e| SettingError {
msg: format!("Failed to write to `{}`: {}", path.display(), e), msg: format!("Failed to write to `{}`: {}", path.display(), e),
setting: crate::settings::SettingVariant::Battery, setting: crate::settings::SettingVariant::Battery,
@ -336,10 +336,10 @@ impl Battery {
self.sysfs_hwmon self.sysfs_hwmon
.set( .set(
MAX_BATTERY_CHARGE_RATE_ATTR, MAX_BATTERY_CHARGE_RATE_ATTR,
self.limits format!("{}\n", self.limits
.charge_rate .charge_rate
.and_then(|lim| lim.max) .and_then(|lim| lim.max)
.unwrap_or(2500), .unwrap_or(100)),
) )
.map_err(|e| SettingError { .map_err(|e| SettingError {
msg: format!("Failed to write to `{}`: {}", path.display(), e), msg: format!("Failed to write to `{}`: {}", path.display(), e),

View file

@ -1,8 +1,7 @@
use std::convert::Into; use std::convert::Into;
use sysfuss::{ use sysfuss::{
capability::attributes, BasicEntityPath, HwMonPath, SysAttribute, SysEntity, capability::attributes, BasicEntityPath, HwMonPath, SysEntity, SysEntityAttributesExt, SysAttributeExt,
SysEntityAttributes, SysEntityAttributesExt,
}; };
use limits_core::json_v2::GenericGpuLimit; use limits_core::json_v2::GenericGpuLimit;
@ -151,7 +150,7 @@ impl Gpu {
if let super::Model::OLED = self.variant { if let super::Model::OLED = self.variant {
if let Ok(f) = self if let Ok(f) = self
.sysfs_card .sysfs_card
.read_value(GPU_CLOCK_READOUT_ATTRIBUTE.to_owned()) .read_value(&GPU_CLOCK_READOUT_ATTRIBUTE.to_owned())
{ {
let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f)); let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f));
return options return options
@ -192,7 +191,7 @@ impl Gpu {
fn quantize_memory_clock(&self, clock: u64) -> u64 { fn quantize_memory_clock(&self, clock: u64) -> u64 {
if let Ok(f) = self if let Ok(f) = self
.sysfs_card .sysfs_card
.read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) .read_value(&GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned())
{ {
let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f)); let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f));
// round (and find) nearest valid clock step // round (and find) nearest valid clock step
@ -238,7 +237,7 @@ impl Gpu {
{ {
let options_count = self let options_count = self
.sysfs_card .sysfs_card
.read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) .read_value(&GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned())
.map(|b| parse_pp_dpm_fclk(&String::from_utf8_lossy(&b)).len()) .map(|b| parse_pp_dpm_fclk(&String::from_utf8_lossy(&b)).len())
.unwrap_or_else(|_| if is_oled { 4 } else { 2 }); .unwrap_or_else(|_| if is_oled { 4 } else { 2 });
let modifier = (options_count - 1) as u64; let modifier = (options_count - 1) as u64;

View file

@ -5,7 +5,7 @@
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use sysfuss::{BasicEntityPath, SysAttribute, SysEntityAttributesExt}; use sysfuss::{BasicEntityPath, SysEntityAttributesExt, SysAttributeExt};
use crate::settings::SettingError; use crate::settings::SettingError;

View file

@ -1,9 +1,13 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::sync::Mutex;
pub const JUPITER_HWMON_NAME: &'static str = "jupiter"; pub const JUPITER_HWMON_NAME: &'static str = "jupiter";
pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon"; pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon";
pub const GPU_HWMON_NAME: &'static str = "amdgpu"; pub const GPU_HWMON_NAME: &'static str = "amdgpu";
pub static THING_EC: Mutex<smokepatio::ec::unnamed_power::UnnamedPowerEC> = Mutex::new(smokepatio::ec::unnamed_power::UnnamedPowerEC::new());
pub fn range_min_or_fallback<I: Copy>( pub fn range_min_or_fallback<I: Copy>(
range: &Option<limits_core::json_v2::RangeLimit<I>>, range: &Option<limits_core::json_v2::RangeLimit<I>>,
fallback: I, fallback: I,
@ -25,23 +29,25 @@ pub fn card_also_has(card: &dyn sysfuss::SysEntity, extensions: &'static [&'stat
} }
const THINGS: &[u8] = &[ const THINGS: &[u8] = &[
0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1,
1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0,
0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
]; ];
const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(200); const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(250);
pub fn flash_led() { pub fn flash_led() {
use smokepatio::ec::ControllerSet; use smokepatio::ec::ControllerSet;
let mut ec = smokepatio::ec::unnamed_power::UnnamedPowerEC::new();
let mut ec = THING_EC.lock().unwrap();
for &code in THINGS { for &code in THINGS {
let on = code != 0; let on = code != 0;
let colour = if on { let colour = if on {
smokepatio::ec::unnamed_power::StaticColour::Red smokepatio::ec::unnamed_power::StaticColour::Red
} else { } else {
smokepatio::ec::unnamed_power::StaticColour::Off smokepatio::ec::unnamed_power::StaticColour::Disabled
}; };
if let Err(e) = ec.set(colour) { if let Err(e) = ec.set(colour) {
log::error!("Thing err: {}", e); log::error!("Thing err: {}", e);

View file

@ -69,7 +69,8 @@ pub fn chown_settings_dir() -> std::io::Result<()> {
.parse() .parse()
.unwrap_or(1000); .unwrap_or(1000);
log::info!( log::info!(
"chmod/chown ~/.config/powertools for user `{}` ({})", "chmod/chown {} for user `{}` ({})",
dir.display(),
deck_user, deck_user,
uid uid
); );

View file

@ -1,6 +1,6 @@
{ {
"name": "PowerTools", "name": "PowerTools",
"version": "2.0.0", "version": "2.0.3",
"description": "Power tweaks for power users", "description": "Power tweaks for power users",
"scripts": { "scripts": {
"build": "shx rm -rf dist && rollup -c", "build": "shx rm -rf dist && rollup -c",

View file

@ -85,7 +85,7 @@ export class Battery extends Component<backend.IdcProps> {
}} }}
/> />
{ get_value(CHARGE_RATE_BATT) != null && <SliderField { get_value(CHARGE_RATE_BATT) != null && <SliderField
label={tr("Maximum (mA)")} label={tr("Maximum (%)")}
value={get_value(CHARGE_RATE_BATT)} value={get_value(CHARGE_RATE_BATT)}
max={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.max} max={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.max}
min={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.min} min={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.min}

View file

@ -364,6 +364,7 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
(ok: boolean) => { (ok: boolean) => {
backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok);
reload(); reload();
isVariantLoading = false;
backend.resolve(backend.waitForComplete(), (_) => { backend.resolve(backend.waitForComplete(), (_) => {
backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant"); backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant");
tryNotifyProfileChange(); tryNotifyProfileChange();

View file

@ -40,18 +40,22 @@ export class StoreResultsPage extends Component<{onNewVariant: () => void}> {
{ tr("No results") /* TODO translate */ } { tr("No results") /* TODO translate */ }
</Focusable>); </Focusable>);
} else { } else {
// TODO return (
return (<Focusable <div style={{
style={{ marginTop: "40px",
display: "flex", marginBottom: "40px",
flexWrap: "wrap", overflowY: "scroll",
justifyContent: "center", }}>
rowGap: "5px", <Focusable
columnGap: "5px", style={{
maxWidth: "100%", display: "flex",
margin: "2em 0.5em", flexWrap: "wrap",
}} justifyContent: "center",
> rowGap: "0.5em",
columnGap: "0.5em",
maxWidth: "100%",
}}
>
{ {
storeItems.map((meta: backend.StoreMetadata) => (<PanelSectionRow> storeItems.map((meta: backend.StoreMetadata) => (<PanelSectionRow>
<Focusable style={{ <Focusable style={{
@ -149,7 +153,8 @@ export class StoreResultsPage extends Component<{onNewVariant: () => void}> {
</Focusable> </Focusable>
</PanelSectionRow>)) </PanelSectionRow>))
} }
</Focusable>); </Focusable>
</div>);
} }
} else { } else {

BIN
translations/en-US.mo Normal file

Binary file not shown.

16
translations/en-US.po Normal file
View file

@ -0,0 +1,16 @@
# TEMPLATE TITLE.
# Copyright (C) 2024 NGnius
# This file is distributed under the same license as the PowerTools package.
# NGnius (Graham) <ngniusness@gmail.com>, 2024.
msgid ""
msgstr ""
"Project-Id-Version: v1.1\n"
"Report-Msgid-Bugs-To: https://git.ngni.us/NG-SD-Plugins/PowerTools/PowerTools/issues\n"
"POT-Creation-Date: 2023-01-09 19:52-0500\n"
"PO-Revision-Date: 2024-05-07 18:42-0500\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en-US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"

View file

@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: v1.1\n" "Project-Id-Version: v1.1\n"
"Report-Msgid-Bugs-To: https://github.com/NGnius/PowerTools/issues\n" "Report-Msgid-Bugs-To: https://git.ngni.us/NG-SD-Plugins/PowerTools/issues\n"
"POT-Creation-Date: 2023-01-09 19:52-0500\n" "POT-Creation-Date: 2023-01-09 19:52-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"