commit 32d25533808f294e24f3aa1111295c927e4f9e1b Author: robonen Date: Sun Aug 2 13:47:00 2026 +0700 feat: initial commit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6886f92 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: windows-latest + permissions: + contents: write # publish release on tag + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Resolve version + id: meta + shell: pwsh + run: | + if ($env:GITHUB_REF -like 'refs/tags/v*') { + $version = $env:GITHUB_REF_NAME -replace '^v', '' + } else { + $version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"' | + Select-Object -First 1).Matches[0].Groups[1].Value + } + "version=$version" >> $env:GITHUB_OUTPUT + Write-Host "version: $version" + + - name: Build + run: cargo build --release --locked + + # Install unconditionally: the script uses ArchitecturesAllowed=x64compatible, + # which needs Inno Setup 6.3+. The version in the runner image is not guaranteed. + - name: Install Inno Setup + shell: pwsh + run: choco install innosetup --no-progress -y + + - name: Build installer + shell: pwsh + run: | + $iscc = Get-ChildItem -ErrorAction SilentlyContinue -Path @( + "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", + "${env:ProgramFiles}\Inno Setup 6\ISCC.exe" + ) | Select-Object -First 1 + if (-not $iscc) { throw "ISCC.exe not found" } + + New-Item -ItemType Directory -Force -Path dist | Out-Null + Copy-Item target\release\syshelper.exe dist\ + + & $iscc.FullName /DAppVersion=${{ steps.meta.outputs.version }} installer\setup.iss + if ($LASTEXITCODE -ne 0) { throw "ISCC exited with $LASTEXITCODE" } + + Get-ChildItem dist | Format-Table Name, Length + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: syshelper-${{ steps.meta.outputs.version }}-windows-x64 + path: dist/*.exe + if-no-files-found: error + retention-days: 30 + + - name: Publish release + if: startsWith(github.ref, 'refs/tags/v') + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $files = (Get-ChildItem dist\*.exe).FullName + gh release create $env:GITHUB_REF_NAME $files --generate-notes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1c26b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +/dist + +# локальное состояние, а не исходники +rules.json + +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a0e6ee8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1747 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syshelper" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "rand 0.8.7", + "reqwest", + "semver", + "serde", + "serde_json", + "sysinfo", + "tokio", +] + +[[package]] +name = "sysinfo" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e518737 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "syshelper" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sysinfo = "0.30" +rand = "0.8" +chrono = "0.4" +semver = "1" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +strip = true +panic = "abort" \ No newline at end of file diff --git a/installer/setup.iss b/installer/setup.iss new file mode 100644 index 0000000..b956088 --- /dev/null +++ b/installer/setup.iss @@ -0,0 +1,99 @@ +; Установщик «System Helper» для Windows x64. +; Сборка: ISCC.exe /DAppVersion=0.1.0 installer\setup.iss +; Результат кладётся в dist\syshelper-<версия>-setup.exe + +#ifndef AppVersion + #define AppVersion "0.0.0-dev" +#endif + +#ifndef ExeSource + #define ExeSource "..\target\release\syshelper.exe" +#endif + +#define AppName "System Helper" +#define AppExeName "syshelper.exe" +#define AppUrl "http://127.0.0.1:8787" +; Имя задачи автообновления — должно совпадать с TASK_NAME в src/update.rs. +#define TaskName "SysHelperUpdate" + +[Setup] +; AppId определяет программу при обновлении и удалении — не менять между версиями. +AppId={{B3F1C0A2-7E5D-4A19-9C3B-2D8E6F4A1B57} +AppName={#AppName} +AppVersion={#AppVersion} +AppVerName={#AppName} {#AppVersion} +DefaultDirName={autopf}\SysHelper +DefaultGroupName={#AppName} +DisableProgramGroupPage=yes +UninstallDisplayName={#AppName} +UninstallDisplayIcon={app}\{#AppExeName} + +OutputDir=..\dist +OutputBaseFilename=syshelper-{#AppVersion}-setup +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern + +; Ставим в 64-битный Program Files; программа собрана только под x64. +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible + +; Program Files и автозапуск через HKLM требуют администратора. +PrivilegesRequired=admin +CloseApplications=yes +RestartApplications=no + +[Languages] +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "autostart"; \ + Description: "Запускать при входе в систему"; \ + GroupDescription: "Дополнительно:" +Name: "desktopicon"; \ + Description: "{cm:CreateDesktopIcon}"; \ + GroupDescription: "Дополнительно:"; \ + Flags: unchecked + +[Dirs] +; Сюда работающая от пользователя программа кладёт скачанный установщик, а +; задача Планировщика (от SYSTEM) забирает его. Нужен доступ на запись обоим. +Name: "{commonappdata}\SysHelper\update"; Permissions: users-modify + +[Files] +Source: "{#ExeSource}"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}" +Name: "{group}\Веб-панель"; Filename: "{#AppUrl}" +Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon + +[Registry] +; Автозапуск для всех пользователей. Запускается без повышения прав — +; этого хватает, чтобы завершать процессы того же пользователя. +Root: HKLM; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \ + ValueType: string; ValueName: "SysHelper"; \ + ValueData: """{app}\{#AppExeName}"""; \ + Flags: uninsdeletevalue; Tasks: autostart + +[Run] +; Задача автообновления: программа скачивает свежий установщик и просит +; Планировщик запустить его от SYSTEM с наивысшими правами — так обновление +; ставится тихо, без запроса UAC. Своего расписания у задачи нет (SC ONCE в +; прошлом), её лишь запускают по требованию из программы. +Filename: "{sys}\schtasks.exe"; \ + Parameters: "/Create /F /RU SYSTEM /RL HIGHEST /SC ONCE /SD 01/01/2000 /ST 00:00 /TN ""{#TaskName}"" /TR ""{commonappdata}\SysHelper\update\setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /NOCANCEL"""; \ + Flags: runhidden; StatusMsg: "Настройка автообновления..." +Filename: "{app}\{#AppExeName}"; \ + Description: "Запустить {#AppName}"; \ + Flags: nowait postinstall skipifsilent +Filename: "{#AppUrl}"; \ + Description: "Открыть веб-панель"; \ + Flags: shellexec nowait postinstall skipifsilent + +[UninstallRun] +Filename: "{sys}\schtasks.exe"; Parameters: "/Delete /F /TN ""{#TaskName}"""; \ + Flags: runhidden; RunOnceId: "DelUpdateTask" +Filename: "{sys}\taskkill.exe"; Parameters: "/f /im {#AppExeName}"; \ + Flags: runhidden; RunOnceId: "StopApp" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2ece211 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,475 @@ +//! Родительский контроль для Windows. +//! Веб-интерфейс и API на http://127.0.0.1:8787 + +use axum::{ + extract::State, + http::header, + response::{Html, IntoResponse}, + routing::{get, post}, + Json, Router, +}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::{ + collections::HashSet, + path::PathBuf, + sync::{Arc, OnceLock}, + time::Duration, +}; +use sysinfo::{Pid, System}; +use tokio::sync::Mutex; + +mod update; + +const SCAN_INTERVAL: Duration = Duration::from_secs(2); +const LOG_LIMIT: usize = 500; + +// фронт вшивается в exe на этапе компиляции +const INDEX_HTML: &str = include_str!("../static/index.html"); +const APP_JS: &str = include_str!("../static/app.js"); + +/// Нельзя убивать никогда — иначе BSOD или сломанный рабочий стол. +const PROTECTED: &[&str] = &[ + "system", "system idle process", "registry", "memory compression", + "smss.exe", "csrss.exe", "wininit.exe", "winlogon.exe", "services.exe", + "lsass.exe", "lsaiso.exe", "svchost.exe", "dwm.exe", "explorer.exe", + "fontdrvhost.exe", "sihost.exe", "ctfmon.exe", "taskhostw.exe", + "runtimebroker.exe", "conhost.exe", "spoolsv.exe", "audiodg.exe", + "searchindexer.exe", "searchhost.exe", "shellexperiencehost.exe", + "startmenuexperiencehost.exe", "wudfhost.exe", "syshelper.exe", +]; + +fn is_protected(name_lower: &str) -> bool { + PROTECTED.contains(&name_lower) +} + +/// Правила храним в профиле пользователя, а не рядом с exe: установленная +/// программа лежит в Program Files, куда без прав администратора не записать. +fn rules_path() -> &'static PathBuf { + static PATH: OnceLock = OnceLock::new(); + PATH.get_or_init(|| { + let base = std::env::var_os("APPDATA") // Windows + .map(PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config"))); + + match base { + Some(dir) => { + let dir = dir.join("SysHelper"); + let _ = std::fs::create_dir_all(&dir); + dir.join("rules.json") + } + // без домашнего каталога остаётся только рабочая директория + None => PathBuf::from("rules.json"), + } + }) +} + +/* ── модель данных ─────────────────────────────────────────── */ + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum Mode { + Blacklist, + Whitelist, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Rules { + mode: Mode, + blocked: HashSet, // всегда в нижнем регистре + allowed: HashSet, + enforce: bool, +} + +impl Default for Rules { + fn default() -> Self { + Self { + mode: Mode::Blacklist, + blocked: HashSet::new(), + allowed: HashSet::new(), + enforce: true, + } + } +} + +impl Rules { + fn load() -> Self { + std::fs::read_to_string(rules_path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() + } + + fn save(&self) { + if let Ok(s) = serde_json::to_string_pretty(self) { + let _ = std::fs::write(rules_path(), s); + } + } + + fn violates(&self, name_lower: &str) -> bool { + match self.mode { + Mode::Blacklist => self.blocked.contains(name_lower), + Mode::Whitelist => !self.allowed.contains(name_lower), + } + } +} + +#[derive(Serialize)] +struct ProcInfo { + pid: u32, + name: String, + exe: Option, + memory_mb: u64, + blocked: bool, +} + +struct AppState { + rules: Mutex, + sys: Mutex, + log: Mutex>, + update: Mutex, +} + +type Shared = Arc; + +impl AppState { + async fn log(&self, msg: impl Into) { + let line = format!( + "[{}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + msg.into() + ); + println!("{line}"); + let mut log = self.log.lock().await; + log.push(line); + if log.len() > LOG_LIMIT { + let extra = log.len() - LOG_LIMIT; + log.drain(0..extra); + } + } +} + +/* ── завершение процессов ──────────────────────────────────── */ + +/// Убивает все процессы с данным именем, возвращает убитые PID. +async fn kill_by_name(state: &Shared, name: &str) -> Vec { + let target = name.trim().to_lowercase(); + if is_protected(&target) { + return Vec::new(); + } + + let me = std::process::id(); + let mut killed = Vec::new(); + + { + let mut sys = state.sys.lock().await; + sys.refresh_processes(); + for (pid, proc_) in sys.processes() { + let pid_u32 = pid.as_u32(); + if pid_u32 == me { + continue; + } + if proc_.name().to_lowercase() == target && proc_.kill() { + killed.push(pid_u32); + } + } + } // гвард дропаем до await ниже + + for pid in &killed { + state.log(format!("убит {target} (PID {pid})")).await; + } + killed +} + +/// Раз в SCAN_INTERVAL применяет правила ко всем процессам. +async fn watchdog(state: Shared) { + loop { + tokio::time::sleep(SCAN_INTERVAL).await; + + let rules = state.rules.lock().await.clone(); + if !rules.enforce { + continue; + } + // whitelist с пустым списком вынес бы всю систему + if rules.mode == Mode::Whitelist && rules.allowed.is_empty() { + continue; + } + + let me = std::process::id(); + let mut killed: Vec<(String, u32)> = Vec::new(); + + { + let mut sys = state.sys.lock().await; + sys.refresh_processes(); + for (pid, proc_) in sys.processes() { + let pid_u32 = pid.as_u32(); + let name = proc_.name().to_lowercase(); + if pid_u32 == me || is_protected(&name) { + continue; + } + if rules.violates(&name) && proc_.kill() { + killed.push((name, pid_u32)); + } + } + } + + for (name, pid) in killed { + state + .log(format!("правило сработало: {name} (PID {pid}) остановлен")) + .await; + } + } +} + +/* ── статика ───────────────────────────────────────────────── */ + +async fn index() -> Html<&'static str> { + Html(INDEX_HTML) +} + +async fn app_js() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "application/javascript; charset=utf-8")], + APP_JS, + ) +} + +/* ── HTTP-обработчики ──────────────────────────────────────── */ + +async fn list_processes(State(state): State) -> Json> { + let rules = state.rules.lock().await.clone(); + let mut sys = state.sys.lock().await; + sys.refresh_processes(); + + let mut list: Vec = sys + .processes() + .iter() + .map(|(pid, p)| { + let name = p.name().to_string(); + let lower = name.to_lowercase(); + ProcInfo { + pid: pid.as_u32(), + blocked: !is_protected(&lower) && rules.violates(&lower), + name, + exe: p.exe().map(|path| path.display().to_string()), + memory_mb: p.memory() / 1024 / 1024, + } + }) + .collect(); + + list.sort_by(|a, b| { + a.name + .to_lowercase() + .cmp(&b.name.to_lowercase()) + .then(a.pid.cmp(&b.pid)) + }); + Json(list) +} + +async fn get_rules(State(state): State) -> Json { + Json(state.rules.lock().await.clone()) +} + +async fn set_rules( + State(state): State, + Json(mut new_rules): Json, +) -> Json { + new_rules.blocked = new_rules.blocked.iter().map(|s| s.to_lowercase()).collect(); + new_rules.allowed = new_rules.allowed.iter().map(|s| s.to_lowercase()).collect(); + { + let mut rules = state.rules.lock().await; + *rules = new_rules; + rules.save(); + } + state.log("правила обновлены").await; + Json(json!({ "ok": true })) +} + +#[derive(Deserialize)] +struct NameReq { + name: String, +} + +async fn rule_block( + State(state): State, + Json(req): Json, +) -> Json { + let name = req.name.trim().to_lowercase(); + if is_protected(&name) { + return Json(json!({ "ok": false, "error": "системный процесс" })); + } + { + let mut rules = state.rules.lock().await; + rules.blocked.insert(name.clone()); + rules.allowed.remove(&name); + rules.save(); + } + state.log(format!("в чёрный список: {name}")).await; + Json(json!({ "ok": true, "name": name })) +} + +async fn rule_allow( + State(state): State, + Json(req): Json, +) -> Json { + let name = req.name.trim().to_lowercase(); + { + let mut rules = state.rules.lock().await; + rules.allowed.insert(name.clone()); + rules.blocked.remove(&name); + rules.save(); + } + state.log(format!("в белый список: {name}")).await; + Json(json!({ "ok": true, "name": name })) +} + +async fn rule_remove( + State(state): State, + Json(req): Json, +) -> Json { + let name = req.name.trim().to_lowercase(); + { + let mut rules = state.rules.lock().await; + rules.blocked.remove(&name); + rules.allowed.remove(&name); + rules.save(); + } + Json(json!({ "ok": true, "name": name })) +} + +async fn kill_now( + State(state): State, + Json(req): Json, +) -> Json { + let killed = kill_by_name(&state, &req.name).await; + Json(json!({ "ok": !killed.is_empty(), "killed": killed })) +} + +#[derive(Deserialize)] +struct PidReq { + pid: u32, +} + +async fn kill_pid(State(state): State, Json(req): Json) -> Json { + let mut sys = state.sys.lock().await; + sys.refresh_processes(); + + let result = match sys.process(Pid::from_u32(req.pid)) { + Some(p) if is_protected(&p.name().to_lowercase()) => { + json!({ "ok": false, "error": "системный процесс" }) + } + Some(p) => json!({ "ok": p.kill(), "name": p.name() }), + None => json!({ "ok": false, "error": "процесс не найден" }), + }; + drop(sys); + + if result["ok"] == json!(true) { + state.log(format!("ручное завершение PID {}", req.pid)).await; + } + Json(result) +} + +#[derive(Deserialize)] +struct RandomKillReq { + name: String, + #[serde(default = "default_min")] + min_secs: u64, + #[serde(default = "default_max")] + max_secs: u64, +} +fn default_min() -> u64 { + 60 +} +fn default_max() -> u64 { + 1800 +} + +/// Завершение в случайный момент внутри окна [min, max]. +async fn kill_random( + State(state): State, + Json(req): Json, +) -> Json { + let min = req.min_secs; + let max = req.max_secs.max(min + 1); + + // ThreadRng не Send — держать его через .await компилятор не даст + let delay = { + let mut rng = rand::thread_rng(); + rng.gen_range(min..=max) + }; + + let name = req.name.clone(); + let st = Arc::clone(&state); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(delay)).await; + let killed = kill_by_name(&st, &name).await; + if killed.is_empty() { + st.log(format!("случайное завершение: {name} уже не запущен")) + .await; + } + }); + + state + .log(format!( + "запланировано случайное завершение {} через {delay} с", + req.name + )) + .await; + Json(json!({ "ok": true, "name": req.name, "delay_secs": delay })) +} + +async fn get_log(State(state): State) -> Json> { + Json(state.log.lock().await.clone()) +} + +async fn update_status(State(state): State) -> Json { + Json(state.update.lock().await.clone()) +} + +async fn update_check(State(state): State) -> Json { + // не блокируем ответ: проверка и возможная установка идут в фоне + tokio::spawn(update::check_spawn(Arc::clone(&state))); + Json(json!({ "ok": true })) +} + +/* ── точка входа ───────────────────────────────────────────── */ + +#[tokio::main] +async fn main() { + let state: Shared = Arc::new(AppState { + rules: Mutex::new(Rules::load()), + sys: Mutex::new(System::new_all()), + log: Mutex::new(Vec::new()), + update: Mutex::new(update::UpdateStatus::new()), + }); + + tokio::spawn(watchdog(Arc::clone(&state))); + tokio::spawn(update::run(Arc::clone(&state))); + + let app = Router::new() + .route("/", get(index)) + .route("/app.js", get(app_js)) + .route("/processes", get(list_processes)) + .route("/rules", get(get_rules).post(set_rules)) + .route("/rules/block", post(rule_block)) + .route("/rules/allow", post(rule_allow)) + .route("/rules/remove", post(rule_remove)) + .route("/kill", post(kill_now)) + .route("/kill/pid", post(kill_pid)) + .route("/kill/random", post(kill_random)) + .route("/log", get(get_log)) + .route("/update", get(update_status)) + .route("/update/check", post(update_check)) + .with_state(Arc::clone(&state)); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:8787") + .await + .expect("порт 8787 занят"); + + state.log("сервер запущен на http://127.0.0.1:8787").await; + state + .log(format!("правила: {}", rules_path().display())) + .await; + axum::serve(listener, app).await.unwrap(); +} \ No newline at end of file diff --git a/src/update.rs b/src/update.rs new file mode 100644 index 0000000..428816a --- /dev/null +++ b/src/update.rs @@ -0,0 +1,319 @@ +//! Автообновление через GitHub Releases. +//! +//! Раз в `CHECK_INTERVAL` спрашиваем последнюю версию репозитория; если она +//! новее вшитой — молча скачиваем установщик и запускаем его. +//! +//! Тонкость с правами. Сервер запускается от обычного пользователя, а +//! установщик пишет в Program Files и требует администратора. Чтобы обновление +//! шло без окна UAC, установщик (`installer/setup.iss`) заранее регистрирует +//! задачу Планировщика «SysHelperUpdate» — запуск от SYSTEM с наивысшими +//! правами. Мы лишь кладём свежий `setup.exe` в фиксированное место +//! (`{ProgramData}\SysHelper\update\setup.exe`) и дёргаем эту задачу: она +//! запускает установщик повышенно и тихо. Путь должен совпадать с тем, что +//! прописан в `[Run]` задачи в `setup.iss`. + +use std::{path::PathBuf, time::Duration}; +#[cfg(windows)] +use std::process::Command; + +use serde::{Deserialize, Serialize}; + +use crate::Shared; + +/// Версия из Cargo.toml — её же CI подставляет в тег релиза и имя установщика. +const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Координаты репозитория. Поправьте, если owner/имя другие. +const GITHUB_OWNER: &str = "robonen"; +const GITHUB_REPO: &str = "syshelper"; + +const CHECK_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60); +/// Имя задачи Планировщика — должно совпадать с `setup.iss`. +const TASK_NAME: &str = "SysHelperUpdate"; + +/* ── статус для веб-панели ──────────────────────────────────── */ + +#[derive(Debug, Clone, Serialize)] +pub struct UpdateStatus { + /// Установленная сейчас версия. + current: String, + /// Последняя известная версия (тег релиза) либо `current`, если проверка не находила новее. + latest: Option, + available: bool, + /// idle | checking | downloading | installing + phase: String, + /// Время последней проверки, локальное. + checked_at: Option, + error: Option, + /// Идёт обновление — второй запуск не начинаем. В JSON не отдаём. + #[serde(skip)] + busy: bool, +} + +impl UpdateStatus { + pub fn new() -> Self { + Self { + current: CURRENT_VERSION.to_string(), + latest: None, + available: false, + phase: "idle".to_string(), + checked_at: None, + error: None, + busy: false, + } + } +} + +/* ── работа с GitHub ────────────────────────────────────────── */ + +#[derive(Deserialize)] +struct Release { + tag_name: String, + #[serde(default)] + assets: Vec, +} + +#[derive(Deserialize)] +struct Asset { + name: String, + browser_download_url: String, +} + +/// Найденное обновление: разобранная версия, исходный тег и ссылка на установщик. +struct Update { + tag: String, + installer_url: String, +} + +fn build_client() -> Result { + reqwest::Client::builder() + .user_agent(concat!("SysHelper/", env!("CARGO_PKG_VERSION"))) + .timeout(Duration::from_secs(120)) + .build() + .map_err(|e| e.to_string()) +} + +/// Возвращает `Some(update)`, если в последнем релизе версия новее текущей. +async fn fetch_update(client: &reqwest::Client) -> Result, String> { + let url = + format!("https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest"); + + let release: Release = client + .get(url) + .header("Accept", "application/vnd.github+json") + .send() + .await + .map_err(|e| e.to_string())? + .error_for_status() + .map_err(|e| e.to_string())? + .json() + .await + .map_err(|e| e.to_string())?; + + // теги вида v0.2.0 — semver парсит без ведущей v + let tag = release.tag_name.trim_start_matches('v'); + let latest = semver::Version::parse(tag) + .map_err(|e| format!("не разобрать версию «{}»: {e}", release.tag_name))?; + let current = semver::Version::parse(CURRENT_VERSION) + .map_err(|e| format!("своя версия «{CURRENT_VERSION}» некорректна: {e}"))?; + + if latest <= current { + return Ok(None); + } + + let installer = release + .assets + .iter() + .find(|a| a.name.to_lowercase().ends_with("-setup.exe")) + .ok_or_else(|| "в релизе нет установщика *-setup.exe".to_string())?; + + Ok(Some(Update { + tag: release.tag_name.clone(), + installer_url: installer.browser_download_url.clone(), + })) +} + +/// Куда кладём скачанный установщик. Совпадает с путём задачи в `setup.iss`. +fn installer_path() -> PathBuf { + let base = std::env::var_os("ProgramData") + .map(PathBuf::from) + .unwrap_or_else(std::env::temp_dir); + base.join("SysHelper").join("update").join("setup.exe") +} + +async fn download(client: &reqwest::Client, url: &str, dest: &PathBuf) -> Result<(), String> { + let bytes = client + .get(url) + .send() + .await + .map_err(|e| e.to_string())? + .error_for_status() + .map_err(|e| e.to_string())? + .bytes() + .await + .map_err(|e| e.to_string())?; + + if let Some(dir) = dest.parent() { + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + std::fs::write(dest, &bytes).map_err(|e| e.to_string()) +} + +/// Запускает установщик повышенно и тихо через заранее зарегистрированную задачу. +/// Если задачи нет (например, портативный запуск) — стартуем напрямую, тогда +/// Windows покажет запрос UAC. +fn launch_installer(dest: &PathBuf) -> Result<(), String> { + #[cfg(windows)] + { + let ran = Command::new("schtasks") + .args(["/Run", "/TN", TASK_NAME]) + .status() + .map(|s| s.success()) + .unwrap_or(false); + if ran { + return Ok(()); + } + Command::new(dest) + .args([ + "/VERYSILENT", + "/SUPPRESSMSGBOXES", + "/NORESTART", + "/NOCANCEL", + ]) + .spawn() + .map(|_| ()) + .map_err(|e| e.to_string()) + } + #[cfg(not(windows))] + { + let _ = (dest, TASK_NAME); + Err("автоустановка поддерживается только на Windows".to_string()) + } +} + +/// Отцепляет короткий скрипт: он переждёт установку и поднимет уже новую +/// версию в текущей пользовательской сессии. Установщик закроет наш процесс, +/// а этот дочерний cmd переживёт закрытие, так как не держит файлы программы. +fn schedule_restart() { + #[cfg(windows)] + if let Ok(exe) = std::env::current_exe() { + let exe = exe.display().to_string(); + // ping вместо timeout — надёжнее работает в отцепленном процессе + let script = format!("ping -n 30 127.0.0.1 >nul & start \"\" \"{exe}\""); + let _ = Command::new("cmd").args(["/C", &script]).spawn(); + } +} + +/* ── оркестрация ────────────────────────────────────────────── */ + +fn now_string() -> String { + chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string() +} + +/// Одна проверка: смотрим релиз и, если версия новее, скачиваем и запускаем. +pub async fn check_now(state: Shared, client: reqwest::Client) { + { + let mut s = state.update.lock().await; + if s.busy { + return; + } + s.phase = "checking".to_string(); + s.error = None; + } + + match fetch_update(&client).await { + Err(e) => { + { + let mut s = state.update.lock().await; + s.phase = "idle".to_string(); + s.checked_at = Some(now_string()); + s.error = Some(e.clone()); + } + state.log(format!("обновление: не удалось проверить — {e}")).await; + } + Ok(None) => { + let mut s = state.update.lock().await; + s.phase = "idle".to_string(); + s.checked_at = Some(now_string()); + s.latest = Some(CURRENT_VERSION.to_string()); + s.available = false; + } + Ok(Some(update)) => { + { + let mut s = state.update.lock().await; + s.busy = true; + s.available = true; + s.latest = Some(update.tag.clone()); + s.checked_at = Some(now_string()); + s.phase = "downloading".to_string(); + } + state + .log(format!("обновление: найдена версия {} — устанавливаю", update.tag)) + .await; + + let dest = installer_path(); + if let Err(e) = download(&client, &update.installer_url, &dest).await { + let mut s = state.update.lock().await; + s.busy = false; + s.phase = "idle".to_string(); + s.error = Some(e.clone()); + drop(s); + state.log(format!("обновление: ошибка загрузки — {e}")).await; + return; + } + + { + let mut s = state.update.lock().await; + s.phase = "installing".to_string(); + } + + // сперва планируем перезапуск, потом запускаем установщик: он вот-вот + // закроет этот процесс + schedule_restart(); + if let Err(e) = launch_installer(&dest) { + let mut s = state.update.lock().await; + s.busy = false; + s.phase = "idle".to_string(); + s.error = Some(e.clone()); + drop(s); + state + .log(format!("обновление: не удалось запустить установщик — {e}")) + .await; + return; + } + state + .log(format!( + "обновление {} запущено; панель перезапустится автоматически", + update.tag + )) + .await; + } + } +} + +/// Проверка по запросу из панели. +pub async fn check_spawn(state: Shared) { + match build_client() { + Ok(client) => check_now(state, client).await, + Err(e) => state.log(format!("обновление: {e}")).await, + } +} + +/// Фоновый цикл: проверяем при старте и далее раз в `CHECK_INTERVAL`. +pub async fn run(state: Shared) { + // даём серверу подняться, прежде чем лезть в сеть + tokio::time::sleep(Duration::from_secs(20)).await; + + let client = match build_client() { + Ok(client) => client, + Err(e) => { + state.log(format!("автообновление отключено: {e}")).await; + return; + } + }; + + loop { + check_now(state.clone(), client.clone()).await; + tokio::time::sleep(CHECK_INTERVAL).await; + } +} diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..28b0d1e --- /dev/null +++ b/static/app.js @@ -0,0 +1,276 @@ +const { createApp, ref, computed, onMounted, onUnmounted } = Vue; + +async function api(path, body) { + const init = body + ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } + : {}; + const res = await fetch(path, init); + if (!res.ok) throw new Error(`${path}: ${res.status}`); + return res.json(); +} + +/* ── composables ───────────────────────────────────────────── */ + +function useRules() { + const rules = ref({ mode: 'blacklist', blocked: [], allowed: [], enforce: true }); + const load = async () => { rules.value = await api('/rules'); }; + const block = (name) => api('/rules/block', { name }).then(load); + const allow = (name) => api('/rules/allow', { name }).then(load); + const remove = (name) => api('/rules/remove', { name }).then(load); + // patch мержим в текущее состояние: сервер принимает объект правил целиком + const save = (patch) => api('/rules', { ...rules.value, ...patch }).then(load); + return { rules, load, block, allow, remove, save }; +} + +function useProcesses() { + const raw = ref([]); + const load = async () => { raw.value = await api('/processes'); }; + const kill = (name) => api('/kill', { name }).then(load); + + // одноимённые процессы схлопываем — иначе двадцать строк chrome.exe + const groups = computed(() => { + const map = new Map(); + for (const p of raw.value) { + const key = p.name.toLowerCase(); + if (!map.has(key)) map.set(key, { key, name: p.name, count: 0, mem: 0, blocked: p.blocked }); + const g = map.get(key); + g.count += 1; + g.mem += p.memory_mb; + } + return [...map.values()].sort((a, b) => b.mem - a.mem); + }); + + return { groups, load, kill }; +} + +function useLog() { + const lines = ref([]); + const load = async () => { lines.value = await api('/log'); }; + return { lines, load }; +} + +function useUpdate() { + const info = ref({ current: '', latest: null, available: false, phase: 'idle', checked_at: null, error: null }); + const load = async () => { info.value = await api('/update'); }; + // после ручной проверки состояние меняется не мгновенно — перечитываем чуть позже + const check = () => api('/update/check', {}).then(() => setTimeout(load, 1500)); + return { info, load, check }; +} + +function usePolling(fn, ms) { + let id = null; + onMounted(() => { fn(); id = setInterval(fn, ms); }); + onUnmounted(() => clearInterval(id)); +} + +/* ── компоненты ────────────────────────────────────────────── */ + +const NameChips = { + props: { + title: { type: String, required: true }, + names: { type: Array, default: () => [] }, + }, + emits: ['remove'], + setup(props) { + return { sorted: computed(() => [...props.names].sort()) }; + }, + template: ` +
+
{{ title }}
+ пусто + + {{ name }} + + +
`, +}; + +const UpdateBar = { + props: { info: { type: Object, required: true } }, + emits: ['check'], + setup(props) { + const busy = computed(() => ['checking', 'downloading', 'installing'].includes(props.info.phase)); + const status = computed(() => { + const i = props.info; + const phases = { checking: 'проверка…', downloading: 'загрузка…', installing: 'установка…' }; + if (phases[i.phase]) return phases[i.phase]; + if (i.error) return 'не удалось проверить обновления'; + if (i.available && i.latest && i.latest !== i.current) return 'доступна версия ' + i.latest; + return 'установлена последняя версия'; + }); + return { busy, status }; + }, + template: ` +
+
+
+

