Compare commits
10 Commits
4905dde4c3
...
3a7956f358
Author | SHA1 | Date | |
---|---|---|---|
3a7956f358 | |||
09995db974 | |||
296463319c | |||
f440c25210 | |||
3aed9867eb | |||
3052a1861f | |||
780be584e6 | |||
f9561f2883 | |||
a79e3a8d3b | |||
3888e95a71 |
467
Cargo.lock
generated
467
Cargo.lock
generated
@ -375,6 +375,27 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@ -527,6 +548,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.6"
|
||||
@ -605,6 +632,40 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset 0.8.0",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.15"
|
||||
@ -614,6 +675,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@ -785,6 +852,12 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.21.0"
|
||||
@ -879,6 +952,22 @@ version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"flume",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@ -898,6 +987,25 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.10.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"nanorand",
|
||||
"pin-project",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@ -1016,8 +1124,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git-version"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899"
|
||||
dependencies = [
|
||||
"git-version-macro",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git-version-macro"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1107,6 +1249,15 @@ dependencies = [
|
||||
"gl_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@ -1144,6 +1295,25 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@ -1214,6 +1384,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.61"
|
||||
@ -1258,6 +1437,12 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.140"
|
||||
@ -1300,6 +1485,21 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line-wrap"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
|
||||
dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.1.4"
|
||||
@ -1373,6 +1573,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@ -1400,6 +1609,15 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanorand"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.7.0"
|
||||
@ -1484,6 +1702,36 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.15.0"
|
||||
@ -1576,6 +1824,28 @@ version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||
|
||||
[[package]]
|
||||
name = "onig"
|
||||
version = "6.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"onig_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onig_sys"
|
||||
version = "69.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.83"
|
||||
@ -1686,6 +1956,26 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
@ -1704,6 +1994,20 @@ version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"indexmap",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.7"
|
||||
@ -1748,6 +2052,12 @@ dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.20+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.53"
|
||||
@ -1757,6 +2067,24 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5c1a97b1bc42b1d550bfb48d4262153fe400a12bab1511821736f7eac76d7e2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.26"
|
||||
@ -1802,6 +2130,28 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f851a03551ceefd30132e447f07f96cb7011d6b658374f3aed847333adb5559"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@ -1882,6 +2232,12 @@ version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -2015,6 +2371,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.8"
|
||||
@ -2095,6 +2457,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh2"
|
||||
version = "0.9.4"
|
||||
@ -2147,6 +2518,29 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syntect"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags",
|
||||
"flate2",
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"once_cell",
|
||||
"onig",
|
||||
"plist",
|
||||
"regex-syntax",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.4.0"
|
||||
@ -2180,6 +2574,44 @@ dependencies = [
|
||||
"syn 2.0.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.8.3"
|
||||
@ -2331,16 +2763,21 @@ dependencies = [
|
||||
"directories-next",
|
||||
"eframe",
|
||||
"egui",
|
||||
"git-version",
|
||||
"hex",
|
||||
"image",
|
||||
"lazy-regex",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shell-escape",
|
||||
"smol",
|
||||
"ssh2",
|
||||
"syntect",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2397,6 +2834,12 @@ version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a449064fee414fcc201356a3e6c1510f6c8829ed28bb06b91c54ebe208ce065"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
@ -2603,6 +3046,12 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -2843,6 +3292,15 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "3.11.1"
|
||||
@ -2908,6 +3366,15 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "3.12.0"
|
||||
|
@ -11,13 +11,18 @@ async-ssh2-lite = { version = "0.4.5", features = ["async-io"] }
|
||||
directories-next = "2.0.0"
|
||||
eframe = "0.21.3"
|
||||
egui = "0.21.0"
|
||||
git-version = "0.3.5"
|
||||
hex = "0.4.3"
|
||||
image = "0.24.6"
|
||||
lazy-regex = "2.5.0"
|
||||
lazy_static = "1.4.0"
|
||||
serde = { version = "1.0.158", features = ["derive"] }
|
||||
serde_json = "1.0.94"
|
||||
shell-escape = "0.1.5"
|
||||
smol = "1.3.0"
|
||||
ssh2 = "0.9.4"
|
||||
syntect = "5.0.0"
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.7.3"
|
||||
version = "3.0.0"
|
||||
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# ubusman
|
||||
|
||||
Kind of like [postman](https://www.postman.com/), but for [ubus](https://openwrt.org/docs/techref/ubus).
|
593
src/app.rs
593
src/app.rs
@ -1,21 +1,602 @@
|
||||
use std::{
|
||||
net::{SocketAddr, SocketAddrV4},
|
||||
sync::{mpsc::{Receiver, Sender}, Arc},
|
||||
vec, rc::Rc, time::SystemTime,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
|
||||
use eframe::CreationContext;
|
||||
use egui::{text::LayoutJob, Color32, ColorImage, TextureHandle};
|
||||
use lazy_regex::regex_replace_all;
|
||||
use serde_json::Value;
|
||||
use lazy_static::lazy_static;
|
||||
use tokio::task::JoinHandle;
|
||||
use git_version::git_version;
|
||||
use version::version;
|
||||
|
||||
use crate::ubus::{self, escape_json};
|
||||
|
||||
const ERROR_COLOR: Color32 = Color32::from_rgb(180, 20, 20);
|
||||
const SUCCESS_COLOR: Color32 = Color32::from_rgb(20, 150, 20);
|
||||
lazy_static! {
|
||||
pub static ref COPY_ICON: ColorImage = load_image_from_memory(include_bytes!("./copy.png"))
|
||||
.expect("Failed to load copy icon") as ColorImage;
|
||||
}
|
||||
|
||||
const SOURCE_CODE_URL: &str = "https://example.com";
|
||||
const VERSION: &str = version!();
|
||||
const GIT_VERSION: &str = git_version!(args = ["--always", "--dirty=*"]);
|
||||
|
||||
pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
|
||||
let image = image::load_from_memory(image_data)?;
|
||||
let size = [image.width() as _, image.height() as _];
|
||||
let image_buffer = image.to_rgba8();
|
||||
let pixels = image_buffer.as_flat_samples();
|
||||
Ok(ColorImage::from_rgba_unmultiplied(
|
||||
size,
|
||||
pixels.as_slice(),
|
||||
))
|
||||
}
|
||||
|
||||
pub enum AsyncEvent {
|
||||
Connect(Result<AsyncSession<AsyncIoTcpStream>>),
|
||||
Disconnect(Result<()>),
|
||||
ListObjects(Result<Vec<ubus::Object>>),
|
||||
Call(Result<Value>)
|
||||
}
|
||||
|
||||
pub struct AppSettings {
|
||||
address: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
password: String,
|
||||
|
||||
show_object_ids: bool,
|
||||
connect_on_start: bool
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
settings: AppSettings,
|
||||
session: Option<AsyncSession<AsyncIoTcpStream>>,
|
||||
ubus_call_handle: Option<JoinHandle<()>>,
|
||||
last_ubus_call_at: SystemTime,
|
||||
|
||||
selected_object: Option<Rc<ubus::Object>>,
|
||||
selected_method: Option<String>,
|
||||
object_filter: String,
|
||||
objects: Vec<Rc<ubus::Object>>,
|
||||
payload: String,
|
||||
response: Option<Result<Value>>,
|
||||
|
||||
is_connecting: bool,
|
||||
is_disconnecting: bool,
|
||||
settings_open: bool,
|
||||
|
||||
tx: Sender<AsyncEvent>,
|
||||
rx: Receiver<AsyncEvent>,
|
||||
|
||||
copy_texture: Option<TextureHandle>
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
Self {
|
||||
settings: AppSettings {
|
||||
address: "172.24.224.1".into(), //"192.168.1.1".to_owned(),
|
||||
port: 22,
|
||||
username: "root".to_owned(),
|
||||
password: "admin01".to_owned(),
|
||||
show_object_ids: false,
|
||||
connect_on_start: true
|
||||
},
|
||||
session: None,
|
||||
ubus_call_handle: None,
|
||||
last_ubus_call_at: SystemTime::UNIX_EPOCH,
|
||||
|
||||
object_filter: "".into(),
|
||||
selected_object: None,
|
||||
selected_method: None,
|
||||
payload: "".into(),
|
||||
response: None,
|
||||
objects: vec![],
|
||||
|
||||
is_connecting: false,
|
||||
is_disconnecting: false,
|
||||
settings_open: false,
|
||||
|
||||
tx,
|
||||
rx,
|
||||
|
||||
copy_texture: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect<A>(
|
||||
socket_addr: A,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<AsyncSession<AsyncIoTcpStream>>
|
||||
where
|
||||
A: Into<SocketAddr>,
|
||||
{
|
||||
let mut session = AsyncSession::<AsyncIoTcpStream>::connect(socket_addr, None).await?;
|
||||
session.handshake().await?;
|
||||
session.userauth_password(&username, &password).await?;
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
async fn disconnect(session: AsyncSession<AsyncIoTcpStream>) -> Result<()> {
|
||||
session
|
||||
.disconnect(
|
||||
Some(ssh2::DisconnectCode::ByApplication),
|
||||
"Disconnect",
|
||||
Some("en"),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_json_comments(text: &str) -> String {
|
||||
let text = regex_replace_all!(r#"/\*(.|\n)*?\*/"#, &text, |_, _| ""); // Multi line comments
|
||||
let text = regex_replace_all!(r#"//.*\n?"#, &text, |_| ""); // Single line comments
|
||||
let text = regex_replace_all!(r#"(,)(\s*[\]}])"#, &text, |_, _, rest: &str| rest.to_string()); // Trailing commas
|
||||
|
||||
text.into()
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App;
|
||||
fn json_layouter(ui: &egui::Ui, string: &str, wrap_width: f32) -> Arc<egui::Galley> {
|
||||
let mut layout_job = crate::syntax_highlighting::highlight(ui.ctx(), string, "json");
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn init(&mut self, _cc: &CreationContext) {
|
||||
pub fn init(&mut self, cc: &CreationContext) {
|
||||
if self.settings.connect_on_start {
|
||||
let username = &self.settings.username;
|
||||
let password = &self.settings.password;
|
||||
if username.is_empty() || password.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let address = self.settings.address.parse();
|
||||
if address.is_err() {
|
||||
return;
|
||||
}
|
||||
let address = address.unwrap();
|
||||
let port = self.settings.port;
|
||||
|
||||
let socket_addr = SocketAddrV4::new(address, port);
|
||||
self.start_connect(socket_addr, username.clone(), password.clone());
|
||||
}
|
||||
|
||||
|
||||
|
||||
self.copy_texture = Some(cc.egui_ctx.load_texture(
|
||||
"clipboard",
|
||||
COPY_ICON.clone(),
|
||||
Default::default()
|
||||
));
|
||||
}
|
||||
|
||||
pub fn get_selected_object(&self) -> Option<&str> {
|
||||
self.selected_object.as_ref().map(|obj| obj.name.as_str())
|
||||
}
|
||||
|
||||
pub fn get_selected_method(&self) -> Option<&str> {
|
||||
self.selected_method.as_ref().map(|method| method.as_str())
|
||||
}
|
||||
|
||||
pub fn get_payload(&self) -> serde_json::Result<Value> {
|
||||
let stripped_payload = remove_json_comments(&self.payload);
|
||||
serde_json::from_str(&stripped_payload)
|
||||
}
|
||||
|
||||
pub fn is_ubus_call_in_progress(&self) -> bool {
|
||||
if let Some(handle) = &self.ubus_call_handle {
|
||||
return !handle.is_finished();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn handle_events(&mut self, _ctx: &egui::Context) {
|
||||
use AsyncEvent::*;
|
||||
|
||||
if let Ok(event) = self.rx.try_recv() {
|
||||
match event {
|
||||
Connect(result) => {
|
||||
self.is_connecting = false;
|
||||
match result {
|
||||
Ok(session) => {
|
||||
self.session = Some(session);
|
||||
self.start_list_objects()
|
||||
}
|
||||
Err(err) => todo!("{}", err),
|
||||
}
|
||||
}
|
||||
|
||||
Disconnect(result) => {
|
||||
self.is_disconnecting = false;
|
||||
|
||||
if let Err(err) = result {
|
||||
todo!("{}", err)
|
||||
}
|
||||
}
|
||||
|
||||
ListObjects(result) => match result {
|
||||
Ok(objects) => self.objects = objects.into_iter().map(Rc::new).collect(),
|
||||
Err(err) => todo!("{}", err),
|
||||
},
|
||||
|
||||
Call(result) => {
|
||||
self.response = Some(result);
|
||||
self.ubus_call_handle = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_connect<A>(&mut self, socket_addr: A, username: String, password: String)
|
||||
where
|
||||
A: Into<SocketAddr>,
|
||||
{
|
||||
if self.session.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.is_connecting = true;
|
||||
let tx = self.tx.clone();
|
||||
let socket_addr = socket_addr.into();
|
||||
tokio::spawn(async move {
|
||||
let result = connect(socket_addr, username, password).await;
|
||||
tx.send(AsyncEvent::Connect(result))
|
||||
.expect("Failed to send event");
|
||||
});
|
||||
}
|
||||
|
||||
fn start_disconnect(&mut self) {
|
||||
if self.session.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.is_disconnecting = true;
|
||||
let tx = self.tx.clone();
|
||||
let session = self.session.clone().unwrap();
|
||||
self.session = None;
|
||||
tokio::spawn(async move {
|
||||
let result = disconnect(session).await;
|
||||
tx.send(AsyncEvent::Disconnect(result))
|
||||
.expect("Failed to send event");
|
||||
});
|
||||
}
|
||||
|
||||
fn start_call(&mut self, object: String, method: String, message: Option<Value>) {
|
||||
if self.session.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tx = self.tx.clone();
|
||||
let session = self.session.clone().unwrap();
|
||||
let handle = tokio::spawn(async move {
|
||||
let result = ubus::call(&session, &object, &method, message.as_ref()).await;
|
||||
tx.send(AsyncEvent::Call(result)).expect("Failed to send event");
|
||||
});
|
||||
self.ubus_call_handle = Some(handle);
|
||||
self.last_ubus_call_at = SystemTime::now()
|
||||
}
|
||||
|
||||
fn start_list_objects(&self) {
|
||||
if self.session.is_none() {
|
||||
return;
|
||||
}
|
||||
let session = self.session.clone().unwrap();
|
||||
|
||||
let tx = self.tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = ubus::list_verbose(&session, None).await;
|
||||
tx.send(AsyncEvent::ListObjects(result))
|
||||
.expect("Failed to send event");
|
||||
});
|
||||
}
|
||||
|
||||
fn show_left_panel(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::*;
|
||||
|
||||
let mut match_exists = false;
|
||||
ui.text_edit_singleline(&mut self.object_filter);
|
||||
ui.add_space(8.0);
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
for obj in self.objects.iter() {
|
||||
let shown_methods;
|
||||
if obj.name.contains(&self.object_filter) {
|
||||
shown_methods = obj.methods.iter()
|
||||
.by_ref()
|
||||
.collect::<Vec<_>>();
|
||||
} else {
|
||||
shown_methods = obj.methods.iter()
|
||||
.filter(|(name, _)| name.contains(&self.object_filter))
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
if shown_methods.len() > 0 {
|
||||
match_exists = true;
|
||||
let style = ui.style();
|
||||
let mut text = LayoutJob::default();
|
||||
text.append(&obj.name, 0.0, TextFormat {
|
||||
..TextFormat::default()
|
||||
});
|
||||
if self.settings.show_object_ids {
|
||||
text.append(&format!("@{:x}", obj.id), 10.0, TextFormat {
|
||||
color: style.noninteractive().fg_stroke.color,
|
||||
italics: true,
|
||||
..TextFormat::default()
|
||||
});
|
||||
}
|
||||
CollapsingHeader::new(text).show(ui, |ui| {
|
||||
for (name, method_params) in &shown_methods {
|
||||
if ui.selectable_label(false, name).clicked() {
|
||||
self.selected_object = Some(obj.clone());
|
||||
self.selected_method = Some(name.clone());
|
||||
let method_params = method_params.iter().map(|(s, p)| (s.as_str(), *p)).collect::<Vec<_>>();
|
||||
self.payload = App::create_default_payload(&method_params);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !match_exists {
|
||||
ui.label("no matches :(");
|
||||
}
|
||||
}
|
||||
|
||||
fn show_settings(ui: &mut egui::Ui, settings: &mut AppSettings) {
|
||||
ui.checkbox(&mut settings.show_object_ids, "Show object IDs");
|
||||
ui.checkbox(&mut settings.connect_on_start, "Connect on start");
|
||||
ui.add_space(16.0);
|
||||
ui.label(format!("Version: {}-{}", VERSION, GIT_VERSION));
|
||||
ui.label("Author: Rokas Puzonas");
|
||||
if ui.link("Source code").clicked() {
|
||||
ui.output_mut(|o| o.open_url(SOURCE_CODE_URL));
|
||||
}
|
||||
}
|
||||
|
||||
fn show_right_panel(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::*;
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.label("Address:");
|
||||
ui.text_edit_singleline(&mut self.settings.address);
|
||||
// TODO: ui.text_edit_singleline(&mut self.port);
|
||||
ui.label("Username:");
|
||||
ui.text_edit_singleline(&mut self.settings.username);
|
||||
ui.label("Password:");
|
||||
ui.text_edit_singleline(&mut self.settings.password);
|
||||
ui.add_space(8.0);
|
||||
if self.is_connecting {
|
||||
ui.add_enabled(false, Button::new("Connecting..."));
|
||||
} else if self.session.is_none() {
|
||||
if ui.button("Connect").clicked() {
|
||||
let socket_addr = SocketAddrV4::new(self.settings.address.parse().unwrap(), self.settings.port);
|
||||
self.start_connect(socket_addr, self.settings.username.clone(), self.settings.password.clone());
|
||||
}
|
||||
} else {
|
||||
if ui.button("Disconnect").clicked() {
|
||||
self.start_disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Settings").clicked() {
|
||||
self.settings_open = !self.settings_open;
|
||||
}
|
||||
let window = egui::Window::new("Settings")
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
|
||||
.open(&mut self.settings_open);
|
||||
window.show(ui.ctx(), |ui| App::show_settings(ui, &mut self.settings));
|
||||
|
||||
ui.add_space(32.0);
|
||||
ui.label("TODO: Add saving calls");
|
||||
ui.label("TODO: Add listen logs");
|
||||
ui.label("TODO: Add monitor logs");
|
||||
});
|
||||
}
|
||||
|
||||
fn create_default_payload(params: &[(&str, ubus::UbusParamType)]) -> String {
|
||||
let mut lines = vec![];
|
||||
for (param_name, param_type) in params {
|
||||
use ubus::UbusParamType::*;
|
||||
let param_value = match param_type {
|
||||
Unknown => "\"<unknown>\"",
|
||||
Integer => "0",
|
||||
Boolean => "false",
|
||||
Table => "{}",
|
||||
String => "\"\"",
|
||||
Array => "[]",
|
||||
Double => "0.00",
|
||||
};
|
||||
lines.push(format!("\t\"{}\": {}, // {}", ¶m_name, ¶m_value, param_type));
|
||||
}
|
||||
|
||||
return format!("{{\n{}\n}}", lines.join("\n"));
|
||||
}
|
||||
|
||||
fn display_response_textbox(ui: &mut egui::Ui, response: &Result<Value>) {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
let mut text = match response {
|
||||
Ok(Value::Null) => "Success!".into(),
|
||||
Ok(response) => serde_json::to_string_pretty(response).unwrap(),
|
||||
Err(err) => format!("Error: {}", err)
|
||||
};
|
||||
|
||||
let textbox = egui::TextEdit::multiline(&mut text)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(4)
|
||||
.lock_focus(true)
|
||||
.desired_width(f32::INFINITY);
|
||||
|
||||
match response {
|
||||
Ok(Value::Null) => {
|
||||
ui.add(textbox.text_color(SUCCESS_COLOR));
|
||||
},
|
||||
Err(_) => {
|
||||
ui.add(textbox.text_color(ERROR_COLOR));
|
||||
},
|
||||
Ok(_) => {
|
||||
ui.add(textbox.layouter(&mut json_layouter));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn show_central_panel(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::*;
|
||||
let time_since_last_call = self.last_ubus_call_at.elapsed().expect("Failed to get time since last ubus call");
|
||||
let call_in_progress = self.is_ubus_call_in_progress() && time_since_last_call.as_millis() >= 100;
|
||||
let call_enabled = self.selected_object.is_some() && self.selected_method.is_some();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled_ui(!call_in_progress, |ui| {
|
||||
// Object dropdown
|
||||
{
|
||||
let object_name = self.selected_object.as_ref().map(|obj| obj.name.as_ref()).unwrap_or("");
|
||||
let object_combobox = egui::ComboBox::from_id_source("selected_object")
|
||||
.selected_text(object_name)
|
||||
.width(200.0)
|
||||
.show_ui(ui, |ui| {
|
||||
let mut selection = None;
|
||||
for object in &self.objects {
|
||||
ui.selectable_value(&mut selection, Some(object.clone()), &object.name);
|
||||
}
|
||||
return selection;
|
||||
});
|
||||
|
||||
match object_combobox.inner {
|
||||
Some(Some(object)) => {
|
||||
self.selected_method = None;
|
||||
self.selected_object = Some(object);
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
// Method dropdown
|
||||
{
|
||||
let selected_method_name = self.selected_method.as_deref().unwrap_or("");
|
||||
let method_combobox = ui.add_enabled_ui(self.selected_object.is_some(), |ui| {
|
||||
let mut selection = None;
|
||||
egui::ComboBox::from_id_source("selected_method")
|
||||
.selected_text(selected_method_name)
|
||||
.width(200.0)
|
||||
.show_ui(ui, |ui| {
|
||||
if let Some(object) = &self.selected_object {
|
||||
for method in &object.methods {
|
||||
let method_name = &method.0;
|
||||
let mut label_response = ui.selectable_label(selected_method_name == method_name, method_name);
|
||||
if label_response.clicked() && selected_method_name != method_name {
|
||||
selection = Some(method_name.clone());
|
||||
label_response.mark_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return selection;
|
||||
});
|
||||
|
||||
match (method_combobox.inner, &self.selected_object) {
|
||||
(Some(method), Some(object)) => {
|
||||
let method_params = object.methods.iter()
|
||||
.find(|(name, _)| name.eq(&method))
|
||||
.map(|(_, params)| params)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(param_name, param_type)| (param_name.as_str(), *param_type))
|
||||
.collect::<Vec<_>>();
|
||||
self.payload = App::create_default_payload(&method_params);
|
||||
self.selected_method = Some(method);
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
if ui.add_enabled(call_enabled, Button::new("call")).clicked() {
|
||||
let object_name = self.get_selected_object().unwrap().into();
|
||||
let method_name = self.get_selected_method().unwrap().into();
|
||||
let payload = self.get_payload().unwrap(); // TODO: handle parsing error
|
||||
self.start_call(object_name, method_name, Some(payload));
|
||||
// TODO: Block sending other requests
|
||||
}
|
||||
});
|
||||
|
||||
let copy_icon = self.copy_texture.as_ref().expect("Copy icon not loaded");
|
||||
let copy_button = Button::image_and_text(copy_icon.id(), Vec2::new(10.0, 10.0), "copy");
|
||||
if ui.add_enabled(call_enabled, copy_button).clicked() {
|
||||
let object_name = self.get_selected_object().unwrap();
|
||||
let method_name = self.get_selected_method().unwrap();
|
||||
let payload = self.get_payload().unwrap(); // TODO: handle parsing error
|
||||
let cmd = format!("ubus call {} {} {}", object_name, method_name, escape_json(&payload));
|
||||
ui.output_mut(|o| o.copied_text = cmd);
|
||||
}
|
||||
|
||||
if call_in_progress {
|
||||
ui.spinner();
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
if let Some(response) = &self.response {
|
||||
egui::TopBottomPanel::bottom("bottom_panel")
|
||||
.resizable(true)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.add_space(10.0);
|
||||
App::display_response_textbox(ui, response);
|
||||
});
|
||||
}
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.show_inside(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.add_enabled(
|
||||
!call_in_progress,
|
||||
egui::TextEdit::multiline(&mut self.payload)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(10)
|
||||
.lock_focus(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.layouter(&mut json_layouter),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
use egui::*;
|
||||
self.handle_events(ctx);
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::SidePanel::left("left_panel")
|
||||
.resizable(true)
|
||||
.width_range(100.0..=300.0)
|
||||
.show_inside(ui, |ui| self.show_left_panel(ui));
|
||||
|
||||
egui::SidePanel::right("right_panel")
|
||||
.resizable(true)
|
||||
.width_range(125.0..=200.0)
|
||||
.show_inside(ui, |ui| self.show_right_panel(ui));
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.show(ctx, |ui| {
|
||||
ui.label("Hello World!");
|
||||
.show_inside(ui, |ui| self.show_central_panel(ui));
|
||||
});
|
||||
}
|
||||
}
|
65
src/async_line_reader.rs
Normal file
65
src/async_line_reader.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use std::net::TcpStream;
|
||||
use async_ssh2_lite::AsyncStream;
|
||||
use smol::{Async, io::AsyncReadExt};
|
||||
use anyhow::Result;
|
||||
|
||||
const INITIAL_CAPACITY: usize = 1024;
|
||||
|
||||
pub struct AsyncLineReader {
|
||||
stream: AsyncStream<Async<TcpStream>>,
|
||||
line_buffer: Vec<u8>,
|
||||
buffer_size: usize
|
||||
}
|
||||
|
||||
impl AsyncLineReader {
|
||||
pub fn new(stream: AsyncStream<Async<TcpStream>>) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
line_buffer: vec![0u8; INITIAL_CAPACITY],
|
||||
buffer_size: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_line(&mut self) -> Result<Vec<u8>> {
|
||||
let delim_pos = self.line_buffer[0..self.buffer_size]
|
||||
.iter()
|
||||
.position(|c| *c == b'\n');
|
||||
|
||||
if let Some(pos) = delim_pos {
|
||||
let line = self.line_buffer[0..pos].to_vec();
|
||||
|
||||
self.line_buffer.copy_within((pos+1)..self.buffer_size, 0);
|
||||
self.buffer_size -= pos;
|
||||
self.buffer_size -= 1;
|
||||
|
||||
return Ok(line);
|
||||
}
|
||||
|
||||
loop {
|
||||
// Double line buffer size if at capacity
|
||||
if self.buffer_size == self.line_buffer.len() {
|
||||
for _ in 0..=self.line_buffer.len() {
|
||||
self.line_buffer.push(0u8);
|
||||
}
|
||||
}
|
||||
|
||||
let n = self.stream.read(&mut self.line_buffer[self.buffer_size..]).await?;
|
||||
|
||||
let delim_pos = self.line_buffer[self.buffer_size..(self.buffer_size+n)]
|
||||
.iter()
|
||||
.position(|c| *c == b'\n')
|
||||
.map(|pos| pos + self.buffer_size);
|
||||
|
||||
self.buffer_size += n;
|
||||
if let Some(pos) = delim_pos {
|
||||
let line = self.line_buffer[0..pos].to_vec();
|
||||
|
||||
self.line_buffer.copy_within((pos+1)..self.buffer_size, 0);
|
||||
self.buffer_size -= pos;
|
||||
self.buffer_size -= 1;
|
||||
|
||||
return Ok(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/copy.png
Normal file
BIN
src/copy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
BIN
src/icon.png
Normal file
BIN
src/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
src/icon.xcf
Normal file
BIN
src/icon.xcf
Normal file
Binary file not shown.
68
src/main.rs
68
src/main.rs
@ -1,64 +1,46 @@
|
||||
#[cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use std::{net::ToSocketAddrs, thread};
|
||||
use anyhow::Result;
|
||||
use smol::{channel, block_on};
|
||||
use async_ssh2_lite::{AsyncSession};
|
||||
use app::App;
|
||||
|
||||
use crate::ubus::{Ubus, UbusEvent};
|
||||
use eframe::IconData;
|
||||
use egui::Vec2;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
mod app;
|
||||
mod ubus;
|
||||
mod async_line_reader;
|
||||
mod syntax_highlighting;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let address = "172.24.224.1:22";
|
||||
let username = "root";
|
||||
let password = "admin01";
|
||||
// TODO: Save config to file
|
||||
// TODO: call history
|
||||
|
||||
let mut session = AsyncSession::<async_ssh2_lite::AsyncIoTcpStream>::connect(
|
||||
address.to_socket_addrs()?.next().unwrap(),
|
||||
None,
|
||||
).await?;
|
||||
|
||||
session.handshake().await?;
|
||||
session.userauth_password(username, password).await?;
|
||||
|
||||
let ubus = Ubus::new(session);
|
||||
let (tx, rx) = channel::unbounded::<UbusEvent>();
|
||||
let listener = {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
println!("before listen");
|
||||
if let Err(err) = ubus.listen(&[], tx).await {
|
||||
dbg!(err);
|
||||
};
|
||||
println!("after listen");
|
||||
pub fn load_icon_from_memory(image_data: &[u8]) -> Result<IconData, image::ImageError> {
|
||||
let image = image::load_from_memory(image_data)?;
|
||||
Ok(IconData {
|
||||
rgba: image.to_rgba8().to_vec(),
|
||||
width: image.width(),
|
||||
height: image.height(),
|
||||
})
|
||||
};
|
||||
|
||||
loop {
|
||||
let e = rx.recv().await?;
|
||||
dbg!(e);
|
||||
}
|
||||
|
||||
/*
|
||||
let mut native_options = eframe::NativeOptions::default();
|
||||
native_options.decorated = true;
|
||||
native_options.resizable = true;
|
||||
fn main() {
|
||||
let rt = Runtime::new().expect("Unable to create Runtime");
|
||||
|
||||
let _enter = rt.enter();
|
||||
|
||||
let mut app = App::default();
|
||||
let mut options = eframe::NativeOptions::default();
|
||||
options.min_window_size = Some(Vec2::new(920.0, 500.0));
|
||||
let icon_data = load_icon_from_memory(include_bytes!("./icon.png")).expect("Failed to load icon data");
|
||||
options.icon_data = Some(icon_data);
|
||||
options.vsync = true;
|
||||
|
||||
eframe::run_native(
|
||||
"ubusman",
|
||||
native_options,
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
app.init(cc);
|
||||
Box::new(app)
|
||||
})
|
||||
)
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
).expect("Unable to create window");
|
||||
}
|
||||
|
||||
|
107
src/syntax_highlighting.rs
Normal file
107
src/syntax_highlighting.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use egui::text::LayoutJob;
|
||||
|
||||
const THEME: &str = "base16-mocha.dark";
|
||||
|
||||
/// Memoized Code highlighting
|
||||
pub fn highlight(ctx: &egui::Context, code: &str, language: &str) -> LayoutJob {
|
||||
impl egui::util::cache::ComputerMut<(&str, &str), LayoutJob> for Highlighter {
|
||||
fn compute(&mut self, (code, lang): (&str, &str)) -> LayoutJob {
|
||||
self.highlight(code, lang)
|
||||
}
|
||||
}
|
||||
|
||||
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
|
||||
|
||||
ctx.memory_mut(|mem| {
|
||||
mem.caches
|
||||
.cache::<HighlightCache>()
|
||||
.get((code, language))
|
||||
})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct Highlighter {
|
||||
ps: syntect::parsing::SyntaxSet,
|
||||
ts: syntect::highlighting::ThemeSet,
|
||||
}
|
||||
|
||||
impl Default for Highlighter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter {
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(&self, code: &str, lang: &str) -> LayoutJob {
|
||||
self.highlight_impl(code, lang).unwrap_or_else(|| {
|
||||
// Fallback:
|
||||
LayoutJob::simple(
|
||||
code.into(),
|
||||
egui::FontId::monospace(12.0),
|
||||
egui::Color32::DARK_GRAY,
|
||||
f32::INFINITY,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn highlight_impl(&self, text: &str, language: &str) -> Option<LayoutJob> {
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::FontStyle;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
let syntax = self
|
||||
.ps
|
||||
.find_syntax_by_name(language)
|
||||
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
||||
|
||||
let mut h = HighlightLines::new(syntax, &self.ts.themes[THEME]);
|
||||
|
||||
use egui::text::{LayoutSection, TextFormat};
|
||||
|
||||
let mut job = LayoutJob {
|
||||
text: text.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for line in LinesWithEndings::from(text) {
|
||||
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
|
||||
let fg = style.foreground;
|
||||
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = if underline {
|
||||
egui::Stroke::new(1.0, text_color)
|
||||
} else {
|
||||
egui::Stroke::NONE
|
||||
};
|
||||
job.sections.push(LayoutSection {
|
||||
leading_space: 0.0,
|
||||
byte_range: as_byte_range(text, range),
|
||||
format: TextFormat {
|
||||
font_id: egui::FontId::monospace(12.0),
|
||||
color: text_color,
|
||||
italics,
|
||||
underline,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(job)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||
let whole_start = whole.as_ptr() as usize;
|
||||
let range_start = range.as_ptr() as usize;
|
||||
assert!(whole_start <= range_start);
|
||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||
let offset = range_start - whole_start;
|
||||
offset..(offset + range.len())
|
||||
}
|
333
src/ubus.rs
333
src/ubus.rs
@ -1,19 +1,21 @@
|
||||
use std::{io::Read, borrow::Cow, net::TcpStream};
|
||||
use std::{borrow::Cow, str::FromStr, fmt::Display};
|
||||
|
||||
use async_ssh2_lite::AsyncSession;
|
||||
use lazy_regex::regex_captures;
|
||||
use serde_json::{Value, json};
|
||||
use shell_escape::unix::escape;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
|
||||
use hex::FromHex;
|
||||
use anyhow::{Result, bail, anyhow};
|
||||
use smol::{Async, io::AsyncReadExt, channel::Sender};
|
||||
use lazy_regex::regex_captures;
|
||||
use serde_json::Value;
|
||||
use shell_escape::unix::escape;
|
||||
use smol::{channel::Sender, io::AsyncReadExt};
|
||||
use thiserror::Error;
|
||||
|
||||
pub struct Ubus {
|
||||
session: AsyncSession<Async<TcpStream>>
|
||||
}
|
||||
use crate::async_line_reader::AsyncLineReader;
|
||||
|
||||
#[derive(Debug)]
|
||||
type Session = AsyncSession<AsyncIoTcpStream>;
|
||||
|
||||
// TODO: Add tests
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UbusParamType {
|
||||
Unknown,
|
||||
Integer,
|
||||
@ -21,7 +23,7 @@ pub enum UbusParamType {
|
||||
Table,
|
||||
String,
|
||||
Array,
|
||||
Double
|
||||
Double,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@ -54,17 +56,113 @@ pub enum UbusError {
|
||||
SystemError,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UbusObject {
|
||||
pub type Method = (String, Vec<(String, UbusParamType)>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Object {
|
||||
pub name: String,
|
||||
pub id: u32,
|
||||
pub methods: Vec<(String, Vec<(String, UbusParamType)>)>
|
||||
pub methods: Vec<Method>,
|
||||
}
|
||||
|
||||
impl PartialEq for Object {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name && self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UbusEvent {
|
||||
pub struct ListenEvent {
|
||||
pub path: String,
|
||||
pub value: Value
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MonitorEventType {
|
||||
Hello,
|
||||
Status,
|
||||
Data,
|
||||
Ping,
|
||||
Lookup,
|
||||
Invoke,
|
||||
AddObject,
|
||||
RemoveObject,
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
Notify,
|
||||
}
|
||||
|
||||
impl ToString for MonitorEventType {
|
||||
fn to_string(&self) -> String {
|
||||
use MonitorEventType::*;
|
||||
match self {
|
||||
Hello => "hello",
|
||||
Status => "status",
|
||||
Data => "data",
|
||||
Ping => "ping",
|
||||
Lookup => "lookup",
|
||||
Invoke => "invoke",
|
||||
AddObject => "add_object",
|
||||
RemoveObject => "remove_object",
|
||||
Subscribe => "subscribe",
|
||||
Unsubscribe => "unsubscribe",
|
||||
Notify => "notify",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MonitorEventType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
use MonitorEventType::*;
|
||||
match s {
|
||||
"hello" => Ok(Hello),
|
||||
"status" => Ok(Status),
|
||||
"data" => Ok(Data),
|
||||
"ping" => Ok(Ping),
|
||||
"lookup" => Ok(Lookup),
|
||||
"invoke" => Ok(Invoke),
|
||||
"add_object" => Ok(AddObject),
|
||||
"remove_object" => Ok(RemoveObject),
|
||||
"subscribe" => Ok(Subscribe),
|
||||
"unsubscribe" => Ok(Unsubscribe),
|
||||
"notify" => Ok(Notify),
|
||||
_ => Err(anyhow!("Unknown event type '{}'", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UbusParamType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use UbusParamType::*;
|
||||
let text = match self {
|
||||
String => "String",
|
||||
Boolean => "Boolean",
|
||||
Integer => "Integer",
|
||||
Double => "Double",
|
||||
Table => "Table",
|
||||
Array => "Array",
|
||||
Unknown => "(unknown)"
|
||||
};
|
||||
f.write_str(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MonitorEvent {
|
||||
direction: MonitorDir,
|
||||
client: u32,
|
||||
peer: u32,
|
||||
kind: MonitorEventType,
|
||||
data: Value, // TODO: Figure out the possible values for every `MonitorEventType`, to make this more safe.
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum MonitorDir {
|
||||
Rx,
|
||||
Tx,
|
||||
}
|
||||
|
||||
fn parse_parameter_type(param: &str) -> Option<UbusParamType> {
|
||||
@ -77,7 +175,7 @@ fn parse_parameter_type(param: &str) -> Option<UbusParamType> {
|
||||
"Table" => Some(Table),
|
||||
"Array" => Some(Array),
|
||||
"(unknown)" => Some(Unknown),
|
||||
_ => None
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,11 +193,11 @@ fn parse_error_code(code: i32) -> Option<UbusError> {
|
||||
7 => Some(Timeout),
|
||||
8 => Some(NotSupported),
|
||||
9 => Some(UnknownError),
|
||||
10 => Some(ConnectionFailed),
|
||||
10 | -1 => Some(ConnectionFailed),
|
||||
11 => Some(NoMemory),
|
||||
12 => Some(ParseError),
|
||||
13 => Some(SystemError),
|
||||
_ => Some(UnknownError)
|
||||
_ => Some(UnknownError),
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,42 +211,38 @@ fn parse_hex_id(id: &str) -> Result<u32> {
|
||||
return Ok(byte1 + byte2 + byte3 + byte4);
|
||||
}
|
||||
|
||||
fn escape_json(json: &Value) -> String {
|
||||
pub fn escape_json(json: &Value) -> String {
|
||||
escape(Cow::from(json.to_string())).into()
|
||||
}
|
||||
|
||||
impl Ubus {
|
||||
pub fn new(session: AsyncSession<Async<TcpStream>>) -> Ubus {
|
||||
Ubus {
|
||||
session
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_cmd(&self, cmd: &str) -> Result<String> {
|
||||
let mut channel = self.session.channel_session().await?;
|
||||
async fn exec_cmd(session: &Session, cmd: &str) -> Result<String> {
|
||||
let mut channel = session.channel_session().await?;
|
||||
channel.exec(cmd).await?;
|
||||
channel.send_eof().await?;
|
||||
channel.wait_eof().await?;
|
||||
channel.close().await?;
|
||||
channel.wait_close().await?;
|
||||
if let Some(err) = parse_error_code(channel.exit_status()?) {
|
||||
return Err(err.into())
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
channel.read_to_string(&mut output).await?;
|
||||
Ok(output)
|
||||
let mut string_buffer = String::new();
|
||||
channel.read_to_string(&mut string_buffer).await?;
|
||||
Ok(string_buffer)
|
||||
}
|
||||
|
||||
pub async fn list(self, path: Option<&str>) -> Result<Vec<String>> {
|
||||
pub async fn list(session: &Session, path: Option<&str>) -> Result<Vec<String>> {
|
||||
let output = match path {
|
||||
Some(path) => self.exec_cmd(&format!("ubus list {}", path)).await?,
|
||||
None => self.exec_cmd("ubus list").await?,
|
||||
Some(path) => exec_cmd(session, &format!("ubus -S list {}", path)).await?,
|
||||
None => exec_cmd(session, "ubus -S list").await?,
|
||||
};
|
||||
Ok(output.lines().map(ToOwned::to_owned).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub async fn list_verbose(self, path: Option<&str>) -> Result<Vec<UbusObject>> {
|
||||
pub async fn list_verbose(session: &Session, path: Option<&str>) -> Result<Vec<Object>> {
|
||||
let output = match path {
|
||||
Some(path) => self.exec_cmd(&format!("ubus -v list {}", path)).await?,
|
||||
None => self.exec_cmd("ubus -v list").await?,
|
||||
Some(path) => exec_cmd(session, &format!("ubus -v list {}", path)).await?,
|
||||
None => exec_cmd(session, "ubus -v list").await?,
|
||||
};
|
||||
|
||||
let mut cur_name = None;
|
||||
@ -157,24 +251,30 @@ impl Ubus {
|
||||
|
||||
let mut objects = vec![];
|
||||
for line in output.lines() {
|
||||
if let Some((_, name, id)) = regex_captures!(r"^'([\w.-]+)' @([0-9a-zA-Z]+)$", line) {
|
||||
if let Some((_, name, id)) = regex_captures!(r"^'([\w.-]+)' @([0-9a-zA-Z]{8})$", line) {
|
||||
if cur_name.is_some() && cur_id.is_some() {
|
||||
objects.push(UbusObject {
|
||||
objects.push(Object {
|
||||
id: cur_id.unwrap(),
|
||||
name: cur_name.unwrap(),
|
||||
methods: cur_methods
|
||||
methods: cur_methods,
|
||||
});
|
||||
cur_methods = vec![];
|
||||
}
|
||||
|
||||
cur_name = Some(name.into());
|
||||
cur_id = Some(parse_hex_id(id)?);
|
||||
} else if let Some((_, name, params_body)) = regex_captures!(r#"^\s+"([\w-]+)":\{(.*)}$"#, line) {
|
||||
} else if let Some((_, name, params_body)) =
|
||||
regex_captures!(r#"^\s+"([\w-]+)":\{(.*)}$"#, line)
|
||||
{
|
||||
let mut params = vec![];
|
||||
if !params_body.is_empty() {
|
||||
for param in params_body.split(",") {
|
||||
let (_, name, param_type_name) = regex_captures!(r#"^"([\w-]+)":"(\w+)"$"#, param)
|
||||
.ok_or(anyhow!("Failed to parse parameter '{}' in line '{}'", param, line))?;
|
||||
let (_, name, param_type_name) =
|
||||
regex_captures!(r#"^"([\w-]+)":"(\w+)"$"#, param).ok_or(anyhow!(
|
||||
"Failed to parse parameter '{}' in line '{}'",
|
||||
param,
|
||||
line
|
||||
))?;
|
||||
|
||||
let param_type = parse_parameter_type(param_type_name)
|
||||
.ok_or(anyhow!("Unknown parameter type '{}'", param_type_name))?;
|
||||
@ -190,43 +290,54 @@ impl Ubus {
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
pub async fn call(self, path: &str, method: &str, message: Option<&Value>) -> Result<Value> {
|
||||
pub async fn call(
|
||||
session: &Session,
|
||||
path: &str,
|
||||
method: &str,
|
||||
message: Option<&Value>,
|
||||
) -> Result<Value> {
|
||||
let cmd = match message {
|
||||
Some(msg) => format!("ubus call {} {} {}", path, method, escape_json(msg)),
|
||||
None => format!("ubus call {} {}", path, method),
|
||||
Some(msg) => format!("ubus -S call {} {} {}", path, method, escape_json(msg)),
|
||||
None => format!("ubus -S call {} {}", path, method),
|
||||
};
|
||||
|
||||
let output = self.exec_cmd(&cmd).await?;
|
||||
// TODO: handle cases where output is empty? ("")
|
||||
let output = exec_cmd(session, &cmd).await?;
|
||||
if output.is_empty() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
|
||||
let value = serde_json::from_str::<Value>(&output)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn send(self, event_type: &str, message: Option<&Value>) -> Result<()> {
|
||||
pub async fn send(session: &Session, event_type: &str, message: Option<&Value>) -> Result<()> {
|
||||
let cmd = match message {
|
||||
Some(msg) => format!("ubus send {} {}", event_type, escape_json(msg)),
|
||||
None => format!("ubus send {}", event_type),
|
||||
Some(msg) => format!("ubus -S send {} {}", event_type, escape_json(msg)),
|
||||
None => format!("ubus -S send {}", event_type),
|
||||
};
|
||||
|
||||
self.exec_cmd(&cmd).await?;
|
||||
exec_cmd(session, &cmd).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait_for(self, objects: &[&str]) -> Result<()> {
|
||||
pub async fn wait_for(session: &Session, objects: &[&str]) -> Result<()> {
|
||||
if objects.len() < 1 {
|
||||
bail!("At least 1 object is required")
|
||||
}
|
||||
let cmd = format!("ubus wait_for {}", objects.join(" "));
|
||||
let mut channel = self.session.channel_session().await?;
|
||||
let cmd = format!("ubus -S wait_for {}", objects.join(" "));
|
||||
let mut channel = session.channel_session().await?;
|
||||
channel.exec(&cmd).await?;
|
||||
channel.close().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_event(bytes: &[u8]) -> Result<UbusEvent> {
|
||||
fn parse_listen_event(bytes: &[u8]) -> Result<ListenEvent> {
|
||||
let event_value: Value = serde_json::from_slice(bytes)?;
|
||||
let event_map = event_value.as_object()
|
||||
let event_map = event_value
|
||||
.as_object()
|
||||
.ok_or(anyhow!("Expected event to be an object"))?;
|
||||
|
||||
if event_map.keys().len() != 1 {
|
||||
@ -236,54 +347,92 @@ impl Ubus {
|
||||
let path = event_map.keys().next().unwrap().clone();
|
||||
let value = event_map.get(&path).unwrap().clone();
|
||||
|
||||
Ok(UbusEvent { path, value })
|
||||
Ok(ListenEvent { path, value })
|
||||
}
|
||||
|
||||
pub async fn listen(self, paths: &[&str], sender: Sender<UbusEvent>) -> Result<()> {
|
||||
let cmd = format!("ubus listen {}", paths.join(" "));
|
||||
let mut channel = self.session.channel_session().await?;
|
||||
pub async fn listen(session: &Session, paths: &[&str], sender: Sender<ListenEvent>) -> Result<()> {
|
||||
let cmd = format!("ubus -S listen {}", paths.join(" "));
|
||||
let mut channel = session.channel_session().await?;
|
||||
channel.exec(&cmd).await?;
|
||||
// TODO: Handle error? 'channel.exit_status()', idk if neededdi
|
||||
// TODO: Handle error? 'channel.exit_status()', idk if needed
|
||||
|
||||
let mut line_buffer = vec![0u8; 1024];
|
||||
let mut buffer_size = 0usize;
|
||||
let mut line_reader = AsyncLineReader::new(channel.stream(0));
|
||||
loop {
|
||||
let n = channel.read(&mut line_buffer[buffer_size..]).await?;
|
||||
|
||||
let delim_pos = line_buffer[buffer_size..(buffer_size+n)]
|
||||
.iter()
|
||||
.position(|c| *c == b'\n')
|
||||
.map(|pos| pos + buffer_size);
|
||||
|
||||
buffer_size += n;
|
||||
if let Some(pos) = delim_pos {
|
||||
let event = Ubus::parse_event(&line_buffer[0..pos])?;
|
||||
let line = line_reader.read_line().await?;
|
||||
let event = parse_listen_event(&line)?;
|
||||
sender.send(event).await?;
|
||||
|
||||
line_buffer.copy_within((pos+1)..buffer_size, 0);
|
||||
buffer_size -= pos;
|
||||
buffer_size -= 1;
|
||||
}
|
||||
|
||||
|
||||
// Double line buffer size if at capacity
|
||||
if buffer_size == line_buffer.len() {
|
||||
for _ in 0..=line_buffer.len() {
|
||||
line_buffer.push(0u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn subscribe(self, paths: &[&str]) -> Result<()> {
|
||||
pub async fn subscribe(session: &Session, paths: &[&str]) -> Result<()> {
|
||||
if paths.len() < 1 {
|
||||
bail!("At least 1 object is required")
|
||||
}
|
||||
let cmd = format!("ubus subscribe {}", paths.join(" "));
|
||||
let mut channel = self.session.channel_session().await?;
|
||||
let cmd = format!("ubus -S subscribe {}", paths.join(" "));
|
||||
let mut channel = session.channel_session().await?;
|
||||
channel.exec(&cmd).await?;
|
||||
|
||||
// TODO: Haven't figured out how to test subscribe event using default objects on ubus.
|
||||
todo!();
|
||||
}
|
||||
|
||||
fn parse_monitor_event(bytes: &[u8]) -> Result<MonitorEvent> {
|
||||
let line = bytes
|
||||
.iter()
|
||||
.map(|c| char::from_u32(*c as u32).unwrap())
|
||||
.collect::<String>();
|
||||
|
||||
let (_, direction_str, client_str, peer_str, kind_str, data_str) = regex_captures!(
|
||||
r"^([<\->]{2}) ([0-9a-zA-Z]{8}) #([0-9a-zA-Z]{8})\s+([a-z_]+): (\{.*\})$",
|
||||
&line
|
||||
)
|
||||
.ok_or(anyhow!("Unknown pattern of monitor message '{}'", line))?;
|
||||
|
||||
let direction = match direction_str {
|
||||
"->" => MonitorDir::Tx,
|
||||
"<-" => MonitorDir::Rx,
|
||||
_ => bail!("Unknown monitor message direction '{}'", direction_str),
|
||||
};
|
||||
let client = parse_hex_id(client_str)?;
|
||||
let peer = parse_hex_id(peer_str)?;
|
||||
let kind = MonitorEventType::from_str(kind_str)?;
|
||||
let data = serde_json::from_str(data_str)?;
|
||||
|
||||
Ok(MonitorEvent {
|
||||
direction,
|
||||
client,
|
||||
peer,
|
||||
kind,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn monitor(
|
||||
session: &Session,
|
||||
dir: Option<MonitorDir>,
|
||||
filter: &[MonitorEventType],
|
||||
sender: Sender<MonitorEvent>,
|
||||
) -> Result<()> {
|
||||
let mut cmd = vec!["ubus -S".into()];
|
||||
if let Some(dir) = dir {
|
||||
if dir == MonitorDir::Rx {
|
||||
cmd.push("-M r".into());
|
||||
} else if dir == MonitorDir::Tx {
|
||||
cmd.push("-M t".into());
|
||||
}
|
||||
}
|
||||
cmd.extend(filter.iter().map(|e| format!("-m {}", e.to_string())));
|
||||
cmd.push("monitor".into());
|
||||
|
||||
let mut channel = session.channel_session().await?;
|
||||
println!("{}", cmd.join(" "));
|
||||
channel.exec(&cmd.join(" ")).await?;
|
||||
// TODO: Handle error? 'channel.exit_status()', idk if needed
|
||||
|
||||
let mut line_reader = AsyncLineReader::new(channel.stream(0));
|
||||
loop {
|
||||
let line = line_reader.read_line().await?;
|
||||
let event = parse_monitor_event(&line)?;
|
||||
sender.send(event).await?;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user