From 2f8d8259706466447a7153f2abb2abc7e951d8de Mon Sep 17 00:00:00 2001 From: Chris Guida Date: Wed, 3 Aug 2022 13:06:25 -0500 Subject: [PATCH] [Feat] follow logs (#1714) * tail logs * add cli * add FE * abstract http to shared * batch new logs * file download for logs * fix modal error when no config Co-authored-by: Chris Guida Co-authored-by: Aiden McClelland Co-authored-by: Matt Hill Co-authored-by: BluJ --- backend/Cargo.lock | 568 +++++++++--------- backend/Cargo.toml | 4 +- backend/src/auth.rs | 12 +- backend/src/bin/embassyd.rs | 35 +- backend/src/context/rpc.rs | 54 +- backend/src/core/rpc_continuations.rs | 93 ++- backend/src/diagnostic.rs | 14 +- backend/src/install/mod.rs | 33 +- backend/src/logs.rs | 396 ++++++++++-- backend/src/system.rs | 125 +++- frontend/angular.json | 3 +- .../src/app/pages/logs/logs.page.html | 46 +- .../src/app/pages/logs/logs.page.ts | 161 ++--- .../src/app/services/api/api.service.ts | 22 +- .../src/app/services/api/live-api.service.ts | 9 +- .../src/app/services/api/mock-api.service.ts | 11 +- .../src/app/services/http.service.ts | 71 --- .../setup-wizard/src/app/app.module.ts | 10 +- .../setup-wizard/src/app/guards/nav-guard.ts | 22 +- .../prod-key-modal/prod-key-modal.page.ts | 8 +- .../app/pages/product-key/product-key.page.ts | 8 +- .../src/app/pages/success/success.page.html | 196 +++--- .../src/app/pages/success/success.page.ts | 55 +- .../src/app/services/api/api.service.ts | 26 +- .../src/app/services/api/http.service.ts | 242 -------- .../src/app/services/api/live-api.service.ts | 68 +-- .../src/app/services/rpc-encrypted.service.ts | 102 ++++ ...module.ts => markdown.component.module.ts} | 0 .../text-spinner/text-spinner.component.html | 2 +- frontend/projects/shared/src/public-api.ts | 8 +- .../src/services/download-html.service.ts | 29 + .../shared/src/services/http.service.ts | 199 ++++++ frontend/projects/shared/src/types/api.ts | 16 + .../shared/src/util/copy-to-clipboard.ts | 19 + frontend/projects/shared/tsconfig.json | 3 +- .../ui/src/app/app/menu/menu.component.html | 2 +- .../ui/src/app/app/menu/menu.component.ts | 8 - .../app/components/logs/logs.component.html | 86 +++ ...ogs.module.ts => logs.component.module.ts} | 11 +- .../app/components/logs/logs.component.scss | 5 + .../src/app/components/logs/logs.component.ts | 226 +++++++ .../ui/src/app/components/logs/logs.page.html | 61 -- .../ui/src/app/components/logs/logs.page.scss | 3 - .../ui/src/app/components/logs/logs.page.ts | 139 ----- .../action-success/action-success.page.html | 14 +- .../action-success/action-success.page.ts | 2 +- .../modals/app-config/app-config.page.html | 16 +- .../app/modals/app-config/app-config.page.ts | 28 +- .../app-interfaces/app-interfaces.page.ts | 3 +- .../apps-routes/app-logs/app-logs.module.ts | 6 +- .../apps-routes/app-logs/app-logs.page.html | 22 +- .../apps-routes/app-logs/app-logs.page.ts | 41 +- .../app-properties/app-properties.page.ts | 8 +- .../pages/apps-routes/apps-routing.module.ts | 31 +- .../kernel-logs/kernel-logs.module.ts | 6 +- .../kernel-logs/kernel-logs.page.html | 24 +- .../kernel-logs/kernel-logs.page.ts | 42 +- .../server-logs/server-logs.module.ts | 6 +- .../server-logs/server-logs.page.html | 24 +- .../server-logs/server-logs.page.ts | 42 +- .../server-specs/server-specs.page.ts | 2 +- .../ui/src/app/services/api/api.fixures.ts | 13 +- .../ui/src/app/services/api/api.types.ts | 36 +- .../app/services/api/embassy-api.service.ts | 27 +- .../services/api/embassy-live-api.service.ts | 30 +- .../services/api/embassy-mock-api.service.ts | 82 ++- .../ui/src/app/services/http.service.ts | 201 ------- frontend/projects/ui/src/app/util/web.util.ts | 20 - frontend/proxy.conf-sample.json | 2 +- libs/helpers/src/lib.rs | 58 ++ 70 files changed, 2202 insertions(+), 1795 deletions(-) delete mode 100644 frontend/projects/diagnostic-ui/src/app/services/http.service.ts delete mode 100644 frontend/projects/setup-wizard/src/app/services/api/http.service.ts create mode 100644 frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts rename frontend/projects/shared/src/components/markdown/{markdown.module.ts => markdown.component.module.ts} (100%) create mode 100644 frontend/projects/shared/src/services/download-html.service.ts create mode 100644 frontend/projects/shared/src/services/http.service.ts create mode 100644 frontend/projects/shared/src/types/api.ts create mode 100644 frontend/projects/shared/src/util/copy-to-clipboard.ts create mode 100644 frontend/projects/ui/src/app/components/logs/logs.component.html rename frontend/projects/ui/src/app/components/logs/{logs.module.ts => logs.component.module.ts} (57%) create mode 100644 frontend/projects/ui/src/app/components/logs/logs.component.scss create mode 100644 frontend/projects/ui/src/app/components/logs/logs.component.ts delete mode 100644 frontend/projects/ui/src/app/components/logs/logs.page.html delete mode 100644 frontend/projects/ui/src/app/components/logs/logs.page.scss delete mode 100644 frontend/projects/ui/src/app/components/logs/logs.page.ts delete mode 100644 frontend/projects/ui/src/app/services/http.service.ts diff --git a/backend/Cargo.lock b/backend/Cargo.lock index b28457788..d89cd7ad2 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -46,7 +46,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "once_cell", "version_check", ] @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" [[package]] name = "arrayref" @@ -110,10 +110,10 @@ checksum = "bc4c00309ed1c8104732df4a5fa9acc3b796b6f8531dfbd5ce0078c86f997244" dependencies = [ "darling 0.10.2", "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -132,20 +132,20 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" dependencies = [ "addr2line", "cc", @@ -258,8 +258,8 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "regex", "rustc-hash", "shlex", @@ -268,9 +268,9 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] @@ -425,9 +425,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cc" @@ -534,9 +534,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.8" +version = "3.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" dependencies = [ "atty", "bitflags", @@ -608,7 +608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ "percent-encoding", - "time 0.3.11", + "time 0.3.12", "version_check", ] @@ -624,7 +624,7 @@ dependencies = [ "publicsuffix", "serde", "serde_json", - "time 0.3.11", + "time 0.3.12", "url", ] @@ -670,9 +670,9 @@ checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -680,12 +680,12 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if 1.0.0", - "lazy_static", + "once_cell", ] [[package]] @@ -696,9 +696,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -792,10 +792,10 @@ checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "strsim 0.9.3", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -806,10 +806,10 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "strsim 0.10.0", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -819,8 +819,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ "darling_core 0.10.2", - "quote 1.0.18", - "syn 1.0.96", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -830,8 +830,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core 0.13.4", - "quote 1.0.18", - "syn 1.0.96", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -841,7 +841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3495912c9c1ccf2e18976439f4443f3fee0fd61f424ff99fde6a66b15ecb448f" dependencies = [ "cfg-if 1.0.0", - "hashbrown 0.12.1", + "hashbrown 0.12.3", "lock_api", "parking_lot_core 0.9.3", ] @@ -916,9 +916,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05520711837dd592d2861319ea3cf2dfd81e39bb92e41758ee9172f3623daebd" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -939,17 +939,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "rustc_version 0.4.0", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] name = "diff" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" @@ -1058,9 +1058,9 @@ dependencies = [ [[package]] name = "either" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" dependencies = [ "serde", ] @@ -1080,7 +1080,7 @@ dependencies = [ "bollard", "chrono", "ciborium", - "clap 3.2.8", + "clap 3.2.16", "color-eyre", "cookie_store", "current_platform", @@ -1152,7 +1152,7 @@ dependencies = [ "tracing", "tracing-error 0.2.0", "tracing-futures", - "tracing-subscriber 0.3.14", + "tracing-subscriber 0.3.15", "trust-dns-server", "typed-builder", "url", @@ -1207,9 +1207,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" dependencies = [ "heck 0.4.0", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -1219,9 +1219,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b940da354ae81ef0926c5eaa428207b8f4f091d3956c891dfbd124162bed99" dependencies = [ "pmutil", - "proc-macro2 1.0.39", + "proc-macro2 1.0.42", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -1252,9 +1252,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "eyre" @@ -1268,9 +1268,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -1286,32 +1286,32 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" +checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.13", - "winapi", + "redox_syscall 0.2.16", + "windows-sys", ] [[package]] name = "fixedbitset" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flume" -version = "0.10.12" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin 0.9.3", + "spin 0.9.4", ] [[package]] @@ -1361,9 +1361,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0951635027ca477be98f8774abd6f0345233439d63f307e47101acb40c7cc63d" dependencies = [ "pmutil", - "proc-macro2 1.0.39", + "proc-macro2 1.0.42", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -1447,9 +1447,9 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -1505,20 +1505,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "gimli" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "git-version" @@ -1537,9 +1537,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" dependencies = [ "proc-macro-hack", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -1581,9 +1581,9 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "ahash", ] @@ -1594,7 +1594,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086" dependencies = [ - "hashbrown 0.12.1", + "hashbrown 0.12.3", ] [[package]] @@ -1848,7 +1848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown 0.12.1", + "hashbrown 0.12.3", "serde", ] @@ -1869,15 +1869,15 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "is-macro" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b2c46692aee0d1b3aad44e781ac0f0e7db42ef27adaa0a877b627040019813" +checksum = "1c068d4c6b922cd6284c609cfa6dec0e41615c9c5a1a4ba729a970d8daba05fb" dependencies = [ "Inflector", "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -1922,9 +1922,9 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -2164,9 +2164,9 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lock_api" @@ -2391,9 +2391,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fbc387afefefd5e9e39493299f3069e14a140dd34dc19b4c1c1a8fddb6a790" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" dependencies = [ "num-traits", ] @@ -2421,9 +2421,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-bigint", @@ -2466,9 +2466,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -2482,18 +2482,18 @@ dependencies = [ [[package]] name = "object" -version = "0.28.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "opaque-debug" @@ -2535,9 +2535,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -2548,9 +2548,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.20.0+1.1.1o" +version = "111.22.0+1.1.1q" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec" +checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853" dependencies = [ "cc", ] @@ -2571,9 +2571,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" [[package]] name = "owo-colors" @@ -2611,7 +2611,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.13", + "redox_syscall 0.2.16", "smallvec", "winapi", ] @@ -2624,16 +2624,16 @@ checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.13", + "redox_syscall 0.2.16", "smallvec", "windows-sys", ] [[package]] name = "password-hash" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", "rand_core 0.6.3", @@ -2673,8 +2673,8 @@ name = "patch-db-macro" version = "0.1.0" dependencies = [ "patch-db-macro-internals", - "proc-macro2 1.0.39", - "syn 1.0.96", + "proc-macro2 1.0.42", + "syn 1.0.98", ] [[package]] @@ -2682,9 +2682,9 @@ name = "patch-db-macro-internals" version = "0.1.0" dependencies = [ "heck 0.3.3", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -2760,9 +2760,9 @@ dependencies = [ "phf_generator", "phf_shared", "proc-macro-hack", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -2795,9 +2795,9 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -2834,9 +2834,9 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -2892,9 +2892,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" dependencies = [ "unicode-ident", ] @@ -2971,11 +2971,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ - "proc-macro2 1.0.39", + "proc-macro2 1.0.42", ] [[package]] @@ -3053,7 +3053,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -3091,9 +3091,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -3115,8 +3115,8 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.6", - "redox_syscall 0.2.13", + "getrandom 0.2.7", + "redox_syscall 0.2.16", "thiserror", ] @@ -3239,11 +3239,11 @@ dependencies = [ [[package]] name = "rpc-toolkit" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b40671737dff8e63a1294536eea6e186f3700930fe92d8b5c7dfd636d0df16" +checksum = "9d5bfeb75c188f3af65774d5fe92f97dac2cede5e313c643c7a1b82a8e53b0e6" dependencies = [ - "clap 3.2.8", + "clap 3.2.16", "futures", "hyper", "lazy_static", @@ -3261,24 +3261,24 @@ dependencies = [ [[package]] name = "rpc-toolkit-macro" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad6cd35336587eb0459cdc01f873ddd5a8a3480f97c5268a46a820e1b23b9aa" +checksum = "ecb48bdaace41cfbb514b3e541ae0fc1ac0fb8283498215ad8a3d22ca2ea5ae5" dependencies = [ - "proc-macro2 1.0.39", + "proc-macro2 1.0.42", "rpc-toolkit-macro-internals", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] name = "rpc-toolkit-macro-internals" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa57bace2d4fe20a2aeac1ef4e9f6f1574f07b72fde1e43c7bb55e963f1702e7" +checksum = "b2a9e2bae02a2beecad48d87255e51cab941d0c89a2bcee05a03a77803a0a282" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -3332,7 +3332,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.9", + "semver 1.0.12", ] [[package]] @@ -3358,9 +3358,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" [[package]] name = "rusty-fork" @@ -3446,9 +3446,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" [[package]] name = "semver-parser" @@ -3458,9 +3458,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.139" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +checksum = "7af873f2c95b99fcb0bd0fe622a43e29514658873c8ceba88c4cb88833a22500" dependencies = [ "serde_derive", ] @@ -3494,13 +3494,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.139" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +checksum = "75743a150d003dd863b51dc809bcad0d73f2102c53632f1e954e738192a3413f" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -3557,16 +3557,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling 0.13.4", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] name = "serde_yaml" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec0091e1f5aa338283ce049bd9dfefd55e1f168ac233e85c1ffe0038fb48cbe" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", @@ -3680,15 +3680,18 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" @@ -3724,9 +3727,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" dependencies = [ "lock_api", ] @@ -3820,14 +3823,14 @@ dependencies = [ "heck 0.4.0", "hex", "once_cell", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "serde", "serde_json", "sha2 0.10.2", "sqlx-core", "sqlx-rt", - "syn 1.0.96", + "syn 1.0.98", "url", ] @@ -3883,8 +3886,8 @@ checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator", "phf_shared", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", ] [[package]] @@ -3894,10 +3897,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f584cc881e9e5f1fd6bf827b0444aa94c30d8fe6378cf241071b5f5700b2871f" dependencies = [ "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -3991,10 +3994,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb64bc03d90fd5c90d6ab917bb2b1d7fbd31957df39e31ea24a3f554b4372251" dependencies = [ "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -4039,10 +4042,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59949619b2ef45eedb6c399d05f2c3c7bc678b5074b3103bb670f9e05bb99042" dependencies = [ "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -4123,10 +4126,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18712e4aab969c6508dff3540ade6358f1e013464aa58b3d30da2ab2d9fcbbed" dependencies = [ "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -4240,9 +4243,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c8f200a2eaed938e7c1a685faaa66e6d42fa9e17da5f62572d3cbc335898f5e" dependencies = [ "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -4252,9 +4255,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5dca3f08d02da4684c3373150f7c045128f81ea00f0c434b1b012bc65a6cce3" dependencies = [ "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -4275,10 +4278,10 @@ checksum = "c3b9b72892df873972549838bf84d6c56234c7502148a7e23b5a3da6e0fedfb8" dependencies = [ "Inflector", "pmutil", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "swc_macros_common", - "syn 1.0.96", + "syn 1.0.98", ] [[package]] @@ -4294,12 +4297,12 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.96" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.42", + "quote 1.0.20", "unicode-ident", ] @@ -4309,9 +4312,9 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", "unicode-xid 0.2.3", ] @@ -4341,7 +4344,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "libc", - "redox_syscall 0.2.13", + "redox_syscall 0.2.16", "remove_dir_all", "winapi", ] @@ -4416,9 +4419,9 @@ version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -4454,11 +4457,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "74b7cc93fc23ba97fde84f7eea56c55d1ba183f495c6715defdfc7b9cb8c870f" dependencies = [ "itoa 1.0.2", + "js-sys", "libc", "num_threads", "time-macros", @@ -4496,10 +4500,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.19.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ + "autocfg", "bytes", "libc", "memchr", @@ -4520,9 +4525,9 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -4578,7 +4583,7 @@ dependencies = [ "filetime", "futures-core", "libc", - "redox_syscall 0.2.13", + "redox_syscall 0.2.16", "tokio", "tokio-stream", "xattr", @@ -4586,13 +4591,15 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06cda1232a49558c46f8a504d5b93101d42c0bf7f911f12a105ba48168f821ae" +checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" dependencies = [ "futures-util", "log", + "native-tls", "tokio", + "tokio-native-tls", "tungstenite", ] @@ -4641,15 +4648,15 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -4659,20 +4666,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ "once_cell", "valuable", @@ -4695,7 +4702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" dependencies = [ "tracing", - "tracing-subscriber 0.3.14", + "tracing-subscriber 0.3.15", ] [[package]] @@ -4732,9 +4739,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" dependencies = [ "ansi_term", "matchers", @@ -4772,7 +4779,7 @@ dependencies = [ "radix_trie", "rand 0.8.5", "thiserror", - "time 0.3.11", + "time 0.3.12", "tokio", "trust-dns-proto", ] @@ -4818,7 +4825,7 @@ dependencies = [ "log", "serde", "thiserror", - "time 0.3.11", + "time 0.3.12", "tokio", "toml", "trust-dns-client", @@ -4833,9 +4840,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tungstenite" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5" +checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ "base64 0.13.0", "byteorder", @@ -4843,6 +4850,7 @@ dependencies = [ "http", "httparse", "log", + "native-tls", "rand 0.8.5", "sha-1", "thiserror", @@ -4862,9 +4870,9 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", ] [[package]] @@ -4887,15 +4895,15 @@ checksum = "69fe8d9274f490a36442acb4edfd0c4e473fdfc6a8b5cd32f28a0235761aedbe" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] @@ -5046,9 +5054,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -5056,24 +5064,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", - "lazy_static", "log", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "once_cell", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -5083,38 +5091,38 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ - "quote 1.0.18", + "quote 1.0.20", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -5280,9 +5288,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94693807d016b2f2d2e14420eb3bfcca689311ff775dcf113d74ea624b7cdf07" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] @@ -5293,8 +5301,8 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", "synstructure", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 87d1d2a19..18ed8b17e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -107,7 +107,7 @@ regex = "1.6.0" reqwest = { version = "0.11.11", features = ["stream", "json", "socks"] } reqwest_cookie_store = "0.3.0" rpassword = "6.0.1" -rpc-toolkit = "0.2.0" +rpc-toolkit = "0.2.1" rust-argon2 = "1.0.0" scopeguard = "1.1" # because avahi-sys fucks your shit up serde = { version = "1.0.139", features = ["derive", "rc"] } @@ -131,7 +131,7 @@ thiserror = "1.0.31" tokio = { version = "1.19.2", features = ["full"] } tokio-stream = { version = "0.1.9", features = ["io-util", "sync"] } tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" } -tokio-tungstenite = "0.17.1" +tokio-tungstenite = { version = "0.17.1", features = ["native-tls"] } tokio-util = { version = "0.7.3", features = ["io"] } torut = "0.2.1" tracing = "0.1.35" diff --git a/backend/src/auth.rs b/backend/src/auth.rs index bc9492c11..000b35e32 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -24,10 +24,14 @@ pub fn auth() -> Result<(), Error> { Ok(()) } -pub fn parse_metadata(_: &str, _: &ArgMatches) -> Result { - Ok(serde_json::json!({ +pub fn cli_metadata() -> Value { + serde_json::json!({ "platforms": ["cli"], - })) + }) +} + +pub fn parse_metadata(_: &str, _: &ArgMatches) -> Result { + Ok(cli_metadata()) } #[test] @@ -106,7 +110,7 @@ pub async fn login( #[arg] password: Option, #[arg( parse(parse_metadata), - default = "", + default = "cli_metadata", help = "RPC Only: This value cannot be overidden from the cli" )] metadata: Value, diff --git a/backend/src/bin/embassyd.rs b/backend/src/bin/embassyd.rs index 8df889268..3dc6772cf 100644 --- a/backend/src/bin/embassyd.rs +++ b/backend/src/bin/embassyd.rs @@ -151,6 +151,33 @@ async fn inner_main(cfg_path: Option<&str>) -> Result, Error> { "/ws/db" => { Ok(subscribe(ctx, req).await.unwrap_or_else(err_to_500)) } + path if path.starts_with("/ws/rpc/") => { + match RequestGuid::from( + path.strip_prefix("/ws/rpc/").unwrap(), + ) { + None => { + tracing::debug!("No Guid Path"); + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + } + Some(guid) => { + match ctx.get_ws_continuation_handler(&guid).await { + Some(cont) => match cont(req).await { + Ok(r) => Ok(r), + Err(e) => Response::builder() + .status( + StatusCode::INTERNAL_SERVER_ERROR, + ) + .body(Body::from(format!("{}", e))), + }, + _ => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()), + } + } + } + } path if path.starts_with("/rest/rpc/") => { match RequestGuid::from( path.strip_prefix("/rest/rpc/").unwrap(), @@ -162,16 +189,12 @@ async fn inner_main(cfg_path: Option<&str>) -> Result, Error> { .body(Body::empty()) } Some(guid) => { - match ctx - .rpc_stream_continuations - .lock() - .await - .remove(&guid) + match ctx.get_rest_continuation_handler(&guid).await { None => Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::empty()), - Some(cont) => match (cont.handler)(req).await { + Some(cont) => match cont(req).await { Ok(r) => Ok(r), Err(e) => Response::builder() .status( diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs index 72a7f4966..508cdd4a3 100644 --- a/backend/src/context/rpc.rs +++ b/backend/src/context/rpc.rs @@ -21,7 +21,7 @@ use tokio::process::Command; use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; use tracing::instrument; -use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation}; use crate::db::model::{Database, InstalledPackageDataEntry, PackageDataEntry}; use crate::hostname::{derive_hostname, derive_id, get_product_key}; use crate::install::cleanup::{cleanup_failed, uninstall, CleanupFailedReceipts}; @@ -387,6 +387,58 @@ impl RpcContext { } Ok(()) } + + #[instrument(skip(self))] + pub async fn clean_continuations(&self) { + let mut continuations = self.rpc_stream_continuations.lock().await; + let mut to_remove = Vec::new(); + for (guid, cont) in &*continuations { + if cont.is_timed_out() { + to_remove.push(guid.clone()); + } + } + for guid in to_remove { + continuations.remove(&guid); + } + } + + #[instrument(skip(self, handler))] + pub async fn add_continuation(&self, guid: RequestGuid, handler: RpcContinuation) { + self.clean_continuations().await; + self.rpc_stream_continuations + .lock() + .await + .insert(guid, handler); + } + + pub async fn get_continuation_handler(&self, guid: &RequestGuid) -> Option { + let mut continuations = self.rpc_stream_continuations.lock().await; + if let Some(cont) = continuations.remove(guid) { + cont.into_handler().await + } else { + None + } + } + + pub async fn get_ws_continuation_handler(&self, guid: &RequestGuid) -> Option { + let continuations = self.rpc_stream_continuations.lock().await; + if matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) { + drop(continuations); + self.get_continuation_handler(guid).await + } else { + None + } + } + + pub async fn get_rest_continuation_handler(&self, guid: &RequestGuid) -> Option { + let continuations = self.rpc_stream_continuations.lock().await; + if matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) { + drop(continuations); + self.get_continuation_handler(guid).await + } else { + None + } + } } impl Context for RpcContext { fn host(&self) -> Host<&str> { diff --git a/backend/src/core/rpc_continuations.rs b/backend/src/core/rpc_continuations.rs index da4a8c7b4..45a1c1b05 100644 --- a/backend/src/core/rpc_continuations.rs +++ b/backend/src/core/rpc_continuations.rs @@ -1,20 +1,27 @@ -use std::time::Instant; +use std::sync::Arc; +use std::time::Duration; use futures::future::BoxFuture; -use http::{Request, Response}; -use hyper::Body; +use futures::FutureExt; +use helpers::TimedResource; +use hyper::upgrade::Upgraded; +use hyper::{Body, Error as HyperError, Request, Response}; use rand::RngCore; +use tokio::task::JoinError; +use tokio_tungstenite::WebSocketStream; + +use crate::{Error, ResultExt}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] -pub struct RequestGuid = String>(T); +pub struct RequestGuid = String>(Arc); impl RequestGuid { pub fn new() -> Self { let mut buf = [0; 40]; rand::thread_rng().fill_bytes(&mut buf); - RequestGuid(base32::encode( + RequestGuid(Arc::new(base32::encode( base32::Alphabet::RFC4648 { padding: false }, &buf, - )) + ))) } pub fn from(r: &str) -> Option { @@ -26,7 +33,7 @@ impl RequestGuid { return None; } } - Some(RequestGuid(r.to_owned())) + Some(RequestGuid(Arc::new(r.to_owned()))) } } #[test] @@ -39,15 +46,71 @@ fn parse_guid() { impl> std::fmt::Display for RequestGuid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.as_ref().fmt(f) + (&*self.0).as_ref().fmt(f) } } -pub struct RpcContinuation { - pub created_at: Instant, - pub handler: Box< - dyn FnOnce(Request) -> BoxFuture<'static, Result, crate::Error>> - + Send - + Sync, - >, +pub type RestHandler = Box< + dyn FnOnce(Request) -> BoxFuture<'static, Result, crate::Error>> + Send, +>; + +pub type WebSocketHandler = Box< + dyn FnOnce( + BoxFuture<'static, Result, HyperError>, JoinError>>, + ) -> BoxFuture<'static, Result<(), Error>> + + Send, +>; + +pub enum RpcContinuation { + Rest(TimedResource), + WebSocket(TimedResource), +} +impl RpcContinuation { + pub fn rest(handler: RestHandler, timeout: Duration) -> Self { + RpcContinuation::Rest(TimedResource::new(handler, timeout)) + } + pub fn ws(handler: WebSocketHandler, timeout: Duration) -> Self { + RpcContinuation::WebSocket(TimedResource::new(handler, timeout)) + } + pub fn is_timed_out(&self) -> bool { + match self { + RpcContinuation::Rest(a) => a.is_timed_out(), + RpcContinuation::WebSocket(a) => a.is_timed_out(), + } + } + pub async fn into_handler(self) -> Option { + match self { + RpcContinuation::Rest(handler) => handler.get().await, + RpcContinuation::WebSocket(handler) => { + if let Some(handler) = handler.get().await { + Some(Box::new( + |req: Request| -> BoxFuture<'static, Result, Error>> { + async move { + let (parts, body) = req.into_parts(); + let req = Request::from_parts(parts, body); + let (res, ws_fut) = hyper_ws_listener::create_ws(req) + .with_kind(crate::ErrorKind::Network)?; + if let Some(ws_fut) = ws_fut { + tokio::task::spawn(async move { + match handler(ws_fut.boxed()).await { + Ok(()) => (), + Err(e) => { + tracing::error!("WebSocket Closed: {}", e); + tracing::debug!("{:?}", e); + } + } + }); + } + + Ok(res) + } + .boxed() + }, + )) + } else { + None + } + } + } + } } diff --git a/backend/src/diagnostic.rs b/backend/src/diagnostic.rs index 50f4e697a..9b5417a6f 100644 --- a/backend/src/diagnostic.rs +++ b/backend/src/diagnostic.rs @@ -6,7 +6,7 @@ use rpc_toolkit::yajrc::RpcError; use crate::context::DiagnosticContext; use crate::disk::repair; -use crate::logs::{display_logs, fetch_logs, LogResponse, LogSource}; +use crate::logs::{fetch_logs, LogResponse, LogSource}; use crate::shutdown::Shutdown; use crate::util::display_none; use crate::Error; @@ -23,19 +23,13 @@ pub fn error(#[context] ctx: DiagnosticContext) -> Result, Error> Ok(ctx.error.clone()) } -#[command(display(display_logs))] +#[command(rpc_only)] pub async fn logs( #[arg] limit: Option, #[arg] cursor: Option, - #[arg] before_flag: Option, + #[arg] before: bool, ) -> Result { - Ok(fetch_logs( - LogSource::Service(SYSTEMD_UNIT), - limit, - cursor, - before_flag.unwrap_or(false), - ) - .await?) + Ok(fetch_logs(LogSource::Service(SYSTEMD_UNIT), limit, cursor, before).await?) } #[command(display(display_none))] diff --git a/backend/src/install/mod.rs b/backend/src/install/mod.rs index 6cd400004..9f0f2be82 100644 --- a/backend/src/install/mod.rs +++ b/backend/src/install/mod.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::atomic::Ordering; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Duration; use color_eyre::eyre::eyre; use emver::VersionRange; @@ -16,8 +16,8 @@ use http::{Request, Response, StatusCode}; use hyper::Body; use patch_db::{DbHandle, LockReceipt, LockType}; use reqwest::Url; +use rpc_toolkit::command; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, Context}; use tokio::fs::{File, OpenOptions}; use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt}; use tokio::process::Command; @@ -478,23 +478,11 @@ pub async fn sideload( } .boxed() }); - let cont = RpcContinuation { - created_at: Instant::now(), // TODO - handler, - }; - // gc the map - let mut guard = ctx.rpc_stream_continuations.lock().await; - let garbage_collected = std::mem::take(&mut *guard) - .into_iter() - .filter(|(_, v)| v.created_at.elapsed() < Duration::from_secs(30)) - .collect::>(); - *guard = garbage_collected; - drop(guard); - // insert the new continuation - ctx.rpc_stream_continuations - .lock() - .await - .insert(guid.clone(), cont); + ctx.add_continuation( + guid.clone(), + RpcContinuation::rest(handler, Duration::from_secs(30)), + ) + .await; Ok(guid) } @@ -537,12 +525,7 @@ async fn cli_install( let body = Body::wrap_stream(tokio_util::io::ReaderStream::new(file)); let res = ctx .client - .post(format!( - "{}://{}/rest/rpc/{}", - ctx.protocol(), - ctx.host(), - guid - )) + .post(format!("{}/rest/rpc/{}", ctx.base_url, guid,)) .header(CONTENT_LENGTH, content_length) .body(body) .send() diff --git a/backend/src/logs.rs b/backend/src/logs.rs index 709171859..858f9e14e 100644 --- a/backend/src/logs.rs +++ b/backend/src/logs.rs @@ -1,22 +1,117 @@ +use std::future::Future; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ops::DerefMut; use std::process::Stdio; use std::time::{Duration, UNIX_EPOCH}; use chrono::{DateTime, Utc}; -use clap::ArgMatches; use color_eyre::eyre::eyre; -use futures::TryStreamExt; +use futures::stream::BoxStream; +use futures::Stream; +use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt}; +use hyper::upgrade::Upgraded; +use hyper::Error as HyperError; use rpc_toolkit::command; +use rpc_toolkit::yajrc::RpcError; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; +use tokio::process::{Child, Command}; +use tokio::task::JoinError; use tokio_stream::wrappers::LinesStream; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; +use tokio_tungstenite::tungstenite::protocol::CloseFrame; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::WebSocketStream; use tracing::instrument; +use crate::context::{CliContext, RpcContext}; +use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::error::ResultExt; use crate::procedure::docker::DockerProcedure; use crate::s9pk::manifest::PackageId; -use crate::util::serde::Reversible; -use crate::Error; +use crate::util::{display_none, serde::Reversible}; +use crate::{Error, ErrorKind}; + +#[pin_project::pin_project] +struct LogStream { + _child: Child, + #[pin] + entries: BoxStream<'static, Result>, +} +impl Deref for LogStream { + type Target = BoxStream<'static, Result>; + fn deref(&self) -> &Self::Target { + &self.entries + } +} +impl DerefMut for LogStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.entries + } +} +impl Stream for LogStream { + type Item = Result; + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + Stream::poll_next(this.entries, cx) + } + + fn size_hint(&self) -> (usize, Option) { + self.entries.size_hint() + } +} + +#[instrument(skip(logs, ws_fut))] +async fn ws_handler< + WSFut: Future, HyperError>, JoinError>>, +>( + first_entry: Option, + mut logs: LogStream, + ws_fut: WSFut, +) -> Result<(), Error> { + let mut stream = ws_fut + .await + .with_kind(crate::ErrorKind::Network)? + .with_kind(crate::ErrorKind::Unknown)?; + + if let Some(first_entry) = first_entry { + stream + .send(Message::Text( + serde_json::to_string(&first_entry).with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } + + while let Some(entry) = tokio::select! { + a = logs.try_next() => Some(a?), + a = stream.try_next() => { a.with_kind(crate::ErrorKind::Network)?; None } + } { + if let Some(entry) = entry { + let (_, log_entry) = entry.log_entry()?; + stream + .send(Message::Text( + serde_json::to_string(&log_entry).with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } + } + + stream + .close(Some(CloseFrame { + code: CloseCode::Normal, + reason: "Log Stream Finished".into(), + })) + .await + .with_kind(ErrorKind::Network)?; + + Ok(()) +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] @@ -25,6 +120,12 @@ pub struct LogResponse { start_cursor: Option, end_cursor: Option, } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case", tag = "type")] +pub struct LogFollowResponse { + start_cursor: Option, + guid: RequestGuid, +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct LogEntry { @@ -111,38 +212,145 @@ pub enum LogSource { Container(PackageId), } -pub fn display_logs(all: LogResponse, _: &ArgMatches) { - for entry in all.entries.iter() { - println!("{}", entry); - } -} - -#[command(display(display_logs))] +#[command( + custom_cli(cli_logs(async, context(CliContext))), + subcommands(self(logs_nofollow(async)), logs_follow), + display(display_none) +)] pub async fn logs( #[arg] id: PackageId, - #[arg] limit: Option, - #[arg] cursor: Option, - #[arg] before_flag: Option, + #[arg(short = 'l', long = "limit")] limit: Option, + #[arg(short = 'c', long = "cursor")] cursor: Option, + #[arg(short = 'B', long = "before", default)] before: bool, + #[arg(short = 'f', long = "follow", default)] follow: bool, +) -> Result<(PackageId, Option, Option, bool, bool), Error> { + Ok((id, limit, cursor, before, follow)) +} +pub async fn cli_logs( + ctx: CliContext, + (id, limit, cursor, before, follow): (PackageId, Option, Option, bool, bool), +) -> Result<(), RpcError> { + if follow { + if cursor.is_some() { + return Err(RpcError::from(Error::new( + eyre!("The argument '--cursor ' cannot be used with '--follow'"), + crate::ErrorKind::InvalidRequest, + ))); + } + if before { + return Err(RpcError::from(Error::new( + eyre!("The argument '--before' cannot be used with '--follow'"), + crate::ErrorKind::InvalidRequest, + ))); + } + cli_logs_generic_follow(ctx, "package.logs.follow", Some(id), limit).await + } else { + cli_logs_generic_nofollow(ctx, "package.logs", Some(id), limit, cursor, before).await + } +} +pub async fn logs_nofollow( + _ctx: (), + (id, limit, cursor, before, _): (PackageId, Option, Option, bool, bool), ) -> Result { - Ok(fetch_logs( - LogSource::Container(id), - limit, - cursor, - before_flag.unwrap_or(false), - ) - .await?) + fetch_logs(LogSource::Container(id), limit, cursor, before).await +} +#[command(rpc_only, rename = "follow", display(display_none))] +pub async fn logs_follow( + #[context] ctx: RpcContext, + #[parent_data] (id, limit, _, _, _): (PackageId, Option, Option, bool, bool), +) -> Result { + follow_logs(ctx, LogSource::Container(id), limit).await } -#[instrument] -pub async fn fetch_logs( - id: LogSource, +pub async fn cli_logs_generic_nofollow( + ctx: CliContext, + method: &str, + id: Option, limit: Option, cursor: Option, - before_flag: bool, -) -> Result { - let mut cmd = Command::new("journalctl"); + before: bool, +) -> Result<(), RpcError> { + let res = rpc_toolkit::command_helpers::call_remote( + ctx.clone(), + method, + serde_json::json!({ + "id": id, + "limit": limit, + "cursor": cursor, + "before": before, + }), + PhantomData::, + ) + .await? + .result?; - let limit = limit.unwrap_or(50); + for entry in res.entries.iter() { + println!("{}", entry); + } + + Ok(()) +} + +pub async fn cli_logs_generic_follow( + ctx: CliContext, + method: &str, + id: Option, + limit: Option, +) -> Result<(), RpcError> { + let res = rpc_toolkit::command_helpers::call_remote( + ctx.clone(), + method, + serde_json::json!({ + "id": id, + "limit": limit, + }), + PhantomData::, + ) + .await? + .result?; + + let mut base_url = ctx.base_url.clone(); + let ws_scheme = match base_url.scheme() { + "https" => "wss", + "http" => "ws", + _ => { + return Err(Error::new( + eyre!("Cannot parse scheme from base URL"), + crate::ErrorKind::ParseUrl, + ) + .into()) + } + }; + base_url.set_scheme(ws_scheme).or_else(|_| { + Err(Error::new( + eyre!("Cannot set URL scheme"), + crate::ErrorKind::ParseUrl, + )) + })?; + let (mut stream, _) = + // base_url is "http://127.0.0.1/", with a trailing slash, so we don't put a leading slash in this path: + tokio_tungstenite::connect_async(format!("{}ws/rpc/{}", base_url, res.guid)).await?; + while let Some(log) = stream.try_next().await? { + match log { + Message::Text(log) => { + println!("{}", serde_json::from_str::(&log)?); + } + _ => (), + } + } + + Ok(()) +} + +async fn journalctl( + id: LogSource, + limit: usize, + cursor: Option<&str>, + before: bool, + follow: bool, +) -> Result { + let mut cmd = Command::new("journalctl"); + cmd.kill_on_drop(true); cmd.arg("--output=json"); cmd.arg("--output-fields=MESSAGE"); @@ -163,16 +371,15 @@ pub async fn fetch_logs( } }; - let cursor_formatted = format!("--after-cursor={}", cursor.clone().unwrap_or("".to_owned())); - let mut get_prev_logs_and_reverse = false; + let cursor_formatted = format!("--after-cursor={}", cursor.clone().unwrap_or("")); if cursor.is_some() { cmd.arg(&cursor_formatted); - if before_flag { - get_prev_logs_and_reverse = true; + if before { + cmd.arg("--reverse"); } } - if get_prev_logs_and_reverse { - cmd.arg("--reverse"); + if follow { + cmd.arg("--follow"); } let mut child = cmd.stdout(Stdio::piped()).spawn()?; @@ -185,7 +392,7 @@ pub async fn fetch_logs( let journalctl_entries = LinesStream::new(out.lines()); - let mut deserialized_entries = journalctl_entries + let deserialized_entries = journalctl_entries .map_err(|e| Error::new(e, crate::ErrorKind::Journald)) .and_then(|s| { futures::future::ready( @@ -194,16 +401,37 @@ pub async fn fetch_logs( ) }); + Ok(LogStream { + _child: child, + entries: deserialized_entries.boxed(), + }) +} + +#[instrument] +pub async fn fetch_logs( + id: LogSource, + limit: Option, + cursor: Option, + before: bool, +) -> Result { + let limit = limit.unwrap_or(50); + let mut stream = journalctl(id, limit, cursor.as_deref(), before, false).await?; + let mut entries = Vec::with_capacity(limit); let mut start_cursor = None; - if let Some(first) = deserialized_entries.try_next().await? { + if let Some(first) = tokio::time::timeout(Duration::from_secs(1), stream.try_next()) + .await + .ok() + .transpose()? + .flatten() + { let (cursor, entry) = first.log_entry()?; start_cursor = Some(cursor); entries.push(entry); } - let (mut end_cursor, entries) = deserialized_entries + let (mut end_cursor, entries) = stream .try_fold( (start_cursor.clone(), entries), |(_, mut acc), entry| async move { @@ -215,7 +443,7 @@ pub async fn fetch_logs( .await?; let mut entries = Reversible::new(entries); // reverse again so output is always in increasing chronological order - if get_prev_logs_and_reverse { + if cursor.is_some() && before { entries.reverse(); std::mem::swap(&mut start_cursor, &mut end_cursor); } @@ -226,21 +454,81 @@ pub async fn fetch_logs( }) } +#[instrument(skip(ctx))] +pub async fn follow_logs( + ctx: RpcContext, + id: LogSource, + limit: Option, +) -> Result { + let limit = limit.unwrap_or(50); + let mut stream = journalctl(id, limit, None, false, true).await?; + + let mut start_cursor = None; + let mut first_entry = None; + + if let Some(first) = tokio::time::timeout(Duration::from_secs(1), stream.try_next()) + .await + .ok() + .transpose()? + .flatten() + { + let (cursor, entry) = first.log_entry()?; + start_cursor = Some(cursor); + first_entry = Some(entry); + } + + let guid = RequestGuid::new(); + ctx.add_continuation( + guid.clone(), + RpcContinuation::ws( + Box::new(move |ws_fut| ws_handler(first_entry, stream, ws_fut).boxed()), + Duration::from_secs(30), + ), + ) + .await; + Ok(LogFollowResponse { start_cursor, guid }) +} + +// #[tokio::test] +// pub async fn test_logs() { +// let response = fetch_logs( +// // change `tor.service` to an actual journald unit on your machine +// // LogSource::Service("tor.service"), +// // first run `docker run --name=hello-world.embassy --log-driver=journald hello-world` +// LogSource::Container("hello-world".parse().unwrap()), +// // Some(5), +// None, +// None, +// // Some("s=1b8c418e28534400856c27b211dd94fd;i=5a7;b=97571c13a1284f87bc0639b5cff5acbe;m=740e916;t=5ca073eea3445;x=f45bc233ca328348".to_owned()), +// false, +// true, +// ) +// .await +// .unwrap(); +// let serialized = serde_json::to_string_pretty(&response).unwrap(); +// println!("{}", serialized); +// } + #[tokio::test] pub async fn test_logs() { - let response = fetch_logs( - // change `tor.service` to an actual journald unit on your machine - // LogSource::Service("tor.service"), - // first run `docker run --name=hello-world.embassy --log-driver=journald hello-world` - LogSource::Container("hello-world".parse().unwrap()), - // Some(5), - None, - None, - // Some("s=1b8c418e28534400856c27b211dd94fd;i=5a7;b=97571c13a1284f87bc0639b5cff5acbe;m=740e916;t=5ca073eea3445;x=f45bc233ca328348".to_owned()), - false, - ) - .await - .unwrap(); - let serialized = serde_json::to_string_pretty(&response).unwrap(); - println!("{}", serialized); + let mut cmd = Command::new("journalctl"); + cmd.kill_on_drop(true); + + cmd.arg("-f"); + cmd.arg("CONTAINER_NAME=hello-world.embassy"); + + let mut child = cmd.stdout(Stdio::piped()).spawn().unwrap(); + let out = BufReader::new( + child + .stdout + .take() + .ok_or_else(|| Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald)) + .unwrap(), + ); + + let mut journalctl_entries = LinesStream::new(out.lines()); + + while let Some(line) = journalctl_entries.try_next().await.unwrap() { + dbg!(line); + } } diff --git a/backend/src/system.rs b/backend/src/system.rs index 5b74a2045..0e5fb4910 100644 --- a/backend/src/system.rs +++ b/backend/src/system.rs @@ -1,49 +1,126 @@ use std::fmt; +use color_eyre::eyre::eyre; use futures::FutureExt; use rpc_toolkit::command; +use rpc_toolkit::yajrc::RpcError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::sync::broadcast::Receiver; use tokio::sync::RwLock; use tracing::instrument; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; use crate::disk::util::{get_available, get_used}; -use crate::logs::{display_logs, fetch_logs, LogResponse, LogSource}; +use crate::logs::{ + cli_logs_generic_follow, cli_logs_generic_nofollow, fetch_logs, follow_logs, LogFollowResponse, + LogResponse, LogSource, +}; use crate::shutdown::Shutdown; +use crate::util::display_none; use crate::util::serde::{display_serializable, IoFormat}; use crate::{Error, ErrorKind}; pub const SYSTEMD_UNIT: &'static str = "embassyd"; -#[command(display(display_logs))] +#[command( + custom_cli(cli_logs(async, context(CliContext))), + subcommands(self(logs_nofollow(async)), logs_follow), + display(display_none) +)] pub async fn logs( - #[arg] limit: Option, - #[arg] cursor: Option, - #[arg] before_flag: Option, + #[arg(short = 'l', long = "limit")] limit: Option, + #[arg(short = 'c', long = "cursor")] cursor: Option, + #[arg(short = 'B', long = "before", default)] before: bool, + #[arg(short = 'f', long = "follow", default)] follow: bool, +) -> Result<(Option, Option, bool, bool), Error> { + Ok((limit, cursor, before, follow)) +} +pub async fn cli_logs( + ctx: CliContext, + (limit, cursor, before, follow): (Option, Option, bool, bool), +) -> Result<(), RpcError> { + if follow { + if cursor.is_some() { + return Err(RpcError::from(Error::new( + eyre!("The argument '--cursor ' cannot be used with '--follow'"), + crate::ErrorKind::InvalidRequest, + ))); + } + if before { + return Err(RpcError::from(Error::new( + eyre!("The argument '--before' cannot be used with '--follow'"), + crate::ErrorKind::InvalidRequest, + ))); + } + cli_logs_generic_follow(ctx, "server.logs.follow", None, limit).await + } else { + cli_logs_generic_nofollow(ctx, "server.logs", None, limit, cursor, before).await + } +} +pub async fn logs_nofollow( + _ctx: (), + (limit, cursor, before, _): (Option, Option, bool, bool), ) -> Result { - Ok(fetch_logs( - LogSource::Service(SYSTEMD_UNIT), - limit, - cursor, - before_flag.unwrap_or(false), - ) - .await?) + fetch_logs(LogSource::Service(SYSTEMD_UNIT), limit, cursor, before).await } -#[command(rename = "kernel-logs", display(display_logs))] +#[command(rpc_only, rename = "follow", display(display_none))] +pub async fn logs_follow( + #[context] ctx: RpcContext, + #[parent_data] (limit, _, _, _): (Option, Option, bool, bool), +) -> Result { + follow_logs(ctx, LogSource::Service(SYSTEMD_UNIT), limit).await +} + +#[command( + rename = "kernel-logs", + custom_cli(cli_kernel_logs(async, context(CliContext))), + subcommands(self(kernel_logs_nofollow(async)), kernel_logs_follow), + display(display_none) +)] pub async fn kernel_logs( - #[arg] limit: Option, - #[arg] cursor: Option, - #[arg] before_flag: Option, + #[arg(short = 'l', long = "limit")] limit: Option, + #[arg(short = 'c', long = "cursor")] cursor: Option, + #[arg(short = 'B', long = "before", default)] before: bool, + #[arg(short = 'f', long = "follow", default)] follow: bool, +) -> Result<(Option, Option, bool, bool), Error> { + Ok((limit, cursor, before, follow)) +} +pub async fn cli_kernel_logs( + ctx: CliContext, + (limit, cursor, before, follow): (Option, Option, bool, bool), +) -> Result<(), RpcError> { + if follow { + if cursor.is_some() { + return Err(RpcError::from(Error::new( + eyre!("The argument '--cursor ' cannot be used with '--follow'"), + crate::ErrorKind::InvalidRequest, + ))); + } + if before { + return Err(RpcError::from(Error::new( + eyre!("The argument '--before' cannot be used with '--follow'"), + crate::ErrorKind::InvalidRequest, + ))); + } + cli_logs_generic_follow(ctx, "server.kernel-logs.follow", None, limit).await + } else { + cli_logs_generic_nofollow(ctx, "server.kernel-logs", None, limit, cursor, before).await + } +} +pub async fn kernel_logs_nofollow( + _ctx: (), + (limit, cursor, before, _): (Option, Option, bool, bool), ) -> Result { - Ok(fetch_logs( - LogSource::Kernel, - limit, - cursor, - before_flag.unwrap_or(false), - ) - .await?) + fetch_logs(LogSource::Service(SYSTEMD_UNIT), limit, cursor, before).await +} + +#[command(rpc_only, rename = "follow", display(display_none))] +pub async fn kernel_logs_follow( + #[context] ctx: RpcContext, + #[parent_data] (limit, _, _, _): (Option, Option, bool, bool), +) -> Result { + follow_logs(ctx, LogSource::Service(SYSTEMD_UNIT), limit).await } #[derive(Serialize, Deserialize)] diff --git a/frontend/angular.json b/frontend/angular.json index 6d2c02db3..2e5e7a5b6 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -427,7 +427,8 @@ } }, "cli": { - "schematicCollections": ["@ionic/angular-toolkit"] + "schematicCollections": ["@ionic/angular-toolkit"], + "analytics": false }, "schematics": { "@ionic/angular-toolkit:component": { diff --git a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.html b/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.html index 151ca61ee..6abfaa929 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.html +++ b/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.html @@ -7,41 +7,51 @@ - - - + +
-
-
-
- - Load More - - - +
+
+
- +
-
diff --git a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts b/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts index 05d92749c..317cd1ea3 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts +++ b/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts @@ -1,7 +1,8 @@ import { Component, ViewChild } from '@angular/core' import { IonContent } from '@ionic/angular' import { ApiService } from 'src/app/services/api/api.service' -import { toLocalIsoString } from '@start9labs/shared' +import { ErrorToastService, toLocalIsoString } from '@start9labs/shared' + var Convert = require('ansi-to-html') var convert = new Convert({ bg: 'transparent', @@ -15,122 +16,80 @@ var convert = new Convert({ export class LogsPage { @ViewChild(IonContent) private content?: IonContent loading = true - loadingMore = false needInfinite = true startCursor?: string - endCursor?: string limit = 200 isOnBottom = true - constructor(private readonly api: ApiService) {} + constructor( + private readonly api: ApiService, + private readonly errToast: ErrorToastService, + ) {} - ngOnInit() { - this.getLogs() + async ngOnInit() { + await this.getLogs() + this.loading = false } - async getLogs() { - try { - // get logs - const logs = await this.fetch() - - if (!logs?.length) return - - const container = document.getElementById('container') - const beforeContainerHeight = container?.scrollHeight || 0 - const newLogs = document.getElementById('template')?.cloneNode(true) - - if (!(newLogs instanceof HTMLElement)) return - - newLogs.innerHTML = - logs - .map( - l => - `${toLocalIsoString( - new Date(l.timestamp), - )} ${convert.toHtml(l.message)}`, - ) - .join('\n') + (logs.length ? '\n' : '') - container?.prepend(newLogs) - - const afterContainerHeight = container?.scrollHeight || 0 - - // scroll down - scrollBy(0, afterContainerHeight - beforeContainerHeight) - this.content?.scrollToPoint( - 0, - afterContainerHeight - beforeContainerHeight, - ) - - if (logs.length < this.limit) { - this.needInfinite = false - } - } catch (e) {} - } - - async fetch(isBefore: boolean = true) { - try { - const cursor = isBefore ? this.startCursor : this.endCursor - - const logsRes = await this.api.getLogs({ - cursor, - before_flag: !!cursor ? isBefore : undefined, - limit: this.limit, - }) - - if ((isBefore || this.startCursor) && logsRes['start-cursor']) { - this.startCursor = logsRes['start-cursor'] - } - - if ((!isBefore || !this.endCursor) && logsRes['end-cursor']) { - this.endCursor = logsRes['end-cursor'] - } - this.loading = false - - return logsRes.entries - } catch (e) { - console.error(e) - } - } - - async loadMore() { - try { - this.loadingMore = true - const logs = await this.fetch(false) - - if (!logs?.length) return (this.loadingMore = false) - - const container = document.getElementById('container') - const newLogs = document.getElementById('template')?.cloneNode(true) - - if (!(newLogs instanceof HTMLElement)) return - - newLogs.innerHTML = - logs - .map( - l => - `${toLocalIsoString( - new Date(l.timestamp), - )} ${convert.toHtml(l.message)}`, - ) - .join('\n') + (logs.length ? '\n' : '') - container?.append(newLogs) - this.loadingMore = false - this.scrollEvent() - } catch (e) {} - } - - scrollEvent() { - const buttonDiv = document.getElementById('button-div') + scrollEnd() { + const bottomDiv = document.getElementById('bottom-div') this.isOnBottom = - !!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight + !!bottomDiv && + bottomDiv.getBoundingClientRect().top - 420 < window.innerHeight } scrollToBottom() { this.content?.scrollToBottom(500) } - async loadData(e: any): Promise { + async doInfinite(e: any): Promise { await this.getLogs() e.target.complete() } + + private async getLogs() { + try { + const { 'start-cursor': startCursor, entries } = await this.api.getLogs({ + cursor: this.startCursor, + before: !!this.startCursor, + limit: this.limit, + }) + + if (!entries.length) return + + this.startCursor = startCursor + + const container = document.getElementById('container') + const newLogs = document.getElementById('template')?.cloneNode(true) + + if (!(newLogs instanceof HTMLElement)) return + + newLogs.innerHTML = entries + .map( + entry => + `${toLocalIsoString( + new Date(entry.timestamp), + )} ${convert.toHtml(entry.message)}`, + ) + .join('\n') + + const beforeContainerHeight = container?.scrollHeight || 0 + container?.prepend(newLogs) + const afterContainerHeight = container?.scrollHeight || 0 + + // scroll down + setTimeout(() => { + this.content?.scrollToPoint( + 0, + afterContainerHeight - beforeContainerHeight, + ) + }, 50) + + if (entries.length < this.limit) { + this.needInfinite = false + } + } catch (e: any) { + this.errToast.present(e) + } + } } diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts b/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts index c11881e5e..d1885a46f 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts @@ -1,9 +1,11 @@ +import { LogsRes, ServerLogsReq } from '@start9labs/shared' + export abstract class ApiService { abstract getError(): Promise abstract restart(): Promise abstract forgetDrive(): Promise abstract repairDisk(): Promise - abstract getLogs(params: GetLogsReq): Promise + abstract getLogs(params: ServerLogsReq): Promise } export interface GetErrorRes { @@ -11,21 +13,3 @@ export interface GetErrorRes { message: string data: { details: string } } - -export type GetLogsReq = { - cursor?: string - before_flag?: boolean - limit?: number -} -export type GetLogsRes = LogsRes - -export type LogsRes = { - entries: Log[] - 'start-cursor'?: string - 'end-cursor'?: string -} - -export interface Log { - timestamp: string - message: string -} diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts b/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts index e4e613ffa..b61a97ac9 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core' -import { HttpService } from '../http.service' -import { ApiService, GetErrorRes, GetLogsReq, GetLogsRes } from './api.service' +import { HttpService } from '@start9labs/shared' +import { ApiService, GetErrorRes } from './api.service' +import { LogsRes, ServerLogsReq } from '@start9labs/shared' @Injectable() export class LiveApiService extends ApiService { @@ -36,8 +37,8 @@ export class LiveApiService extends ApiService { }) } - getLogs(params: GetLogsReq): Promise { - return this.http.rpcRequest({ + getLogs(params: ServerLogsReq): Promise { + return this.http.rpcRequest({ method: 'diagnostic.logs', params, }) diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts b/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts index 85bb2f6d4..b99e0979d 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts @@ -1,12 +1,7 @@ import { Injectable } from '@angular/core' import { pauseFor } from '@start9labs/shared' -import { - ApiService, - GetErrorRes, - GetLogsReq, - GetLogsRes, - Log, -} from './api.service' +import { ApiService, GetErrorRes } from './api.service' +import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared' @Injectable() export class MockApiService extends ApiService { @@ -35,7 +30,7 @@ export class MockApiService extends ApiService { await pauseFor(1000) } - async getLogs(params: GetLogsReq): Promise { + async getLogs(params: ServerLogsReq): Promise { await pauseFor(1000) let entries: Log[] if (Math.random() < 0.2) { diff --git a/frontend/projects/diagnostic-ui/src/app/services/http.service.ts b/frontend/projects/diagnostic-ui/src/app/services/http.service.ts deleted file mode 100644 index 78cf4194c..000000000 --- a/frontend/projects/diagnostic-ui/src/app/services/http.service.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Injectable } from '@angular/core' -import { HttpClient } from '@angular/common/http' -import { HttpError, RpcError } from '@start9labs/shared' -import { firstValueFrom } from 'rxjs' - -@Injectable({ - providedIn: 'root', -}) -export class HttpService { - constructor(private readonly http: HttpClient) {} - - async rpcRequest(options: RPCOptions): Promise { - const res = await this.httpRequest>(options) - if (isRpcError(res)) throw new RpcError(res.error) - return res.result - } - - async httpRequest(body: RPCOptions): Promise { - const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/rpc/v1` - return firstValueFrom(this.http.post(url, body)).catch(e => { - throw new HttpError(e) - }) - } -} - -function isRpcError( - arg: { error: Error } | { result: Result }, -): arg is { error: Error } { - return (arg as any).error !== undefined -} - -export interface RPCOptions { - method: string - params: { [param: string]: Params } -} - -export interface RequestError { - code: number - message: string - details: string -} - -export type Params = string | number | boolean | object | string[] | number[] - -interface RPCBase { - jsonrpc: '2.0' - id: string -} - -export interface RPCRequest extends RPCBase { - method: string - params?: T -} - -export interface RPCSuccess extends RPCBase { - result: T -} - -export interface RPCError extends RPCBase { - error: { - code: number - message: string - data?: - | { - details: string - } - | string - } -} - -export type RPCResponse = RPCSuccess | RPCError diff --git a/frontend/projects/setup-wizard/src/app/app.module.ts b/frontend/projects/setup-wizard/src/app/app.module.ts index 291ab8abc..dc888bad1 100644 --- a/frontend/projects/setup-wizard/src/app/app.module.ts +++ b/frontend/projects/setup-wizard/src/app/app.module.ts @@ -5,7 +5,6 @@ import { HttpClientModule } from '@angular/common/http' import { ApiService } from './services/api/api.service' import { MockApiService } from './services/api/mock-api.service' import { LiveApiService } from './services/api/live-api.service' -import { HttpService } from './services/api/http.service' import { IonicModule, IonicRouteStrategy, @@ -45,14 +44,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: ApiService, - useFactory: (http: HttpService) => { - if (useMocks) { - return new MockApiService() - } else { - return new LiveApiService(http) - } - }, - deps: [HttpService], + useClass: useMocks ? MockApiService : LiveApiService, }, { provide: ErrorHandler, useClass: GlobalErrorHandler }, ], diff --git a/frontend/projects/setup-wizard/src/app/guards/nav-guard.ts b/frontend/projects/setup-wizard/src/app/guards/nav-guard.ts index 893c7ebf2..3eafa03f6 100644 --- a/frontend/projects/setup-wizard/src/app/guards/nav-guard.ts +++ b/frontend/projects/setup-wizard/src/app/guards/nav-guard.ts @@ -1,19 +1,19 @@ import { Injectable } from '@angular/core' import { CanActivate, Router } from '@angular/router' -import { HttpService } from '../services/api/http.service' +import { RPCEncryptedService } from '../services/rpc-encrypted.service' import { StateService } from '../services/state.service' @Injectable({ providedIn: 'root', }) export class NavGuard implements CanActivate { - constructor ( + constructor( private readonly router: Router, - private readonly httpService: HttpService, - ) { } + private readonly encrypted: RPCEncryptedService, + ) {} - canActivate (): boolean { - if (this.httpService.productKey) { + canActivate(): boolean { + if (this.encrypted.productKey) { return true } else { this.router.navigateByUrl('product-key') @@ -26,14 +26,14 @@ export class NavGuard implements CanActivate { providedIn: 'root', }) export class RecoveryNavGuard implements CanActivate { - constructor ( + constructor( private readonly router: Router, - private readonly httpService: HttpService, + private readonly encrypted: RPCEncryptedService, private readonly stateService: StateService, - ) { } + ) {} - canActivate (): boolean { - if (this.httpService.productKey || !this.stateService.hasProductKey) { + canActivate(): boolean { + if (this.encrypted.productKey || !this.stateService.hasProductKey) { return true } else { this.router.navigateByUrl('product-key') diff --git a/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts b/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts index 5bc6b9f66..87a265dea 100644 --- a/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts +++ b/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts @@ -1,7 +1,7 @@ import { Component, Input, ViewChild } from '@angular/core' import { IonInput, LoadingController, ModalController } from '@ionic/angular' import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service' -import { HttpService } from 'src/app/services/api/http.service' +import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service' @Component({ selector: 'prod-key-modal', @@ -20,7 +20,7 @@ export class ProdKeyModal { private readonly modalController: ModalController, private readonly apiService: ApiService, private readonly loadingCtrl: LoadingController, - private readonly httpService: HttpService, + private readonly encrypted: RPCEncryptedService, ) {} ngAfterViewInit() { @@ -37,11 +37,11 @@ export class ProdKeyModal { try { await this.apiService.set02XDrive(this.target.logicalname) - this.httpService.productKey = this.productKey + this.encrypted.productKey = this.productKey await this.apiService.verifyProductKey() this.modalController.dismiss({ productKey: this.productKey }, 'success') } catch (e) { - this.httpService.productKey = undefined + this.encrypted.productKey = undefined this.error = 'Invalid Product Key' } finally { loader.dismiss() diff --git a/frontend/projects/setup-wizard/src/app/pages/product-key/product-key.page.ts b/frontend/projects/setup-wizard/src/app/pages/product-key/product-key.page.ts index 5594667f8..f0e73bca3 100644 --- a/frontend/projects/setup-wizard/src/app/pages/product-key/product-key.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/product-key/product-key.page.ts @@ -1,7 +1,7 @@ import { Component, ViewChild } from '@angular/core' import { IonInput, LoadingController, NavController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/api.service' -import { HttpService } from 'src/app/services/api/http.service' +import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service' import { StateService } from 'src/app/services/state.service' @Component({ @@ -19,7 +19,7 @@ export class ProductKeyPage { private readonly stateService: StateService, private readonly apiService: ApiService, private readonly loadingCtrl: LoadingController, - private readonly httpService: HttpService, + private readonly encrypted: RPCEncryptedService, ) {} ionViewDidEnter() { @@ -35,7 +35,7 @@ export class ProductKeyPage { await loader.present() try { - this.httpService.productKey = this.productKey + this.encrypted.productKey = this.productKey await this.apiService.verifyProductKey() if (this.stateService.isMigrating) { await this.navCtrl.navigateForward(`/loading`) @@ -44,7 +44,7 @@ export class ProductKeyPage { } } catch (e) { this.error = 'Invalid Product Key' - this.httpService.productKey = undefined + this.encrypted.productKey = undefined } finally { loader.dismiss() } diff --git a/frontend/projects/setup-wizard/src/app/pages/success/success.page.html b/frontend/projects/setup-wizard/src/app/pages/success/success.page.html index 14ee4a253..5fb046cb5 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/success.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/success/success.page.html @@ -8,7 +8,7 @@ style="font-size: 80px" name="checkmark-circle-outline" > - Setup Complete! + Setup Complete
@@ -17,61 +17,21 @@ >

You can now safely unplug your backup drive.

- -
-

Tor Instructions:

- -
+

+ You have successully claimed your Embassy! You can now access your + device using the methods below. +

-
-
-

- To use your Embassy over Tor, visit its unique Tor address - from any Tor-enabled browser. For a list of recommended - browsers, click - here. -

-
-

Tor Address

- - - {{ torAddress }} - - - - - -
-
-
-
+
+ +

+ Note: embassy.local was for setup purposes only, it will no + longer work. +

-

LAN Instructions (Slightly Advanced):

+

From Home (LAN)

-

To use your Embassy locally, you must:

-
    -
  1. - Currently be connected to the same Local Area Network (LAN) - as your Embassy. -
  2. -
  3. Download your Embassy's Root Certificate Authority.
  4. -
  5. - Trust your Embassy's Root CA on both your - computer/phone and in your browser settings. -
  6. -

- For step-by-step instructions, click - here. + Visit the address below when you are conncted to the same WiFi + or Local Area Network (LAN) as your Embassy:

-

- Please note, once setup is complete, the embassy.local - address will no longer connect to your Embassy. -

- - - Download Root CA - - - -

LAN Address

- {{ lanAddress }}{{ lanAddress }} + +

+ Important! + Your browser will warn you that the website is untrusted. You + can bypass this warning on most browsers. The warning will go + away after you + + download and trust + + your Embassy's Root Certificate Authority. +

+ + + Download Root CA + +
+

+ + +
+

On The Go (Tor)

+ +
+ +
+
+

Visit the address below when you are away from home:

+ + + + {{ torAddress }} + + + + + + +

+ Important! + This address will only work from a + + Tor-enabled browser . +

+
+ +
+
+
+
{ - const success = await this.copyToClipboard(address) + const success = await copyToClipboard(address) const message = success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.' @@ -70,49 +78,24 @@ export class SuccessPage { } installCert() { - document.getElementById('install-cert')?.click() + this.document.getElementById('install-cert')?.click() } download() { - const torAddress = document.getElementById('tor-addr') - const lanAddress = document.getElementById('lan-addr') + const torAddress = this.document.getElementById('tor-addr') + const lanAddress = this.document.getElementById('lan-addr') if (torAddress) torAddress.innerHTML = this.stateService.torAddress if (lanAddress) lanAddress.innerHTML = this.stateService.lanAddress - document + this.document .getElementById('cert') ?.setAttribute( 'href', 'data:application/x-x509-ca-cert;base64,' + encodeURIComponent(this.stateService.cert), ) - let html = document.getElementById('downloadable')?.innerHTML || '' - const filename = 'embassy-info.html' - - const elem = document.createElement('a') - elem.setAttribute( - 'href', - 'data:text/plain;charset=utf-8,' + encodeURIComponent(html), - ) - elem.setAttribute('download', filename) - elem.style.display = 'none' - - document.body.appendChild(elem) - elem.click() - document.body.removeChild(elem) - } - - private async copyToClipboard(str: string): Promise { - const el = document.createElement('textarea') - el.value = str - el.setAttribute('readonly', '') - el.style.position = 'absolute' - el.style.left = '-9999px' - document.body.appendChild(el) - el.select() - const copy = document.execCommand('copy') - document.body.removeChild(el) - return copy + let html = this.document.getElementById('downloadable')?.innerHTML || '' + this.downloadHtml.download('embassy-info.html', html) } } diff --git a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts index 0e719ae4d..d64d1e398 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts @@ -13,42 +13,42 @@ export abstract class ApiService { abstract setupComplete(): Promise // setup.complete } -export interface GetStatusRes { +export type GetStatusRes = { 'product-key': boolean migrating: boolean } -export interface ImportDriveReq { +export type ImportDriveReq = { guid: string 'embassy-password': string } -export interface SetupEmbassyReq { +export type SetupEmbassyReq = { 'embassy-logicalname': string 'embassy-password': string 'recovery-source': CifsRecoverySource | DiskRecoverySource | null 'recovery-password': string | null } -export interface SetupEmbassyRes { +export type SetupEmbassyRes = { 'tor-address': string 'lan-address': string 'root-ca': string } -export interface EmbassyOSRecoveryInfo { +export type EmbassyOSRecoveryInfo = { version: string full: boolean 'password-hash': string | null 'wrapped-key': string | null } -export interface DiskListResponse { +export type DiskListResponse = { disks: DiskInfo[] reconnect: string[] } -export interface DiskBackupTarget { +export type DiskBackupTarget = { vendor: string | null model: string | null logicalname: string | null @@ -58,7 +58,7 @@ export interface DiskBackupTarget { 'embassy-os': EmbassyOSRecoveryInfo | null } -export interface CifsBackupTarget { +export type CifsBackupTarget = { hostname: string path: string username: string @@ -66,12 +66,12 @@ export interface CifsBackupTarget { 'embassy-os': EmbassyOSRecoveryInfo | null } -export interface DiskRecoverySource { +export type DiskRecoverySource = { type: 'disk' logicalname: string // partition logicalname } -export interface CifsRecoverySource { +export type CifsRecoverySource = { type: 'cifs' hostname: string path: string @@ -79,7 +79,7 @@ export interface CifsRecoverySource { password: string | null } -export interface DiskInfo { +export type DiskInfo = { logicalname: string vendor: string | null model: string | null @@ -88,13 +88,13 @@ export interface DiskInfo { guid: string | null // cant back up if guid exists } -export interface RecoveryStatusRes { +export type RecoveryStatusRes = { 'bytes-transferred': number 'total-bytes': number complete: boolean } -export interface PartitionInfo { +export type PartitionInfo = { logicalname: string label: string | null capacity: number diff --git a/frontend/projects/setup-wizard/src/app/services/api/http.service.ts b/frontend/projects/setup-wizard/src/app/services/api/http.service.ts deleted file mode 100644 index 9d75e5197..000000000 --- a/frontend/projects/setup-wizard/src/app/services/api/http.service.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { Injectable } from '@angular/core' -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' -import { firstValueFrom, Observable } from 'rxjs' -import * as aesjs from 'aes-js' -import * as pbkdf2 from 'pbkdf2' -import { HttpError, RpcError } from '@start9labs/shared' - -@Injectable({ - providedIn: 'root', -}) -export class HttpService { - fullUrl: string - productKey?: string - - constructor(private readonly http: HttpClient) { - const port = window.location.port - this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}/rpc/v1` - } - - async rpcRequest(body: RPCOptions, encrypted = true): Promise { - const httpOpts = { - method: Method.POST, - body, - url: this.fullUrl, - } - - let res: RPCResponse - - if (encrypted) { - res = await this.encryptedHttpRequest>(httpOpts) - } else { - res = await this.httpRequest>(httpOpts) - } - - if (isRpcError(res)) { - console.error('RPC ERROR: ', res) - throw new RpcError(res.error) - } - - return res.result - } - - async encryptedHttpRequest(httpOpts: { - body: RPCOptions - url: string - }): Promise { - const urlIsRelative = httpOpts.url.startsWith('/') - const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url - - const encryptedBody = await AES_CTR.encryptPbkdf2( - this.productKey || '', - encodeUtf8(JSON.stringify(httpOpts.body)), - ) - const options = { - responseType: 'arraybuffer', - body: encryptedBody.buffer, - observe: 'response', - headers: { - 'Content-Encoding': 'aesctr256', - 'Content-Type': 'application/json', - }, - } as any - - const req = this.http.post(url, options.body, options) - - return firstValueFrom(req) - .then(res => - AES_CTR.decryptPbkdf2( - this.productKey || '', - (res as any).body as ArrayBuffer, - ), - ) - .then(res => JSON.parse(res)) - .catch(e => { - if (!e.status && !e.statusText) { - throw new EncryptionError() - } else { - throw new HttpError(e) - } - }) - } - - async httpRequest(httpOpts: { - body: RPCOptions - url: string - }): Promise { - const urlIsRelative = httpOpts.url.startsWith('/') - const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url - - const options = { - responseType: 'json', - body: httpOpts.body, - observe: 'response', - headers: { - 'content-type': 'application/json', - accept: 'application/json', - }, - } as any - - const req: Observable<{ body: T }> = this.http.post( - url, - httpOpts.body, - options, - ) as any - - return firstValueFrom(req) - .then(res => res.body) - .catch(e => { - throw new HttpError(e) - }) - } -} - -class EncryptionError { - readonly code = null - readonly message = 'Invalid Key' - readonly details = null -} - -function isRpcError( - arg: { error: Error } | { result: Result }, -): arg is { error: Error } { - return (arg as any).error !== undefined -} - -export enum Method { - GET = 'GET', - POST = 'POST', - PUT = 'PUT', - PATCH = 'PATCH', - DELETE = 'DELETE', -} - -export interface RPCOptions { - method: string - params?: { - [param: string]: string | number | boolean | object | string[] | number[] - } -} - -interface RPCBase { - jsonrpc: '2.0' - id: string -} - -export interface RPCRequest extends RPCBase { - method: string - params?: T -} - -export interface RPCSuccess extends RPCBase { - result: T -} - -export interface RPCError extends RPCBase { - error: { - code: number - message: string - data?: - | { - details: string - } - | string - } -} - -export type RPCResponse = RPCSuccess | RPCError - -export interface HttpOptions { - method: Method - url: string - headers?: - | HttpHeaders - | { - [header: string]: string | string[] - } - params?: - | HttpParams - | { - [param: string]: string | string[] - } - responseType?: 'json' | 'text' | 'arrayBuffer' - withCredentials?: boolean - body?: any - timeout?: number -} - -type AES_CTR = { - encryptPbkdf2: ( - secretKey: string, - messageBuffer: Uint8Array, - ) => Promise - decryptPbkdf2: (secretKey: string, arr: ArrayBuffer) => Promise -} - -export const AES_CTR: AES_CTR = { - encryptPbkdf2: async (secretKey: string, messageBuffer: Uint8Array) => { - const salt = window.crypto.getRandomValues(new Uint8Array(16)) - const counter = window.crypto.getRandomValues(new Uint8Array(16)) - - const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') - - const aesCtr = new aesjs.ModeOfOperation.ctr( - key, - new aesjs.Counter(counter), - ) - const encryptedBytes = aesCtr.encrypt(messageBuffer) - return new Uint8Array([...counter, ...salt, ...encryptedBytes]) - }, - decryptPbkdf2: async (secretKey: string, arr: ArrayBuffer) => { - const buff = new Uint8Array(arr) - const counter = buff.slice(0, 16) - const salt = buff.slice(16, 32) - - const cipher = buff.slice(32) - const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') - - const aesCtr = new aesjs.ModeOfOperation.ctr( - key, - new aesjs.Counter(counter), - ) - const decryptedBytes = aesCtr.decrypt(cipher) - - return aesjs.utils.utf8.fromBytes(decryptedBytes) - }, -} - -export const encode16 = (buffer: Uint8Array) => - buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') -export const decode16 = (hexString: string) => - new Uint8Array( - hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || [], - ) - -export function encodeUtf8(str: string): Uint8Array { - const encoder = new TextEncoder() - return encoder.encode(str) -} - -export function decodeUtf8(arr: Uint8Array): string { - return new TextDecoder().decode(arr) -} diff --git a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts index 36c2f714d..264767a00 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core' +import { HttpService } from '@start9labs/shared' import { ApiService, CifsRecoverySource, @@ -11,79 +12,70 @@ import { SetupEmbassyReq, SetupEmbassyRes, } from './api.service' -import { HttpService } from './http.service' +import { RPCEncryptedService } from '../rpc-encrypted.service' @Injectable({ providedIn: 'root', }) export class LiveApiService extends ApiService { - constructor(private readonly http: HttpService) { + constructor( + private readonly unencrypted: HttpService, + private readonly encrypted: RPCEncryptedService, + ) { super() } // ** UNENCRYPTED ** async getStatus() { - return this.http.rpcRequest( - { - method: 'setup.status', - params: {}, - }, - false, - ) + return this.unencrypted.rpcRequest({ + method: 'setup.status', + params: {}, + }) } async getDrives() { - return this.http.rpcRequest( - { - method: 'setup.disk.list', - params: {}, - }, - false, - ) + return this.unencrypted.rpcRequest({ + method: 'setup.disk.list', + params: {}, + }) } async set02XDrive(logicalname: string) { - return this.http.rpcRequest( - { - method: 'setup.recovery.v2.set', - params: { logicalname }, - }, - false, - ) + return this.unencrypted.rpcRequest({ + method: 'setup.recovery.v2.set', + params: { logicalname }, + }) } async getRecoveryStatus() { - return this.http.rpcRequest( - { - method: 'setup.recovery.status', - params: {}, - }, - false, - ) + return this.unencrypted.rpcRequest({ + method: 'setup.recovery.status', + params: {}, + }) } // ** ENCRYPTED ** async verifyCifs(source: CifsRecoverySource) { source.path = source.path.replace('/\\/g', '/') - return this.http.rpcRequest({ + return this.encrypted.rpcRequest({ method: 'setup.cifs.verify', - params: source as any, + params: source, }) } async verifyProductKey() { - return this.http.rpcRequest({ + return this.encrypted.rpcRequest({ method: 'echo', params: { message: 'hello' }, }) } async importDrive(params: ImportDriveReq) { - const res = await this.http.rpcRequest({ + const res = await this.encrypted.rpcRequest({ method: 'setup.attach', - params: params as any, + params, }) return { @@ -99,9 +91,9 @@ export class LiveApiService extends ApiService { ].path.replace('/\\/g', '/') } - const res = await this.http.rpcRequest({ + const res = await this.encrypted.rpcRequest({ method: 'setup.execute', - params: setupInfo as any, + params: setupInfo, }) return { @@ -111,7 +103,7 @@ export class LiveApiService extends ApiService { } async setupComplete() { - const res = await this.http.rpcRequest({ + const res = await this.encrypted.rpcRequest({ method: 'setup.complete', params: {}, }) diff --git a/frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts b/frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts new file mode 100644 index 000000000..afd7a6444 --- /dev/null +++ b/frontend/projects/setup-wizard/src/app/services/rpc-encrypted.service.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@angular/core' +import * as aesjs from 'aes-js' +import * as pbkdf2 from 'pbkdf2' +import { + HttpError, + RpcError, + HttpService, + RPCOptions, + Method, + RPCResponse, + isRpcError, +} from '@start9labs/shared' + +@Injectable({ + providedIn: 'root', +}) +export class RPCEncryptedService { + productKey?: string + + constructor(private readonly http: HttpService) {} + + async rpcRequest(opts: Omit): Promise { + const encryptedBody = await AES_CTR.encryptPbkdf2( + this.productKey || '', + encodeUtf8(JSON.stringify(opts)), + ) + + const res: RPCResponse = await this.http + .httpRequest({ + method: Method.POST, + url: this.http.relativeUrl, + body: encryptedBody.buffer, + responseType: 'arrayBuffer', + headers: { + 'Content-Encoding': 'aesctr256', + 'Content-Type': 'application/json', + }, + }) + .then(body => AES_CTR.decryptPbkdf2(this.productKey || '', body)) + .then(res => JSON.parse(res)) + .catch(e => { + if (!e.status && !e.statusText) { + throw new EncryptionError() + } else { + throw new HttpError(e) + } + }) + if (isRpcError(res)) throw new RpcError(res.error) + return res.result + } +} + +class EncryptionError { + readonly code = null + readonly message = 'Invalid Key' + readonly details = null +} + +type AES_CTR = { + encryptPbkdf2: ( + secretKey: string, + messageBuffer: Uint8Array, + ) => Promise + decryptPbkdf2: (secretKey: string, arr: ArrayBuffer) => Promise +} + +const AES_CTR: AES_CTR = { + encryptPbkdf2: async (secretKey: string, messageBuffer: Uint8Array) => { + const salt = window.crypto.getRandomValues(new Uint8Array(16)) + const counter = window.crypto.getRandomValues(new Uint8Array(16)) + + const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') + + const aesCtr = new aesjs.ModeOfOperation.ctr( + key, + new aesjs.Counter(counter), + ) + const encryptedBytes = aesCtr.encrypt(messageBuffer) + return new Uint8Array([...counter, ...salt, ...encryptedBytes]) + }, + decryptPbkdf2: async (secretKey: string, arr: ArrayBuffer) => { + const buff = new Uint8Array(arr) + const counter = buff.slice(0, 16) + const salt = buff.slice(16, 32) + + const cipher = buff.slice(32) + const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') + + const aesCtr = new aesjs.ModeOfOperation.ctr( + key, + new aesjs.Counter(counter), + ) + const decryptedBytes = aesCtr.decrypt(cipher) + + return aesjs.utils.utf8.fromBytes(decryptedBytes) + }, +} + +function encodeUtf8(str: string): Uint8Array { + const encoder = new TextEncoder() + return encoder.encode(str) +} diff --git a/frontend/projects/shared/src/components/markdown/markdown.module.ts b/frontend/projects/shared/src/components/markdown/markdown.component.module.ts similarity index 100% rename from frontend/projects/shared/src/components/markdown/markdown.module.ts rename to frontend/projects/shared/src/components/markdown/markdown.component.module.ts diff --git a/frontend/projects/shared/src/components/text-spinner/text-spinner.component.html b/frontend/projects/shared/src/components/text-spinner/text-spinner.component.html index 65019bceb..170f4cc9f 100644 --- a/frontend/projects/shared/src/components/text-spinner/text-spinner.component.html +++ b/frontend/projects/shared/src/components/text-spinner/text-spinner.component.html @@ -1,7 +1,7 @@ - +

{{ text }}

diff --git a/frontend/projects/shared/src/public-api.ts b/frontend/projects/shared/src/public-api.ts index 4cbb0c642..003f5a8aa 100644 --- a/frontend/projects/shared/src/public-api.ts +++ b/frontend/projects/shared/src/public-api.ts @@ -6,9 +6,9 @@ export * from './classes/http-error' export * from './classes/rpc-error' export * from './components/markdown/markdown.component' -export * from './components/markdown/markdown.module' -export * from './components/text-spinner/text-spinner.component.module' +export * from './components/markdown/markdown.component.module' export * from './components/text-spinner/text-spinner.component' +export * from './components/text-spinner/text-spinner.component.module' export * from './directives/element/element.directive' export * from './directives/element/element.module' @@ -27,13 +27,17 @@ export * from './pipes/unit-conversion/unit-conversion.module' export * from './pipes/unit-conversion/unit-conversion.pipe' export * from './services/destroy.service' +export * from './services/download-html.service' export * from './services/emver.service' export * from './services/error-toast.service' +export * from './services/http.service' +export * from './types/api' export * from './types/rpc-error-details' export * from './types/url' export * from './types/workspace-config' +export * from './util/copy-to-clipboard' export * from './util/get-pkg-id' export * from './util/misc.util' export * from './util/to-local-iso-string' diff --git a/frontend/projects/shared/src/services/download-html.service.ts b/frontend/projects/shared/src/services/download-html.service.ts new file mode 100644 index 000000000..db4c1978b --- /dev/null +++ b/frontend/projects/shared/src/services/download-html.service.ts @@ -0,0 +1,29 @@ +import { DOCUMENT } from '@angular/common' +import { Inject, Injectable } from '@angular/core' + +@Injectable() +export class DownloadHTMLService { + constructor(@Inject(DOCUMENT) private readonly document: Document) {} + + async download(filename: string, html: string, styleObj = {}) { + const entries = Object.entries(styleObj) + .map(([k, v]) => `${k}:${v}`) + .join(';') + const styleString = entries ? `` : '' + + console.log('STYLES', styleString) + html = styleString + html + + const elem = this.document.createElement('a') + elem.setAttribute( + 'href', + 'data:text/plain;charset=utf-8,' + encodeURIComponent(html), + ) + elem.setAttribute('download', filename) + elem.style.display = 'none' + + this.document.body.appendChild(elem) + elem.click() + this.document.body.removeChild(elem) + } +} diff --git a/frontend/projects/shared/src/services/http.service.ts b/frontend/projects/shared/src/services/http.service.ts new file mode 100644 index 000000000..9a43c7784 --- /dev/null +++ b/frontend/projects/shared/src/services/http.service.ts @@ -0,0 +1,199 @@ +import { Inject, Injectable } from '@angular/core' +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' +import { HttpError, RpcError, WorkspaceConfig } from '@start9labs/shared' +import { + firstValueFrom, + from, + interval, + lastValueFrom, + map, + Observable, + race, + take, +} from 'rxjs' +import { DOCUMENT } from '@angular/common' + +const { + ui: { api }, +} = require('../../../../config.json') as WorkspaceConfig + +@Injectable({ + providedIn: 'root', +}) +export class HttpService { + relativeUrl = `/${api.url}/${api.version}` + private fullUrl: string + + constructor( + @Inject(DOCUMENT) private readonly document: Document, + private readonly http: HttpClient, + ) { + const { protocol, hostname, port } = this.document.location + this.fullUrl = `${protocol}//${hostname}:${port}` + } + + async rpcRequest(opts: RPCOptions): Promise { + const { method, params, timeout } = opts + + const res = await this.httpRequest>({ + method: Method.POST, + url: this.relativeUrl, + body: { method, params }, + timeout, + }) + if (isRpcError(res)) throw new RpcError(res.error) + return res.result + } + + async httpRequest(opts: HttpOptions): Promise { + let { method, url, headers, body, responseType, timeout } = opts + + url = opts.url.startsWith('/') ? this.fullUrl + url : url + + const { params } = opts + if (hasParams(params)) { + Object.keys(params).forEach(key => { + if (params[key] === undefined) { + delete params[key] + } + }) + } + + const options: HttpAngularOptions = { + observe: 'response', + withCredentials: true, + headers, + params, + responseType: responseType || 'json', + } + + let req: Observable<{ body: T }> + if (method === Method.GET) { + req = this.http.get(url, options as any) as any + } else { + req = this.http.post(url, body, options as any) as any + } + + return firstValueFrom(timeout ? withTimeout(req, timeout) : req) + .then(res => res.body) + .catch(e => { + throw new HttpError(e) + }) + } +} + +// ** RPC types ** + +interface RPCBase { + jsonrpc: '2.0' + id: string +} + +export interface RPCRequest extends RPCBase { + method: string + params?: T +} + +export interface RPCSuccess extends RPCBase { + result: T +} + +export interface RPCError extends RPCBase { + error: { + code: number + message: string + data?: + | { + details: string + } + | string + } +} + +export type RPCResponse = RPCSuccess | RPCError + +export interface RPCOptions { + method: string + params: { + [param: string]: + | string + | number + | boolean + | object + | string[] + | number[] + | null + } + timeout?: number +} + +export function isRpcError( + arg: { error: Error } | { result: Result }, +): arg is { error: Error } { + return (arg as any).error !== undefined +} + +// ** HTTP types ** + +export enum Method { + GET = 'GET', + POST = 'POST', +} + +export interface HttpOptions { + method: Method + url: string + headers?: + | HttpHeaders + | { + [header: string]: string | string[] + } + params?: + | HttpParams + | { + [param: string]: string | string[] + } + responseType?: 'json' | 'text' | 'arrayBuffer' + body?: any + timeout?: number +} + +interface HttpAngularOptions { + observe: 'response' + withCredentials: true + headers?: + | HttpHeaders + | { + [header: string]: string | string[] + } + params?: + | HttpParams + | { + [param: string]: string | string[] + } + responseType?: 'json' | 'text' | 'arrayBuffer' +} + +function hasParams( + params?: HttpOptions['params'], +): params is Record { + return !!params +} + +function withTimeout(req: Observable, timeout: number): Observable { + return race( + from(lastValueFrom(req)), // this guarantees it only emits on completion, intermediary emissions are suppressed. + interval(timeout).pipe( + take(1), + map(() => { + throw new Error('timeout') + }), + ), + ) +} + +export interface RequestError { + code: number + message: string + details: string +} diff --git a/frontend/projects/shared/src/types/api.ts b/frontend/projects/shared/src/types/api.ts new file mode 100644 index 000000000..5c483d50e --- /dev/null +++ b/frontend/projects/shared/src/types/api.ts @@ -0,0 +1,16 @@ +export type ServerLogsReq = { + before: boolean + cursor?: string + limit?: number +} + +export type LogsRes = { + entries: Log[] + 'start-cursor'?: string + 'end-cursor'?: string +} + +export interface Log { + timestamp: string + message: string +} diff --git a/frontend/projects/shared/src/util/copy-to-clipboard.ts b/frontend/projects/shared/src/util/copy-to-clipboard.ts new file mode 100644 index 000000000..79d48dd79 --- /dev/null +++ b/frontend/projects/shared/src/util/copy-to-clipboard.ts @@ -0,0 +1,19 @@ +export async function copyToClipboard(str: string): Promise { + if (window.isSecureContext) { + return navigator.clipboard + .writeText(str) + .then(() => true) + .catch(() => false) + } + + const el = document.createElement('textarea') + el.value = str + el.setAttribute('readonly', '') + el.style.position = 'absolute' + el.style.left = '-9999px' + document.body.appendChild(el) + el.select() + const didCopy = document.execCommand('copy') + document.body.removeChild(el) + return didCopy +} diff --git a/frontend/projects/shared/tsconfig.json b/frontend/projects/shared/tsconfig.json index e3a6b521c..e1f4625bf 100644 --- a/frontend/projects/shared/tsconfig.json +++ b/frontend/projects/shared/tsconfig.json @@ -6,8 +6,7 @@ "outDir": "../../out-tsc/lib", "declaration": true, "declarationMap": true, - "inlineSources": true, - "types": [] + "inlineSources": true }, "exclude": ["src/test.ts", "**/*.spec.ts"] } diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.html b/frontend/projects/ui/src/app/app/menu/menu.component.html index 3bd06c372..0351745f4 100644 --- a/frontend/projects/ui/src/app/app/menu/menu.component.html +++ b/frontend/projects/ui/src/app/app/menu/menu.component.html @@ -1,4 +1,4 @@ -
diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.ts b/frontend/projects/ui/src/app/app/menu/menu.component.ts index 3a45c989a..a16f872e5 100644 --- a/frontend/projects/ui/src/app/app/menu/menu.component.ts +++ b/frontend/projects/ui/src/app/app/menu/menu.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { AlertController } from '@ionic/angular' -import { ConfigService } from '../../services/config.service' import { LocalStorageService } from '../../services/local-storage.service' import { EOSService } from '../../services/eos.service' import { ApiService } from '../../services/api/embassy-api.service' @@ -62,7 +61,6 @@ export class MenuComponent { .pipe(map(pkgs => pkgs.length)) constructor( - private readonly config: ConfigService, private readonly alertCtrl: AlertController, private readonly embassyApi: ApiService, private readonly authService: AuthService, @@ -73,12 +71,6 @@ export class MenuComponent { private readonly marketplaceService: MarketplaceService, ) {} - get href(): string { - return this.config.isTor() - ? 'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion' - : 'https://start9.com' - } - async presentAlertLogout() { const alert = await this.alertCtrl.create({ header: 'Caution', diff --git a/frontend/projects/ui/src/app/components/logs/logs.component.html b/frontend/projects/ui/src/app/components/logs/logs.component.html new file mode 100644 index 000000000..34a7bac9e --- /dev/null +++ b/frontend/projects/ui/src/app/components/logs/logs.component.html @@ -0,0 +1,86 @@ + + + + + + {{ title }} + + + + + + + + + + +
+
+
+ + +
+
+ Websocket failed.... +
+ +
+ + + +
+
+
+ + + +
+ +

Autoscroll

+
+ + Download + + +
+
diff --git a/frontend/projects/ui/src/app/components/logs/logs.module.ts b/frontend/projects/ui/src/app/components/logs/logs.component.module.ts similarity index 57% rename from frontend/projects/ui/src/app/components/logs/logs.module.ts rename to frontend/projects/ui/src/app/components/logs/logs.component.module.ts index 1ee7bcca7..f00ff7660 100644 --- a/frontend/projects/ui/src/app/components/logs/logs.module.ts +++ b/frontend/projects/ui/src/app/components/logs/logs.component.module.ts @@ -1,12 +1,13 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' -import { LogsPage } from './logs.page' +import { LogsComponent } from './logs.component' +import { FormsModule } from '@angular/forms' import { TextSpinnerComponentModule } from '@start9labs/shared' @NgModule({ - declarations: [LogsPage], - imports: [CommonModule, IonicModule, TextSpinnerComponentModule], - exports: [LogsPage], + declarations: [LogsComponent], + imports: [CommonModule, IonicModule, TextSpinnerComponentModule, FormsModule], + exports: [LogsComponent], }) -export class LogsPageModule {} +export class LogsComponentModule {} diff --git a/frontend/projects/ui/src/app/components/logs/logs.component.scss b/frontend/projects/ui/src/app/components/logs/logs.component.scss new file mode 100644 index 000000000..1e73b9928 --- /dev/null +++ b/frontend/projects/ui/src/app/components/logs/logs.component.scss @@ -0,0 +1,5 @@ +#container { + padding-bottom: 16px; + font-family: monospace; + white-space: pre-line; +} diff --git a/frontend/projects/ui/src/app/components/logs/logs.component.ts b/frontend/projects/ui/src/app/components/logs/logs.component.ts new file mode 100644 index 000000000..f018faecc --- /dev/null +++ b/frontend/projects/ui/src/app/components/logs/logs.component.ts @@ -0,0 +1,226 @@ +import { DOCUMENT } from '@angular/common' +import { Component, Inject, Input, ViewChild } from '@angular/core' +import { IonContent, LoadingController } from '@ionic/angular' +import { map, takeUntil, timer } from 'rxjs' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' +import { + LogsRes, + ServerLogsReq, + DestroyService, + ErrorToastService, + toLocalIsoString, + Log, + DownloadHTMLService, +} from '@start9labs/shared' +import { RR } from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' + +var Convert = require('ansi-to-html') +var convert = new Convert({ + newline: true, + bg: 'transparent', + colors: { + 4: 'Cyan', + }, + escapeXML: true, +}) + +@Component({ + selector: 'logs', + templateUrl: './logs.component.html', + styleUrls: ['./logs.component.scss'], + providers: [DestroyService, DownloadHTMLService], +}) +export class LogsComponent { + @ViewChild(IonContent) + private content?: IonContent + + @Input() followLogs!: ( + params: RR.FollowServerLogsReq, + ) => Promise + @Input() fetchLogs!: (params: ServerLogsReq) => Promise + @Input() defaultBack!: string + @Input() title!: string + + loading = true + needInfinite = true + startCursor?: string + isOnBottom = true + autoScroll = true + websocketFail = false + limit = 200 + toProcess: Log[] = [] + + constructor( + @Inject(DOCUMENT) private readonly document: Document, + private readonly errToast: ErrorToastService, + private readonly destroy$: DestroyService, + private readonly api: ApiService, + private readonly loadingCtrl: LoadingController, + private readonly downloadHtml: DownloadHTMLService, + ) {} + + async ngOnInit() { + try { + const { 'start-cursor': startCursor, guid } = await this.followLogs({ + limit: 100, + }) + + this.startCursor = startCursor + + const host = this.document.location.host + const protocol = + this.document.location.protocol === 'http:' ? 'ws' : 'wss' + + const config: WebSocketSubjectConfig = { + url: `${protocol}://${host}/ws/rpc/${guid}`, + openObserver: { + next: () => { + console.log('**** LOGS WEBSOCKET OPEN ****') + this.websocketFail = false + this.processJob() + }, + }, + } + + this.api + .openLogsWebsocket$(config) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: msg => { + this.toProcess.push(msg) + }, + error: () => { + this.websocketFail = true + if (this.isOnBottom) this.scrollToBottom() + }, + }) + } catch (e: any) { + this.errToast.present(e) + } + } + + async doInfinite(e: any): Promise { + try { + const res = await this.fetchLogs({ + cursor: this.startCursor, + before: true, + limit: this.limit, + }) + + this.processRes(res) + } catch (e: any) { + this.errToast.present(e) + } finally { + e.target.complete() + } + } + + handleScroll(e: any) { + if (e.detail.deltaY < 0) this.autoScroll = false + } + + handleScrollEnd() { + const bottomDiv = document.getElementById('bottom-div') + this.isOnBottom = + !!bottomDiv && + bottomDiv.getBoundingClientRect().top - 420 < window.innerHeight + } + + scrollToBottom() { + this.content?.scrollToBottom(250) + } + + async download() { + const loader = await this.loadingCtrl.create({ + message: 'Processing 10,000 logs...', + }) + await loader.present() + + try { + const { entries } = await this.fetchLogs({ + before: true, + limit: 10000, + }) + + const styles = { + 'background-color': '#222428', + color: '#e0e0e0', + 'font-family': 'monospace', + } + const html = this.convertToAnsi(entries) + + this.downloadHtml.download('logs.html', html, styles) + } catch (e: any) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + + private processJob() { + timer(0, 500) + .pipe( + map((_, index) => index), + takeUntil(this.destroy$), + ) + .subscribe(index => { + this.processRes({ entries: this.toProcess }) + this.toProcess = [] + if (index === 0) this.loading = false + }) + } + + private processRes(res: LogsRes) { + const { entries, 'start-cursor': startCursor } = res + + if (!entries.length) return + + const container = document.getElementById('container') + const newLogs = document.getElementById('template')?.cloneNode() + + if (!(newLogs instanceof HTMLElement)) return + + newLogs.innerHTML = this.convertToAnsi(entries) + + // if respone contains startCursor, it means we are scrolling backwards + if (startCursor) { + this.startCursor = startCursor + + const beforeContainerHeight = container?.scrollHeight || 0 + container?.prepend(newLogs) + const afterContainerHeight = container?.scrollHeight || 0 + + // scroll down + setTimeout(() => { + this.content?.scrollToPoint( + 0, + afterContainerHeight - beforeContainerHeight, + ) + }, 25) + + if (entries.length < this.limit) { + this.needInfinite = false + } + } else { + container?.append(newLogs) + if (this.autoScroll) { + // scroll to bottom + setTimeout(() => { + this.scrollToBottom() + }, 25) + } + } + } + + private convertToAnsi(entries: Log[]) { + return entries + .map( + entry => + `${toLocalIsoString( + new Date(entry.timestamp), + )}  ${convert.toHtml(entry.message)}`, + ) + .join('
') + } +} diff --git a/frontend/projects/ui/src/app/components/logs/logs.page.html b/frontend/projects/ui/src/app/components/logs/logs.page.html deleted file mode 100644 index fb6f00e4b..000000000 --- a/frontend/projects/ui/src/app/components/logs/logs.page.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - -
-
-
-
- - Load More - - - -
- -
- - - -
-
diff --git a/frontend/projects/ui/src/app/components/logs/logs.page.scss b/frontend/projects/ui/src/app/components/logs/logs.page.scss deleted file mode 100644 index dcd863c00..000000000 --- a/frontend/projects/ui/src/app/components/logs/logs.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -#container { - padding-bottom: 16px; -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/components/logs/logs.page.ts b/frontend/projects/ui/src/app/components/logs/logs.page.ts deleted file mode 100644 index 3fc542168..000000000 --- a/frontend/projects/ui/src/app/components/logs/logs.page.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { formatDate } from '@angular/common' -import { Component, Input, ViewChild } from '@angular/core' -import { IonContent } from '@ionic/angular' -import { ErrorToastService, toLocalIsoString } from '@start9labs/shared' -import { RR } from 'src/app/services/api/api.types' -var Convert = require('ansi-to-html') -var convert = new Convert({ - bg: 'transparent', - colors: { - 4: 'Cyan', - }, - escapeXML: true, -}) - -@Component({ - selector: 'logs', - templateUrl: './logs.page.html', - styleUrls: ['./logs.page.scss'], -}) -export class LogsPage { - @ViewChild(IonContent) - private content?: IonContent - - @Input() - fetchLogs!: (params: { - before_flag?: boolean - limit?: number - cursor?: string - }) => Promise - - loading = true - loadingNext = false - needInfinite = true - startCursor?: string - endCursor?: string - limit = 400 - isOnBottom = true - - constructor(private readonly errToast: ErrorToastService) {} - - async ngOnInit() { - await this.getPrior() - this.loading = false - } - - async getNext() { - this.loadingNext = true - const logs = await this.fetch(false) - if (!logs?.length) return (this.loadingNext = false) - - const container = document.getElementById('container') - const newLogs = document.getElementById('template')?.cloneNode(true) - - if (!(newLogs instanceof HTMLElement)) return - - newLogs.innerHTML = - logs - .map( - l => - `${toLocalIsoString(new Date(l.timestamp))} ${convert.toHtml( - l.message, - )}`, - ) - .join('\n') + (logs.length ? '\n' : '') - container?.append(newLogs) - this.loadingNext = false - this.scrollEvent() - } - - async doInfinite(e: any): Promise { - await this.getPrior() - e.target.complete() - } - - scrollEvent() { - const buttonDiv = document.getElementById('button-div') - this.isOnBottom = - !!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight - } - - scrollToBottom() { - this.content?.scrollToBottom(500) - } - - private async getPrior() { - // get logs - const logs = await this.fetch() - if (!logs?.length) return - - const container = document.getElementById('container') - const beforeContainerHeight = container?.scrollHeight || 0 - const newLogs = document.getElementById('template')?.cloneNode(true) - - if (!(newLogs instanceof HTMLElement)) return - - newLogs.innerHTML = - logs - .map( - l => - `${toLocalIsoString(new Date(l.timestamp))} ${convert.toHtml( - l.message, - )}`, - ) - .join('\n') + (logs.length ? '\n' : '') - container?.prepend(newLogs) - const afterContainerHeight = container?.scrollHeight || 0 - - // scroll down - scrollBy(0, afterContainerHeight - beforeContainerHeight) - this.content?.scrollToPoint(0, afterContainerHeight - beforeContainerHeight) - - if (logs.length < this.limit) { - this.needInfinite = false - } - } - - private async fetch(isBefore: boolean = true) { - try { - const cursor = isBefore ? this.startCursor : this.endCursor - const logsRes = await this.fetchLogs({ - cursor, - before_flag: !!cursor ? isBefore : undefined, - limit: this.limit, - }) - - if ((isBefore || this.startCursor) && logsRes['start-cursor']) { - this.startCursor = logsRes['start-cursor'] - } - - if ((!isBefore || !this.endCursor) && logsRes['end-cursor']) { - this.endCursor = logsRes['end-cursor'] - } - - return logsRes.entries - } catch (e: any) { - this.errToast.present(e) - } - } -} diff --git a/frontend/projects/ui/src/app/modals/action-success/action-success.page.html b/frontend/projects/ui/src/app/modals/action-success/action-success.page.html index 2a91d6c0d..bc660313d 100644 --- a/frontend/projects/ui/src/app/modals/action-success/action-success.page.html +++ b/frontend/projects/ui/src/app/modals/action-success/action-success.page.html @@ -1,22 +1,18 @@ - + Execution Complete + - Execution Complete - - - -

{{ actionRes.message }}

-
-
+ +

{{ actionRes.message }}

-
+
diff --git a/frontend/projects/ui/src/app/modals/action-success/action-success.page.ts b/frontend/projects/ui/src/app/modals/action-success/action-success.page.ts index 01b7ea186..30c5c02cd 100644 --- a/frontend/projects/ui/src/app/modals/action-success/action-success.page.ts +++ b/frontend/projects/ui/src/app/modals/action-success/action-success.page.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core' import { ModalController, ToastController } from '@ionic/angular' import { ActionResponse } from 'src/app/services/api/api.types' -import { copyToClipboard } from 'src/app/util/web.util' +import { copyToClipboard } from '@start9labs/shared' @Component({ selector: 'action-success', diff --git a/frontend/projects/ui/src/app/modals/app-config/app-config.page.html b/frontend/projects/ui/src/app/modals/app-config/app-config.page.html index 9db615976..03a9f3257 100644 --- a/frontend/projects/ui/src/app/modals/app-config/app-config.page.html +++ b/frontend/projects/ui/src/app/modals/app-config/app-config.page.html @@ -25,7 +25,7 @@ - +

- +

No config options for {{ pkg.manifest.title }} {{ @@ -91,7 +91,11 @@ -

+ - + Reset Defaults @@ -115,7 +119,7 @@ - - - - - Logs - - - - - - -
- -
+ diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts index 31019d442..103d0bc0e 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { getPkgId } from '@start9labs/shared' -import { ToastController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { copyToClipboard, strip } from 'src/app/util/web.util' +import { RR } from 'src/app/services/api/api.types' @Component({ selector: 'app-logs', @@ -16,39 +15,23 @@ export class AppLogsPage { constructor( private readonly route: ActivatedRoute, private readonly embassyApi: ApiService, - private readonly toastCtrl: ToastController, ) {} - fetchFetchLogs() { - return async (params: { - before_flag?: boolean - limit?: number - cursor?: string - }) => { - return this.embassyApi.getPackageLogs({ + followLogs() { + return async (params: RR.FollowServerLogsReq) => { + return this.embassyApi.followPackageLogs({ id: this.pkgId, - before_flag: params.before_flag, - cursor: params.cursor, - limit: params.limit, + ...params, }) } } - async copy(): Promise { - const logs = document - .getElementById('template') - ?.cloneNode(true) as HTMLElement - const formatted = '```' + strip(logs.innerHTML) + '```' - const success = await copyToClipboard(formatted) - const message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() + fetchLogs() { + return async (params: RR.GetServerLogsReq) => { + return this.embassyApi.getPackageLogs({ + id: this.pkgId, + ...params, + }) + } } } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts index 4d962e5a2..5e3cc89ca 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts @@ -1,7 +1,6 @@ import { Component, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { copyToClipboard } from 'src/app/util/web.util' import { AlertController, IonBackButtonDelegate, @@ -13,7 +12,12 @@ import { PackageProperties } from 'src/app/util/properties.util' import { QRComponent } from 'src/app/components/qr/qr.component' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PackageMainStatus } from 'src/app/services/patch-db/data-model' -import { DestroyService, ErrorToastService, getPkgId } from '@start9labs/shared' +import { + DestroyService, + ErrorToastService, + getPkgId, + copyToClipboard, +} from '@start9labs/shared' import { getValueByPointer } from 'fast-json-patch' import { map, takeUntil } from 'rxjs/operators' diff --git a/frontend/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts b/frontend/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts index 8135e2c8a..9dfbddcad 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts @@ -9,31 +9,46 @@ const routes: Routes = [ }, { path: 'list', - loadChildren: () => import('./app-list/app-list.module').then(m => m.AppListPageModule), + loadChildren: () => + import('./app-list/app-list.module').then(m => m.AppListPageModule), }, { path: ':pkgId', - loadChildren: () => import('./app-show/app-show.module').then(m => m.AppShowPageModule), + loadChildren: () => + import('./app-show/app-show.module').then(m => m.AppShowPageModule), }, { path: ':pkgId/actions', - loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule), + loadChildren: () => + import('./app-actions/app-actions.module').then( + m => m.AppActionsPageModule, + ), }, { path: ':pkgId/interfaces', - loadChildren: () => import('./app-interfaces/app-interfaces.module').then(m => m.AppInterfacesPageModule), + loadChildren: () => + import('./app-interfaces/app-interfaces.module').then( + m => m.AppInterfacesPageModule, + ), }, { path: ':pkgId/logs', - loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), + loadChildren: () => + import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), }, { path: ':pkgId/metrics', - loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule), + loadChildren: () => + import('./app-metrics/app-metrics.module').then( + m => m.AppMetricsPageModule, + ), }, { path: ':pkgId/properties', - loadChildren: () => import('./app-properties/app-properties.module').then(m => m.AppPropertiesPageModule), + loadChildren: () => + import('./app-properties/app-properties.module').then( + m => m.AppPropertiesPageModule, + ), }, ] @@ -41,4 +56,4 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) -export class AppsRoutingModule { } \ No newline at end of file +export class AppsRoutingModule {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.module.ts b/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.module.ts index 1d9885d5f..96c3f152c 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.module.ts @@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { KernelLogsPage } from './kernel-logs.page' -import { SharedPipesModule } from '@start9labs/shared' -import { LogsPageModule } from 'src/app/components/logs/logs.module' +import { LogsComponentModule } from 'src/app/components/logs/logs.component.module' const routes: Routes = [ { @@ -18,8 +17,7 @@ const routes: Routes = [ CommonModule, IonicModule, RouterModule.forChild(routes), - SharedPipesModule, - LogsPageModule, + LogsComponentModule, ], declarations: [KernelLogsPage], }) diff --git a/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.html b/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.html index b1f001eb4..67718bd0f 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.html @@ -1,17 +1,7 @@ - - - - - - Kernel Logs - - - - - - - - -
- -
+ diff --git a/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.ts b/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.ts index 7a67320b8..42118d02c 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/kernel-logs/kernel-logs.page.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core' -import { ToastController } from '@ionic/angular' +import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { copyToClipboard, strip } from 'src/app/util/web.util' @Component({ selector: 'kernel-logs', @@ -9,40 +8,17 @@ import { copyToClipboard, strip } from 'src/app/util/web.util' styleUrls: ['./kernel-logs.page.scss'], }) export class KernelLogsPage { - constructor( - private readonly embassyApi: ApiService, - private readonly toastCtrl: ToastController, - ) {} + constructor(private readonly embassyApi: ApiService) {} - fetchFetchLogs() { - return async (params: { - before_flag?: boolean - limit?: number - cursor?: string - }) => { - return this.embassyApi.getKernelLogs({ - before_flag: params.before_flag, - cursor: params.cursor, - limit: params.limit, - }) + followLogs() { + return async (params: RR.FollowServerLogsReq) => { + return this.embassyApi.followKernelLogs(params) } } - async copy(): Promise { - const logs = document - .getElementById('template') - ?.cloneNode(true) as HTMLElement - const formatted = '```' + strip(logs.innerHTML) + '```' - const success = await copyToClipboard(formatted) - const message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() + fetchLogs() { + return async (params: RR.GetServerLogsReq) => { + return this.embassyApi.getKernelLogs(params) + } } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts index 90bd3f02f..14ec7f488 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts @@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { ServerLogsPage } from './server-logs.page' -import { SharedPipesModule } from '@start9labs/shared' -import { LogsPageModule } from 'src/app/components/logs/logs.module' +import { LogsComponentModule } from 'src/app/components/logs/logs.component.module' const routes: Routes = [ { @@ -18,8 +17,7 @@ const routes: Routes = [ CommonModule, IonicModule, RouterModule.forChild(routes), - SharedPipesModule, - LogsPageModule, + LogsComponentModule, ], declarations: [ServerLogsPage], }) diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.html index 43a95ef78..440b9a404 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.html @@ -1,17 +1,7 @@ - - - - - - OS Logs - - - - - - - - -
- -
+ diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts index 8f2c0cd69..5fa903876 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core' -import { ToastController } from '@ionic/angular' +import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { copyToClipboard, strip } from 'src/app/util/web.util' @Component({ selector: 'server-logs', @@ -9,40 +8,17 @@ import { copyToClipboard, strip } from 'src/app/util/web.util' styleUrls: ['./server-logs.page.scss'], }) export class ServerLogsPage { - constructor( - private readonly embassyApi: ApiService, - private readonly toastCtrl: ToastController, - ) {} + constructor(private readonly embassyApi: ApiService) {} - fetchFetchLogs() { - return async (params: { - before_flag?: boolean - limit?: number - cursor?: string - }) => { - return this.embassyApi.getServerLogs({ - before_flag: params.before_flag, - cursor: params.cursor, - limit: params.limit, - }) + followLogs() { + return async (params: RR.FollowServerLogsReq) => { + return this.embassyApi.followServerLogs(params) } } - async copy(): Promise { - const logs = document - .getElementById('template') - ?.cloneNode(true) as HTMLElement - const formatted = '```' + strip(logs.innerHTML) + '```' - const success = await copyToClipboard(formatted) - const message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() + fetchLogs() { + return async (params: RR.GetServerLogsReq) => { + return this.embassyApi.getServerLogs(params) + } } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts index 0bdf27bba..3dd80968a 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core' import { ToastController } from '@ionic/angular' -import { copyToClipboard } from 'src/app/util/web.util' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { ConfigService } from 'src/app/services/config.service' +import { copyToClipboard } from '@start9labs/shared' @Component({ selector: 'server-specs', diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index 527a09ac6..0ff627db3 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -7,16 +7,11 @@ import { PackageState, ServerStatusInfo, } from 'src/app/services/patch-db/data-model' -import { - Log, - Metric, - RR, - NotificationLevel, - ServerNotifications, -} from './api.types' +import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types' import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' import { MarketplacePkg } from '@start9labs/marketplace' +import { Log } from '@start9labs/shared' export module Mock { export const ServerUpdated: ServerStatusInfo = { @@ -955,7 +950,7 @@ export module Mock { { timestamp: '2019-12-26T14:21:30.872Z', message: - '\u001b[34mPOST \u001b[0;32;49m200\u001b[0m photoview.embassy/api/graphql \u001b[0;36;49m1.169406ms\u001b[0m unauthenticated

TEST PARAGRAPH

', + '\u001b[34mPOST \u001b[0;32;49m200\u001b[0m photoview.embassy/api/graphql \u001b[0;36;49m1.169406ms\u001b', }, { timestamp: '2019-12-26T14:22:30.872Z', @@ -1439,7 +1434,7 @@ export module Mock { 'bitcoin-node': { name: 'Bitcoin Node Settings', type: 'union', - description: 'The node settings', + description: 'Options
  • Item 1
  • Item 2
', default: 'internal', warning: 'Careful changing this', tag: { diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index 0166dfc9a..c0621c5f0 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -7,6 +7,7 @@ import { DependencyError, Manifest, } from 'src/app/services/patch-db/data-model' +import { LogsRes, ServerLogsReq } from '@start9labs/shared' export module RR { // DB @@ -28,13 +29,15 @@ export module RR { // server - export type GetServerLogsReq = { - cursor?: string - before_flag?: boolean - limit?: number - } + export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs export type GetServerLogsRes = LogsRes + export type FollowServerLogsReq = { limit?: number } // server.logs.follow & server.kernel-logs.follow + export type FollowServerLogsRes = { + 'start-cursor': string + guid: string + } + export type GetServerMetricsReq = {} // server.metrics export type GetServerMetricsRes = Metrics @@ -160,20 +163,12 @@ export module RR { export type GetPackagePropertiesRes = PackagePropertiesVersioned - export type LogsRes = { - entries: Log[] - 'start-cursor'?: string - 'end-cursor'?: string - } - - export type GetPackageLogsReq = { - id: string - cursor?: string - before_flag?: boolean - limit?: number - } // package.logs + export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs export type GetPackageLogsRes = LogsRes + export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow + export type FollowPackageLogsRes = FollowServerLogsRes + export type GetPackageMetricsReq = { id: string } // package.metrics export type GetPackageMetricsRes = Metric @@ -238,7 +233,7 @@ export module RR { spec: ConfigSpec } - export interface SideloadPackageReq { + export type SideloadPackageReq = { manifest: Manifest icon: string // base64 } @@ -288,11 +283,6 @@ export interface TaggedDependencyError { error: DependencyError } -export interface Log { - timestamp: string - message: string -} - export interface ActionResponse { message: string value: string | null diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index 922a9be6e..ab37f7f06 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -10,8 +10,9 @@ import { } from 'patch-db-client' import { RR } from './api.types' import { DataModel } from 'src/app/services/patch-db/data-model' -import { RequestError } from '../http.service' +import { Log, RequestError } from '@start9labs/shared' import { map } from 'rxjs/operators' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' export abstract class ApiService implements Source, Http { protected readonly sync$ = new Subject>() @@ -24,6 +25,14 @@ export abstract class ApiService implements Source, Http { .pipe(map(result => ({ result, jsonrpc: '2.0' }))) } + // websocket + + abstract openLogsWebsocket$( + config: WebSocketSubjectConfig, + ): Observable + + // http + // for getting static files: ex icons, instructions, licenses abstract getStatic(url: string): Promise @@ -62,6 +71,14 @@ export abstract class ApiService implements Source, Http { params: RR.GetServerLogsReq, ): Promise + abstract followServerLogs( + params: RR.FollowServerLogsReq, + ): Promise + + abstract followKernelLogs( + params: RR.FollowServerLogsReq, + ): Promise + abstract getServerMetrics( params: RR.GetServerMetricsReq, ): Promise @@ -193,6 +210,10 @@ export abstract class ApiService implements Source, Http { params: RR.GetPackageLogsReq, ): Promise + abstract followPackageLogs( + params: RR.FollowPackageLogsReq, + ): Promise + protected abstract installPackageRaw( params: RR.InstallPackageReq, ): Promise @@ -280,7 +301,7 @@ export abstract class ApiService implements Source, Http { // } return f(a) - .catch((e: RequestError) => { + .catch((e: UIRequestError) => { if (e.revision) this.sync$.next(e.revision) throw e }) @@ -291,3 +312,5 @@ export abstract class ApiService implements Source, Http { } } } + +type UIRequestError = RequestError & { revision: Revision } diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index db10e8144..c1b153646 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -1,9 +1,11 @@ import { Injectable } from '@angular/core' -import { HttpService, Method } from '../http.service' +import { HttpService, Log, LogsRes, Method } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { RR } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { ConfigService } from '../config.service' +import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' +import { Observable } from 'rxjs' @Injectable() export class LiveApiService extends ApiService { @@ -12,7 +14,11 @@ export class LiveApiService extends ApiService { private readonly config: ConfigService, ) { super() - ; (window as any).rpcClient = this + ; (window as any).rpcClient = this + } + + openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { + return webSocket(config) } async getStatic(url: string): Promise { @@ -39,7 +45,7 @@ export class LiveApiService extends ApiService { } async getDump(): Promise { - return this.http.rpcRequest({ method: 'db.dump' }) + return this.http.rpcRequest({ method: 'db.dump', params: {} }) } async setDbValueRaw(params: RR.SetDBValueReq): Promise { @@ -78,6 +84,18 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'server.kernel-logs', params }) } + async followServerLogs( + params: RR.FollowServerLogsReq, + ): Promise { + return this.http.rpcRequest({ method: 'server.logs.follow', params }) + } + + async followKernelLogs( + params: RR.FollowServerLogsReq, + ): Promise { + return this.http.rpcRequest({ method: 'server.kernel-logs.follow', params }) + } + async getServerMetrics( params: RR.GetServerMetricsReq, ): Promise { @@ -252,6 +270,12 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'package.logs', params }) } + async followPackageLogs( + params: RR.FollowServerLogsReq, + ): Promise { + return this.http.rpcRequest({ method: 'package.logs.follow', params }) + } + async getPkgMetrics( params: RR.GetPackageMetricsReq, ): Promise { diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 67d21494a..18e50d2de 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { pauseFor } from '@start9labs/shared' +import { pauseFor, Log, LogsRes } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client' import { @@ -11,13 +11,14 @@ import { PackageState, ServerStatus, } from 'src/app/services/patch-db/data-model' -import { CifsBackupTarget, Log, RR, WithRevision } from './api.types' +import { CifsBackupTarget, RR, WithRevision } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { Mock } from './api.fixures' import markdown from 'raw-loader!../../../../../../assets/markdown/md-sample.md' -import { BehaviorSubject } from 'rxjs' +import { BehaviorSubject, interval, map, Observable, tap } from 'rxjs' import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap' import { mockPatchData } from './mock-patch' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' const PROGRESS: InstallProgress = { size: 120, @@ -43,6 +44,16 @@ export class MockApiService extends ApiService { super() } + openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { + return interval(100).pipe( + map((_, index) => { + // mock fire open observer + if (index === 0) config.openObserver?.next(new Event('')) + return Mock.ServerLogs[0] + }), + ) + } + async getStatic(url: string): Promise { await pauseFor(2000) return markdown @@ -113,17 +124,8 @@ export class MockApiService extends ApiService { params: RR.GetServerLogsReq, ): Promise { await pauseFor(2000) - let entries: Log[] - if (Math.random() < 0.2) { - entries = Mock.ServerLogs - } else { - const arrLength = params.limit - ? Math.ceil(params.limit / Mock.ServerLogs.length) - : 10 - entries = new Array(arrLength) - .fill(Mock.ServerLogs) - .reduce((acc, val) => acc.concat(val), []) - } + const entries = this.randomLogs(params.limit) + return { entries, 'start-cursor': 'startCursor', @@ -135,17 +137,8 @@ export class MockApiService extends ApiService { params: RR.GetServerLogsReq, ): Promise { await pauseFor(2000) - let entries: Log[] - if (Math.random() < 0.2) { - entries = Mock.ServerLogs - } else { - const arrLength = params.limit - ? Math.ceil(params.limit / Mock.ServerLogs.length) - : 10 - entries = new Array(arrLength) - .fill(Mock.ServerLogs) - .reduce((acc, val) => acc.concat(val), []) - } + const entries = this.randomLogs(params.limit) + return { entries, 'start-cursor': 'startCursor', @@ -153,6 +146,35 @@ export class MockApiService extends ApiService { } } + async followServerLogs( + params: RR.FollowServerLogsReq, + ): Promise { + await pauseFor(2000) + return { + 'start-cursor': 'start-cursor', + guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + } + } + + async followKernelLogs( + params: RR.FollowServerLogsReq, + ): Promise { + await pauseFor(2000) + return { + 'start-cursor': 'start-cursor', + guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + } + } + + randomLogs(limit = 1): Log[] { + const arrLength = Math.ceil(limit / Mock.ServerLogs.length) + const logs = new Array(arrLength) + .fill(Mock.ServerLogs) + .reduce((acc, val) => acc.concat(val), []) + + return logs + } + async getServerMetrics( params: RR.GetServerMetricsReq, ): Promise { @@ -485,6 +507,16 @@ export class MockApiService extends ApiService { } } + async followPackageLogs( + params: RR.FollowPackageLogsReq, + ): Promise { + await pauseFor(2000) + return { + 'start-cursor': 'start-cursor', + guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + } + } + async installPackageRaw( params: RR.InstallPackageReq, ): Promise { diff --git a/frontend/projects/ui/src/app/services/http.service.ts b/frontend/projects/ui/src/app/services/http.service.ts deleted file mode 100644 index b17edf1a5..000000000 --- a/frontend/projects/ui/src/app/services/http.service.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Injectable } from '@angular/core' -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' -import { - Observable, - from, - interval, - race, - firstValueFrom, - lastValueFrom, -} from 'rxjs' -import { map, take } from 'rxjs/operators' -import { ConfigService } from './config.service' -import { Revision } from 'patch-db-client' -import { AuthService } from './auth.service' -import { HttpError, RpcError } from '@start9labs/shared' - -@Injectable({ - providedIn: 'root', -}) -export class HttpService { - fullUrl: string - - constructor( - private readonly http: HttpClient, - private readonly config: ConfigService, - private readonly auth: AuthService, - ) { - const port = window.location.port - this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}` - } - - // @ts-ignore TODO: fix typing - async rpcRequest(rpcOpts: RPCOptions): Promise { - const { url, version } = this.config.api - rpcOpts.params = rpcOpts.params || {} - const httpOpts: HttpOptions = { - method: Method.POST, - body: rpcOpts, - url: `/${url}/${version}`, - } - if (rpcOpts.timeout) httpOpts.timeout = rpcOpts.timeout - - const res = await this.httpRequest>(httpOpts) - if (isRpcError(res)) { - // code 34 is authorization error ie. invalid session - if (res.error.code === 34) this.auth.setUnverified() - throw new RpcError(res.error) - } - - return res.result - } - - async httpRequest(httpOpts: HttpOptions): Promise { - if (httpOpts.withCredentials !== false) { - httpOpts.withCredentials = true - } - - const urlIsRelative = httpOpts.url.startsWith('/') - const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url - const { params } = httpOpts - - if (hasParams(params)) { - Object.keys(params).forEach(key => { - if (params[key] === undefined) { - delete params[key] - } - }) - } - - const options = { - responseType: httpOpts.responseType || 'json', - body: httpOpts.body, - observe: 'response', - withCredentials: httpOpts.withCredentials, - headers: httpOpts.headers, - params: httpOpts.params, - timeout: httpOpts.timeout, - } as any - - let req: Observable<{ body: T }> - switch (httpOpts.method) { - case Method.GET: - req = this.http.get(url, options) as any - break - case Method.POST: - req = this.http.post(url, httpOpts.body, options) as any - break - case Method.PUT: - req = this.http.put(url, httpOpts.body, options) as any - break - case Method.PATCH: - req = this.http.patch(url, httpOpts.body, options) as any - break - case Method.DELETE: - req = this.http.delete(url, options) as any - break - } - - return firstValueFrom( - httpOpts.timeout ? withTimeout(req, httpOpts.timeout) : req, - ) - .then(res => res.body) - .catch(e => { - throw new HttpError(e) - }) - } -} - -function isRpcError( - arg: { error: Error } | { result: Result }, -): arg is { error: Error } { - return (arg as any).error !== undefined -} - -export interface RequestError { - code: number - message: string - details: string - revision: Revision | null -} - -export enum Method { - GET = 'GET', - POST = 'POST', - PUT = 'PUT', - PATCH = 'PATCH', - DELETE = 'DELETE', -} - -export interface RPCOptions { - method: string - params?: object - timeout?: number -} - -interface RPCBase { - jsonrpc: '2.0' - id: string -} - -export interface RPCRequest extends RPCBase { - method: string - params?: T -} - -export interface RPCSuccess extends RPCBase { - result: T -} - -export interface RPCError extends RPCBase { - error: { - code: number - message: string - data?: - | { - details: string - revision: Revision | null - debug: string | null - } - | string - } -} - -export type RPCResponse = RPCSuccess | RPCError - -export interface HttpOptions { - method: Method - url: string - headers?: - | HttpHeaders - | { - [header: string]: string | string[] - } - params?: - | HttpParams - | { - [param: string]: string | string[] - } - responseType?: 'json' | 'text' - withCredentials?: boolean - body?: any - timeout?: number -} - -function hasParams( - params?: HttpOptions['params'], -): params is Record { - return !!params -} - -function withTimeout(req: Observable, timeout: number): Observable { - return race( - from(lastValueFrom(req)), // this guarantees it only emits on completion, intermediary emissions are suppressed. - interval(timeout).pipe( - take(1), - map(() => { - throw new Error('timeout') - }), - ), - ) -} diff --git a/frontend/projects/ui/src/app/util/web.util.ts b/frontend/projects/ui/src/app/util/web.util.ts index 40d1f1c34..ef8e25e98 100644 --- a/frontend/projects/ui/src/app/util/web.util.ts +++ b/frontend/projects/ui/src/app/util/web.util.ts @@ -1,23 +1,3 @@ -export async function copyToClipboard(str: string): Promise { - if (window.isSecureContext) { - return navigator.clipboard - .writeText(str) - .then(() => true) - .catch(() => false) - } - - const el = document.createElement('textarea') - el.value = str - el.setAttribute('readonly', '') - el.style.position = 'absolute' - el.style.left = '-9999px' - document.body.appendChild(el) - el.select() - const copy = document.execCommand('copy') - document.body.removeChild(el) - return copy -} - export function strip(html: string) { let doc = new DOMParser().parseFromString(html, 'text/html') return doc.body.textContent || '' diff --git a/frontend/proxy.conf-sample.json b/frontend/proxy.conf-sample.json index 0a1db349b..23bc247ca 100644 --- a/frontend/proxy.conf-sample.json +++ b/frontend/proxy.conf-sample.json @@ -2,7 +2,7 @@ "/rpc/v1": { "target": "http:///rpc/v1" }, - "/ws/db": { + "/ws/*": { "target": "http://", "secure": false, "ws": true diff --git a/libs/helpers/src/lib.rs b/libs/helpers/src/lib.rs index 51d3d5cb4..348d78c12 100644 --- a/libs/helpers/src/lib.rs +++ b/libs/helpers/src/lib.rs @@ -1,10 +1,12 @@ use std::future::Future; use std::path::{Path, PathBuf}; +use std::time::Duration; use color_eyre::eyre::{eyre, Context, Error}; use futures::future::BoxFuture; use futures::FutureExt; use tokio::fs::File; +use tokio::sync::oneshot; use tokio::task::{JoinError, JoinHandle}; mod script_dir; @@ -150,3 +152,59 @@ impl Drop for AtomicFile { } } } + +pub struct TimedResource { + handle: NonDetachingJoinHandle>, + ready: oneshot::Sender<()>, +} +impl TimedResource { + pub fn new(resource: T, timer: Duration) -> Self { + let (send, recv) = oneshot::channel(); + let handle = tokio::spawn(async move { + tokio::select! { + _ = tokio::time::sleep(timer) => { + drop(resource); + None + }, + _ = recv => Some(resource), + } + }); + Self { + handle: handle.into(), + ready: send, + } + } + + pub fn new_with_destructor< + Fn: FnOnce(T) -> Fut + Send + 'static, + Fut: Future + Send, + >( + resource: T, + timer: Duration, + destructor: Fn, + ) -> Self { + let (send, recv) = oneshot::channel(); + let handle = tokio::spawn(async move { + tokio::select! { + _ = tokio::time::sleep(timer) => { + destructor(resource).await; + None + }, + _ = recv => Some(resource), + } + }); + Self { + handle: handle.into(), + ready: send, + } + } + + pub async fn get(self) -> Option { + let _ = self.ready.send(()); + self.handle.await.unwrap() + } + + pub fn is_timed_out(&self) -> bool { + self.ready.is_closed() + } +}