Версия {{ info.current || '—' }}

+ {{ status }} +
+ +
+
`, +}; + +const RulesPanel = { + components: { NameChips }, + props: { rules: { type: Object, required: true } }, + emits: ['update', 'remove'], + template: ` +
+

Правила

+
+ + +
+

+ Белый список пуст — сервер намеренно ничего не завершает, иначе снесло бы систему. +

+ + +
`, +}; + +const RandomKill = { + emits: ['schedule'], + setup(_, { emit }) { + const name = ref(''); + const min = ref(5); + const max = ref(30); + + const submit = () => { + const value = name.value.trim(); + if (!value) return; + emit('schedule', { + name: value, + min_secs: Math.max(0, min.value) * 60, + max_secs: Math.max(1, max.value) * 60, + }); + name.value = ''; + }; + + return { name, min, max, submit }; + }, + template: ` +
+

Случайное завершение

+
+ + — + + мин + +
+
`, +}; + +const ProcessTable = { + props: { + groups: { type: Array, default: () => [] }, + whitelist: { type: Boolean, default: false }, + }, + emits: ['toggle', 'kill'], + setup(props) { + const filter = ref(''); + const visible = computed(() => { + const q = filter.value.trim().toLowerCase(); + return q ? props.groups.filter((g) => g.key.includes(q)) : props.groups; + }); + return { filter, visible }; + }, + template: ` +
+

