From 500256759efcb4399cc356f919307380e840349e Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 2 Aug 2024 12:39:57 +0300 Subject: [PATCH 01/11] [desktop] Minor tauri fixes and improvments --- Earthfile | 6 +- desktop/tauri/src-tauri/Cargo.lock | 201 ++++++++++++------ desktop/tauri/src-tauri/Cargo.toml | 4 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 1 - .../src-tauri/gen/schemas/linux-schema.json | 1 - desktop/tauri/src-tauri/src/cli.rs | 107 ++++++++++ desktop/tauri/src-tauri/src/main.rs | 78 +------ packaging/linux/portmaster.service | 8 +- 9 files changed, 267 insertions(+), 141 deletions(-) create mode 100644 desktop/tauri/src-tauri/src/cli.rs diff --git a/Earthfile b/Earthfile index 9d016b082..4a81900b4 100644 --- a/Earthfile +++ b/Earthfile @@ -506,8 +506,8 @@ tauri-build: RUN echo output: $(ls -R "target/${target}/release") # Binaries - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/app" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app" - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/app.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app.exe" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app.exe" SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/WebView2Loader.dll" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/WebView2Loader.dll" # Installers @@ -575,7 +575,7 @@ tauri-build-windows-bundle: RUN echo "Version Suffix: $VERSION_SUFFIX" RUN echo output: $(ls -R "target/${target}/release") - RUN mv "target/${target}/release/app.exe" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" + RUN mv "target/${target}/release/portmaster.exe" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" RUN zip "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" -j portmaster-app${VERSION_SUFFIX}.exe "target/${target}/release/WebView2Loader.dll" -j WebView2Loader.dll SAVE ARTIFACT --if-exists "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/" diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index 8d9389859..8e0373038 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -74,7 +74,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -3768,7 +3768,7 @@ dependencies = [ name = "indexmap" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -4176,7 +4176,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "bytecount", - "clap 4.5.16", + "clap 4.5.9", "fancy-regex", "fraction", "getrandom 0.2.15", @@ -4645,8 +4645,8 @@ dependencies = [ ] [[package]] -name = "mio" -version = "1.0.2" +name = "muda" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ @@ -4980,14 +4980,35 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive 0.7.2", ] [[package]] name = "num_enum_derive" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", @@ -5145,9 +5166,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -5663,8 +5684,7 @@ dependencies = [ "nom", "num-bigint-dig", "num-traits", - "num_enum", - "ocb3", + "num_enum 0.7.2", "p256", "p384", "p521", @@ -5900,7 +5920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.4.0", + "indexmap 2.2.6", "quick-xml 0.32.0", "serde", "time", @@ -5962,6 +5982,58 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portmaster" +version = "0.1.0" +dependencies = [ + "assert_matches", + "cached", + "clap_lex", + "ctor", + "dark-light", + "dataurl", + "dirs 1.0.5", + "futures-util", + "gdk-pixbuf", + "gdk-pixbuf-sys", + "gio-sys", + "glib", + "glib-sys", + "gtk", + "gtk-sys", + "http 1.1.0", + "lazy_static", + "log", + "notify-rust", + "open", + "reqwest 0.12.5", + "rfd", + "rust-ini 0.20.0", + "serde", + "serde_json", + "sha", + "tauri", + "tauri-build", + "tauri-cli", + "tauri-plugin-clipboard-manager", + "tauri-plugin-dialog", + "tauri-plugin-log", + "tauri-plugin-notification", + "tauri-plugin-os", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-window-state", + "tauri-winrt-notification 0.3.1", + "thiserror", + "tokio", + "tokio-websockets", + "url", + "uuid", + "which", + "windows 0.54.0", + "windows-service", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5970,12 +6042,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "precomputed-hash" @@ -7022,11 +7091,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "itoa 1.0.11", "memchr", "ryu", @@ -7085,7 +7154,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -7805,9 +7874,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.0-rc.8" +version = "2.0.0-beta.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8345ccc676ef16e26b61fc0f5340b4e770678b1e1f53f08c69ebdac5e56b422" +checksum = "3eab508aad4ae86e23865e294b20a7bb89bd7afea523897b7478329b841d4295" dependencies = [ "anyhow", "bytes", @@ -7877,9 +7946,9 @@ dependencies = [ [[package]] name = "tauri-bundler" -version = "2.0.1-rc.6" +version = "2.0.1-beta.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00a0e120d67416e774923fc07a7b531ba6898ed08f0d6e07fe79d7e3da7e0c04" +checksum = "05cc597af0bcb88c1966ccba76a6dcde5b6530c97a4fc2d798f7d3fddf182944" dependencies = [ "anyhow", "ar", @@ -7921,15 +7990,15 @@ dependencies = [ [[package]] name = "tauri-cli" -version = "2.0.0-rc.8" +version = "2.0.0-beta.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc42d318b25224c40f88550308f8cc8746a38534d450ce9c553f8c93f335499" +checksum = "24129a8dc9c3c910fca89ba0fb87ad9028868abf9dadb2514cd569fef9ad53af" dependencies = [ "anyhow", "axum", "base64 0.22.1", "cargo-mobile2", - "clap 4.5.16", + "clap 4.5.9", "clap_complete", "colored", "common-path", @@ -8076,8 +8145,8 @@ dependencies = [ "schemars", "serde", "serde_json", - "tauri-utils 2.0.0-rc.7", - "toml 0.8.19", + "tauri-utils 2.0.0-beta.19", + "toml 0.8.15", "walkdir", ] @@ -8099,9 +8168,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.0.0-rc.1" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf99017391fdc40b6c8ae0dae8d970cc8151a8177d48b8805f320f52cac0e3c" +checksum = "8860dd73c96969eb14813f9f04d8665f2853342670456fb6619d637137ef0d09" dependencies = [ "dunce", "log", @@ -8117,9 +8186,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.0.0-rc.0" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df6b25b1f2b7b61565e66c4dbee9eb39e5635d2a763206e380e07cc3f601a67" +checksum = "461853268fe115ca19ee21e5986d505944f0b826048fe1bd726d74753fdf1df6" dependencies = [ "anyhow", "glob", @@ -8136,9 +8205,9 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.0.0-rc.0" +version = "2.0.0-beta.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380d27f23c39cde6a73024e65d8ec9b5b0af861e968dbe16b3aad86cd2c578e5" +checksum = "80f80d78a6e8102acf05a1e735f006991a2abfc71566d4e484f820b7495cd52c" dependencies = [ "android_logger", "byte-unit", @@ -8158,9 +8227,9 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.0.0-rc.1" +version = "2.0.0-beta.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35677cdcdb4dc3f3ef6891f31b8ea314045064752912d66e676a4f1577b57ffa" +checksum = "a5a09eb4d9e0919ce954da68d3707ddb161e5e37447da26609d0d0f5aebbc69a" dependencies = [ "log", "notify-rust", @@ -8177,9 +8246,9 @@ dependencies = [ [[package]] name = "tauri-plugin-os" -version = "2.0.0-rc.0" +version = "2.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b54cfeb26356822d3be3db4282041b03552f573a694b6b28aded7d95c62a039" +checksum = "79a0466f11f45fd3f640a17b5ba5e34c62912c9b391141c818155125ae9f0917" dependencies = [ "gethostname", "log", @@ -8195,9 +8264,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.0.0-rc.1" +version = "2.0.0-beta.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2133e5c6fe2ae0263ff5920feed477d3b1413f89033f537966831b0cb6f61f8e" +checksum = "8a9fa8c4e3d9ec343f6c3eb081672045566128a6c48ff6f6eeea85251ff38d3f" dependencies = [ "encoding_rs", "log", @@ -8216,9 +8285,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.0.0-rc.0" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de552151b4c9ba9ff72c7244dccaadd47f88d1f0d5caa2603c5c1c12b7636edc" +checksum = "b21866e185e2f9c5d40afb851441e3292a4f94f4a26af6ae0dff6e7e5ba03f42" dependencies = [ "log", "serde", @@ -8231,9 +8300,9 @@ dependencies = [ [[package]] name = "tauri-plugin-window-state" -version = "2.0.0-rc.1" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb6839228cbd225b95681c766cc51113e9dad62c4b3f6ebb102234413ba85ee2" +checksum = "b6f7cea222b8eeb3598c7b3e19e9c7e6b1c2d60207b87225e0c3bb1c24c8fdec" dependencies = [ "bitflags 2.6.0", "log", @@ -8246,9 +8315,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.0.0-rc.7" +version = "2.0.0-beta.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c72b844f387bfc3341c355f3e16b8cbf4161848fa4e348670effb222cd3ba5" +checksum = "fe978df03966febbebc608931dc2cf26ef94df70855a18b05f07134cf474de09" dependencies = [ "dpi", "gtk", @@ -8265,9 +8334,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.0.0-rc.7" +version = "2.0.0-beta.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73accf936a7cd01d1382de7850726fdf6c1f6ab3b01ccb7a0950cb852e332596" +checksum = "11e4d568f61095f507b3fc4254dfbfff3b20de2a1d66167ffca3f6d90b14db8f" dependencies = [ "cocoa 0.26.0", "gtk", @@ -8576,14 +8645,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.3" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio", + "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", @@ -8711,7 +8781,7 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -8733,7 +8803,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -8746,7 +8816,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -8757,7 +8827,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -8768,7 +8838,7 @@ version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -9523,9 +9593,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "6.0.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" dependencies = [ "either", "home", @@ -10306,13 +10376,22 @@ dependencies = [ "zvariant 4.2.0", ] +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive 0.6.6", +] + [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive", ] diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index db19639b6..3dcfdef56 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "app" +name = "portmaster" version = "0.1.0" description = "Portmaster UI" authors = ["Safing"] license = "" repository = "" -default-run = "app" +default-run = "portmaster" edition = "2021" rust-version = "1.60" diff --git a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json index 233ccc01d..85397e542 100644 --- a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json +++ b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"clipboard-manager":{"default_permission":null,"permissions":{"allow-read":{"identifier":"allow-read","description":"Enables the read command without any pre-configured scope.","commands":{"allow":["read"],"deny":[]}},"allow-write":{"identifier":"allow-write","description":"Enables the write command without any pre-configured scope.","commands":{"allow":["write"],"deny":[]}},"deny-read":{"identifier":"deny-read","description":"Denies the read command without any pre-configured scope.","commands":{"allow":[],"deny":["read"]}},"deny-write":{"identifier":"deny-write","description":"Denies the write command without any pre-configured scope.","commands":{"allow":[],"deny":["write"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} diff --git a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json index 797ccb5c8..2119bf212 100644 --- a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json @@ -3072,4 +3072,3 @@ ] } } -} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json index 797ccb5c8..2119bf212 100644 --- a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json @@ -3072,4 +3072,3 @@ ] } } -} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/cli.rs b/desktop/tauri/src-tauri/src/cli.rs new file mode 100644 index 000000000..d11c2aed3 --- /dev/null +++ b/desktop/tauri/src-tauri/src/cli.rs @@ -0,0 +1,107 @@ +use log::LevelFilter; + +#[cfg(not(debug_assertions))] +const DEFAULT_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Warn; + +#[cfg(debug_assertions)] +const DEFAULT_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Debug; + +#[derive(Debug)] +pub struct CliArguments { + // Path to the installation directory + pub data: Option, + + // Log level to use: off, error, warn, info, debug, trace + pub log_level: log::LevelFilter, + + // Start in the background without opening a window + pub background: bool, + + // Enable experimental notifications via Tauri. Replaces the notifier app. + pub with_prompts: bool, + + // Enable experimental prompt support via Tauri. Replaces the notifier app. + pub with_notifications: bool, +} + +impl CliArguments { + fn parse_log(&mut self, level: String) { + self.log_level = match level.as_ref() { + "off" => LevelFilter::Off, + "error" => LevelFilter::Error, + "warn" => LevelFilter::Warn, + "info" => LevelFilter::Info, + "debug" => LevelFilter::Debug, + "trace" => LevelFilter::Trace, + _ => DEFAULT_LOG_LEVEL, + } + } +} + +pub fn parse(raw: impl IntoIterator>) -> CliArguments { + let mut cli = CliArguments { + data: None, + log_level: DEFAULT_LOG_LEVEL, + background: false, + with_prompts: false, + with_notifications: false, + }; + + let raw = clap_lex::RawArgs::new(raw); + let mut cursor = raw.cursor(); + raw.next(&mut cursor); // Skip the bin + + while let Some(arg) = raw.next(&mut cursor) { + if let Some((long, value)) = arg.to_long() { + match long { + Ok("data") => { + if let Some(value) = value { + cli.data = Some(value.to_string_lossy().into_owned()); + } + } + Ok("log") => { + if let Some(value) = value { + cli.parse_log(value.to_string_lossy().into_owned()); + } + } + Ok("background") => { + cli.background = true; + } + Ok("with_prompts") => { + cli.with_prompts = true; + } + Ok("with_notifications") => { + cli.with_notifications = true; + } + _ => { + // Ignore unexpected flags + } + } + } else if let Some(mut shorts) = arg.to_short() { + while let Some(short) = shorts.next() { + match short { + Ok('l') => { + if let Some(value) = shorts.next_value_os() { + let mut str = value.to_string_lossy().into_owned(); + _ = str.remove(0); // remove first "=" from value (in -l=warn value will be "=warn") + cli.parse_log(str); + } + } + Ok('d') => { + if let Some(value) = shorts.next_value_os() { + let mut str = value.to_string_lossy().into_owned(); + _ = str.remove(0); // remove first "=" from value (in -d=/data value will be "=/data") + cli.data = Some(str); + } + } + Ok('b') => cli.background = true, + _ => { + // Ignore unexpected flags + } + } + } + } + } + + cli +} diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index d269c4f39..77c1d0557 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -3,7 +3,6 @@ use std::{env, path::Path, time::Duration}; -use clap::{Arg, Command}; use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent}; // Library crates @@ -15,11 +14,12 @@ mod xdg; // App modules mod config; +mod cli; mod portmaster; mod traymenu; mod window; -use log::{debug, error, info, LevelFilter}; +use log::{debug, error, info}; use portmaster::PortmasterExt; use tauri_plugin_log::RotationStrategy; use traymenu::setup_tray_menu; @@ -30,12 +30,6 @@ extern crate lazy_static; const FALLBACK_TO_OLD_UI_EXIT_CODE: i32 = 77; -#[cfg(not(debug_assertions))] -const LOG_LEVEL: LevelFilter = LevelFilter::Warn; - -#[cfg(debug_assertions)] -const LOG_LEVEL: LevelFilter = LevelFilter::Debug; - #[derive(Clone, serde::Serialize)] struct Payload { args: Vec, @@ -49,23 +43,6 @@ struct WsHandler { is_first_connect: bool, } -struct CliArguments { - // Path to the installation directory - data: Option, - - // Log level to use: off, error, warn, info, debug, trace - log: String, - - // Start in the background without opening a window - background: bool, - - // Enable experimental notifications via Tauri. Replaces the notifier app. - with_prompts: bool, - - // Enable experimental prompt support via Tauri. Replaces the notifier app. - with_notifications: bool, -} - impl portmaster::Handler for WsHandler { fn name(&self) -> String { "main-handler".to_string() @@ -147,44 +124,7 @@ fn main() { std::process::exit(show_webview_not_installed_dialog()); } - let matches = Command::new("Portmaster") - .ignore_errors(true) - .arg( - Arg::new("data") - .short('d') - .long("data") - .required(false) - .help("Path to the installation directory."), - ) - .arg( - Arg::new("log") - .short('l') - .long("log") - .required(false) - .help("Log level to use: off, error, warn, info, debug, trace."), - ) - .arg( - Arg::new("background") - .short('b') - .long("background") - .required(false) - .help("Start in the background without opening a window."), - ) - .arg( - Arg::new("with_prompts") - .long("with_prompts") - .required(false) - .action(clap::ArgAction::SetTrue) - .help("Enable experimental notifications via Tauri. Replaces the notifier app."), - ) - .arg( - Arg::new("with_notifications") - .long("with_notifications") - .required(false) - .action(clap::ArgAction::SetTrue) - .help("Enable experimental prompt support via Tauri. Replaces the notifier app."), - ) - .get_matches(); + let cli_args = cli::parse(std::env::args()); let mut cli = CliArguments { data: None, @@ -245,7 +185,7 @@ fn main() { // Initialize Logging plugin. .plugin( tauri_plugin_log::Builder::default() - .level(log_level) + .level(cli_args.log_level) .rotation_strategy(RotationStrategy::KeepAll) .clear_targets() .target(log_target) @@ -287,16 +227,18 @@ fn main() { }); // Handle cli flags: - app.portmaster().set_show_after_bootstrap(!cli.background); app.portmaster() - .with_notification_support(cli.with_notifications); - app.portmaster().with_connection_prompts(cli.with_prompts); + .set_show_after_bootstrap(!cli_args.background); + app.portmaster() + .with_notification_support(cli_args.with_notifications); + app.portmaster() + .with_connection_prompts(cli_args.with_prompts); // prepare a custom portmaster plugin handler that will show the splash-screen // (if not in --background) and launch the tray-icon handler. let handler = WsHandler { handle: app.handle().clone(), - background: cli.background, + background: cli_args.background, is_first_connect: true, }; diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service index 7541d4991..5490ac6f9 100644 --- a/packaging/linux/portmaster.service +++ b/packaging/linux/portmaster.service @@ -30,12 +30,12 @@ ProtectKernelTunables=yes ProtectKernelLogs=yes ProtectControlGroups=yes PrivateDevices=yes -AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid -CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid +AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon +CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon StateDirectory=portmaster # TODO(ppacher): add --disable-software-updates once it's merged and the release process changed. -ExecStart=/usr/bin/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS -ExecStopPost=-/usr/bin/portmaster-start recover-iptables +ExecStart=/usr/bin/portmaster-core --data /opt/safing/portmaster -- $PORTMASTER_ARGS +ExecStopPost=-/usr/bin/portmaster-core recover-iptables [Install] WantedBy=multi-user.target From 9472a1f8f51b62553439081c06b1626680a977c5 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 2 Aug 2024 12:46:12 +0300 Subject: [PATCH 02/11] [desktop] Add update tauri process README --- desktop/tauri/src-tauri/README.md | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 desktop/tauri/src-tauri/README.md diff --git a/desktop/tauri/src-tauri/README.md b/desktop/tauri/src-tauri/README.md new file mode 100644 index 000000000..ad2388106 --- /dev/null +++ b/desktop/tauri/src-tauri/README.md @@ -0,0 +1,39 @@ +# Update Tauri guide + +Check latest versions of tauri packages and update them accordingly: +```toml +[build-dependencies] +tauri-build = { version = "2.0.0-beta.19", features = [] } # Update to latest + +[dependencies] +# Tauri +tauri = { version = "2.0.0-beta.24", features = ["tray-icon", "image-png", "config-json5", "devtools"] } # Update to latest +tauri-plugin-shell = "2.0.0-beta" +tauri-plugin-dialog = "2.0.0-beta" +tauri-plugin-clipboard-manager = "2.0.0-beta" +tauri-plugin-os = "2.0.0-beta" +tauri-plugin-single-instance = "2.0.0-beta" +tauri-plugin-cli = "2.0.0-beta" +tauri-plugin-notification = "2.0.0-beta" +tauri-plugin-log = "2.0.0-beta" +tauri-plugin-window-state = "2.0.0-beta" + +tauri-cli = "2.0.0-beta.21" # Update to latest +``` + +> The plugins will be auto updated based on tauri version. + +Run: +```sh +cargo update +``` + +Update WIX installer template: +1. Get the latests [main.wxs](https://github.com/tauri-apps/tauri/blob/dev/tooling/bundler/src/bundle/windows/templates/main.wxs) template from the repository. +2. Replace the contents of `templates/main_original.wxs` with the repository version. +3. Replace the contents of `templates/main.wsx` and add the fallowing lines at the end of the file, inside the `Product` tag. +```xml + + + +``` From 4c340f7b709a9aabcc460917f450a1c823a83141 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 2 Aug 2024 17:50:15 +0300 Subject: [PATCH 03/11] [desktop] Fix all clippy warning. Add clippy to CI. --- .github/workflows/tauri.yml | 19 +++ Earthfile | 36 +++++ desktop/tauri/src-tauri/Cargo.toml | 5 +- desktop/tauri/src-tauri/src/main.rs | 46 +++--- .../tauri/src-tauri/src/portapi/message.rs | 143 +++++++++--------- .../src-tauri/src/portapi/models/config.rs | 6 +- .../src-tauri/src/portmaster/commands.rs | 6 +- desktop/tauri/src-tauri/src/portmaster/mod.rs | 17 +-- .../src-tauri/src/portmaster/notifications.rs | 8 +- .../tauri/src-tauri/src/service/systemd.rs | 6 +- desktop/tauri/src-tauri/src/traymenu.rs | 57 +++---- desktop/tauri/src-tauri/src/window.rs | 9 +- desktop/tauri/src-tauri/src/xdg/mod.rs | 19 +-- 13 files changed, 209 insertions(+), 168 deletions(-) diff --git a/.github/workflows/tauri.yml b/.github/workflows/tauri.yml index 9ae0da998..a84f74e3d 100644 --- a/.github/workflows/tauri.yml +++ b/.github/workflows/tauri.yml @@ -34,3 +34,22 @@ jobs: - name: Build tauri project run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-ci + + lint: + name: Linter + runs-on: ubuntu-latest + steps: + - uses: earthly/actions-setup@v1 + with: + version: v0.8.0 + - uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build tauri project + run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-lint diff --git a/Earthfile b/Earthfile index 4a81900b4..a000f8559 100644 --- a/Earthfile +++ b/Earthfile @@ -636,6 +636,42 @@ tauri-release: BUILD +tauri-build --target="${arch}" END +tauri-lint: + FROM +tauri-src + + # Clippy (rust linter) will try to build the project before it runs the linter. + # Make sure we have evrything needed to build the project. + ARG target="x86_64-unknown-linux-gnu" + + # if we want tauri to create the installer bundles we also need to provide all external binaries + # we need to do some magic here because tauri expects the binaries to include the rust target tripple. + # We already know that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING + # function from below to parse the triple and guess wich GOOS and GOARCH we need. + RUN mkdir /tmp/gobuild + RUN mkdir ./binaries + + DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" + RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" + + # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling + # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. + # See documentation for externalBins for more information on how tauri searches for the binaries. + COPY (+go-build/output --CMDS="portmaster-start portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild + + # Place them in the correct folder with the rust target tripple attached. + FOR bin IN $(ls /tmp/gobuild) + # ${bin$.*} does not work in SET commands unfortunately so we use a shell + # snippet here: + RUN set -e ; \ + dest="./binaries/${bin}-${target}" ; \ + if [ -z "${bin##*.exe}" ]; then \ + dest="./binaries/${bin%.*}-${target}.exe" ; \ + fi ; \ + cp "/tmp/gobuild/${bin}" "${dest}" ; + END + DO rust+SET_CACHE_MOUNTS_ENV + RUN cargo clippy --all-targets --all-features -- -D warnings + kext-build: FROM ${rust_builder_image} diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index 3dcfdef56..f89ca02fb 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -7,7 +7,7 @@ license = "" repository = "" default-run = "portmaster" edition = "2021" -rust-version = "1.60" +rust-version = "1.64" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -80,3 +80,6 @@ ctor = "0.2.6" # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. # DO NOT REMOVE!! custom-protocol = [ "tauri/custom-protocol" ] + +[package.metadata.clippy] +allow = ["clippy::collapsible_else_if"] \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 77c1d0557..c0adcb5c7 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -48,7 +48,7 @@ impl portmaster::Handler for WsHandler { "main-handler".to_string() } - fn on_connect(&mut self, cli: portapi::client::PortAPI) -> () { + fn on_connect(&mut self, cli: portapi::client::PortAPI) { info!("connection established, creating main window"); // we successfully connected to Portmaster. Set is_first_connect to false @@ -116,11 +116,11 @@ fn show_webview_not_installed_dialog() -> i32 { } } - return FALLBACK_TO_OLD_UI_EXIT_CODE; + FALLBACK_TO_OLD_UI_EXIT_CODE } fn main() { - if let Err(_) = tauri::webview_version() { + if tauri::webview_version().is_err() { std::process::exit(show_webview_not_installed_dialog()); } @@ -251,8 +251,8 @@ fn main() { .build(tauri::generate_context!()) .expect("error while running tauri application"); - app.run(|handle, e| match e { - RunEvent::WindowEvent { label, event, .. } => { + app.run(|handle, e| { + if let RunEvent::WindowEvent { label, event, .. } = e { if label != "main" { // We only have one window at most so any other label is unexpected return; @@ -266,32 +266,22 @@ fn main() { // // Note: the above javascript does NOT trigger the CloseRequested event so // there's no need to handle that case here. - // - match event { - WindowEvent::CloseRequested { api, .. } => { - debug!( - "window (label={}) close request received, forwarding to user-interface.", - label - ); - - api.prevent_close(); - if let Some(window) = handle.get_webview_window(label.as_str()) { - let result = window.emit("exit-requested", ""); - if let Err(err) = result { - error!("failed to emit event: {}", err.to_string()); - } - } else { - error!("window was None"); + if let WindowEvent::CloseRequested { api, .. } = event { + debug!( + "window (label={}) close request received, forwarding to user-interface.", + label + ); + + api.prevent_close(); + if let Some(window) = handle.get_webview_window(label.as_str()) { + let result = window.emit("exit-requested", ""); + if let Err(err) = result { + error!("failed to emit event: {}", err.to_string()); } + } else { + error!("window was None"); } - _ => {} } } - - // TODO(vladimir): why was this needed? - // RunEvent::ExitRequested { api, .. } => { - // api.prevent_exit(); - // } - _ => {} }); } diff --git a/desktop/tauri/src-tauri/src/portapi/message.rs b/desktop/tauri/src-tauri/src/portapi/message.rs index 46eb7c77a..6230bc55c 100644 --- a/desktop/tauri/src-tauri/src/portapi/message.rs +++ b/desktop/tauri/src-tauri/src/portapi/message.rs @@ -26,48 +26,46 @@ pub enum MessageError { InvalidPayload(#[from] serde_json::Error), } - /// Payload defines the payload type and content of a PortAPI message. -/// +/// /// For the time being, only JSON payloads (indicated by a prefixed 'J' of the payload content) /// is directly supported in `Payload::parse()`. -/// +/// /// For other payload types (like CBOR, BSON, ...) it's the user responsibility to figure out /// appropriate decoding from the `Payload::UNKNOWN` variant. #[derive(PartialEq, Debug, Clone)] pub enum Payload { - JSON(String), - UNKNOWN(String), + Json(String), + Unknown(String), } /// ParseError is returned from `Payload::parse()`. #[derive(Debug, Error)] pub enum ParseError { #[error(transparent)] - JSON(#[from] serde_json::Error), + Json(#[from] serde_json::Error), #[error("unknown error while parsing")] - UNKNOWN + Unknown, } - impl Payload { /// Parse the payload into T. - /// + /// /// Only JSON parsing is supported for now. See [Payload] for more information. - pub fn parse<'a, T>(self: &'a Self) -> std::result::Result + pub fn parse<'a, T>(&'a self) -> std::result::Result where - T: serde::de::Deserialize<'a> { - + T: serde::de::Deserialize<'a>, + { match self { - Payload::JSON(blob) => Ok(serde_json::from_str::(blob.as_str())?), - Payload::UNKNOWN(_) => Err(ParseError::UNKNOWN), + Payload::Json(blob) => Ok(serde_json::from_str::(blob.as_str())?), + Payload::Unknown(_) => Err(ParseError::Unknown), } } } /// Supports creating a Payload instance from a String. -/// +/// /// See [Payload] for more information. impl std::convert::From for Payload { fn from(value: String) -> Payload { @@ -77,10 +75,10 @@ impl std::convert::From for Payload { match first { Some(c) => match c { - 'J' => Payload::JSON(rest), - _ => Payload::UNKNOWN(value), + 'J' => Payload::Json(rest), + _ => Payload::Unknown(value), }, - None => Payload::UNKNOWN("".to_string()) + None => Payload::Unknown("".to_string()), } } } @@ -89,10 +87,10 @@ impl std::convert::From for Payload { impl std::fmt::Display for Payload { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Payload::JSON(payload) => { + Payload::Json(payload) => { write!(f, "J{}", payload) - }, - Payload::UNKNOWN(payload) => { + } + Payload::Unknown(payload) => { write!(f, "{}", payload) } } @@ -100,9 +98,9 @@ impl std::fmt::Display for Payload { } /// Message is an internal representation of a PortAPI message. -/// Users should more likely use `portapi::types::Request` and `portapi::types::Response` +/// Users should more likely use `portapi::types::Request` and `portapi::types::Response` /// instead of directly using `Message`. -/// +/// /// The struct is still public since it might be useful for debugging or to implement new /// commands not yet supported by the `portapi::types` crate. #[derive(PartialEq, Debug, Clone)] @@ -115,23 +113,23 @@ pub struct Message { /// Implementation to marshal a PortAPI message into it's wire-format representation /// (which is a string). -/// +/// /// Note that this conversion does not check for invalid messages! impl std::convert::From for String { fn from(value: Message) -> Self { - let mut result = "".to_owned(); + let mut result = String::new(); result.push_str(value.id.to_string().as_str()); - result.push_str("|"); + result.push('|'); result.push_str(&value.cmd); if let Some(key) = value.key { - result.push_str("|"); + result.push('|'); result.push_str(key.as_str()); } if let Some(payload) = value.payload { - result.push_str("|"); + result.push('|'); result.push_str(payload.to_string().as_str()) } @@ -141,15 +139,15 @@ impl std::convert::From for String { /// An implementation for `String::parse()` to convert a wire-format representation /// of a PortAPI message to a Message instance. -/// +/// /// Any errors returned from `String::parse()` will be of type `MessageError` impl std::str::FromStr for Message { type Err = MessageError; fn from_str(line: &str) -> Result { - let parts = line.split("|").collect::>(); + let parts = line.split('|').collect::>(); - let id = match parts.get(0) { + let id = match parts.first() { Some(s) => match (*s).parse::() { Ok(id) => Ok(id), Err(_) => Err(MessageError::InvalidID), @@ -163,18 +161,15 @@ impl std::str::FromStr for Message { }? .to_string(); - let key = parts.get(2) - .and_then(|key| Some(key.to_string())); + let key = parts.get(2).map(|key| key.to_string()); + let payload: Option = parts.get(3).map(|p| p.to_string().into()); - let payload : Option = parts.get(3) - .and_then(|p| Some(p.to_string().into())); - - return Ok(Message { + Ok(Message { id, cmd, key, - payload: payload - }); + payload, + }) } } @@ -191,67 +186,79 @@ mod tests { #[test] fn payload_to_string() { - let p = Payload::JSON("{}".to_string()); + let p = Payload::Json("{}".to_string()); assert_eq!(p.to_string(), "J{}"); - let p = Payload::UNKNOWN("some unknown content".to_string()); + let p = Payload::Unknown("some unknown content".to_string()); assert_eq!(p.to_string(), "some unknown content"); } #[test] fn payload_from_string() { let p: Payload = "J{}".to_string().into(); - assert_eq!(p, Payload::JSON("{}".to_string())); + assert_eq!(p, Payload::Json("{}".to_string())); let p: Payload = "some unknown content".to_string().into(); - assert_eq!(p, Payload::UNKNOWN("some unknown content".to_string())); + assert_eq!(p, Payload::Unknown("some unknown content".to_string())); } #[test] fn payload_parse() { let p: Payload = "J{\"a\": 100, \"s\": \"string\"}".to_string().into(); - let t: Test = p.parse() - .expect("Expected payload parsing to work"); + let t: Test = p.parse().expect("Expected payload parsing to work"); - assert_eq!(t, Test{ - a: 100, - s: "string".to_string(), - }); + assert_eq!( + t, + Test { + a: 100, + s: "string".to_string(), + } + ); } #[test] fn parse_message() { - let m = "10|insert|some:key|J{}".parse::() + let m = "10|insert|some:key|J{}" + .parse::() .expect("Expected message to parse"); - assert_eq!(m, Message{ - id: 10, - cmd: "insert".to_string(), - key: Some("some:key".to_string()), - payload: Some(Payload::JSON("{}".to_string())), - }); + assert_eq!( + m, + Message { + id: 10, + cmd: "insert".to_string(), + key: Some("some:key".to_string()), + payload: Some(Payload::Json("{}".to_string())), + } + ); - let m = "1|done".parse::() + let m = "1|done" + .parse::() .expect("Expected message to parse"); - assert_eq!(m, Message{ - id: 1, - cmd: "done".to_string(), - key: None, - payload: None - }); + assert_eq!( + m, + Message { + id: 1, + cmd: "done".to_string(), + key: None, + payload: None + } + ); - let m = "".parse::() - .expect_err("Expected parsing to fail"); - if let MessageError::InvalidID = m {} else { + let m = "".parse::().expect_err("Expected parsing to fail"); + if let MessageError::InvalidID = m { + } else { panic!("unexpected error value: {}", m) } - let m = "1".parse::() + let m = "1" + .parse::() .expect_err("Expected parsing to fail"); - if let MessageError::MissingCommand = m {} else { + if let MessageError::MissingCommand = m { + } else { panic!("unexpected error value: {}", m) } } diff --git a/desktop/tauri/src-tauri/src/portapi/models/config.rs b/desktop/tauri/src-tauri/src/portapi/models/config.rs index e29474a81..72b8a1d87 100644 --- a/desktop/tauri/src-tauri/src/portapi/models/config.rs +++ b/desktop/tauri/src-tauri/src/portapi/models/config.rs @@ -1,5 +1,5 @@ -use serde::*; use super::super::message::Payload; +use serde::*; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct BooleanValue { @@ -13,6 +13,6 @@ impl TryInto for BooleanValue { fn try_into(self) -> Result { let str = serde_json::to_string(&self)?; - Ok(Payload::JSON(str)) + Ok(Payload::Json(str)) } -} \ No newline at end of file +} diff --git a/desktop/tauri/src-tauri/src/portmaster/commands.rs b/desktop/tauri/src-tauri/src/portmaster/commands.rs index d8e9db08d..39c057880 100644 --- a/desktop/tauri/src-tauri/src/portmaster/commands.rs +++ b/desktop/tauri/src-tauri/src/portmaster/commands.rs @@ -86,7 +86,7 @@ pub fn get_app_info( matching_path, }; - if id == "" { + if id.is_empty() { id = uuid::Uuid::new_v4().to_string() } let cloned = id.clone(); @@ -137,7 +137,7 @@ pub fn get_app_info( pub fn get_service_manager_status(window: Window, response_id: String) -> Result { let mut id = response_id; - if id == "" { + if id.is_empty() { id = uuid::Uuid::new_v4().to_string(); } let cloned = id.clone(); @@ -161,7 +161,7 @@ pub fn get_service_manager_status(window: Window, response_id: St pub fn start_service(window: Window, response_id: String) -> Result { let mut id = response_id; - if id == "" { + if id.is_empty() { id = uuid::Uuid::new_v4().to_string(); } let cloned = id.clone(); diff --git a/desktop/tauri/src-tauri/src/portmaster/mod.rs b/desktop/tauri/src-tauri/src/portmaster/mod.rs index 80049860e..0941f306b 100644 --- a/desktop/tauri/src-tauri/src/portmaster/mod.rs +++ b/desktop/tauri/src-tauri/src/portmaster/mod.rs @@ -32,14 +32,13 @@ use std::{ }; use log::{debug, error}; -use serde; use std::sync::Mutex; use tauri::{AppHandle, Emitter, Manager, Runtime}; -const PORTMASTER_BASE_URL: &'static str = "http://127.0.0.1:817/api/v1/"; +const PORTMASTER_BASE_URL: &str = "http://127.0.0.1:817/api/v1/"; pub trait Handler { - fn on_connect(&mut self, cli: PortAPI) -> (); + fn on_connect(&mut self, cli: PortAPI); fn on_disconnect(&mut self); fn name(&self) -> String; } @@ -81,10 +80,7 @@ impl PortmasterInterface { let map = self.state.lock(); if let Ok(map) = map { - match map.get(&key) { - Some(value) => Some(value.clone()), - None => None, - } + map.get(&key).cloned() } else { None } @@ -129,11 +125,8 @@ impl PortmasterInterface { /// Returns the current portapi client. pub fn get_api(&self) -> Option { - if let Ok(mut api) = self.api.lock() { - match &mut *api { - Some(api) => Some(api.clone()), - None => None, - } + if let Ok(api) = self.api.lock() { + (*api).clone() } else { None } diff --git a/desktop/tauri/src-tauri/src/portmaster/notifications.rs b/desktop/tauri/src-tauri/src/portmaster/notifications.rs index bf851230b..452db9962 100644 --- a/desktop/tauri/src-tauri/src/portmaster/notifications.rs +++ b/desktop/tauri/src-tauri/src/portmaster/notifications.rs @@ -29,13 +29,13 @@ pub async fn notification_handler(cli: PortAPI) { } // Skip if this action has already been acted on - if n.selected_action_id != "" { + if n.selected_action_id.is_empty() { return; } show_notification(&cli, key, n).await; } Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse notification: {}", err); } _ => { @@ -81,7 +81,7 @@ pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) { let _ = cli_clone .request(Request::Update( key, - Payload::JSON( + Payload::Json( json!({ "SelectedActionID": value }) @@ -125,7 +125,7 @@ pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) { let _ = cli .request(Request::Update( key, - Payload::JSON( + Payload::Json( json!({ "SelectedActionID": value }) diff --git a/desktop/tauri/src-tauri/src/service/systemd.rs b/desktop/tauri/src-tauri/src/service/systemd.rs index 770a9139d..c316f3755 100644 --- a/desktop/tauri/src-tauri/src/service/systemd.rs +++ b/desktop/tauri/src-tauri/src/service/systemd.rs @@ -26,7 +26,7 @@ impl From for ServiceManagerError { .ok() .filter(|s| !s.trim().is_empty()) }) - .unwrap_or_else(|| format!("Failed to run `systemctl`")); + .unwrap_or_else(|| "Failed to run `systemctl`".to_string()); ServiceManagerError::Other(output.status, msg) } @@ -231,11 +231,11 @@ fn trim_newline(s: &mut String) { } fn get_sudo_cmd() -> std::result::Result { - if let Ok(_) = fs::metadata("/usr/bin/pkexec") { + if fs::metadata("/usr/bin/pkexec").is_ok() { return Ok(SudoCommand::Pkexec); } - if let Ok(_) = fs::metadata("/usr/bin/gksudo") { + if fs::metadata("/usr/bin/gksudo").is_ok() { return Ok(SudoCommand::Gksu); } diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 9d775475d..c947bb9ed 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -46,13 +46,11 @@ enum IconColor { static CURRENT_ICON_COLOR: RwLock = RwLock::new(IconColor::Red); pub static USER_THEME: RwLock = RwLock::new(dark_light::Mode::Default); -lazy_static! { - static ref SPN_STATUS: Mutex>> = Mutex::new(None); - static ref SPN_BUTTON: Mutex>> = Mutex::new(None); - static ref GLOBAL_STATUS: Mutex>> = Mutex::new(None); -} +static SPN_STATUS: Mutex>> = Mutex::new(None); +static SPN_BUTTON: Mutex>> = Mutex::new(None); +static GLOBAL_STATUS: Mutex>> = Mutex::new(None); -const PM_TRAY_ICON_ID: &'static str = "pm_icon"; +const PM_TRAY_ICON_ID: &str = "pm_icon"; // Icons @@ -64,9 +62,9 @@ fn get_theme_mode() -> dark_light::Mode { } fn get_green_icon() -> &'static [u8] { - const LIGHT_GREEN_ICON: &'static [u8] = + const LIGHT_GREEN_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_green_64.png"); - const DARK_GREEN_ICON: &'static [u8] = + const DARK_GREEN_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_green_64.png"); match get_theme_mode() { @@ -76,9 +74,9 @@ fn get_green_icon() -> &'static [u8] { } fn get_blue_icon() -> &'static [u8] { - const LIGHT_BLUE_ICON: &'static [u8] = + const LIGHT_BLUE_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_blue_64.png"); - const DARK_BLUE_ICON: &'static [u8] = + const DARK_BLUE_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_blue_64.png"); match get_theme_mode() { dark_light::Mode::Light => DARK_BLUE_ICON, @@ -87,20 +85,27 @@ fn get_blue_icon() -> &'static [u8] { } fn get_red_icon() -> &'static [u8] { - const LIGHT_RED_ICON: &'static [u8] = + const LIGHT_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_red_64.png"); const DARK_RED_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); match get_theme_mode() { + const DARK_RED_ICON: &'static [u8] = + include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); + let mode = dark_light::detect(); + match mode { + const DARK_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); + let mode = dark_light::detect(); + match mode { dark_light::Mode::Light => DARK_RED_ICON, _ => LIGHT_RED_ICON, } } fn get_yellow_icon() -> &'static [u8] { - const LIGHT_YELLOW_ICON: &'static [u8] = + const LIGHT_YELLOW_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_yellow_64.png"); - const DARK_YELLOW_ICON: &'static [u8] = + const DARK_YELLOW_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_yellow_64.png"); match get_theme_mode() { dark_light::Mode::Light => DARK_YELLOW_ICON, @@ -257,10 +262,8 @@ pub fn setup_tray_menu( button_state, } = event { - if let MouseButton::Left = button { - if let MouseButtonState::Down = button_state { - let _ = open_window(tray.app_handle()); - } + if let (MouseButton::Left, MouseButtonState::Down) = (button, button_state) { + let _ = open_window(tray.app_handle()); } } }) @@ -269,20 +272,20 @@ pub fn setup_tray_menu( } pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_status: String) { - // iterate over the subsytems and check if there's a module failure - let failure = subsystems - .values() - .into_iter() - .map(|s| &s.module_status) - .fold((subsystem::FAILURE_NONE, "".to_string()), |mut acc, s| { + // iterate over the subsystems and check if there's a module failure + let failure = subsystems.values().map(|s| &s.module_status).fold( + (subsystem::FAILURE_NONE, "".to_string()), + |mut acc, s| { for m in s { if m.failure_status > acc.0 { acc = (m.failure_status, m.failure_msg.clone()) } } acc - }); + }, + ); + #[allow(clippy::collapsible_else_if)] if failure.0 == subsystem::FAILURE_NONE { if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) { _ = global_status.set_text("Status: Secured"); @@ -405,7 +408,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse subsystem: {}", err); } _ => { @@ -437,7 +440,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse spn status value: {}", err) }, _ => { @@ -466,7 +469,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_spn_ui_state(value.value.unwrap_or(false)); }, Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse config value: {}", err) }, _ => { diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs index b5e526a4c..6cdcfe086 100644 --- a/desktop/tauri/src-tauri/src/window.rs +++ b/desktop/tauri/src-tauri/src/window.rs @@ -6,9 +6,8 @@ use tauri::{ use crate::{portmaster::PortmasterExt, traymenu}; -const LIGHT_PM_ICON: &'static [u8] = - include_bytes!("../../../../assets/data/icons/pm_light_512.png"); -const DARK_PM_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png"); +const LIGHT_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_512.png"); +const DARK_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png"); /// Either returns the existing "main" window or creates a new one. /// @@ -54,7 +53,7 @@ pub fn create_main_window(app: &AppHandle) -> Result { set_window_icon(&window); #[cfg(debug_assertions)] - if let Ok(_) = std::env::var("TAURI_SHOW_IMMEDIATELY") { + if std::env::var("TAURI_SHOW_IMMEDIATELY").is_ok() { debug!("[tauri] TAURI_SHOW_IMMEDIATELY is set, opening window"); if let Err(err) = window.show() { @@ -92,7 +91,7 @@ pub fn close_splash_window(app: &AppHandle) -> Result<()> { let _ = window.hide(); return window.destroy(); } - return Err(tauri::Error::WindowNotFound); + Err(tauri::Error::WindowNotFound) } pub fn hide_splash_window(app: &AppHandle) -> Result<()> { diff --git a/desktop/tauri/src-tauri/src/xdg/mod.rs b/desktop/tauri/src-tauri/src/xdg/mod.rs index 8ff6fd11c..3db679421 100644 --- a/desktop/tauri/src-tauri/src/xdg/mod.rs +++ b/desktop/tauri/src-tauri/src/xdg/mod.rs @@ -18,7 +18,6 @@ use std::{ }; use thiserror::Error; -use dirs; use ini::{Ini, ParseOption}; static mut GTK_DEFAULT_THEME: Option<*mut GtkIconTheme> = None; @@ -146,7 +145,7 @@ pub fn get_app_info(process_info: ProcessInfo) -> Result { .unwrap() .insert(process_info.exec_path, None); - Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into()) + Err(Error::new(ErrorKind::NotFound, "failed to find app info".to_string()).into()) } else { // sort matches by length matches.sort_by(|a, b| a.1.cmp(&b.1)); @@ -178,7 +177,7 @@ pub fn get_app_info(process_info: ProcessInfo) -> Result { }; } - Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into()) + Err(Error::new(ErrorKind::NotFound, "failed to find app info".to_string()).into()) } } @@ -336,7 +335,7 @@ fn try_get_app_info( } } - if result.len() > 0 { + if !result.is_empty() { Ok(result) } else { Err(Error::new(ErrorKind::NotFound, "no matching .desktop files found").into()) @@ -393,7 +392,7 @@ fn get_icon_as_png_dataurl(name: &str, size: i8) -> Result<(String, String)> { // - network // name_without_ext - .split("-") + .split('-') .for_each(|part| icons.push(part)); for name in icons { @@ -554,15 +553,7 @@ mod tests { matching_path: bin.clone(), pid: 0, }) - .expect( - format!( - "expected to find app info for {} ({})", - bin, - cmd.to_string() - ) - .as_str(), - ); - + .unwrap_or_else(|_| panic!("expected to find app info for {} ({})", bin, cmd)); let empty_string = String::from(""); // just make sure all fields are populated From 556e5dd92168a8b8af4e9bed0d07c5b668b8d445 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 5 Aug 2024 14:58:35 +0300 Subject: [PATCH 04/11] [desktop] tauri remove some global variables. --- desktop/tauri/src-tauri/src/traymenu.rs | 88 ++++++++++++------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index c947bb9ed..600af46d3 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -4,11 +4,12 @@ use std::sync::{Mutex, RwLock}; use std::{collections::HashMap, sync::atomic::Ordering}; use log::{debug, error}; +use tauri::menu::{Menu, MenuItemKind}; use tauri::tray::{MouseButton, MouseButtonState}; -use tauri::Manager; +use tauri::Runtime; use tauri::{ image::Image, - menu::{MenuBuilder, MenuItem, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, + menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, tray::{TrayIcon, TrayIconBuilder}, Wry, }; @@ -46,9 +47,9 @@ enum IconColor { static CURRENT_ICON_COLOR: RwLock = RwLock::new(IconColor::Red); pub static USER_THEME: RwLock = RwLock::new(dark_light::Mode::Default); -static SPN_STATUS: Mutex>> = Mutex::new(None); -static SPN_BUTTON: Mutex>> = Mutex::new(None); -static GLOBAL_STATUS: Mutex>> = Mutex::new(None); +static SPN_STATUS_KEY: &str = "spn_status"; +static SPN_BUTTON_KEY: &str = "spn_toggle"; +static GLOBAL_STATUS_KEY: &str = "global_status"; const PM_TRAY_ICON_ID: &str = "pm_icon"; @@ -135,13 +136,9 @@ pub fn setup_tray_menu( .enabled(false) .build(app) .unwrap(); - { - let mut button_ref = GLOBAL_STATUS.lock()?; - *button_ref = Some(global_status.clone()); - } // Setup SPN status - let spn_status = MenuItemBuilder::with_id("spn_status", "SPN: Disabled") + let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, "SPN: Disabled") .enabled(false) .build(app) .unwrap(); @@ -149,15 +146,10 @@ pub fn setup_tray_menu( let mut button_ref = SPN_STATUS.lock()?; *button_ref = Some(spn_status.clone()); } - // Setup SPN button - let spn = MenuItemBuilder::with_id("spn_toggle", "Enable SPN") + let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, "Enable SPN") .build(app) .unwrap(); - { - let mut button_ref = SPN_BUTTON.lock()?; - *button_ref = Some(spn.clone()); - } let system_theme = MenuItemBuilder::with_id("system_theme", "System") .build(app) @@ -185,7 +177,7 @@ pub fn setup_tray_menu( &global_status, &PredefinedMenuItem::separator(app)?, &spn_status, - &spn, + &spn_button, &PredefinedMenuItem::separator(app)?, &theme_menu, &PredefinedMenuItem::separator(app)?, @@ -271,7 +263,12 @@ pub fn setup_tray_menu( Ok(icon) } -pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_status: String) { +pub fn update_icon( + icon: AppIcon, + menu: Option>, + subsystems: HashMap, + spn_status: String, +) { // iterate over the subsystems and check if there's a module failure let failure = subsystems.values().map(|s| &s.module_status).fold( (subsystem::FAILURE_NONE, "".to_string()), @@ -285,14 +282,13 @@ pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_st }, ); - #[allow(clippy::collapsible_else_if)] - if failure.0 == subsystem::FAILURE_NONE { - if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) { - _ = global_status.set_text("Status: Secured"); - } - } else { - if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) { - _ = global_status.set_text(format!("Status: {}", failure.1)); + if let Some(menu) = menu { + if let Some(MenuItemKind::MenuItem(global_status)) = menu.get(GLOBAL_STATUS_KEY) { + if failure.0 == subsystem::FAILURE_NONE { + _ = global_status.set_text("Status: Secured"); + } else { + _ = global_status.set_text(format!("Status: {}", failure.1)); + } } } @@ -405,7 +401,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { Ok(n) => { subsystems.insert(n.id.clone(), n); - update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => { @@ -437,7 +433,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { debug!("SPN status update: {}", value.status); spn_status = value.status.clone(); - update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => { @@ -466,7 +462,9 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { if let Some((_, payload)) = res { match payload.parse::() { Ok(value) => { - update_spn_ui_state(value.value.unwrap_or(false)); + if let Some(menu) = app.menu() { + update_spn_ui_state(menu, value.value.unwrap_or(false)); + } }, Err(err) => match err { ParseError::Json(err) => { @@ -563,23 +561,23 @@ fn save_theme(app: &tauri::AppHandle, mode: dark_light::Mode) { } Err(err) => error!("failed to load config file: {}", err), } + if let Some(menu) = app.menu() { + update_spn_ui_state(menu, false); + } + _ = icon.set_icon(Some(Image::from_bytes(get_red_icon()).unwrap())); } -fn update_spn_ui_state(enabled: bool) { - let mut spn_status = SPN_STATUS.lock().unwrap(); - let Some(spn_status_ref) = &mut *spn_status else { - return; - }; - let mut spn_btn = SPN_BUTTON.lock().unwrap(); - let Some(spn_btn_ref) = &mut *spn_btn else { - return; - }; - if enabled { - _ = spn_status_ref.set_text("SPN: Connected"); - _ = spn_btn_ref.set_text("Disable SPN"); - } else { - _ = spn_status_ref.set_text("SPN: Disabled"); - _ = spn_btn_ref.set_text("Enable SPN"); +fn update_spn_ui_state(menu: Menu, enabled: bool) { + if let (Some(MenuItemKind::MenuItem(spn_status)), Some(MenuItemKind::MenuItem(spn_btn))) = + (menu.get(SPN_STATUS_KEY), menu.get(SPN_BUTTON_KEY)) + { + if enabled { + _ = spn_status.set_text("SPN: Connected"); + _ = spn_btn.set_text("Disable SPN"); + } else { + _ = spn_status.set_text("SPN: Disabled"); + _ = spn_btn.set_text("Enable SPN"); + } + SPN_STATE.store(enabled, Ordering::Release); } - SPN_STATE.store(enabled, Ordering::SeqCst); } From abf444630b25289baf28f3eedd041ffcecf4553c Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 13 Aug 2024 18:13:34 +0300 Subject: [PATCH 05/11] [WIP] New updater --- Earthfile | 7 +- base/updater/get.go | 95 +++---- desktop/tauri/src-tauri/tauri.conf.json5 | 1 + service/updates/bundle.go | 247 +++++++++++++++++ service/updates/index.go | 110 ++++++++ service/updates/main.go | 339 ++++++++++------------- service/updates/module.go | 117 +++++++- service/updates/registry.go | 1 + service/updates/upgrader.go | 12 +- 9 files changed, 671 insertions(+), 258 deletions(-) create mode 100644 service/updates/bundle.go create mode 100644 service/updates/index.go create mode 100644 service/updates/registry.go diff --git a/Earthfile b/Earthfile index a000f8559..c2c43497c 100644 --- a/Earthfile +++ b/Earthfile @@ -462,17 +462,14 @@ tauri-build: # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. # See documentation for externalBins for more information on how tauri searches for the binaries. - COPY (+go-build/output --CMDS="portmaster-start portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild + COPY (+go-build/output --CMDS="portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild # Place them in the correct folder with the rust target tripple attached. FOR bin IN $(ls /tmp/gobuild) # ${bin$.*} does not work in SET commands unfortunately so we use a shell # snippet here: RUN set -e ; \ - dest="./binaries/${bin}-${target}" ; \ - if [ -z "${bin##*.exe}" ]; then \ - dest="./binaries/${bin%.*}-${target}.exe" ; \ - fi ; \ + dest="./binaries/${bin}" ; \ cp "/tmp/gobuild/${bin}" "${dest}" ; END diff --git a/base/updater/get.go b/base/updater/get.go index d50d28b36..7b54c8e66 100644 --- a/base/updater/get.go +++ b/base/updater/get.go @@ -1,12 +1,8 @@ package updater import ( - "context" "errors" "fmt" - "net/http" - - "github.com/safing/portmaster/base/log" ) // Errors returned by the updater package. @@ -19,59 +15,60 @@ var ( // GetFile returns the selected (mostly newest) file with the given // identifier or an error, if it fails. func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { - reg.RLock() - res, ok := reg.resources[identifier] - reg.RUnlock() - if !ok { - return nil, ErrNotFound - } + return nil, fmt.Errorf("invalid file: %s", identifier) + // reg.RLock() + // res, ok := reg.resources[identifier] + // reg.RUnlock() + // if !ok { + // return nil, ErrNotFound + // } - file := res.GetFile() - // check if file is available locally - if file.version.Available { - file.markActiveWithLocking() + // file := res.GetFile() + // // check if file is available locally + // if file.version.Available { + // file.markActiveWithLocking() - // Verify file, if configured. - _, err := file.Verify() - if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { - // TODO: If verification is required, try deleting the resource and downloading it again. - return nil, fmt.Errorf("failed to verify file: %w", err) - } + // // Verify file, if configured. + // _, err := file.Verify() + // if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { + // // TODO: If verification is required, try deleting the resource and downloading it again. + // return nil, fmt.Errorf("failed to verify file: %w", err) + // } - return file, nil - } + // return file, nil + // } - // check if online - if !reg.Online { - return nil, ErrNotAvailableLocally - } + // // check if online + // if !reg.Online { + // return nil, ErrNotAvailableLocally + // } - // check download dir - err := reg.tmpDir.Ensure() - if err != nil { - return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) - } + // // check download dir + // err := reg.tmpDir.Ensure() + // if err != nil { + // return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) + // } - // Start registry operation. - reg.state.StartOperation(StateFetching) - defer reg.state.EndOperation() + // // Start registry operation. + // reg.state.StartOperation(StateFetching) + // defer reg.state.EndOperation() - // download file - log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) - client := &http.Client{} - for tries := range 5 { - err = reg.fetchFile(context.TODO(), client, file.version, tries) - if err != nil { - log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) - } else { - file.markActiveWithLocking() + // // download file + // log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) + // client := &http.Client{} + // for tries := range 5 { + // err = reg.fetchFile(context.TODO(), client, file.version, tries) + // if err != nil { + // log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) + // } else { + // file.markActiveWithLocking() - // TODO: We just download the file - should we verify it again? - return file, nil - } - } - log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) - return nil, err + // // TODO: We just download the file - should we verify it again? + // return file, nil + // } + // } + // log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) + // return nil, err } // GetVersion returns the selected version of the given identifier. diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index 7363808c1..beba5cd79 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -73,6 +73,7 @@ "release": "1", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", diff --git a/service/updates/bundle.go b/service/updates/bundle.go new file mode 100644 index 000000000..5214db1b4 --- /dev/null +++ b/service/updates/bundle.go @@ -0,0 +1,247 @@ +package updates + +import ( + "archive/zip" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/safing/portmaster/base/log" +) + +const MaxUnpackSize = 1 << 30 // 2^30 == 1GB + +type Artifact struct { + Filename string `json:"Filename"` + SHA256 string `json:"SHA256"` + URLs []string `json:"URLs"` + Platform string `json:"Platform,omitempty"` + Unpack string `json:"Unpack,omitempty"` + Version string `json:"Version,omitempty"` +} + +type Bundle struct { + Name string `json:"Bundle"` + Version string `json:"Version"` + Published time.Time `json:"Published"` + Artifacts []Artifact `json:"Artifacts"` +} + +func (bundle Bundle) downloadAndVerify(dataDir string) { + client := http.Client{} + for _, artifact := range bundle.Artifacts { + + filePath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + // TODO(vladimir): is this needed? + _ = os.MkdirAll(filepath.Dir(filePath), os.ModePerm) + + // Check file is already downloaded and valid. + exists, err := checkIfFileIsValid(filePath, artifact) + if exists { + log.Debugf("file already download: %s", filePath) + continue + } else if err != nil { + log.Errorf("error while checking old download: %s", err) + } + + // Download artifact + err = processArtifact(&client, artifact, filePath) + if err != nil { + log.Errorf("updates: %s", err) + } + } +} + +func (bundle Bundle) Verify(dataDir string) error { + for _, artifact := range bundle.Artifacts { + artifactPath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + file, err := os.Open(artifactPath) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", artifactPath, err) + } + defer func() { _ = file.Close() }() + + isValid, err := checkIfFileIsValid(artifactPath, artifact) + if err != nil { + return err + } + + if !isValid { + return fmt.Errorf("file is not valid: %s", artifact.Filename) + } + } + + return nil +} + +func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { + // Check if file already exists + file, err := os.Open(filename) + if err != nil { + //nolint:nilerr + return false, nil + } + defer func() { _ = file.Close() }() + + providedHash, err := hex.DecodeString(artifact.SHA256) + if err != nil || len(providedHash) != sha256.Size { + return false, fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) + } + + // Calculate hash of the file + fileHash := sha256.New() + if _, err := io.Copy(fileHash, file); err != nil { + return false, fmt.Errorf("failed to read file: %w", err) + } + hashInBytes := fileHash.Sum(nil) + if !bytes.Equal(providedHash, hashInBytes) { + return false, fmt.Errorf("file exist but the hash does not match: %s", filename) + } + return true, nil +} + +func processArtifact(client *http.Client, artifact Artifact, filePath string) error { + providedHash, err := hex.DecodeString(artifact.SHA256) + if err != nil || len(providedHash) != sha256.Size { + return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) + } + + // Download + content, err := downloadFile(client, artifact.URLs) + if err != nil { + return fmt.Errorf("failed to download artifact: %w", err) + } + + // Decompress + if artifact.Unpack != "" { + content, err = unpack(artifact.Unpack, content) + if err != nil { + return fmt.Errorf("failed to decompress artifact: %w", err) + } + } + + // Verify + hash := sha256.Sum256(content) + if !bytes.Equal(providedHash, hash[:]) { + // FIXME(vladimir): just for testing. Make it an error before commit. + err = fmt.Errorf("failed to verify artifact: %s", artifact.Filename) + log.Debugf("updates: %s", err) + } + + // Save + tmpFilename := fmt.Sprintf("%s.download", filePath) + file, err := os.Create(tmpFilename) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + _, err = file.Write(content) + if err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + + // Rename + err = os.Rename(tmpFilename, filePath) + if err != nil { + return fmt.Errorf("failed to rename file: %w", err) + } + + return nil +} + +func downloadFile(client *http.Client, urls []string) ([]byte, error) { + for _, url := range urls { + // Try to make the request + resp, err := client.Get(url) + if err != nil { + log.Warningf("failed a get file request to: %s", err) + continue + } + defer func() { _ = resp.Body.Close() }() + + // Check if the server returned an error + if resp.StatusCode != http.StatusOK { + log.Warningf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status) + continue + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + log.Warningf("failed to read body of response: %s", err) + continue + } + return content, nil + } + + return nil, fmt.Errorf("failed to download file from the provided urls") +} + +func unpack(cType string, fileBytes []byte) ([]byte, error) { + switch cType { + case "zip": + { + return decompressZip(fileBytes) + } + case "gz": + { + return decompressGzip(fileBytes) + } + default: + { + return nil, fmt.Errorf("unsupported compression type") + } + } +} + +func decompressGzip(data []byte) ([]byte, error) { + // Create a gzip reader from the byte array + gzipReader, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { _ = gzipReader.Close() }() + + var buf bytes.Buffer + _, err = io.CopyN(&buf, gzipReader, MaxUnpackSize) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("failed to read gzip file: %w", err) + } + + return buf.Bytes(), nil +} + +func decompressZip(data []byte) ([]byte, error) { + // Create a zip reader from the byte array + zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return nil, fmt.Errorf("failed to create zip reader: %w", err) + } + + // Ensure there is only one file in the zip + if len(zipReader.File) != 1 { + return nil, fmt.Errorf("zip file must contain exactly one file") + } + + // Read the single file in the zip + file := zipReader.File[0] + fileReader, err := file.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file in zip: %w", err) + } + defer func() { _ = fileReader.Close() }() + + var buf bytes.Buffer + _, err = io.CopyN(&buf, fileReader, MaxUnpackSize) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("failed to read file in zip: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/service/updates/index.go b/service/updates/index.go new file mode 100644 index 000000000..adf4564f0 --- /dev/null +++ b/service/updates/index.go @@ -0,0 +1,110 @@ +package updates + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/safing/portmaster/base/log" +) + +type UpdateIndex struct { + Directory string + DownloadDirectory string + Ignore []string + IndexURLs []string + IndexFile string + AutoApply bool +} + +func (ui *UpdateIndex) downloadIndexFile() (err error) { + _ = os.MkdirAll(ui.Directory, defaultDirMode) + _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) + for _, url := range ui.IndexURLs { + err = ui.downloadIndexFileFromURL(url) + if err != nil { + log.Warningf("updates: %s", err) + continue + } + // Downloading was successful. + err = nil + break + } + return +} + +func (ui *UpdateIndex) checkForUpdates() (bool, error) { + err := ui.downloadIndexFile() + if err != nil { + return false, err + } + + currentBundle, err := ui.GetInstallBundle() + if err != nil { + return true, err // Current installed bundle not found, act as there is update. + } + updateBundle, err := ui.GetUpdateBundle() + if err != nil { + return false, err + } + + return currentBundle.Version != updateBundle.Version, nil +} + +func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { + client := http.Client{} + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("failed a get request to %s: %w", url, err) + } + defer func() { _ = resp.Body.Close() }() + filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) + file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + + return nil +} + +func (ui *UpdateIndex) GetInstallBundle() (*Bundle, error) { + indexFile := fmt.Sprintf("%s/%s", ui.Directory, ui.IndexFile) + return ui.GetBundle(indexFile) +} + +func (ui *UpdateIndex) GetUpdateBundle() (*Bundle, error) { + indexFile := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) + return ui.GetBundle(indexFile) +} + +func (ui *UpdateIndex) GetBundle(indexFile string) (*Bundle, error) { + // Check if the file exists. + file, err := os.Open(indexFile) + if err != nil { + return nil, fmt.Errorf("failed to open index file: %w", err) + } + defer func() { _ = file.Close() }() + + // Read + content, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + // Parse + var bundle Bundle + err = json.Unmarshal(content, &bundle) + if err != nil { + return nil, err + } + + return &bundle, nil +} diff --git a/service/updates/main.go b/service/updates/main.go index bb942993a..3e11064cb 100644 --- a/service/updates/main.go +++ b/service/updates/main.go @@ -1,20 +1,14 @@ package updates import ( - "context" - "errors" - "flag" "fmt" - "net/url" "runtime" "time" "github.com/safing/portmaster/base/database" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" ) const ( @@ -48,9 +42,6 @@ var ( userAgentFromFlag string updateServerFromFlag string - updateASAP bool - disableTaskSchedule bool - db = database.NewInterface(&database.Options{ Local: true, Internal: true, @@ -60,163 +51,126 @@ var ( // more context to requests made by the registry when // fetching resources from the update server. UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) - - // DefaultUpdateURLs defines the default base URLs of the update server. - DefaultUpdateURLs = []string{ - "https://updates.safing.io", - } - - // DisableSoftwareAutoUpdate specifies whether software updates should be disabled. - // This is used on Android, as it will never require binary updates. - DisableSoftwareAutoUpdate = false ) const ( - updatesDirName = "updates" - updateTaskRepeatDuration = 1 * time.Hour ) -func init() { - flag.StringVar(&updateServerFromFlag, "update-server", "", "set an alternative update server (full URL)") - flag.StringVar(&userAgentFromFlag, "update-agent", "", "set an alternative user agent for requests to the update server") -} - -func prep() error { - // Check if update server URL supplied via flag is a valid URL. - if updateServerFromFlag != "" { - u, err := url.Parse(updateServerFromFlag) - if err != nil { - return fmt.Errorf("supplied update server URL is invalid: %w", err) - } - if u.Scheme != "https" { - return errors.New("supplied update server URL must use HTTPS") - } - } - - if err := registerConfig(); err != nil { - return err - } - - return registerAPIEndpoints() -} - func start() error { - initConfig() - - module.restartWorkerMgr.Repeat(10 * time.Minute) - module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig) - - // create registry - registry = &updater.ResourceRegistry{ - Name: ModuleName, - UpdateURLs: DefaultUpdateURLs, - UserAgent: UserAgent, - MandatoryUpdates: helper.MandatoryUpdates(), - AutoUnpack: helper.AutoUnpackUpdates(), - Verification: helper.VerificationConfig, - DevMode: devMode(), - Online: true, - } - // Override values from flags. - if userAgentFromFlag != "" { - registry.UserAgent = userAgentFromFlag - } - if updateServerFromFlag != "" { - registry.UpdateURLs = []string{updateServerFromFlag} - } + // module.restartWorkerMgr.Repeat(10 * time.Minute) + // module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig) + + // // create registry + // registry = &updater.ResourceRegistry{ + // Name: ModuleName, + // UpdateURLs: DefaultUpdateURLs, + // UserAgent: UserAgent, + // MandatoryUpdates: helper.MandatoryUpdates(), + // AutoUnpack: helper.AutoUnpackUpdates(), + // Verification: helper.VerificationConfig, + // DevMode: devMode(), + // Online: true, + // } + // // Override values from flags. + // if userAgentFromFlag != "" { + // registry.UserAgent = userAgentFromFlag + // } + // if updateServerFromFlag != "" { + // registry.UpdateURLs = []string{updateServerFromFlag} + // } - // pre-init state - updateStateExport, err := LoadStateExport() - if err != nil { - log.Debugf("updates: failed to load exported update state: %s", err) - } else if updateStateExport.UpdateState != nil { - err := registry.PreInitUpdateState(*updateStateExport.UpdateState) - if err != nil { - return err - } - } + // // pre-init state + // updateStateExport, err := LoadStateExport() + // if err != nil { + // log.Debugf("updates: failed to load exported update state: %s", err) + // } else if updateStateExport.UpdateState != nil { + // err := registry.PreInitUpdateState(*updateStateExport.UpdateState) + // if err != nil { + // return err + // } + // } // initialize - err = registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755)) - if err != nil { - return err - } + // err := registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755)) + // if err != nil { + // return err + // } - // register state provider - err = registerRegistryStateProvider() - if err != nil { - return err - } - registry.StateNotifyFunc = pushRegistryState - - // Set indexes based on the release channel. - warning := helper.SetIndexes( - registry, - initialReleaseChannel, - true, - enableSoftwareUpdates() && !DisableSoftwareAutoUpdate, - enableIntelUpdates(), - ) - if warning != nil { - log.Warningf("updates: %s", warning) - } + // // register state provider + // err = registerRegistryStateProvider() + // if err != nil { + // return err + // } + // registry.StateNotifyFunc = pushRegistryState + + // // Set indexes based on the release channel. + // warning := helper.SetIndexes( + // registry, + // initialReleaseChannel, + // true, + // enableSoftwareUpdates() && !DisableSoftwareAutoUpdate, + // enableIntelUpdates(), + // ) + // if warning != nil { + // log.Warningf("updates: %s", warning) + // } - err = registry.LoadIndexes(module.m.Ctx()) - if err != nil { - log.Warningf("updates: failed to load indexes: %s", err) - } + // err = registry.LoadIndexes(module.m.Ctx()) + // if err != nil { + // log.Warningf("updates: failed to load indexes: %s", err) + // } - err = registry.ScanStorage("") - if err != nil { - log.Warningf("updates: error during storage scan: %s", err) - } + // err = registry.ScanStorage("") + // if err != nil { + // log.Warningf("updates: error during storage scan: %s", err) + // } - registry.SelectVersions() - module.EventVersionsUpdated.Submit(struct{}{}) + // registry.SelectVersions() + // module.EventVersionsUpdated.Submit(struct{}{}) - // Initialize the version export - this requires the registry to be set up. - err = initVersionExport() - if err != nil { - return err - } + // // Initialize the version export - this requires the registry to be set up. + // err = initVersionExport() + // if err != nil { + // return err + // } - // start updater task - if !disableTaskSchedule { - _ = module.updateWorkerMgr.Repeat(30 * time.Minute) - } + // // start updater task + // if !disableTaskSchedule { + // _ = module.updateWorkerMgr.Repeat(30 * time.Minute) + // } - if updateASAP { - module.updateWorkerMgr.Go() - } + // if updateASAP { + // module.updateWorkerMgr.Go() + // } - // react to upgrades - if err := initUpgrader(); err != nil { - return err - } + // // react to upgrades + // if err := initUpgrader(); err != nil { + // return err + // } - warnOnIncorrectParentPath() + // warnOnIncorrectParentPath() return nil } // TriggerUpdate queues the update task to execute ASAP. func TriggerUpdate(forceIndexCheck, downloadAll bool) error { - switch { - case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates(): - return errors.New("automatic updating is disabled") - - default: - if forceIndexCheck { - forceCheck.Set() - } - if downloadAll { - forceDownload.Set() - } - - // If index check if forced, start quicker. - module.updateWorkerMgr.Go() - } + // switch { + // case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates(): + // return errors.New("automatic updating is disabled") + + // default: + // if forceIndexCheck { + // forceCheck.Set() + // } + // if downloadAll { + // forceDownload.Set() + // } + + // // If index check if forced, start quicker. + // module.updateWorkerMgr.Go() + // } log.Debugf("updates: triggering update to run as soon as possible") return nil @@ -232,68 +186,66 @@ func DisableUpdateSchedule() error { // return errors.New("module already online") // } - disableTaskSchedule = true - return nil } func checkForUpdates(ctx *mgr.WorkerCtx) (err error) { // Set correct error if context was canceled. - defer func() { - select { - case <-ctx.Done(): - err = context.Canceled - default: - } - }() - - // Get flags. - forceIndexCheck := forceCheck.SetToIf(true, false) - downloadAll := forceDownload.SetToIf(true, false) - - // Check again if downloading updates is enabled, or forced. - if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() { - log.Warningf("updates: automatic updates are disabled") - return nil - } - - defer func() { - // Resolve any error and send success notification. - if err == nil { - log.Infof("updates: successfully checked for updates") - notifyUpdateSuccess(forceIndexCheck) - return - } - - // Log and notify error. - log.Errorf("updates: check failed: %s", err) - notifyUpdateCheckFailed(forceIndexCheck, err) - }() + // defer func() { + // select { + // case <-ctx.Done(): + // err = context.Canceled + // default: + // } + // }() + + // // Get flags. + // forceIndexCheck := forceCheck.SetToIf(true, false) + // downloadAll := forceDownload.SetToIf(true, false) + + // // Check again if downloading updates is enabled, or forced. + // if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() { + // log.Warningf("updates: automatic updates are disabled") + // return nil + // } - if err = registry.UpdateIndexes(ctx.Ctx()); err != nil { - err = fmt.Errorf("failed to update indexes: %w", err) - return //nolint:nakedret // TODO: Would "return err" work with the defer? - } + // defer func() { + // // Resolve any error and send success notification. + // if err == nil { + // log.Infof("updates: successfully checked for updates") + // notifyUpdateSuccess(forceIndexCheck) + // return + // } + + // // Log and notify error. + // log.Errorf("updates: check failed: %s", err) + // notifyUpdateCheckFailed(forceIndexCheck, err) + // }() + + // if err = registry.UpdateIndexes(ctx.Ctx()); err != nil { + // err = fmt.Errorf("failed to update indexes: %w", err) + // return //nolint:nakedret // TODO: Would "return err" work with the defer? + // } - err = registry.DownloadUpdates(ctx.Ctx(), downloadAll) - if err != nil { - err = fmt.Errorf("failed to download updates: %w", err) - return //nolint:nakedret // TODO: Would "return err" work with the defer? - } + // err = registry.DownloadUpdates(ctx.Ctx(), downloadAll) + // if err != nil { + // err = fmt.Errorf("failed to download updates: %w", err) + // return //nolint:nakedret // TODO: Would "return err" work with the defer? + // } - registry.SelectVersions() + // registry.SelectVersions() - // Unpack selected resources. - err = registry.UnpackResources() - if err != nil { - err = fmt.Errorf("failed to unpack updates: %w", err) - return //nolint:nakedret // TODO: Would "return err" work with the defer? - } + // // Unpack selected resources. + // err = registry.UnpackResources() + // if err != nil { + // err = fmt.Errorf("failed to unpack updates: %w", err) + // return //nolint:nakedret // TODO: Would "return err" work with the defer? + // } - // Purge old resources - registry.Purge(2) + // // Purge old resources + // registry.Purge(2) - module.EventResourcesUpdated.Submit(struct{}{}) + // module.EventResourcesUpdated.Submit(struct{}{}) return nil } @@ -314,5 +266,6 @@ func RootPath() string { // return "" // } - return registry.StorageDir().Path + // return registry.StorageDir().Path + return "" } diff --git a/service/updates/module.go b/service/updates/module.go index c7cae1a92..002e315ec 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -2,14 +2,24 @@ package updates import ( "errors" + "fmt" + "os" + "path/filepath" + "strings" "sync/atomic" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" ) +const ( + defaultFileMode = os.FileMode(0o0644) + defaultDirMode = os.FileMode(0o0755) +) + // Updates provides access to released artifacts. type Updates struct { m *mgr.Manager @@ -21,6 +31,9 @@ type Updates struct { EventResourcesUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}] + binUpdates UpdateIndex + intelUpdates UpdateIndex + instance instance } @@ -40,19 +53,84 @@ func New(instance instance) (*Updates, error) { m: m, states: m.NewStateMgr(), - updateWorkerMgr: m.NewWorkerMgr("updater", checkForUpdates, nil), - restartWorkerMgr: m.NewWorkerMgr("automatic restart", automaticRestart, nil), EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), instance: instance, } - if err := prep(); err != nil { - return nil, err + + // Events + module.updateWorkerMgr = m.NewWorkerMgr("updater", module.checkForUpdates, nil) + module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil) + + module.binUpdates = UpdateIndex{ + Directory: "/usr/lib/portmaster", + DownloadDirectory: "/var/portmaster/new_bin", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://localhost:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + module.intelUpdates = UpdateIndex{ + Directory: "/var/portmaster/intel", + DownloadDirectory: "/var/portmaster/new_intel", + IndexURLs: []string{"http://localhost:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, } return module, nil } +func deleteUnfinishedDownloads(rootDir string) error { + return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Check if the current file has the specified extension + if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { + log.Warningf("updates deleting unfinished: %s\n", path) + err := os.Remove(path) + if err != nil { + return fmt.Errorf("failed to delete file %s: %w", path, err) + } + } + + return nil + }) +} + +func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { + _ = deleteUnfinishedDownloads(u.binUpdates.DownloadDirectory) + hasUpdate, err := u.binUpdates.checkForUpdates() + if err != nil { + log.Warningf("failed to get binary index file: %s", err) + } + if hasUpdate { + binBundle, err := u.binUpdates.GetUpdateBundle() + if err == nil { + log.Debugf("Bin Bundle: %+v", binBundle) + _ = os.MkdirAll(u.binUpdates.DownloadDirectory, defaultDirMode) + binBundle.downloadAndVerify(u.binUpdates.DownloadDirectory) + } + } + _ = deleteUnfinishedDownloads(u.intelUpdates.DownloadDirectory) + hasUpdate, err = u.intelUpdates.checkForUpdates() + if err != nil { + log.Warningf("failed to get intel index file: %s", err) + } + if hasUpdate { + intelBundle, err := u.intelUpdates.GetUpdateBundle() + if err == nil { + log.Debugf("Intel Bundle: %+v", intelBundle) + _ = os.MkdirAll(u.intelUpdates.DownloadDirectory, defaultDirMode) + intelBundle.downloadAndVerify(u.intelUpdates.DownloadDirectory) + } + } + return nil +} + // States returns the state manager. func (u *Updates) States() *mgr.StateMgr { return u.states @@ -65,7 +143,36 @@ func (u *Updates) Manager() *mgr.Manager { // Start starts the module. func (u *Updates) Start() error { - return start() + initConfig() + u.m.Go("check for updates", func(w *mgr.WorkerCtx) error { + binBundle, err := u.binUpdates.GetInstallBundle() + if err != nil { + log.Warningf("failed to get binary bundle: %s", err) + } else { + err = binBundle.Verify(u.binUpdates.Directory) + if err != nil { + log.Warningf("binary bundle is not valid: %s", err) + } else { + log.Infof("binary bundle is valid") + } + } + + intelBundle, err := u.intelUpdates.GetInstallBundle() + if err != nil { + log.Warningf("failed to get intel bundle: %s", err) + } else { + err = intelBundle.Verify(u.intelUpdates.Directory) + if err != nil { + log.Warningf("intel bundle is not valid: %s", err) + } else { + log.Infof("intel bundle is valid") + } + } + + return nil + }) + u.updateWorkerMgr.Go() + return nil } // Stop stops the module. diff --git a/service/updates/registry.go b/service/updates/registry.go new file mode 100644 index 000000000..de15a98b4 --- /dev/null +++ b/service/updates/registry.go @@ -0,0 +1 @@ +package updates diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go index 622b3909b..e4685c3b8 100644 --- a/service/updates/upgrader.go +++ b/service/updates/upgrader.go @@ -181,16 +181,16 @@ func upgradeHub() error { DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) // Increase update checks in order to detect aborts better. - if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(10 * time.Minute) - } + // if !disableTaskSchedule { + module.updateWorkerMgr.Repeat(10 * time.Minute) + // } } else { AbortRestart() // Set update task schedule back to normal. - if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(updateTaskRepeatDuration) - } + // if !disableTaskSchedule { + module.updateWorkerMgr.Repeat(updateTaskRepeatDuration) + // } } return nil From 9bae1afd73ef527a1989271d6223e4b49966b88c Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 16 Aug 2024 16:05:01 +0300 Subject: [PATCH 06/11] [WIP] New updater first working prototype --- base/updater/export.go | 22 +- base/updater/fetch.go | 690 ++++++------- base/updater/file.go | 254 +++-- base/updater/get.go | 119 ++- base/updater/notifier.go | 52 +- base/updater/registry.go | 532 +++++----- base/updater/resource.go | 1118 +++++++++++----------- base/updater/signing.go | 94 +- base/updater/state.go | 356 +++---- base/updater/storage.go | 540 +++++------ base/updater/unpacking.go | 386 ++++---- base/updater/updating.go | 714 +++++++------- service/broadcasts/data.go | 24 +- service/broadcasts/module.go | 5 +- service/broadcasts/notify.go | 3 +- service/core/api.go | 2 +- service/intel/filterlists/database.go | 22 +- service/intel/filterlists/index.go | 26 +- service/intel/filterlists/updater.go | 86 +- service/intel/geoip/database.go | 31 +- service/netenv/main.go | 5 +- service/netenv/online-status.go | 3 +- service/network/api.go | 3 +- service/ui/module.go | 9 +- service/ui/serve.go | 7 +- service/updates/api.go | 304 +++--- service/updates/config.go | 304 +++--- service/updates/export.go | 471 +++++---- service/updates/get.go | 91 +- service/updates/helper/electron.go | 111 +-- service/updates/helper/indexes.go | 268 +++--- service/updates/helper/signing.go | 72 +- service/updates/helper/updates.go | 186 ++-- service/updates/index.go | 110 --- service/updates/main.go | 209 +--- service/updates/module.go | 147 ++- service/updates/notify.go | 286 +++--- service/updates/os_integration_linux.go | 401 ++++---- service/updates/registry.go | 1 - service/updates/{ => registry}/bundle.go | 38 +- service/updates/registry/index.go | 56 ++ service/updates/registry/registry.go | 245 +++++ service/updates/restart.go | 22 +- service/updates/state.go | 94 +- service/updates/upgrader.go | 805 ++++++++-------- spn/captain/intel.go | 10 +- 46 files changed, 4646 insertions(+), 4688 deletions(-) delete mode 100644 service/updates/index.go delete mode 100644 service/updates/registry.go rename service/updates/{ => registry}/bundle.go (86%) create mode 100644 service/updates/registry/index.go create mode 100644 service/updates/registry/registry.go diff --git a/base/updater/export.go b/base/updater/export.go index 55b64a3f3..51889bde5 100644 --- a/base/updater/export.go +++ b/base/updater/export.go @@ -1,15 +1,15 @@ package updater -// Export exports the list of resources. -func (reg *ResourceRegistry) Export() map[string]*Resource { - reg.RLock() - defer reg.RUnlock() +// // Export exports the list of resources. +// func (reg *ResourceRegistry) Export() map[string]*Resource { +// reg.RLock() +// defer reg.RUnlock() - // copy the map - copiedResources := make(map[string]*Resource) - for key, val := range reg.resources { - copiedResources[key] = val.Export() - } +// // copy the map +// copiedResources := make(map[string]*Resource) +// for key, val := range reg.resources { +// copiedResources[key] = val.Export() +// } - return copiedResources -} +// return copiedResources +// } diff --git a/base/updater/fetch.go b/base/updater/fetch.go index f324709d4..89456774a 100644 --- a/base/updater/fetch.go +++ b/base/updater/fetch.go @@ -1,347 +1,347 @@ package updater -import ( - "bytes" - "context" - "errors" - "fmt" - "hash" - "io" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "time" - - "github.com/safing/jess/filesig" - "github.com/safing/jess/lhash" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils/renameio" -) - -func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { - // backoff when retrying - if tries > 0 { - select { - case <-ctx.Done(): - return nil // module is shutting down - case <-time.After(time.Duration(tries*tries) * time.Second): - } - } - - // check destination dir - dirPath := filepath.Dir(rv.storagePath()) - err := reg.storageDir.EnsureAbsPath(dirPath) - if err != nil { - return fmt.Errorf("could not create updates folder: %s", dirPath) - } - - // If verification is enabled, download signature first. - var ( - verifiedHash *lhash.LabeledHash - sigFileData []byte - ) - if rv.resource.VerificationOptions != nil { - verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( - ctx, client, - rv.resource.VerificationOptions, - rv.versionedSigPath(), rv.SigningMetadata(), - tries, - ) - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("signature verification failed: %w", err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - } - } - } - - // open file for writing - atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) - if err != nil { - return fmt.Errorf("could not create temp file for download: %w", err) - } - defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - - // start file download - resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) - if err != nil { - return err - } - defer func() { - _ = resp.Body.Close() - }() - - // Write to the hasher at the same time, if needed. - var hasher hash.Hash - var writeDst io.Writer = atomicFile - if verifiedHash != nil { - hasher = verifiedHash.Algorithm().RawHasher() - writeDst = io.MultiWriter(hasher, atomicFile) - } - - // Download and write file. - n, err := io.Copy(writeDst, resp.Body) - if err != nil { - return fmt.Errorf("failed to download %q: %w", downloadURL, err) - } - if resp.ContentLength != n { - return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) - } - - // Before file is finalized, check if hash, if available. - if hasher != nil { - downloadDigest := hasher.Sum(nil) - if verifiedHash.EqualRaw(downloadDigest) { - log.Infof("%s: verified signature of %s", reg.Name, downloadURL) - } else { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return errors.New("file does not match signed checksum") - case SignaturePolicyWarn: - log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) - case SignaturePolicyDisable: - log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) - } - - // Reset hasher to signal that the sig should not be written. - hasher = nil - } - } - - // Write signature file, if we have one and if verification succeeded. - if len(sigFileData) > 0 && hasher != nil { - sigFilePath := rv.storagePath() + filesig.Extension - err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) - } - } - } - - // finalize file - err = atomicFile.CloseAtomicallyReplace() - if err != nil { - return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) - } - // set permissions - if !onWindows { - // TODO: only set executable files to 0755, set other to 0644 - err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. - if err != nil { - log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) - } - } - - log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) - return nil -} - -func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { - // backoff when retrying - if tries > 0 { - select { - case <-ctx.Done(): - return nil // module is shutting down - case <-time.After(time.Duration(tries*tries) * time.Second): - } - } - - // Check destination dir. - dirPath := filepath.Dir(rv.storagePath()) - err := reg.storageDir.EnsureAbsPath(dirPath) - if err != nil { - return fmt.Errorf("could not create updates folder: %s", dirPath) - } - - // Download and verify the missing signature. - verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( - ctx, client, - rv.resource.VerificationOptions, - rv.versionedSigPath(), rv.SigningMetadata(), - tries, - ) - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("signature verification failed: %w", err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - } - return nil - } - - // Check if the signature matches the resource file. - ok, err := verifiedHash.MatchesFile(rv.storagePath()) - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("error while verifying resource file: %w", err) - case SignaturePolicyWarn: - log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) - case SignaturePolicyDisable: - log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) - } - return nil - } - if !ok { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return errors.New("resource file does not match signed checksum") - case SignaturePolicyWarn: - log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) - case SignaturePolicyDisable: - log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) - } - return nil - } - - // Write signature file. - err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) - } - } - - log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) - return nil -} - -func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { - // Download signature file. - resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) - if err != nil { - return nil, nil, err - } - defer func() { - _ = resp.Body.Close() - }() - sigFileData, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, err - } - - // Extract all signatures. - sigs, err := filesig.ParseSigFile(sigFileData) - switch { - case len(sigs) == 0 && err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - case len(sigs) == 0: - return nil, nil, errors.New("no signatures found in signature file") - case err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - } - - // Verify all signatures. - var verifiedHash *lhash.LabeledHash - for _, sig := range sigs { - fd, err := filesig.VerifyFileData( - sig, - requiredMetadata, - verifOpts.TrustStore, - ) - if err != nil { - return nil, sigFileData, err - } - - // Save or check verified hash. - if verifiedHash == nil { - verifiedHash = fd.FileHash() - } else if !fd.FileHash().Equal(verifiedHash) { - // Return an error if two valid hashes mismatch. - // For simplicity, all hash algorithms must be the same for now. - return nil, sigFileData, errors.New("file hashes from different signatures do not match") - } - } - - return verifiedHash, sigFileData, nil -} - -func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { - // backoff when retrying - if tries > 0 { - select { - case <-ctx.Done(): - return nil, "", nil // module is shutting down - case <-time.After(time.Duration(tries*tries) * time.Second): - } - } - - // start file download - resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) - if err != nil { - return nil, downloadURL, err - } - defer func() { - _ = resp.Body.Close() - }() - - // download and write file - buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) - n, err := io.Copy(buf, resp.Body) - if err != nil { - return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) - } - if resp.ContentLength != n { - return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) - } - - return buf.Bytes(), downloadURL, nil -} - -func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { - // parse update URL - updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] - u, err := url.Parse(updateBaseURL) - if err != nil { - return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) - } - // add download path - u.Path = path.Join(u.Path, downloadPath) - // compile URL - downloadURL = u.String() - - // create request - req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) - if err != nil { - return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) - } - - // set user agent - if reg.UserAgent != "" { - req.Header.Set("User-Agent", reg.UserAgent) - } - - // start request - resp, err = client.Do(req) - if err != nil { - return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) - } - - // check return code - if resp.StatusCode != http.StatusOK { - _ = resp.Body.Close() - return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) - } - - return resp, downloadURL, err -} +// import ( +// "bytes" +// "context" +// "errors" +// "fmt" +// "hash" +// "io" +// "net/http" +// "net/url" +// "os" +// "path" +// "path/filepath" +// "time" + +// "github.com/safing/jess/filesig" +// "github.com/safing/jess/lhash" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils/renameio" +// ) + +// func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { +// // backoff when retrying +// if tries > 0 { +// select { +// case <-ctx.Done(): +// return nil // module is shutting down +// case <-time.After(time.Duration(tries*tries) * time.Second): +// } +// } + +// // check destination dir +// dirPath := filepath.Dir(rv.storagePath()) +// err := reg.storageDir.EnsureAbsPath(dirPath) +// if err != nil { +// return fmt.Errorf("could not create updates folder: %s", dirPath) +// } + +// // If verification is enabled, download signature first. +// var ( +// verifiedHash *lhash.LabeledHash +// sigFileData []byte +// ) +// if rv.resource.VerificationOptions != nil { +// verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( +// ctx, client, +// rv.resource.VerificationOptions, +// rv.versionedSigPath(), rv.SigningMetadata(), +// tries, +// ) +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("signature verification failed: %w", err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// } +// } +// } + +// // open file for writing +// atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) +// if err != nil { +// return fmt.Errorf("could not create temp file for download: %w", err) +// } +// defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway + +// // start file download +// resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) +// if err != nil { +// return err +// } +// defer func() { +// _ = resp.Body.Close() +// }() + +// // Write to the hasher at the same time, if needed. +// var hasher hash.Hash +// var writeDst io.Writer = atomicFile +// if verifiedHash != nil { +// hasher = verifiedHash.Algorithm().RawHasher() +// writeDst = io.MultiWriter(hasher, atomicFile) +// } + +// // Download and write file. +// n, err := io.Copy(writeDst, resp.Body) +// if err != nil { +// return fmt.Errorf("failed to download %q: %w", downloadURL, err) +// } +// if resp.ContentLength != n { +// return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) +// } + +// // Before file is finalized, check if hash, if available. +// if hasher != nil { +// downloadDigest := hasher.Sum(nil) +// if verifiedHash.EqualRaw(downloadDigest) { +// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) +// } else { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return errors.New("file does not match signed checksum") +// case SignaturePolicyWarn: +// log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) +// case SignaturePolicyDisable: +// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) +// } + +// // Reset hasher to signal that the sig should not be written. +// hasher = nil +// } +// } + +// // Write signature file, if we have one and if verification succeeded. +// if len(sigFileData) > 0 && hasher != nil { +// sigFilePath := rv.storagePath() + filesig.Extension +// err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) +// } +// } +// } + +// // finalize file +// err = atomicFile.CloseAtomicallyReplace() +// if err != nil { +// return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) +// } +// // set permissions +// if !onWindows { +// // TODO: only set executable files to 0755, set other to 0644 +// err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. +// if err != nil { +// log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) +// } +// } + +// log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) +// return nil +// } + +// func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { +// // backoff when retrying +// if tries > 0 { +// select { +// case <-ctx.Done(): +// return nil // module is shutting down +// case <-time.After(time.Duration(tries*tries) * time.Second): +// } +// } + +// // Check destination dir. +// dirPath := filepath.Dir(rv.storagePath()) +// err := reg.storageDir.EnsureAbsPath(dirPath) +// if err != nil { +// return fmt.Errorf("could not create updates folder: %s", dirPath) +// } + +// // Download and verify the missing signature. +// verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( +// ctx, client, +// rv.resource.VerificationOptions, +// rv.versionedSigPath(), rv.SigningMetadata(), +// tries, +// ) +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("signature verification failed: %w", err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// } +// return nil +// } + +// // Check if the signature matches the resource file. +// ok, err := verifiedHash.MatchesFile(rv.storagePath()) +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("error while verifying resource file: %w", err) +// case SignaturePolicyWarn: +// log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) +// case SignaturePolicyDisable: +// log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) +// } +// return nil +// } +// if !ok { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return errors.New("resource file does not match signed checksum") +// case SignaturePolicyWarn: +// log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) +// case SignaturePolicyDisable: +// log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) +// } +// return nil +// } + +// // Write signature file. +// err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) +// } +// } + +// log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) +// return nil +// } + +// func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { +// // Download signature file. +// resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) +// if err != nil { +// return nil, nil, err +// } +// defer func() { +// _ = resp.Body.Close() +// }() +// sigFileData, err := io.ReadAll(resp.Body) +// if err != nil { +// return nil, nil, err +// } + +// // Extract all signatures. +// sigs, err := filesig.ParseSigFile(sigFileData) +// switch { +// case len(sigs) == 0 && err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// case len(sigs) == 0: +// return nil, nil, errors.New("no signatures found in signature file") +// case err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// } + +// // Verify all signatures. +// var verifiedHash *lhash.LabeledHash +// for _, sig := range sigs { +// fd, err := filesig.VerifyFileData( +// sig, +// requiredMetadata, +// verifOpts.TrustStore, +// ) +// if err != nil { +// return nil, sigFileData, err +// } + +// // Save or check verified hash. +// if verifiedHash == nil { +// verifiedHash = fd.FileHash() +// } else if !fd.FileHash().Equal(verifiedHash) { +// // Return an error if two valid hashes mismatch. +// // For simplicity, all hash algorithms must be the same for now. +// return nil, sigFileData, errors.New("file hashes from different signatures do not match") +// } +// } + +// return verifiedHash, sigFileData, nil +// } + +// func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { +// // backoff when retrying +// if tries > 0 { +// select { +// case <-ctx.Done(): +// return nil, "", nil // module is shutting down +// case <-time.After(time.Duration(tries*tries) * time.Second): +// } +// } + +// // start file download +// resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) +// if err != nil { +// return nil, downloadURL, err +// } +// defer func() { +// _ = resp.Body.Close() +// }() + +// // download and write file +// buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) +// n, err := io.Copy(buf, resp.Body) +// if err != nil { +// return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) +// } +// if resp.ContentLength != n { +// return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) +// } + +// return buf.Bytes(), downloadURL, nil +// } + +// func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { +// // parse update URL +// updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] +// u, err := url.Parse(updateBaseURL) +// if err != nil { +// return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) +// } +// // add download path +// u.Path = path.Join(u.Path, downloadPath) +// // compile URL +// downloadURL = u.String() + +// // create request +// req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) +// if err != nil { +// return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) +// } + +// // set user agent +// if reg.UserAgent != "" { +// req.Header.Set("User-Agent", reg.UserAgent) +// } + +// // start request +// resp, err = client.Do(req) +// if err != nil { +// return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) +// } + +// // check return code +// if resp.StatusCode != http.StatusOK { +// _ = resp.Body.Close() +// return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) +// } + +// return resp, downloadURL, err +// } diff --git a/base/updater/file.go b/base/updater/file.go index 90b7d3565..758f2a7be 100644 --- a/base/updater/file.go +++ b/base/updater/file.go @@ -1,105 +1,97 @@ package updater import ( - "errors" - "io" - "io/fs" - "os" - "strings" - semver "github.com/hashicorp/go-version" +// semver "github.com/hashicorp/go-version" - "github.com/safing/jess/filesig" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" ) // File represents a file from the update system. -type File struct { - resource *Resource - version *ResourceVersion - notifier *notifier - versionedPath string - storagePath string -} - -// Identifier returns the identifier of the file. -func (file *File) Identifier() string { - return file.resource.Identifier -} - -// Version returns the version of the file. -func (file *File) Version() string { - return file.version.VersionNumber -} - -// SemVer returns the semantic version of the file. -func (file *File) SemVer() *semver.Version { - return file.version.semVer -} - -// EqualsVersion normalizes the given version and checks equality with semver. -func (file *File) EqualsVersion(version string) bool { - return file.version.EqualsVersion(version) -} - -// Path returns the absolute filepath of the file. -func (file *File) Path() string { - return file.storagePath -} - -// SigningMetadata returns the metadata to be included in signatures. -func (file *File) SigningMetadata() map[string]string { - return map[string]string{ - "id": file.Identifier(), - "version": file.Version(), - } -} +// type File struct { +// resource *Resource +// version *ResourceVersion +// notifier *notifier +// versionedPath string +// storagePath string +// } + +// // Identifier returns the identifier of the file. +// func (file *File) Identifier() string { +// return file.resource.Identifier +// } + +// // Version returns the version of the file. +// func (file *File) Version() string { +// return file.version.VersionNumber +// } + +// // SemVer returns the semantic version of the file. +// func (file *File) SemVer() *semver.Version { +// return file.version.semVer +// } + +// // EqualsVersion normalizes the given version and checks equality with semver. +// func (file *File) EqualsVersion(version string) bool { +// return file.version.EqualsVersion(version) +// } + +// // Path returns the absolute filepath of the file. +// func (file *File) Path() string { +// return file.storagePath +// } + +// // SigningMetadata returns the metadata to be included in signatures. +// func (file *File) SigningMetadata() map[string]string { +// return map[string]string{ +// "id": file.Identifier(), +// "version": file.Version(), +// } +// } // Verify verifies the given file. -func (file *File) Verify() ([]*filesig.FileData, error) { - // Check if verification is configured. - if file.resource.VerificationOptions == nil { - return nil, ErrVerificationNotConfigured - } - - // Verify file. - fileData, err := filesig.VerifyFile( - file.storagePath, - file.storagePath+filesig.Extension, - file.SigningMetadata(), - file.resource.VerificationOptions.TrustStore, - ) - if err != nil { - switch file.resource.VerificationOptions.DiskLoadPolicy { - case SignaturePolicyRequire: - return nil, err - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) - } - } - - return fileData, nil -} +// func (file *File) Verify() ([]*filesig.FileData, error) { +// // Check if verification is configured. +// if file.resource.VerificationOptions == nil { +// return nil, ErrVerificationNotConfigured +// } + +// // Verify file. +// fileData, err := filesig.VerifyFile( +// file.storagePath, +// file.storagePath+filesig.Extension, +// file.SigningMetadata(), +// file.resource.VerificationOptions.TrustStore, +// ) +// if err != nil { +// switch file.resource.VerificationOptions.DiskLoadPolicy { +// case SignaturePolicyRequire: +// return nil, err +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) +// } +// } + +// return fileData, nil +// } // Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on, until restarted. -func (file *File) Blacklist() error { - return file.resource.Blacklist(file.version.VersionNumber) -} +// func (file *File) Blacklist() error { +// return file.resource.Blacklist(file.version.VersionNumber) +// } // markActiveWithLocking marks the file as active, locking the resource in the process. -func (file *File) markActiveWithLocking() { - file.resource.Lock() - defer file.resource.Unlock() +// func (file *File) markActiveWithLocking() { +// file.resource.Lock() +// defer file.resource.Unlock() - // update last used version - if file.resource.ActiveVersion != file.version { - log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber) - file.resource.ActiveVersion = file.version - } -} +// // update last used version +// if file.resource.ActiveVersion != file.version { +// log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber) +// file.resource.ActiveVersion = file.version +// } +// } // Unpacker describes the function that is passed to // File.Unpack. It receives a reader to the compressed/packed @@ -107,50 +99,50 @@ func (file *File) markActiveWithLocking() { // unpacked file contents. If the returned reader implements // io.Closer it's close method is invoked when an error // or io.EOF is returned from Read(). -type Unpacker func(io.Reader) (io.Reader, error) +// type Unpacker func(io.Reader) (io.Reader, error) // Unpack returns the path to the unpacked version of file and // unpacks it on demand using unpacker. -func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { - path := strings.TrimSuffix(file.Path(), suffix) - - if suffix == "" { - path += "-unpacked" - } - - _, err := os.Stat(path) - if err == nil { - return path, nil - } - - if !errors.Is(err, fs.ErrNotExist) { - return "", err - } - - f, err := os.Open(file.Path()) - if err != nil { - return "", err - } - defer func() { - _ = f.Close() - }() - - r, err := unpacker(f) - if err != nil { - return "", err - } - - ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ - TempDir: file.resource.registry.TmpDir().Path, - }) - - if c, ok := r.(io.Closer); ok { - if err := c.Close(); err != nil && ioErr == nil { - // if ioErr is already set we ignore the error from - // closing the unpacker. - ioErr = err - } - } - - return path, ioErr -} +// func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { +// path := strings.TrimSuffix(file.Path(), suffix) + +// if suffix == "" { +// path += "-unpacked" +// } + +// _, err := os.Stat(path) +// if err == nil { +// return path, nil +// } + +// if !errors.Is(err, fs.ErrNotExist) { +// return "", err +// } + +// f, err := os.Open(file.Path()) +// if err != nil { +// return "", err +// } +// defer func() { +// _ = f.Close() +// }() + +// r, err := unpacker(f) +// if err != nil { +// return "", err +// } + +// ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ +// TempDir: file.resource.registry.TmpDir().Path, +// }) + +// if c, ok := r.(io.Closer); ok { +// if err := c.Close(); err != nil && ioErr == nil { +// // if ioErr is already set we ignore the error from +// // closing the unpacker. +// ioErr = err +// } +// } + +// return path, ioErr +// } diff --git a/base/updater/get.go b/base/updater/get.go index 7b54c8e66..365cb7362 100644 --- a/base/updater/get.go +++ b/base/updater/get.go @@ -2,7 +2,6 @@ package updater import ( "errors" - "fmt" ) // Errors returned by the updater package. @@ -14,75 +13,75 @@ var ( // GetFile returns the selected (mostly newest) file with the given // identifier or an error, if it fails. -func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { - return nil, fmt.Errorf("invalid file: %s", identifier) - // reg.RLock() - // res, ok := reg.resources[identifier] - // reg.RUnlock() - // if !ok { - // return nil, ErrNotFound - // } +// func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { +// return nil, fmt.Errorf("invalid file: %s", identifier) +// reg.RLock() +// res, ok := reg.resources[identifier] +// reg.RUnlock() +// if !ok { +// return nil, ErrNotFound +// } - // file := res.GetFile() - // // check if file is available locally - // if file.version.Available { - // file.markActiveWithLocking() +// file := res.GetFile() +// // check if file is available locally +// if file.version.Available { +// file.markActiveWithLocking() - // // Verify file, if configured. - // _, err := file.Verify() - // if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { - // // TODO: If verification is required, try deleting the resource and downloading it again. - // return nil, fmt.Errorf("failed to verify file: %w", err) - // } +// // Verify file, if configured. +// _, err := file.Verify() +// if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { +// // TODO: If verification is required, try deleting the resource and downloading it again. +// return nil, fmt.Errorf("failed to verify file: %w", err) +// } - // return file, nil - // } +// return file, nil +// } - // // check if online - // if !reg.Online { - // return nil, ErrNotAvailableLocally - // } +// // check if online +// if !reg.Online { +// return nil, ErrNotAvailableLocally +// } - // // check download dir - // err := reg.tmpDir.Ensure() - // if err != nil { - // return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) - // } +// // check download dir +// err := reg.tmpDir.Ensure() +// if err != nil { +// return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) +// } - // // Start registry operation. - // reg.state.StartOperation(StateFetching) - // defer reg.state.EndOperation() +// // Start registry operation. +// reg.state.StartOperation(StateFetching) +// defer reg.state.EndOperation() - // // download file - // log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) - // client := &http.Client{} - // for tries := range 5 { - // err = reg.fetchFile(context.TODO(), client, file.version, tries) - // if err != nil { - // log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) - // } else { - // file.markActiveWithLocking() +// // download file +// log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) +// client := &http.Client{} +// for tries := range 5 { +// err = reg.fetchFile(context.TODO(), client, file.version, tries) +// if err != nil { +// log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) +// } else { +// file.markActiveWithLocking() - // // TODO: We just download the file - should we verify it again? - // return file, nil - // } - // } - // log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) - // return nil, err -} +// // TODO: We just download the file - should we verify it again? +// return file, nil +// } +// } +// log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) +// return nil, err +// } // GetVersion returns the selected version of the given identifier. // The returned resource version may not be modified. -func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) { - reg.RLock() - res, ok := reg.resources[identifier] - reg.RUnlock() - if !ok { - return nil, ErrNotFound - } +// func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) { +// reg.RLock() +// res, ok := reg.resources[identifier] +// reg.RUnlock() +// if !ok { +// return nil, ErrNotFound +// } - res.Lock() - defer res.Unlock() +// res.Lock() +// defer res.Unlock() - return res.SelectedVersion, nil -} +// return res.SelectedVersion, nil +// } diff --git a/base/updater/notifier.go b/base/updater/notifier.go index 66b2832df..75de01e45 100644 --- a/base/updater/notifier.go +++ b/base/updater/notifier.go @@ -1,33 +1,33 @@ package updater -import ( - "github.com/tevino/abool" -) +// import ( +// "github.com/tevino/abool" +// ) -type notifier struct { - upgradeAvailable *abool.AtomicBool - notifyChannel chan struct{} -} +// type notifier struct { +// upgradeAvailable *abool.AtomicBool +// notifyChannel chan struct{} +// } -func newNotifier() *notifier { - return ¬ifier{ - upgradeAvailable: abool.NewBool(false), - notifyChannel: make(chan struct{}), - } -} +// func newNotifier() *notifier { +// return ¬ifier{ +// upgradeAvailable: abool.NewBool(false), +// notifyChannel: make(chan struct{}), +// } +// } -func (n *notifier) markAsUpgradeable() { - if n.upgradeAvailable.SetToIf(false, true) { - close(n.notifyChannel) - } -} +// func (n *notifier) markAsUpgradeable() { +// if n.upgradeAvailable.SetToIf(false, true) { +// close(n.notifyChannel) +// } +// } -// UpgradeAvailable returns whether an upgrade is available for this file. -func (file *File) UpgradeAvailable() bool { - return file.notifier.upgradeAvailable.IsSet() -} +// // UpgradeAvailable returns whether an upgrade is available for this file. +// func (file *File) UpgradeAvailable() bool { +// return file.notifier.upgradeAvailable.IsSet() +// } -// WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available. -func (file *File) WaitForAvailableUpgrade() <-chan struct{} { - return file.notifier.notifyChannel -} +// // WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available. +// func (file *File) WaitForAvailableUpgrade() <-chan struct{} { +// return file.notifier.notifyChannel +// } diff --git a/base/updater/registry.go b/base/updater/registry.go index 8deda74ed..3d4861bf6 100644 --- a/base/updater/registry.go +++ b/base/updater/registry.go @@ -1,270 +1,270 @@ package updater -import ( - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "sync" - - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) - -const ( - onWindows = runtime.GOOS == "windows" -) +// import ( +// "errors" +// "fmt" +// "os" +// "path/filepath" +// "runtime" +// "strings" +// "sync" + +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) + +// const ( +// onWindows = runtime.GOOS == "windows" +// ) // ResourceRegistry is a registry for managing update resources. -type ResourceRegistry struct { - sync.RWMutex - - Name string - storageDir *utils.DirStructure - tmpDir *utils.DirStructure - indexes []*Index - state *RegistryState - - resources map[string]*Resource - UpdateURLs []string - UserAgent string - MandatoryUpdates []string - AutoUnpack []string - - // Verification holds a map of VerificationOptions assigned to their - // applicable identifier path prefix. - // Use an empty string to denote the default. - // Use empty options to disable verification for a path prefix. - Verification map[string]*VerificationOptions - - // UsePreReleases signifies that pre-releases should be used when selecting a - // version. Even if false, a pre-release version will still be used if it is - // defined as the current version by an index. - UsePreReleases bool - - // DevMode specifies if a local 0.0.0 version should be always chosen, when available. - DevMode bool - - // Online specifies if resources may be downloaded if not available locally. - Online bool - - // StateNotifyFunc may be set to receive any changes to the registry state. - // The specified function may lock the state, but may not block or take a - // lot of time. - StateNotifyFunc func(*RegistryState) -} - -// AddIndex adds a new index to the resource registry. -// The order is important, as indexes added later will override the current -// release from earlier indexes. -func (reg *ResourceRegistry) AddIndex(idx Index) { - reg.Lock() - defer reg.Unlock() - - // Get channel name from path. - idx.Channel = strings.TrimSuffix( - filepath.Base(idx.Path), filepath.Ext(idx.Path), - ) - - reg.indexes = append(reg.indexes, &idx) -} - -// PreInitUpdateState sets the initial update state of the registry before initialization. -func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error { - if reg.state != nil { - return errors.New("registry already initialized") - } - - reg.state = &RegistryState{ - Updates: s, - } - return nil -} - -// Initialize initializes a raw registry struct and makes it ready for usage. -func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error { - // check if storage dir is available - err := storageDir.Ensure() - if err != nil { - return err - } - - // set default name - if reg.Name == "" { - reg.Name = "updater" - } - - // initialize private attributes - reg.storageDir = storageDir - reg.tmpDir = storageDir.ChildDir("tmp", 0o0700) - reg.resources = make(map[string]*Resource) - if reg.state == nil { - reg.state = &RegistryState{} - } - reg.state.ID = StateReady - reg.state.reg = reg - - // remove tmp dir to delete old entries - err = reg.Cleanup() - if err != nil { - log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err) - } - - // (re-)create tmp dir - err = reg.tmpDir.Ensure() - if err != nil { - log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err) - } - - // Check verification options. - if reg.Verification != nil { - for prefix, opts := range reg.Verification { - // Check if verification is disable for this prefix. - if opts == nil { - continue - } - - // If enabled, a trust store is required. - if opts.TrustStore == nil { - return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix) - } - - // DownloadPolicy must be equal or stricter than DiskLoadPolicy. - if opts.DiskLoadPolicy < opts.DownloadPolicy { - return errors.New("verification download policy must be equal or stricter than the disk load policy") - } - - // Warn if all policies are disabled. - if opts.DownloadPolicy == SignaturePolicyDisable && - opts.DiskLoadPolicy == SignaturePolicyDisable { - log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix) - } - } - } - - return nil -} - -// StorageDir returns the main storage dir of the resource registry. -func (reg *ResourceRegistry) StorageDir() *utils.DirStructure { - return reg.storageDir -} - -// TmpDir returns the temporary working dir of the resource registry. -func (reg *ResourceRegistry) TmpDir() *utils.DirStructure { - return reg.tmpDir -} - -// SetDevMode sets the development mode flag. -func (reg *ResourceRegistry) SetDevMode(on bool) { - reg.Lock() - defer reg.Unlock() - - reg.DevMode = on -} - -// SetUsePreReleases sets the UsePreReleases flag. -func (reg *ResourceRegistry) SetUsePreReleases(yes bool) { - reg.Lock() - defer reg.Unlock() - - reg.UsePreReleases = yes -} - -// AddResource adds a resource to the registry. Does _not_ select new version. -func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { - reg.Lock() - defer reg.Unlock() - - err := reg.addResource(identifier, version, index, available, currentRelease, preRelease) - return err -} - -func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { - res, ok := reg.resources[identifier] - if !ok { - res = reg.newResource(identifier) - reg.resources[identifier] = res - } - res.Index = index - - return res.AddVersion(version, available, currentRelease, preRelease) -} - -// AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions. -func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error { - reg.Lock() - defer reg.Unlock() - - // add versions and their flags to registry - var lastError error - for identifier, version := range versions { - lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease) - if lastError != nil { - log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError) - } - } - - return lastError -} - -// SelectVersions selects new resource versions depending on the current registry state. -func (reg *ResourceRegistry) SelectVersions() { - reg.RLock() - defer reg.RUnlock() - - for _, res := range reg.resources { - res.Lock() - res.selectVersion() - res.Unlock() - } -} - -// GetSelectedVersions returns a list of the currently selected versions. -func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) { - reg.RLock() - defer reg.RUnlock() - - for _, res := range reg.resources { - res.Lock() - versions[res.Identifier] = res.SelectedVersion.VersionNumber - res.Unlock() - } - - return -} - -// Purge deletes old updates, retaining a certain amount, specified by the keep -// parameter. Will at least keep 2 updates per resource. -func (reg *ResourceRegistry) Purge(keep int) { - reg.RLock() - defer reg.RUnlock() - - for _, res := range reg.resources { - res.Purge(keep) - } -} - -// ResetResources removes all resources from the registry. -func (reg *ResourceRegistry) ResetResources() { - reg.Lock() - defer reg.Unlock() - - reg.resources = make(map[string]*Resource) -} - -// ResetIndexes removes all indexes from the registry. -func (reg *ResourceRegistry) ResetIndexes() { - reg.Lock() - defer reg.Unlock() - - reg.indexes = make([]*Index, 0, len(reg.indexes)) -} - -// Cleanup removes temporary files. -func (reg *ResourceRegistry) Cleanup() error { - // delete download tmp dir - return os.RemoveAll(reg.tmpDir.Path) -} +// type ResourceRegistry struct { +// sync.RWMutex + +// Name string +// storageDir *utils.DirStructure +// tmpDir *utils.DirStructure +// indexes []*Index +// state *RegistryState + +// resources map[string]*Resource +// UpdateURLs []string +// UserAgent string +// MandatoryUpdates []string +// AutoUnpack []string + +// // Verification holds a map of VerificationOptions assigned to their +// // applicable identifier path prefix. +// // Use an empty string to denote the default. +// // Use empty options to disable verification for a path prefix. +// Verification map[string]*VerificationOptions + +// // UsePreReleases signifies that pre-releases should be used when selecting a +// // version. Even if false, a pre-release version will still be used if it is +// // defined as the current version by an index. +// UsePreReleases bool + +// // DevMode specifies if a local 0.0.0 version should be always chosen, when available. +// DevMode bool + +// // Online specifies if resources may be downloaded if not available locally. +// Online bool + +// // StateNotifyFunc may be set to receive any changes to the registry state. +// // The specified function may lock the state, but may not block or take a +// // lot of time. +// StateNotifyFunc func(*RegistryState) +// } + +// // AddIndex adds a new index to the resource registry. +// // The order is important, as indexes added later will override the current +// // release from earlier indexes. +// func (reg *ResourceRegistry) AddIndex(idx Index) { +// reg.Lock() +// defer reg.Unlock() + +// // Get channel name from path. +// idx.Channel = strings.TrimSuffix( +// filepath.Base(idx.Path), filepath.Ext(idx.Path), +// ) + +// reg.indexes = append(reg.indexes, &idx) +// } + +// // PreInitUpdateState sets the initial update state of the registry before initialization. +// func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error { +// if reg.state != nil { +// return errors.New("registry already initialized") +// } + +// reg.state = &RegistryState{ +// Updates: s, +// } +// return nil +// } + +// // Initialize initializes a raw registry struct and makes it ready for usage. +// func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error { +// // check if storage dir is available +// err := storageDir.Ensure() +// if err != nil { +// return err +// } + +// // set default name +// if reg.Name == "" { +// reg.Name = "updater" +// } + +// // initialize private attributes +// reg.storageDir = storageDir +// reg.tmpDir = storageDir.ChildDir("tmp", 0o0700) +// reg.resources = make(map[string]*Resource) +// if reg.state == nil { +// reg.state = &RegistryState{} +// } +// reg.state.ID = StateReady +// reg.state.reg = reg + +// // remove tmp dir to delete old entries +// err = reg.Cleanup() +// if err != nil { +// log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err) +// } + +// // (re-)create tmp dir +// err = reg.tmpDir.Ensure() +// if err != nil { +// log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err) +// } + +// // Check verification options. +// if reg.Verification != nil { +// for prefix, opts := range reg.Verification { +// // Check if verification is disable for this prefix. +// if opts == nil { +// continue +// } + +// // If enabled, a trust store is required. +// if opts.TrustStore == nil { +// return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix) +// } + +// // DownloadPolicy must be equal or stricter than DiskLoadPolicy. +// if opts.DiskLoadPolicy < opts.DownloadPolicy { +// return errors.New("verification download policy must be equal or stricter than the disk load policy") +// } + +// // Warn if all policies are disabled. +// if opts.DownloadPolicy == SignaturePolicyDisable && +// opts.DiskLoadPolicy == SignaturePolicyDisable { +// log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix) +// } +// } +// } + +// return nil +// } + +// // StorageDir returns the main storage dir of the resource registry. +// func (reg *ResourceRegistry) StorageDir() *utils.DirStructure { +// return reg.storageDir +// } + +// // TmpDir returns the temporary working dir of the resource registry. +// func (reg *ResourceRegistry) TmpDir() *utils.DirStructure { +// return reg.tmpDir +// } + +// // SetDevMode sets the development mode flag. +// func (reg *ResourceRegistry) SetDevMode(on bool) { +// reg.Lock() +// defer reg.Unlock() + +// reg.DevMode = on +// } + +// // SetUsePreReleases sets the UsePreReleases flag. +// func (reg *ResourceRegistry) SetUsePreReleases(yes bool) { +// reg.Lock() +// defer reg.Unlock() + +// reg.UsePreReleases = yes +// } + +// // AddResource adds a resource to the registry. Does _not_ select new version. +// func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { +// reg.Lock() +// defer reg.Unlock() + +// err := reg.addResource(identifier, version, index, available, currentRelease, preRelease) +// return err +// } + +// func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { +// res, ok := reg.resources[identifier] +// if !ok { +// res = reg.newResource(identifier) +// reg.resources[identifier] = res +// } +// res.Index = index + +// return res.AddVersion(version, available, currentRelease, preRelease) +// } + +// // AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions. +// func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error { +// reg.Lock() +// defer reg.Unlock() + +// // add versions and their flags to registry +// var lastError error +// for identifier, version := range versions { +// lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease) +// if lastError != nil { +// log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError) +// } +// } + +// return lastError +// } + +// // SelectVersions selects new resource versions depending on the current registry state. +// func (reg *ResourceRegistry) SelectVersions() { +// reg.RLock() +// defer reg.RUnlock() + +// for _, res := range reg.resources { +// res.Lock() +// res.selectVersion() +// res.Unlock() +// } +// } + +// // GetSelectedVersions returns a list of the currently selected versions. +// func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) { +// reg.RLock() +// defer reg.RUnlock() + +// for _, res := range reg.resources { +// res.Lock() +// versions[res.Identifier] = res.SelectedVersion.VersionNumber +// res.Unlock() +// } + +// return +// } + +// // Purge deletes old updates, retaining a certain amount, specified by the keep +// // parameter. Will at least keep 2 updates per resource. +// func (reg *ResourceRegistry) Purge(keep int) { +// reg.RLock() +// defer reg.RUnlock() + +// for _, res := range reg.resources { +// res.Purge(keep) +// } +// } + +// // ResetResources removes all resources from the registry. +// func (reg *ResourceRegistry) ResetResources() { +// reg.Lock() +// defer reg.Unlock() + +// reg.resources = make(map[string]*Resource) +// } + +// // ResetIndexes removes all indexes from the registry. +// func (reg *ResourceRegistry) ResetIndexes() { +// reg.Lock() +// defer reg.Unlock() + +// reg.indexes = make([]*Index, 0, len(reg.indexes)) +// } + +// // Cleanup removes temporary files. +// func (reg *ResourceRegistry) Cleanup() error { +// // delete download tmp dir +// return os.RemoveAll(reg.tmpDir.Path) +// } diff --git a/base/updater/resource.go b/base/updater/resource.go index 325f70cc5..9180d0c9b 100644 --- a/base/updater/resource.go +++ b/base/updater/resource.go @@ -1,582 +1,582 @@ package updater -import ( - "errors" - "io/fs" - "os" - "path/filepath" - "sort" - "strings" - "sync" - - semver "github.com/hashicorp/go-version" - - "github.com/safing/jess/filesig" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) - -var devVersion *semver.Version - -func init() { - var err error - devVersion, err = semver.NewVersion("0") - if err != nil { - panic(err) - } -} +// import ( +// "errors" +// "io/fs" +// "os" +// "path/filepath" +// "sort" +// "strings" +// "sync" + +// semver "github.com/hashicorp/go-version" + +// "github.com/safing/jess/filesig" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) + +// var devVersion *semver.Version + +// func init() { +// var err error +// devVersion, err = semver.NewVersion("0") +// if err != nil { +// panic(err) +// } +// } // Resource represents a resource (via an identifier) and multiple file versions. -type Resource struct { - sync.Mutex - registry *ResourceRegistry - notifier *notifier - - // Identifier is the unique identifier for that resource. - // It forms a file path using a forward-slash as the - // path separator. - Identifier string - - // Versions holds all available resource versions. - Versions []*ResourceVersion - - // ActiveVersion is the last version of the resource - // that someone requested using GetFile(). - ActiveVersion *ResourceVersion - - // SelectedVersion is newest, selectable version of - // that resource that is available. A version - // is selectable if it's not blacklisted by the user. - // Note that it's not guaranteed that the selected version - // is available locally. In that case, GetFile will attempt - // to download the latest version from the updates servers - // specified in the resource registry. - SelectedVersion *ResourceVersion - - // VerificationOptions holds the verification options for this resource. - VerificationOptions *VerificationOptions - - // Index holds a reference to the index this resource was last defined in. - // Will be nil if resource was only found on disk. - Index *Index -} +// type Resource struct { +// sync.Mutex +// registry *ResourceRegistry +// notifier *notifier + +// // Identifier is the unique identifier for that resource. +// // It forms a file path using a forward-slash as the +// // path separator. +// Identifier string + +// // Versions holds all available resource versions. +// Versions []*ResourceVersion + +// // ActiveVersion is the last version of the resource +// // that someone requested using GetFile(). +// ActiveVersion *ResourceVersion + +// // SelectedVersion is newest, selectable version of +// // that resource that is available. A version +// // is selectable if it's not blacklisted by the user. +// // Note that it's not guaranteed that the selected version +// // is available locally. In that case, GetFile will attempt +// // to download the latest version from the updates servers +// // specified in the resource registry. +// SelectedVersion *ResourceVersion + +// // VerificationOptions holds the verification options for this resource. +// VerificationOptions *VerificationOptions + +// // Index holds a reference to the index this resource was last defined in. +// // Will be nil if resource was only found on disk. +// Index *Index +// } // ResourceVersion represents a single version of a resource. -type ResourceVersion struct { - resource *Resource +// type ResourceVersion struct { +// resource *Resource - // VersionNumber is the string representation of the resource - // version. - VersionNumber string - semVer *semver.Version +// // VersionNumber is the string representation of the resource +// // version. +// VersionNumber string +// semVer *semver.Version - // Available indicates if this version is available locally. - Available bool +// // Available indicates if this version is available locally. +// Available bool - // SigAvailable indicates if the signature of this version is available locally. - SigAvailable bool +// // SigAvailable indicates if the signature of this version is available locally. +// SigAvailable bool - // CurrentRelease indicates that this is the current release that should be - // selected, if possible. - CurrentRelease bool +// // CurrentRelease indicates that this is the current release that should be +// // selected, if possible. +// CurrentRelease bool - // PreRelease indicates that this version is pre-release. - PreRelease bool +// // PreRelease indicates that this version is pre-release. +// PreRelease bool - // Blacklisted may be set to true if this version should - // be skipped and not used. This is useful if the version - // is known to be broken. - Blacklisted bool -} +// // Blacklisted may be set to true if this version should +// // be skipped and not used. This is useful if the version +// // is known to be broken. +// Blacklisted bool +// } -func (rv *ResourceVersion) String() string { - return rv.VersionNumber -} +// func (rv *ResourceVersion) String() string { +// return rv.VersionNumber +// } -// SemVer returns the semantic version of the resource. -func (rv *ResourceVersion) SemVer() *semver.Version { - return rv.semVer -} +// // SemVer returns the semantic version of the resource. +// func (rv *ResourceVersion) SemVer() *semver.Version { +// return rv.semVer +// } // EqualsVersion normalizes the given version and checks equality with semver. -func (rv *ResourceVersion) EqualsVersion(version string) bool { - cmpSemVer, err := semver.NewVersion(version) - if err != nil { - return false - } - - return rv.semVer.Equal(cmpSemVer) -} - -// isSelectable returns true if the version represented by rv is selectable. -// A version is selectable if it's not blacklisted and either already locally -// available or ready to be downloaded. -func (rv *ResourceVersion) isSelectable() bool { - switch { - case rv.Blacklisted: - // Should not be used. - return false - case rv.Available: - // Is available locally, use! - return true - case !rv.resource.registry.Online: - // Cannot download, because registry is set to offline. - return false - case rv.resource.Index == nil: - // Cannot download, because resource is not part of an index. - return false - case !rv.resource.Index.AutoDownload: - // Cannot download, because index may not automatically download. - return false - default: - // Is not available locally, but we are allowed to download it on request! - return true - } -} +// func (rv *ResourceVersion) EqualsVersion(version string) bool { +// cmpSemVer, err := semver.NewVersion(version) +// if err != nil { +// return false +// } + +// return rv.semVer.Equal(cmpSemVer) +// } + +// // isSelectable returns true if the version represented by rv is selectable. +// // A version is selectable if it's not blacklisted and either already locally +// // available or ready to be downloaded. +// func (rv *ResourceVersion) isSelectable() bool { +// switch { +// case rv.Blacklisted: +// // Should not be used. +// return false +// case rv.Available: +// // Is available locally, use! +// return true +// case !rv.resource.registry.Online: +// // Cannot download, because registry is set to offline. +// return false +// case rv.resource.Index == nil: +// // Cannot download, because resource is not part of an index. +// return false +// case !rv.resource.Index.AutoDownload: +// // Cannot download, because index may not automatically download. +// return false +// default: +// // Is not available locally, but we are allowed to download it on request! +// return true +// } +// } // isBetaVersionNumber checks if rv is marked as a beta version by checking // the version string. It does not honor the BetaRelease field of rv! -func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused - // "b" suffix check if for backwards compatibility - // new versions should use the pre-release suffix as - // declared by https://semver.org - // i.e. 1.2.3-beta - switch rv.semVer.Prerelease() { - case "b", "beta": - return true - default: - return false - } -} +// func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused +// // "b" suffix check if for backwards compatibility +// // new versions should use the pre-release suffix as +// // declared by https://semver.org +// // i.e. 1.2.3-beta +// switch rv.semVer.Prerelease() { +// case "b", "beta": +// return true +// default: +// return false +// } +// } // Export makes a copy of the resource with only the exposed information. // Attributes are copied and safe to access. // Any ResourceVersion must not be modified. -func (res *Resource) Export() *Resource { - res.Lock() - defer res.Unlock() - - // Copy attibutes. - export := &Resource{ - Identifier: res.Identifier, - Versions: make([]*ResourceVersion, len(res.Versions)), - ActiveVersion: res.ActiveVersion, - SelectedVersion: res.SelectedVersion, - } - // Copy Versions slice. - copy(export.Versions, res.Versions) - - return export -} - -// Len is the number of elements in the collection. -// It implements sort.Interface for ResourceVersion. -func (res *Resource) Len() int { - return len(res.Versions) -} - -// Less reports whether the element with index i should -// sort before the element with index j. -// It implements sort.Interface for ResourceVersions. -func (res *Resource) Less(i, j int) bool { - return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer) -} - -// Swap swaps the elements with indexes i and j. -// It implements sort.Interface for ResourceVersions. -func (res *Resource) Swap(i, j int) { - res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i] -} - -// available returns whether any version of the resource is available. -func (res *Resource) available() bool { - for _, rv := range res.Versions { - if rv.Available { - return true - } - } - return false -} - -// inUse returns true if the resource is currently in use. -func (res *Resource) inUse() bool { - return res.ActiveVersion != nil -} - -// AnyVersionAvailable returns true if any version of -// res is locally available. -func (res *Resource) AnyVersionAvailable() bool { - res.Lock() - defer res.Unlock() - - return res.available() -} - -func (reg *ResourceRegistry) newResource(identifier string) *Resource { - return &Resource{ - registry: reg, - Identifier: identifier, - Versions: make([]*ResourceVersion, 0, 1), - VerificationOptions: reg.GetVerificationOptions(identifier), - } -} - -// AddVersion adds a resource version to a resource. -func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error { - res.Lock() - defer res.Unlock() - - // reset current release flags - if currentRelease { - for _, rv := range res.Versions { - rv.CurrentRelease = false - } - } - - var rv *ResourceVersion - // check for existing version - for _, possibleMatch := range res.Versions { - if possibleMatch.VersionNumber == version { - rv = possibleMatch - break - } - } - - // create new version if none found - if rv == nil { - // parse to semver - sv, err := semver.NewVersion(version) - if err != nil { - return err - } - - rv = &ResourceVersion{ - resource: res, - VersionNumber: sv.String(), // Use normalized version. - semVer: sv, - } - res.Versions = append(res.Versions, rv) - } - - // set flags - if available { - rv.Available = true - - // If available and signatures are enabled for this resource, check if the - // signature is available. - if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) { - rv.SigAvailable = true - } - } - if currentRelease { - rv.CurrentRelease = true - } - if preRelease || rv.semVer.Prerelease() != "" { - rv.PreRelease = true - } - - return nil -} - -// GetFile returns the selected version as a *File. -func (res *Resource) GetFile() *File { - res.Lock() - defer res.Unlock() - - // check for notifier - if res.notifier == nil { - // create new notifier - res.notifier = newNotifier() - } - - // check if version is selected - if res.SelectedVersion == nil { - res.selectVersion() - } - - // create file - return &File{ - resource: res, - version: res.SelectedVersion, - notifier: res.notifier, - versionedPath: res.SelectedVersion.versionedPath(), - storagePath: res.SelectedVersion.storagePath(), - } -} - -//nolint:gocognit // function already kept as simple as possible -func (res *Resource) selectVersion() { - sort.Sort(res) - - // export after we finish - var fallback bool - defer func() { - if fallback { - log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier) - } else { - log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier) - } - - if res.inUse() && - res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version - res.notifier != nil { - - res.notifier.markAsUpgradeable() - res.notifier = nil - - log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber) - } - }() - - if len(res.Versions) == 0 { - // TODO: find better way to deal with an empty version slice (which should not happen) - res.SelectedVersion = nil - return - } - - // Target selection - - // 1) Dev release if dev mode is active and ignore blacklisting - if res.registry.DevMode { - // Get last version, as this will be v0.0.0, if available. - rv := res.Versions[len(res.Versions)-1] - // Check if it's v0.0.0. - if rv.semVer.Equal(devVersion) && rv.Available { - res.SelectedVersion = rv - return - } - } - - // 2) Find the current release. This may be also be a pre-release. - for _, rv := range res.Versions { - if rv.CurrentRelease { - if rv.isSelectable() { - res.SelectedVersion = rv - return - } - // There can only be once current release, - // so we can abort after finding one. - break - } - } - - // 3) If UsePreReleases is set, find any newest version. - if res.registry.UsePreReleases { - for _, rv := range res.Versions { - if rv.isSelectable() { - res.SelectedVersion = rv - return - } - } - } - - // 4) Find the newest stable version. - for _, rv := range res.Versions { - if !rv.PreRelease && rv.isSelectable() { - res.SelectedVersion = rv - return - } - } - - // 5) Default to newest. - res.SelectedVersion = res.Versions[0] - fallback = true -} - -// Blacklist blacklists the specified version and selects a new version. -func (res *Resource) Blacklist(version string) error { - res.Lock() - defer res.Unlock() - - // count available and valid versions - valid := 0 - for _, rv := range res.Versions { - if rv.semVer.Equal(devVersion) { - continue // ignore dev versions - } - if !rv.Blacklisted { - valid++ - } - } - if valid <= 1 { - return errors.New("cannot blacklist last version") // last one, cannot blacklist! - } - - // find version and blacklist - for _, rv := range res.Versions { - if rv.VersionNumber == version { - // blacklist and update - rv.Blacklisted = true - res.selectVersion() - return nil - } - } - - return errors.New("could not find version") -} - -// Purge deletes old updates, retaining a certain amount, specified by -// the keep parameter. Purge will always keep at least 2 versions so -// specifying a smaller keep value will have no effect. -func (res *Resource) Purge(keepExtra int) { //nolint:gocognit - res.Lock() - defer res.Unlock() - - // If there is any blacklisted version within the resource, pause purging. - // In this case we may need extra available versions beyond what would be - // available after purging. - for _, rv := range res.Versions { - if rv.Blacklisted { - log.Debugf( - "%s: pausing purging of resource %s, as it contains blacklisted items", - res.registry.Name, - rv.resource.Identifier, - ) - return - } - } - - // Safeguard the amount of extra version to keep. - if keepExtra < 2 { - keepExtra = 2 - } - - // Search for purge boundary. - var purgeBoundary int - var skippedActiveVersion bool - var skippedSelectedVersion bool - var skippedStableVersion bool -boundarySearch: - for i, rv := range res.Versions { - // Check if required versions are already skipped. - switch { - case !skippedActiveVersion && res.ActiveVersion != nil: - // Skip versions until the active version, if it's set. - case !skippedSelectedVersion && res.SelectedVersion != nil: - // Skip versions until the selected version, if it's set. - case !skippedStableVersion: - // Skip versions until the stable version. - default: - // All required version skipped, set purge boundary. - purgeBoundary = i + keepExtra - break boundarySearch - } - - // Check if current instance is a required version. - if rv == res.ActiveVersion { - skippedActiveVersion = true - } - if rv == res.SelectedVersion { - skippedSelectedVersion = true - } - if !rv.PreRelease { - skippedStableVersion = true - } - } - - // Check if there is anything to purge at all. - if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) { - return - } - - // Purge everything beyond the purge boundary. - for _, rv := range res.Versions[purgeBoundary:] { - // Only remove if resource file is actually available. - if !rv.Available { - continue - } - - // Remove resource file. - storagePath := rv.storagePath() - err := os.Remove(storagePath) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) - } - } else { - log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) - } - - // Remove resource signature file. - err = os.Remove(rv.storageSigPath()) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) - } - } else { - log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) - } - - // Remove unpacked version of resource. - ext := filepath.Ext(storagePath) - if ext == "" { - // Nothing to do if file does not have an extension. - continue - } - unpackedPath := strings.TrimSuffix(storagePath, ext) - - // Remove if it exists, or an error occurs on access. - _, err = os.Stat(unpackedPath) - if err == nil || !errors.Is(err, fs.ErrNotExist) { - err = os.Remove(unpackedPath) - if err != nil { - log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) - } else { - log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) - } - } - } - - // remove entries of deleted files - res.Versions = res.Versions[purgeBoundary:] -} - -// SigningMetadata returns the metadata to be included in signatures. -func (rv *ResourceVersion) SigningMetadata() map[string]string { - return map[string]string{ - "id": rv.resource.Identifier, - "version": rv.VersionNumber, - } -} - -// GetFile returns the version as a *File. -// It locks the resource for doing so. -func (rv *ResourceVersion) GetFile() *File { - rv.resource.Lock() - defer rv.resource.Unlock() - - // check for notifier - if rv.resource.notifier == nil { - // create new notifier - rv.resource.notifier = newNotifier() - } - - // create file - return &File{ - resource: rv.resource, - version: rv, - notifier: rv.resource.notifier, - versionedPath: rv.versionedPath(), - storagePath: rv.storagePath(), - } -} - -// versionedPath returns the versioned identifier. -func (rv *ResourceVersion) versionedPath() string { - return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) -} - -// versionedSigPath returns the versioned identifier of the file signature. -func (rv *ResourceVersion) versionedSigPath() string { - return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension -} - -// storagePath returns the absolute storage path. -func (rv *ResourceVersion) storagePath() string { - return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath())) -} - -// storageSigPath returns the absolute storage path of the file signature. -func (rv *ResourceVersion) storageSigPath() string { - return rv.storagePath() + filesig.Extension -} +// func (res *Resource) Export() *Resource { +// res.Lock() +// defer res.Unlock() + +// // Copy attibutes. +// export := &Resource{ +// Identifier: res.Identifier, +// Versions: make([]*ResourceVersion, len(res.Versions)), +// ActiveVersion: res.ActiveVersion, +// SelectedVersion: res.SelectedVersion, +// } +// // Copy Versions slice. +// copy(export.Versions, res.Versions) + +// return export +// } + +// // Len is the number of elements in the collection. +// // It implements sort.Interface for ResourceVersion. +// func (res *Resource) Len() int { +// return len(res.Versions) +// } + +// // Less reports whether the element with index i should +// // sort before the element with index j. +// // It implements sort.Interface for ResourceVersions. +// func (res *Resource) Less(i, j int) bool { +// return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer) +// } + +// // Swap swaps the elements with indexes i and j. +// // It implements sort.Interface for ResourceVersions. +// func (res *Resource) Swap(i, j int) { +// res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i] +// } + +// // available returns whether any version of the resource is available. +// func (res *Resource) available() bool { +// for _, rv := range res.Versions { +// if rv.Available { +// return true +// } +// } +// return false +// } + +// // inUse returns true if the resource is currently in use. +// func (res *Resource) inUse() bool { +// return res.ActiveVersion != nil +// } + +// // AnyVersionAvailable returns true if any version of +// // res is locally available. +// func (res *Resource) AnyVersionAvailable() bool { +// res.Lock() +// defer res.Unlock() + +// return res.available() +// } + +// func (reg *ResourceRegistry) newResource(identifier string) *Resource { +// return &Resource{ +// registry: reg, +// Identifier: identifier, +// Versions: make([]*ResourceVersion, 0, 1), +// VerificationOptions: reg.GetVerificationOptions(identifier), +// } +// } + +// // AddVersion adds a resource version to a resource. +// func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error { +// res.Lock() +// defer res.Unlock() + +// // reset current release flags +// if currentRelease { +// for _, rv := range res.Versions { +// rv.CurrentRelease = false +// } +// } + +// var rv *ResourceVersion +// // check for existing version +// for _, possibleMatch := range res.Versions { +// if possibleMatch.VersionNumber == version { +// rv = possibleMatch +// break +// } +// } + +// // create new version if none found +// if rv == nil { +// // parse to semver +// sv, err := semver.NewVersion(version) +// if err != nil { +// return err +// } + +// rv = &ResourceVersion{ +// resource: res, +// VersionNumber: sv.String(), // Use normalized version. +// semVer: sv, +// } +// res.Versions = append(res.Versions, rv) +// } + +// // set flags +// if available { +// rv.Available = true + +// // If available and signatures are enabled for this resource, check if the +// // signature is available. +// if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) { +// rv.SigAvailable = true +// } +// } +// if currentRelease { +// rv.CurrentRelease = true +// } +// if preRelease || rv.semVer.Prerelease() != "" { +// rv.PreRelease = true +// } + +// return nil +// } + +// // GetFile returns the selected version as a *File. +// func (res *Resource) GetFile() *File { +// res.Lock() +// defer res.Unlock() + +// // check for notifier +// if res.notifier == nil { +// // create new notifier +// res.notifier = newNotifier() +// } + +// // check if version is selected +// if res.SelectedVersion == nil { +// res.selectVersion() +// } + +// // create file +// return &File{ +// resource: res, +// version: res.SelectedVersion, +// notifier: res.notifier, +// versionedPath: res.SelectedVersion.versionedPath(), +// storagePath: res.SelectedVersion.storagePath(), +// } +// } + +// //nolint:gocognit // function already kept as simple as possible +// func (res *Resource) selectVersion() { +// sort.Sort(res) + +// // export after we finish +// var fallback bool +// defer func() { +// if fallback { +// log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier) +// } else { +// log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier) +// } + +// if res.inUse() && +// res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version +// res.notifier != nil { + +// res.notifier.markAsUpgradeable() +// res.notifier = nil + +// log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber) +// } +// }() + +// if len(res.Versions) == 0 { +// // TODO: find better way to deal with an empty version slice (which should not happen) +// res.SelectedVersion = nil +// return +// } + +// // Target selection + +// // 1) Dev release if dev mode is active and ignore blacklisting +// if res.registry.DevMode { +// // Get last version, as this will be v0.0.0, if available. +// rv := res.Versions[len(res.Versions)-1] +// // Check if it's v0.0.0. +// if rv.semVer.Equal(devVersion) && rv.Available { +// res.SelectedVersion = rv +// return +// } +// } + +// // 2) Find the current release. This may be also be a pre-release. +// for _, rv := range res.Versions { +// if rv.CurrentRelease { +// if rv.isSelectable() { +// res.SelectedVersion = rv +// return +// } +// // There can only be once current release, +// // so we can abort after finding one. +// break +// } +// } + +// // 3) If UsePreReleases is set, find any newest version. +// if res.registry.UsePreReleases { +// for _, rv := range res.Versions { +// if rv.isSelectable() { +// res.SelectedVersion = rv +// return +// } +// } +// } + +// // 4) Find the newest stable version. +// for _, rv := range res.Versions { +// if !rv.PreRelease && rv.isSelectable() { +// res.SelectedVersion = rv +// return +// } +// } + +// // 5) Default to newest. +// res.SelectedVersion = res.Versions[0] +// fallback = true +// } + +// // Blacklist blacklists the specified version and selects a new version. +// func (res *Resource) Blacklist(version string) error { +// res.Lock() +// defer res.Unlock() + +// // count available and valid versions +// valid := 0 +// for _, rv := range res.Versions { +// if rv.semVer.Equal(devVersion) { +// continue // ignore dev versions +// } +// if !rv.Blacklisted { +// valid++ +// } +// } +// if valid <= 1 { +// return errors.New("cannot blacklist last version") // last one, cannot blacklist! +// } + +// // find version and blacklist +// for _, rv := range res.Versions { +// if rv.VersionNumber == version { +// // blacklist and update +// rv.Blacklisted = true +// res.selectVersion() +// return nil +// } +// } + +// return errors.New("could not find version") +// } + +// // Purge deletes old updates, retaining a certain amount, specified by +// // the keep parameter. Purge will always keep at least 2 versions so +// // specifying a smaller keep value will have no effect. +// func (res *Resource) Purge(keepExtra int) { //nolint:gocognit +// res.Lock() +// defer res.Unlock() + +// // If there is any blacklisted version within the resource, pause purging. +// // In this case we may need extra available versions beyond what would be +// // available after purging. +// for _, rv := range res.Versions { +// if rv.Blacklisted { +// log.Debugf( +// "%s: pausing purging of resource %s, as it contains blacklisted items", +// res.registry.Name, +// rv.resource.Identifier, +// ) +// return +// } +// } + +// // Safeguard the amount of extra version to keep. +// if keepExtra < 2 { +// keepExtra = 2 +// } + +// // Search for purge boundary. +// var purgeBoundary int +// var skippedActiveVersion bool +// var skippedSelectedVersion bool +// var skippedStableVersion bool +// boundarySearch: +// for i, rv := range res.Versions { +// // Check if required versions are already skipped. +// switch { +// case !skippedActiveVersion && res.ActiveVersion != nil: +// // Skip versions until the active version, if it's set. +// case !skippedSelectedVersion && res.SelectedVersion != nil: +// // Skip versions until the selected version, if it's set. +// case !skippedStableVersion: +// // Skip versions until the stable version. +// default: +// // All required version skipped, set purge boundary. +// purgeBoundary = i + keepExtra +// break boundarySearch +// } + +// // Check if current instance is a required version. +// if rv == res.ActiveVersion { +// skippedActiveVersion = true +// } +// if rv == res.SelectedVersion { +// skippedSelectedVersion = true +// } +// if !rv.PreRelease { +// skippedStableVersion = true +// } +// } + +// // Check if there is anything to purge at all. +// if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) { +// return +// } + +// // Purge everything beyond the purge boundary. +// for _, rv := range res.Versions[purgeBoundary:] { +// // Only remove if resource file is actually available. +// if !rv.Available { +// continue +// } + +// // Remove resource file. +// storagePath := rv.storagePath() +// err := os.Remove(storagePath) +// if err != nil { +// if !errors.Is(err, fs.ErrNotExist) { +// log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) +// } +// } else { +// log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) +// } + +// // Remove resource signature file. +// err = os.Remove(rv.storageSigPath()) +// if err != nil { +// if !errors.Is(err, fs.ErrNotExist) { +// log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) +// } +// } else { +// log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) +// } + +// // Remove unpacked version of resource. +// ext := filepath.Ext(storagePath) +// if ext == "" { +// // Nothing to do if file does not have an extension. +// continue +// } +// unpackedPath := strings.TrimSuffix(storagePath, ext) + +// // Remove if it exists, or an error occurs on access. +// _, err = os.Stat(unpackedPath) +// if err == nil || !errors.Is(err, fs.ErrNotExist) { +// err = os.Remove(unpackedPath) +// if err != nil { +// log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) +// } else { +// log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) +// } +// } +// } + +// // remove entries of deleted files +// res.Versions = res.Versions[purgeBoundary:] +// } + +// // SigningMetadata returns the metadata to be included in signatures. +// func (rv *ResourceVersion) SigningMetadata() map[string]string { +// return map[string]string{ +// "id": rv.resource.Identifier, +// "version": rv.VersionNumber, +// } +// } + +// // GetFile returns the version as a *File. +// // It locks the resource for doing so. +// func (rv *ResourceVersion) GetFile() *File { +// rv.resource.Lock() +// defer rv.resource.Unlock() + +// // check for notifier +// if rv.resource.notifier == nil { +// // create new notifier +// rv.resource.notifier = newNotifier() +// } + +// // create file +// return &File{ +// resource: rv.resource, +// version: rv, +// notifier: rv.resource.notifier, +// versionedPath: rv.versionedPath(), +// storagePath: rv.storagePath(), +// } +// } + +// // versionedPath returns the versioned identifier. +// func (rv *ResourceVersion) versionedPath() string { +// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) +// } + +// // versionedSigPath returns the versioned identifier of the file signature. +// func (rv *ResourceVersion) versionedSigPath() string { +// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension +// } + +// // storagePath returns the absolute storage path. +// func (rv *ResourceVersion) storagePath() string { +// return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath())) +// } + +// // storageSigPath returns the absolute storage path of the file signature. +// func (rv *ResourceVersion) storageSigPath() string { +// return rv.storagePath() + filesig.Extension +// } diff --git a/base/updater/signing.go b/base/updater/signing.go index cffd5cbed..85f4eb6fb 100644 --- a/base/updater/signing.go +++ b/base/updater/signing.go @@ -1,49 +1,49 @@ package updater -import ( - "strings" - - "github.com/safing/jess" -) - -// VerificationOptions holds options for verification of files. -type VerificationOptions struct { - TrustStore jess.TrustStore - DownloadPolicy SignaturePolicy - DiskLoadPolicy SignaturePolicy -} - -// GetVerificationOptions returns the verification options for the given identifier. -func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions { - if reg.Verification == nil { - return nil - } - - var ( - longestPrefix = -1 - bestMatch *VerificationOptions - ) - for prefix, opts := range reg.Verification { - if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) { - longestPrefix = len(prefix) - bestMatch = opts - } - } - - return bestMatch -} - -// SignaturePolicy defines behavior in case of errors. -type SignaturePolicy uint8 - -// Signature Policies. -const ( - // SignaturePolicyRequire fails on any error. - SignaturePolicyRequire = iota - - // SignaturePolicyWarn only warns on errors. - SignaturePolicyWarn - - // SignaturePolicyDisable only downloads signatures, but does not verify them. - SignaturePolicyDisable -) +// import ( +// "strings" + +// "github.com/safing/jess" +// ) + +// // VerificationOptions holds options for verification of files. +// type VerificationOptions struct { +// TrustStore jess.TrustStore +// DownloadPolicy SignaturePolicy +// DiskLoadPolicy SignaturePolicy +// } + +// // GetVerificationOptions returns the verification options for the given identifier. +// func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions { +// if reg.Verification == nil { +// return nil +// } + +// var ( +// longestPrefix = -1 +// bestMatch *VerificationOptions +// ) +// for prefix, opts := range reg.Verification { +// if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) { +// longestPrefix = len(prefix) +// bestMatch = opts +// } +// } + +// return bestMatch +// } + +// // SignaturePolicy defines behavior in case of errors. +// type SignaturePolicy uint8 + +// // Signature Policies. +// const ( +// // SignaturePolicyRequire fails on any error. +// SignaturePolicyRequire = iota + +// // SignaturePolicyWarn only warns on errors. +// SignaturePolicyWarn + +// // SignaturePolicyDisable only downloads signatures, but does not verify them. +// SignaturePolicyDisable +// ) diff --git a/base/updater/state.go b/base/updater/state.go index 20c27f465..8cbada6f8 100644 --- a/base/updater/state.go +++ b/base/updater/state.go @@ -1,180 +1,180 @@ package updater -import ( - "sort" - "sync" - "time" - - "github.com/safing/portmaster/base/utils" -) - -// Registry States. -const ( - StateReady = "ready" // Default idle state. - StateChecking = "checking" // Downloading indexes. - StateDownloading = "downloading" // Downloading updates. - StateFetching = "fetching" // Fetching a single file. -) - -// RegistryState describes the registry state. -type RegistryState struct { - sync.Mutex - reg *ResourceRegistry - - // ID holds the ID of the state the registry is currently in. - ID string - - // Details holds further information about the current state. - Details any - - // Updates holds generic information about the current status of pending - // and recently downloaded updates. - Updates UpdateState - - // operationLock locks the operation of any state changing operation. - // This is separate from the registry lock, which locks access to the - // registry struct. - operationLock sync.Mutex -} - -// StateDownloadingDetails holds details of the downloading state. -type StateDownloadingDetails struct { - // Resources holds the resource IDs that are being downloaded. - Resources []string - - // FinishedUpTo holds the index of Resources that is currently being - // downloaded. Previous resources have finished downloading. - FinishedUpTo int -} - -// UpdateState holds generic information about the current status of pending -// and recently downloaded updates. -type UpdateState struct { - // LastCheckAt holds the time of the last update check. - LastCheckAt *time.Time - // LastCheckError holds the error of the last check. - LastCheckError error - // PendingDownload holds the resources that are pending download. - PendingDownload []string - - // LastDownloadAt holds the time when resources were downloaded the last time. - LastDownloadAt *time.Time - // LastDownloadError holds the error of the last download. - LastDownloadError error - // LastDownload holds the resources that we downloaded the last time updates - // were downloaded. - LastDownload []string - - // LastSuccessAt holds the time of the last successful update (check). - LastSuccessAt *time.Time -} - -// GetState returns the current registry state. -// The returned data must not be modified. -func (reg *ResourceRegistry) GetState() RegistryState { - reg.state.Lock() - defer reg.state.Unlock() - - return RegistryState{ - ID: reg.state.ID, - Details: reg.state.Details, - Updates: reg.state.Updates, - } -} - -// StartOperation starts an operation. -func (s *RegistryState) StartOperation(id string) bool { - defer s.notify() - - s.operationLock.Lock() - - s.Lock() - defer s.Unlock() - - s.ID = id - return true -} - -// UpdateOperationDetails updates the details of an operation. -// The supplied struct should be a copy and must not be changed after calling -// this function. -func (s *RegistryState) UpdateOperationDetails(details any) { - defer s.notify() - - s.Lock() - defer s.Unlock() - - s.Details = details -} - -// EndOperation ends an operation. -func (s *RegistryState) EndOperation() { - defer s.notify() - defer s.operationLock.Unlock() - - s.Lock() - defer s.Unlock() - - s.ID = StateReady - s.Details = nil -} - -// ReportUpdateCheck reports an update check to the registry state. -func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) { - defer s.notify() - - sort.Strings(pendingDownload) - - s.Lock() - defer s.Unlock() - - now := time.Now() - s.Updates.LastCheckAt = &now - s.Updates.LastCheckError = failed - s.Updates.PendingDownload = pendingDownload - - if failed == nil { - s.Updates.LastSuccessAt = &now - } -} - -// ReportDownloads reports downloaded updates to the registry state. -func (s *RegistryState) ReportDownloads(downloaded []string, failed error) { - defer s.notify() - - sort.Strings(downloaded) - - s.Lock() - defer s.Unlock() - - now := time.Now() - s.Updates.LastDownloadAt = &now - s.Updates.LastDownloadError = failed - s.Updates.LastDownload = downloaded - - // Remove downloaded resources from the pending list. - if len(s.Updates.PendingDownload) > 0 { - newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload)) - for _, pending := range s.Updates.PendingDownload { - if !utils.StringInSlice(downloaded, pending) { - newPendingDownload = append(newPendingDownload, pending) - } - } - s.Updates.PendingDownload = newPendingDownload - } - - if failed == nil { - s.Updates.LastSuccessAt = &now - } -} - -func (s *RegistryState) notify() { - switch { - case s.reg == nil: - return - case s.reg.StateNotifyFunc == nil: - return - } - - s.reg.StateNotifyFunc(s) -} +// import ( +// "sort" +// "sync" +// "time" + +// "github.com/safing/portmaster/base/utils" +// ) + +// // Registry States. +// const ( +// StateReady = "ready" // Default idle state. +// StateChecking = "checking" // Downloading indexes. +// StateDownloading = "downloading" // Downloading updates. +// StateFetching = "fetching" // Fetching a single file. +// ) + +// // RegistryState describes the registry state. +// type RegistryState struct { +// sync.Mutex +// reg *ResourceRegistry + +// // ID holds the ID of the state the registry is currently in. +// ID string + +// // Details holds further information about the current state. +// Details any + +// // Updates holds generic information about the current status of pending +// // and recently downloaded updates. +// Updates UpdateState + +// // operationLock locks the operation of any state changing operation. +// // This is separate from the registry lock, which locks access to the +// // registry struct. +// operationLock sync.Mutex +// } + +// // StateDownloadingDetails holds details of the downloading state. +// type StateDownloadingDetails struct { +// // Resources holds the resource IDs that are being downloaded. +// Resources []string + +// // FinishedUpTo holds the index of Resources that is currently being +// // downloaded. Previous resources have finished downloading. +// FinishedUpTo int +// } + +// // UpdateState holds generic information about the current status of pending +// // and recently downloaded updates. +// type UpdateState struct { +// // LastCheckAt holds the time of the last update check. +// LastCheckAt *time.Time +// // LastCheckError holds the error of the last check. +// LastCheckError error +// // PendingDownload holds the resources that are pending download. +// PendingDownload []string + +// // LastDownloadAt holds the time when resources were downloaded the last time. +// LastDownloadAt *time.Time +// // LastDownloadError holds the error of the last download. +// LastDownloadError error +// // LastDownload holds the resources that we downloaded the last time updates +// // were downloaded. +// LastDownload []string + +// // LastSuccessAt holds the time of the last successful update (check). +// LastSuccessAt *time.Time +// } + +// // GetState returns the current registry state. +// // The returned data must not be modified. +// func (reg *ResourceRegistry) GetState() RegistryState { +// reg.state.Lock() +// defer reg.state.Unlock() + +// return RegistryState{ +// ID: reg.state.ID, +// Details: reg.state.Details, +// Updates: reg.state.Updates, +// } +// } + +// // StartOperation starts an operation. +// func (s *RegistryState) StartOperation(id string) bool { +// defer s.notify() + +// s.operationLock.Lock() + +// s.Lock() +// defer s.Unlock() + +// s.ID = id +// return true +// } + +// // UpdateOperationDetails updates the details of an operation. +// // The supplied struct should be a copy and must not be changed after calling +// // this function. +// func (s *RegistryState) UpdateOperationDetails(details any) { +// defer s.notify() + +// s.Lock() +// defer s.Unlock() + +// s.Details = details +// } + +// // EndOperation ends an operation. +// func (s *RegistryState) EndOperation() { +// defer s.notify() +// defer s.operationLock.Unlock() + +// s.Lock() +// defer s.Unlock() + +// s.ID = StateReady +// s.Details = nil +// } + +// // ReportUpdateCheck reports an update check to the registry state. +// func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) { +// defer s.notify() + +// sort.Strings(pendingDownload) + +// s.Lock() +// defer s.Unlock() + +// now := time.Now() +// s.Updates.LastCheckAt = &now +// s.Updates.LastCheckError = failed +// s.Updates.PendingDownload = pendingDownload + +// if failed == nil { +// s.Updates.LastSuccessAt = &now +// } +// } + +// // ReportDownloads reports downloaded updates to the registry state. +// func (s *RegistryState) ReportDownloads(downloaded []string, failed error) { +// defer s.notify() + +// sort.Strings(downloaded) + +// s.Lock() +// defer s.Unlock() + +// now := time.Now() +// s.Updates.LastDownloadAt = &now +// s.Updates.LastDownloadError = failed +// s.Updates.LastDownload = downloaded + +// // Remove downloaded resources from the pending list. +// if len(s.Updates.PendingDownload) > 0 { +// newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload)) +// for _, pending := range s.Updates.PendingDownload { +// if !utils.StringInSlice(downloaded, pending) { +// newPendingDownload = append(newPendingDownload, pending) +// } +// } +// s.Updates.PendingDownload = newPendingDownload +// } + +// if failed == nil { +// s.Updates.LastSuccessAt = &now +// } +// } + +// func (s *RegistryState) notify() { +// switch { +// case s.reg == nil: +// return +// case s.reg.StateNotifyFunc == nil: +// return +// } + +// s.reg.StateNotifyFunc(s) +// } diff --git a/base/updater/storage.go b/base/updater/storage.go index cd05bdbda..8cfe84444 100644 --- a/base/updater/storage.go +++ b/base/updater/storage.go @@ -1,272 +1,272 @@ package updater -import ( - "context" - "errors" - "fmt" - "io/fs" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/safing/jess/filesig" - "github.com/safing/jess/lhash" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) - -// ScanStorage scans root within the storage dir and adds found -// resources to the registry. If an error occurred, it is logged -// and the last error is returned. Everything that was found -// despite errors is added to the registry anyway. Leave root -// empty to scan the full storage dir. -func (reg *ResourceRegistry) ScanStorage(root string) error { - var lastError error - - // prep root - if root == "" { - root = reg.storageDir.Path - } else { - var err error - root, err = filepath.Abs(root) - if err != nil { - return err - } - if !strings.HasPrefix(root, reg.storageDir.Path) { - return errors.New("supplied scan root path not within storage") - } - } - - // walk fs - _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - // skip tmp dir (including errors trying to read it) - if strings.HasPrefix(path, reg.tmpDir.Path) { - return filepath.SkipDir - } - - // handle walker error - if err != nil { - lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err) - log.Warning(lastError.Error()) - return nil - } - - // Ignore file signatures. - if strings.HasSuffix(path, filesig.Extension) { - return nil - } - - // get relative path to storage - relativePath, err := filepath.Rel(reg.storageDir.Path, path) - if err != nil { - lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err) - log.Warning(lastError.Error()) - return nil - } - - // convert to identifier and version - relativePath = filepath.ToSlash(relativePath) - identifier, version, ok := GetIdentifierAndVersion(relativePath) - if !ok { - // file does not conform to format - return nil - } - - // fully ignore directories that also have an identifier - these will be unpacked resources - if info.IsDir() { - return filepath.SkipDir - } - - // save - err = reg.AddResource(identifier, version, nil, true, false, false) - if err != nil { - lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err) - log.Warning(lastError.Error()) - } - return nil - }) - - return lastError -} - -// LoadIndexes loads the current release indexes from disk -// or will fetch a new version if not available and the -// registry is marked as online. -func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error { - var firstErr error - client := &http.Client{} - for _, idx := range reg.getIndexes() { - err := reg.loadIndexFile(idx) - if err == nil { - log.Debugf("%s: loaded index %s", reg.Name, idx.Path) - } else if reg.Online { - // try to download the index file if a local disk version - // does not exist or we don't have permission to read it. - if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { - err = reg.downloadIndex(ctx, client, idx) - } - } - - if err != nil && firstErr == nil { - firstErr = err - } - } - - return firstErr -} - -// getIndexes returns a copy of the index. -// The indexes itself are references. -func (reg *ResourceRegistry) getIndexes() []*Index { - reg.RLock() - defer reg.RUnlock() - - indexes := make([]*Index, len(reg.indexes)) - copy(indexes, reg.indexes) - return indexes -} - -func (reg *ResourceRegistry) loadIndexFile(idx *Index) error { - indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)) - indexData, err := os.ReadFile(indexPath) - if err != nil { - return fmt.Errorf("failed to read index file %s: %w", idx.Path, err) - } - - // Verify signature, if enabled. - if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil { - // Load and check signature. - verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension) - if err != nil { - switch verifOpts.DiskLoadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) - } - } - - // Check if signature checksum matches the index data. - if err == nil && !verifiedHash.Matches(indexData) { - switch verifOpts.DiskLoadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("index file %s does not match signature", idx.Path) - case SignaturePolicyWarn: - log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path) - case SignaturePolicyDisable: - log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path) - } - } - } - - // Parse the index file. - indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) - if err != nil { - return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err) - } - - // Update last seen release. - idx.LastRelease = indexFile.Published - - // Warn if there aren't any releases in the index. - if len(indexFile.Releases) == 0 { - log.Debugf("%s: index %s has no releases", reg.Name, idx.Path) - return nil - } - - // Add index releases to available resources. - err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease) - if err != nil { - log.Warningf("%s: failed to add resource: %s", reg.Name, err) - } - return nil -} - -func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) { - // Load signature file. - sigFileData, err := os.ReadFile(sigFilePath) - if err != nil { - return nil, nil, fmt.Errorf("failed to read signature file: %w", err) - } - - // Extract all signatures. - sigs, err := filesig.ParseSigFile(sigFileData) - switch { - case len(sigs) == 0 && err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - case len(sigs) == 0: - return nil, nil, errors.New("no signatures found in signature file") - case err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - } - - // Verify all signatures. - var verifiedHash *lhash.LabeledHash - for _, sig := range sigs { - fd, err := filesig.VerifyFileData( - sig, - nil, - verifOpts.TrustStore, - ) - if err != nil { - return nil, sigFileData, err - } - - // Save or check verified hash. - if verifiedHash == nil { - verifiedHash = fd.FileHash() - } else if !fd.FileHash().Equal(verifiedHash) { - // Return an error if two valid hashes mismatch. - // For simplicity, all hash algorithms must be the same for now. - return nil, sigFileData, errors.New("file hashes from different signatures do not match") - } - } - - return verifiedHash, sigFileData, nil -} - -// CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list. -func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error { - err := os.RemoveAll(symlinkRoot.Path) - if err != nil { - return fmt.Errorf("failed to wipe symlink root: %w", err) - } - - err = symlinkRoot.Ensure() - if err != nil { - return fmt.Errorf("failed to create symlink root: %w", err) - } - - reg.RLock() - defer reg.RUnlock() - - for _, res := range reg.resources { - if res.SelectedVersion == nil { - return fmt.Errorf("no selected version available for %s", res.Identifier) - } - - targetPath := res.SelectedVersion.storagePath() - linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier)) - linkPathDir := filepath.Dir(linkPath) - - err = symlinkRoot.EnsureAbsPath(linkPathDir) - if err != nil { - return fmt.Errorf("failed to create dir for link: %w", err) - } - - relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath) - if err != nil { - return fmt.Errorf("failed to get relative target path: %w", err) - } - - err = os.Symlink(relativeTargetPath, linkPath) - if err != nil { - return fmt.Errorf("failed to link %s: %w", res.Identifier, err) - } - } - - return nil -} +// import ( +// "context" +// "errors" +// "fmt" +// "io/fs" +// "net/http" +// "os" +// "path/filepath" +// "strings" + +// "github.com/safing/jess/filesig" +// "github.com/safing/jess/lhash" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) + +// // ScanStorage scans root within the storage dir and adds found +// // resources to the registry. If an error occurred, it is logged +// // and the last error is returned. Everything that was found +// // despite errors is added to the registry anyway. Leave root +// // empty to scan the full storage dir. +// func (reg *ResourceRegistry) ScanStorage(root string) error { +// var lastError error + +// // prep root +// if root == "" { +// root = reg.storageDir.Path +// } else { +// var err error +// root, err = filepath.Abs(root) +// if err != nil { +// return err +// } +// if !strings.HasPrefix(root, reg.storageDir.Path) { +// return errors.New("supplied scan root path not within storage") +// } +// } + +// // walk fs +// _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { +// // skip tmp dir (including errors trying to read it) +// if strings.HasPrefix(path, reg.tmpDir.Path) { +// return filepath.SkipDir +// } + +// // handle walker error +// if err != nil { +// lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err) +// log.Warning(lastError.Error()) +// return nil +// } + +// // Ignore file signatures. +// if strings.HasSuffix(path, filesig.Extension) { +// return nil +// } + +// // get relative path to storage +// relativePath, err := filepath.Rel(reg.storageDir.Path, path) +// if err != nil { +// lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err) +// log.Warning(lastError.Error()) +// return nil +// } + +// // convert to identifier and version +// relativePath = filepath.ToSlash(relativePath) +// identifier, version, ok := GetIdentifierAndVersion(relativePath) +// if !ok { +// // file does not conform to format +// return nil +// } + +// // fully ignore directories that also have an identifier - these will be unpacked resources +// if info.IsDir() { +// return filepath.SkipDir +// } + +// // save +// err = reg.AddResource(identifier, version, nil, true, false, false) +// if err != nil { +// lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err) +// log.Warning(lastError.Error()) +// } +// return nil +// }) + +// return lastError +// } + +// // LoadIndexes loads the current release indexes from disk +// // or will fetch a new version if not available and the +// // registry is marked as online. +// func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error { +// var firstErr error +// client := &http.Client{} +// for _, idx := range reg.getIndexes() { +// err := reg.loadIndexFile(idx) +// if err == nil { +// log.Debugf("%s: loaded index %s", reg.Name, idx.Path) +// } else if reg.Online { +// // try to download the index file if a local disk version +// // does not exist or we don't have permission to read it. +// if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { +// err = reg.downloadIndex(ctx, client, idx) +// } +// } + +// if err != nil && firstErr == nil { +// firstErr = err +// } +// } + +// return firstErr +// } + +// // getIndexes returns a copy of the index. +// // The indexes itself are references. +// func (reg *ResourceRegistry) getIndexes() []*Index { +// reg.RLock() +// defer reg.RUnlock() + +// indexes := make([]*Index, len(reg.indexes)) +// copy(indexes, reg.indexes) +// return indexes +// } + +// func (reg *ResourceRegistry) loadIndexFile(idx *Index) error { +// indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)) +// indexData, err := os.ReadFile(indexPath) +// if err != nil { +// return fmt.Errorf("failed to read index file %s: %w", idx.Path, err) +// } + +// // Verify signature, if enabled. +// if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil { +// // Load and check signature. +// verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension) +// if err != nil { +// switch verifOpts.DiskLoadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) +// } +// } + +// // Check if signature checksum matches the index data. +// if err == nil && !verifiedHash.Matches(indexData) { +// switch verifOpts.DiskLoadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("index file %s does not match signature", idx.Path) +// case SignaturePolicyWarn: +// log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path) +// case SignaturePolicyDisable: +// log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path) +// } +// } +// } + +// // Parse the index file. +// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) +// if err != nil { +// return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err) +// } + +// // Update last seen release. +// idx.LastRelease = indexFile.Published + +// // Warn if there aren't any releases in the index. +// if len(indexFile.Releases) == 0 { +// log.Debugf("%s: index %s has no releases", reg.Name, idx.Path) +// return nil +// } + +// // Add index releases to available resources. +// err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease) +// if err != nil { +// log.Warningf("%s: failed to add resource: %s", reg.Name, err) +// } +// return nil +// } + +// func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) { +// // Load signature file. +// sigFileData, err := os.ReadFile(sigFilePath) +// if err != nil { +// return nil, nil, fmt.Errorf("failed to read signature file: %w", err) +// } + +// // Extract all signatures. +// sigs, err := filesig.ParseSigFile(sigFileData) +// switch { +// case len(sigs) == 0 && err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// case len(sigs) == 0: +// return nil, nil, errors.New("no signatures found in signature file") +// case err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// } + +// // Verify all signatures. +// var verifiedHash *lhash.LabeledHash +// for _, sig := range sigs { +// fd, err := filesig.VerifyFileData( +// sig, +// nil, +// verifOpts.TrustStore, +// ) +// if err != nil { +// return nil, sigFileData, err +// } + +// // Save or check verified hash. +// if verifiedHash == nil { +// verifiedHash = fd.FileHash() +// } else if !fd.FileHash().Equal(verifiedHash) { +// // Return an error if two valid hashes mismatch. +// // For simplicity, all hash algorithms must be the same for now. +// return nil, sigFileData, errors.New("file hashes from different signatures do not match") +// } +// } + +// return verifiedHash, sigFileData, nil +// } + +// // CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list. +// func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error { +// err := os.RemoveAll(symlinkRoot.Path) +// if err != nil { +// return fmt.Errorf("failed to wipe symlink root: %w", err) +// } + +// err = symlinkRoot.Ensure() +// if err != nil { +// return fmt.Errorf("failed to create symlink root: %w", err) +// } + +// reg.RLock() +// defer reg.RUnlock() + +// for _, res := range reg.resources { +// if res.SelectedVersion == nil { +// return fmt.Errorf("no selected version available for %s", res.Identifier) +// } + +// targetPath := res.SelectedVersion.storagePath() +// linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier)) +// linkPathDir := filepath.Dir(linkPath) + +// err = symlinkRoot.EnsureAbsPath(linkPathDir) +// if err != nil { +// return fmt.Errorf("failed to create dir for link: %w", err) +// } + +// relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath) +// if err != nil { +// return fmt.Errorf("failed to get relative target path: %w", err) +// } + +// err = os.Symlink(relativeTargetPath, linkPath) +// if err != nil { +// return fmt.Errorf("failed to link %s: %w", res.Identifier, err) +// } +// } + +// return nil +// } diff --git a/base/updater/unpacking.go b/base/updater/unpacking.go index 75d489212..5b157695c 100644 --- a/base/updater/unpacking.go +++ b/base/updater/unpacking.go @@ -1,195 +1,195 @@ package updater -import ( - "archive/zip" - "compress/gzip" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path" - "path/filepath" - "strings" - - "github.com/hashicorp/go-multierror" - - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) - -// MaxUnpackSize specifies the maximum size that will be unpacked. -const MaxUnpackSize = 1000000000 // 1GB - -// UnpackGZIP unpacks a GZIP compressed reader r -// and returns a new reader. It's suitable to be -// used with registry.GetPackedFile. -func UnpackGZIP(r io.Reader) (io.Reader, error) { - return gzip.NewReader(r) -} - -// UnpackResources unpacks all resources defined in the AutoUnpack list. -func (reg *ResourceRegistry) UnpackResources() error { - reg.RLock() - defer reg.RUnlock() - - var multierr *multierror.Error - for _, res := range reg.resources { - if utils.StringInSlice(reg.AutoUnpack, res.Identifier) { - err := res.UnpackArchive() - if err != nil { - multierr = multierror.Append( - multierr, - fmt.Errorf("%s: %w", res.Identifier, err), - ) - } - } - } - - return multierr.ErrorOrNil() -} - -const ( - zipSuffix = ".zip" -) - -// UnpackArchive unpacks the archive the resource refers to. The contents are -// unpacked into a directory with the same name as the file, excluding the -// suffix. If the destination folder already exists, it is assumed that the -// contents have already been correctly unpacked. -func (res *Resource) UnpackArchive() error { - res.Lock() - defer res.Unlock() - - // Only unpack selected versions. - if res.SelectedVersion == nil { - return nil - } - - switch { - case strings.HasSuffix(res.Identifier, zipSuffix): - return res.unpackZipArchive() - default: - return fmt.Errorf("unsupported file type for unpacking") - } -} - -func (res *Resource) unpackZipArchive() error { - // Get file and directory paths. - archiveFile := res.SelectedVersion.storagePath() - destDir := strings.TrimSuffix(archiveFile, zipSuffix) - tmpDir := filepath.Join( - res.registry.tmpDir.Path, - filepath.FromSlash(strings.TrimSuffix( - path.Base(res.SelectedVersion.versionedPath()), - zipSuffix, - )), - ) - - // Check status of destination. - dstStat, err := os.Stat(destDir) - switch { - case errors.Is(err, fs.ErrNotExist): - // The destination does not exist, continue with unpacking. - case err != nil: - return fmt.Errorf("cannot access destination for unpacking: %w", err) - case !dstStat.IsDir(): - return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name()) - default: - // Archive already seems to be unpacked. - return nil - } - - // Create the tmp directory for unpacking. - err = res.registry.tmpDir.EnsureAbsPath(tmpDir) - if err != nil { - return fmt.Errorf("failed to create tmp dir for unpacking: %w", err) - } - - // Defer clean up of directories. - defer func() { - // Always clean up the tmp dir. - _ = os.RemoveAll(tmpDir) - // Cleanup the destination in case of an error. - if err != nil { - _ = os.RemoveAll(destDir) - } - }() - - // Open the archive for reading. - var archiveReader *zip.ReadCloser - archiveReader, err = zip.OpenReader(archiveFile) - if err != nil { - return fmt.Errorf("failed to open zip reader: %w", err) - } - defer func() { - _ = archiveReader.Close() - }() - - // Save all files to the tmp dir. - for _, file := range archiveReader.File { - err = copyFromZipArchive( - file, - filepath.Join(tmpDir, filepath.FromSlash(file.Name)), - ) - if err != nil { - return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err) - } - } - - // Make the final move. - err = os.Rename(tmpDir, destDir) - if err != nil { - return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err) - } - - // Fix permissions on the destination dir. - err = res.registry.storageDir.EnsureAbsPath(destDir) - if err != nil { - return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err) - } - - log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath()) - return nil -} - -func copyFromZipArchive(archiveFile *zip.File, dstPath string) error { - // If file is a directory, create it and continue. - if archiveFile.FileInfo().IsDir() { - err := os.Mkdir(dstPath, archiveFile.Mode()) - if err != nil { - return fmt.Errorf("failed to create directory %s: %w", dstPath, err) - } - return nil - } - - // Open archived file for reading. - fileReader, err := archiveFile.Open() - if err != nil { - return fmt.Errorf("failed to open file in archive: %w", err) - } - defer func() { - _ = fileReader.Close() - }() - - // Open destination file for writing. - dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode()) - if err != nil { - return fmt.Errorf("failed to open destination file %s: %w", dstPath, err) - } - defer func() { - _ = dstFile.Close() - }() - - // Copy full file from archive to dst. - if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil { - // EOF is expected here as the archive is likely smaller - // thane MaxUnpackSize - if errors.Is(err, io.EOF) { - return nil - } - return err - } - - return nil -} +// import ( +// "archive/zip" +// "compress/gzip" +// "errors" +// "fmt" +// "io" +// "io/fs" +// "os" +// "path" +// "path/filepath" +// "strings" + +// "github.com/hashicorp/go-multierror" + +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) + +// // MaxUnpackSize specifies the maximum size that will be unpacked. +// const MaxUnpackSize = 1000000000 // 1GB + +// // UnpackGZIP unpacks a GZIP compressed reader r +// // and returns a new reader. It's suitable to be +// // used with registry.GetPackedFile. +// func UnpackGZIP(r io.Reader) (io.Reader, error) { +// return gzip.NewReader(r) +// } + +// // UnpackResources unpacks all resources defined in the AutoUnpack list. +// func (reg *ResourceRegistry) UnpackResources() error { +// reg.RLock() +// defer reg.RUnlock() + +// var multierr *multierror.Error +// for _, res := range reg.resources { +// if utils.StringInSlice(reg.AutoUnpack, res.Identifier) { +// err := res.UnpackArchive() +// if err != nil { +// multierr = multierror.Append( +// multierr, +// fmt.Errorf("%s: %w", res.Identifier, err), +// ) +// } +// } +// } + +// return multierr.ErrorOrNil() +// } + +// const ( +// zipSuffix = ".zip" +// ) + +// // UnpackArchive unpacks the archive the resource refers to. The contents are +// // unpacked into a directory with the same name as the file, excluding the +// // suffix. If the destination folder already exists, it is assumed that the +// // contents have already been correctly unpacked. +// func (res *Resource) UnpackArchive() error { +// res.Lock() +// defer res.Unlock() + +// // Only unpack selected versions. +// if res.SelectedVersion == nil { +// return nil +// } + +// switch { +// case strings.HasSuffix(res.Identifier, zipSuffix): +// return res.unpackZipArchive() +// default: +// return fmt.Errorf("unsupported file type for unpacking") +// } +// } + +// func (res *Resource) unpackZipArchive() error { +// // Get file and directory paths. +// archiveFile := res.SelectedVersion.storagePath() +// destDir := strings.TrimSuffix(archiveFile, zipSuffix) +// tmpDir := filepath.Join( +// res.registry.tmpDir.Path, +// filepath.FromSlash(strings.TrimSuffix( +// path.Base(res.SelectedVersion.versionedPath()), +// zipSuffix, +// )), +// ) + +// // Check status of destination. +// dstStat, err := os.Stat(destDir) +// switch { +// case errors.Is(err, fs.ErrNotExist): +// // The destination does not exist, continue with unpacking. +// case err != nil: +// return fmt.Errorf("cannot access destination for unpacking: %w", err) +// case !dstStat.IsDir(): +// return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name()) +// default: +// // Archive already seems to be unpacked. +// return nil +// } + +// // Create the tmp directory for unpacking. +// err = res.registry.tmpDir.EnsureAbsPath(tmpDir) +// if err != nil { +// return fmt.Errorf("failed to create tmp dir for unpacking: %w", err) +// } + +// // Defer clean up of directories. +// defer func() { +// // Always clean up the tmp dir. +// _ = os.RemoveAll(tmpDir) +// // Cleanup the destination in case of an error. +// if err != nil { +// _ = os.RemoveAll(destDir) +// } +// }() + +// // Open the archive for reading. +// var archiveReader *zip.ReadCloser +// archiveReader, err = zip.OpenReader(archiveFile) +// if err != nil { +// return fmt.Errorf("failed to open zip reader: %w", err) +// } +// defer func() { +// _ = archiveReader.Close() +// }() + +// // Save all files to the tmp dir. +// for _, file := range archiveReader.File { +// err = copyFromZipArchive( +// file, +// filepath.Join(tmpDir, filepath.FromSlash(file.Name)), +// ) +// if err != nil { +// return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err) +// } +// } + +// // Make the final move. +// err = os.Rename(tmpDir, destDir) +// if err != nil { +// return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err) +// } + +// // Fix permissions on the destination dir. +// err = res.registry.storageDir.EnsureAbsPath(destDir) +// if err != nil { +// return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err) +// } + +// log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath()) +// return nil +// } + +// func copyFromZipArchive(archiveFile *zip.File, dstPath string) error { +// // If file is a directory, create it and continue. +// if archiveFile.FileInfo().IsDir() { +// err := os.Mkdir(dstPath, archiveFile.Mode()) +// if err != nil { +// return fmt.Errorf("failed to create directory %s: %w", dstPath, err) +// } +// return nil +// } + +// // Open archived file for reading. +// fileReader, err := archiveFile.Open() +// if err != nil { +// return fmt.Errorf("failed to open file in archive: %w", err) +// } +// defer func() { +// _ = fileReader.Close() +// }() + +// // Open destination file for writing. +// dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode()) +// if err != nil { +// return fmt.Errorf("failed to open destination file %s: %w", dstPath, err) +// } +// defer func() { +// _ = dstFile.Close() +// }() + +// // Copy full file from archive to dst. +// if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil { +// // EOF is expected here as the archive is likely smaller +// // thane MaxUnpackSize +// if errors.Is(err, io.EOF) { +// return nil +// } +// return err +// } + +// return nil +// } diff --git a/base/updater/updating.go b/base/updater/updating.go index cf87472e8..3c76304a9 100644 --- a/base/updater/updating.go +++ b/base/updater/updating.go @@ -1,359 +1,359 @@ package updater -import ( - "context" - "fmt" - "net/http" - "os" - "path" - "path/filepath" - "strings" - - "golang.org/x/exp/slices" - - "github.com/safing/jess/filesig" - "github.com/safing/jess/lhash" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) - -// UpdateIndexes downloads all indexes. An error is only returned when all -// indexes fail to update. -func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { - var lastErr error - var anySuccess bool - - // Start registry operation. - reg.state.StartOperation(StateChecking) - defer reg.state.EndOperation() - - client := &http.Client{} - for _, idx := range reg.getIndexes() { - if err := reg.downloadIndex(ctx, client, idx); err != nil { - lastErr = err - log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) - } else { - anySuccess = true - } - } - - // If all indexes failed to update, fail. - if !anySuccess { - err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) - reg.state.ReportUpdateCheck(nil, err) - return err - } - - // Get pending resources and update status. - pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) - reg.state.ReportUpdateCheck( - humanInfoFromResourceVersions(pendingResourceVersions), - nil, - ) - - return nil -} - -func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { - var ( - // Index. - indexErr error - indexData []byte - downloadURL string - - // Signature. - sigErr error - verifiedHash *lhash.LabeledHash - sigFileData []byte - verifOpts = reg.GetVerificationOptions(idx.Path) - ) - - // Upgrade to v2 index if verification is enabled. - downloadIndexPath := idx.Path - if verifOpts != nil { - downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension - } - - // Download new index and signature. - for tries := range 3 { - // Index and signature need to be fetched together, so that they are - // fetched from the same source. One source should always have a matching - // index and signature. Backup sources may be behind a little. - // If the signature verification fails, another source should be tried. - - // Get index data. - indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) - if indexErr != nil { - log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) - continue - } - - // Get signature and verify it. - if verifOpts != nil { - verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( - ctx, client, - verifOpts, downloadIndexPath+filesig.Extension, nil, - tries, - ) - if sigErr != nil { - log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) - continue - } - - // Check if the index matches the verified hash. - if verifiedHash.Matches(indexData) { - log.Infof("%s: verified signature of %s", reg.Name, downloadURL) - } else { - sigErr = ErrIndexChecksumMismatch - log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) - continue - } - } - - break - } - if indexErr != nil { - return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) - } - if sigErr != nil { - return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) - } - - // Parse the index file. - indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) - if err != nil { - return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) - } - - // Add index data to registry. - if len(indexFile.Releases) > 0 { - // Check if all resources are within the indexes' authority. - authoritativePath := path.Dir(idx.Path) + "/" - if authoritativePath == "./" { - // Fix path for indexes at the storage root. - authoritativePath = "" - } - cleanedData := make(map[string]string, len(indexFile.Releases)) - for key, version := range indexFile.Releases { - if strings.HasPrefix(key, authoritativePath) { - cleanedData[key] = version - } else { - log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) - } - } - - // add resources to registry - err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) - if err != nil { - log.Warningf("%s: failed to add resources: %s", reg.Name, err) - } - } else { - log.Debugf("%s: index %s is empty", reg.Name, idx.Path) - } - - // Check if dest dir exists. - indexDir := filepath.FromSlash(path.Dir(idx.Path)) - err = reg.storageDir.EnsureRelPath(indexDir) - if err != nil { - log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) - } - - // Index files must be readable by portmaster-staert with user permissions in order to load the index. - err = os.WriteFile( //nolint:gosec - filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), - indexData, 0o0644, - ) - if err != nil { - log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) - } - - // Write signature file, if we have one. - if len(sigFileData) > 0 { - err = os.WriteFile( //nolint:gosec - filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), - sigFileData, 0o0644, - ) - if err != nil { - log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) - } - } - - log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) - return nil -} - -// DownloadUpdates checks if updates are available and downloads updates of used components. -func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { - // Start registry operation. - reg.state.StartOperation(StateDownloading) - defer reg.state.EndOperation() - - // Get pending updates. - toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) - downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) - reg.state.UpdateOperationDetails(&StateDownloadingDetails{ - Resources: downloadDetailsResources, - }) - - // nothing to update - if len(toUpdate) == 0 && len(missingSigs) == 0 { - log.Infof("%s: everything up to date", reg.Name) - return nil - } - - // check download dir - if err := reg.tmpDir.Ensure(); err != nil { - return fmt.Errorf("could not prepare tmp directory for download: %w", err) - } - - // download updates - log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) - client := &http.Client{} - var reportError error - - for i, rv := range toUpdate { - log.Infof( - "%s: downloading update [%d/%d]: %s version %s", - reg.Name, - i+1, len(toUpdate), - rv.resource.Identifier, rv.VersionNumber, - ) - var err error - for tries := range 3 { - err = reg.fetchFile(ctx, client, rv, tries) - if err == nil { - // Update resource version state. - rv.resource.Lock() - rv.Available = true - if rv.resource.VerificationOptions != nil { - rv.SigAvailable = true - } - rv.resource.Unlock() - - break - } - } - if err != nil { - reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) - log.Warningf("%s: %s", reg.Name, reportError) - } - - reg.state.UpdateOperationDetails(&StateDownloadingDetails{ - Resources: downloadDetailsResources, - FinishedUpTo: i + 1, - }) - } - - if len(missingSigs) > 0 { - log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) - - for _, rv := range missingSigs { - var err error - for tries := range 3 { - err = reg.fetchMissingSig(ctx, client, rv, tries) - if err == nil { - // Update resource version state. - rv.resource.Lock() - rv.SigAvailable = true - rv.resource.Unlock() - - break - } - } - if err != nil { - reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) - log.Warningf("%s: %s", reg.Name, reportError) - } - } - } - - reg.state.ReportDownloads( - downloadDetailsResources, - reportError, - ) - log.Infof("%s: finished downloading updates", reg.Name) - - return nil -} - -// DownloadUpdates checks if updates are available and downloads updates of used components. - -// GetPendingDownloads returns the list of pending downloads. -// If manual is set, indexes with AutoDownload=false will be checked. -// If auto is set, indexes with AutoDownload=true will be checked. -func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { - reg.RLock() - defer reg.RUnlock() - - // create list of downloads - var toUpdate []*ResourceVersion - var missingSigs []*ResourceVersion - - for _, res := range reg.resources { - func() { - res.Lock() - defer res.Unlock() - - // Skip resources without index or indexes that should not be reported - // according to parameters. - switch { - case res.Index == nil: - // Cannot download if resource is not part of an index. - return - case manual && !res.Index.AutoDownload: - // Manual update report and index is not auto-download. - case auto && res.Index.AutoDownload: - // Auto update report and index is auto-download. - default: - // Resource should not be reported. - return - } - - // Skip resources we don't need. - switch { - case res.inUse(): - // Update if resource is in use. - case res.available(): - // Update if resource is available locally, ie. was used in the past. - case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): - // Update is set as mandatory. - default: - // Resource does not need to be updated. - return - } - - // Go through all versions until we find versions that need updating. - for _, rv := range res.Versions { - switch { - case !rv.CurrentRelease: - // We are not interested in older releases. - case !rv.Available: - // File not available locally, download! - toUpdate = append(toUpdate, rv) - case !rv.SigAvailable && res.VerificationOptions != nil: - // File signature is not available and verification is enabled, download signature! - missingSigs = append(missingSigs, rv) - } - } - }() - } - - slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int { - return strings.Compare(a.resource.Identifier, b.resource.Identifier) - }) - slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int { - return strings.Compare(a.resource.Identifier, b.resource.Identifier) - }) - - return toUpdate, missingSigs -} - -func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { - identifiers := make([]string, len(resourceVersions)) - - for i, rv := range resourceVersions { - identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) - } - - return identifiers -} +// import ( +// "context" +// "fmt" +// "net/http" +// "os" +// "path" +// "path/filepath" +// "strings" + +// "golang.org/x/exp/slices" + +// "github.com/safing/jess/filesig" +// "github.com/safing/jess/lhash" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) + +// // UpdateIndexes downloads all indexes. An error is only returned when all +// // indexes fail to update. +// func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { +// var lastErr error +// var anySuccess bool + +// // Start registry operation. +// reg.state.StartOperation(StateChecking) +// defer reg.state.EndOperation() + +// client := &http.Client{} +// for _, idx := range reg.getIndexes() { +// if err := reg.downloadIndex(ctx, client, idx); err != nil { +// lastErr = err +// log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) +// } else { +// anySuccess = true +// } +// } + +// // If all indexes failed to update, fail. +// if !anySuccess { +// err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) +// reg.state.ReportUpdateCheck(nil, err) +// return err +// } + +// // Get pending resources and update status. +// pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) +// reg.state.ReportUpdateCheck( +// humanInfoFromResourceVersions(pendingResourceVersions), +// nil, +// ) + +// return nil +// } + +// func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { +// var ( +// // Index. +// indexErr error +// indexData []byte +// downloadURL string + +// // Signature. +// sigErr error +// verifiedHash *lhash.LabeledHash +// sigFileData []byte +// verifOpts = reg.GetVerificationOptions(idx.Path) +// ) + +// // Upgrade to v2 index if verification is enabled. +// downloadIndexPath := idx.Path +// if verifOpts != nil { +// downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension +// } + +// // Download new index and signature. +// for tries := range 3 { +// // Index and signature need to be fetched together, so that they are +// // fetched from the same source. One source should always have a matching +// // index and signature. Backup sources may be behind a little. +// // If the signature verification fails, another source should be tried. + +// // Get index data. +// indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) +// if indexErr != nil { +// log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) +// continue +// } + +// // Get signature and verify it. +// if verifOpts != nil { +// verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( +// ctx, client, +// verifOpts, downloadIndexPath+filesig.Extension, nil, +// tries, +// ) +// if sigErr != nil { +// log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) +// continue +// } + +// // Check if the index matches the verified hash. +// if verifiedHash.Matches(indexData) { +// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) +// } else { +// sigErr = ErrIndexChecksumMismatch +// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) +// continue +// } +// } + +// break +// } +// if indexErr != nil { +// return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) +// } +// if sigErr != nil { +// return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) +// } + +// // Parse the index file. +// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) +// if err != nil { +// return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) +// } + +// // Add index data to registry. +// if len(indexFile.Releases) > 0 { +// // Check if all resources are within the indexes' authority. +// authoritativePath := path.Dir(idx.Path) + "/" +// if authoritativePath == "./" { +// // Fix path for indexes at the storage root. +// authoritativePath = "" +// } +// cleanedData := make(map[string]string, len(indexFile.Releases)) +// for key, version := range indexFile.Releases { +// if strings.HasPrefix(key, authoritativePath) { +// cleanedData[key] = version +// } else { +// log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) +// } +// } + +// // add resources to registry +// err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) +// if err != nil { +// log.Warningf("%s: failed to add resources: %s", reg.Name, err) +// } +// } else { +// log.Debugf("%s: index %s is empty", reg.Name, idx.Path) +// } + +// // Check if dest dir exists. +// indexDir := filepath.FromSlash(path.Dir(idx.Path)) +// err = reg.storageDir.EnsureRelPath(indexDir) +// if err != nil { +// log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) +// } + +// // Index files must be readable by portmaster-staert with user permissions in order to load the index. +// err = os.WriteFile( //nolint:gosec +// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), +// indexData, 0o0644, +// ) +// if err != nil { +// log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) +// } + +// // Write signature file, if we have one. +// if len(sigFileData) > 0 { +// err = os.WriteFile( //nolint:gosec +// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), +// sigFileData, 0o0644, +// ) +// if err != nil { +// log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) +// } +// } + +// log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) +// return nil +// } + +// // DownloadUpdates checks if updates are available and downloads updates of used components. +// func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { +// // Start registry operation. +// reg.state.StartOperation(StateDownloading) +// defer reg.state.EndOperation() + +// // Get pending updates. +// toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) +// downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) +// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ +// Resources: downloadDetailsResources, +// }) + +// // nothing to update +// if len(toUpdate) == 0 && len(missingSigs) == 0 { +// log.Infof("%s: everything up to date", reg.Name) +// return nil +// } + +// // check download dir +// if err := reg.tmpDir.Ensure(); err != nil { +// return fmt.Errorf("could not prepare tmp directory for download: %w", err) +// } + +// // download updates +// log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) +// client := &http.Client{} +// var reportError error + +// for i, rv := range toUpdate { +// log.Infof( +// "%s: downloading update [%d/%d]: %s version %s", +// reg.Name, +// i+1, len(toUpdate), +// rv.resource.Identifier, rv.VersionNumber, +// ) +// var err error +// for tries := range 3 { +// err = reg.fetchFile(ctx, client, rv, tries) +// if err == nil { +// // Update resource version state. +// rv.resource.Lock() +// rv.Available = true +// if rv.resource.VerificationOptions != nil { +// rv.SigAvailable = true +// } +// rv.resource.Unlock() + +// break +// } +// } +// if err != nil { +// reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) +// log.Warningf("%s: %s", reg.Name, reportError) +// } + +// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ +// Resources: downloadDetailsResources, +// FinishedUpTo: i + 1, +// }) +// } + +// if len(missingSigs) > 0 { +// log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) + +// for _, rv := range missingSigs { +// var err error +// for tries := range 3 { +// err = reg.fetchMissingSig(ctx, client, rv, tries) +// if err == nil { +// // Update resource version state. +// rv.resource.Lock() +// rv.SigAvailable = true +// rv.resource.Unlock() + +// break +// } +// } +// if err != nil { +// reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) +// log.Warningf("%s: %s", reg.Name, reportError) +// } +// } +// } + +// reg.state.ReportDownloads( +// downloadDetailsResources, +// reportError, +// ) +// log.Infof("%s: finished downloading updates", reg.Name) + +// return nil +// } + +// // DownloadUpdates checks if updates are available and downloads updates of used components. + +// // GetPendingDownloads returns the list of pending downloads. +// // If manual is set, indexes with AutoDownload=false will be checked. +// // If auto is set, indexes with AutoDownload=true will be checked. +// func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { +// reg.RLock() +// defer reg.RUnlock() + +// // create list of downloads +// var toUpdate []*ResourceVersion +// var missingSigs []*ResourceVersion + +// for _, res := range reg.resources { +// func() { +// res.Lock() +// defer res.Unlock() + +// // Skip resources without index or indexes that should not be reported +// // according to parameters. +// switch { +// case res.Index == nil: +// // Cannot download if resource is not part of an index. +// return +// case manual && !res.Index.AutoDownload: +// // Manual update report and index is not auto-download. +// case auto && res.Index.AutoDownload: +// // Auto update report and index is auto-download. +// default: +// // Resource should not be reported. +// return +// } + +// // Skip resources we don't need. +// switch { +// case res.inUse(): +// // Update if resource is in use. +// case res.available(): +// // Update if resource is available locally, ie. was used in the past. +// case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): +// // Update is set as mandatory. +// default: +// // Resource does not need to be updated. +// return +// } + +// // Go through all versions until we find versions that need updating. +// for _, rv := range res.Versions { +// switch { +// case !rv.CurrentRelease: +// // We are not interested in older releases. +// case !rv.Available: +// // File not available locally, download! +// toUpdate = append(toUpdate, rv) +// case !rv.SigAvailable && res.VerificationOptions != nil: +// // File signature is not available and verification is enabled, download signature! +// missingSigs = append(missingSigs, rv) +// } +// } +// }() +// } + +// slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int { +// return strings.Compare(a.resource.Identifier, b.resource.Identifier) +// }) +// slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int { +// return strings.Compare(a.resource.Identifier, b.resource.Identifier) +// }) + +// return toUpdate, missingSigs +// } + +// func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { +// identifiers := make([]string, len(resourceVersions)) + +// for i, rv := range resourceVersions { +// identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) +// } + +// return identifiers +// } diff --git a/service/broadcasts/data.go b/service/broadcasts/data.go index 2b59e4e63..e92ce84fc 100644 --- a/service/broadcasts/data.go +++ b/service/broadcasts/data.go @@ -7,7 +7,6 @@ import ( "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/service/intel/geoip" "github.com/safing/portmaster/service/netenv" - "github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/spn/access" "github.com/safing/portmaster/spn/access/account" "github.com/safing/portmaster/spn/captain" @@ -18,18 +17,19 @@ var portmasterStarted = time.Now() func collectData() interface{} { data := make(map[string]interface{}) + // TODO(vladimir) // Get data about versions. - versions := updates.GetSimpleVersions() - data["Updates"] = versions - data["Version"] = versions.Build.Version - numericVersion, err := MakeNumericVersion(versions.Build.Version) - if err != nil { - data["NumericVersion"] = &DataError{ - Error: err, - } - } else { - data["NumericVersion"] = numericVersion - } + // versions := updates.GetSimpleVersions() + // data["Updates"] = versions + // data["Version"] = versions.Build.Version + // numericVersion, err := MakeNumericVersion(versions.Build.Version) + // if err != nil { + // data["NumericVersion"] = &DataError{ + // Error: err, + // } + // } else { + // data["NumericVersion"] = numericVersion + // } // Get data about install. installInfo, err := GetInstallInfo() diff --git a/service/broadcasts/module.go b/service/broadcasts/module.go index a3968933e..2d99115b2 100644 --- a/service/broadcasts/module.go +++ b/service/broadcasts/module.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" ) type Broadcasts struct { @@ -91,4 +92,6 @@ func New(instance instance) (*Broadcasts, error) { return module, nil } -type instance interface{} +type instance interface { + Updates() *updates.Updates +} diff --git a/service/broadcasts/notify.go b/service/broadcasts/notify.go index a010f2494..73b05f989 100644 --- a/service/broadcasts/notify.go +++ b/service/broadcasts/notify.go @@ -18,7 +18,6 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" ) const ( @@ -68,7 +67,7 @@ type BroadcastNotification struct { func broadcastNotify(ctx *mgr.WorkerCtx) error { // Get broadcast notifications file, load it from disk and parse it. - broadcastsResource, err := updates.GetFile(broadcastsResourcePath) + broadcastsResource, err := module.instance.Updates().GetFile(broadcastsResourcePath) if err != nil { return fmt.Errorf("failed to get broadcast notifications update: %w", err) } diff --git a/service/core/api.go b/service/core/api.go index c4758cda3..c633956e8 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -149,7 +149,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) { config.AddToDebugInfo(di) // Detailed information. - updates.AddToDebugInfo(di) + // TODO(vladimir): updates.AddToDebugInfo(di) compat.AddToDebugInfo(di) module.instance.AddWorkerInfoToDebugInfo(di) di.AddGoroutineStack() diff --git a/service/intel/filterlists/database.go b/service/intel/filterlists/database.go index 5f55323c6..d6b5b7578 100644 --- a/service/intel/filterlists/database.go +++ b/service/intel/filterlists/database.go @@ -14,15 +14,14 @@ import ( "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" ) const ( - baseListFilePath = "intel/lists/base.dsdl" - intermediateListFilePath = "intel/lists/intermediate.dsdl" - urgentListFilePath = "intel/lists/urgent.dsdl" - listIndexFilePath = "intel/lists/index.dsd" + baseListFilePath = "base.dsdl" + intermediateListFilePath = "intermediate.dsdl" + urgentListFilePath = "urgent.dsdl" + listIndexFilePath = "index.dsd" ) // default bloomfilter element sizes (estimated). @@ -40,9 +39,9 @@ var ( filterListLock sync.RWMutex // Updater files for tracking upgrades. - baseFile *updater.File - intermediateFile *updater.File - urgentFile *updater.File + baseFile *registry.File + intermediateFile *registry.File + urgentFile *registry.File filterListsLoaded chan struct{} ) @@ -56,11 +55,10 @@ var cache = database.NewInterface(&database.Options{ // getFileFunc is the function used to get a file from // the updater. It's basically updates.GetFile and used // for unit testing. -type getFileFunc func(string) (*updater.File, error) // getFile points to updates.GetFile but may be set to // something different during unit testing. -var getFile getFileFunc = updates.GetFile +// var getFile getFileFunc = registry.GetFile func init() { filterListsLoaded = make(chan struct{}) @@ -79,7 +77,7 @@ func isLoaded() bool { // processListFile opens the latest version of file and decodes it's DSDL // content. It calls processEntry for each decoded filterlists entry. -func processListFile(ctx context.Context, filter *scopedBloom, file *updater.File) error { +func processListFile(ctx context.Context, filter *scopedBloom, file *registry.File) error { f, err := os.Open(file.Path()) if err != nil { return err diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go index 4b59adde4..842a96b20 100644 --- a/service/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -4,14 +4,12 @@ import ( "errors" "fmt" "os" - "strings" "sync" "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" "github.com/safing/structures/dsd" ) @@ -164,7 +162,7 @@ func getListIndexFromCache() (*ListIndexFile, error) { var ( // listIndexUpdate must only be used by updateListIndex. - listIndexUpdate *updater.File + listIndexUpdate *registry.File listIndexUpdateLock sync.Mutex ) @@ -177,24 +175,24 @@ func updateListIndex() error { case listIndexUpdate == nil: // This is the first time this function is run, get updater file for index. var err error - listIndexUpdate, err = updates.GetFile(listIndexFilePath) + listIndexUpdate, err = module.instance.Updates().GetFile(listIndexFilePath) if err != nil { return err } // Check if the version in the cache is current. - index, err := getListIndexFromCache() + _, err = getListIndexFromCache() switch { case errors.Is(err, database.ErrNotFound): log.Info("filterlists: index not in cache, starting update") case err != nil: log.Warningf("filterlists: failed to load index from cache, starting update: %s", err) - case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")): - log.Infof( - "filterlists: index from cache is outdated, starting update (%s != %s)", - strings.TrimPrefix(index.Version, "v"), - listIndexUpdate.Version(), - ) + // case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")): + // log.Infof( + // "filterlists: index from cache is outdated, starting update (%s != %s)", + // strings.TrimPrefix(index.Version, "v"), + // listIndexUpdate.Version(), + // ) default: // List is in cache and current, there is nothing to do. log.Debug("filterlists: index is up to date") @@ -204,8 +202,8 @@ func updateListIndex() error { return nil } - case listIndexUpdate.UpgradeAvailable(): - log.Info("filterlists: index update available, starting update") + // case listIndexUpdate.UpgradeAvailable(): + // log.Info("filterlists: index update available, starting update") default: // Index is loaded and no update is available, there is nothing to do. return nil diff --git a/service/intel/filterlists/updater.go b/service/intel/filterlists/updater.go index 72f7b82e7..f2e5f8d2e 100644 --- a/service/intel/filterlists/updater.go +++ b/service/intel/filterlists/updater.go @@ -13,8 +13,8 @@ import ( "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/query" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates/registry" ) var updateInProgress = abool.New() @@ -174,51 +174,51 @@ func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error { // getUpgradableFiles returns a slice of filterlists files // that should be updated. The files MUST be updated and // processed in the returned order! -func getUpgradableFiles() ([]*updater.File, error) { - var updateOrder []*updater.File - - cacheDBInUse := isLoaded() - - if baseFile == nil || baseFile.UpgradeAvailable() || !cacheDBInUse { - var err error - baseFile, err = getFile(baseListFilePath) - if err != nil { - return nil, err - } - log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version()) - updateOrder = append(updateOrder, baseFile) - } - - if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse { - var err error - intermediateFile, err = getFile(intermediateListFilePath) - if err != nil && !errors.Is(err, updater.ErrNotFound) { - return nil, err - } - - if err == nil { - log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version()) - updateOrder = append(updateOrder, intermediateFile) - } - } - - if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse { - var err error - urgentFile, err = getFile(urgentListFilePath) - if err != nil && !errors.Is(err, updater.ErrNotFound) { - return nil, err - } - - if err == nil { - log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version()) - updateOrder = append(updateOrder, urgentFile) - } - } +func getUpgradableFiles() ([]*registry.File, error) { + var updateOrder []*registry.File + + // cacheDBInUse := isLoaded() + + // if baseFile == nil || !cacheDBInUse { // TODO(vladimir): || baseFile.UpgradeAvailable() + // var err error + // baseFile, err = module.instance.Updates().GetFile(baseListFilePath) + // if err != nil { + // return nil, err + // } + // log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version()) + // updateOrder = append(updateOrder, baseFile) + // } + + // if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse { + // var err error + // intermediateFile, err = getFile(intermediateListFilePath) + // if err != nil && !errors.Is(err, updater.ErrNotFound) { + // return nil, err + // } + + // if err == nil { + // log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version()) + // updateOrder = append(updateOrder, intermediateFile) + // } + // } + + // if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse { + // var err error + // urgentFile, err = getFile(urgentListFilePath) + // if err != nil && !errors.Is(err, updater.ErrNotFound) { + // return nil, err + // } + + // if err == nil { + // log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version()) + // updateOrder = append(updateOrder, urgentFile) + // } + // } return resolveUpdateOrder(updateOrder) } -func resolveUpdateOrder(updateOrder []*updater.File) ([]*updater.File, error) { +func resolveUpdateOrder(updateOrder []*registry.File) ([]*registry.File, error) { // sort the update order by ascending version sort.Sort(byAscVersion(updateOrder)) log.Tracef("intel/filterlists: order of updates: %v", updateOrder) @@ -258,7 +258,7 @@ func resolveUpdateOrder(updateOrder []*updater.File) ([]*updater.File, error) { return updateOrder[startAtIdx:], nil } -type byAscVersion []*updater.File +type byAscVersion []*registry.File func (fs byAscVersion) Len() int { return len(fs) } diff --git a/service/intel/geoip/database.go b/service/intel/geoip/database.go index 5f0258a73..72197cd89 100644 --- a/service/intel/geoip/database.go +++ b/service/intel/geoip/database.go @@ -8,9 +8,8 @@ import ( maxminddb "github.com/oschwald/maxminddb-golang" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" ) var worker *updateWorker @@ -22,13 +21,13 @@ func init() { } const ( - v4MMDBResource = "intel/geoip/geoipv4.mmdb.gz" - v6MMDBResource = "intel/geoip/geoipv6.mmdb.gz" + v4MMDBResource = "geoipv4.mmdb" + v6MMDBResource = "geoipv6.mmdb" ) type geoIPDB struct { *maxminddb.Reader - file *updater.File + file *registry.File } // updateBroadcaster stores a geoIPDB and provides synchronized @@ -47,7 +46,7 @@ func (ub *updateBroadcaster) NeedsUpdate() bool { ub.rw.RLock() defer ub.rw.RUnlock() - return ub.db == nil || ub.db.file.UpgradeAvailable() + return ub.db == nil // TODO(vladimir) is this needed: || ub.db.file.UpgradeAvailable() } // ReplaceDatabase replaces (or initially sets) the mmdb database. @@ -181,12 +180,12 @@ func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error { func getGeoIPDB(resource string) (*geoIPDB, error) { log.Debugf("geoip: opening database %s", resource) - file, unpackedPath, err := openAndUnpack(resource) + file, err := open(resource) if err != nil { return nil, err } - reader, err := maxminddb.Open(unpackedPath) + reader, err := maxminddb.Open(file.Path()) if err != nil { return nil, fmt.Errorf("failed to open: %w", err) } @@ -198,16 +197,16 @@ func getGeoIPDB(resource string) (*geoIPDB, error) { }, nil } -func openAndUnpack(resource string) (*updater.File, string, error) { - f, err := updates.GetFile(resource) +func open(resource string) (*registry.File, error) { + f, err := module.instance.Updates().GetFile(resource) if err != nil { - return nil, "", fmt.Errorf("getting file: %w", err) + return nil, fmt.Errorf("getting file: %w", err) } - unpacked, err := f.Unpack(".gz", updater.UnpackGZIP) - if err != nil { - return nil, "", fmt.Errorf("unpacking file: %w", err) - } + // unpacked, err := f.Unpack(".gz", updater.UnpackGZIP) + // if err != nil { + // return nil, "", fmt.Errorf("unpacking file: %w", err) + // } - return f, unpacked, nil + return f, nil } diff --git a/service/netenv/main.go b/service/netenv/main.go index e1a681500..204267722 100644 --- a/service/netenv/main.go +++ b/service/netenv/main.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" ) // Event Names. @@ -105,4 +106,6 @@ func New(instance instance) (*NetEnv, error) { return module, nil } -type instance interface{} +type instance interface { + Updates() *updates.Updates +} diff --git a/service/netenv/online-status.go b/service/netenv/online-status.go index 554fc0046..137ce410e 100644 --- a/service/netenv/online-status.go +++ b/service/netenv/online-status.go @@ -17,7 +17,6 @@ import ( "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network/netutils" - "github.com/safing/portmaster/service/updates" ) // OnlineStatus represent a state of connectivity to the Internet. @@ -221,7 +220,7 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) // Trigger update check when coming (semi) online. if Online() { - _ = updates.TriggerUpdate(false, false) + module.instance.Updates().EventResourcesUpdated.Submit(struct{}{}) } } } diff --git a/service/network/api.go b/service/network/api.go index 82b11ad0e..8af6eb26f 100644 --- a/service/network/api.go +++ b/service/network/api.go @@ -16,7 +16,6 @@ import ( "github.com/safing/portmaster/service/process" "github.com/safing/portmaster/service/resolver" "github.com/safing/portmaster/service/status" - "github.com/safing/portmaster/service/updates" ) func registerAPIEndpoints() error { @@ -94,7 +93,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) { config.AddToDebugInfo(di) // Detailed information. - updates.AddToDebugInfo(di) + // TODO(vladimir): updates.AddToDebugInfo(di) // compat.AddToDebugInfo(di) // TODO: Cannot use due to interception import requirement which we don't want for SPN Hubs. di.AddGoroutineStack() diff --git a/service/ui/module.go b/service/ui/module.go index 630808e50..e9a8b324c 100644 --- a/service/ui/module.go +++ b/service/ui/module.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" ) func prep() error { @@ -56,7 +57,10 @@ func (ui *UI) Stop() error { return nil } -var shimLoaded atomic.Bool +var ( + shimLoaded atomic.Bool + module *UI +) // New returns a new UI module. func New(instance instance) (*UI, error) { @@ -64,7 +68,7 @@ func New(instance instance) (*UI, error) { return nil, errors.New("only one instance allowed") } m := mgr.New("UI") - module := &UI{ + module = &UI{ mgr: m, instance: instance, } @@ -78,4 +82,5 @@ func New(instance instance) (*UI, error) { type instance interface { API() *api.API + Updates() *updates.Updates } diff --git a/service/ui/serve.go b/service/ui/serve.go index 9dca6c309..1455c3f6e 100644 --- a/service/ui/serve.go +++ b/service/ui/serve.go @@ -15,9 +15,8 @@ import ( "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/base/utils" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" ) var ( @@ -92,9 +91,9 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // get file from update system - zipFile, err := updates.GetFile(fmt.Sprintf("ui/modules/%s.zip", moduleName)) + zipFile, err := module.instance.Updates().GetFile(fmt.Sprintf("%s.zip", moduleName)) if err != nil { - if errors.Is(err, updater.ErrNotFound) { + if errors.Is(err, registry.ErrNotFound) { log.Tracef("ui: requested module %s does not exist", moduleName) http.Error(w, err.Error(), http.StatusNotFound) } else { diff --git a/service/updates/api.go b/service/updates/api.go index 886596203..6c4dbf0c6 100644 --- a/service/updates/api.go +++ b/service/updates/api.go @@ -1,161 +1,161 @@ package updates import ( - "bytes" - "io" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/ghodss/yaml" - - "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" +// "bytes" +// "io" +// "net/http" +// "os" +// "path/filepath" +// "strings" + +// "github.com/ghodss/yaml" + +// "github.com/safing/portmaster/base/api" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" ) const ( apiPathCheckForUpdates = "updates/check" ) -func registerAPIEndpoints() error { - if err := api.RegisterEndpoint(api.Endpoint{ - Name: "Check for Updates", - Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", - Parameters: []api.Parameter{{ - Method: http.MethodPost, - Field: "download", - Value: "", - Description: "Force downloading and applying of all updates, regardless of auto-update settings.", - }}, - Path: apiPathCheckForUpdates, - Write: api.PermitUser, - ActionFunc: func(r *api.Request) (msg string, err error) { - // Check if we should also download regardless of settings. - downloadAll := r.URL.Query().Has("download") - - // Trigger update task. - err = TriggerUpdate(true, downloadAll) - if err != nil { - return "", err - } - - // Report how we triggered. - if downloadAll { - return "downloading all updates...", nil - } - return "checking for updates...", nil - }, - }); err != nil { - return err - } - - if err := api.RegisterEndpoint(api.Endpoint{ - Name: "Get Resource", - Description: "Returns the requested resource from the udpate system", - Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, - Read: api.PermitUser, - ReadMethod: http.MethodGet, - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - // Get identifier from URL. - var identifier string - if ar := api.GetAPIRequest(r); ar != nil { - identifier = ar.URLVars["identifier"] - } - if identifier == "" { - http.Error(w, "no resource speicified", http.StatusBadRequest) - return - } - - // Get resource. - resource, err := registry.GetFile(identifier) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - // Open file for reading. - file, err := os.Open(resource.Path()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() //nolint:errcheck,gosec - - // Assign file to reader - var reader io.Reader = file - - // Add version to header. - w.Header().Set("Resource-Version", resource.Version()) - - // Set Content-Type. - contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) - w.Header().Set("Content-Type", contentType) - - // Check if the content type may be returned. - accept := r.Header.Get("Accept") - if accept != "" { - mimeTypes := strings.Split(accept, ",") - // First, clean mime types. - for i, mimeType := range mimeTypes { - mimeType = strings.TrimSpace(mimeType) - mimeType, _, _ = strings.Cut(mimeType, ";") - mimeTypes[i] = mimeType - } - // Second, check if we may return anything. - var acceptsAny bool - for _, mimeType := range mimeTypes { - switch mimeType { - case "*", "*/*": - acceptsAny = true - } - } - // Third, check if we can convert. - if !acceptsAny { - var converted bool - sourceType, _, _ := strings.Cut(contentType, ";") - findConvertiblePair: - for _, mimeType := range mimeTypes { - switch { - case sourceType == "application/yaml" && mimeType == "application/json": - yamlData, err := io.ReadAll(reader) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - jsonData, err := yaml.YAMLToJSON(yamlData) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - reader = bytes.NewReader(jsonData) - converted = true - break findConvertiblePair - } - } - - // If we could not convert to acceptable format, return an error. - if !converted { - http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) - return - } - } - } - - // Write file. - w.WriteHeader(http.StatusOK) - if r.Method != http.MethodHead { - _, err = io.Copy(w, reader) - if err != nil { - log.Errorf("updates: failed to serve resource file: %s", err) - return - } - } - }, - }); err != nil { - return err - } - - return nil -} +// func registerAPIEndpoints() error { +// if err := api.RegisterEndpoint(api.Endpoint{ +// Name: "Check for Updates", +// Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", +// Parameters: []api.Parameter{{ +// Method: http.MethodPost, +// Field: "download", +// Value: "", +// Description: "Force downloading and applying of all updates, regardless of auto-update settings.", +// }}, +// Path: apiPathCheckForUpdates, +// Write: api.PermitUser, +// ActionFunc: func(r *api.Request) (msg string, err error) { +// // Check if we should also download regardless of settings. +// downloadAll := r.URL.Query().Has("download") + +// // Trigger update task. +// err = TriggerUpdate(true, downloadAll) +// if err != nil { +// return "", err +// } + +// // Report how we triggered. +// if downloadAll { +// return "downloading all updates...", nil +// } +// return "checking for updates...", nil +// }, +// }); err != nil { +// return err +// } + +// if err := api.RegisterEndpoint(api.Endpoint{ +// Name: "Get Resource", +// Description: "Returns the requested resource from the udpate system", +// Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, +// Read: api.PermitUser, +// ReadMethod: http.MethodGet, +// HandlerFunc: func(w http.ResponseWriter, r *http.Request) { +// // Get identifier from URL. +// var identifier string +// if ar := api.GetAPIRequest(r); ar != nil { +// identifier = ar.URLVars["identifier"] +// } +// if identifier == "" { +// http.Error(w, "no resource speicified", http.StatusBadRequest) +// return +// } + +// // Get resource. +// resource, err := registry.GetFile(identifier) +// if err != nil { +// http.Error(w, err.Error(), http.StatusNotFound) +// return +// } + +// // Open file for reading. +// file, err := os.Open(resource.Path()) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// defer file.Close() //nolint:errcheck,gosec + +// // Assign file to reader +// var reader io.Reader = file + +// // Add version to header. +// w.Header().Set("Resource-Version", resource.Version()) + +// // Set Content-Type. +// contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) +// w.Header().Set("Content-Type", contentType) + +// // Check if the content type may be returned. +// accept := r.Header.Get("Accept") +// if accept != "" { +// mimeTypes := strings.Split(accept, ",") +// // First, clean mime types. +// for i, mimeType := range mimeTypes { +// mimeType = strings.TrimSpace(mimeType) +// mimeType, _, _ = strings.Cut(mimeType, ";") +// mimeTypes[i] = mimeType +// } +// // Second, check if we may return anything. +// var acceptsAny bool +// for _, mimeType := range mimeTypes { +// switch mimeType { +// case "*", "*/*": +// acceptsAny = true +// } +// } +// // Third, check if we can convert. +// if !acceptsAny { +// var converted bool +// sourceType, _, _ := strings.Cut(contentType, ";") +// findConvertiblePair: +// for _, mimeType := range mimeTypes { +// switch { +// case sourceType == "application/yaml" && mimeType == "application/json": +// yamlData, err := io.ReadAll(reader) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// jsonData, err := yaml.YAMLToJSON(yamlData) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// reader = bytes.NewReader(jsonData) +// converted = true +// break findConvertiblePair +// } +// } + +// // If we could not convert to acceptable format, return an error. +// if !converted { +// http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) +// return +// } +// } +// } + +// // Write file. +// w.WriteHeader(http.StatusOK) +// if r.Method != http.MethodHead { +// _, err = io.Copy(w, reader) +// if err != nil { +// log.Errorf("updates: failed to serve resource file: %s", err) +// return +// } +// } +// }, +// }); err != nil { +// return err +// } + +// return nil +// } diff --git a/service/updates/config.go b/service/updates/config.go index f765fd4c0..e563d557a 100644 --- a/service/updates/config.go +++ b/service/updates/config.go @@ -4,9 +4,9 @@ import ( "github.com/tevino/abool" "github.com/safing/portmaster/base/config" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" + // "github.com/safing/portmaster/base/log" + // "github.com/safing/portmaster/service/mgr" + // "github.com/safing/portmaster/service/updates/helper" ) const cfgDevModeKey = "core/devMode" @@ -27,152 +27,152 @@ var ( forceDownload = abool.New() ) -func registerConfig() error { - err := config.Register(&config.Option{ - Name: "Release Channel", - Key: helper.ReleaseChannelKey, - Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, - OptType: config.OptTypeString, - ExpertiseLevel: config.ExpertiseLevelExpert, - ReleaseLevel: config.ReleaseLevelStable, - RequiresRestart: true, - DefaultValue: helper.ReleaseChannelStable, - PossibleValues: []config.PossibleValue{ - { - Name: "Stable", - Description: "Production releases.", - Value: helper.ReleaseChannelStable, - }, - { - Name: "Beta", - Description: "Production releases for testing new features that may break and cause interruption.", - Value: helper.ReleaseChannelBeta, - }, - { - Name: "Support", - Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", - Value: helper.ReleaseChannelSupport, - }, - { - Name: "Staging", - Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", - Value: helper.ReleaseChannelStaging, - }, - }, - Annotations: config.Annotations{ - config.DisplayOrderAnnotation: -4, - config.DisplayHintAnnotation: config.DisplayHintOneOf, - config.CategoryAnnotation: "Updates", - }, - }) - if err != nil { - return err - } - - err = config.Register(&config.Option{ - Name: "Automatic Software Updates", - Key: enableSoftwareUpdatesKey, - Description: "Automatically check for and download software updates. This does not include intelligence data updates.", - OptType: config.OptTypeBool, - ExpertiseLevel: config.ExpertiseLevelExpert, - ReleaseLevel: config.ReleaseLevelStable, - RequiresRestart: false, - DefaultValue: true, - Annotations: config.Annotations{ - config.DisplayOrderAnnotation: -12, - config.CategoryAnnotation: "Updates", - }, - }) - if err != nil { - return err - } - - err = config.Register(&config.Option{ - Name: "Automatic Intelligence Data Updates", - Key: enableIntelUpdatesKey, - Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.", - OptType: config.OptTypeBool, - ExpertiseLevel: config.ExpertiseLevelExpert, - ReleaseLevel: config.ReleaseLevelStable, - RequiresRestart: false, - DefaultValue: true, - Annotations: config.Annotations{ - config.DisplayOrderAnnotation: -11, - config.CategoryAnnotation: "Updates", - }, - }) - if err != nil { - return err - } - - return nil -} - -func initConfig() { - releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable) - initialReleaseChannel = releaseChannel() - previousReleaseChannel = releaseChannel() - - enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true) - enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true) - softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() - intelUpdatesCurrentlyEnabled = enableIntelUpdates() - - devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false) - previousDevMode = devMode() -} - -func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { - changed := false - - if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled { - softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() - changed = true - } - - if enableIntelUpdates() != intelUpdatesCurrentlyEnabled { - intelUpdatesCurrentlyEnabled = enableIntelUpdates() - changed = true - } - - if devMode() != previousDevMode { - registry.SetDevMode(devMode()) - previousDevMode = devMode() - changed = true - } - - if releaseChannel() != previousReleaseChannel { - previousReleaseChannel = releaseChannel() - changed = true - } - - if changed { - // Update indexes based on new settings. - warning := helper.SetIndexes( - registry, - releaseChannel(), - true, - softwareUpdatesCurrentlyEnabled, - intelUpdatesCurrentlyEnabled, - ) - if warning != nil { - log.Warningf("updates: %s", warning) - } - - // Select versions depending on new indexes and modes. - registry.SelectVersions() - module.EventVersionsUpdated.Submit(struct{}{}) - - if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled { - module.states.Clear() - if err := TriggerUpdate(true, false); err != nil { - log.Warningf("updates: failed to trigger update: %s", err) - } - log.Infof("updates: automatic updates are now enabled") - } else { - log.Warningf("updates: automatic updates are now completely disabled") - } - } - - return false, nil -} +// func registerConfig() error { +// err := config.Register(&config.Option{ +// Name: "Release Channel", +// Key: helper.ReleaseChannelKey, +// Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, +// OptType: config.OptTypeString, +// ExpertiseLevel: config.ExpertiseLevelExpert, +// ReleaseLevel: config.ReleaseLevelStable, +// RequiresRestart: true, +// DefaultValue: helper.ReleaseChannelStable, +// PossibleValues: []config.PossibleValue{ +// { +// Name: "Stable", +// Description: "Production releases.", +// Value: helper.ReleaseChannelStable, +// }, +// { +// Name: "Beta", +// Description: "Production releases for testing new features that may break and cause interruption.", +// Value: helper.ReleaseChannelBeta, +// }, +// { +// Name: "Support", +// Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", +// Value: helper.ReleaseChannelSupport, +// }, +// { +// Name: "Staging", +// Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", +// Value: helper.ReleaseChannelStaging, +// }, +// }, +// Annotations: config.Annotations{ +// config.DisplayOrderAnnotation: -4, +// config.DisplayHintAnnotation: config.DisplayHintOneOf, +// config.CategoryAnnotation: "Updates", +// }, +// }) +// if err != nil { +// return err +// } + +// err = config.Register(&config.Option{ +// Name: "Automatic Software Updates", +// Key: enableSoftwareUpdatesKey, +// Description: "Automatically check for and download software updates. This does not include intelligence data updates.", +// OptType: config.OptTypeBool, +// ExpertiseLevel: config.ExpertiseLevelExpert, +// ReleaseLevel: config.ReleaseLevelStable, +// RequiresRestart: false, +// DefaultValue: true, +// Annotations: config.Annotations{ +// config.DisplayOrderAnnotation: -12, +// config.CategoryAnnotation: "Updates", +// }, +// }) +// if err != nil { +// return err +// } + +// err = config.Register(&config.Option{ +// Name: "Automatic Intelligence Data Updates", +// Key: enableIntelUpdatesKey, +// Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.", +// OptType: config.OptTypeBool, +// ExpertiseLevel: config.ExpertiseLevelExpert, +// ReleaseLevel: config.ReleaseLevelStable, +// RequiresRestart: false, +// DefaultValue: true, +// Annotations: config.Annotations{ +// config.DisplayOrderAnnotation: -11, +// config.CategoryAnnotation: "Updates", +// }, +// }) +// if err != nil { +// return err +// } + +// return nil +// } + +// func initConfig() { +// releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable) +// initialReleaseChannel = releaseChannel() +// previousReleaseChannel = releaseChannel() + +// enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true) +// enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true) +// softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() +// intelUpdatesCurrentlyEnabled = enableIntelUpdates() + +// devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false) +// previousDevMode = devMode() +// } + +// func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { +// changed := false + +// if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled { +// softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() +// changed = true +// } + +// if enableIntelUpdates() != intelUpdatesCurrentlyEnabled { +// intelUpdatesCurrentlyEnabled = enableIntelUpdates() +// changed = true +// } + +// if devMode() != previousDevMode { +// registry.SetDevMode(devMode()) +// previousDevMode = devMode() +// changed = true +// } + +// if releaseChannel() != previousReleaseChannel { +// previousReleaseChannel = releaseChannel() +// changed = true +// } + +// if changed { +// // Update indexes based on new settings. +// warning := helper.SetIndexes( +// registry, +// releaseChannel(), +// true, +// softwareUpdatesCurrentlyEnabled, +// intelUpdatesCurrentlyEnabled, +// ) +// if warning != nil { +// log.Warningf("updates: %s", warning) +// } + +// // Select versions depending on new indexes and modes. +// registry.SelectVersions() +// module.EventVersionsUpdated.Submit(struct{}{}) + +// if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled { +// module.states.Clear() +// if err := TriggerUpdate(true, false); err != nil { +// log.Warningf("updates: failed to trigger update: %s", err) +// } +// log.Infof("updates: automatic updates are now enabled") +// } else { +// log.Warningf("updates: automatic updates are now completely disabled") +// } +// } + +// return false, nil +// } diff --git a/service/updates/export.go b/service/updates/export.go index c230f3672..c736855c8 100644 --- a/service/updates/export.go +++ b/service/updates/export.go @@ -1,238 +1,237 @@ package updates -import ( - "fmt" - "sort" - "strings" - "sync" - - "github.com/safing/portmaster/base/database/record" - "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils/debug" - "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" -) - -const ( - // versionsDBKey is the database key for update version information. - versionsDBKey = "core:status/versions" - - // versionsDBKey is the database key for simple update version information. - simpleVersionsDBKey = "core:status/simple-versions" - - // updateStatusDBKey is the database key for update status information. - updateStatusDBKey = "core:status/updates" -) - -// Versions holds update versions and status information. -type Versions struct { - record.Base - sync.Mutex - - Core *info.Info - Resources map[string]*updater.Resource - Channel string - Beta bool - Staging bool -} - -// SimpleVersions holds simplified update versions and status information. -type SimpleVersions struct { - record.Base - sync.Mutex - - Build *info.Info - Resources map[string]*SimplifiedResourceVersion - Channel string -} - -// SimplifiedResourceVersion holds version information about one resource. -type SimplifiedResourceVersion struct { - Version string -} - -// UpdateStateExport is a wrapper to export the updates state. -type UpdateStateExport struct { - record.Base - sync.Mutex - - *updater.UpdateState -} - -// GetVersions returns the update versions and status information. -// Resources must be locked when accessed. -func GetVersions() *Versions { - return &Versions{ - Core: info.GetInfo(), - Resources: registry.Export(), - Channel: initialReleaseChannel, - Beta: initialReleaseChannel == helper.ReleaseChannelBeta, - Staging: initialReleaseChannel == helper.ReleaseChannelStaging, - } -} - -// GetSimpleVersions returns the simplified update versions and status information. -func GetSimpleVersions() *SimpleVersions { - // Fill base info. - v := &SimpleVersions{ - Build: info.GetInfo(), - Resources: make(map[string]*SimplifiedResourceVersion), - Channel: initialReleaseChannel, - } - - // Iterate through all versions and add version info. - for id, resource := range registry.Export() { - func() { - resource.Lock() - defer resource.Unlock() - - // Get current in-used or selected version. - var rv *updater.ResourceVersion - switch { - case resource.ActiveVersion != nil: - rv = resource.ActiveVersion - case resource.SelectedVersion != nil: - rv = resource.SelectedVersion - } - - // Get information from resource. - if rv != nil { - v.Resources[id] = &SimplifiedResourceVersion{ - Version: rv.VersionNumber, - } - } - }() - } - - return v -} - -// GetStateExport gets the update state from the registry and returns it in an -// exportable struct. -func GetStateExport() *UpdateStateExport { - export := registry.GetState() - return &UpdateStateExport{ - UpdateState: &export.Updates, - } -} - -// LoadStateExport loads the exported update state from the database. -func LoadStateExport() (*UpdateStateExport, error) { - r, err := db.Get(updateStatusDBKey) - if err != nil { - return nil, err - } - - // unwrap - if r.IsWrapped() { - // only allocate a new struct, if we need it - newRecord := &UpdateStateExport{} - err = record.Unwrap(r, newRecord) - if err != nil { - return nil, err - } - return newRecord, nil - } - - // or adjust type - newRecord, ok := r.(*UpdateStateExport) - if !ok { - return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r) - } - return newRecord, nil -} - -func initVersionExport() (err error) { - if err := GetVersions().save(); err != nil { - log.Warningf("updates: failed to export version information: %s", err) - } - if err := GetSimpleVersions().save(); err != nil { - log.Warningf("updates: failed to export version information: %s", err) - } - - module.EventVersionsUpdated.AddCallback("export version status", export) - return nil -} - -func (v *Versions) save() error { - if !v.KeyIsSet() { - v.SetKey(versionsDBKey) - } - return db.Put(v) -} - -func (v *SimpleVersions) save() error { - if !v.KeyIsSet() { - v.SetKey(simpleVersionsDBKey) - } - return db.Put(v) -} - -func (s *UpdateStateExport) save() error { - if !s.KeyIsSet() { - s.SetKey(updateStatusDBKey) - } - return db.Put(s) -} - -// export is an event hook. -func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { - // Export versions. - if err := GetVersions().save(); err != nil { - return false, err - } - if err := GetSimpleVersions().save(); err != nil { - return false, err - } - // Export udpate state. - if err := GetStateExport().save(); err != nil { - return false, err - } - - return false, nil -} - -// AddToDebugInfo adds the update system status to the given debug.Info. -func AddToDebugInfo(di *debug.Info) { - // Get resources from registry. - resources := registry.Export() - platformPrefix := helper.PlatformIdentifier("") - - // Collect data for debug info. - var active, selected []string - var activeCnt, totalCnt int - for id, r := range resources { - // Ignore resources for other platforms. - if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) { - continue - } - - totalCnt++ - if r.ActiveVersion != nil { - activeCnt++ - active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber)) - } - if r.SelectedVersion != nil { - selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber)) - } - } - sort.Strings(active) - sort.Strings(selected) - - // Compile to one list. - lines := make([]string, 0, len(active)+len(selected)+3) - lines = append(lines, "Active:") - lines = append(lines, active...) - lines = append(lines, "") - lines = append(lines, "Selected:") - lines = append(lines, selected...) - - // Add section. - di.AddSection( - fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt), - debug.UseCodeSection|debug.AddContentLineBreaks, - lines..., - ) -} +// import ( +// "fmt" +// "sort" +// "sync" + +// "github.com/safing/portmaster/base/database/record" +// "github.com/safing/portmaster/base/info" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/updater" +// "github.com/safing/portmaster/base/utils/debug" +// "github.com/safing/portmaster/service/mgr" +// "github.com/safing/portmaster/service/updates/helper" +// ) + +// const ( +// // versionsDBKey is the database key for update version information. +// versionsDBKey = "core:status/versions" + +// // versionsDBKey is the database key for simple update version information. +// simpleVersionsDBKey = "core:status/simple-versions" + +// // updateStatusDBKey is the database key for update status information. +// updateStatusDBKey = "core:status/updates" +// ) + +// // Versions holds update versions and status information. +// type Versions struct { +// record.Base +// sync.Mutex + +// Core *info.Info +// Resources map[string]*updater.Resource +// Channel string +// Beta bool +// Staging bool +// } + +// // SimpleVersions holds simplified update versions and status information. +// type SimpleVersions struct { +// record.Base +// sync.Mutex + +// Build *info.Info +// Resources map[string]*SimplifiedResourceVersion +// Channel string +// } + +// // SimplifiedResourceVersion holds version information about one resource. +// type SimplifiedResourceVersion struct { +// Version string +// } + +// // UpdateStateExport is a wrapper to export the updates state. +// type UpdateStateExport struct { +// record.Base +// sync.Mutex + +// *updater.UpdateState +// } + +// // GetVersions returns the update versions and status information. +// // Resources must be locked when accessed. +// func GetVersions() *Versions { +// return &Versions{ +// Core: info.GetInfo(), +// Resources: nil, +// Channel: initialReleaseChannel, +// Beta: initialReleaseChannel == helper.ReleaseChannelBeta, +// Staging: initialReleaseChannel == helper.ReleaseChannelStaging, +// } +// } + +// // GetSimpleVersions returns the simplified update versions and status information. +// func GetSimpleVersions() *SimpleVersions { +// // Fill base info. +// v := &SimpleVersions{ +// Build: info.GetInfo(), +// Resources: make(map[string]*SimplifiedResourceVersion), +// Channel: initialReleaseChannel, +// } + +// // Iterate through all versions and add version info. +// // for id, resource := range registry.Export() { +// // func() { +// // resource.Lock() +// // defer resource.Unlock() + +// // // Get current in-used or selected version. +// // var rv *updater.ResourceVersion +// // switch { +// // case resource.ActiveVersion != nil: +// // rv = resource.ActiveVersion +// // case resource.SelectedVersion != nil: +// // rv = resource.SelectedVersion +// // } + +// // // Get information from resource. +// // if rv != nil { +// // v.Resources[id] = &SimplifiedResourceVersion{ +// // Version: rv.VersionNumber, +// // } +// // } +// // }() +// // } + +// return v +// } + +// // GetStateExport gets the update state from the registry and returns it in an +// // exportable struct. +// func GetStateExport() *UpdateStateExport { +// // export := registry.GetState() +// return &UpdateStateExport{ +// // UpdateState: &export.Updates, +// } +// } + +// // LoadStateExport loads the exported update state from the database. +// func LoadStateExport() (*UpdateStateExport, error) { +// r, err := db.Get(updateStatusDBKey) +// if err != nil { +// return nil, err +// } + +// // unwrap +// if r.IsWrapped() { +// // only allocate a new struct, if we need it +// newRecord := &UpdateStateExport{} +// err = record.Unwrap(r, newRecord) +// if err != nil { +// return nil, err +// } +// return newRecord, nil +// } + +// // or adjust type +// newRecord, ok := r.(*UpdateStateExport) +// if !ok { +// return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r) +// } +// return newRecord, nil +// } + +// func initVersionExport() (err error) { +// if err := GetVersions().save(); err != nil { +// log.Warningf("updates: failed to export version information: %s", err) +// } +// if err := GetSimpleVersions().save(); err != nil { +// log.Warningf("updates: failed to export version information: %s", err) +// } + +// // module.EventVersionsUpdated.AddCallback("export version status", export) +// return nil +// } + +// func (v *Versions) save() error { +// if !v.KeyIsSet() { +// v.SetKey(versionsDBKey) +// } +// return db.Put(v) +// } + +// func (v *SimpleVersions) save() error { +// if !v.KeyIsSet() { +// v.SetKey(simpleVersionsDBKey) +// } +// return db.Put(v) +// } + +// func (s *UpdateStateExport) save() error { +// if !s.KeyIsSet() { +// s.SetKey(updateStatusDBKey) +// } +// return db.Put(s) +// } + +// // export is an event hook. +// func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { +// // Export versions. +// if err := GetVersions().save(); err != nil { +// return false, err +// } +// if err := GetSimpleVersions().save(); err != nil { +// return false, err +// } +// // Export udpate state. +// if err := GetStateExport().save(); err != nil { +// return false, err +// } + +// return false, nil +// } + +// // AddToDebugInfo adds the update system status to the given debug.Info. +// func AddToDebugInfo(di *debug.Info) { +// // Get resources from registry. +// // resources := registry.Export() +// // platformPrefix := helper.PlatformIdentifier("") + +// // Collect data for debug info. +// var active, selected []string +// var activeCnt, totalCnt int +// // for id, r := range resources { +// // // Ignore resources for other platforms. +// // if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) { +// // continue +// // } + +// // totalCnt++ +// // if r.ActiveVersion != nil { +// // activeCnt++ +// // active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber)) +// // } +// // if r.SelectedVersion != nil { +// // selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber)) +// // } +// // } +// sort.Strings(active) +// sort.Strings(selected) + +// // Compile to one list. +// lines := make([]string, 0, len(active)+len(selected)+3) +// lines = append(lines, "Active:") +// lines = append(lines, active...) +// lines = append(lines, "") +// lines = append(lines, "Selected:") +// lines = append(lines, selected...) + +// // Add section. +// di.AddSection( +// fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt), +// debug.UseCodeSection|debug.AddContentLineBreaks, +// lines..., +// ) +// } diff --git a/service/updates/get.go b/service/updates/get.go index bac9ae148..75fc4c3e7 100644 --- a/service/updates/get.go +++ b/service/updates/get.go @@ -1,72 +1,65 @@ package updates -import ( - "path" - - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates/helper" -) - // GetPlatformFile returns the latest platform specific file identified by the given identifier. -func GetPlatformFile(identifier string) (*updater.File, error) { - identifier = helper.PlatformIdentifier(identifier) +// func GetPlatformFile(identifier string) (*updater.File, error) { +// identifier = helper.PlatformIdentifier(identifier) - file, err := registry.GetFile(identifier) - if err != nil { - return nil, err - } +// file, err := registry.GetFile(identifier) +// if err != nil { +// return nil, err +// } - module.EventVersionsUpdated.Submit(struct{}{}) - return file, nil -} +// module.EventVersionsUpdated.Submit(struct{}{}) +// return file, nil +// } // GetFile returns the latest generic file identified by the given identifier. -func GetFile(identifier string) (*updater.File, error) { - identifier = path.Join("all", identifier) +// func GetFile(identifier string) (*updater.File, error) { +// identifier = path.Join("all", identifier) - file, err := registry.GetFile(identifier) - if err != nil { - return nil, err - } +// file, err := registry.GetFile(identifier) +// if err != nil { +// return nil, err +// } - module.EventVersionsUpdated.Submit(struct{}{}) - return file, nil -} +// module.EventVersionsUpdated.Submit(struct{}{}) +// return file, nil +// } // GetPlatformVersion returns the selected platform specific version of the // given identifier. // The returned resource version may not be modified. -func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) { - identifier = helper.PlatformIdentifier(identifier) +// func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) { +// identifier = helper.PlatformIdentifier(identifier) - rv, err := registry.GetVersion(identifier) - if err != nil { - return nil, err - } +// rv, err := registry.GetVersion(identifier) +// if err != nil { +// return nil, err +// } - return rv, nil -} +// return rv, nil +// } // GetVersion returns the selected generic version of the given identifier. // The returned resource version may not be modified. -func GetVersion(identifier string) (*updater.ResourceVersion, error) { - identifier = path.Join("all", identifier) +// func GetVersion(identifier string) (*updater.ResourceVersion, error) { +// identifier = path.Join("all", identifier) - rv, err := registry.GetVersion(identifier) - if err != nil { - return nil, err - } +// rv, err := registry.GetVersion(identifier) +// if err != nil { +// return nil, err +// } - return rv, nil -} +// return rv, nil +// } // GetVersionWithFullID returns the selected generic version of the given full identifier. // The returned resource version may not be modified. -func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) { - rv, err := registry.GetVersion(identifier) - if err != nil { - return nil, err - } - - return rv, nil -} +// func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) { +// rv, err := registry.GetVersion(identifier) +// if err != nil { +// return nil, err +// } + +// return rv, nil +// } diff --git a/service/updates/helper/electron.go b/service/updates/helper/electron.go index 4c8c4a07d..a0c10149a 100644 --- a/service/updates/helper/electron.go +++ b/service/updates/helper/electron.go @@ -1,57 +1,58 @@ package helper -import ( - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" -) - -var pmElectronUpdate *updater.File - -const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s` - -// EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed -// by our app-electron package has the SUID bit set on systems that do not -// allow unprivileged CLONE_NEWUSER (clone(3)). -// On non-linux systems or systems that have kernel.unprivileged_userns_clone -// set to 1 EnsureChromeSandboPermissions is a NO-OP. -func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error { - if runtime.GOOS != "linux" { - return nil - } - - if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() { - return nil - } - - identifier := PlatformIdentifier("app/portmaster-app.zip") - - var err error - pmElectronUpdate, err = reg.GetFile(identifier) - if err != nil { - if errors.Is(err, updater.ErrNotAvailableLocally) { - return nil - } - return fmt.Errorf("failed to get file: %w", err) - } - - unpackedPath := strings.TrimSuffix( - pmElectronUpdate.Path(), - filepath.Ext(pmElectronUpdate.Path()), - ) - sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox") - if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil { - log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile) - - return fmt.Errorf("failed to chmod: %w", err) - } - log.Debugf("updates: fixed SUID permission for chrome-sandbox") - - return nil -} +// import ( +// "errors" +// "fmt" +// "os" +// "path/filepath" +// "runtime" +// "strings" + +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/updater" +// "github.com/safing/portmaster/service/updates/registry" +// ) + +// var pmElectronUpdate *registry.File + +// const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s` + +// // EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed +// // by our app-electron package has the SUID bit set on systems that do not +// // allow unprivileged CLONE_NEWUSER (clone(3)). +// // On non-linux systems or systems that have kernel.unprivileged_userns_clone +// // set to 1 EnsureChromeSandboPermissions is a NO-OP. +// func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error { +// if runtime.GOOS != "linux" { +// return nil +// } + +// if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() { +// return nil +// } + +// identifier := PlatformIdentifier("app/portmaster-app.zip") + +// var err error +// pmElectronUpdate, err = reg.GetFile(identifier) +// if err != nil { +// if errors.Is(err, updater.ErrNotAvailableLocally) { +// return nil +// } +// return fmt.Errorf("failed to get file: %w", err) +// } + +// unpackedPath := strings.TrimSuffix( +// pmElectronUpdate.Path(), +// filepath.Ext(pmElectronUpdate.Path()), +// ) +// sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox") +// if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil { +// log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile) + +// return fmt.Errorf("failed to chmod: %w", err) +// } +// log.Debugf("updates: fixed SUID permission for chrome-sandbox") + +// return nil +// } diff --git a/service/updates/helper/indexes.go b/service/updates/helper/indexes.go index 72457bc53..7b9e671ed 100644 --- a/service/updates/helper/indexes.go +++ b/service/updates/helper/indexes.go @@ -1,136 +1,136 @@ package helper -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - - "github.com/safing/jess/filesig" - "github.com/safing/portmaster/base/updater" -) - -// Release Channel Configuration Keys. -const ( - ReleaseChannelKey = "core/releaseChannel" - ReleaseChannelJSONKey = "core.releaseChannel" -) - -// Release Channels. -const ( - ReleaseChannelStable = "stable" - ReleaseChannelBeta = "beta" - ReleaseChannelStaging = "staging" - ReleaseChannelSupport = "support" -) - -const jsonSuffix = ".json" - -// SetIndexes sets the update registry indexes and also configures the registry -// to use pre-releases based on the channel. -func SetIndexes( - registry *updater.ResourceRegistry, - releaseChannel string, - deleteUnusedIndexes bool, - autoDownload bool, - autoDownloadIntel bool, -) (warning error) { - usePreReleases := false - - // Be reminded that the order is important, as indexes added later will - // override the current release from earlier indexes. - - // Reset indexes before adding them (again). - registry.ResetIndexes() - - // Add the intel index first, in order to be able to override it with the - // other indexes when needed. - registry.AddIndex(updater.Index{ - Path: "all/intel/intel.json", - AutoDownload: autoDownloadIntel, - }) - - // Always add the stable index as a base. - registry.AddIndex(updater.Index{ - Path: ReleaseChannelStable + jsonSuffix, - AutoDownload: autoDownload, - }) - - // Add beta index if in beta or staging channel. - indexPath := ReleaseChannelBeta + jsonSuffix - if releaseChannel == ReleaseChannelBeta || - releaseChannel == ReleaseChannelStaging || - (releaseChannel == "" && indexExists(registry, indexPath)) { - registry.AddIndex(updater.Index{ - Path: indexPath, - PreRelease: true, - AutoDownload: autoDownload, - }) - usePreReleases = true - } else if deleteUnusedIndexes { - err := deleteIndex(registry, indexPath) - if err != nil { - warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) - } - } - - // Add staging index if in staging channel. - indexPath = ReleaseChannelStaging + jsonSuffix - if releaseChannel == ReleaseChannelStaging || - (releaseChannel == "" && indexExists(registry, indexPath)) { - registry.AddIndex(updater.Index{ - Path: indexPath, - PreRelease: true, - AutoDownload: autoDownload, - }) - usePreReleases = true - } else if deleteUnusedIndexes { - err := deleteIndex(registry, indexPath) - if err != nil { - warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) - } - } - - // Add support index if in support channel. - indexPath = ReleaseChannelSupport + jsonSuffix - if releaseChannel == ReleaseChannelSupport || - (releaseChannel == "" && indexExists(registry, indexPath)) { - registry.AddIndex(updater.Index{ - Path: indexPath, - AutoDownload: autoDownload, - }) - usePreReleases = true - } else if deleteUnusedIndexes { - err := deleteIndex(registry, indexPath) - if err != nil { - warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) - } - } - - // Set pre-release usage. - registry.SetUsePreReleases(usePreReleases) - - return warning -} - -func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { - _, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath)) - return err == nil -} - -func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { - // Remove index itself. - err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } - - // Remove any accompanying signature. - err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } - - return nil -} +// import ( +// "errors" +// "fmt" +// "io/fs" +// "os" +// "path/filepath" + +// "github.com/safing/jess/filesig" +// "github.com/safing/portmaster/base/updater" +// ) + +// // Release Channel Configuration Keys. +// const ( +// ReleaseChannelKey = "core/releaseChannel" +// ReleaseChannelJSONKey = "core.releaseChannel" +// ) + +// // Release Channels. +// const ( +// ReleaseChannelStable = "stable" +// ReleaseChannelBeta = "beta" +// ReleaseChannelStaging = "staging" +// ReleaseChannelSupport = "support" +// ) + +// const jsonSuffix = ".json" + +// // SetIndexes sets the update registry indexes and also configures the registry +// // to use pre-releases based on the channel. +// func SetIndexes( +// registry *updater.ResourceRegistry, +// releaseChannel string, +// deleteUnusedIndexes bool, +// autoDownload bool, +// autoDownloadIntel bool, +// ) (warning error) { +// usePreReleases := false + +// // Be reminded that the order is important, as indexes added later will +// // override the current release from earlier indexes. + +// // Reset indexes before adding them (again). +// registry.ResetIndexes() + +// // Add the intel index first, in order to be able to override it with the +// // other indexes when needed. +// registry.AddIndex(updater.Index{ +// Path: "all/intel/intel.json", +// AutoDownload: autoDownloadIntel, +// }) + +// // Always add the stable index as a base. +// registry.AddIndex(updater.Index{ +// Path: ReleaseChannelStable + jsonSuffix, +// AutoDownload: autoDownload, +// }) + +// // Add beta index if in beta or staging channel. +// indexPath := ReleaseChannelBeta + jsonSuffix +// if releaseChannel == ReleaseChannelBeta || +// releaseChannel == ReleaseChannelStaging || +// (releaseChannel == "" && indexExists(registry, indexPath)) { +// registry.AddIndex(updater.Index{ +// Path: indexPath, +// PreRelease: true, +// AutoDownload: autoDownload, +// }) +// usePreReleases = true +// } else if deleteUnusedIndexes { +// err := deleteIndex(registry, indexPath) +// if err != nil { +// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) +// } +// } + +// // Add staging index if in staging channel. +// indexPath = ReleaseChannelStaging + jsonSuffix +// if releaseChannel == ReleaseChannelStaging || +// (releaseChannel == "" && indexExists(registry, indexPath)) { +// registry.AddIndex(updater.Index{ +// Path: indexPath, +// PreRelease: true, +// AutoDownload: autoDownload, +// }) +// usePreReleases = true +// } else if deleteUnusedIndexes { +// err := deleteIndex(registry, indexPath) +// if err != nil { +// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) +// } +// } + +// // Add support index if in support channel. +// indexPath = ReleaseChannelSupport + jsonSuffix +// if releaseChannel == ReleaseChannelSupport || +// (releaseChannel == "" && indexExists(registry, indexPath)) { +// registry.AddIndex(updater.Index{ +// Path: indexPath, +// AutoDownload: autoDownload, +// }) +// usePreReleases = true +// } else if deleteUnusedIndexes { +// err := deleteIndex(registry, indexPath) +// if err != nil { +// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) +// } +// } + +// // Set pre-release usage. +// registry.SetUsePreReleases(usePreReleases) + +// return warning +// } + +// func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { +// _, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath)) +// return err == nil +// } + +// func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { +// // Remove index itself. +// err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) +// if err != nil && !errors.Is(err, fs.ErrNotExist) { +// return err +// } + +// // Remove any accompanying signature. +// err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) +// if err != nil && !errors.Is(err, fs.ErrNotExist) { +// return err +// } + +// return nil +// } diff --git a/service/updates/helper/signing.go b/service/updates/helper/signing.go index 136b1970b..9af986992 100644 --- a/service/updates/helper/signing.go +++ b/service/updates/helper/signing.go @@ -1,42 +1,42 @@ package helper -import ( - "github.com/safing/jess" - "github.com/safing/portmaster/base/updater" -) +// import ( +// "github.com/safing/jess" +// "github.com/safing/portmaster/base/updater" +// ) -var ( - // VerificationConfig holds the complete verification configuration for the registry. - VerificationConfig = map[string]*updater.VerificationOptions{ - "": { // Default. - TrustStore: BinarySigningTrustStore, - DownloadPolicy: updater.SignaturePolicyRequire, - DiskLoadPolicy: updater.SignaturePolicyWarn, - }, - "all/intel/": nil, // Disable until IntelHub supports signing. - } +// var ( +// // VerificationConfig holds the complete verification configuration for the registry. +// VerificationConfig = map[string]*updater.VerificationOptions{ +// "": { // Default. +// TrustStore: BinarySigningTrustStore, +// DownloadPolicy: updater.SignaturePolicyRequire, +// DiskLoadPolicy: updater.SignaturePolicyWarn, +// }, +// "all/intel/": nil, // Disable until IntelHub supports signing. +// } - // BinarySigningKeys holds the signing keys in text format. - BinarySigningKeys = []string{ - // Safing Code Signing Key #1 - "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", - // Safing Code Signing Key #2 - "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", - } +// // BinarySigningKeys holds the signing keys in text format. +// BinarySigningKeys = []string{ +// // Safing Code Signing Key #1 +// "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", +// // Safing Code Signing Key #2 +// "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", +// } - // BinarySigningTrustStore is an in-memory trust store with the signing keys. - BinarySigningTrustStore = jess.NewMemTrustStore() -) +// // BinarySigningTrustStore is an in-memory trust store with the signing keys. +// BinarySigningTrustStore = jess.NewMemTrustStore() +// ) -func init() { - for _, signingKey := range BinarySigningKeys { - rcpt, err := jess.RecipientFromTextFormat(signingKey) - if err != nil { - panic(err) - } - err = BinarySigningTrustStore.StoreSignet(rcpt) - if err != nil { - panic(err) - } - } -} +// func init() { +// for _, signingKey := range BinarySigningKeys { +// rcpt, err := jess.RecipientFromTextFormat(signingKey) +// if err != nil { +// panic(err) +// } +// err = BinarySigningTrustStore.StoreSignet(rcpt) +// if err != nil { +// panic(err) +// } +// } +// } diff --git a/service/updates/helper/updates.go b/service/updates/helper/updates.go index efae917d0..135c12227 100644 --- a/service/updates/helper/updates.go +++ b/service/updates/helper/updates.go @@ -1,95 +1,95 @@ package helper -import ( - "fmt" - "runtime" - - "github.com/tevino/abool" -) - -const onWindows = runtime.GOOS == "windows" - -var intelOnly = abool.New() - -// IntelOnly specifies that only intel data is mandatory. -func IntelOnly() { - intelOnly.Set() -} - -// PlatformIdentifier converts identifier for the current platform. -func PlatformIdentifier(identifier string) string { - // From https://golang.org/pkg/runtime/#GOARCH - // GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on. - // GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on. - return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier) -} - -// MandatoryUpdates returns mandatory updates that should be loaded on install -// or reset. -func MandatoryUpdates() (identifiers []string) { - // Intel - identifiers = append( - identifiers, - - // Filter lists data - "all/intel/lists/index.dsd", - "all/intel/lists/base.dsdl", - "all/intel/lists/intermediate.dsdl", - "all/intel/lists/urgent.dsdl", - - // Geo IP data - "all/intel/geoip/geoipv4.mmdb.gz", - "all/intel/geoip/geoipv6.mmdb.gz", - ) - - // Stop here if we only want intel data. - if intelOnly.IsSet() { - return identifiers - } - - // Binaries - if onWindows { - identifiers = append( - identifiers, - PlatformIdentifier("core/portmaster-core.exe"), - PlatformIdentifier("kext/portmaster-kext.sys"), - PlatformIdentifier("kext/portmaster-kext.pdb"), - PlatformIdentifier("start/portmaster-start.exe"), - PlatformIdentifier("notifier/portmaster-notifier.exe"), - PlatformIdentifier("notifier/portmaster-wintoast.dll"), - PlatformIdentifier("app2/portmaster-app.zip"), - ) - } else { - identifiers = append( - identifiers, - PlatformIdentifier("core/portmaster-core"), - PlatformIdentifier("start/portmaster-start"), - PlatformIdentifier("notifier/portmaster-notifier"), - PlatformIdentifier("app2/portmaster-app"), - ) - } - - // Components, Assets and Data - identifiers = append( - identifiers, - - // User interface components - PlatformIdentifier("app/portmaster-app.zip"), - "all/ui/modules/portmaster.zip", - "all/ui/modules/assets.zip", - ) - - return identifiers -} - -// AutoUnpackUpdates returns assets that need unpacking. -func AutoUnpackUpdates() []string { - if intelOnly.IsSet() { - return []string{} - } - - return []string{ - PlatformIdentifier("app/portmaster-app.zip"), - PlatformIdentifier("app2/portmaster-app.zip"), - } -} +// import ( +// "fmt" +// "runtime" + +// "github.com/tevino/abool" +// ) + +// const onWindows = runtime.GOOS == "windows" + +// var intelOnly = abool.New() + +// // IntelOnly specifies that only intel data is mandatory. +// func IntelOnly() { +// intelOnly.Set() +// } + +// // PlatformIdentifier converts identifier for the current platform. +// func PlatformIdentifier(identifier string) string { +// // From https://golang.org/pkg/runtime/#GOARCH +// // GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on. +// // GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on. +// return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier) +// } + +// // MandatoryUpdates returns mandatory updates that should be loaded on install +// // or reset. +// func MandatoryUpdates() (identifiers []string) { +// // Intel +// identifiers = append( +// identifiers, + +// // Filter lists data +// "all/intel/lists/index.dsd", +// "all/intel/lists/base.dsdl", +// "all/intel/lists/intermediate.dsdl", +// "all/intel/lists/urgent.dsdl", + +// // Geo IP data +// "all/intel/geoip/geoipv4.mmdb.gz", +// "all/intel/geoip/geoipv6.mmdb.gz", +// ) + +// // Stop here if we only want intel data. +// if intelOnly.IsSet() { +// return identifiers +// } + +// // Binaries +// if onWindows { +// identifiers = append( +// identifiers, +// PlatformIdentifier("core/portmaster-core.exe"), +// PlatformIdentifier("kext/portmaster-kext.sys"), +// PlatformIdentifier("kext/portmaster-kext.pdb"), +// PlatformIdentifier("start/portmaster-start.exe"), +// PlatformIdentifier("notifier/portmaster-notifier.exe"), +// PlatformIdentifier("notifier/portmaster-wintoast.dll"), +// PlatformIdentifier("app2/portmaster-app.zip"), +// ) +// } else { +// identifiers = append( +// identifiers, +// PlatformIdentifier("core/portmaster-core"), +// PlatformIdentifier("start/portmaster-start"), +// PlatformIdentifier("notifier/portmaster-notifier"), +// PlatformIdentifier("app2/portmaster-app"), +// ) +// } + +// // Components, Assets and Data +// identifiers = append( +// identifiers, + +// // User interface components +// PlatformIdentifier("app/portmaster-app.zip"), +// "all/ui/modules/portmaster.zip", +// "all/ui/modules/assets.zip", +// ) + +// return identifiers +// } + +// // AutoUnpackUpdates returns assets that need unpacking. +// func AutoUnpackUpdates() []string { +// if intelOnly.IsSet() { +// return []string{} +// } + +// return []string{ +// PlatformIdentifier("app/portmaster-app.zip"), +// PlatformIdentifier("app2/portmaster-app.zip"), +// } +// } diff --git a/service/updates/index.go b/service/updates/index.go deleted file mode 100644 index adf4564f0..000000000 --- a/service/updates/index.go +++ /dev/null @@ -1,110 +0,0 @@ -package updates - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - - "github.com/safing/portmaster/base/log" -) - -type UpdateIndex struct { - Directory string - DownloadDirectory string - Ignore []string - IndexURLs []string - IndexFile string - AutoApply bool -} - -func (ui *UpdateIndex) downloadIndexFile() (err error) { - _ = os.MkdirAll(ui.Directory, defaultDirMode) - _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) - for _, url := range ui.IndexURLs { - err = ui.downloadIndexFileFromURL(url) - if err != nil { - log.Warningf("updates: %s", err) - continue - } - // Downloading was successful. - err = nil - break - } - return -} - -func (ui *UpdateIndex) checkForUpdates() (bool, error) { - err := ui.downloadIndexFile() - if err != nil { - return false, err - } - - currentBundle, err := ui.GetInstallBundle() - if err != nil { - return true, err // Current installed bundle not found, act as there is update. - } - updateBundle, err := ui.GetUpdateBundle() - if err != nil { - return false, err - } - - return currentBundle.Version != updateBundle.Version, nil -} - -func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { - client := http.Client{} - resp, err := client.Get(url) - if err != nil { - return fmt.Errorf("failed a get request to %s: %w", url, err) - } - defer func() { _ = resp.Body.Close() }() - filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) - file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode) - if err != nil { - return err - } - defer func() { _ = file.Close() }() - - _, err = io.Copy(file, resp.Body) - if err != nil { - return err - } - - return nil -} - -func (ui *UpdateIndex) GetInstallBundle() (*Bundle, error) { - indexFile := fmt.Sprintf("%s/%s", ui.Directory, ui.IndexFile) - return ui.GetBundle(indexFile) -} - -func (ui *UpdateIndex) GetUpdateBundle() (*Bundle, error) { - indexFile := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) - return ui.GetBundle(indexFile) -} - -func (ui *UpdateIndex) GetBundle(indexFile string) (*Bundle, error) { - // Check if the file exists. - file, err := os.Open(indexFile) - if err != nil { - return nil, fmt.Errorf("failed to open index file: %w", err) - } - defer func() { _ = file.Close() }() - - // Read - content, err := io.ReadAll(file) - if err != nil { - return nil, err - } - - // Parse - var bundle Bundle - err = json.Unmarshal(content, &bundle) - if err != nil { - return nil, err - } - - return &bundle, nil -} diff --git a/service/updates/main.go b/service/updates/main.go index 3e11064cb..f9e68b8b1 100644 --- a/service/updates/main.go +++ b/service/updates/main.go @@ -6,9 +6,6 @@ import ( "time" "github.com/safing/portmaster/base/database" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/mgr" ) const ( @@ -17,10 +14,6 @@ const ( enableSoftwareUpdatesKey = "core/automaticUpdates" enableIntelUpdatesKey = "core/automaticIntelUpdates" - // ModuleName is the name of the update module - // and can be used when declaring module dependencies. - ModuleName = "updates" - // VersionUpdateEvent is emitted every time a new // version of a monitored resource is selected. // During module initialization VersionUpdateEvent @@ -37,8 +30,6 @@ const ( ) var ( - registry *updater.ResourceRegistry - userAgentFromFlag string updateServerFromFlag string @@ -57,206 +48,14 @@ const ( updateTaskRepeatDuration = 1 * time.Hour ) -func start() error { - // module.restartWorkerMgr.Repeat(10 * time.Minute) - // module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig) - - // // create registry - // registry = &updater.ResourceRegistry{ - // Name: ModuleName, - // UpdateURLs: DefaultUpdateURLs, - // UserAgent: UserAgent, - // MandatoryUpdates: helper.MandatoryUpdates(), - // AutoUnpack: helper.AutoUnpackUpdates(), - // Verification: helper.VerificationConfig, - // DevMode: devMode(), - // Online: true, - // } - // // Override values from flags. - // if userAgentFromFlag != "" { - // registry.UserAgent = userAgentFromFlag - // } - // if updateServerFromFlag != "" { - // registry.UpdateURLs = []string{updateServerFromFlag} - // } - - // // pre-init state - // updateStateExport, err := LoadStateExport() - // if err != nil { - // log.Debugf("updates: failed to load exported update state: %s", err) - // } else if updateStateExport.UpdateState != nil { - // err := registry.PreInitUpdateState(*updateStateExport.UpdateState) +func stop() error { + // if registry != nil { + // err := registry.Cleanup() // if err != nil { - // return err - // } - // } - - // initialize - // err := registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755)) - // if err != nil { - // return err - // } - - // // register state provider - // err = registerRegistryStateProvider() - // if err != nil { - // return err - // } - // registry.StateNotifyFunc = pushRegistryState - - // // Set indexes based on the release channel. - // warning := helper.SetIndexes( - // registry, - // initialReleaseChannel, - // true, - // enableSoftwareUpdates() && !DisableSoftwareAutoUpdate, - // enableIntelUpdates(), - // ) - // if warning != nil { - // log.Warningf("updates: %s", warning) - // } - - // err = registry.LoadIndexes(module.m.Ctx()) - // if err != nil { - // log.Warningf("updates: failed to load indexes: %s", err) - // } - - // err = registry.ScanStorage("") - // if err != nil { - // log.Warningf("updates: error during storage scan: %s", err) - // } - - // registry.SelectVersions() - // module.EventVersionsUpdated.Submit(struct{}{}) - - // // Initialize the version export - this requires the registry to be set up. - // err = initVersionExport() - // if err != nil { - // return err - // } - - // // start updater task - // if !disableTaskSchedule { - // _ = module.updateWorkerMgr.Repeat(30 * time.Minute) - // } - - // if updateASAP { - // module.updateWorkerMgr.Go() - // } - - // // react to upgrades - // if err := initUpgrader(); err != nil { - // return err - // } - - // warnOnIncorrectParentPath() - - return nil -} - -// TriggerUpdate queues the update task to execute ASAP. -func TriggerUpdate(forceIndexCheck, downloadAll bool) error { - // switch { - // case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates(): - // return errors.New("automatic updating is disabled") - - // default: - // if forceIndexCheck { - // forceCheck.Set() - // } - // if downloadAll { - // forceDownload.Set() + // log.Warningf("updates: failed to clean up registry: %s", err) // } - - // // If index check if forced, start quicker. - // module.updateWorkerMgr.Go() - // } - - log.Debugf("updates: triggering update to run as soon as possible") - return nil -} - -// DisableUpdateSchedule disables the update schedule. -// If called, updates are only checked when TriggerUpdate() -// is called. -func DisableUpdateSchedule() error { - // TODO: Updater state should be always on - // switch module.Status() { - // case modules.StatusStarting, modules.StatusOnline, modules.StatusStopping: - // return errors.New("module already online") - // } - - return nil -} - -func checkForUpdates(ctx *mgr.WorkerCtx) (err error) { - // Set correct error if context was canceled. - // defer func() { - // select { - // case <-ctx.Done(): - // err = context.Canceled - // default: - // } - // }() - - // // Get flags. - // forceIndexCheck := forceCheck.SetToIf(true, false) - // downloadAll := forceDownload.SetToIf(true, false) - - // // Check again if downloading updates is enabled, or forced. - // if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() { - // log.Warningf("updates: automatic updates are disabled") - // return nil // } - // defer func() { - // // Resolve any error and send success notification. - // if err == nil { - // log.Infof("updates: successfully checked for updates") - // notifyUpdateSuccess(forceIndexCheck) - // return - // } - - // // Log and notify error. - // log.Errorf("updates: check failed: %s", err) - // notifyUpdateCheckFailed(forceIndexCheck, err) - // }() - - // if err = registry.UpdateIndexes(ctx.Ctx()); err != nil { - // err = fmt.Errorf("failed to update indexes: %w", err) - // return //nolint:nakedret // TODO: Would "return err" work with the defer? - // } - - // err = registry.DownloadUpdates(ctx.Ctx(), downloadAll) - // if err != nil { - // err = fmt.Errorf("failed to download updates: %w", err) - // return //nolint:nakedret // TODO: Would "return err" work with the defer? - // } - - // registry.SelectVersions() - - // // Unpack selected resources. - // err = registry.UnpackResources() - // if err != nil { - // err = fmt.Errorf("failed to unpack updates: %w", err) - // return //nolint:nakedret // TODO: Would "return err" work with the defer? - // } - - // // Purge old resources - // registry.Purge(2) - - // module.EventResourcesUpdated.Submit(struct{}{}) - return nil -} - -func stop() error { - if registry != nil { - err := registry.Cleanup() - if err != nil { - log.Warningf("updates: failed to clean up registry: %s", err) - } - } - return nil } diff --git a/service/updates/module.go b/service/updates/module.go index 002e315ec..989ad0208 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -2,10 +2,8 @@ package updates import ( "errors" + "flag" "fmt" - "os" - "path/filepath" - "strings" "sync/atomic" "github.com/safing/portmaster/base/api" @@ -13,34 +11,33 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates/registry" ) -const ( - defaultFileMode = os.FileMode(0o0644) - defaultDirMode = os.FileMode(0o0755) -) +var applyUpdates bool + +func init() { + flag.BoolVar(&applyUpdates, "update", false, "apply downloaded updates") +} // Updates provides access to released artifacts. type Updates struct { m *mgr.Manager states *mgr.StateMgr - updateWorkerMgr *mgr.WorkerMgr - restartWorkerMgr *mgr.WorkerMgr + updateBinaryWorkerMgr *mgr.WorkerMgr + updateIntelWorkerMgr *mgr.WorkerMgr + restartWorkerMgr *mgr.WorkerMgr EventResourcesUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}] - binUpdates UpdateIndex - intelUpdates UpdateIndex + registry registry.Registry instance instance } -var ( - module *Updates - shimLoaded atomic.Bool -) +var shimLoaded atomic.Bool // New returns a new UI module. func New(instance instance) (*Updates, error) { @@ -49,20 +46,22 @@ func New(instance instance) (*Updates, error) { } m := mgr.New("Updates") - module = &Updates{ + module := &Updates{ m: m, states: m.NewStateMgr(), EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), - instance: instance, + + instance: instance, } // Events - module.updateWorkerMgr = m.NewWorkerMgr("updater", module.checkForUpdates, nil) + module.updateBinaryWorkerMgr = m.NewWorkerMgr("binary updater", module.checkForBinaryUpdates, nil) + module.updateIntelWorkerMgr = m.NewWorkerMgr("intel updater", module.checkForIntelUpdates, nil) module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil) - module.binUpdates = UpdateIndex{ + binIndex := registry.UpdateIndex{ Directory: "/usr/lib/portmaster", DownloadDirectory: "/var/portmaster/new_bin", Ignore: []string{"databases", "intel", "config.json"}, @@ -71,62 +70,48 @@ func New(instance instance) (*Updates, error) { AutoApply: false, } - module.intelUpdates = UpdateIndex{ + intelIndex := registry.UpdateIndex{ Directory: "/var/portmaster/intel", DownloadDirectory: "/var/portmaster/new_intel", IndexURLs: []string{"http://localhost:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, } + module.registry = registry.New(binIndex, intelIndex) return module, nil } -func deleteUnfinishedDownloads(rootDir string) error { - return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Check if the current file has the specified extension - if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { - log.Warningf("updates deleting unfinished: %s\n", path) - err := os.Remove(path) - if err != nil { - return fmt.Errorf("failed to delete file %s: %w", path, err) - } - } - - return nil - }) -} - -func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { - _ = deleteUnfinishedDownloads(u.binUpdates.DownloadDirectory) - hasUpdate, err := u.binUpdates.checkForUpdates() +func (u *Updates) checkForBinaryUpdates(_ *mgr.WorkerCtx) error { + hasUpdates, err := u.registry.CheckForBinaryUpdates() if err != nil { - log.Warningf("failed to get binary index file: %s", err) + log.Errorf("updates: failed to check for binary updates: %s", err) } - if hasUpdate { - binBundle, err := u.binUpdates.GetUpdateBundle() - if err == nil { - log.Debugf("Bin Bundle: %+v", binBundle) - _ = os.MkdirAll(u.binUpdates.DownloadDirectory, defaultDirMode) - binBundle.downloadAndVerify(u.binUpdates.DownloadDirectory) + if hasUpdates { + log.Infof("updates: there is updates available in the binary bundle") + err = u.registry.DownloadBinaryUpdates() + if err != nil { + log.Errorf("updates: failed to download bundle: %s", err) } + } else { + log.Infof("updates: no new binary updates") } - _ = deleteUnfinishedDownloads(u.intelUpdates.DownloadDirectory) - hasUpdate, err = u.intelUpdates.checkForUpdates() + return nil +} + +func (u *Updates) checkForIntelUpdates(_ *mgr.WorkerCtx) error { + hasUpdates, err := u.registry.CheckForIntelUpdates() if err != nil { - log.Warningf("failed to get intel index file: %s", err) + log.Errorf("updates: failed to check for intel updates: %s", err) } - if hasUpdate { - intelBundle, err := u.intelUpdates.GetUpdateBundle() - if err == nil { - log.Debugf("Intel Bundle: %+v", intelBundle) - _ = os.MkdirAll(u.intelUpdates.DownloadDirectory, defaultDirMode) - intelBundle.downloadAndVerify(u.intelUpdates.DownloadDirectory) + if hasUpdates { + log.Infof("updates: there is updates available in the intel bundle") + err = u.registry.DownloadIntelUpdates() + if err != nil { + log.Errorf("updates: failed to download bundle: %s", err) } + } else { + log.Infof("updates: no new intel data updates") } return nil } @@ -143,38 +128,36 @@ func (u *Updates) Manager() *mgr.Manager { // Start starts the module. func (u *Updates) Start() error { - initConfig() - u.m.Go("check for updates", func(w *mgr.WorkerCtx) error { - binBundle, err := u.binUpdates.GetInstallBundle() + // initConfig() + + if applyUpdates { + err := u.registry.ApplyBinaryUpdates() if err != nil { - log.Warningf("failed to get binary bundle: %s", err) - } else { - err = binBundle.Verify(u.binUpdates.Directory) - if err != nil { - log.Warningf("binary bundle is not valid: %s", err) - } else { - log.Infof("binary bundle is valid") - } + log.Errorf("updates: failed to apply binary updates: %s", err) } - - intelBundle, err := u.intelUpdates.GetInstallBundle() + err = u.registry.ApplyIntelUpdates() if err != nil { - log.Warningf("failed to get intel bundle: %s", err) - } else { - err = intelBundle.Verify(u.intelUpdates.Directory) - if err != nil { - log.Warningf("intel bundle is not valid: %s", err) - } else { - log.Infof("intel bundle is valid") - } + log.Errorf("updates: failed to apply intel updates: %s", err) } - + u.instance.Restart() return nil - }) - u.updateWorkerMgr.Go() + } + + err := u.registry.Initialize() + if err != nil { + // TODO(vladimir): Find a better way to handle this error. The service will stop if parsing of the bundle files fails. + return fmt.Errorf("failed to initialize registry: %w", err) + } + + u.updateBinaryWorkerMgr.Go() + u.updateIntelWorkerMgr.Go() return nil } +func (u *Updates) GetFile(id string) (*registry.File, error) { + return u.registry.GetFile(id) +} + // Stop stops the module. func (u *Updates) Stop() error { return stop() diff --git a/service/updates/notify.go b/service/updates/notify.go index 076eea347..1a539f7a0 100644 --- a/service/updates/notify.go +++ b/service/updates/notify.go @@ -1,12 +1,8 @@ package updates import ( - "fmt" - "strings" "sync/atomic" "time" - - "github.com/safing/portmaster/base/notifications" ) const ( @@ -25,109 +21,109 @@ func (u *Updates) notificationsEnabled() bool { return u.instance.Notifications() != nil } -func notifyUpdateSuccess(force bool) { - if !module.notificationsEnabled() { - return - } - - updateFailedCnt.Store(0) - module.states.Clear() - updateState := registry.GetState().Updates - - flavor := updateSuccess - switch { - case len(updateState.PendingDownload) > 0: - // Show notification if there are pending downloads. - flavor = updateSuccessPending - case updateState.LastDownloadAt != nil && - time.Since(*updateState.LastDownloadAt) < 5*time.Second: - // Show notification if we downloaded something within the last minute. - flavor = updateSuccessDownloaded - case force: - // Always show notification if update was manually triggered. - default: - // Otherwise, the update was uneventful. Do not show notification. - return - } - - switch flavor { - case updateSuccess: - notifications.Notify(¬ifications.Notification{ - EventID: updateSuccess, - Type: notifications.Info, - Title: "Portmaster Is Up-To-Date", - Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(), - Expires: time.Now().Add(1 * time.Minute).Unix(), - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "OK", - }, - }, - }) - - case updateSuccessPending: - msg := fmt.Sprintf( - `%d updates are available for download: - -- %s - -Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`, - len(updateState.PendingDownload), - strings.Join(updateState.PendingDownload, "\n- "), - ) - - notifications.Notify(¬ifications.Notification{ - EventID: updateSuccess, - Type: notifications.Info, - Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)), - Message: msg, - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "OK", - }, - { - ID: "download", - Text: "Download Now", - Type: notifications.ActionTypeWebhook, - Payload: ¬ifications.ActionTypeWebhookPayload{ - URL: apiPathCheckForUpdates + "?download", - ResultAction: "display", - }, - }, - }, - }) - - case updateSuccessDownloaded: - msg := fmt.Sprintf( - `%d updates were downloaded and applied: - -- %s - -%s -`, - len(updateState.LastDownload), - strings.Join(updateState.LastDownload, "\n- "), - getUpdatingInfoMsg(), - ) - - notifications.Notify(¬ifications.Notification{ - EventID: updateSuccess, - Type: notifications.Info, - Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)), - Message: msg, - Expires: time.Now().Add(1 * time.Minute).Unix(), - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "OK", - }, - }, - }) - - } -} +// func notifyUpdateSuccess(force bool) { +// if !module.notificationsEnabled() { +// return +// } + +// updateFailedCnt.Store(0) +// module.states.Clear() +// updateState := registry.GetState().Updates + +// flavor := updateSuccess +// switch { +// case len(updateState.PendingDownload) > 0: +// // Show notification if there are pending downloads. +// flavor = updateSuccessPending +// case updateState.LastDownloadAt != nil && +// time.Since(*updateState.LastDownloadAt) < 5*time.Second: +// // Show notification if we downloaded something within the last minute. +// flavor = updateSuccessDownloaded +// case force: +// // Always show notification if update was manually triggered. +// default: +// // Otherwise, the update was uneventful. Do not show notification. +// return +// } + +// switch flavor { +// case updateSuccess: +// notifications.Notify(¬ifications.Notification{ +// EventID: updateSuccess, +// Type: notifications.Info, +// Title: "Portmaster Is Up-To-Date", +// Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(), +// Expires: time.Now().Add(1 * time.Minute).Unix(), +// AvailableActions: []*notifications.Action{ +// { +// ID: "ack", +// Text: "OK", +// }, +// }, +// }) + +// case updateSuccessPending: +// msg := fmt.Sprintf( +// `%d updates are available for download: + +// - %s + +// Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`, +// len(updateState.PendingDownload), +// strings.Join(updateState.PendingDownload, "\n- "), +// ) + +// notifications.Notify(¬ifications.Notification{ +// EventID: updateSuccess, +// Type: notifications.Info, +// Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)), +// Message: msg, +// AvailableActions: []*notifications.Action{ +// { +// ID: "ack", +// Text: "OK", +// }, +// { +// ID: "download", +// Text: "Download Now", +// Type: notifications.ActionTypeWebhook, +// Payload: ¬ifications.ActionTypeWebhookPayload{ +// URL: apiPathCheckForUpdates + "?download", +// ResultAction: "display", +// }, +// }, +// }, +// }) + +// case updateSuccessDownloaded: +// msg := fmt.Sprintf( +// `%d updates were downloaded and applied: + +// - %s + +// %s +// `, +// len(updateState.LastDownload), +// strings.Join(updateState.LastDownload, "\n- "), +// getUpdatingInfoMsg(), +// ) + +// notifications.Notify(¬ifications.Notification{ +// EventID: updateSuccess, +// Type: notifications.Info, +// Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)), +// Message: msg, +// Expires: time.Now().Add(1 * time.Minute).Unix(), +// AvailableActions: []*notifications.Action{ +// { +// ID: "ack", +// Text: "OK", +// }, +// }, +// }) + +// } +// } func getUpdatingInfoMsg() string { switch { @@ -140,41 +136,41 @@ func getUpdatingInfoMsg() string { } } -func notifyUpdateCheckFailed(force bool, err error) { - if !module.notificationsEnabled() { - return - } - - failedCnt := updateFailedCnt.Add(1) - lastSuccess := registry.GetState().Updates.LastSuccessAt - - switch { - case force: - // Always show notification if update was manually triggered. - case failedCnt < failedUpdateNotifyCountThreshold: - // Not failed often enough for notification. - return - case lastSuccess == nil: - // No recorded successful update. - case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess): - // Failed too recently for notification. - return - } - - notifications.NotifyWarn( - updateFailed, - "Update Check Failed", - fmt.Sprintf( - "Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s", - err, - ), - notifications.Action{ - Text: "Try Again Now", - Type: notifications.ActionTypeWebhook, - Payload: ¬ifications.ActionTypeWebhookPayload{ - URL: apiPathCheckForUpdates, - ResultAction: "display", - }, - }, - ).SyncWithState(module.states) -} +// func notifyUpdateCheckFailed(force bool, err error) { +// if !module.notificationsEnabled() { +// return +// } + +// failedCnt := updateFailedCnt.Add(1) +// lastSuccess := registry.GetState().Updates.LastSuccessAt + +// switch { +// case force: +// // Always show notification if update was manually triggered. +// case failedCnt < failedUpdateNotifyCountThreshold: +// // Not failed often enough for notification. +// return +// case lastSuccess == nil: +// // No recorded successful update. +// case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess): +// // Failed too recently for notification. +// return +// } + +// notifications.NotifyWarn( +// updateFailed, +// "Update Check Failed", +// fmt.Sprintf( +// "Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s", +// err, +// ), +// notifications.Action{ +// Text: "Try Again Now", +// Type: notifications.ActionTypeWebhook, +// Payload: ¬ifications.ActionTypeWebhookPayload{ +// URL: apiPathCheckForUpdates, +// ResultAction: "display", +// }, +// }, +// ).SyncWithState(module.states) +// } diff --git a/service/updates/os_integration_linux.go b/service/updates/os_integration_linux.go index cef0b9ef1..cd1b6137e 100644 --- a/service/updates/os_integration_linux.go +++ b/service/updates/os_integration_linux.go @@ -1,204 +1,201 @@ package updates -import ( - "bytes" - "crypto/sha256" - _ "embed" - "encoding/hex" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - - "github.com/tevino/abool" - "golang.org/x/exp/slices" - - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils/renameio" -) - -var ( - portmasterCoreServiceFilePath = "portmaster.service" - portmasterNotifierServiceFilePath = "portmaster_notifier.desktop" - backupExtension = ".backup" - - //go:embed assets/portmaster.service - currentPortmasterCoreServiceFile []byte - - checkedSystemIntegration = abool.New() - - // ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade. - ErrRequiresManualUpgrade = errors.New("requires a manual upgrade") -) - -func upgradeSystemIntegration() { - // Check if we already checked the system integration. - if !checkedSystemIntegration.SetToIf(false, true) { - return - } - - // Upgrade portmaster core systemd service. - err := upgradeSystemIntegrationFile( - "portmaster core systemd service", - filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath), - 0o0600, - currentPortmasterCoreServiceFile, - []string{ - "bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25 - "cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24 - "d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07 - }, - ) - if err != nil { - log.Warningf("updates: %s", err) - return - } - - // Upgrade portmaster notifier systemd user service. - // Permissions only! - err = upgradeSystemIntegrationFile( - "portmaster notifier systemd user service", - filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath), - 0o0644, - nil, // Do not update contents. - nil, // Do not update contents. - ) - if err != nil { - log.Warningf("updates: %s", err) - return - } -} - -// upgradeSystemIntegrationFile upgrades the file contents and permissions. -// System integration files are not necessarily present and may also be -// edited by third parties, such as the OS itself or other installers. -// The supplied hashes must be sha256 hex-encoded. -func upgradeSystemIntegrationFile( - name string, - filePath string, - fileMode fs.FileMode, - fileData []byte, - permittedUpgradeHashes []string, -) error { - // Upgrade file contents. - if len(fileData) > 0 { - if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil { - return err - } - } - - // Upgrade file permissions. - if fileMode != 0 { - if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil { - return err - } - } - - return nil -} - -// upgradeSystemIntegrationFileContents upgrades the file contents. -// System integration files are not necessarily present and may also be -// edited by third parties, such as the OS itself or other installers. -// The supplied hashes must be sha256 hex-encoded. -func upgradeSystemIntegrationFileContents( - name string, - filePath string, - fileData []byte, - permittedUpgradeHashes []string, -) error { - // Read existing file. - existingFileData, err := os.ReadFile(filePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err) - } - - // Check if file is already the current version. - existingSum := sha256.Sum256(existingFileData) - existingHexSum := hex.EncodeToString(existingSum[:]) - currentSum := sha256.Sum256(fileData) - currentHexSum := hex.EncodeToString(currentSum[:]) - if existingHexSum == currentHexSum { - log.Debugf("updates: %s at %s is up to date", name, filePath) - return nil - } - - // Check if we are allowed to upgrade from the existing file. - if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) { - return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade) - } - - // Start with upgrade! - - // Make backup of existing file. - err = CopyFile(filePath, filePath+backupExtension) - if err != nil { - return fmt.Errorf( - "failed to create backup of %s from %s to %s: %w", - name, - filePath, - filePath+backupExtension, - err, - ) - } - - // Open destination file for writing. - atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath) - if err != nil { - return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err) - } - defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - - // Write file. - _, err = io.Copy(atomicDstFile, bytes.NewReader(fileData)) - if err != nil { - return err - } - - // Finalize file. - err = atomicDstFile.CloseAtomicallyReplace() - if err != nil { - return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err) - } - - log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum) - return nil -} - -// upgradeSystemIntegrationFilePermissions upgrades the file permissions. -// System integration files are not necessarily present and may also be -// edited by third parties, such as the OS itself or other installers. -func upgradeSystemIntegrationFilePermissions( - name string, - filePath string, - fileMode fs.FileMode, -) error { - // Get current file permissions. - stat, err := os.Stat(filePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err) - } - - // If permissions are as expected, do nothing. - if stat.Mode().Perm() == fileMode { - return nil - } - - // Otherwise, set correct permissions. - err = os.Chmod(filePath, fileMode) - if err != nil { - return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err) - } - - log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode) - return nil -} +// import ( +// "crypto/sha256" +// _ "embed" +// "encoding/hex" +// "errors" +// "fmt" +// "io/fs" +// "os" +// "path/filepath" + +// "github.com/tevino/abool" +// "golang.org/x/exp/slices" + +// "github.com/safing/portmaster/base/dataroot" +// "github.com/safing/portmaster/base/log" +// ) + +// var ( +// portmasterCoreServiceFilePath = "portmaster.service" +// portmasterNotifierServiceFilePath = "portmaster_notifier.desktop" +// backupExtension = ".backup" + +// //go:embed assets/portmaster.service +// currentPortmasterCoreServiceFile []byte + +// checkedSystemIntegration = abool.New() + +// // ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade. +// ErrRequiresManualUpgrade = errors.New("requires a manual upgrade") +// ) + +// func upgradeSystemIntegration() { +// // Check if we already checked the system integration. +// if !checkedSystemIntegration.SetToIf(false, true) { +// return +// } + +// // Upgrade portmaster core systemd service. +// err := upgradeSystemIntegrationFile( +// "portmaster core systemd service", +// filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath), +// 0o0600, +// currentPortmasterCoreServiceFile, +// []string{ +// "bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25 +// "cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24 +// "d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07 +// }, +// ) +// if err != nil { +// log.Warningf("updates: %s", err) +// return +// } + +// // Upgrade portmaster notifier systemd user service. +// // Permissions only! +// err = upgradeSystemIntegrationFile( +// "portmaster notifier systemd user service", +// filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath), +// 0o0644, +// nil, // Do not update contents. +// nil, // Do not update contents. +// ) +// if err != nil { +// log.Warningf("updates: %s", err) +// return +// } +// } + +// // upgradeSystemIntegrationFile upgrades the file contents and permissions. +// // System integration files are not necessarily present and may also be +// // edited by third parties, such as the OS itself or other installers. +// // The supplied hashes must be sha256 hex-encoded. +// func upgradeSystemIntegrationFile( +// name string, +// filePath string, +// fileMode fs.FileMode, +// fileData []byte, +// permittedUpgradeHashes []string, +// ) error { +// // Upgrade file contents. +// if len(fileData) > 0 { +// if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil { +// return err +// } +// } + +// // Upgrade file permissions. +// if fileMode != 0 { +// if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil { +// return err +// } +// } + +// return nil +// } + +// // upgradeSystemIntegrationFileContents upgrades the file contents. +// // System integration files are not necessarily present and may also be +// // edited by third parties, such as the OS itself or other installers. +// // The supplied hashes must be sha256 hex-encoded. +// func upgradeSystemIntegrationFileContents( +// name string, +// filePath string, +// fileData []byte, +// permittedUpgradeHashes []string, +// ) error { +// // Read existing file. +// existingFileData, err := os.ReadFile(filePath) +// if err != nil { +// if errors.Is(err, os.ErrNotExist) { +// return nil +// } +// return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err) +// } + +// // Check if file is already the current version. +// existingSum := sha256.Sum256(existingFileData) +// existingHexSum := hex.EncodeToString(existingSum[:]) +// currentSum := sha256.Sum256(fileData) +// currentHexSum := hex.EncodeToString(currentSum[:]) +// if existingHexSum == currentHexSum { +// log.Debugf("updates: %s at %s is up to date", name, filePath) +// return nil +// } + +// // Check if we are allowed to upgrade from the existing file. +// if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) { +// return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade) +// } + +// // Start with upgrade! + +// // Make backup of existing file. +// err = CopyFile(filePath, filePath+backupExtension) +// if err != nil { +// return fmt.Errorf( +// "failed to create backup of %s from %s to %s: %w", +// name, +// filePath, +// filePath+backupExtension, +// err, +// ) +// } + +// // Open destination file for writing. +// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath) +// // if err != nil { +// // return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err) +// // } +// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway + +// // // Write file. +// // _, err = io.Copy(atomicDstFile, bytes.NewReader(fileData)) +// // if err != nil { +// // return err +// // } + +// // // Finalize file. +// // err = atomicDstFile.CloseAtomicallyReplace() +// // if err != nil { +// // return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err) +// // } + +// log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum) +// return nil +// } + +// // upgradeSystemIntegrationFilePermissions upgrades the file permissions. +// // System integration files are not necessarily present and may also be +// // edited by third parties, such as the OS itself or other installers. +// func upgradeSystemIntegrationFilePermissions( +// name string, +// filePath string, +// fileMode fs.FileMode, +// ) error { +// // Get current file permissions. +// stat, err := os.Stat(filePath) +// if err != nil { +// if errors.Is(err, os.ErrNotExist) { +// return nil +// } +// return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err) +// } + +// // If permissions are as expected, do nothing. +// if stat.Mode().Perm() == fileMode { +// return nil +// } + +// // Otherwise, set correct permissions. +// err = os.Chmod(filePath, fileMode) +// if err != nil { +// return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err) +// } + +// log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode) +// return nil +// } diff --git a/service/updates/registry.go b/service/updates/registry.go deleted file mode 100644 index de15a98b4..000000000 --- a/service/updates/registry.go +++ /dev/null @@ -1 +0,0 @@ -package updates diff --git a/service/updates/bundle.go b/service/updates/registry/bundle.go similarity index 86% rename from service/updates/bundle.go rename to service/updates/registry/bundle.go index 5214db1b4..3034438a9 100644 --- a/service/updates/bundle.go +++ b/service/updates/registry/bundle.go @@ -1,4 +1,4 @@ -package updates +package registry import ( "archive/zip" @@ -17,6 +17,12 @@ import ( "github.com/safing/portmaster/base/log" ) +const ( + defaultFileMode = os.FileMode(0o0644) + executableFileMode = os.FileMode(0o0744) + defaultDirMode = os.FileMode(0o0755) +) + const MaxUnpackSize = 1 << 30 // 2^30 == 1GB type Artifact struct { @@ -29,40 +35,40 @@ type Artifact struct { } type Bundle struct { + dir string Name string `json:"Bundle"` Version string `json:"Version"` Published time.Time `json:"Published"` Artifacts []Artifact `json:"Artifacts"` } -func (bundle Bundle) downloadAndVerify(dataDir string) { +func (bundle Bundle) downloadAndVerify() { client := http.Client{} for _, artifact := range bundle.Artifacts { - filePath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + filePath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) // TODO(vladimir): is this needed? - _ = os.MkdirAll(filepath.Dir(filePath), os.ModePerm) + _ = os.MkdirAll(filepath.Dir(filePath), defaultDirMode) // Check file is already downloaded and valid. - exists, err := checkIfFileIsValid(filePath, artifact) + exists, _ := checkIfFileIsValid(filePath, artifact) if exists { - log.Debugf("file already download: %s", filePath) + log.Debugf("updates: file already downloaded: %s", filePath) continue - } else if err != nil { - log.Errorf("error while checking old download: %s", err) } // Download artifact - err = processArtifact(&client, artifact, filePath) + err := processArtifact(&client, artifact, filePath) if err != nil { log.Errorf("updates: %s", err) } } } -func (bundle Bundle) Verify(dataDir string) error { +// Verify checks if the files are present int the dataDir and have the correct hash. +func (bundle Bundle) Verify() error { for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) file, err := os.Open(artifactPath) if err != nil { return fmt.Errorf("failed to open file %s: %w", artifactPath, err) @@ -86,8 +92,7 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { // Check if file already exists file, err := os.Open(filename) if err != nil { - //nolint:nilerr - return false, nil + return false, err } defer func() { _ = file.Close() }() @@ -131,7 +136,7 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er // Verify hash := sha256.Sum256(content) if !bytes.Equal(providedHash, hash[:]) { - // FIXME(vladimir): just for testing. Make it an error before commit. + // FIXME(vladimir): just for testing. Make it an error. err = fmt.Errorf("failed to verify artifact: %s", artifact.Filename) log.Debugf("updates: %s", err) } @@ -142,6 +147,11 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er if err != nil { return fmt.Errorf("failed to create file: %w", err) } + if artifact.Platform == "" { + _ = file.Chmod(defaultFileMode) + } else { + _ = file.Chmod(executableFileMode) + } _, err = file.Write(content) if err != nil { return fmt.Errorf("failed to write to file: %w", err) diff --git a/service/updates/registry/index.go b/service/updates/registry/index.go new file mode 100644 index 000000000..f5900b3fd --- /dev/null +++ b/service/updates/registry/index.go @@ -0,0 +1,56 @@ +package registry + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/safing/portmaster/base/log" +) + +type UpdateIndex struct { + Directory string + DownloadDirectory string + Ignore []string + IndexURLs []string + IndexFile string + AutoApply bool +} + +func (ui *UpdateIndex) downloadIndexFile() (err error) { + _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) + for _, url := range ui.IndexURLs { + err = ui.downloadIndexFileFromURL(url) + if err != nil { + log.Warningf("updates: %s", err) + continue + } + // Downloading was successful. + err = nil + break + } + return +} + +func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { + client := http.Client{} + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("failed a get request to %s: %w", url, err) + } + defer func() { _ = resp.Body.Close() }() + filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) + file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + + return nil +} diff --git a/service/updates/registry/registry.go b/service/updates/registry/registry.go new file mode 100644 index 000000000..223b90dec --- /dev/null +++ b/service/updates/registry/registry.go @@ -0,0 +1,245 @@ +package registry + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/safing/portmaster/base/log" +) + +var ErrNotFound error = errors.New("file not found") + +type File struct { + id string + path string +} + +func (f *File) Identifier() string { + return f.id +} + +func (f *File) Path() string { + return f.path +} + +func (f *File) Version() string { + return "" +} + +type Registry struct { + binaryUpdateIndex UpdateIndex + intelUpdateIndex UpdateIndex + + binaryBundle *Bundle + intelBundle *Bundle + + binaryUpdateBundle *Bundle + intelUpdateBundle *Bundle + + files map[string]File +} + +// New create new Registry. +func New(binIndex UpdateIndex, intelIndex UpdateIndex) Registry { + return Registry{ + binaryUpdateIndex: binIndex, + intelUpdateIndex: intelIndex, + files: make(map[string]File), + } +} + +// Initialize parses and initializes currently installed bundles. +func (reg *Registry) Initialize() error { + var err error + + // Parse current installed binary bundle. + reg.binaryBundle, err = parseBundle(reg.binaryUpdateIndex.Directory, reg.binaryUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse binary bundle: %w", err) + } + // Parse current installed intel bundle. + reg.intelBundle, err = parseBundle(reg.intelUpdateIndex.Directory, reg.intelUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse intel bundle: %w", err) + } + + // Add bundle artifacts to registry. + reg.processBundle(reg.binaryBundle) + reg.processBundle(reg.intelBundle) + + return nil +} + +func (reg *Registry) processBundle(bundle *Bundle) { + for _, artifact := range bundle.Artifacts { + artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath} + } +} + +// GetFile returns the object of a artifact by id. +func (reg *Registry) GetFile(id string) (*File, error) { + file, ok := reg.files[id] + if ok { + return &file, nil + } else { + log.Errorf("updates: requested file id not found: %s", id) + return nil, ErrNotFound + } +} + +// CheckForBinaryUpdates checks if there is a new binary bundle updates. +func (reg *Registry) CheckForBinaryUpdates() (bool, error) { + err := reg.binaryUpdateIndex.downloadIndexFile() + if err != nil { + return false, err + } + + reg.binaryUpdateBundle, err = parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) + if err != nil { + return false, fmt.Errorf("failed to parse bundle file: %w", err) + } + + // TODO(vladimir): Make a better check. + if reg.binaryBundle.Version != reg.binaryUpdateBundle.Version { + return true, nil + } + + return false, nil +} + +// DownloadBinaryUpdates downloads available binary updates. +func (reg *Registry) DownloadBinaryUpdates() error { + if reg.binaryUpdateBundle == nil { + // CheckForBinaryUpdates needs to be called before this. + return fmt.Errorf("no valid update bundle found") + } + _ = deleteUnfinishedDownloads(reg.binaryBundle.dir) + reg.binaryUpdateBundle.downloadAndVerify() + return nil +} + +// CheckForIntelUpdates checks if there is a new intel data bundle updates. +func (reg *Registry) CheckForIntelUpdates() (bool, error) { + err := reg.intelUpdateIndex.downloadIndexFile() + if err != nil { + return false, err + } + + reg.intelUpdateBundle, err = parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) + if err != nil { + return false, fmt.Errorf("failed to parse bundle file: %w", err) + } + + // TODO(vladimir): Make a better check. + if reg.intelBundle.Version != reg.intelUpdateBundle.Version { + return true, nil + } + + return false, nil +} + +// DownloadIntelUpdates downloads available intel data updates. +func (reg *Registry) DownloadIntelUpdates() error { + if reg.intelUpdateBundle == nil { + // CheckForIntelUpdates needs to be called before this. + return fmt.Errorf("no valid update bundle found") + } + _ = deleteUnfinishedDownloads(reg.intelBundle.dir) + reg.intelUpdateBundle.downloadAndVerify() + return nil +} + +// ApplyBinaryUpdates removes the current binary folder and replaces it with the downloaded one. +func (reg *Registry) ApplyBinaryUpdates() error { + bundle, err := parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse index file: %w", err) + } + err = bundle.Verify() + if err != nil { + return fmt.Errorf("binary bundle is not valid: %w", err) + } + + err = os.RemoveAll(reg.binaryUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to remove dir: %w", err) + } + err = os.Rename(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to move dir: %w", err) + } + return nil +} + +// ApplyIntelUpdates removes the current intel folder and replaces it with the downloaded one. +func (reg *Registry) ApplyIntelUpdates() error { + bundle, err := parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse index file: %w", err) + } + err = bundle.Verify() + if err != nil { + return fmt.Errorf("binary bundle is not valid: %w", err) + } + + err = os.RemoveAll(reg.intelUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to remove dir: %w", err) + } + err = os.Rename(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to move dir: %w", err) + } + return nil +} + +func parseBundle(dir string, indexFile string) (*Bundle, error) { + filepath := fmt.Sprintf("%s/%s", dir, indexFile) + // Check if the file exists. + file, err := os.Open(filepath) + if err != nil { + return nil, fmt.Errorf("failed to open index file: %w", err) + } + defer func() { _ = file.Close() }() + + // Read + content, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + // Parse + var bundle Bundle + err = json.Unmarshal(content, &bundle) + if err != nil { + return nil, err + } + bundle.dir = dir + + return &bundle, nil +} + +func deleteUnfinishedDownloads(rootDir string) error { + return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Check if the current file has the specified extension + if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { + log.Warningf("updates deleting unfinished: %s\n", path) + err := os.Remove(path) + if err != nil { + return fmt.Errorf("failed to delete file %s: %w", path, err) + } + } + + return nil + }) +} diff --git a/service/updates/restart.go b/service/updates/restart.go index 729853ff1..30fc9289f 100644 --- a/service/updates/restart.go +++ b/service/updates/restart.go @@ -54,7 +54,7 @@ func DelayedRestart(delay time.Duration) { // Schedule the restart task. log.Warningf("updates: restart triggered, will execute in %s", delay) restartAt := time.Now().Add(delay) - module.restartWorkerMgr.Delay(delay) + // module.restartWorkerMgr.Delay(delay) // Set restartTime. restartTimeLock.Lock() @@ -68,23 +68,23 @@ func AbortRestart() { log.Warningf("updates: restart aborted") // Cancel schedule. - module.restartWorkerMgr.Delay(0) + // module.restartWorkerMgr.Delay(0) } } // TriggerRestartIfPending triggers an automatic restart, if one is pending. // This can be used to prepone a scheduled restart if the conditions are preferable. func TriggerRestartIfPending() { - if restartPending.IsSet() { - module.restartWorkerMgr.Go() - } + // if restartPending.IsSet() { + // module.restartWorkerMgr.Go() + // } } // RestartNow immediately executes a restart. // This only works if the process is managed by portmaster-start. func RestartNow() { restartPending.Set() - module.restartWorkerMgr.Go() + // module.restartWorkerMgr.Go() } func automaticRestart(w *mgr.WorkerCtx) error { @@ -108,11 +108,11 @@ func automaticRestart(w *mgr.WorkerCtx) error { } // Set restart exit code. - if !rebooting { - module.instance.Restart() - } else { - module.instance.Shutdown() - } + // if !rebooting { + // module.instance.Restart() + // } else { + // module.instance.Shutdown() + // } } return nil diff --git a/service/updates/state.go b/service/updates/state.go index 3a1144b12..ef1043086 100644 --- a/service/updates/state.go +++ b/service/updates/state.go @@ -1,49 +1,49 @@ package updates -import ( - "github.com/safing/portmaster/base/database/record" - "github.com/safing/portmaster/base/runtime" - "github.com/safing/portmaster/base/updater" -) - -var pushRegistryStatusUpdate runtime.PushFunc - -// RegistryStateExport is a wrapper to export the registry state. -type RegistryStateExport struct { - record.Base - *updater.RegistryState -} - -func exportRegistryState(s *updater.RegistryState) *RegistryStateExport { - if s == nil { - state := registry.GetState() - s = &state - } - - export := &RegistryStateExport{ - RegistryState: s, - } - - export.CreateMeta() - export.SetKey("runtime:core/updates/state") - - return export -} - -func pushRegistryState(s *updater.RegistryState) { - export := exportRegistryState(s) - pushRegistryStatusUpdate(export) -} - -func registerRegistryStateProvider() (err error) { - registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { - return []record.Record{exportRegistryState(nil)}, nil - }) - - pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider) - if err != nil { - return err - } - - return nil -} +// import ( +// "github.com/safing/portmaster/base/database/record" +// "github.com/safing/portmaster/base/runtime" +// "github.com/safing/portmaster/base/updater" +// ) + +// var pushRegistryStatusUpdate runtime.PushFunc + +// // RegistryStateExport is a wrapper to export the registry state. +// type RegistryStateExport struct { +// record.Base +// *updater.RegistryState +// } + +// func exportRegistryState(s *updater.RegistryState) *RegistryStateExport { +// // if s == nil { +// // state := registry.GetState() +// // s = &state +// // } + +// export := &RegistryStateExport{ +// RegistryState: s, +// } + +// export.CreateMeta() +// export.SetKey("runtime:core/updates/state") + +// return export +// } + +// func pushRegistryState(s *updater.RegistryState) { +// export := exportRegistryState(s) +// pushRegistryStatusUpdate(export) +// } + +// func registerRegistryStateProvider() (err error) { +// registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { +// return []record.Record{exportRegistryState(nil)}, nil +// }) + +// pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider) +// if err != nil { +// return err +// } + +// return nil +// } diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go index e4685c3b8..334ebe889 100644 --- a/service/updates/upgrader.go +++ b/service/updates/upgrader.go @@ -1,406 +1,403 @@ package updates -import ( - "context" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "time" - - processInfo "github.com/shirou/gopsutil/process" - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/notifications" - "github.com/safing/portmaster/base/rng" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils/renameio" - "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" -) - -const ( - upgradedSuffix = "-upgraded" - exeExt = ".exe" -) - -var ( - upgraderActive = abool.NewBool(false) - - pmCtrlUpdate *updater.File - pmCoreUpdate *updater.File - - spnHubUpdate *updater.File - - rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`) -) - -func initUpgrader() error { - module.EventResourcesUpdated.AddCallback("run upgrades", upgrader) - return nil -} - -func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { - // Lock runs, but discard additional runs. - if !upgraderActive.SetToIf(false, true) { - return false, nil - } - defer upgraderActive.SetTo(false) - - // Upgrade portmaster-start. - err = upgradePortmasterStart() - if err != nil { - log.Warningf("updates: failed to upgrade portmaster-start: %s", err) - } - - // Upgrade based on binary. - binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0] - switch binBaseName { - case "portmaster-core": - // Notify about upgrade. - if err := upgradeCoreNotify(); err != nil { - log.Warningf("updates: failed to notify about core upgrade: %s", err) - } - - // Fix chrome sandbox permissions. - if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { - log.Warningf("updates: failed to handle electron upgrade: %s", err) - } - - // Upgrade system integration. - upgradeSystemIntegration() - - case "spn-hub": - // Trigger upgrade procedure. - if err := upgradeHub(); err != nil { - log.Warningf("updates: failed to initiate hub upgrade: %s", err) - } - } - - return false, nil -} - -func upgradeCoreNotify() error { - if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() { - return nil - } - - // make identifier - identifier := "core/portmaster-core" // identifier, use forward slash! - if onWindows { - identifier += exeExt - } - - // get newest portmaster-core - newFile, err := GetPlatformFile(identifier) - if err != nil { - return err - } - pmCoreUpdate = newFile - - // check for new version - if info.VersionNumber() != pmCoreUpdate.Version() { - n := notifications.Notify(¬ifications.Notification{ - EventID: "updates:core-update-available", - Type: notifications.Info, - Title: fmt.Sprintf( - "Portmaster Update v%s Is Ready!", - pmCoreUpdate.Version(), - ), - Category: "Core", - Message: fmt.Sprintf( - `A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`, - pmCoreUpdate.Version(), - ), - ShowOnSystem: true, - AvailableActions: []*notifications.Action{ - // TODO: Use special UI action in order to reload UI on restart. - { - ID: "restart", - Text: "Restart", - }, - { - ID: "later", - Text: "Not now", - }, - }, - }) - n.SetActionFunction(upgradeCoreNotifyActionHandler) - - log.Debugf("updates: new portmaster version available, sending notification to user") - } - - return nil -} - -func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error { - switch n.SelectedActionID { - case "restart": - log.Infof("updates: user triggered restart via core update notification") - RestartNow() - case "later": - n.Delete() - } - - return nil -} - -func upgradeHub() error { - if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() { - return nil - } - - // Make identifier for getting file from updater. - identifier := "hub/spn-hub" // identifier, use forward slash! - if onWindows { - identifier += exeExt - } - - // Get newest spn-hub file. - newFile, err := GetPlatformFile(identifier) - if err != nil { - return err - } - spnHubUpdate = newFile - - // Check if the new version is different. - if info.GetInfo().Version != spnHubUpdate.Version() { - // Get random delay with up to three hours. - delayMinutes, err := rng.Number(3 * 60) - if err != nil { - return err - } - - // Delay restart for at least one hour for preparations. - DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) - - // Increase update checks in order to detect aborts better. - // if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(10 * time.Minute) - // } - } else { - AbortRestart() - - // Set update task schedule back to normal. - // if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(updateTaskRepeatDuration) - // } - } - - return nil -} - -func upgradePortmasterStart() error { - filename := "portmaster-start" - if onWindows { - filename += exeExt - } - - // check if we can upgrade - if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() { - // get newest portmaster-start - newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash! - if err != nil { - return err - } - pmCtrlUpdate = newFile - } else { - return nil - } - - // update portmaster-start in data root - rootPmStartPath := filepath.Join(dataroot.Root().Path, filename) - err := upgradeBinary(rootPmStartPath, pmCtrlUpdate) - if err != nil { - return err - } - - return nil -} - -func warnOnIncorrectParentPath() { - expectedFileName := "portmaster-start" - if onWindows { - expectedFileName += exeExt - } - - // upgrade parent process, if it's portmaster-start - parent, err := processInfo.NewProcess(int32(os.Getppid())) - if err != nil { - log.Tracef("could not get parent process: %s", err) - return - } - parentName, err := parent.Name() - if err != nil { - log.Tracef("could not get parent process name: %s", err) - return - } - if parentName != expectedFileName { - // Only warn about this if not in dev mode. - if !devMode() { - log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName) - } - - // TODO(ppacher): once we released a new installer and folks had time - // to update we should send a module warning/hint to the - // UI notifying the user that he's still using portmaster-control. - return - } - - parentPath, err := parent.Exe() - if err != nil { - log.Tracef("could not get parent process path: %s", err) - return - } - - absPath, err := filepath.Abs(parentPath) - if err != nil { - log.Tracef("could not get absolut parent process path: %s", err) - return - } - - root := filepath.Dir(registry.StorageDir().Path) - if !strings.HasPrefix(absPath, root) { - log.Warningf("detected unexpected path %s for portmaster-start", absPath) - notifications.NotifyWarn( - "updates:unsupported-parent", - "Unsupported Launcher", - fmt.Sprintf( - "The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", - expectedFileName, - absPath, - filepath.Join(root, expectedFileName), - ), - ) - } -} - -func upgradeBinary(fileToUpgrade string, file *updater.File) error { - fileExists := false - _, err := os.Stat(fileToUpgrade) - if err == nil { - // file exists and is accessible - fileExists = true - } - - if fileExists { - // get current version - var currentVersion string - cmd := exec.Command(fileToUpgrade, "version", "--short") - out, err := cmd.Output() - if err == nil { - // abort if version matches - currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") - if currentVersion == file.Version() { - log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version()) - // already up to date! - return nil - } - } else { - log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) - currentVersion = "0.0.0" - } - - // test currentVersion for sanity - if !rawVersionRegex.MatchString(currentVersion) { - log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) - } - - // try removing old version - err = os.Remove(fileToUpgrade) - if err != nil { - // ensure tmp dir is here - err = registry.TmpDir().Ensure() - if err != nil { - return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err) - } - - // maybe we're on windows and it's in use, try moving - err = os.Rename(fileToUpgrade, filepath.Join( - registry.TmpDir().Path, - fmt.Sprintf( - "%s-%d%s", - filepath.Base(fileToUpgrade), - time.Now().UTC().Unix(), - upgradedSuffix, - ), - )) - if err != nil { - return fmt.Errorf("unable to move file that needs upgrade: %w", err) - } - } - } - - // copy upgrade - err = CopyFile(file.Path(), fileToUpgrade) - if err != nil { - // try again - time.Sleep(1 * time.Second) - err = CopyFile(file.Path(), fileToUpgrade) - if err != nil { - return err - } - } - - // check permissions - if !onWindows { - info, err := os.Stat(fileToUpgrade) - if err != nil { - return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err) - } - if info.Mode() != 0o0755 { - err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions. - if err != nil { - return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err) - } - } - } - - log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version()) - return nil -} - -// CopyFile atomically copies a file using the update registry's tmp dir. -func CopyFile(srcPath, dstPath string) error { - // check tmp dir - err := registry.TmpDir().Ensure() - if err != nil { - return fmt.Errorf("could not prepare tmp directory for copying file: %w", err) - } - - // open file for writing - atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath) - if err != nil { - return fmt.Errorf("could not create temp file for atomic copy: %w", err) - } - defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - - // open source - srcFile, err := os.Open(srcPath) - if err != nil { - return err - } - defer func() { - _ = srcFile.Close() - }() - - // copy data - _, err = io.Copy(atomicDstFile, srcFile) - if err != nil { - return err - } - - // finalize file - err = atomicDstFile.CloseAtomicallyReplace() - if err != nil { - return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err) - } - - return nil -} +// import ( +// "context" +// "fmt" +// "os" +// "os/exec" +// "path/filepath" +// "regexp" +// "strings" +// "time" + +// processInfo "github.com/shirou/gopsutil/process" +// "github.com/tevino/abool" + +// "github.com/safing/portmaster/base/dataroot" +// "github.com/safing/portmaster/base/info" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/notifications" +// "github.com/safing/portmaster/base/rng" +// "github.com/safing/portmaster/base/updater" +// "github.com/safing/portmaster/service/mgr" +// ) + +// const ( +// upgradedSuffix = "-upgraded" +// exeExt = ".exe" +// ) + +// var ( +// upgraderActive = abool.NewBool(false) + +// pmCtrlUpdate *updater.File +// pmCoreUpdate *updater.File + +// spnHubUpdate *updater.File + +// rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`) +// ) + +// func initUpgrader() error { +// // module.EventResourcesUpdated.AddCallback("run upgrades", upgrader) +// return nil +// } + +// func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { +// // Lock runs, but discard additional runs. +// if !upgraderActive.SetToIf(false, true) { +// return false, nil +// } +// defer upgraderActive.SetTo(false) + +// // Upgrade portmaster-start. +// err = upgradePortmasterStart() +// if err != nil { +// log.Warningf("updates: failed to upgrade portmaster-start: %s", err) +// } + +// // Upgrade based on binary. +// binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0] +// switch binBaseName { +// case "portmaster-core": +// // Notify about upgrade. +// if err := upgradeCoreNotify(); err != nil { +// log.Warningf("updates: failed to notify about core upgrade: %s", err) +// } + +// // Fix chrome sandbox permissions. +// // if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { +// // log.Warningf("updates: failed to handle electron upgrade: %s", err) +// // } + +// // Upgrade system integration. +// upgradeSystemIntegration() + +// case "spn-hub": +// // Trigger upgrade procedure. +// if err := upgradeHub(); err != nil { +// log.Warningf("updates: failed to initiate hub upgrade: %s", err) +// } +// } + +// return false, nil +// } + +// func upgradeCoreNotify() error { +// if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() { +// return nil +// } + +// // make identifier +// identifier := "core/portmaster-core" // identifier, use forward slash! +// if onWindows { +// identifier += exeExt +// } + +// // get newest portmaster-core +// // newFile, err := GetPlatformFile(identifier) +// // if err != nil { +// // return err +// // } +// // pmCoreUpdate = newFile + +// // check for new version +// if info.VersionNumber() != pmCoreUpdate.Version() { +// n := notifications.Notify(¬ifications.Notification{ +// EventID: "updates:core-update-available", +// Type: notifications.Info, +// Title: fmt.Sprintf( +// "Portmaster Update v%s Is Ready!", +// pmCoreUpdate.Version(), +// ), +// Category: "Core", +// Message: fmt.Sprintf( +// `A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`, +// pmCoreUpdate.Version(), +// ), +// ShowOnSystem: true, +// AvailableActions: []*notifications.Action{ +// // TODO: Use special UI action in order to reload UI on restart. +// { +// ID: "restart", +// Text: "Restart", +// }, +// { +// ID: "later", +// Text: "Not now", +// }, +// }, +// }) +// n.SetActionFunction(upgradeCoreNotifyActionHandler) + +// log.Debugf("updates: new portmaster version available, sending notification to user") +// } + +// return nil +// } + +// func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error { +// switch n.SelectedActionID { +// case "restart": +// log.Infof("updates: user triggered restart via core update notification") +// RestartNow() +// case "later": +// n.Delete() +// } + +// return nil +// } + +// func upgradeHub() error { +// if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() { +// return nil +// } + +// // Make identifier for getting file from updater. +// identifier := "hub/spn-hub" // identifier, use forward slash! +// if onWindows { +// identifier += exeExt +// } + +// // Get newest spn-hub file. +// // newFile, err := GetPlatformFile(identifier) +// // if err != nil { +// // return err +// // } +// // spnHubUpdate = newFile + +// // Check if the new version is different. +// if info.GetInfo().Version != spnHubUpdate.Version() { +// // Get random delay with up to three hours. +// delayMinutes, err := rng.Number(3 * 60) +// if err != nil { +// return err +// } + +// // Delay restart for at least one hour for preparations. +// DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) + +// // Increase update checks in order to detect aborts better. +// // if !disableTaskSchedule { +// // module.updateBinaryWorkerMgr.Repeat(10 * time.Minute) +// // } +// } else { +// AbortRestart() + +// // Set update task schedule back to normal. +// // if !disableTaskSchedule { +// // module.updateBinaryWorkerMgr.Repeat(updateTaskRepeatDuration) +// // } +// } + +// return nil +// } + +// func upgradePortmasterStart() error { +// filename := "portmaster-start" +// if onWindows { +// filename += exeExt +// } + +// // check if we can upgrade +// if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() { +// // get newest portmaster-start +// // newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash! +// // if err != nil { +// // return err +// // } +// // pmCtrlUpdate = newFile +// } else { +// return nil +// } + +// // update portmaster-start in data root +// rootPmStartPath := filepath.Join(dataroot.Root().Path, filename) +// err := upgradeBinary(rootPmStartPath, pmCtrlUpdate) +// if err != nil { +// return err +// } + +// return nil +// } + +// func warnOnIncorrectParentPath() { +// expectedFileName := "portmaster-start" +// if onWindows { +// expectedFileName += exeExt +// } + +// // upgrade parent process, if it's portmaster-start +// parent, err := processInfo.NewProcess(int32(os.Getppid())) +// if err != nil { +// log.Tracef("could not get parent process: %s", err) +// return +// } +// parentName, err := parent.Name() +// if err != nil { +// log.Tracef("could not get parent process name: %s", err) +// return +// } +// if parentName != expectedFileName { +// // Only warn about this if not in dev mode. +// if !devMode() { +// log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName) +// } + +// // TODO(ppacher): once we released a new installer and folks had time +// // to update we should send a module warning/hint to the +// // UI notifying the user that he's still using portmaster-control. +// return +// } + +// // parentPath, err := parent.Exe() +// // if err != nil { +// // log.Tracef("could not get parent process path: %s", err) +// // return +// // } + +// // absPath, err := filepath.Abs(parentPath) +// // if err != nil { +// // log.Tracef("could not get absolut parent process path: %s", err) +// // return +// // } + +// // root := filepath.Dir(registry.StorageDir().Path) +// // if !strings.HasPrefix(absPath, root) { +// // log.Warningf("detected unexpected path %s for portmaster-start", absPath) +// // notifications.NotifyWarn( +// // "updates:unsupported-parent", +// // "Unsupported Launcher", +// // fmt.Sprintf( +// // "The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", +// // expectedFileName, +// // absPath, +// // filepath.Join(root, expectedFileName), +// // ), +// // ) +// // } +// } + +// func upgradeBinary(fileToUpgrade string, file *updater.File) error { +// fileExists := false +// _, err := os.Stat(fileToUpgrade) +// if err == nil { +// // file exists and is accessible +// fileExists = true +// } + +// if fileExists { +// // get current version +// var currentVersion string +// cmd := exec.Command(fileToUpgrade, "version", "--short") +// out, err := cmd.Output() +// if err == nil { +// // abort if version matches +// currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") +// if currentVersion == file.Version() { +// log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version()) +// // already up to date! +// return nil +// } +// } else { +// log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) +// currentVersion = "0.0.0" +// } + +// // test currentVersion for sanity +// if !rawVersionRegex.MatchString(currentVersion) { +// log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) +// } + +// // try removing old version +// err = os.Remove(fileToUpgrade) +// if err != nil { +// // ensure tmp dir is here +// // err = registry.TmpDir().Ensure() +// // if err != nil { +// // return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err) +// // } + +// // maybe we're on windows and it's in use, try moving +// // err = os.Rename(fileToUpgrade, filepath.Join( +// // registry.TmpDir().Path, +// // fmt.Sprintf( +// // "%s-%d%s", +// // filepath.Base(fileToUpgrade), +// // time.Now().UTC().Unix(), +// // upgradedSuffix, +// // ), +// // )) +// // if err != nil { +// // return fmt.Errorf("unable to move file that needs upgrade: %w", err) +// // } +// } +// } + +// // copy upgrade +// err = CopyFile(file.Path(), fileToUpgrade) +// if err != nil { +// // try again +// time.Sleep(1 * time.Second) +// err = CopyFile(file.Path(), fileToUpgrade) +// if err != nil { +// return err +// } +// } + +// // check permissions +// if !onWindows { +// info, err := os.Stat(fileToUpgrade) +// if err != nil { +// return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err) +// } +// if info.Mode() != 0o0755 { +// err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions. +// if err != nil { +// return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err) +// } +// } +// } + +// log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version()) +// return nil +// } + +// // CopyFile atomically copies a file using the update registry's tmp dir. +// func CopyFile(srcPath, dstPath string) error { +// // check tmp dir +// // err := registry.TmpDir().Ensure() +// // if err != nil { +// // return fmt.Errorf("could not prepare tmp directory for copying file: %w", err) +// // } + +// // open file for writing +// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath) +// // if err != nil { +// // return fmt.Errorf("could not create temp file for atomic copy: %w", err) +// // } +// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway + +// // // open source +// // srcFile, err := os.Open(srcPath) +// // if err != nil { +// // return err +// // } +// // defer func() { +// // _ = srcFile.Close() +// // }() + +// // // copy data +// // _, err = io.Copy(atomicDstFile, srcFile) +// // if err != nil { +// // return err +// // } + +// // // finalize file +// // err = atomicDstFile.CloseAtomicallyReplace() +// // if err != nil { +// // return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err) +// // } + +// return nil +// } diff --git a/spn/captain/intel.go b/spn/captain/intel.go index c71ce663c..6411f4c6f 100644 --- a/spn/captain/intel.go +++ b/spn/captain/intel.go @@ -6,9 +6,8 @@ import ( "os" "sync" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" "github.com/safing/portmaster/spn/conf" "github.com/safing/portmaster/spn/hub" "github.com/safing/portmaster/spn/navigator" @@ -16,7 +15,7 @@ import ( ) var ( - intelResource *updater.File + intelResource *registry.File intelResourcePath = "intel/spn/main-intel.yaml" intelResourceMapName = "main" intelResourceUpdateLock sync.Mutex @@ -44,12 +43,13 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) { } // Check if there is something to do. - if intelResource != nil && !intelResource.UpgradeAvailable() { + // TODO(vladimir): is update check needed + if intelResource != nil { //&& !intelResource.UpgradeAvailable() { return nil } // Get intel file and load it from disk. - intelResource, err = updates.GetFile(intelResourcePath) + intelResource, err = module.instance.Updates().GetFile(intelResourcePath) if err != nil { return fmt.Errorf("failed to get SPN intel update: %w", err) } From f7abb700bf13e48652f2722fc7bb1d57b613b976 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 27 Aug 2024 14:30:04 +0300 Subject: [PATCH 07/11] [WIP] Fix SELinux permissions --- packaging/linux/portmaster.service | 5 +++-- packaging/linux/postinst | 11 +++++++++++ packaging/linux/postrm | 9 +++++++++ service/updates/module.go | 6 +++--- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service index 5490ac6f9..c16068d9a 100644 --- a/packaging/linux/portmaster.service +++ b/packaging/linux/portmaster.service @@ -34,8 +34,9 @@ AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_ne CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon StateDirectory=portmaster # TODO(ppacher): add --disable-software-updates once it's merged and the release process changed. -ExecStart=/usr/bin/portmaster-core --data /opt/safing/portmaster -- $PORTMASTER_ARGS -ExecStopPost=-/usr/bin/portmaster-core recover-iptables +WorkingDirectory=/var/lib/portmaster/data +ExecStart=/usr/lib/portmaster/portmaster-core --data /var/lib/portmaster/data -devmode -- $PORTMASTER_ARGS +ExecStopPost=-/usr/bin/portmaster/portmaster-core recover-iptables [Install] WantedBy=multi-user.target diff --git a/packaging/linux/postinst b/packaging/linux/postinst index 8f727403f..ab0567233 100644 --- a/packaging/linux/postinst +++ b/packaging/linux/postinst @@ -1,5 +1,16 @@ #!/bin/bash +chmod +x /usr/lib/portmaster/portmaster-core + +# +# Fix selinux permissions for portmaster-core if we have semanage +# available. +# +if command -V semanage >/dev/null 2>&1; then + semanage fcontext -a -t bin_t -s system_u $(realpath /usr/lib)'/portmaster/portmaster-core' || : + restorecon -R /usr/lib/portmaster/portmaster-core 2>/dev/null >&2 || : +fi + systemctl daemon-reload systemctl enable portmaster.service diff --git a/packaging/linux/postrm b/packaging/linux/postrm index a9bf588e2..9dddf8e94 100644 --- a/packaging/linux/postrm +++ b/packaging/linux/postrm @@ -1 +1,10 @@ #!/bin/bash + +# +# Remove selinux permissions for portmaster-core if we have semanage +# available. +# +if command -V semanage >/dev/null 2>&1; then + semanage fcontext --delete $(realpath /usr/lib)'/portmaster/portmaster-core' || : + restorecon -R /usr/lib/portmaster/portmaster-core 2>/dev/null >&2 || : +fi \ No newline at end of file diff --git a/service/updates/module.go b/service/updates/module.go index 989ad0208..31de626c8 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -63,7 +63,7 @@ func New(instance instance) (*Updates, error) { binIndex := registry.UpdateIndex{ Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/portmaster/new_bin", + DownloadDirectory: "/var/lib/portmaster/new_bin", Ignore: []string{"databases", "intel", "config.json"}, IndexURLs: []string{"http://localhost:8000/test-binary.json"}, IndexFile: "bin-index.json", @@ -71,8 +71,8 @@ func New(instance instance) (*Updates, error) { } intelIndex := registry.UpdateIndex{ - Directory: "/var/portmaster/intel", - DownloadDirectory: "/var/portmaster/new_intel", + Directory: "/var/lib/portmaster/intel", + DownloadDirectory: "/var/lib/portmaster/new_intel", IndexURLs: []string{"http://localhost:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, From 701505ae7535475cf9a84c7794e4200ae6fb7bb9 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 30 Aug 2024 12:40:51 +0300 Subject: [PATCH 08/11] [WIP] working download and replace. --- service/broadcasts/module.go | 2 +- service/broadcasts/notify.go | 2 +- service/instance.go | 77 +++++++++----- service/intel/filterlists/index.go | 2 +- service/intel/filterlists/module.go | 5 +- service/intel/geoip/database.go | 2 +- service/intel/geoip/module.go | 4 +- service/netenv/main.go | 2 +- service/netenv/online-status.go | 2 +- service/ui/module.go | 2 +- service/ui/serve.go | 2 +- service/updates/module.go | 115 ++++++-------------- service/updates/registry/bundle.go | 15 +-- service/updates/registry/index.go | 5 +- service/updates/registry/registry.go | 151 ++++++++++----------------- service/updates/restart.go | 50 --------- spn/captain/intel.go | 4 +- spn/captain/module.go | 2 +- 18 files changed, 168 insertions(+), 276 deletions(-) diff --git a/service/broadcasts/module.go b/service/broadcasts/module.go index 2d99115b2..19c1641f2 100644 --- a/service/broadcasts/module.go +++ b/service/broadcasts/module.go @@ -93,5 +93,5 @@ func New(instance instance) (*Broadcasts, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates } diff --git a/service/broadcasts/notify.go b/service/broadcasts/notify.go index 73b05f989..235a1bf03 100644 --- a/service/broadcasts/notify.go +++ b/service/broadcasts/notify.go @@ -67,7 +67,7 @@ type BroadcastNotification struct { func broadcastNotify(ctx *mgr.WorkerCtx) error { // Get broadcast notifications file, load it from disk and parse it. - broadcastsResource, err := module.instance.Updates().GetFile(broadcastsResourcePath) + broadcastsResource, err := module.instance.IntelUpdates().GetFile(broadcastsResourcePath) if err != nil { return fmt.Errorf("failed to get broadcast notifications update: %w", err) } diff --git a/service/instance.go b/service/instance.go index ad6e9dab9..43c7562b2 100644 --- a/service/instance.go +++ b/service/instance.go @@ -34,6 +34,7 @@ import ( "github.com/safing/portmaster/service/sync" "github.com/safing/portmaster/service/ui" "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" "github.com/safing/portmaster/spn/access" "github.com/safing/portmaster/spn/cabin" "github.com/safing/portmaster/spn/captain" @@ -46,6 +47,23 @@ import ( "github.com/safing/portmaster/spn/terminal" ) +var binaryUpdateIndex = registry.UpdateIndex{ + Directory: "/usr/lib/portmaster", + DownloadDirectory: "/var/lib/portmaster/new_bin", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://localhost:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, +} + +var intelUpdateIndex = registry.UpdateIndex{ + Directory: "/var/lib/portmaster/intel", + DownloadDirectory: "/var/lib/portmaster/new_intel", + IndexURLs: []string{"http://localhost:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, +} + // Instance is an instance of a Portmaster service. type Instance struct { ctx context.Context @@ -63,25 +81,26 @@ type Instance struct { rng *rng.Rng base *base.Base - core *core.Core - updates *updates.Updates - geoip *geoip.GeoIP - netenv *netenv.NetEnv - ui *ui.UI - profile *profile.ProfileModule - network *network.Network - netquery *netquery.NetQuery - firewall *firewall.Firewall - filterLists *filterlists.FilterLists - interception *interception.Interception - customlist *customlists.CustomList - status *status.Status - broadcasts *broadcasts.Broadcasts - compat *compat.Compat - nameserver *nameserver.NameServer - process *process.ProcessModule - resolver *resolver.ResolverModule - sync *sync.Sync + core *core.Core + binaryUpdates *updates.Updates + intelUpdates *updates.Updates + geoip *geoip.GeoIP + netenv *netenv.NetEnv + ui *ui.UI + profile *profile.ProfileModule + network *network.Network + netquery *netquery.NetQuery + firewall *firewall.Firewall + filterLists *filterlists.FilterLists + interception *interception.Interception + customlist *customlists.CustomList + status *status.Status + broadcasts *broadcasts.Broadcasts + compat *compat.Compat + nameserver *nameserver.NameServer + process *process.ProcessModule + resolver *resolver.ResolverModule + sync *sync.Sync access *access.Access @@ -147,7 +166,11 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx if err != nil { return instance, fmt.Errorf("create core module: %w", err) } - instance.updates, err = updates.New(instance) + instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex) + if err != nil { + return instance, fmt.Errorf("create updates module: %w", err) + } + instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex) if err != nil { return instance, fmt.Errorf("create updates module: %w", err) } @@ -274,7 +297,8 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx instance.notifications, instance.core, - instance.updates, + instance.binaryUpdates, + instance.intelUpdates, instance.geoip, instance.netenv, @@ -373,9 +397,14 @@ func (i *Instance) Base() *base.Base { return i.base } -// Updates returns the updates module. -func (i *Instance) Updates() *updates.Updates { - return i.updates +// BinaryUpdates returns the updates module. +func (i *Instance) BinaryUpdates() *updates.Updates { + return i.binaryUpdates +} + +// IntelUpdates returns the updates module. +func (i *Instance) IntelUpdates() *updates.Updates { + return i.intelUpdates } // GeoIP returns the geoip module. diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go index 842a96b20..ba5267c2c 100644 --- a/service/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -175,7 +175,7 @@ func updateListIndex() error { case listIndexUpdate == nil: // This is the first time this function is run, get updater file for index. var err error - listIndexUpdate, err = module.instance.Updates().GetFile(listIndexFilePath) + listIndexUpdate, err = module.instance.IntelUpdates().GetFile(listIndexFilePath) if err != nil { return err } diff --git a/service/intel/filterlists/module.go b/service/intel/filterlists/module.go index 92f6576e1..ecded5dc1 100644 --- a/service/intel/filterlists/module.go +++ b/service/intel/filterlists/module.go @@ -57,11 +57,12 @@ func init() { } func prep() error { - module.instance.Updates().EventResourcesUpdated.AddCallback("Check for blocklist updates", + module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("Check for blocklist updates", func(wc *mgr.WorkerCtx, s struct{}) (bool, error) { if ignoreUpdateEvents.IsSet() { return false, nil } + log.Debugf("performing filter list upadte") return false, tryListUpdate(wc.Ctx()) }) @@ -141,6 +142,6 @@ func New(instance instance) (*FilterLists, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates NetEnv() *netenv.NetEnv } diff --git a/service/intel/geoip/database.go b/service/intel/geoip/database.go index 72197cd89..99f02318d 100644 --- a/service/intel/geoip/database.go +++ b/service/intel/geoip/database.go @@ -198,7 +198,7 @@ func getGeoIPDB(resource string) (*geoIPDB, error) { } func open(resource string) (*registry.File, error) { - f, err := module.instance.Updates().GetFile(resource) + f, err := module.instance.IntelUpdates().GetFile(resource) if err != nil { return nil, fmt.Errorf("getting file: %w", err) } diff --git a/service/intel/geoip/module.go b/service/intel/geoip/module.go index 6c2bb55ef..2ebde990c 100644 --- a/service/intel/geoip/module.go +++ b/service/intel/geoip/module.go @@ -19,7 +19,7 @@ func (g *GeoIP) Manager() *mgr.Manager { } func (g *GeoIP) Start() error { - module.instance.Updates().EventResourcesUpdated.AddCallback( + module.instance.IntelUpdates().EventResourcesUpdated.AddCallback( "Check for GeoIP database updates", func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) { worker.triggerUpdate() @@ -66,5 +66,5 @@ func New(instance instance) (*GeoIP, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates } diff --git a/service/netenv/main.go b/service/netenv/main.go index 204267722..81cd540fb 100644 --- a/service/netenv/main.go +++ b/service/netenv/main.go @@ -107,5 +107,5 @@ func New(instance instance) (*NetEnv, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates } diff --git a/service/netenv/online-status.go b/service/netenv/online-status.go index 137ce410e..1d5feeafb 100644 --- a/service/netenv/online-status.go +++ b/service/netenv/online-status.go @@ -220,7 +220,7 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) // Trigger update check when coming (semi) online. if Online() { - module.instance.Updates().EventResourcesUpdated.Submit(struct{}{}) + module.instance.IntelUpdates().EventResourcesUpdated.Submit(struct{}{}) } } } diff --git a/service/ui/module.go b/service/ui/module.go index e9a8b324c..2e4f1d9ab 100644 --- a/service/ui/module.go +++ b/service/ui/module.go @@ -82,5 +82,5 @@ func New(instance instance) (*UI, error) { type instance interface { API() *api.API - Updates() *updates.Updates + BinaryUpdates() *updates.Updates } diff --git a/service/ui/serve.go b/service/ui/serve.go index 1455c3f6e..ad41b2d58 100644 --- a/service/ui/serve.go +++ b/service/ui/serve.go @@ -91,7 +91,7 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // get file from update system - zipFile, err := module.instance.Updates().GetFile(fmt.Sprintf("%s.zip", moduleName)) + zipFile, err := module.instance.BinaryUpdates().GetFile(fmt.Sprintf("%s.zip", moduleName)) if err != nil { if errors.Is(err, registry.ErrNotFound) { log.Tracef("ui: requested module %s does not exist", moduleName) diff --git a/service/updates/module.go b/service/updates/module.go index 31de626c8..a4b6490c2 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -1,10 +1,8 @@ package updates import ( - "errors" "flag" - "fmt" - "sync/atomic" + "time" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" @@ -14,10 +12,10 @@ import ( "github.com/safing/portmaster/service/updates/registry" ) -var applyUpdates bool +var autoUpdate bool func init() { - flag.BoolVar(&applyUpdates, "update", false, "apply downloaded updates") + flag.BoolVar(&autoUpdate, "auto-update", false, "auto apply downloaded updates") } // Updates provides access to released artifacts. @@ -25,9 +23,8 @@ type Updates struct { m *mgr.Manager states *mgr.StateMgr - updateBinaryWorkerMgr *mgr.WorkerMgr - updateIntelWorkerMgr *mgr.WorkerMgr - restartWorkerMgr *mgr.WorkerMgr + updateCheckWorkerMgr *mgr.WorkerMgr + upgraderWorkerMgr *mgr.WorkerMgr EventResourcesUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}] @@ -37,15 +34,9 @@ type Updates struct { instance instance } -var shimLoaded atomic.Bool - -// New returns a new UI module. -func New(instance instance) (*Updates, error) { - if !shimLoaded.CompareAndSwap(false, true) { - return nil, errors.New("only one instance allowed") - } - - m := mgr.New("Updates") +// New returns a new Updates module. +func New(instance instance, name string, index registry.UpdateIndex) (*Updates, error) { + m := mgr.New(name) module := &Updates{ m: m, states: m.NewStateMgr(), @@ -57,63 +48,47 @@ func New(instance instance) (*Updates, error) { } // Events - module.updateBinaryWorkerMgr = m.NewWorkerMgr("binary updater", module.checkForBinaryUpdates, nil) - module.updateIntelWorkerMgr = m.NewWorkerMgr("intel updater", module.checkForIntelUpdates, nil) - module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil) - - binIndex := registry.UpdateIndex{ - Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/lib/portmaster/new_bin", - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://localhost:8000/test-binary.json"}, - IndexFile: "bin-index.json", - AutoApply: false, - } + module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil) + module.updateCheckWorkerMgr.Repeat(30 * time.Second) + module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", func(w *mgr.WorkerCtx) error { + err := module.registry.ApplyUpdates() + if err != nil { + // TODO(vladimir): Send notification to UI + log.Errorf("updates: failed to apply updates: %s", err) + } else { + module.instance.Restart() + } + return nil + }, nil) - intelIndex := registry.UpdateIndex{ - Directory: "/var/lib/portmaster/intel", - DownloadDirectory: "/var/lib/portmaster/new_intel", - IndexURLs: []string{"http://localhost:8000/test-intel.json"}, - IndexFile: "intel-index.json", - AutoApply: true, - } - module.registry = registry.New(binIndex, intelIndex) + module.registry = registry.New(index) + _ = module.registry.Initialize() return module, nil } -func (u *Updates) checkForBinaryUpdates(_ *mgr.WorkerCtx) error { - hasUpdates, err := u.registry.CheckForBinaryUpdates() +func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { + hasUpdates, err := u.registry.CheckForUpdates() if err != nil { - log.Errorf("updates: failed to check for binary updates: %s", err) + log.Errorf("updates: failed to check for updates: %s", err) } if hasUpdates { - log.Infof("updates: there is updates available in the binary bundle") - err = u.registry.DownloadBinaryUpdates() + log.Infof("updates: there is updates available") + err = u.registry.DownloadUpdates() if err != nil { log.Errorf("updates: failed to download bundle: %s", err) + } else if autoUpdate { + u.ApplyUpdates() } } else { - log.Infof("updates: no new binary updates") + log.Infof("updates: no new updates") + u.EventResourcesUpdated.Submit(struct{}{}) } return nil } -func (u *Updates) checkForIntelUpdates(_ *mgr.WorkerCtx) error { - hasUpdates, err := u.registry.CheckForIntelUpdates() - if err != nil { - log.Errorf("updates: failed to check for intel updates: %s", err) - } - if hasUpdates { - log.Infof("updates: there is updates available in the intel bundle") - err = u.registry.DownloadIntelUpdates() - if err != nil { - log.Errorf("updates: failed to download bundle: %s", err) - } - } else { - log.Infof("updates: no new intel data updates") - } - return nil +func (u *Updates) ApplyUpdates() { + u.upgraderWorkerMgr.Go() } // States returns the state manager. @@ -128,29 +103,7 @@ func (u *Updates) Manager() *mgr.Manager { // Start starts the module. func (u *Updates) Start() error { - // initConfig() - - if applyUpdates { - err := u.registry.ApplyBinaryUpdates() - if err != nil { - log.Errorf("updates: failed to apply binary updates: %s", err) - } - err = u.registry.ApplyIntelUpdates() - if err != nil { - log.Errorf("updates: failed to apply intel updates: %s", err) - } - u.instance.Restart() - return nil - } - - err := u.registry.Initialize() - if err != nil { - // TODO(vladimir): Find a better way to handle this error. The service will stop if parsing of the bundle files fails. - return fmt.Errorf("failed to initialize registry: %w", err) - } - - u.updateBinaryWorkerMgr.Go() - u.updateIntelWorkerMgr.Go() + u.updateCheckWorkerMgr.Go() return nil } diff --git a/service/updates/registry/bundle.go b/service/updates/registry/bundle.go index 3034438a9..249539666 100644 --- a/service/updates/registry/bundle.go +++ b/service/updates/registry/bundle.go @@ -17,12 +17,6 @@ import ( "github.com/safing/portmaster/base/log" ) -const ( - defaultFileMode = os.FileMode(0o0644) - executableFileMode = os.FileMode(0o0744) - defaultDirMode = os.FileMode(0o0755) -) - const MaxUnpackSize = 1 << 30 // 2^30 == 1GB type Artifact struct { @@ -35,18 +29,17 @@ type Artifact struct { } type Bundle struct { - dir string Name string `json:"Bundle"` Version string `json:"Version"` Published time.Time `json:"Published"` Artifacts []Artifact `json:"Artifacts"` } -func (bundle Bundle) downloadAndVerify() { +func (bundle Bundle) downloadAndVerify(dir string) { client := http.Client{} for _, artifact := range bundle.Artifacts { - filePath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + filePath := fmt.Sprintf("%s/%s", dir, artifact.Filename) // TODO(vladimir): is this needed? _ = os.MkdirAll(filepath.Dir(filePath), defaultDirMode) @@ -66,9 +59,9 @@ func (bundle Bundle) downloadAndVerify() { } // Verify checks if the files are present int the dataDir and have the correct hash. -func (bundle Bundle) Verify() error { +func (bundle Bundle) Verify(dir string) error { for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + artifactPath := fmt.Sprintf("%s/%s", dir, artifact.Filename) file, err := os.Open(artifactPath) if err != nil { return fmt.Errorf("failed to open file %s: %w", artifactPath, err) diff --git a/service/updates/registry/index.go b/service/updates/registry/index.go index f5900b3fd..cc2371639 100644 --- a/service/updates/registry/index.go +++ b/service/updates/registry/index.go @@ -12,6 +12,7 @@ import ( type UpdateIndex struct { Directory string DownloadDirectory string + PurgeDirectory string Ignore []string IndexURLs []string IndexFile string @@ -23,7 +24,7 @@ func (ui *UpdateIndex) downloadIndexFile() (err error) { for _, url := range ui.IndexURLs { err = ui.downloadIndexFileFromURL(url) if err != nil { - log.Warningf("updates: %s", err) + log.Warningf("updates: failed while downloading index file %s", err) continue } // Downloading was successful. @@ -37,7 +38,7 @@ func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { client := http.Client{} resp, err := client.Get(url) if err != nil { - return fmt.Errorf("failed a get request to %s: %w", url, err) + return fmt.Errorf("failed GET request to %s: %w", url, err) } defer func() { _ = resp.Body.Close() }() filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) diff --git a/service/updates/registry/registry.go b/service/updates/registry/registry.go index 223b90dec..f0e77adb1 100644 --- a/service/updates/registry/registry.go +++ b/service/updates/registry/registry.go @@ -14,6 +14,12 @@ import ( var ErrNotFound error = errors.New("file not found") +const ( + defaultFileMode = os.FileMode(0o0644) + executableFileMode = os.FileMode(0o0744) + defaultDirMode = os.FileMode(0o0755) +) + type File struct { id string path string @@ -32,24 +38,19 @@ func (f *File) Version() string { } type Registry struct { - binaryUpdateIndex UpdateIndex - intelUpdateIndex UpdateIndex - - binaryBundle *Bundle - intelBundle *Bundle + updateIndex UpdateIndex - binaryUpdateBundle *Bundle - intelUpdateBundle *Bundle + bundle *Bundle + updateBundle *Bundle files map[string]File } // New create new Registry. -func New(binIndex UpdateIndex, intelIndex UpdateIndex) Registry { +func New(index UpdateIndex) Registry { return Registry{ - binaryUpdateIndex: binIndex, - intelUpdateIndex: intelIndex, - files: make(map[string]File), + updateIndex: index, + files: make(map[string]File), } } @@ -58,26 +59,20 @@ func (reg *Registry) Initialize() error { var err error // Parse current installed binary bundle. - reg.binaryBundle, err = parseBundle(reg.binaryUpdateIndex.Directory, reg.binaryUpdateIndex.IndexFile) + reg.bundle, err = parseBundle(reg.updateIndex.Directory, reg.updateIndex.IndexFile) if err != nil { return fmt.Errorf("failed to parse binary bundle: %w", err) } - // Parse current installed intel bundle. - reg.intelBundle, err = parseBundle(reg.intelUpdateIndex.Directory, reg.intelUpdateIndex.IndexFile) - if err != nil { - return fmt.Errorf("failed to parse intel bundle: %w", err) - } // Add bundle artifacts to registry. - reg.processBundle(reg.binaryBundle) - reg.processBundle(reg.intelBundle) + reg.processBundle(reg.bundle) return nil } func (reg *Registry) processBundle(bundle *Bundle) { for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename) reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath} } } @@ -89,112 +84,84 @@ func (reg *Registry) GetFile(id string) (*File, error) { return &file, nil } else { log.Errorf("updates: requested file id not found: %s", id) + for _, file := range reg.files { + log.Debugf("File: %s", file) + } return nil, ErrNotFound } } -// CheckForBinaryUpdates checks if there is a new binary bundle updates. -func (reg *Registry) CheckForBinaryUpdates() (bool, error) { - err := reg.binaryUpdateIndex.downloadIndexFile() +// CheckForUpdates checks if there is a new binary bundle updates. +func (reg *Registry) CheckForUpdates() (bool, error) { + err := reg.updateIndex.downloadIndexFile() if err != nil { return false, err } - reg.binaryUpdateBundle, err = parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) + reg.updateBundle, err = parseBundle(reg.updateIndex.DownloadDirectory, reg.updateIndex.IndexFile) if err != nil { - return false, fmt.Errorf("failed to parse bundle file: %w", err) + return false, err } // TODO(vladimir): Make a better check. - if reg.binaryBundle.Version != reg.binaryUpdateBundle.Version { + if reg.bundle.Version != reg.updateBundle.Version { return true, nil } return false, nil } -// DownloadBinaryUpdates downloads available binary updates. -func (reg *Registry) DownloadBinaryUpdates() error { - if reg.binaryUpdateBundle == nil { +// DownloadUpdates downloads available binary updates. +func (reg *Registry) DownloadUpdates() error { + if reg.updateBundle == nil { // CheckForBinaryUpdates needs to be called before this. return fmt.Errorf("no valid update bundle found") } - _ = deleteUnfinishedDownloads(reg.binaryBundle.dir) - reg.binaryUpdateBundle.downloadAndVerify() + _ = deleteUnfinishedDownloads(reg.updateIndex.DownloadDirectory) + reg.updateBundle.downloadAndVerify(reg.updateIndex.DownloadDirectory) return nil } -// CheckForIntelUpdates checks if there is a new intel data bundle updates. -func (reg *Registry) CheckForIntelUpdates() (bool, error) { - err := reg.intelUpdateIndex.downloadIndexFile() - if err != nil { - return false, err - } - - reg.intelUpdateBundle, err = parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) +// ApplyUpdates removes the current binary folder and replaces it with the downloaded one. +func (reg *Registry) ApplyUpdates() error { + // Create purge dir. + err := os.MkdirAll(filepath.Dir(reg.updateIndex.PurgeDirectory), defaultDirMode) if err != nil { - return false, fmt.Errorf("failed to parse bundle file: %w", err) - } - - // TODO(vladimir): Make a better check. - if reg.intelBundle.Version != reg.intelUpdateBundle.Version { - return true, nil - } - - return false, nil -} - -// DownloadIntelUpdates downloads available intel data updates. -func (reg *Registry) DownloadIntelUpdates() error { - if reg.intelUpdateBundle == nil { - // CheckForIntelUpdates needs to be called before this. - return fmt.Errorf("no valid update bundle found") + return fmt.Errorf("failed to create directory: %w", err) } - _ = deleteUnfinishedDownloads(reg.intelBundle.dir) - reg.intelUpdateBundle.downloadAndVerify() - return nil -} -// ApplyBinaryUpdates removes the current binary folder and replaces it with the downloaded one. -func (reg *Registry) ApplyBinaryUpdates() error { - bundle, err := parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) - if err != nil { - return fmt.Errorf("failed to parse index file: %w", err) - } - err = bundle.Verify() + // Read all files in the current version folder. + files, err := os.ReadDir(reg.updateIndex.Directory) if err != nil { - return fmt.Errorf("binary bundle is not valid: %w", err) + return err } - err = os.RemoveAll(reg.binaryUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to remove dir: %w", err) - } - err = os.Rename(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to move dir: %w", err) + // Move current version files into purge folder. + for _, file := range files { + filepath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, file.Name()) + purgePath := fmt.Sprintf("%s/%s", reg.updateIndex.PurgeDirectory, file.Name()) + err := os.Rename(filepath, purgePath) + if err != nil { + return fmt.Errorf("failed to move file %s: %w", filepath, err) + } } - return nil -} -// ApplyIntelUpdates removes the current intel folder and replaces it with the downloaded one. -func (reg *Registry) ApplyIntelUpdates() error { - bundle, err := parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) + // Move the new index file + indexFile := fmt.Sprintf("%s/%s", reg.updateIndex.DownloadDirectory, reg.updateIndex.IndexFile) + newIndexFile := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, reg.updateIndex.IndexFile) + err = os.Rename(indexFile, newIndexFile) if err != nil { - return fmt.Errorf("failed to parse index file: %w", err) - } - err = bundle.Verify() - if err != nil { - return fmt.Errorf("binary bundle is not valid: %w", err) + return fmt.Errorf("failed to move index file %s: %w", indexFile, err) } - err = os.RemoveAll(reg.intelUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to remove dir: %w", err) - } - err = os.Rename(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to move dir: %w", err) + // Move downloaded files to the current version folder. + for _, artifact := range reg.bundle.Artifacts { + fromFilepath := fmt.Sprintf("%s/%s", reg.updateIndex.DownloadDirectory, artifact.Filename) + toFilepath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename) + err = os.Rename(fromFilepath, toFilepath) + if err != nil { + return fmt.Errorf("failed to move file %s: %w", fromFilepath, err) + } } return nil } @@ -220,8 +187,6 @@ func parseBundle(dir string, indexFile string) (*Bundle, error) { if err != nil { return nil, err } - bundle.dir = dir - return &bundle, nil } diff --git a/service/updates/restart.go b/service/updates/restart.go index 30fc9289f..66ee82d8d 100644 --- a/service/updates/restart.go +++ b/service/updates/restart.go @@ -1,15 +1,12 @@ package updates import ( - "os/exec" - "runtime" "sync" "time" "github.com/tevino/abool" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/service/mgr" ) var ( @@ -86,50 +83,3 @@ func RestartNow() { restartPending.Set() // module.restartWorkerMgr.Go() } - -func automaticRestart(w *mgr.WorkerCtx) error { - // Check if the restart is still scheduled. - if restartPending.IsNotSet() { - return nil - } - - // Trigger restart. - if restartTriggered.SetToIf(false, true) { - log.Warning("updates: initiating (automatic) restart") - - // Check if we should reboot instead. - var rebooting bool - if RebootOnRestart { - // Trigger system reboot and record success. - rebooting = triggerSystemReboot() - if !rebooting { - log.Warningf("updates: rebooting failed, only restarting service instead") - } - } - - // Set restart exit code. - // if !rebooting { - // module.instance.Restart() - // } else { - // module.instance.Shutdown() - // } - } - - return nil -} - -func triggerSystemReboot() (success bool) { - switch runtime.GOOS { - case "linux": - err := exec.Command("systemctl", "reboot").Run() - if err != nil { - log.Errorf("updates: triggering reboot with systemctl failed: %s", err) - return false - } - default: - log.Warningf("updates: rebooting is not support on %s", runtime.GOOS) - return false - } - - return true -} diff --git a/spn/captain/intel.go b/spn/captain/intel.go index 6411f4c6f..df04c0160 100644 --- a/spn/captain/intel.go +++ b/spn/captain/intel.go @@ -22,7 +22,7 @@ var ( ) func registerIntelUpdateHook() error { - module.instance.Updates().EventResourcesUpdated.AddCallback("update SPN intel", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) { + module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("update SPN intel", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) { return false, updateSPNIntel(wc.Ctx(), nil) }) @@ -49,7 +49,7 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) { } // Get intel file and load it from disk. - intelResource, err = module.instance.Updates().GetFile(intelResourcePath) + intelResource, err = module.instance.IntelUpdates().GetFile(intelResourcePath) if err != nil { return fmt.Errorf("failed to get SPN intel update: %w", err) } diff --git a/spn/captain/module.go b/spn/captain/module.go index 1f82d7632..2cd167191 100644 --- a/spn/captain/module.go +++ b/spn/captain/module.go @@ -249,6 +249,6 @@ type instance interface { NetEnv() *netenv.NetEnv Patrol() *patrol.Patrol Config() *config.Config - Updates() *updates.Updates + IntelUpdates() *updates.Updates SPNGroup() *mgr.ExtendedGroup } From 8c6eb04292d46bb0dace559e85ce9697abc4f105 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 5 Sep 2024 11:21:19 +0300 Subject: [PATCH 09/11] [desktop] Fix merge issues --- desktop/tauri/src-tauri/Cargo.lock | 627 ++++++++---------- desktop/tauri/src-tauri/Cargo.toml | 2 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 1 + .../src-tauri/gen/schemas/linux-schema.json | 1 + desktop/tauri/src-tauri/src/main.rs | 39 +- desktop/tauri/src-tauri/src/traymenu.rs | 21 +- 7 files changed, 291 insertions(+), 402 deletions(-) diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index 8e0373038..0d3cdbafb 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -74,7 +80,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -209,58 +215,6 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "app" -version = "0.1.0" -dependencies = [ - "assert_matches", - "cached", - "clap 4.5.16", - "ctor", - "dark-light", - "dataurl", - "dirs 1.0.5", - "futures-util", - "gdk-pixbuf", - "gdk-pixbuf-sys", - "gio-sys", - "glib", - "glib-sys", - "gtk", - "gtk-sys", - "http", - "lazy_static", - "log", - "notify-rust", - "open", - "reqwest", - "rfd", - "rust-ini", - "serde", - "serde_json", - "sha", - "tauri", - "tauri-build", - "tauri-cli", - "tauri-plugin-clipboard-manager", - "tauri-plugin-dialog", - "tauri-plugin-log", - "tauri-plugin-notification", - "tauri-plugin-os", - "tauri-plugin-shell", - "tauri-plugin-single-instance", - "tauri-plugin-window-state", - "tauri-winrt-notification 0.3.1", - "thiserror", - "tokio", - "tokio-websockets", - "url", - "uuid", - "which", - "windows 0.54.0", - "windows-service", -] - [[package]] name = "ar" version = "0.9.0" @@ -302,7 +256,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -424,7 +378,7 @@ checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-lite 2.3.0", "slab", ] @@ -485,7 +439,7 @@ dependencies = [ "futures-lite 2.3.0", "parking", "polling 3.7.3", - "rustix 0.38.34", + "rustix 0.38.35", "slab", "tracing", "windows-sys 0.59.0", @@ -535,7 +489,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.34", + "rustix 0.38.35", "windows-sys 0.48.0", ] @@ -554,7 +508,7 @@ dependencies = [ "cfg-if", "event-listener 5.3.1", "futures-lite 2.3.0", - "rustix 0.38.34", + "rustix 0.38.35", "tracing", "windows-sys 0.59.0", ] @@ -567,7 +521,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -582,7 +536,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.34", + "rustix 0.38.35", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -596,13 +550,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -742,7 +696,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -843,9 +797,9 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.5.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" [[package]] name = "bitvec" @@ -952,10 +906,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "syn_derive", ] @@ -1061,9 +1015,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytemuck" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31" +checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" [[package]] name = "byteorder" @@ -1186,9 +1140,9 @@ dependencies = [ [[package]] name = "cargo-mobile2" -version = "0.15.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b8132519bea2d46174e777bd36d480d93afbe1df31c27cacfb411ff152bba1" +checksum = "93ede7b4200c8794c5fe7bc25c93a8f1756b87d50968cc20def67f2618035f65" dependencies = [ "colored", "core-foundation 0.10.0", @@ -1273,9 +1227,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.13" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" dependencies = [ "jobserver", "libc", @@ -1372,9 +1326,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.16" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -1382,9 +1336,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -1394,11 +1348,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.18" +version = "4.5.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee158892bd7ce77aa15c208abbdb73e155d191c287a659b57abd5adb92feb03" +checksum = "18d7f143a7e709cbe6c34853dcd3bb1370c7e1bb4d9e7310ca8cb40b490ae035" dependencies = [ - "clap 4.5.16", + "clap 4.5.17", ] [[package]] @@ -1410,7 +1364,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1817,7 +1771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1827,7 +1781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1873,7 +1827,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1939,7 +1893,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1961,7 +1915,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2059,38 +2013,38 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "derive_builder" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "derive_builder_macro" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2103,7 +2057,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2190,7 +2144,7 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.5", + "redox_users 0.4.6", "windows-sys 0.48.0", ] @@ -2201,7 +2155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users 0.4.5", + "redox_users 0.4.6", "winapi", ] @@ -2219,7 +2173,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2242,7 +2196,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2468,7 +2422,7 @@ checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" dependencies = [ "num-traits", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2489,7 +2443,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2595,7 +2549,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -2623,9 +2577,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fdeflate" @@ -2673,9 +2627,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", @@ -2685,12 +2639,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -2773,7 +2727,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2911,7 +2865,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-core", "futures-io", "parking", @@ -2926,7 +2880,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -3223,7 +3177,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -3326,14 +3280,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -3341,7 +3295,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.4.0", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -3546,9 +3500,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http", @@ -3766,9 +3720,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -3839,7 +3793,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -3895,9 +3849,9 @@ dependencies = [ [[package]] name = "iter-read" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b" +checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" [[package]] name = "itertools" @@ -4168,15 +4122,15 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0afd06142c9bcb03f4a8787c77897a87b6be9c4918f1946c33caa714c27578" +checksum = "f5f037c58cadb17e8591b620b523cc6a7ab2b91b6ce3121f8eb4171f8d80115c" dependencies = [ "ahash 0.8.11", "anyhow", "base64 0.22.1", "bytecount", - "clap 4.5.9", + "clap 4.5.17", "fancy-regex", "fraction", "getrandom 0.2.15", @@ -4370,9 +4324,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-ip-address" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696" +checksum = "b435d7dd476416a905f9634dff8c330cee8d3168fdd1fbd472a17d1a75c00c3e" dependencies = [ "libc", "neli", @@ -4596,7 +4550,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -4632,6 +4586,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -4645,8 +4608,8 @@ dependencies = [ ] [[package]] -name = "muda" -version = "0.13.5" +name = "mio" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ @@ -4658,9 +4621,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c410a9d21523a819e84881603fbc00331c8001eb899964952046671deddb9c" +checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ "cocoa 0.26.0", "crossbeam-channel", @@ -4930,7 +4893,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -4980,40 +4943,19 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" -dependencies = [ - "num_enum_derive 0.7.2", + "num_enum_derive", ] [[package]] name = "num_enum_derive" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5166,9 +5108,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -5241,7 +5183,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5362,7 +5304,7 @@ checksum = "d0a07c44bbe07756ba25605059fa4a94543f6a75730fd8bd1105795d0b3d668d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5626,7 +5568,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5684,7 +5626,8 @@ dependencies = [ "nom", "num-bigint-dig", "num-traits", - "num_enum 0.7.2", + "num_enum", + "ocb3", "p256", "p384", "p521", @@ -5807,7 +5750,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5860,7 +5803,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5882,7 +5825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-io", ] @@ -5920,7 +5863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.2.6", + "indexmap 2.5.0", "quick-xml 0.32.0", "serde", "time", @@ -5936,7 +5879,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -5965,7 +5908,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.34", + "rustix 0.38.35", "tracing", "windows-sys 0.59.0", ] @@ -6001,14 +5944,14 @@ dependencies = [ "glib-sys", "gtk", "gtk-sys", - "http 1.1.0", + "http", "lazy_static", "log", "notify-rust", "open", - "reqwest 0.12.5", + "reqwest", "rfd", - "rust-ini 0.20.0", + "rust-ini", "serde", "serde_json", "sha", @@ -6042,9 +5985,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "precomputed-hash" @@ -6082,11 +6028,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.21.1", + "toml_edit 0.22.20", ] [[package]] @@ -6144,7 +6090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -6202,9 +6148,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -6399,9 +6345,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", @@ -6463,9 +6409,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64 0.22.1", "bytes", @@ -6504,7 +6450,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -6560,9 +6506,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -6593,9 +6539,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", @@ -6611,9 +6557,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ "proc-macro2", "quote", @@ -6727,9 +6673,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.35.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec 0.7.6", "borsh", @@ -6755,9 +6701,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -6778,9 +6724,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ "bitflags 2.6.0", "errno", @@ -6835,9 +6781,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" dependencies = [ "ring", "rustls-pki-types", @@ -6924,7 +6870,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7030,9 +6976,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -7060,13 +7006,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7077,7 +7023,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7091,11 +7037,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "itoa 1.0.11", "memchr", "ryu", @@ -7120,7 +7066,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7154,7 +7100,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_derive", "serde_json", @@ -7171,7 +7117,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7678,9 +7624,9 @@ dependencies = [ [[package]] name = "svgtypes" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" +checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" dependencies = [ "kurbo", "siphasher 1.0.1", @@ -7710,9 +7656,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.75" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -7728,7 +7674,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7742,6 +7688,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "sys-locale" @@ -7767,20 +7716,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation 0.9.4", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -7801,9 +7750,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a97abbc7d6cfd0720da3e06fcb1cf2ac87cbfdb5bbbce103a1279a211c4d81" +checksum = "2a93f2c6b8fdaeb7f417bda89b5bc767999745c3052969664ae1fa65892deb7e" dependencies = [ "bitflags 2.6.0", "cocoa 0.26.0", @@ -7840,13 +7789,13 @@ dependencies = [ [[package]] name = "tao-macros" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] @@ -7874,9 +7823,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.0-beta.24" +version = "2.0.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eab508aad4ae86e23865e294b20a7bb89bd7afea523897b7478329b841d4295" +checksum = "0b805e6bf5f6a4df7d1a64b2952d33fca6d538746efe9c9cdae4157a1efc5b17" dependencies = [ "anyhow", "bytes", @@ -7910,7 +7859,7 @@ dependencies = [ "tauri-macros", "tauri-runtime", "tauri-runtime-wry", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "thiserror", "tokio", "tray-icon", @@ -7924,9 +7873,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.0-rc.7" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5ad5fcfaf02cf79aa6727f6c5df38567d8dce172b00b62690c6bc46c08b7ce" +checksum = "4acec578ff9de14da177722c8fb5e3d6c88af296696190c70b83bec91437248a" dependencies = [ "anyhow", "cargo_toml", @@ -7938,7 +7887,7 @@ dependencies = [ "semver", "serde", "serde_json", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "tauri-winres", "toml 0.8.19", "walkdir", @@ -7946,9 +7895,9 @@ dependencies = [ [[package]] name = "tauri-bundler" -version = "2.0.1-beta.18" +version = "2.0.1-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cc597af0bcb88c1966ccba76a6dcde5b6530c97a4fc2d798f7d3fddf182944" +checksum = "850672a9b3316d0c3abea71891d07184c6e5176b68e7c6fb0b4d5432c08f7e43" dependencies = [ "anyhow", "ar", @@ -7976,7 +7925,7 @@ dependencies = [ "tar", "tauri-icns", "tauri-macos-sign", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "tempfile", "thiserror", "time", @@ -7990,15 +7939,16 @@ dependencies = [ [[package]] name = "tauri-cli" -version = "2.0.0-beta.22" +version = "2.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24129a8dc9c3c910fca89ba0fb87ad9028868abf9dadb2514cd569fef9ad53af" +checksum = "6581f7a8e460dd8b2d9359728a90a034e7ccb0eed6d42cfb1da43f8c4842077d" dependencies = [ "anyhow", + "ar", "axum", "base64 0.22.1", "cargo-mobile2", - "clap 4.5.9", + "clap 4.5.17", "clap_complete", "colored", "common-path", @@ -8032,6 +7982,7 @@ dependencies = [ "minisign", "notify", "notify-debouncer-mini", + "object", "os_info", "os_pipe", "oxc_allocator", @@ -8052,7 +8003,7 @@ dependencies = [ "tauri-icns", "tauri-macos-sign", "tauri-utils 1.6.0", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "tempfile", "tokio", "toml 0.8.19", @@ -8065,9 +8016,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.0.0-rc.7" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809ef6316726fc72593d296cf6f4e7461326e310c313d6a6c42b6e7f1e2671cf" +checksum = "0744bec087358e5de9a078a1b19346ed9b775f578395975f5a74ccd0c717b22a" dependencies = [ "base64 0.22.1", "brotli", @@ -8081,8 +8032,8 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.75", - "tauri-utils 2.0.0-rc.7", + "syn 2.0.77", + "tauri-utils 2.0.0-rc.8", "thiserror", "time", "url", @@ -8121,23 +8072,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.0-rc.6" +version = "2.0.0-rc.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1359e8861d210d25731f8b1bfbb4d111dd06406cf73c59659366ef450364d811" +checksum = "b043cac341130f288044dca76fae8e62d7c18fdcd8012239a66af03868b7ca37" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "tauri-codegen", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", ] [[package]] name = "tauri-plugin" -version = "2.0.0-rc.3" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec01af01098a286d3e430c1fa947bfd77bc8011ecb209438af4444b02d82b29e" +checksum = "1c9bb31aad7296f85df545171023c72a263b54aac350197f923893fb5e6f90b4" dependencies = [ "anyhow", "glob", @@ -8145,16 +8096,16 @@ dependencies = [ "schemars", "serde", "serde_json", - "tauri-utils 2.0.0-beta.19", - "toml 0.8.15", + "tauri-utils 2.0.0-rc.8", + "toml 0.8.19", "walkdir", ] [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.0.0-rc.1" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6719e5f3fcc6c6f5afda68da944a44a50c28f30a3506c457ea7dbcc13377bfe0" +checksum = "4a9d78deb78704d9eeedc23892fc6b25a944c7f9c59354a0ea9a4fb5ab043794" dependencies = [ "arboard", "image 0.24.9", @@ -8168,9 +8119,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.0.0-beta.11" +version = "2.0.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8860dd73c96969eb14813f9f04d8665f2853342670456fb6619d637137ef0d09" +checksum = "191a81b01083881a388879e269eec60b582eed6fc3e16884dc394ff1791b0c19" dependencies = [ "dunce", "log", @@ -8182,13 +8133,14 @@ dependencies = [ "tauri-plugin", "tauri-plugin-fs", "thiserror", + "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.0.0-beta.11" +version = "2.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461853268fe115ca19ee21e5986d505944f0b826048fe1bd726d74753fdf1df6" +checksum = "0704205734f3b4f37d7bf9e9072537716371741bf98b22a22cc5e5340e512467" dependencies = [ "anyhow", "glob", @@ -8205,9 +8157,9 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.0.0-beta.9" +version = "2.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f80d78a6e8102acf05a1e735f006991a2abfc71566d4e484f820b7495cd52c" +checksum = "b57e4666c4a5d81f81b7bb8eacf51ae32c4e69c35071aabb480ad20a80836e4e" dependencies = [ "android_logger", "byte-unit", @@ -8227,9 +8179,9 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.0.0-beta.10" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a09eb4d9e0919ce954da68d3707ddb161e5e37447da26609d0d0f5aebbc69a" +checksum = "6bf32fba3a2650f89c7dfc532b38de566a37779e482f07dccabf45fe24444e26" dependencies = [ "log", "notify-rust", @@ -8246,9 +8198,9 @@ dependencies = [ [[package]] name = "tauri-plugin-os" -version = "2.0.0-beta.8" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a0466f11f45fd3f640a17b5ba5e34c62912c9b391141c818155125ae9f0917" +checksum = "ebc4ee761edd532fce2232453e9c8e0f7d9c0b6fe125c4b90b3eb4362ee84224" dependencies = [ "gethostname", "log", @@ -8264,9 +8216,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.0.0-beta.9" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9fa8c4e3d9ec343f6c3eb081672045566128a6c48ff6f6eeea85251ff38d3f" +checksum = "e83800ddf78b820172efb5ed7310344e8e4f97fd30cd8237a3f20c12a79eb136" dependencies = [ "encoding_rs", "log", @@ -8285,24 +8237,24 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.0.0-beta.11" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b21866e185e2f9c5d40afb851441e3292a4f94f4a26af6ae0dff6e7e5ba03f42" +checksum = "d73c92c98d44d4daba0118d905f45243dfcd6eaac82216c3382a02d17cb74cf2" dependencies = [ "log", "serde", "serde_json", "tauri", "thiserror", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "zbus 4.4.0", ] [[package]] name = "tauri-plugin-window-state" -version = "2.0.0-beta.11" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f7cea222b8eeb3598c7b3e19e9c7e6b1c2d60207b87225e0c3bb1c24c8fdec" +checksum = "303569dd7858361d4c623845448b136b4c95d53b5d2bde6630bea9d7f0022d45" dependencies = [ "bitflags 2.6.0", "log", @@ -8315,9 +8267,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.0.0-beta.20" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe978df03966febbebc608931dc2cf26ef94df70855a18b05f07134cf474de09" +checksum = "d7c7a6530acc06640e8f07cfeb01ac694f1de2f4e565525a2199e0dca80ff9f7" dependencies = [ "dpi", "gtk", @@ -8326,7 +8278,7 @@ dependencies = [ "raw-window-handle", "serde", "serde_json", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "thiserror", "url", "windows 0.58.0", @@ -8334,9 +8286,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.0.0-beta.20" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11e4d568f61095f507b3fc4254dfbfff3b20de2a1d66167ffca3f6d90b14db8f" +checksum = "5fcadbc24646c8d3362ed4e332cb42932e08c632220a20a61cb7e5fe36ddd85c" dependencies = [ "cocoa 0.26.0", "gtk", @@ -8348,7 +8300,7 @@ dependencies = [ "softbuffer", "tao", "tauri-runtime", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "url", "webkit2gtk", "webview2-com", @@ -8389,9 +8341,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.0-rc.7" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53d9fe87e985b273696ae22ce2b9f099a8f1b44bc8fb127467bda5fcb3e4371" +checksum = "201498c8281ab2597e344b4a4c923e8d491782305979d71e7cdf8fb79aab5948" dependencies = [ "aes-gcm", "brotli", @@ -8465,9 +8417,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", - "fastrand 2.1.0", + "fastrand 2.1.1", "once_cell", - "rustix 0.38.34", + "rustix 0.38.35", "windows-sys 0.59.0", ] @@ -8536,7 +8488,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -8645,15 +8597,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.2", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", @@ -8670,7 +8621,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -8731,9 +8682,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -8781,7 +8732,7 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -8803,7 +8754,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -8816,18 +8767,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.2.6", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" -dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "toml_datetime", "winnow 0.5.40", ] @@ -8838,7 +8778,7 @@ version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -8893,7 +8833,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9125,9 +9065,9 @@ dependencies = [ [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-script" @@ -9428,7 +9368,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -9462,7 +9402,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9542,9 +9482,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" dependencies = [ "rustls-pki-types", ] @@ -9571,7 +9511,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9593,13 +9533,13 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "6.0.1" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "rustix 0.38.34", + "rustix 0.38.35", "winsafe", ] @@ -9744,7 +9684,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9755,7 +9695,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9766,7 +9706,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9777,7 +9717,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -10089,9 +10029,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wry" -version = "0.42.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b8049c8f239cdbfaaea4bacb9646f6b208938ceec0acd5b3e99cd05f70903f" +checksum = "f4d715cf5fe88e9647f3d17b207b6d060d4a88e7171d4ccb2d2c657dd1d44728" dependencies = [ "base64 0.22.1", "block", @@ -10164,7 +10104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix 0.38.34", + "rustix 0.38.35", "x11rb-protocol", ] @@ -10213,7 +10153,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.14", - "rustix 0.38.34", + "rustix 0.38.35", ] [[package]] @@ -10347,10 +10287,10 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "zvariant_utils 2.1.0", ] @@ -10376,22 +10316,13 @@ dependencies = [ "zvariant 4.2.0", ] -[[package]] -name = "zerocopy" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", -] - [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -10403,7 +10334,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -10423,21 +10354,21 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "zip" -version = "2.1.6" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.4.0", + "indexmap 2.5.0", "memchr", "thiserror", "zopfli", @@ -10557,10 +10488,10 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "zvariant_utils 2.1.0", ] @@ -10583,5 +10514,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index f89ca02fb..f38c96792 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -27,7 +27,7 @@ tauri-plugin-log = "2.0.0-rc" tauri-plugin-window-state = "2.0.0-rc" tauri-cli = "2.0.0-rc.8" -clap = { version = "4" } +clap_lex = "0.7.2" # General serde_json = "1.0" diff --git a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json index 85397e542..233ccc01d 100644 --- a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json +++ b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"clipboard-manager":{"default_permission":null,"permissions":{"allow-read":{"identifier":"allow-read","description":"Enables the read command without any pre-configured scope.","commands":{"allow":["read"],"deny":[]}},"allow-write":{"identifier":"allow-write","description":"Enables the write command without any pre-configured scope.","commands":{"allow":["write"],"deny":[]}},"deny-read":{"identifier":"deny-read","description":"Denies the read command without any pre-configured scope.","commands":{"allow":[],"deny":["read"]}},"deny-write":{"identifier":"deny-write","description":"Denies the write command without any pre-configured scope.","commands":{"allow":[],"deny":["write"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} +{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json index 2119bf212..797ccb5c8 100644 --- a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json @@ -3072,3 +3072,4 @@ ] } } +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json index 2119bf212..797ccb5c8 100644 --- a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json @@ -3072,3 +3072,4 @@ ] } } +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index c0adcb5c7..7d605021b 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -13,8 +13,8 @@ mod service; mod xdg; // App modules -mod config; mod cli; +mod config; mod portmaster; mod traymenu; mod window; @@ -126,32 +126,8 @@ fn main() { let cli_args = cli::parse(std::env::args()); - let mut cli = CliArguments { - data: None, - log: LOG_LEVEL.to_string(), - background: false, - with_prompts: false, - with_notifications: false, - }; - - if let Some(data) = matches.get_one::("data") { - cli.data = Some(data.to_string()); - } - - if let Some(log) = matches.get_one::("log") { - cli.log = log.to_string(); - } - - if let Some(value) = matches.get_one::("with_prompts") { - cli.with_prompts = *value; - } - - if let Some(value) = matches.get_one::("with_notifications") { - cli.with_notifications = *value; - } - #[cfg(target_os = "linux")] - let log_target = if let Some(data_dir) = cli.data { + let log_target = if let Some(data_dir) = cli_args.data { tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder { path: Path::new(&format!("{}/logs/app2", data_dir)).into(), file_name: None, @@ -168,17 +144,6 @@ fn main() { tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout) }; - let mut log_level = LOG_LEVEL; - match cli.log.as_ref() { - "off" => log_level = LevelFilter::Off, - "error" => log_level = LevelFilter::Error, - "warn" => log_level = LevelFilter::Warn, - "info" => log_level = LevelFilter::Info, - "debug" => log_level = LevelFilter::Debug, - "trace" => log_level = LevelFilter::Trace, - _ => {} - } - let app = tauri::Builder::default() // Shell plugin for open_external support .plugin(tauri_plugin_shell::init()) diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 600af46d3..127c21943 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -1,18 +1,18 @@ use std::ops::Deref; use std::sync::atomic::AtomicBool; -use std::sync::{Mutex, RwLock}; +use std::sync::RwLock; use std::{collections::HashMap, sync::atomic::Ordering}; use log::{debug, error}; use tauri::menu::{Menu, MenuItemKind}; use tauri::tray::{MouseButton, MouseButtonState}; -use tauri::Runtime; use tauri::{ image::Image, menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, tray::{TrayIcon, TrayIconBuilder}, Wry, }; +use tauri::{Manager, Runtime}; use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use crate::config; @@ -91,13 +91,6 @@ fn get_red_icon() -> &'static [u8] { const DARK_RED_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); match get_theme_mode() { - const DARK_RED_ICON: &'static [u8] = - include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); - let mode = dark_light::detect(); - match mode { - const DARK_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); - let mode = dark_light::detect(); - match mode { dark_light::Mode::Light => DARK_RED_ICON, _ => LIGHT_RED_ICON, } @@ -142,10 +135,7 @@ pub fn setup_tray_menu( .enabled(false) .build(app) .unwrap(); - { - let mut button_ref = SPN_STATUS.lock()?; - *button_ref = Some(spn_status.clone()); - } + // Setup SPN button let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, "Enable SPN") .build(app) @@ -496,7 +486,9 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { } } } - update_spn_ui_state(false); + if let Some(menu) = app.menu() { + update_spn_ui_state(menu, false); + } update_icon_color(&icon, IconColor::Red); } @@ -564,7 +556,6 @@ fn save_theme(app: &tauri::AppHandle, mode: dark_light::Mode) { if let Some(menu) = app.menu() { update_spn_ui_state(menu, false); } - _ = icon.set_icon(Some(Image::from_bytes(get_red_icon()).unwrap())); } fn update_spn_ui_state(menu: Menu, enabled: bool) { From 83ec18f552cca4a7ef62f381f0f5f631243e740b Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 11 Sep 2024 18:52:36 +0300 Subject: [PATCH 10/11] [WIP] Updater support for windows --- cmds/portmaster-core/main.go | 94 +-------- cmds/portmaster-core/main_linux.go | 100 ++++++++++ cmds/portmaster-core/main_windows.go | 180 ++++++++++++++++++ desktop/tauri/src-tauri/src/main.rs | 1 + desktop/tauri/src-tauri/tauri.conf.json5 | 23 ++- .../interception/interception_windows.go | 7 +- service/firewall/interception/module.go | 5 +- .../interception/windowskext2/kext.go | 3 + service/instance.go | 60 ++++-- service/updates/registry/bundle.go | 9 + service/updates/registry/registry.go | 4 +- 11 files changed, 371 insertions(+), 115 deletions(-) create mode 100644 cmds/portmaster-core/main_linux.go create mode 100644 cmds/portmaster-core/main_windows.go diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index bc9f4f28a..630f58278 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -8,14 +8,11 @@ import ( "io" "log/slog" "os" - "os/signal" "runtime" "runtime/pprof" "syscall" - "time" "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service" "github.com/safing/portmaster/service/mgr" @@ -36,6 +33,11 @@ func init() { } func main() { + instance := initialize() + run(instance) +} + +func initialize() *service.Instance { flag.Parse() // set information @@ -80,91 +82,7 @@ func main() { } os.Exit(0) } - - // Set default log level. - log.SetLogLevel(log.WarningLevel) - _ = log.Start() - - // Start - go func() { - err = instance.Start() - if err != nil { - fmt.Printf("instance start failed: %s\n", err) - - // Print stack on start failure, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") - } - - os.Exit(1) - } - }() - - // Wait for signal. - signalCh := make(chan os.Signal, 1) - if enableInputSignals { - go inputSignals(signalCh) - } - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - sigUSR1, - ) - - select { - case sig := <-signalCh: - // Only print and continue to wait if SIGUSR1 - if sig == sigUSR1 { - printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") - } else { - fmt.Println(" ") // CLI output. - slog.Warn("program was interrupted, stopping") - } - - case <-instance.Stopped(): - log.Shutdown() - os.Exit(instance.ExitCode()) - } - - // Catch signals during shutdown. - // Rapid unplanned disassembly after 5 interrupts. - go func() { - forceCnt := 5 - for { - <-signalCh - forceCnt-- - if forceCnt > 0 { - fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) - } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") - os.Exit(1) - } - } - }() - - // Rapid unplanned disassembly after 3 minutes. - go func() { - time.Sleep(3 * time.Minute) - printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") - os.Exit(1) - }() - - // Stop instance. - if err := instance.Stop(); err != nil { - slog.Error("failed to stop", "err", err) - } - log.Shutdown() - - // Print stack on shutdown, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON EXIT") - } - - os.Exit(instance.ExitCode()) + return instance } func printStackTo(writer io.Writer, msg string) { diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go new file mode 100644 index 000000000..aa8d9a60c --- /dev/null +++ b/cmds/portmaster-core/main_linux.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service" +) + +func run(instance *service.Instance) { + // Set default log level. + log.SetLogLevel(log.WarningLevel) + _ = log.Start() + + // Start + go func() { + err := instance.Start() + if err != nil { + fmt.Printf("instance start failed: %s\n", err) + + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + } + + os.Exit(1) + } + }() + + // Wait for signal. + signalCh := make(chan os.Signal, 1) + if enableInputSignals { + go inputSignals(signalCh) + } + signal.Notify( + signalCh, + os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + sigUSR1, + ) + + select { + case sig := <-signalCh: + // Only print and continue to wait if SIGUSR1 + if sig == sigUSR1 { + printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") + } else { + fmt.Println(" ") // CLI output. + slog.Warn("program was interrupted, stopping") + } + + case <-instance.Stopped(): + log.Shutdown() + os.Exit(instance.ExitCode()) + } + + // Catch signals during shutdown. + // Rapid unplanned disassembly after 5 interrupts. + go func() { + forceCnt := 5 + for { + <-signalCh + forceCnt-- + if forceCnt > 0 { + fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) + } else { + printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + os.Exit(1) + } + } + }() + + // Rapid unplanned disassembly after 3 minutes. + go func() { + time.Sleep(3 * time.Minute) + printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") + os.Exit(1) + }() + + // Stop instance. + if err := instance.Stop(); err != nil { + slog.Error("failed to stop", "err", err) + } + log.Shutdown() + + // Print stack on shutdown, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON EXIT") + } + + os.Exit(instance.ExitCode()) +} diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go new file mode 100644 index 000000000..61d4f8404 --- /dev/null +++ b/cmds/portmaster-core/main_windows.go @@ -0,0 +1,180 @@ +package main + +// Based on the official Go examples from +// https://github.com/golang/sys/blob/master/windows/svc/example +// by The Go Authors. +// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory. + +import ( + "fmt" + "log/slog" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" +) + +var ( + // wait groups + runWg sync.WaitGroup + finishWg sync.WaitGroup +) + +const serviceName = "PortmasterCore" + +type windowsService struct { + instance *service.Instance +} + +func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + + startupComplete := make(chan struct{}) + go func() { + for !ws.instance.Ready() { + time.Sleep(1 * time.Second) + } + startupComplete <- struct{}{} + }() + +service: + for { + select { + case <-startupComplete: + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + case <-ws.instance.Stopped(): + changes <- svc.Status{State: svc.StopPending} + break service + case c := <-changeRequests: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + ws.instance.Shutdown() + default: + log.Errorf("unexpected control request: #%d\n", c) + } + } + } + + // wait until everything else is finished + finishWg.Wait() + + log.Shutdown() + + // send stopped status + changes <- svc.Status{State: svc.Stopped} + // wait a little for the status to reach Windows + time.Sleep(100 * time.Millisecond) + + return ssec, errno +} + +func run(instance *service.Instance) error { + log.SetLogLevel(log.WarningLevel) + _ = log.Start() + // check if we are running interactively + isService, err := svc.IsWindowsService() + if err != nil { + return fmt.Errorf("could not determine if running interactively: %s", err) + } + // select service run type + svcRun := svc.Run + if !isService { + log.Warningf("running interactively, switching to debug execution (no real service).\n") + svcRun = debug.Run + go registerSignalHandler(instance) + } + + runWg.Add(2) + + // run service client + go func() { + sErr := svcRun(serviceName, &windowsService{ + instance: instance, + }) + if sErr != nil { + log.Infof("shuting down service with error: %s", sErr) + } else { + log.Infof("shuting down service") + } + instance.Shutdown() + runWg.Done() + }() + + finishWg.Add(1) + // run service + go func() { + // run slightly delayed + time.Sleep(250 * time.Millisecond) + instance.Start() + + if err != nil { + fmt.Printf("instance start failed: %s\n", err) + + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + } + + } + runWg.Done() + finishWg.Done() + }() + + runWg.Wait() + + return err +} + +func registerSignalHandler(instance *service.Instance) { + // Wait for signal. + signalCh := make(chan os.Signal, 1) + if enableInputSignals { + go inputSignals(signalCh) + } + signal.Notify( + signalCh, + os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + sigUSR1, + ) + + select { + case sig := <-signalCh: + // Only print and continue to wait if SIGUSR1 + if sig == sigUSR1 { + printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") + } else { + fmt.Println(" ") // CLI output. + slog.Warn("program was interrupted, stopping") + instance.Shutdown() + } + } + + // Catch signals during shutdown. + // Rapid unplanned disassembly after 5 interrupts. + go func() { + forceCnt := 5 + for { + <-signalCh + forceCnt-- + if forceCnt > 0 { + fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) + } else { + printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + os.Exit(1) + } + } + }() +} diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 7d605021b..7dd9df423 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -120,6 +120,7 @@ fn show_webview_not_installed_dialog() -> i32 { } fn main() { + env::set_var("GDK_BACKEND", "x11"); if tauri::webview_version().is_err() { std::process::exit(show_webview_not_installed_dialog()); } diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index beba5cd79..a2d11d8ad 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -60,6 +60,17 @@ "desktopTemplate": "../../../packaging/linux/portmaster.desktop", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", + "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", + "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", @@ -73,7 +84,17 @@ "release": "1", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", + "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", @@ -106,4 +127,4 @@ "../../../assets/data/icons/pm_light.ico" ] } -} \ No newline at end of file +} diff --git a/service/firewall/interception/interception_windows.go b/service/firewall/interception/interception_windows.go index cb97376b7..3f9df6f8b 100644 --- a/service/firewall/interception/interception_windows.go +++ b/service/firewall/interception/interception_windows.go @@ -10,14 +10,13 @@ import ( "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network" "github.com/safing/portmaster/service/network/packet" - "github.com/safing/portmaster/service/updates" ) var useOldKext = false // start starts the interception. func startInterception(packets chan packet.Packet) error { - kextFile, err := updates.GetPlatformFile("kext/portmaster-kext.sys") + kextFile, err := module.instance.BinaryUpdates().GetFile("portmaster-kext.sys") if err != nil { return fmt.Errorf("interception: could not get kext sys: %s", err) } @@ -77,7 +76,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) @@ -95,7 +93,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) @@ -112,7 +109,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) } @@ -159,5 +155,4 @@ func GetKextVersion() (string, error) { } return version.String(), nil } - } diff --git a/service/firewall/interception/module.go b/service/firewall/interception/module.go index 072eb3b5e..158432e04 100644 --- a/service/firewall/interception/module.go +++ b/service/firewall/interception/module.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/updates" ) // Interception is the packet interception module. @@ -97,4 +98,6 @@ func New(instance instance) (*Interception, error) { return module, nil } -type instance interface{} +type instance interface { + BinaryUpdates() *updates.Updates +} diff --git a/service/firewall/interception/windowskext2/kext.go b/service/firewall/interception/windowskext2/kext.go index ed15476ee..43be43cd9 100644 --- a/service/firewall/interception/windowskext2/kext.go +++ b/service/firewall/interception/windowskext2/kext.go @@ -62,6 +62,9 @@ func GetKextServiceHandle() windows.Handle { // Stop intercepting. func Stop() error { + if kextFile == nil { + return fmt.Errorf("kextfile is nil") + } // Prepare kernel for shutdown err := shutdownRequest() if err != nil { diff --git a/service/instance.go b/service/instance.go index 43c7562b2..8f63a78b7 100644 --- a/service/instance.go +++ b/service/instance.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + go_runtime "runtime" "sync/atomic" "time" @@ -47,23 +48,6 @@ import ( "github.com/safing/portmaster/spn/terminal" ) -var binaryUpdateIndex = registry.UpdateIndex{ - Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/lib/portmaster/new_bin", - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://localhost:8000/test-binary.json"}, - IndexFile: "bin-index.json", - AutoApply: false, -} - -var intelUpdateIndex = registry.UpdateIndex{ - Directory: "/var/lib/portmaster/intel", - DownloadDirectory: "/var/lib/portmaster/new_intel", - IndexURLs: []string{"http://localhost:8000/test-intel.json"}, - IndexFile: "intel-index.json", - AutoApply: true, -} - // Instance is an instance of a Portmaster service. type Instance struct { ctx context.Context @@ -121,6 +105,48 @@ type Instance struct { // New returns a new Portmaster service instance. func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx + var binaryUpdateIndex registry.UpdateIndex + var intelUpdateIndex registry.UpdateIndex + if go_runtime.GOOS == "windows" { + binaryUpdateIndex = registry.UpdateIndex{ + Directory: "C:/Program Files/Portmaster/binary", + DownloadDirectory: "C:/Program Files/Portmaster/new_binary", + PurgeDirectory: "C:/Program Files/Portmaster/old_binary", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + intelUpdateIndex = registry.UpdateIndex{ + Directory: "C:/Program Files/Portmaster/intel", + DownloadDirectory: "C:/Program Files/Portmaster/new_intel", + PurgeDirectory: "C:/Program Files/Portmaster/old_intel", + IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, + } + } else if go_runtime.GOOS == "linux" { + binaryUpdateIndex = registry.UpdateIndex{ + Directory: "/usr/lib/portmaster", + DownloadDirectory: "/var/lib/portmaster/new_bin", + PurgeDirectory: "/var/lib/portmaster/old_bin", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://localhost:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + intelUpdateIndex = registry.UpdateIndex{ + Directory: "/var/lib/portmaster/intel", + DownloadDirectory: "/var/lib/portmaster/new_intel", + PurgeDirectory: "/var/lib/portmaster/intel_bin", + IndexURLs: []string{"http://localhost:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, + } + } + // Create instance to pass it to modules. instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) diff --git a/service/updates/registry/bundle.go b/service/updates/registry/bundle.go index 249539666..4df887ada 100644 --- a/service/updates/registry/bundle.go +++ b/service/updates/registry/bundle.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "time" "github.com/safing/portmaster/base/log" @@ -19,6 +20,8 @@ import ( const MaxUnpackSize = 1 << 30 // 2^30 == 1GB +const current_platform = runtime.GOOS + "_" + runtime.GOARCH + type Artifact struct { Filename string `json:"Filename"` SHA256 string `json:"SHA256"` @@ -107,6 +110,11 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } func processArtifact(client *http.Client, artifact Artifact, filePath string) error { + // Skip artifacts not meant for this machine. + if artifact.Platform != "" && artifact.Platform != current_platform { + return nil + } + providedHash, err := hex.DecodeString(artifact.SHA256) if err != nil || len(providedHash) != sha256.Size { return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) @@ -149,6 +157,7 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er if err != nil { return fmt.Errorf("failed to write to file: %w", err) } + file.Close() // Rename err = os.Rename(tmpFilename, filePath) diff --git a/service/updates/registry/registry.go b/service/updates/registry/registry.go index f0e77adb1..f670df26f 100644 --- a/service/updates/registry/registry.go +++ b/service/updates/registry/registry.go @@ -125,7 +125,7 @@ func (reg *Registry) DownloadUpdates() error { // ApplyUpdates removes the current binary folder and replaces it with the downloaded one. func (reg *Registry) ApplyUpdates() error { // Create purge dir. - err := os.MkdirAll(filepath.Dir(reg.updateIndex.PurgeDirectory), defaultDirMode) + err := os.MkdirAll(reg.updateIndex.PurgeDirectory, defaultDirMode) if err != nil { return fmt.Errorf("failed to create directory: %w", err) } @@ -198,7 +198,7 @@ func deleteUnfinishedDownloads(rootDir string) error { // Check if the current file has the specified extension if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { - log.Warningf("updates deleting unfinished: %s\n", path) + log.Warningf("updates: deleting unfinished: %s\n", path) err := os.Remove(path) if err != nil { return fmt.Errorf("failed to delete file %s: %w", path, err) From 84a15b5fb30ee6bead52ee46848e742bb8cce4fc Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 17 Sep 2024 12:36:44 +0300 Subject: [PATCH 11/11] [WIP] updater rafactoring, minor improvments --- cmds/portmaster-core/main_windows.go | 56 ++--- desktop/tauri/src-tauri/README.md | 2 + desktop/tauri/src-tauri/tauri.conf.json5 | 4 +- desktop/tauri/src-tauri/templates/files.wxs | 36 +++ desktop/tauri/src-tauri/templates/main.wxs | 10 +- desktop/tauri/src-tauri/templates/service.wxs | 2 +- service/instance.go | 50 +++-- service/intel/filterlists/database.go | 10 +- service/intel/filterlists/index.go | 4 +- service/intel/filterlists/updater.go | 10 +- service/intel/geoip/database.go | 6 +- service/ui/serve.go | 4 +- service/updates/{registry => }/bundle.go | 75 ++++++- service/updates/{registry => }/index.go | 4 +- service/updates/module.go | 129 ++++++++--- service/updates/registry/registry.go | 210 ------------------ service/updates/updater.go | 78 +++++++ spn/captain/intel.go | 4 +- 18 files changed, 382 insertions(+), 312 deletions(-) create mode 100644 desktop/tauri/src-tauri/templates/files.wxs rename service/updates/{registry => }/bundle.go (75%) rename service/updates/{registry => }/index.go (93%) delete mode 100644 service/updates/registry/registry.go create mode 100644 service/updates/updater.go diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go index 61d4f8404..9b3798c3e 100644 --- a/cmds/portmaster-core/main_windows.go +++ b/cmds/portmaster-core/main_windows.go @@ -35,20 +35,12 @@ type windowsService struct { func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown changes <- svc.Status{State: svc.StartPending} - - startupComplete := make(chan struct{}) - go func() { - for !ws.instance.Ready() { - time.Sleep(1 * time.Second) - } - startupComplete <- struct{}{} - }() + ws.instance.Start() + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} service: for { select { - case <-startupComplete: - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} case <-ws.instance.Stopped(): changes <- svc.Status{State: svc.StopPending} break service @@ -59,13 +51,13 @@ service: case svc.Stop, svc.Shutdown: ws.instance.Shutdown() default: - log.Errorf("unexpected control request: #%d\n", c) + log.Errorf("unexpected control request: #%d", c) } } } // wait until everything else is finished - finishWg.Wait() + // finishWg.Wait() log.Shutdown() @@ -88,12 +80,12 @@ func run(instance *service.Instance) error { // select service run type svcRun := svc.Run if !isService { - log.Warningf("running interactively, switching to debug execution (no real service).\n") + log.Warningf("running interactively, switching to debug execution (no real service).") svcRun = debug.Run go registerSignalHandler(instance) } - runWg.Add(2) + runWg.Add(1) // run service client go func() { @@ -105,29 +97,27 @@ func run(instance *service.Instance) error { } else { log.Infof("shuting down service") } - instance.Shutdown() runWg.Done() }() - finishWg.Add(1) + // finishWg.Add(1) // run service - go func() { - // run slightly delayed - time.Sleep(250 * time.Millisecond) - instance.Start() - - if err != nil { - fmt.Printf("instance start failed: %s\n", err) - - // Print stack on start failure, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") - } - - } - runWg.Done() - finishWg.Done() - }() + // go func() { + // // run slightly delayed + // time.Sleep(250 * time.Millisecond) + + // if err != nil { + // fmt.Printf("instance start failed: %s\n", err) + + // // Print stack on start failure, if enabled. + // if printStackOnExit { + // printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + // } + + // } + // runWg.Done() + // finishWg.Done() + // }() runWg.Wait() diff --git a/desktop/tauri/src-tauri/README.md b/desktop/tauri/src-tauri/README.md index ad2388106..a057e2851 100644 --- a/desktop/tauri/src-tauri/README.md +++ b/desktop/tauri/src-tauri/README.md @@ -33,7 +33,9 @@ Update WIX installer template: 2. Replace the contents of `templates/main_original.wxs` with the repository version. 3. Replace the contents of `templates/main.wsx` and add the fallowing lines at the end of the file, inside the `Product` tag. ```xml + + ``` diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index a2d11d8ad..2d9de0d7e 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -109,8 +109,10 @@ }, "wix": { "fragmentPaths": [ - "templates/service.wxs" + "templates/service.wxs", + "templates/files.wxs" ], + "componentGroupRefs": ["BinaryAndIntelFiles"], "template": "templates/main.wxs" } }, diff --git a/desktop/tauri/src-tauri/templates/files.wxs b/desktop/tauri/src-tauri/templates/files.wxs new file mode 100644 index 000000000..0eec08f1e --- /dev/null +++ b/desktop/tauri/src-tauri/templates/files.wxs @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/tauri/src-tauri/templates/main.wxs b/desktop/tauri/src-tauri/templates/main.wxs index 249eccf06..f42ba777f 100644 --- a/desktop/tauri/src-tauri/templates/main.wxs +++ b/desktop/tauri/src-tauri/templates/main.wxs @@ -341,14 +341,16 @@ {{/if}} - - - - AUTOLAUNCHAPP AND NOT Installed + + + + + + \ No newline at end of file diff --git a/desktop/tauri/src-tauri/templates/service.wxs b/desktop/tauri/src-tauri/templates/service.wxs index 31ff064c2..52f5339e0 100644 --- a/desktop/tauri/src-tauri/templates/service.wxs +++ b/desktop/tauri/src-tauri/templates/service.wxs @@ -3,7 +3,7 @@