Процессы

+
+ + {{ visible.length }} из {{ groups.length }} +
+
+ + + + + + + + + + + + +
ИмяЭкз.Память
{{ g.name }}{{ g.count }}{{ g.mem }} МБ + + +
+
+
`, +}; + +const EventLog = { + props: { lines: { type: Array, default: () => [] } }, + setup(props) { + return { recent: computed(() => props.lines.slice(-40).reverse().join('\n')) }; + }, + template: ` +
+

Журнал

+
{{ recent || 'пусто' }}
+
`, +}; + +/* ── корневой компонент ────────────────────────────────────── */ + +const App = { + components: { UpdateBar, RulesPanel, RandomKill, ProcessTable, EventLog }, + setup() { + const { rules, load: loadRules, block, allow, remove, save } = useRules(); + const { groups, load: loadProcesses, kill } = useProcesses(); + const { lines, load: loadLog } = useLog(); + const { info: update, load: loadUpdate, check: checkUpdate } = useUpdate(); + + onMounted(loadRules); + usePolling(() => { loadProcesses(); loadLog(); loadUpdate(); }, 3000); + + // в whitelist «заблокирован» означает отсутствие в allowed — действия зеркальные + const toggle = async (g) => { + if (rules.value.mode === 'whitelist') { + await (g.blocked ? allow(g.name) : remove(g.name)); + } else { + await (g.blocked ? remove(g.name) : block(g.name)); + } + loadProcesses(); + }; + + const scheduleRandom = async (payload) => { + await api('/kill/random', payload); + loadLog(); + }; + + const whitelist = computed(() => rules.value.mode === 'whitelist'); + + return { rules, groups, lines, update, whitelist, save, remove, toggle, kill, scheduleRandom, checkUpdate }; + }, + template: ` +

Родительский контроль

+ + + + + `, +}; + +createApp(App).mount('#app'); \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..66f7b9e --- /dev/null +++ b/static/index.html @@ -0,0 +1,48 @@ + + + + + +Родительский контроль + + + +
+ + + + \ No newline at end of file