├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.sh ├── docker └── Dockerfile ├── docs ├── screenshot-1.png ├── screenshot-2.png └── svg-demo │ ├── example.svg │ ├── screen-1.png │ ├── screen-2.png │ ├── screen-3.png │ ├── screen-4.png │ └── screen-5.png ├── examples ├── dynamic │ ├── config.yaml │ ├── monitor.d │ │ ├── router │ │ │ ├── config.yaml │ │ │ └── test.sh │ │ ├── server │ │ │ ├── config.yaml │ │ │ └── test.sh │ │ └── timeout │ │ │ ├── config.yaml │ │ │ └── test.sh │ └── static │ │ └── index.html ├── group │ ├── config.yaml │ ├── monitor.d │ │ └── group │ │ │ ├── config.yaml │ │ │ └── test.sh │ └── static │ │ └── index.html ├── metadata │ ├── config.yaml │ ├── monitor.d │ │ ├── metadata1 │ │ │ ├── config.yaml │ │ │ └── test.sh │ │ ├── metadata2 │ │ │ ├── config.yaml │ │ │ └── test.sh │ │ └── metadata3 │ │ │ ├── config.yaml │ │ │ └── test.sh │ └── static │ │ └── index.html └── simple_network │ ├── config.yaml │ ├── monitor.d │ ├── router │ │ ├── config.yaml │ │ └── test.sh │ ├── server │ │ ├── config.yaml │ │ └── test.sh │ └── timeout │ │ ├── config.yaml │ │ └── test.sh │ └── static │ └── index.html └── src ├── config ├── args.rs ├── mod.rs └── structs.rs ├── css.rs ├── http.rs ├── interpolate.rs ├── main.rs ├── monitor.rs ├── status.rs ├── testcases ├── group_complete │ ├── config.yaml │ └── test.sh ├── group_fail │ ├── config.yaml │ └── test.sh ├── group_incomplete │ ├── config.yaml │ └── test.sh ├── metadata_fail │ ├── config.yaml │ └── test.sh ├── metadata_success │ ├── config.yaml │ └── test.sh └── v1.yaml └── worker ├── linebuf.rs └── mod.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: tokio 10 | versions: 11 | - ">= 0.3.a, < 0.4" 12 | - dependency-name: serde 13 | versions: 14 | - 1.0.124 15 | - dependency-name: tokio 16 | versions: 17 | - 1.3.0 18 | - dependency-name: serde_yaml 19 | versions: 20 | - 0.8.16 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: stable 28 | override: true 29 | - uses: actions-rs/cargo@v1 30 | with: 31 | command: test 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Upload Release Asset 8 | 9 | jobs: 10 | build: 11 | name: Upload Release Asset 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Build project # This would actually build your project, using zip for an example artifact 17 | run: | 18 | cargo install cross 19 | cross build --release --target arm-unknown-linux-musleabi 20 | cross build --release --target arm-unknown-linux-musleabihf 21 | cross build --release --target aarch64-unknown-linux-musl 22 | cross build --release --target x86_64-unknown-linux-musl 23 | - name: Create Release 24 | id: create_release 25 | uses: actions/create-release@v1 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | tag_name: ${{ github.ref }} 30 | release_name: Release ${{ github.ref }} 31 | draft: false 32 | prerelease: false 33 | - name: Upload Release Asset 34 | id: upload-release-asset-musleabi 35 | uses: actions/upload-release-asset@v1 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | with: 39 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 40 | asset_path: ./target/arm-unknown-linux-musleabi/release/stylus 41 | asset_name: stylus_linux_arm 42 | asset_content_type: application/binary 43 | - name: Upload Release Asset 44 | id: upload-release-asset-musleabihf 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 50 | asset_path: ./target/arm-unknown-linux-musleabihf/release/stylus 51 | asset_name: stylus_linux_armhf 52 | asset_content_type: application/binary 53 | - name: Upload Release Asset 54 | id: upload-release-asset-aarch64 55 | uses: actions/upload-release-asset@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 60 | asset_path: ./target/aarch64-unknown-linux-musl/release/stylus 61 | asset_name: stylus_linux_arm64 62 | asset_content_type: application/binary 63 | - name: Upload Release Asset 64 | id: upload-release-asset-x86_64 65 | uses: actions/upload-release-asset@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 70 | asset_path: ./target/x86_64-unknown-linux-musl/release/stylus 71 | asset_name: stylus_linux_amd64 72 | asset_content_type: application/binary 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android_system_properties" 16 | version = "0.1.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 19 | dependencies = [ 20 | "libc", 21 | ] 22 | 23 | [[package]] 24 | name = "ansi_term" 25 | version = "0.12.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 28 | dependencies = [ 29 | "winapi", 30 | ] 31 | 32 | [[package]] 33 | name = "atty" 34 | version = "0.2.14" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 37 | dependencies = [ 38 | "hermit-abi 0.1.19", 39 | "libc", 40 | "winapi", 41 | ] 42 | 43 | [[package]] 44 | name = "autocfg" 45 | version = "1.1.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 48 | 49 | [[package]] 50 | name = "base64" 51 | version = "0.13.1" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 54 | 55 | [[package]] 56 | name = "bitflags" 57 | version = "1.3.2" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 60 | 61 | [[package]] 62 | name = "block-buffer" 63 | version = "0.10.3" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" 66 | dependencies = [ 67 | "generic-array", 68 | ] 69 | 70 | [[package]] 71 | name = "buf_redux" 72 | version = "0.8.4" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 75 | dependencies = [ 76 | "memchr", 77 | "safemem", 78 | ] 79 | 80 | [[package]] 81 | name = "bumpalo" 82 | version = "3.12.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 85 | 86 | [[package]] 87 | name = "bytecount" 88 | version = "0.6.3" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" 91 | 92 | [[package]] 93 | name = "byteorder" 94 | version = "1.4.3" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 97 | 98 | [[package]] 99 | name = "bytes" 100 | version = "1.4.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 103 | 104 | [[package]] 105 | name = "camino" 106 | version = "1.1.3" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3" 109 | dependencies = [ 110 | "serde", 111 | ] 112 | 113 | [[package]] 114 | name = "cargo-platform" 115 | version = "0.1.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" 118 | dependencies = [ 119 | "serde", 120 | ] 121 | 122 | [[package]] 123 | name = "cargo_metadata" 124 | version = "0.14.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" 127 | dependencies = [ 128 | "camino", 129 | "cargo-platform", 130 | "semver", 131 | "serde", 132 | "serde_json", 133 | ] 134 | 135 | [[package]] 136 | name = "cc" 137 | version = "1.0.79" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 140 | 141 | [[package]] 142 | name = "cfg-if" 143 | version = "1.0.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 146 | 147 | [[package]] 148 | name = "chrono" 149 | version = "0.4.23" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" 152 | dependencies = [ 153 | "iana-time-zone", 154 | "num-integer", 155 | "num-traits", 156 | "winapi", 157 | ] 158 | 159 | [[package]] 160 | name = "clap" 161 | version = "2.34.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 164 | dependencies = [ 165 | "ansi_term", 166 | "atty", 167 | "bitflags", 168 | "strsim", 169 | "textwrap", 170 | "unicode-width", 171 | "vec_map", 172 | ] 173 | 174 | [[package]] 175 | name = "codespan-reporting" 176 | version = "0.11.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 179 | dependencies = [ 180 | "termcolor", 181 | "unicode-width", 182 | ] 183 | 184 | [[package]] 185 | name = "convert_case" 186 | version = "0.4.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 189 | 190 | [[package]] 191 | name = "core-foundation-sys" 192 | version = "0.8.3" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 195 | 196 | [[package]] 197 | name = "cpufeatures" 198 | version = "0.2.5" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" 201 | dependencies = [ 202 | "libc", 203 | ] 204 | 205 | [[package]] 206 | name = "crypto-common" 207 | version = "0.1.6" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 210 | dependencies = [ 211 | "generic-array", 212 | "typenum", 213 | ] 214 | 215 | [[package]] 216 | name = "cxx" 217 | version = "1.0.91" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" 220 | dependencies = [ 221 | "cc", 222 | "cxxbridge-flags", 223 | "cxxbridge-macro", 224 | "link-cplusplus", 225 | ] 226 | 227 | [[package]] 228 | name = "cxx-build" 229 | version = "1.0.91" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" 232 | dependencies = [ 233 | "cc", 234 | "codespan-reporting", 235 | "once_cell", 236 | "proc-macro2", 237 | "quote", 238 | "scratch", 239 | "syn", 240 | ] 241 | 242 | [[package]] 243 | name = "cxxbridge-flags" 244 | version = "1.0.91" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" 247 | 248 | [[package]] 249 | name = "cxxbridge-macro" 250 | version = "1.0.91" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" 253 | dependencies = [ 254 | "proc-macro2", 255 | "quote", 256 | "syn", 257 | ] 258 | 259 | [[package]] 260 | name = "derive_more" 261 | version = "0.99.17" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 264 | dependencies = [ 265 | "convert_case", 266 | "proc-macro2", 267 | "quote", 268 | "rustc_version", 269 | "syn", 270 | ] 271 | 272 | [[package]] 273 | name = "digest" 274 | version = "0.10.6" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 277 | dependencies = [ 278 | "block-buffer", 279 | "crypto-common", 280 | ] 281 | 282 | [[package]] 283 | name = "either" 284 | version = "1.8.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 287 | 288 | [[package]] 289 | name = "env_logger" 290 | version = "0.10.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 293 | dependencies = [ 294 | "humantime", 295 | "is-terminal", 296 | "log", 297 | "regex", 298 | "termcolor", 299 | ] 300 | 301 | [[package]] 302 | name = "errno" 303 | version = "0.2.8" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 306 | dependencies = [ 307 | "errno-dragonfly", 308 | "libc", 309 | "winapi", 310 | ] 311 | 312 | [[package]] 313 | name = "errno-dragonfly" 314 | version = "0.1.2" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 317 | dependencies = [ 318 | "cc", 319 | "libc", 320 | ] 321 | 322 | [[package]] 323 | name = "error-chain" 324 | version = "0.12.4" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" 327 | dependencies = [ 328 | "version_check", 329 | ] 330 | 331 | [[package]] 332 | name = "fastrand" 333 | version = "1.9.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 336 | dependencies = [ 337 | "instant", 338 | ] 339 | 340 | [[package]] 341 | name = "fnv" 342 | version = "1.0.7" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 345 | 346 | [[package]] 347 | name = "form_urlencoded" 348 | version = "1.1.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 351 | dependencies = [ 352 | "percent-encoding", 353 | ] 354 | 355 | [[package]] 356 | name = "futures-channel" 357 | version = "0.3.26" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" 360 | dependencies = [ 361 | "futures-core", 362 | "futures-sink", 363 | ] 364 | 365 | [[package]] 366 | name = "futures-core" 367 | version = "0.3.26" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" 370 | 371 | [[package]] 372 | name = "futures-sink" 373 | version = "0.3.26" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" 376 | 377 | [[package]] 378 | name = "futures-task" 379 | version = "0.3.26" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" 382 | 383 | [[package]] 384 | name = "futures-util" 385 | version = "0.3.26" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" 388 | dependencies = [ 389 | "futures-core", 390 | "futures-sink", 391 | "futures-task", 392 | "pin-project-lite", 393 | "pin-utils", 394 | "slab", 395 | ] 396 | 397 | [[package]] 398 | name = "generic-array" 399 | version = "0.14.6" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 402 | dependencies = [ 403 | "typenum", 404 | "version_check", 405 | ] 406 | 407 | [[package]] 408 | name = "getrandom" 409 | version = "0.2.8" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 412 | dependencies = [ 413 | "cfg-if", 414 | "libc", 415 | "wasi", 416 | ] 417 | 418 | [[package]] 419 | name = "glob" 420 | version = "0.3.1" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 423 | 424 | [[package]] 425 | name = "h2" 426 | version = "0.3.16" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" 429 | dependencies = [ 430 | "bytes", 431 | "fnv", 432 | "futures-core", 433 | "futures-sink", 434 | "futures-util", 435 | "http", 436 | "indexmap", 437 | "slab", 438 | "tokio", 439 | "tokio-util", 440 | "tracing", 441 | ] 442 | 443 | [[package]] 444 | name = "handlebars" 445 | version = "4.3.6" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "035ef95d03713f2c347a72547b7cd38cbc9af7cd51e6099fb62d586d4a6dee3a" 448 | dependencies = [ 449 | "log", 450 | "pest", 451 | "pest_derive", 452 | "serde", 453 | "serde_json", 454 | "thiserror", 455 | ] 456 | 457 | [[package]] 458 | name = "hashbrown" 459 | version = "0.12.3" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 462 | 463 | [[package]] 464 | name = "headers" 465 | version = "0.3.8" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" 468 | dependencies = [ 469 | "base64", 470 | "bitflags", 471 | "bytes", 472 | "headers-core", 473 | "http", 474 | "httpdate", 475 | "mime", 476 | "sha1", 477 | ] 478 | 479 | [[package]] 480 | name = "headers-core" 481 | version = "0.2.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 484 | dependencies = [ 485 | "http", 486 | ] 487 | 488 | [[package]] 489 | name = "heck" 490 | version = "0.3.3" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 493 | dependencies = [ 494 | "unicode-segmentation", 495 | ] 496 | 497 | [[package]] 498 | name = "hermit-abi" 499 | version = "0.1.19" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 502 | dependencies = [ 503 | "libc", 504 | ] 505 | 506 | [[package]] 507 | name = "hermit-abi" 508 | version = "0.2.6" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 511 | dependencies = [ 512 | "libc", 513 | ] 514 | 515 | [[package]] 516 | name = "hermit-abi" 517 | version = "0.3.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 520 | 521 | [[package]] 522 | name = "http" 523 | version = "0.2.9" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 526 | dependencies = [ 527 | "bytes", 528 | "fnv", 529 | "itoa", 530 | ] 531 | 532 | [[package]] 533 | name = "http-body" 534 | version = "0.4.5" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 537 | dependencies = [ 538 | "bytes", 539 | "http", 540 | "pin-project-lite", 541 | ] 542 | 543 | [[package]] 544 | name = "httparse" 545 | version = "1.8.0" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 548 | 549 | [[package]] 550 | name = "httpdate" 551 | version = "1.0.2" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 554 | 555 | [[package]] 556 | name = "humantime" 557 | version = "2.1.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 560 | 561 | [[package]] 562 | name = "humantime-serde" 563 | version = "1.1.1" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" 566 | dependencies = [ 567 | "humantime", 568 | "serde", 569 | ] 570 | 571 | [[package]] 572 | name = "hyper" 573 | version = "0.14.24" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" 576 | dependencies = [ 577 | "bytes", 578 | "futures-channel", 579 | "futures-core", 580 | "futures-util", 581 | "h2", 582 | "http", 583 | "http-body", 584 | "httparse", 585 | "httpdate", 586 | "itoa", 587 | "pin-project-lite", 588 | "socket2", 589 | "tokio", 590 | "tower-service", 591 | "tracing", 592 | "want", 593 | ] 594 | 595 | [[package]] 596 | name = "iana-time-zone" 597 | version = "0.1.53" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 600 | dependencies = [ 601 | "android_system_properties", 602 | "core-foundation-sys", 603 | "iana-time-zone-haiku", 604 | "js-sys", 605 | "wasm-bindgen", 606 | "winapi", 607 | ] 608 | 609 | [[package]] 610 | name = "iana-time-zone-haiku" 611 | version = "0.1.1" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 614 | dependencies = [ 615 | "cxx", 616 | "cxx-build", 617 | ] 618 | 619 | [[package]] 620 | name = "idna" 621 | version = "0.3.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 624 | dependencies = [ 625 | "unicode-bidi", 626 | "unicode-normalization", 627 | ] 628 | 629 | [[package]] 630 | name = "indexmap" 631 | version = "1.9.2" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 634 | dependencies = [ 635 | "autocfg", 636 | "hashbrown", 637 | ] 638 | 639 | [[package]] 640 | name = "instant" 641 | version = "0.1.12" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 644 | dependencies = [ 645 | "cfg-if", 646 | ] 647 | 648 | [[package]] 649 | name = "io-lifetimes" 650 | version = "1.0.5" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" 653 | dependencies = [ 654 | "libc", 655 | "windows-sys 0.45.0", 656 | ] 657 | 658 | [[package]] 659 | name = "is-terminal" 660 | version = "0.4.4" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" 663 | dependencies = [ 664 | "hermit-abi 0.3.1", 665 | "io-lifetimes", 666 | "rustix", 667 | "windows-sys 0.45.0", 668 | ] 669 | 670 | [[package]] 671 | name = "itertools" 672 | version = "0.10.5" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 675 | dependencies = [ 676 | "either", 677 | ] 678 | 679 | [[package]] 680 | name = "itoa" 681 | version = "1.0.6" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 684 | 685 | [[package]] 686 | name = "js-sys" 687 | version = "0.3.61" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 690 | dependencies = [ 691 | "wasm-bindgen", 692 | ] 693 | 694 | [[package]] 695 | name = "keepcalm" 696 | version = "0.3.5" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "031ddc7e27bbb011c78958881a3723873608397b8b10e146717fc05cf3364d78" 699 | dependencies = [ 700 | "once_cell", 701 | "parking_lot", 702 | "serde", 703 | ] 704 | 705 | [[package]] 706 | name = "lazy_static" 707 | version = "1.4.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 710 | 711 | [[package]] 712 | name = "libc" 713 | version = "0.2.139" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 716 | 717 | [[package]] 718 | name = "link-cplusplus" 719 | version = "1.0.8" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 722 | dependencies = [ 723 | "cc", 724 | ] 725 | 726 | [[package]] 727 | name = "linux-raw-sys" 728 | version = "0.1.4" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 731 | 732 | [[package]] 733 | name = "lock_api" 734 | version = "0.4.9" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 737 | dependencies = [ 738 | "autocfg", 739 | "scopeguard", 740 | ] 741 | 742 | [[package]] 743 | name = "log" 744 | version = "0.4.17" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 747 | dependencies = [ 748 | "cfg-if", 749 | ] 750 | 751 | [[package]] 752 | name = "memchr" 753 | version = "2.5.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 756 | 757 | [[package]] 758 | name = "mime" 759 | version = "0.3.16" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 762 | 763 | [[package]] 764 | name = "mime_guess" 765 | version = "2.0.4" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 768 | dependencies = [ 769 | "mime", 770 | "unicase", 771 | ] 772 | 773 | [[package]] 774 | name = "mio" 775 | version = "0.8.6" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" 778 | dependencies = [ 779 | "libc", 780 | "log", 781 | "wasi", 782 | "windows-sys 0.45.0", 783 | ] 784 | 785 | [[package]] 786 | name = "multipart" 787 | version = "0.18.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" 790 | dependencies = [ 791 | "buf_redux", 792 | "httparse", 793 | "log", 794 | "mime", 795 | "mime_guess", 796 | "quick-error", 797 | "rand", 798 | "safemem", 799 | "tempfile", 800 | "twoway", 801 | ] 802 | 803 | [[package]] 804 | name = "num-integer" 805 | version = "0.1.45" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 808 | dependencies = [ 809 | "autocfg", 810 | "num-traits", 811 | ] 812 | 813 | [[package]] 814 | name = "num-traits" 815 | version = "0.2.15" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 818 | dependencies = [ 819 | "autocfg", 820 | ] 821 | 822 | [[package]] 823 | name = "num_cpus" 824 | version = "1.15.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 827 | dependencies = [ 828 | "hermit-abi 0.2.6", 829 | "libc", 830 | ] 831 | 832 | [[package]] 833 | name = "once_cell" 834 | version = "1.17.1" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 837 | 838 | [[package]] 839 | name = "parking_lot" 840 | version = "0.12.1" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 843 | dependencies = [ 844 | "lock_api", 845 | "parking_lot_core", 846 | ] 847 | 848 | [[package]] 849 | name = "parking_lot_core" 850 | version = "0.9.7" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 853 | dependencies = [ 854 | "cfg-if", 855 | "libc", 856 | "redox_syscall", 857 | "smallvec", 858 | "windows-sys 0.45.0", 859 | ] 860 | 861 | [[package]] 862 | name = "percent-encoding" 863 | version = "2.2.0" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 866 | 867 | [[package]] 868 | name = "pest" 869 | version = "2.5.5" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660" 872 | dependencies = [ 873 | "thiserror", 874 | "ucd-trie", 875 | ] 876 | 877 | [[package]] 878 | name = "pest_derive" 879 | version = "2.5.5" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "2ac3922aac69a40733080f53c1ce7f91dcf57e1a5f6c52f421fadec7fbdc4b69" 882 | dependencies = [ 883 | "pest", 884 | "pest_generator", 885 | ] 886 | 887 | [[package]] 888 | name = "pest_generator" 889 | version = "2.5.5" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "d06646e185566b5961b4058dd107e0a7f56e77c3f484549fb119867773c0f202" 892 | dependencies = [ 893 | "pest", 894 | "pest_meta", 895 | "proc-macro2", 896 | "quote", 897 | "syn", 898 | ] 899 | 900 | [[package]] 901 | name = "pest_meta" 902 | version = "2.5.5" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "e6f60b2ba541577e2a0c307c8f39d1439108120eb7903adeb6497fa880c59616" 905 | dependencies = [ 906 | "once_cell", 907 | "pest", 908 | "sha2", 909 | ] 910 | 911 | [[package]] 912 | name = "pin-project" 913 | version = "1.0.12" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 916 | dependencies = [ 917 | "pin-project-internal", 918 | ] 919 | 920 | [[package]] 921 | name = "pin-project-internal" 922 | version = "1.0.12" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 925 | dependencies = [ 926 | "proc-macro2", 927 | "quote", 928 | "syn", 929 | ] 930 | 931 | [[package]] 932 | name = "pin-project-lite" 933 | version = "0.2.9" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 936 | 937 | [[package]] 938 | name = "pin-utils" 939 | version = "0.1.0" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 942 | 943 | [[package]] 944 | name = "ppv-lite86" 945 | version = "0.2.17" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 948 | 949 | [[package]] 950 | name = "proc-macro-error" 951 | version = "1.0.4" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 954 | dependencies = [ 955 | "proc-macro-error-attr", 956 | "proc-macro2", 957 | "quote", 958 | "syn", 959 | "version_check", 960 | ] 961 | 962 | [[package]] 963 | name = "proc-macro-error-attr" 964 | version = "1.0.4" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 967 | dependencies = [ 968 | "proc-macro2", 969 | "quote", 970 | "version_check", 971 | ] 972 | 973 | [[package]] 974 | name = "proc-macro2" 975 | version = "1.0.51" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 978 | dependencies = [ 979 | "unicode-ident", 980 | ] 981 | 982 | [[package]] 983 | name = "pulldown-cmark" 984 | version = "0.9.2" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" 987 | dependencies = [ 988 | "bitflags", 989 | "memchr", 990 | "unicase", 991 | ] 992 | 993 | [[package]] 994 | name = "quick-error" 995 | version = "1.2.3" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 998 | 999 | [[package]] 1000 | name = "quote" 1001 | version = "1.0.23" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 1004 | dependencies = [ 1005 | "proc-macro2", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "rand" 1010 | version = "0.8.5" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1013 | dependencies = [ 1014 | "libc", 1015 | "rand_chacha", 1016 | "rand_core", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "rand_chacha" 1021 | version = "0.3.1" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1024 | dependencies = [ 1025 | "ppv-lite86", 1026 | "rand_core", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "rand_core" 1031 | version = "0.6.4" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1034 | dependencies = [ 1035 | "getrandom", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "redox_syscall" 1040 | version = "0.2.16" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1043 | dependencies = [ 1044 | "bitflags", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "regex" 1049 | version = "1.7.1" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 1052 | dependencies = [ 1053 | "aho-corasick", 1054 | "memchr", 1055 | "regex-syntax", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "regex-syntax" 1060 | version = "0.6.28" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 1063 | 1064 | [[package]] 1065 | name = "rustc_version" 1066 | version = "0.4.0" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1069 | dependencies = [ 1070 | "semver", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "rustix" 1075 | version = "0.36.8" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" 1078 | dependencies = [ 1079 | "bitflags", 1080 | "errno", 1081 | "io-lifetimes", 1082 | "libc", 1083 | "linux-raw-sys", 1084 | "windows-sys 0.45.0", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "rustls-pemfile" 1089 | version = "0.2.1" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" 1092 | dependencies = [ 1093 | "base64", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "ryu" 1098 | version = "1.0.13" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 1101 | 1102 | [[package]] 1103 | name = "safemem" 1104 | version = "0.3.3" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 1107 | 1108 | [[package]] 1109 | name = "same-file" 1110 | version = "1.0.6" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1113 | dependencies = [ 1114 | "winapi-util", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "scoped-tls" 1119 | version = "1.0.1" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 1122 | 1123 | [[package]] 1124 | name = "scopeguard" 1125 | version = "1.1.0" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1128 | 1129 | [[package]] 1130 | name = "scratch" 1131 | version = "1.0.4" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "5d5e082f6ea090deaf0e6dd04b68360fd5cddb152af6ce8927c9d25db299f98c" 1134 | 1135 | [[package]] 1136 | name = "semver" 1137 | version = "1.0.16" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" 1140 | dependencies = [ 1141 | "serde", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "serde" 1146 | version = "1.0.152" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 1149 | dependencies = [ 1150 | "serde_derive", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "serde-aux" 1155 | version = "4.1.2" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "c599b3fd89a75e0c18d6d2be693ddb12cccaf771db4ff9e39097104808a014c0" 1158 | dependencies = [ 1159 | "chrono", 1160 | "serde", 1161 | "serde_json", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "serde_derive" 1166 | version = "1.0.152" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 1169 | dependencies = [ 1170 | "proc-macro2", 1171 | "quote", 1172 | "syn", 1173 | ] 1174 | 1175 | [[package]] 1176 | name = "serde_json" 1177 | version = "1.0.93" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" 1180 | dependencies = [ 1181 | "itoa", 1182 | "ryu", 1183 | "serde", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "serde_urlencoded" 1188 | version = "0.7.1" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1191 | dependencies = [ 1192 | "form_urlencoded", 1193 | "itoa", 1194 | "ryu", 1195 | "serde", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "serde_yaml" 1200 | version = "0.9.17" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" 1203 | dependencies = [ 1204 | "indexmap", 1205 | "itoa", 1206 | "ryu", 1207 | "serde", 1208 | "unsafe-libyaml", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "sha-1" 1213 | version = "0.10.1" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" 1216 | dependencies = [ 1217 | "cfg-if", 1218 | "cpufeatures", 1219 | "digest", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "sha1" 1224 | version = "0.10.5" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 1227 | dependencies = [ 1228 | "cfg-if", 1229 | "cpufeatures", 1230 | "digest", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "sha2" 1235 | version = "0.10.6" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" 1238 | dependencies = [ 1239 | "cfg-if", 1240 | "cpufeatures", 1241 | "digest", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "skeptic" 1246 | version = "0.13.7" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" 1249 | dependencies = [ 1250 | "bytecount", 1251 | "cargo_metadata", 1252 | "error-chain", 1253 | "glob", 1254 | "pulldown-cmark", 1255 | "tempfile", 1256 | "walkdir", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "slab" 1261 | version = "0.4.8" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 1264 | dependencies = [ 1265 | "autocfg", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "smallvec" 1270 | version = "1.10.0" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 1273 | 1274 | [[package]] 1275 | name = "socket2" 1276 | version = "0.4.9" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 1279 | dependencies = [ 1280 | "libc", 1281 | "winapi", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "strsim" 1286 | version = "0.8.0" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1289 | 1290 | [[package]] 1291 | name = "structopt" 1292 | version = "0.3.26" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 1295 | dependencies = [ 1296 | "clap", 1297 | "lazy_static", 1298 | "structopt-derive", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "structopt-derive" 1303 | version = "0.4.18" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 1306 | dependencies = [ 1307 | "heck", 1308 | "proc-macro-error", 1309 | "proc-macro2", 1310 | "quote", 1311 | "syn", 1312 | ] 1313 | 1314 | [[package]] 1315 | name = "structopt-flags" 1316 | version = "0.3.6" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "4654ef901a3897697bc76c48c1d0e73f925e5d801959db6d870d39a87beeae85" 1319 | dependencies = [ 1320 | "log", 1321 | "skeptic", 1322 | "structopt", 1323 | ] 1324 | 1325 | [[package]] 1326 | name = "stylus" 1327 | version = "0.9.14" 1328 | dependencies = [ 1329 | "derive_more", 1330 | "env_logger", 1331 | "handlebars", 1332 | "humantime-serde", 1333 | "itertools", 1334 | "keepcalm", 1335 | "log", 1336 | "serde", 1337 | "serde-aux", 1338 | "serde_json", 1339 | "serde_yaml", 1340 | "structopt", 1341 | "structopt-flags", 1342 | "subprocess", 1343 | "tokio", 1344 | "walkdir", 1345 | "warp", 1346 | ] 1347 | 1348 | [[package]] 1349 | name = "subprocess" 1350 | version = "0.2.9" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" 1353 | dependencies = [ 1354 | "libc", 1355 | "winapi", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "syn" 1360 | version = "1.0.109" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1363 | dependencies = [ 1364 | "proc-macro2", 1365 | "quote", 1366 | "unicode-ident", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "tempfile" 1371 | version = "3.4.0" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 1374 | dependencies = [ 1375 | "cfg-if", 1376 | "fastrand", 1377 | "redox_syscall", 1378 | "rustix", 1379 | "windows-sys 0.42.0", 1380 | ] 1381 | 1382 | [[package]] 1383 | name = "termcolor" 1384 | version = "1.2.0" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 1387 | dependencies = [ 1388 | "winapi-util", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "textwrap" 1393 | version = "0.11.0" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1396 | dependencies = [ 1397 | "unicode-width", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "thiserror" 1402 | version = "1.0.38" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 1405 | dependencies = [ 1406 | "thiserror-impl", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "thiserror-impl" 1411 | version = "1.0.38" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 1414 | dependencies = [ 1415 | "proc-macro2", 1416 | "quote", 1417 | "syn", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "tinyvec" 1422 | version = "1.6.0" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1425 | dependencies = [ 1426 | "tinyvec_macros", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "tinyvec_macros" 1431 | version = "0.1.1" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1434 | 1435 | [[package]] 1436 | name = "tokio" 1437 | version = "1.26.0" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" 1440 | dependencies = [ 1441 | "autocfg", 1442 | "bytes", 1443 | "libc", 1444 | "memchr", 1445 | "mio", 1446 | "num_cpus", 1447 | "pin-project-lite", 1448 | "socket2", 1449 | "tokio-macros", 1450 | "windows-sys 0.45.0", 1451 | ] 1452 | 1453 | [[package]] 1454 | name = "tokio-macros" 1455 | version = "1.8.2" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 1458 | dependencies = [ 1459 | "proc-macro2", 1460 | "quote", 1461 | "syn", 1462 | ] 1463 | 1464 | [[package]] 1465 | name = "tokio-stream" 1466 | version = "0.1.12" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" 1469 | dependencies = [ 1470 | "futures-core", 1471 | "pin-project-lite", 1472 | "tokio", 1473 | ] 1474 | 1475 | [[package]] 1476 | name = "tokio-tungstenite" 1477 | version = "0.17.2" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" 1480 | dependencies = [ 1481 | "futures-util", 1482 | "log", 1483 | "tokio", 1484 | "tungstenite", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "tokio-util" 1489 | version = "0.7.7" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" 1492 | dependencies = [ 1493 | "bytes", 1494 | "futures-core", 1495 | "futures-sink", 1496 | "pin-project-lite", 1497 | "tokio", 1498 | "tracing", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "tower-service" 1503 | version = "0.3.2" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1506 | 1507 | [[package]] 1508 | name = "tracing" 1509 | version = "0.1.37" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1512 | dependencies = [ 1513 | "cfg-if", 1514 | "log", 1515 | "pin-project-lite", 1516 | "tracing-core", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "tracing-core" 1521 | version = "0.1.30" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1524 | dependencies = [ 1525 | "once_cell", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "try-lock" 1530 | version = "0.2.4" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1533 | 1534 | [[package]] 1535 | name = "tungstenite" 1536 | version = "0.17.3" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" 1539 | dependencies = [ 1540 | "base64", 1541 | "byteorder", 1542 | "bytes", 1543 | "http", 1544 | "httparse", 1545 | "log", 1546 | "rand", 1547 | "sha-1", 1548 | "thiserror", 1549 | "url", 1550 | "utf-8", 1551 | ] 1552 | 1553 | [[package]] 1554 | name = "twoway" 1555 | version = "0.1.8" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 1558 | dependencies = [ 1559 | "memchr", 1560 | ] 1561 | 1562 | [[package]] 1563 | name = "typenum" 1564 | version = "1.16.0" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1567 | 1568 | [[package]] 1569 | name = "ucd-trie" 1570 | version = "0.1.5" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" 1573 | 1574 | [[package]] 1575 | name = "unicase" 1576 | version = "2.6.0" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1579 | dependencies = [ 1580 | "version_check", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "unicode-bidi" 1585 | version = "0.3.10" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" 1588 | 1589 | [[package]] 1590 | name = "unicode-ident" 1591 | version = "1.0.7" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "775c11906edafc97bc378816b94585fbd9a054eabaf86fdd0ced94af449efab7" 1594 | 1595 | [[package]] 1596 | name = "unicode-normalization" 1597 | version = "0.1.22" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1600 | dependencies = [ 1601 | "tinyvec", 1602 | ] 1603 | 1604 | [[package]] 1605 | name = "unicode-segmentation" 1606 | version = "1.10.1" 1607 | source = "registry+https://github.com/rust-lang/crates.io-index" 1608 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 1609 | 1610 | [[package]] 1611 | name = "unicode-width" 1612 | version = "0.1.10" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1615 | 1616 | [[package]] 1617 | name = "unsafe-libyaml" 1618 | version = "0.2.6" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "137b80f8d41159fdb7c990322f0679b9ccbeb84d73f426844b7e838500b92b31" 1621 | 1622 | [[package]] 1623 | name = "url" 1624 | version = "2.3.1" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1627 | dependencies = [ 1628 | "form_urlencoded", 1629 | "idna", 1630 | "percent-encoding", 1631 | ] 1632 | 1633 | [[package]] 1634 | name = "utf-8" 1635 | version = "0.7.6" 1636 | source = "registry+https://github.com/rust-lang/crates.io-index" 1637 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1638 | 1639 | [[package]] 1640 | name = "vec_map" 1641 | version = "0.8.2" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1644 | 1645 | [[package]] 1646 | name = "version_check" 1647 | version = "0.9.4" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1650 | 1651 | [[package]] 1652 | name = "walkdir" 1653 | version = "2.3.2" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1656 | dependencies = [ 1657 | "same-file", 1658 | "winapi", 1659 | "winapi-util", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "want" 1664 | version = "0.3.0" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1667 | dependencies = [ 1668 | "log", 1669 | "try-lock", 1670 | ] 1671 | 1672 | [[package]] 1673 | name = "warp" 1674 | version = "0.3.3" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "ed7b8be92646fc3d18b06147664ebc5f48d222686cb11a8755e561a735aacc6d" 1677 | dependencies = [ 1678 | "bytes", 1679 | "futures-channel", 1680 | "futures-util", 1681 | "headers", 1682 | "http", 1683 | "hyper", 1684 | "log", 1685 | "mime", 1686 | "mime_guess", 1687 | "multipart", 1688 | "percent-encoding", 1689 | "pin-project", 1690 | "rustls-pemfile", 1691 | "scoped-tls", 1692 | "serde", 1693 | "serde_json", 1694 | "serde_urlencoded", 1695 | "tokio", 1696 | "tokio-stream", 1697 | "tokio-tungstenite", 1698 | "tokio-util", 1699 | "tower-service", 1700 | "tracing", 1701 | ] 1702 | 1703 | [[package]] 1704 | name = "wasi" 1705 | version = "0.11.0+wasi-snapshot-preview1" 1706 | source = "registry+https://github.com/rust-lang/crates.io-index" 1707 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1708 | 1709 | [[package]] 1710 | name = "wasm-bindgen" 1711 | version = "0.2.84" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 1714 | dependencies = [ 1715 | "cfg-if", 1716 | "wasm-bindgen-macro", 1717 | ] 1718 | 1719 | [[package]] 1720 | name = "wasm-bindgen-backend" 1721 | version = "0.2.84" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 1724 | dependencies = [ 1725 | "bumpalo", 1726 | "log", 1727 | "once_cell", 1728 | "proc-macro2", 1729 | "quote", 1730 | "syn", 1731 | "wasm-bindgen-shared", 1732 | ] 1733 | 1734 | [[package]] 1735 | name = "wasm-bindgen-macro" 1736 | version = "0.2.84" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 1739 | dependencies = [ 1740 | "quote", 1741 | "wasm-bindgen-macro-support", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "wasm-bindgen-macro-support" 1746 | version = "0.2.84" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 1749 | dependencies = [ 1750 | "proc-macro2", 1751 | "quote", 1752 | "syn", 1753 | "wasm-bindgen-backend", 1754 | "wasm-bindgen-shared", 1755 | ] 1756 | 1757 | [[package]] 1758 | name = "wasm-bindgen-shared" 1759 | version = "0.2.84" 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" 1761 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 1762 | 1763 | [[package]] 1764 | name = "winapi" 1765 | version = "0.3.9" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1768 | dependencies = [ 1769 | "winapi-i686-pc-windows-gnu", 1770 | "winapi-x86_64-pc-windows-gnu", 1771 | ] 1772 | 1773 | [[package]] 1774 | name = "winapi-i686-pc-windows-gnu" 1775 | version = "0.4.0" 1776 | source = "registry+https://github.com/rust-lang/crates.io-index" 1777 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1778 | 1779 | [[package]] 1780 | name = "winapi-util" 1781 | version = "0.1.5" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1784 | dependencies = [ 1785 | "winapi", 1786 | ] 1787 | 1788 | [[package]] 1789 | name = "winapi-x86_64-pc-windows-gnu" 1790 | version = "0.4.0" 1791 | source = "registry+https://github.com/rust-lang/crates.io-index" 1792 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1793 | 1794 | [[package]] 1795 | name = "windows-sys" 1796 | version = "0.42.0" 1797 | source = "registry+https://github.com/rust-lang/crates.io-index" 1798 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1799 | dependencies = [ 1800 | "windows_aarch64_gnullvm", 1801 | "windows_aarch64_msvc", 1802 | "windows_i686_gnu", 1803 | "windows_i686_msvc", 1804 | "windows_x86_64_gnu", 1805 | "windows_x86_64_gnullvm", 1806 | "windows_x86_64_msvc", 1807 | ] 1808 | 1809 | [[package]] 1810 | name = "windows-sys" 1811 | version = "0.45.0" 1812 | source = "registry+https://github.com/rust-lang/crates.io-index" 1813 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1814 | dependencies = [ 1815 | "windows-targets", 1816 | ] 1817 | 1818 | [[package]] 1819 | name = "windows-targets" 1820 | version = "0.42.1" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 1823 | dependencies = [ 1824 | "windows_aarch64_gnullvm", 1825 | "windows_aarch64_msvc", 1826 | "windows_i686_gnu", 1827 | "windows_i686_msvc", 1828 | "windows_x86_64_gnu", 1829 | "windows_x86_64_gnullvm", 1830 | "windows_x86_64_msvc", 1831 | ] 1832 | 1833 | [[package]] 1834 | name = "windows_aarch64_gnullvm" 1835 | version = "0.42.1" 1836 | source = "registry+https://github.com/rust-lang/crates.io-index" 1837 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 1838 | 1839 | [[package]] 1840 | name = "windows_aarch64_msvc" 1841 | version = "0.42.1" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 1844 | 1845 | [[package]] 1846 | name = "windows_i686_gnu" 1847 | version = "0.42.1" 1848 | source = "registry+https://github.com/rust-lang/crates.io-index" 1849 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 1850 | 1851 | [[package]] 1852 | name = "windows_i686_msvc" 1853 | version = "0.42.1" 1854 | source = "registry+https://github.com/rust-lang/crates.io-index" 1855 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 1856 | 1857 | [[package]] 1858 | name = "windows_x86_64_gnu" 1859 | version = "0.42.1" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 1862 | 1863 | [[package]] 1864 | name = "windows_x86_64_gnullvm" 1865 | version = "0.42.1" 1866 | source = "registry+https://github.com/rust-lang/crates.io-index" 1867 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 1868 | 1869 | [[package]] 1870 | name = "windows_x86_64_msvc" 1871 | version = "0.42.1" 1872 | source = "registry+https://github.com/rust-lang/crates.io-index" 1873 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 1874 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stylus" 3 | version = "0.9.14" 4 | authors = ["Matt Mastracci "] 5 | edition = "2021" 6 | description = "Stylus (style + status) is a lightweight status page for home infrastructure." 7 | license = "MIT" 8 | repository = "https://github.com/mmastrac/stylus" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | tokio = { version = "1.6", features = ["macros", "rt-multi-thread"] } 13 | warp = "0.3" 14 | derive_more = "0.99" 15 | serde = { version = "1", features = ["derive", "rc"] } 16 | serde_yaml = "0.9" 17 | serde_json = { version = "1", features = ["raw_value"] } 18 | serde-aux = "4" 19 | humantime-serde = "1.1.1" 20 | walkdir = "2.3.2" 21 | handlebars = "4.2.2" 22 | subprocess = "0.2.8" 23 | log = "0.4.17" 24 | env_logger = "0.10" 25 | itertools = "0.10" 26 | structopt = "0.3" 27 | structopt-flags = "0.3" 28 | keepcalm = { version = "0.3.5", features = ["serde"] } 29 | 30 | [profile.release] 31 | codegen-units = 1 32 | incremental = false 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stylus ![CI](https://github.com/mmastrac/stylus/workflows/CI/badge.svg?branch=master) [![crates.io](https://img.shields.io/crates/v/stylus.svg)](https://crates.io/crates/stylus) 2 | 3 | **Stylus** (_style + status_) is a lightweight status page for home infrastructure. Configure a set of bash scripts that test 4 | the various parts of your infrastructure, set up HTML/SVG with a diagram of your network, and stylus will 5 | generate you a dynamic stylesheet to give you a visual overview of the current state. 6 | 7 | Note that this project was originally written using deno, but was rewritten in Rust to support Raspberry Pis. The 8 | original deno source is available in the `deno` branch. 9 | 10 | ## Running 11 | 12 | [See the wiki page on running Stylus here](https://github.com/mmastrac/stylus/wiki/Running-Stylus). 13 | 14 | ## Theory of operation 15 | 16 | **Stylus** acts as a webserver with special endpoints and a status monitoring tool. 17 | 18 | The status monitoring portion is based around scripts, written in any shell scripting language you like. Each 19 | script is run on an interval, and if the script returns `0` that is considered "up" for a given service. If the 20 | service times out, or returns a non-zero error this is considered a soft ("yellow") or hard ("red") failure. 21 | 22 | The special endpoints available on the webserver are: 23 | 24 | * `/style.css`: A dynamically generated CSS file based on the current 25 | * `/status.json`: A JSON representation of the current state 26 | 27 | The `style.css` endpoint may be linked by a HTML or SVG file served from the `static` directory that is configured. If 28 | desired, the HTML page can dynamically refresh the CSS periodically using Javascript. See the included example for a 29 | sample of how this might work. 30 | 31 | ## Getting started 32 | 33 | The first step to get started is copying the example to a location you'd like to use to run your status monitoring 34 | scripts. For each of the servers you'd like to monitor, create a new subdirectory under `monitor.d` with the name of 35 | that server. 36 | 37 | Using a tool like [diagrams.net], create an SVG diagram of your network. Attach an SVG DOM attribute to the elements 38 | you'd like to style with status changes. If you're using [diagrams.net], this can be done using the `svgdata` plugin. 39 | Alternatively, you can use the automatic identifiers generated by your SVG editor as your monitoring identifiers. 40 | 41 | From the SVG you've generated, create CSS selectors and rules that will apply styles to the appropriate elements as 42 | statuses change. The SVG `fill` attribute is a good candidate to change, but ensure that you're using `!important` on 43 | all your rules to override the fill colors created by your SVG editor. 44 | 45 | ## Configuration 46 | 47 | Example `config.yaml` for a **Stylus** install. This configuration attaches metadata to the various states and has 48 | selectors that apply to both and HTML (for a status table) and CSS (for a status SVG image). 49 | 50 | ```yaml 51 | version: 1 52 | server: 53 | port: 8000 54 | static: static/ 55 | 56 | monitor: 57 | dir: monitor.d/ 58 | 59 | css: 60 | # Arbitrary metadata can be associated with the four states 61 | metadata: 62 | blank: 63 | color: "white" 64 | red: 65 | color: "#fa897b" 66 | yellow: 67 | color: "#ffdd94" 68 | green: 69 | color: "#d0e6a5" 70 | rules: 71 | # Multiple CSS rules with handlebars replacements are supported 72 | - selectors: "#{{monitor.id}}" 73 | declarations: " 74 | background-color: {{monitor.status.css.metadata.color}} !important; 75 | " 76 | ``` 77 | 78 | The monitors are configured by creating a subdirectory in the monitor directory (default `monitor.d/`) and 79 | placing a `config.yaml` in that monitor subdirectory. 80 | 81 | ```yaml 82 | # ID is optional and will be inferred from the directory 83 | id: router-1 84 | test: 85 | interval: 60s 86 | timeout: 30s 87 | command: test.sh 88 | ``` 89 | 90 | ## Test scripts 91 | 92 | The test scripts are usually pretty simple. Note that the docker container ships with a number of useful utilities, 93 | but you can consider manually installing additional packages (either creating an additional docker container or manually 94 | running alpine's `apk` tool inside the container) to handle your specific cases. 95 | 96 | ### Ping 97 | 98 | Unless you have a particularly lossy connection, a single ping should be enough to test whether a host is up: 99 | 100 | ```bash 101 | #!/bin/bash 102 | set -xeuf -o pipefail 103 | ping -c 1 8.8.8.8 104 | ``` 105 | 106 | ### cURL 107 | 108 | For hosts with services that may be up or down, you may want to use cURL to test whether the service itself 109 | is reachable. 110 | 111 | ```bash 112 | #!/bin/bash 113 | set -xeuf -o pipefail 114 | curl --retry 2 --max-time 5 --connect-timeout 5 http://192.168.1.1:9000 115 | ``` 116 | 117 | ### Advanced techniques 118 | 119 | Tools such as `jq`, `sed`, or `awk` can be used for more advanced tests (ie: APIs). If needed, ssh can be used to 120 | connect to hosts and remote tests can be executed. `snmpwalk` and `snmpget` can also be used to construct tests for 121 | devices that speak SNMP. 122 | 123 | ## Performance 124 | 125 | **Stylus** is very lightweight, both from a processing and memory perspective. 126 | 127 | On a Raspberry Pi 1B, **Stylus** uses less than 1% of CPU while refreshing CSS at a rate of 1/s. On a 2015 MacBook Pro, 128 | Stylus uses approximately 0.1% of a single core while actively refreshing. 129 | 130 | **Stylus** uses approxmately 2MB to monitor 15 services on a Raspberry Pi (according to [ps_mem](https://raw.githubusercontent.com/pixelb/ps_mem/master/ps_mem.py)). 131 | 132 | When not actively monitored, **Stylus** uses a nearly unmeasurable amount of CPU and is pretty much limited by how 133 | heavyweight your test scripts are. 134 | 135 | ## Screenshots 136 | 137 | ### Included example 138 | 139 | ![Screenshot](docs/screenshot-1.png) 140 | 141 | ### My personal network 142 | 143 | ![Screenshot](docs/screenshot-2.png) 144 | 145 | [diagrams.net]: https://app.diagrams.net/?splash=0&p=svgdata 146 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | if [ "$#" != "1" ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | VERSION="$1" 9 | echo "Building Docker container for $VERSION..." 10 | 11 | docker build --no-cache --platform linux/arm/v6 \ 12 | --build-arg VERSION=$VERSION \ 13 | --build-arg BINARYPLATFORM=linux_arm \ 14 | --build-arg BUILDPLATFORM=arm32v6 \ 15 | --build-arg RUSTPLATFORM=arm-unknown-linux-musleabi \ 16 | -t mmastrac/stylus:latest-arm -f docker/Dockerfile . 17 | docker build --no-cache --platform linux/arm64 \ 18 | --build-arg VERSION=$VERSION \ 19 | --build-arg BINARYPLATFORM=linux_arm64 \ 20 | --build-arg BUILDPLATFORM=arm64v8 \ 21 | --build-arg RUSTPLATFORM=aarch64-unknown-linux-musl \ 22 | -t mmastrac/stylus:latest-arm64 -f docker/Dockerfile . 23 | docker build --no-cache --platform linux/amd64 \ 24 | --build-arg VERSION=$VERSION \ 25 | --build-arg BINARYPLATFORM=linux_amd64 \ 26 | --build-arg BUILDPLATFORM=amd64 \ 27 | --build-arg RUSTPLATFORM=x86_64-unknown-linux-musl \ 28 | -t mmastrac/stylus:latest-x86_64 -f docker/Dockerfile . 29 | 30 | docker push mmastrac/stylus:latest-arm 31 | docker push mmastrac/stylus:latest-arm64 32 | docker push mmastrac/stylus:latest-x86_64 33 | 34 | # TBH I don't fully understand manifests, but this seems to work 35 | rm -rf ~/.docker/manifests/docker.io_mmastrac_stylus-latest 36 | docker manifest create mmastrac/stylus:latest \ 37 | mmastrac/stylus:latest-arm \ 38 | mmastrac/stylus:latest-arm64 \ 39 | mmastrac/stylus:latest-x86_64 40 | 41 | docker manifest annotate --os linux --arch arm --variant v6 mmastrac/stylus:latest mmastrac/stylus:latest-arm 42 | docker manifest annotate --os linux --arch arm64 --variant v8 mmastrac/stylus:latest mmastrac/stylus:latest-arm64 43 | docker manifest annotate --os linux --arch amd64 mmastrac/stylus:latest mmastrac/stylus:latest-x86_64 44 | 45 | docker manifest push mmastrac/stylus:latest 46 | 47 | rm -rf ~/.docker/manifests/docker.io_mmastrac_stylus-$VERSION 48 | docker manifest create mmastrac/stylus:$VERSION \ 49 | mmastrac/stylus:latest-arm \ 50 | mmastrac/stylus:latest-arm64 \ 51 | mmastrac/stylus:latest-x86_64 52 | 53 | docker manifest annotate --os linux --arch arm --variant v6 mmastrac/stylus:$VERSION mmastrac/stylus:latest-arm 54 | docker manifest annotate --os linux --arch arm64 --variant v8 mmastrac/stylus:$VERSION mmastrac/stylus:latest-arm64 55 | docker manifest annotate --os linux --arch amd64 mmastrac/stylus:$VERSION mmastrac/stylus:latest-x86_64 56 | 57 | docker manifest push mmastrac/stylus:$VERSION 58 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILDPLATFORM 2 | FROM ${BUILDPLATFORM}/alpine:latest 3 | ARG VERSION 4 | ARG RUSTPLATFORM 5 | ARG BINARYPLATFORM 6 | ARG PUPVERSION=0.4.0 7 | VOLUME /srv 8 | EXPOSE 8000/tcp 9 | 10 | # Our useful set of tools from the Alpine repository 11 | RUN apk update && \ 12 | apk upgrade && \ 13 | apk add jq curl netcat-openbsd bash openssh-client net-snmp-tools tini openssl stunnel 14 | 15 | # Install pup via curl 16 | RUN curl -fL https://github.com/ericchiang/pup/releases/download/v${PUPVERSION}/pup_v${PUPVERSION}_${BINARYPLATFORM}.zip \ 17 | | unzip -p - > /usr/local/bin/pup \ 18 | && chmod a+x /usr/local/bin/pup 19 | 20 | # Install stylus via curl 21 | RUN curl -fL https://github.com/mmastrac/stylus/releases/download/${VERSION}/stylus_${BINARYPLATFORM} > /usr/local/bin/stylus \ 22 | && chmod a+x /usr/local/bin/stylus 23 | 24 | ENV FORCE_CONTAINER_LISTEN_ADDR=0.0.0.0 FORCE_CONTAINER_PATH=/srv/config.yaml FORCE_CONTAINER_PORT=8000 25 | CMD [] 26 | 27 | # Use tini for proper signal handling 28 | ENV TINI_VERBOSITY=0 29 | ENTRYPOINT [ "tini", "--", "/usr/local/bin/stylus" ] 30 | -------------------------------------------------------------------------------- /docs/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/stylus/f43531ac4d05a682788a93afe0a412813354c832/docs/screenshot-1.png -------------------------------------------------------------------------------- /docs/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/stylus/f43531ac4d05a682788a93afe0a412813354c832/docs/screenshot-2.png -------------------------------------------------------------------------------- /docs/svg-demo/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/svg-demo/screen-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/stylus/f43531ac4d05a682788a93afe0a412813354c832/docs/svg-demo/screen-1.png -------------------------------------------------------------------------------- /docs/svg-demo/screen-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/stylus/f43531ac4d05a682788a93afe0a412813354c832/docs/svg-demo/screen-2.png -------------------------------------------------------------------------------- /docs/svg-demo/screen-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/stylus/f43531ac4d05a682788a93afe0a412813354c832/docs/svg-demo/screen-3.png -------------------------------------------------------------------------------- /docs/svg-demo/screen-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/stylus/f43531ac4d05a682788a93afe0a412813354c832/docs/svg-demo/screen-4.png -------------------------------------------------------------------------------- /docs/svg-demo/screen-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/stylus/f43531ac4d05a682788a93afe0a412813354c832/docs/svg-demo/screen-5.png -------------------------------------------------------------------------------- /examples/dynamic/config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | server: 3 | port: 8000 4 | static: static 5 | 6 | monitor: 7 | dir: monitor.d 8 | 9 | css: 10 | # Use metadata to get prettier colors - note that we can add arbitrary string keys and values here 11 | metadata: 12 | red: 13 | color: "#fa897b" 14 | yellow: 15 | color: "#ffdd94" 16 | green: 17 | color: "#d0e6a5" 18 | rules: 19 | # Style the HTML/SVG with the appropriate status color 20 | - selectors: " 21 | #{{monitor.id}}, 22 | [data-monitor-id=\"{{monitor.id}}\"] > * 23 | " 24 | declarations: " 25 | background-color: {{monitor.status.css.metadata.color}} !important; 26 | fill: {{monitor.status.css.metadata.color}} !important; 27 | " 28 | # Add some text for the status/return value of the script 29 | - selectors: " 30 | #{{monitor.id}} td:nth-child(2)::after 31 | " 32 | declarations: " 33 | content: \"status={{monitor.status.status}} retval={{monitor.status.code}}\" 34 | " 35 | # Add the full description to the table 36 | - selectors: " 37 | #{{monitor.id}} td:nth-child(3)::after 38 | " 39 | declarations: " 40 | content: \"{{monitor.status.description}}\" 41 | " -------------------------------------------------------------------------------- /examples/dynamic/monitor.d/router/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/dynamic/monitor.d/router/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | ping -c 1 8.8.8.8 4 | -------------------------------------------------------------------------------- /examples/dynamic/monitor.d/server/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/dynamic/monitor.d/server/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo Curling 4 | curl --retry 2 --max-time 5 --connect-timeout 5 http://192.168.1.1:9000 5 | -------------------------------------------------------------------------------- /examples/dynamic/monitor.d/timeout/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 2s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/dynamic/monitor.d/timeout/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xeuf -o pipefail 3 | echo "Sleeping..." 4 | sleep 10 5 | echo "Done!" 6 | -------------------------------------------------------------------------------- /examples/dynamic/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 52 | 53 | 54 |

Dynamic Scripting Example

55 |

This example uses hand-rolled React.js code to create a dynamic status page.

56 | 57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /examples/group/config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | server: 3 | port: 8000 4 | static: static 5 | 6 | monitor: 7 | dir: monitor.d 8 | 9 | css: 10 | # Use metadata to get prettier colors - note that we can add arbitrary string keys and values here 11 | metadata: 12 | red: 13 | color: "#fa897b" 14 | yellow: 15 | color: "#ffdd94" 16 | green: 17 | color: "#d0e6a5" 18 | rules: 19 | # Style the HTML/SVG with the appropriate status color 20 | - selectors: " 21 | #{{monitor.id}}, 22 | [data-monitor-id=\"{{monitor.id}}\"] > * 23 | " 24 | declarations: " 25 | background-color: {{monitor.status.css.metadata.color}} !important; 26 | " 27 | - selectors: " 28 | #{{monitor.id}}::after 29 | " 30 | declarations: " 31 | content: \"status={{monitor.status.status}} retval={{monitor.status.code}}\" 32 | " 33 | -------------------------------------------------------------------------------- /examples/group/monitor.d/group/config.yaml: -------------------------------------------------------------------------------- 1 | group: 2 | id: port-{{ index }} 3 | axes: 4 | - name: index 5 | values: [0, 1, 2, 3, 4, 5, 6, 7] 6 | test: 7 | interval: 60s 8 | timeout: 30s 9 | command: test.sh 10 | -------------------------------------------------------------------------------- /examples/group/monitor.d/group/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ group.port-0.status.status="yellow"' 4 | echo '@@STYLUS@@ group.port-1.status.status="green"' 5 | echo '@@STYLUS@@ group.port-2.status.status="yellow"' 6 | echo '@@STYLUS@@ group.port-3.status.status="green"' 7 | echo '@@STYLUS@@ group.port-4.status.status="green"' 8 | echo '@@STYLUS@@ group.port-5.status.status="yellow"' 9 | echo '@@STYLUS@@ group.port-6.status.status="yellow"' 10 | echo '@@STYLUS@@ group.port-7.status.status="red"' 11 | -------------------------------------------------------------------------------- /examples/group/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Status page 5 | 6 | 22 | 23 | 47 | 48 | 49 |

Group Test

50 | 51 |

Status Table

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/metadata/config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | server: 3 | port: 8000 4 | static: static 5 | 6 | monitor: 7 | dir: monitor.d 8 | 9 | css: 10 | # Use metadata to get prettier colors - note that we can add arbitrary string keys and values here 11 | metadata: 12 | red: 13 | color: "#fa897b" 14 | yellow: 15 | color: "#ffdd94" 16 | green: 17 | color: "#d0e6a5" 18 | rules: 19 | # Style the HTML/SVG with the appropriate status color 20 | - selectors: " 21 | #{{monitor.id}}, 22 | [data-monitor-id=\"{{monitor.id}}\"] > * 23 | " 24 | declarations: " 25 | background-color: {{monitor.status.css.metadata.color}} !important; 26 | fill: {{monitor.status.css.metadata.color}} !important; 27 | " 28 | # Add some text for the status/return value of the script 29 | - selectors: " 30 | #{{monitor.id}} td:nth-child(2)::after 31 | " 32 | declarations: " 33 | content: \"status={{monitor.status.status}} retval={{monitor.status.code}}\" 34 | " 35 | # Add the full description to the table 36 | - selectors: " 37 | #{{monitor.id}} td:nth-child(3)::after 38 | " 39 | declarations: " 40 | content: \"{{monitor.status.description}}\" 41 | " 42 | # Add the metadata to the table 43 | - selectors: " 44 | #{{monitor.id}} td:nth-child(4)::after 45 | " 46 | declarations: " 47 | content: \"{{monitor.status.metadata.key}}\" 48 | " 49 | -------------------------------------------------------------------------------- /examples/metadata/monitor.d/metadata1/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/metadata/monitor.d/metadata1/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ status.description="Custom (yellow)"' 4 | echo '@@STYLUS@@ status.status="yellow"' 5 | echo '@@STYLUS@@ status.metadata.key="value1"' 6 | -------------------------------------------------------------------------------- /examples/metadata/monitor.d/metadata2/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/metadata/monitor.d/metadata2/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ status.description="Custom (red)"' 4 | echo '@@STYLUS@@ status.status="red"' 5 | echo '@@STYLUS@@ status.metadata.key="value2"' 6 | -------------------------------------------------------------------------------- /examples/metadata/monitor.d/metadata3/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/metadata/monitor.d/metadata3/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ status.description="Custom (blank)"' 4 | echo '@@STYLUS@@ status.status="blank"' 5 | echo '@@STYLUS@@ status.metadata.key="value3"' 6 | -------------------------------------------------------------------------------- /examples/metadata/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Status page 5 | 6 | 18 | 19 | 43 | 44 | 45 |

Providing Metadata

46 | 47 |

Status Table

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
NameStatusDescriptionCustom Metadata
metadata1
metadata2
metadata3
75 | 76 | 77 | -------------------------------------------------------------------------------- /examples/simple_network/config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | server: 3 | port: 8000 4 | static: static 5 | 6 | monitor: 7 | dir: monitor.d 8 | 9 | css: 10 | # Use metadata to get prettier colors - note that we can add arbitrary string keys and values here 11 | metadata: 12 | red: 13 | color: "#fa897b" 14 | yellow: 15 | color: "#ffdd94" 16 | green: 17 | color: "#d0e6a5" 18 | rules: 19 | # Style the HTML/SVG with the appropriate status color 20 | - selectors: " 21 | #{{monitor.id}}, 22 | [data-monitor-id=\"{{monitor.id}}\"] > * 23 | " 24 | declarations: " 25 | background-color: {{monitor.status.css.metadata.color}} !important; 26 | fill: {{monitor.status.css.metadata.color}} !important; 27 | " 28 | # Add some text for the status/return value of the script 29 | - selectors: " 30 | #{{monitor.id}} td:nth-child(2)::after 31 | " 32 | declarations: " 33 | content: \"status={{monitor.status.status}} retval={{monitor.status.code}}\" 34 | " 35 | # Add the full description to the table 36 | - selectors: " 37 | #{{monitor.id}} td:nth-child(3)::after 38 | " 39 | declarations: " 40 | content: \"{{monitor.status.description}}\" 41 | " 42 | -------------------------------------------------------------------------------- /examples/simple_network/monitor.d/router/config.yaml: -------------------------------------------------------------------------------- 1 | # Explicitly set the id here 2 | id: router 3 | test: 4 | interval: 60s 5 | timeout: 30s 6 | command: test.sh 7 | -------------------------------------------------------------------------------- /examples/simple_network/monitor.d/router/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | ping -c 1 8.8.8.8 4 | -------------------------------------------------------------------------------- /examples/simple_network/monitor.d/server/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/simple_network/monitor.d/server/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo Curling 4 | curl --retry 2 --max-time 5 --connect-timeout 5 http://192.168.1.1:9000 5 | -------------------------------------------------------------------------------- /examples/simple_network/monitor.d/timeout/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 2s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /examples/simple_network/monitor.d/timeout/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xeuf -o pipefail 3 | echo "Sleeping..." 4 | sleep 10 5 | echo "Done!" 6 | -------------------------------------------------------------------------------- /examples/simple_network/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Status page 5 | 6 | 18 | 19 | 43 | 44 | 45 |

Example Status Page

46 | 47 |

Status Table

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
NameStatusDescription
router
server
timeout
71 | 72 |

Status Diagram

73 | 74 | 75 |
Wifi
Wifi
Internet
Internet
Firewall
Firewall
Viewer does not support full SVG 1.1
76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/config/args.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use structopt::{clap::ArgGroup, StructOpt}; 3 | 4 | #[derive(Debug, StructOpt)] 5 | #[structopt(group = ArgGroup::with_name("path").required(true))] 6 | pub struct Args { 7 | // TODO 8 | // /// Daemonize stylus and detact from the tty 9 | // #[structopt(long, short, parse(from_flag))] 10 | // pub daemonize: bool, 11 | /// Pass multiple times to increase the level of verbosity (overwritten by STYLUS_LOG) 12 | #[structopt( 13 | name = "verbose", 14 | long = "verbose", 15 | short = "v", 16 | parse(from_occurrences), 17 | global = true 18 | )] 19 | verbose: u8, 20 | 21 | /// Dumps the effective configuration without running 22 | #[structopt(long, parse(from_flag))] 23 | pub dump: bool, 24 | 25 | /// If specified, runs the given test immediately and displays the status of the given monitor after it completes 26 | #[structopt(long, conflicts_with = "dump")] 27 | pub test: Option, 28 | 29 | /// The configuration file 30 | #[structopt(name = "FILE", parse(from_os_str), group = "path")] 31 | pub config: Option, 32 | 33 | /// Advanced: if running in a container, allows the container to override any port specified in config.yaml 34 | #[structopt(long, env = "FORCE_CONTAINER_PORT")] 35 | pub force_container_port: Option, 36 | 37 | /// Advanced: if running in a container, allows the container to specify that stylus should listen on the wildcard address 38 | #[structopt(long, env = "FORCE_CONTAINER_LISTEN_ADDR")] 39 | pub force_container_listen_addr: Option, 40 | 41 | /// Advanced: if running a container, allows the container to override any path specified on the command line 42 | #[structopt(long, env = "FORCE_CONTAINER_PATH", group = "path")] 43 | pub force_container_path: Option, 44 | } 45 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::error::Error; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use itertools::Itertools; 6 | use structopt::StructOpt; 7 | use walkdir::WalkDir; 8 | 9 | use self::args::Args; 10 | pub use self::structs::*; 11 | use crate::interpolate::*; 12 | 13 | mod args; 14 | mod structs; 15 | 16 | pub fn parse_config_from_args() -> Result> { 17 | let args = Args::from_args(); 18 | let config_path = if let Some(path) = args.force_container_path { 19 | path 20 | } else { 21 | args.config.unwrap() 22 | }; 23 | let mut config = parse_config(&config_path)?; 24 | if let Some(port) = args.force_container_port { 25 | config.server.port = port 26 | }; 27 | if let Some(addr) = args.force_container_listen_addr { 28 | config.server.listen_addr = addr 29 | } 30 | debug!("{:?}", config); 31 | 32 | if args.dump { 33 | Ok(OperationMode::Dump(config)) 34 | } else if let Some(test) = args.test { 35 | Ok(OperationMode::Test(config, test)) 36 | } else { 37 | Ok(OperationMode::Run(config)) 38 | } 39 | } 40 | 41 | pub fn parse_config(file: &Path) -> Result> { 42 | let curr = std::env::current_dir()?; 43 | let mut path = Path::new(&file).into(); 44 | canonicalize("configuration", Some(&curr), &mut path)?; 45 | if path.is_dir() { 46 | warn!("Passed configuration location {:?} was a directory -- inferring 'config.yaml' in that directory", file); 47 | path = path.join("config.yaml"); 48 | } 49 | let s = std::fs::read_to_string(&path)?; 50 | parse_config_string(&path, s) 51 | } 52 | 53 | /// Given a base path and a relative path, gets the full path (or errors out if it doesn't exist). 54 | fn canonicalize( 55 | what: &str, 56 | base_path: Option<&Path>, 57 | path: &mut PathBuf, 58 | ) -> Result<(), Box> { 59 | let new = match base_path { 60 | None => path.clone(), 61 | Some(base_path) => base_path.join(&path), 62 | }; 63 | 64 | if !new.exists() { 65 | Err(if let Some(base_path) = base_path { 66 | format!( 67 | "{} does not exist ({}, base path was {})", 68 | what, 69 | path.to_string_lossy(), 70 | base_path.to_string_lossy() 71 | ) 72 | } else { 73 | format!("{} does not exist ({})", what, path.to_string_lossy()) 74 | } 75 | .into()) 76 | } else { 77 | *path = new.canonicalize()?; 78 | Ok(()) 79 | } 80 | } 81 | 82 | pub fn parse_config_string(file: &Path, s: String) -> Result> { 83 | let mut config: Config = serde_yaml::from_str(&s)?; 84 | if Iterator::count(config.base_path.components()) == 0 { 85 | config.base_path = Path::parent(Path::new(&file)) 86 | .ok_or("Failed to get base path")? 87 | .into(); 88 | } 89 | 90 | for mut css in config.css.rules.iter_mut() { 91 | if css.declarations.contains("monitor.config.id") 92 | || css.selectors.contains("monitor.config.id") 93 | { 94 | let msg = "Found deprecated 'monitor.config.id' in template. Please use 'monitor.id'"; 95 | warn!("{}", msg); 96 | return Err(msg.into()); 97 | } 98 | 99 | css.declarations = css.declarations.trim().to_string(); 100 | css.selectors = css.selectors.trim().to_string(); 101 | } 102 | 103 | // Canonical paths 104 | canonicalize("base path", None, &mut config.base_path)?; 105 | canonicalize( 106 | "static file path", 107 | Some(&config.base_path), 108 | &mut config.server.r#static, 109 | )?; 110 | canonicalize( 111 | "monitor directory path", 112 | Some(&config.base_path), 113 | &mut config.monitor.dir, 114 | )?; 115 | 116 | Ok(config) 117 | } 118 | 119 | pub fn parse_monitor_configs(root: &Path) -> Result, Box> { 120 | let mut monitor_configs = vec![]; 121 | for e in WalkDir::new(root) 122 | .min_depth(1) 123 | .max_depth(1) 124 | .follow_links(true) 125 | .into_iter() 126 | { 127 | let e = e?; 128 | if e.file_type().is_dir() { 129 | let mut p = e.into_path(); 130 | p.push("config.yaml"); 131 | if p.exists() { 132 | monitor_configs.push(parse_monitor_config(&p)?); 133 | info!("Found monitor in {:?}", p); 134 | } else { 135 | debug!("Ignoring {:?} as there was no config.yaml", p); 136 | } 137 | } else { 138 | debug!("Ignoring {:?} as it was not a directory", e.path()); 139 | } 140 | } 141 | 142 | if monitor_configs.is_empty() { 143 | Err(format!( 144 | "Unable to locate any valid configurations in {}", 145 | root.to_string_lossy() 146 | ) 147 | .into()) 148 | } else { 149 | Ok(monitor_configs) 150 | } 151 | } 152 | 153 | pub fn parse_monitor_config(file: &Path) -> Result> { 154 | let s = std::fs::read_to_string(&file)?; 155 | parse_monitor_config_string(file, s) 156 | } 157 | 158 | pub fn parse_monitor_config_string( 159 | file: &Path, 160 | s: String, 161 | ) -> Result> { 162 | let mut config: MonitorDirConfig = serde_yaml::from_str(&s)?; 163 | if Iterator::count(config.base_path.components()) == 0 { 164 | config.base_path = Path::parent(file).ok_or("Failed to get base path")?.into(); 165 | } 166 | 167 | // Canonical paths 168 | canonicalize("base path", None, &mut config.base_path)?; 169 | 170 | if config.id.is_empty() { 171 | config.id = file 172 | .parent() 173 | .ok_or("Invalid parent")? 174 | .file_name() 175 | .ok_or("Invalid file name")? 176 | .to_string_lossy() 177 | .to_string(); 178 | } 179 | 180 | let test = config.root.test_mut(); 181 | test.command = Path::canonicalize(&config.base_path.join(&test.command))?; 182 | 183 | let mut children = BTreeMap::new(); 184 | if let MonitorDirRootConfig::Group(ref mut group) = config.root { 185 | for values in group 186 | .axes 187 | .iter() 188 | .map(|axis| axis.values.iter().map(move |v| (v, &axis.name))) 189 | .multi_cartesian_product() 190 | { 191 | let mut axes = BTreeMap::new(); 192 | for val in values { 193 | axes.insert(val.1.to_owned(), val.0.to_owned()); 194 | } 195 | 196 | let id = interpolate_id(&axes, &group.id)?; 197 | let child = MonitorDirChildConfig { 198 | axes, 199 | test: group.test.clone(), 200 | }; 201 | children.insert(id, child); 202 | } 203 | group.children = children; 204 | } 205 | 206 | Ok(config) 207 | } 208 | 209 | #[cfg(test)] 210 | mod test { 211 | use super::*; 212 | 213 | #[test] 214 | fn deserialize_config_test() -> Result<(), Box> { 215 | let config = parse_config(Path::new("src/testcases/v1.yaml"))?; 216 | assert_eq!(config.base_path, Path::new("src/testcases").canonicalize()?); 217 | Ok(()) 218 | } 219 | 220 | #[test] 221 | fn deserialize_monitor_test() -> Result<(), Box> { 222 | let config = parse_monitor_config_string( 223 | &Path::new("/tmp/test.yaml"), 224 | r#" 225 | # Explicitly set the id here 226 | id: router 227 | test: 228 | interval: 60s 229 | timeout: 30s 230 | command: /bin/sleep 231 | "# 232 | .into(), 233 | )?; 234 | 235 | match config.root { 236 | MonitorDirRootConfig::Test(test) => { 237 | assert_eq!(test.command, Path::new("/bin/sleep").canonicalize()?) 238 | } 239 | _ => panic!(""), 240 | } 241 | 242 | Ok(()) 243 | } 244 | 245 | #[test] 246 | fn deserialize_monitor_group() -> Result<(), Box> { 247 | let config = parse_monitor_config_string( 248 | &Path::new("/tmp/test.yaml"), 249 | r#" 250 | # Explicitly set the id here 251 | id: router 252 | group: 253 | id: group-{{ index }} 254 | axes: 255 | - values: [1, 2, 3] 256 | name: index 257 | test: 258 | interval: 60s 259 | timeout: 30s 260 | command: /bin/sleep 261 | "# 262 | .into(), 263 | )?; 264 | 265 | match config.root { 266 | MonitorDirRootConfig::Group(group) => { 267 | assert_eq!(group.test.command, Path::new("/bin/sleep").canonicalize()?) 268 | } 269 | _ => panic!(""), 270 | } 271 | 272 | Ok(()) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/config/structs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::path::PathBuf; 3 | use std::sync::Arc; 4 | use std::time::Duration; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | pub enum OperationMode { 9 | Run(Config), 10 | Dump(Config), 11 | Test(Config, String), 12 | } 13 | 14 | fn default_server_port() -> u16 { 15 | 80 16 | } 17 | 18 | fn default_listen_addr() -> String { 19 | "0.0.0.0".into() 20 | } 21 | 22 | fn default_server_static() -> PathBuf { 23 | "static".into() 24 | } 25 | 26 | #[derive(Clone, Debug, Serialize, Deserialize)] 27 | #[serde(deny_unknown_fields)] 28 | pub struct Config { 29 | pub version: u32, 30 | pub server: ServerConfig, 31 | pub monitor: MonitorConfig, 32 | pub css: CssConfig, 33 | #[serde(default)] 34 | pub base_path: PathBuf, 35 | } 36 | 37 | #[derive(Clone, Debug, Serialize, Deserialize)] 38 | #[serde(deny_unknown_fields)] 39 | pub struct ServerConfig { 40 | #[serde(default = "default_server_port")] 41 | pub port: u16, 42 | #[serde(default = "default_listen_addr")] 43 | pub listen_addr: String, 44 | #[serde(default = "default_server_static")] 45 | pub r#static: PathBuf, 46 | } 47 | 48 | #[derive(Clone, Debug, Serialize, Deserialize)] 49 | #[serde(deny_unknown_fields)] 50 | pub struct MonitorConfig { 51 | pub dir: PathBuf, 52 | } 53 | 54 | #[derive(Clone, Debug, Serialize, Deserialize)] 55 | #[serde(deny_unknown_fields)] 56 | pub struct CssConfig { 57 | pub metadata: CssMetadataConfig, 58 | pub rules: Vec, 59 | } 60 | 61 | #[derive(Clone, Debug, Serialize, Deserialize)] 62 | #[serde(deny_unknown_fields)] 63 | pub struct CssRule { 64 | pub selectors: String, 65 | pub declarations: String, 66 | } 67 | 68 | #[derive(Clone, Debug, Serialize, Deserialize)] 69 | #[serde(deny_unknown_fields)] 70 | pub struct CssMetadataConfig { 71 | #[serde(default)] 72 | pub blank: Arc>, 73 | #[serde(default)] 74 | pub red: Arc>, 75 | #[serde(default)] 76 | pub yellow: Arc>, 77 | #[serde(default)] 78 | pub green: Arc>, 79 | } 80 | 81 | #[cfg(test)] 82 | impl Default for CssMetadataConfig { 83 | fn default() -> Self { 84 | Self { 85 | blank: Arc::new(Default::default()), 86 | red: Arc::new(Default::default()), 87 | yellow: Arc::new(Default::default()), 88 | green: Arc::new(Default::default()), 89 | } 90 | } 91 | } 92 | 93 | #[derive(Clone, Debug, Serialize, Deserialize)] 94 | #[serde(deny_unknown_fields)] 95 | pub struct MonitorDirConfig { 96 | #[serde(flatten)] 97 | pub root: MonitorDirRootConfig, 98 | #[serde(default)] 99 | pub base_path: PathBuf, 100 | #[serde(default)] 101 | pub id: String, 102 | } 103 | 104 | #[derive(Clone, Debug, Serialize, Deserialize)] 105 | #[serde(rename_all = "snake_case")] 106 | #[serde(deny_unknown_fields)] 107 | pub enum MonitorDirRootConfig { 108 | Test(MonitorDirTestConfig), 109 | Group(MonitorDirGroupConfig), 110 | } 111 | 112 | impl MonitorDirRootConfig { 113 | /// Get the MonitorDirTestConfig for this. 114 | pub fn test(&self) -> &MonitorDirTestConfig { 115 | match self { 116 | MonitorDirRootConfig::Test(ref test) => test, 117 | MonitorDirRootConfig::Group(ref group) => &group.test, 118 | } 119 | } 120 | 121 | /// Get the MonitorDirTestConfig for this. 122 | pub fn test_mut(&mut self) -> &mut MonitorDirTestConfig { 123 | match self { 124 | MonitorDirRootConfig::Test(ref mut test) => test, 125 | MonitorDirRootConfig::Group(ref mut group) => &mut group.test, 126 | } 127 | } 128 | } 129 | 130 | #[derive(Clone, Debug, Serialize, Deserialize)] 131 | #[serde(deny_unknown_fields)] 132 | pub struct MonitorDirGroupConfig { 133 | pub id: String, 134 | pub test: MonitorDirTestConfig, 135 | pub axes: Vec, 136 | #[serde(skip_deserializing)] 137 | pub children: BTreeMap, 138 | } 139 | 140 | #[derive(Clone, Debug, Serialize, Deserialize)] 141 | #[serde(deny_unknown_fields)] 142 | pub struct MonitorDirChildConfig { 143 | pub axes: BTreeMap, 144 | pub test: MonitorDirTestConfig, 145 | } 146 | 147 | #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)] 148 | #[serde(untagged)] 149 | #[serde(deny_unknown_fields)] 150 | pub enum MonitorDirAxisValue { 151 | String(String), 152 | Number(i64), 153 | } 154 | 155 | #[derive(Clone, Debug, Serialize, Deserialize)] 156 | #[serde(deny_unknown_fields)] 157 | pub struct MonitorDirAxisConfig { 158 | pub values: Vec, 159 | pub name: String, 160 | } 161 | 162 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 163 | #[serde(deny_unknown_fields)] 164 | pub struct MonitorDirTestConfig { 165 | #[serde(with = "humantime_serde")] 166 | pub interval: Duration, 167 | #[serde(with = "humantime_serde")] 168 | pub timeout: Duration, 169 | pub command: PathBuf, 170 | } 171 | -------------------------------------------------------------------------------- /src/css.rs: -------------------------------------------------------------------------------- 1 | use crate::config::*; 2 | use crate::interpolate::*; 3 | use crate::status::*; 4 | 5 | pub fn generate_css_for_state(config: &CssConfig, status: &Status) -> String { 6 | let mut css = format!("/* Generated at {:?} */\n", std::time::Instant::now()); 7 | for monitor in &status.monitors { 8 | css += "\n"; 9 | let mut monitor = monitor.write(); 10 | 11 | // Build the css from cache 12 | let mut cache = monitor.css.take(); 13 | css += cache.get_or_insert_with(|| generate_css_for_monitor(&config, &monitor)); 14 | monitor.css = cache; 15 | } 16 | css 17 | } 18 | 19 | pub fn generate_css_for_monitor(config: &CssConfig, monitor: &MonitorState) -> String { 20 | let mut css = format!("/* {} */\n", monitor.id); 21 | for rule in &config.rules { 22 | css += &interpolate_monitor( 23 | &monitor.id, 24 | &monitor.config, 25 | &monitor.status, 26 | &rule.selectors, 27 | ) 28 | .unwrap_or_else(|_| "/* failed */".into()); 29 | css += " {\n"; 30 | css += &interpolate_monitor( 31 | &monitor.id, 32 | &monitor.config, 33 | &monitor.status, 34 | &rule.declarations, 35 | ) 36 | .unwrap_or_else(|_| "/* failed */".into()); 37 | css += "\n}\n\n"; 38 | } 39 | for child in monitor.children.iter() { 40 | for rule in &config.rules { 41 | css += &interpolate_monitor(child.0, &monitor.config, &child.1.status, &rule.selectors) 42 | .unwrap_or_else(|_| "/* failed */".into()); 43 | css += " {\n"; 44 | css += &interpolate_monitor( 45 | child.0, 46 | &monitor.config, 47 | &child.1.status, 48 | &rule.declarations, 49 | ) 50 | .unwrap_or_else(|_| "/* failed */".into()); 51 | css += "\n}\n\n"; 52 | } 53 | } 54 | css 55 | } 56 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::net::{IpAddr, SocketAddr}; 3 | use std::sync::Arc; 4 | 5 | use warp::path; 6 | use warp::Filter; 7 | 8 | use crate::config::Config; 9 | use crate::css::generate_css_for_state; 10 | use crate::monitor::Monitor; 11 | use crate::status::Status; 12 | 13 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 14 | 15 | async fn css_request(monitor: Arc) -> Result { 16 | Ok(generate_css_for_state( 17 | &monitor.config.css, 18 | &monitor.status(), 19 | )) 20 | } 21 | 22 | async fn status_request(monitor: Arc) -> Result { 23 | Ok(monitor.status()) 24 | } 25 | 26 | async fn log_request(monitor: Arc, s: String) -> Result { 27 | for monitor in monitor.status().monitors { 28 | let monitor = monitor.read(); 29 | if monitor.id == s { 30 | let mut logs = String::new(); 31 | for log in &monitor.status.log { 32 | logs += &log; 33 | logs += "\n"; 34 | } 35 | return Ok(logs); 36 | } 37 | } 38 | Ok("Not found".to_owned()) 39 | } 40 | 41 | pub async fn run(config: Config) { 42 | let monitor = Arc::new(Monitor::new(&config).expect("Unable to create monitor")); 43 | let with_monitor = warp::any().map(move || monitor.clone()); 44 | 45 | // style.css for formatting 46 | let style = path!("style.css") 47 | .and(with_monitor.clone()) 48 | .and_then(css_request) 49 | .with(warp::reply::with::header("Content-Type", "text/css")); 50 | 51 | // status.json for advanced integrations 52 | let status = path!("status.json") 53 | .and(with_monitor.clone()) 54 | .and_then(status_request) 55 | .map(|s| warp::reply::json(&s)) 56 | .with(warp::reply::with::header( 57 | "Content-Type", 58 | "application/json", 59 | )); 60 | 61 | // logging endpoint 62 | let log = path!("log" / String) 63 | .and(with_monitor.clone()) 64 | .and_then(|s, m| log_request(m, s)) 65 | .with(warp::reply::with::header("Content-Type", "text/plain")); 66 | 67 | // static pages 68 | let r#static = warp::fs::dir(config.server.r#static); 69 | 70 | let routes = warp::get().and(style.or(status).or(log).or(r#static)); 71 | 72 | let ip_addr = config 73 | .server 74 | .listen_addr 75 | .parse::() 76 | .expect("Failed to parse listen address"); 77 | let addr = SocketAddr::new(ip_addr, config.server.port); 78 | 79 | // We print one and only one message 80 | eprintln!("Stylus {} is listening on {}!", VERSION, addr); 81 | 82 | warp::serve(routes).run(addr).await; 83 | } 84 | -------------------------------------------------------------------------------- /src/interpolate.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::error::Error; 3 | 4 | use handlebars::*; 5 | use itertools::Itertools; 6 | use serde::Serialize; 7 | use serde_json::value::*; 8 | 9 | use crate::config::{MonitorDirAxisValue, MonitorDirTestConfig}; 10 | use crate::status::*; 11 | 12 | pub fn interpolate_monitor( 13 | id: &str, 14 | config: &MonitorDirTestConfig, 15 | status: &MonitorStatus, 16 | s: &str, 17 | ) -> Result> { 18 | // TODO: avoid creating this handlebars registry every time 19 | let mut handlebars = Handlebars::new(); 20 | handlebars.register_template_string("t", s)?; 21 | 22 | let mut map = BTreeMap::new(); 23 | #[derive(Clone, Debug, Serialize)] 24 | struct Monitor<'a> { 25 | id: &'a str, 26 | config: &'a MonitorDirTestConfig, 27 | status: &'a MonitorStatus, 28 | } 29 | map.insert("monitor", Monitor { id, config, status }); 30 | Ok(handlebars.render("t", &map)?.trim().to_owned()) 31 | } 32 | 33 | pub fn interpolate_id( 34 | values: &BTreeMap, 35 | s: &str, 36 | ) -> Result> { 37 | // TODO: avoid creating this handlebars registry every time 38 | let mut handlebars = Handlebars::new(); 39 | handlebars.set_strict_mode(true); 40 | handlebars.register_template_string("t", s)?; 41 | 42 | Ok(handlebars.render("t", values)?.trim().to_owned()) 43 | } 44 | 45 | pub fn interpolate_modify<'a>( 46 | mut status: &'a mut MonitorStatus, 47 | children: &'a mut BTreeMap, 48 | s: &str, 49 | ) -> Result<(), Box> { 50 | let (raw_path, value) = s.splitn(2, '=').next_tuple().ok_or("Invalid expression")?; 51 | let value: Value = serde_json::from_str(value)?; 52 | let mut path = raw_path.split('.'); 53 | 54 | match path.next() { 55 | Some("status") => {} 56 | Some("group") => { 57 | let part = path.next().ok_or("Missing group child")?; 58 | status = &mut children 59 | .get_mut(part) 60 | .ok_or(format!("Could not find child '{}'", part))? 61 | .status; 62 | if path.next() != Some("status") { 63 | return Err(format!("Invalid path: {}", raw_path).into()); 64 | } 65 | } 66 | _ => return Err(format!("Invalid path: {}", raw_path).into()), 67 | }; 68 | 69 | let pending = status 70 | .pending 71 | .get_or_insert_with(MonitorPendingStatus::default); 72 | 73 | match path.next() { 74 | Some("status") => { 75 | pending.status = Some(serde_json::from_value(value)?); 76 | } 77 | Some("description") => { 78 | pending.description = Some(serde_json::from_value(value)?); 79 | } 80 | Some("metadata") => match path.next() { 81 | Some(s) => { 82 | let metadata = pending.metadata.get_or_insert_with(BTreeMap::new); 83 | drop(metadata.insert(s.to_owned(), serde_json::from_value(value)?)) 84 | } 85 | _ => return Err(format!("Invalid path: {}", raw_path).into()), 86 | }, 87 | _ => return Err(format!("Invalid path: {}", raw_path).into()), 88 | } 89 | 90 | Ok(()) 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use super::*; 96 | use std::iter::FromIterator; 97 | use std::sync::Arc; 98 | 99 | fn update(s: &'static str) -> Result> { 100 | let mut status = Default::default(); 101 | 102 | interpolate_modify(&mut status, &mut BTreeMap::new(), s)?; 103 | Ok(status) 104 | } 105 | 106 | #[test] 107 | fn test_interpolate_id() -> Result<(), Box> { 108 | let mut values = BTreeMap::new(); 109 | values.insert("index".to_owned(), MonitorDirAxisValue::Number(2)); 110 | assert_eq!(interpolate_id(&values, "port-{{ index }}")?, "port-2"); 111 | Ok(()) 112 | } 113 | 114 | #[test] 115 | fn test_interpolate_error() -> Result<(), Box> { 116 | let mut values = BTreeMap::new(); 117 | values.insert("index".to_owned(), MonitorDirAxisValue::Number(2)); 118 | assert!(interpolate_id(&values, "port-{{ ondex }}").is_err()); 119 | Ok(()) 120 | } 121 | 122 | #[test] 123 | fn test_replace() -> Result<(), Box> { 124 | let config = Default::default(); 125 | let mut status = MonitorStatus::default(); 126 | status.css.metadata = Arc::new(BTreeMap::from_iter(vec![( 127 | "color".to_owned(), 128 | "blue".to_owned(), 129 | )])); 130 | assert_eq!( 131 | interpolate_monitor( 132 | "id", 133 | &config, 134 | &status, 135 | "{{monitor.status.css.metadata.color}}" 136 | )?, 137 | "blue" 138 | ); 139 | Ok(()) 140 | } 141 | 142 | #[test] 143 | fn test_modify() -> Result<(), Box> { 144 | let status = update("status.status=\"red\"")?; 145 | assert_eq!(status.pending.unwrap().status.unwrap(), StatusState::Red); 146 | let status = update("status.description=\"foo\"")?; 147 | assert_eq!(status.pending.unwrap().description.unwrap(), "foo"); 148 | let status = update("status.metadata.foo=\"bar\"")?; 149 | let mut map = BTreeMap::new(); 150 | map.insert("foo".to_owned(), "bar".to_owned()); 151 | assert_eq!(status.pending.unwrap().metadata.unwrap(), map); 152 | Ok(()) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | use env_logger::Env; 3 | use keepcalm::SharedMut; 4 | 5 | mod config; 6 | mod css; 7 | mod http; 8 | mod interpolate; 9 | mod monitor; 10 | mod status; 11 | mod worker; 12 | 13 | #[macro_use] 14 | extern crate log; 15 | #[macro_use] 16 | extern crate derive_more; 17 | 18 | use crate::config::{parse_config_from_args, parse_monitor_configs, OperationMode}; 19 | use crate::status::{MonitorState, Status}; 20 | use crate::worker::monitor_run; 21 | 22 | #[tokio::main] 23 | async fn main() -> () { 24 | run().await 25 | } 26 | 27 | async fn run() { 28 | // Manually bootstrap logging from args 29 | let default = match std::env::args().filter(|s| s == "-v").count() { 30 | 0 => "stylus=warn", 31 | 1 => "stylus=info", 32 | 2 => "stylus=debug", 33 | _ => "debug", 34 | }; 35 | env_logger::init_from_env(Env::new().filter_or("STYLUS_LOG", default)); 36 | 37 | match parse_config_from_args().expect("Unable to parse configuration") { 38 | OperationMode::Run(config) => crate::http::run(config).await, 39 | OperationMode::Dump(config) => { 40 | let monitors = parse_monitor_configs(&config.monitor.dir) 41 | .expect("Unable to parse monitor configurations"); 42 | let status = Status { 43 | config, 44 | monitors: monitors 45 | .iter() 46 | .map(|m| SharedMut::new(m.into())) 47 | .collect(), 48 | }; 49 | println!( 50 | "{}", 51 | serde_json::to_string_pretty(&status) 52 | .expect("Unable to pretty-print configuration") 53 | ); 54 | } 55 | OperationMode::Test(config, id) => { 56 | let monitors = parse_monitor_configs(&config.monitor.dir) 57 | .expect("Unable to parse monitor configurations"); 58 | for monitor in monitors.iter() { 59 | if monitor.id == id { 60 | let mut state: MonitorState = monitor.into(); 61 | println!("Monitor Log"); 62 | println!("-----------"); 63 | println!(); 64 | 65 | monitor_run(&monitor, &mut |_, msg| { 66 | state 67 | .process_message(&monitor.id, msg, &config.css.metadata, &mut |m| { 68 | eprintln!("{}", m); 69 | }) 70 | .expect("Failed to process message"); 71 | Ok(()) 72 | }) 73 | .1 74 | .expect("Failed to run the monitor"); 75 | 76 | println!(); 77 | println!("State"); 78 | println!("-----"); 79 | println!(); 80 | println!( 81 | "{}", 82 | serde_json::to_string_pretty(&state) 83 | .expect("Unable to pretty-print configuration") 84 | ); 85 | 86 | println!(); 87 | println!("CSS"); 88 | println!("---"); 89 | println!(); 90 | 91 | println!( 92 | "{}", 93 | crate::css::generate_css_for_monitor(&config.css, &state) 94 | ); 95 | return; 96 | } 97 | } 98 | 99 | panic!("Unable to locate monitor with id '{}'", id) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::thread; 3 | 4 | use keepcalm::SharedMut; 5 | 6 | use crate::config::*; 7 | use crate::status::*; 8 | use crate::worker::{monitor_thread, ShuttingDown}; 9 | 10 | /// We don't want to store the actual sender in the MonitorThread, just a boxed version of it that 11 | /// will correctly drop to trigger the thread to shut down. 12 | trait OpaqueSender: std::fmt::Debug + Send + Sync {} 13 | 14 | impl OpaqueSender for T where T: std::fmt::Debug + Send + Sync {} 15 | 16 | #[derive(Debug)] 17 | struct MonitorThread { 18 | /// This is solely used to detect when [`MonitorThread`] is dropped. 19 | #[allow(unused)] 20 | drop_detect: SharedMut<()>, 21 | state: SharedMut, 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct Monitor { 26 | pub config: Config, 27 | monitors: Vec, 28 | } 29 | 30 | impl MonitorThread { 31 | /// Create a new monitor thread and release it 32 | fn create( 33 | monitor: MonitorDirConfig, 34 | mut state: MonitorState, 35 | css_config: CssMetadataConfig, 36 | ) -> Result> { 37 | state.status.initialize(&css_config); 38 | for state in &mut state.children { 39 | state.1.status.initialize(&css_config); 40 | } 41 | let state = SharedMut::new(state); 42 | 43 | let monitor_state = state.clone(); 44 | let drop_detect = SharedMut::new(()); 45 | let mut drop_detect_clone = Some(drop_detect.clone()); 46 | let _thread = thread::spawn(move || { 47 | monitor_thread(monitor, move |id, m| { 48 | drop_detect_clone = if let Some(drop_detect) = drop_detect_clone.take() { 49 | match drop_detect.try_unwrap() { 50 | Ok(_) => None, 51 | Err(drop_detect) => Some(drop_detect), 52 | } 53 | } else { 54 | None 55 | }; 56 | 57 | if drop_detect_clone.is_none() { 58 | return Err(ShuttingDown::default().into()); 59 | } 60 | monitor_state.write().process_message( 61 | id, 62 | m, 63 | &css_config, 64 | &mut |_| {}, 65 | ) 66 | }); 67 | }); 68 | 69 | let thread = MonitorThread { 70 | state, 71 | drop_detect 72 | }; 73 | 74 | Ok(thread) 75 | } 76 | } 77 | 78 | impl Monitor { 79 | pub fn new(config: &Config) -> Result> { 80 | let config = config.clone(); 81 | let mut monitors = Vec::new(); 82 | for monitor_config in &parse_monitor_configs(&config.monitor.dir)? { 83 | monitors.push(MonitorThread::create( 84 | monitor_config.clone(), 85 | monitor_config.into(), 86 | config.css.metadata.clone(), 87 | )?); 88 | } 89 | Ok(Monitor { config, monitors }) 90 | } 91 | 92 | pub fn status(&self) -> Status { 93 | Status { 94 | config: self.config.clone(), 95 | monitors: self.monitors.iter().map(|m| m.state.clone()).collect(), 96 | } 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod test { 102 | use super::*; 103 | use crate::worker::monitor_run; 104 | use std::path::Path; 105 | 106 | fn extract_status(status: &MonitorStatus) -> (StatusState, String, i64) { 107 | ( 108 | status.status.unwrap(), 109 | status.description.clone(), 110 | status.code, 111 | ) 112 | } 113 | 114 | fn extract_child_results(state: MonitorState) -> Vec<(StatusState, String, i64)> { 115 | state 116 | .children 117 | .iter() 118 | .map(|c| extract_status(&c.1.status)) 119 | .collect() 120 | } 121 | 122 | fn run_test(test: &str) -> Result> { 123 | let config = 124 | parse_monitor_config(Path::new(&format!("src/testcases/{}/config.yaml", test)))?; 125 | let mut state: MonitorState = (&config).into(); 126 | let metadata = CssMetadataConfig::default(); 127 | monitor_run(&config, &mut |id, m| { 128 | state.process_message(id, m, &metadata, &mut |_| {}) 129 | }) 130 | .1?; 131 | Ok(state) 132 | } 133 | 134 | /// Test if metadata is set correctly when a script succeeds. 135 | #[test] 136 | fn metadata_success_test() -> Result<(), Box> { 137 | use StatusState::*; 138 | let state = run_test("metadata_success")?; 139 | assert_eq!( 140 | extract_status(&state.status), 141 | (Yellow, "Custom (yellow)".into(), 0) 142 | ); 143 | Ok(()) 144 | } 145 | 146 | /// Test if metadata is not set when the script fails. 147 | #[test] 148 | fn metadata_fail_test() -> Result<(), Box> { 149 | use StatusState::*; 150 | let state = run_test("metadata_fail")?; 151 | assert_eq!(extract_status(&state.status), (Red, "Failed".into(), 1)); 152 | Ok(()) 153 | } 154 | 155 | /// Tests if a complete group is correctly represented in the output. 156 | #[test] 157 | fn group_complete_test() -> Result<(), Box> { 158 | use StatusState::*; 159 | let state = run_test("group_complete")?; 160 | assert_eq!(extract_status(&state.status), (Green, "Success".into(), 0)); 161 | assert_eq!( 162 | extract_child_results(state), 163 | vec![ 164 | (Yellow, "Success".into(), 0), 165 | (Green, "Success".into(), 0), 166 | (Yellow, "Success".into(), 0), 167 | (Red, "Success".into(), 0) 168 | ] 169 | ); 170 | Ok(()) 171 | } 172 | 173 | /// Test whether the group adopts the parent script's results when the script failed. 174 | #[test] 175 | fn group_fail_test() -> Result<(), Box> { 176 | use StatusState::*; 177 | let state = run_test("group_fail")?; 178 | assert_eq!(extract_status(&state.status), (Red, "Failed".into(), 1)); 179 | assert_eq!( 180 | extract_child_results(state), 181 | vec![ 182 | (Red, "Failed".into(), 1), 183 | (Red, "Failed".into(), 1), 184 | (Red, "Failed".into(), 1), 185 | (Red, "Failed".into(), 1) 186 | ] 187 | ); 188 | Ok(()) 189 | } 190 | 191 | /// Tests whether the incomplete members of a group are correctly blanked out. 192 | #[test] 193 | fn group_incomplete_test() -> Result<(), Box> { 194 | use StatusState::*; 195 | let state = run_test("group_incomplete")?; 196 | assert_eq!(extract_status(&state.status), (Green, "Success".into(), 0)); 197 | assert_eq!( 198 | extract_child_results(state), 199 | vec![ 200 | (Yellow, "Success".into(), 0), 201 | (Green, "Success".into(), 0), 202 | (Yellow, "Success".into(), 0), 203 | (Blank, "".into(), 0) 204 | ] 205 | ); 206 | Ok(()) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/status.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, VecDeque}; 2 | use std::error::Error; 3 | use std::sync::Arc; 4 | 5 | use keepcalm::SharedMut; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::config::*; 9 | use crate::interpolate::interpolate_modify; 10 | use crate::worker::LogStream; 11 | use crate::worker::WorkerMessage; 12 | 13 | #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] 14 | #[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))] 15 | pub enum StatusState { 16 | Blank, 17 | Green, 18 | Yellow, 19 | Red, 20 | } 21 | 22 | #[derive(Clone, Debug, Serialize)] 23 | pub struct Status { 24 | pub config: Config, 25 | pub monitors: Vec>, 26 | } 27 | 28 | #[derive(Clone, Debug, Serialize, Deserialize)] 29 | pub struct MonitorState { 30 | pub id: String, 31 | pub config: MonitorDirTestConfig, 32 | #[serde(skip_serializing_if = "MonitorStatus::is_uninitialized")] 33 | pub status: MonitorStatus, 34 | #[serde(skip)] 35 | pub css: Option, 36 | pub children: BTreeMap, 37 | } 38 | 39 | #[derive(Clone, Debug, Serialize, Deserialize)] 40 | pub struct MonitorChildStatus { 41 | pub axes: BTreeMap, 42 | 43 | #[serde(skip_serializing_if = "MonitorStatus::is_uninitialized")] 44 | pub status: MonitorStatus, 45 | } 46 | 47 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 48 | pub struct MonitorStatus { 49 | pub status: Option, 50 | pub code: i64, 51 | pub description: String, 52 | pub css: MonitorCssStatus, 53 | pub metadata: BTreeMap, 54 | pub log: VecDeque, 55 | #[serde(skip)] 56 | pub pending: Option, 57 | } 58 | 59 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 60 | pub struct MonitorPendingStatus { 61 | pub status: Option, 62 | pub description: Option, 63 | pub metadata: Option>, 64 | } 65 | 66 | #[derive(Clone, Debug, Serialize, Deserialize)] 67 | pub struct MonitorCssStatus { 68 | pub metadata: Arc>, 69 | } 70 | 71 | impl Default for MonitorCssStatus { 72 | fn default() -> Self { 73 | Self { 74 | metadata: Arc::new(Default::default()), 75 | } 76 | } 77 | } 78 | 79 | impl MonitorState { 80 | /// Internal use only 81 | fn new_internal(id: String, config: MonitorDirTestConfig) -> Self { 82 | MonitorState { 83 | id, 84 | config, 85 | status: Default::default(), 86 | css: None, 87 | children: Default::default(), 88 | } 89 | } 90 | 91 | fn process_log_message ()>( 92 | &mut self, 93 | stream: &str, 94 | message: &str, 95 | direct_logger: &mut T, 96 | ) { 97 | let msg = format!("[{}] {}", stream, message); 98 | direct_logger(&msg); 99 | self.status.log.push_back(msg); 100 | } 101 | 102 | pub fn process_message ()>( 103 | &mut self, 104 | id: &str, 105 | msg: WorkerMessage, 106 | config: &CssMetadataConfig, 107 | direct_logger: &mut T, 108 | ) -> Result<(), Box> { 109 | debug!("[{}] Worker message {:?}", id, msg); 110 | match msg { 111 | WorkerMessage::Starting => { 112 | // Note that we don't update the state here 113 | self.status.pending = None; 114 | self.status.log.clear(); 115 | } 116 | WorkerMessage::LogMessage(stream, m) => { 117 | let stream = match stream { 118 | LogStream::StdOut => "stdout", 119 | LogStream::StdErr => "stderr", 120 | }; 121 | // TODO: Long lines without \n at the end should have some sort of other delimiter inserted 122 | self.process_log_message(stream, m.trim_end(), direct_logger); 123 | } 124 | WorkerMessage::Metadata(expr) => { 125 | // Make borrow checker happy 126 | let status = &mut self.status; 127 | let children = &mut self.children; 128 | if let Err(err) = interpolate_modify(status, children, &expr) { 129 | self.process_log_message("error ", &expr, direct_logger); 130 | self.process_log_message("error ", &err.to_string(), direct_logger); 131 | error!("Metadata update error: {}", err); 132 | } else { 133 | self.process_log_message("meta ", &expr.to_string(), direct_logger); 134 | } 135 | } 136 | WorkerMessage::AbnormalTermination(s) => { 137 | self.finish(StatusState::Yellow, -1, s, &config); 138 | } 139 | WorkerMessage::Termination(code) => { 140 | if code == 0 { 141 | self.finish(StatusState::Green, code, "Success".into(), &config); 142 | } else { 143 | self.finish(StatusState::Red, code, "Failed".into(), &config); 144 | } 145 | } 146 | } 147 | Ok(()) 148 | } 149 | 150 | fn finish( 151 | &mut self, 152 | status: StatusState, 153 | code: i64, 154 | description: String, 155 | config: &CssMetadataConfig, 156 | ) { 157 | self.css = None; 158 | 159 | for child in self.children.iter_mut() { 160 | let child_status = &mut child.1.status; 161 | if child_status.is_pending_status_set() || status != StatusState::Green { 162 | child_status.finish(status, code, description.clone(), &config); 163 | } else { 164 | child_status.finish(StatusState::Blank, code, "".into(), &config); 165 | } 166 | } 167 | 168 | self.status.finish(status, code, description, config); 169 | } 170 | } 171 | 172 | impl From<&MonitorDirConfig> for MonitorState { 173 | fn from(other: &MonitorDirConfig) -> Self { 174 | let mut state = MonitorState::new_internal(other.id.clone(), other.root.test().clone()); 175 | if let MonitorDirRootConfig::Group(ref group) = other.root { 176 | for child in group.children.iter() { 177 | state.children.insert( 178 | child.0.clone(), 179 | MonitorChildStatus { 180 | axes: child.1.axes.clone(), 181 | status: MonitorStatus::default(), 182 | }, 183 | ); 184 | } 185 | } 186 | state 187 | } 188 | } 189 | 190 | impl MonitorStatus { 191 | pub fn initialize(&mut self, config: &CssMetadataConfig) { 192 | self.description = "Unknown (initializing)".into(); 193 | self.status = Some(StatusState::Blank); 194 | self.css.metadata = config.blank.clone(); 195 | } 196 | 197 | pub fn is_pending_status_set(&self) -> bool { 198 | if let Some(ref pending) = self.pending { 199 | if pending.status.is_none() { 200 | return false; 201 | } 202 | } else { 203 | return false; 204 | } 205 | 206 | true 207 | } 208 | 209 | pub fn is_uninitialized(&self) -> bool { 210 | self.status.is_none() 211 | } 212 | 213 | fn finish( 214 | &mut self, 215 | status: StatusState, 216 | code: i64, 217 | description: String, 218 | config: &CssMetadataConfig, 219 | ) { 220 | let (pending_status, pending_description, pending_metadata) = self 221 | .pending 222 | .take() 223 | .map(|pending| (pending.status, pending.description, pending.metadata)) 224 | .unwrap_or_default(); 225 | self.code = code; 226 | 227 | // Start with the regular update 228 | self.status = Some(status); 229 | self.description = description; 230 | self.metadata.clear(); 231 | 232 | // Metadata/status can only be overwritten if the process terminated normally 233 | if status == StatusState::Green { 234 | if let Some(metadata) = pending_metadata { 235 | self.metadata = metadata; 236 | } 237 | if let Some(status) = pending_status { 238 | self.status = Some(status); 239 | } 240 | if let Some(description) = pending_description { 241 | self.description = description; 242 | } 243 | } 244 | 245 | // Update the CSS metadata with the final status 246 | if let Some(status) = self.status { 247 | self.css.metadata = match status { 248 | StatusState::Blank => config.blank.clone(), 249 | StatusState::Green => config.green.clone(), 250 | StatusState::Yellow => config.yellow.clone(), 251 | StatusState::Red => config.red.clone(), 252 | }; 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/testcases/group_complete/config.yaml: -------------------------------------------------------------------------------- 1 | group: 2 | id: port-{{ index }} 3 | axes: 4 | - name: index 5 | values: [0, 1, 2, 3] 6 | test: 7 | interval: 60s 8 | timeout: 30s 9 | command: test.sh 10 | -------------------------------------------------------------------------------- /src/testcases/group_complete/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ group.port-0.status.status="yellow"' 4 | echo '@@STYLUS@@ group.port-1.status.status="green"' 5 | echo '@@STYLUS@@ group.port-2.status.status="yellow"' 6 | echo '@@STYLUS@@ group.port-3.status.status="red"' 7 | -------------------------------------------------------------------------------- /src/testcases/group_fail/config.yaml: -------------------------------------------------------------------------------- 1 | group: 2 | id: port-{{ index }} 3 | axes: 4 | - name: index 5 | values: [0, 1, 2, 3] 6 | test: 7 | interval: 60s 8 | timeout: 30s 9 | command: test.sh 10 | -------------------------------------------------------------------------------- /src/testcases/group_fail/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ group.port-0.status.status="yellow"' 4 | echo '@@STYLUS@@ group.port-1.status.status="green"' 5 | echo '@@STYLUS@@ group.port-2.status.status="yellow"' 6 | exit 1 7 | -------------------------------------------------------------------------------- /src/testcases/group_incomplete/config.yaml: -------------------------------------------------------------------------------- 1 | group: 2 | id: port-{{ index }} 3 | axes: 4 | - name: index 5 | values: [0, 1, 2, 3] 6 | test: 7 | interval: 60s 8 | timeout: 30s 9 | command: test.sh 10 | -------------------------------------------------------------------------------- /src/testcases/group_incomplete/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ group.port-0.status.status="yellow"' 4 | echo '@@STYLUS@@ group.port-1.status.status="green"' 5 | echo '@@STYLUS@@ group.port-2.status.status="yellow"' 6 | # Missing port-3! -------------------------------------------------------------------------------- /src/testcases/metadata_fail/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /src/testcases/metadata_fail/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ status.description="Custom (yellow)"' 4 | echo '@@STYLUS@@ status.status="yellow"' 5 | echo '@@STYLUS@@ status.metadata.key="value1"' 6 | exit 1 -------------------------------------------------------------------------------- /src/testcases/metadata_success/config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | interval: 60s 3 | timeout: 30s 4 | command: test.sh 5 | -------------------------------------------------------------------------------- /src/testcases/metadata_success/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuf -o pipefail 3 | echo '@@STYLUS@@ status.description="Custom (yellow)"' 4 | echo '@@STYLUS@@ status.status="yellow"' 5 | echo '@@STYLUS@@ status.metadata.key="value1"' 6 | -------------------------------------------------------------------------------- /src/testcases/v1.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | server: 3 | port: 8000 4 | static: /tmp/ 5 | 6 | monitor: 7 | dir: /tmp/ 8 | 9 | css: 10 | # Use metadata to get prettier colors - note that we can add arbitrary string keys and values here 11 | metadata: 12 | red: 13 | color: "#fa897b" 14 | yellow: 15 | color: "#ffdd94" 16 | green: 17 | color: "#d0e6a5" 18 | rules: 19 | # Style the HTML/SVG with the appropriate status color 20 | - selectors: " 21 | #{{monitor.id}}, 22 | [data-monitor-id=\"{{monitor.id}}\"] > * 23 | " 24 | declarations: " 25 | background-color: {{monitor.status.css.metadata.color}} !important; 26 | fill: {{monitor.status.css.metadata.color}} !important; 27 | " 28 | # Add some text for the status/return value of the script 29 | - selectors: " 30 | #{{monitor.id}} td:nth-child(2)::after 31 | " 32 | declarations: " 33 | content: \"status={{monitor.status.status}} retval={{monitor.status.code}}\" 34 | " 35 | # Add the full description to the table 36 | - selectors: " 37 | #{{monitor.id}} td:nth-child(3)::after 38 | " 39 | declarations: " 40 | content: \"{{monitor.status.description}}\" 41 | " -------------------------------------------------------------------------------- /src/worker/linebuf.rs: -------------------------------------------------------------------------------- 1 | pub struct LineBuf { 2 | buf: Vec, 3 | buffer_size: usize, 4 | pending_cr: bool, 5 | } 6 | 7 | const LF: u8 = b'\n'; 8 | const CR: u8 = b'\r'; 9 | 10 | impl LineBuf { 11 | pub fn new(buffer_size: usize) -> Self { 12 | let buf = Vec::with_capacity(buffer_size); 13 | LineBuf { 14 | buf, 15 | buffer_size, 16 | pending_cr: false, 17 | } 18 | } 19 | 20 | pub fn accept(&mut self, bytes: &[u8], accept_lines: &mut T) { 21 | debug_assert!(self.buf.capacity() == self.buffer_size); 22 | for b in bytes.iter() { 23 | if *b == CR { 24 | self.pending_cr = true; 25 | } else if *b == LF { 26 | if self.pending_cr { 27 | // Ignore CR+LF 28 | self.pending_cr = false; 29 | } 30 | let mut out = self.buf.split_off(0); 31 | out.push(LF); 32 | (accept_lines)(String::from_utf8_lossy(&out).into()); 33 | } else { 34 | if self.pending_cr { 35 | self.buf.clear(); 36 | self.pending_cr = false; 37 | } 38 | if self.buf.len() == self.buf.capacity() { 39 | (accept_lines)(String::from_utf8_lossy(&self.buf).into()); 40 | self.buf.clear(); 41 | } 42 | self.buf.push(*b); 43 | } 44 | } 45 | debug_assert!(self.buf.capacity() == self.buffer_size); 46 | } 47 | 48 | pub fn close(self, accept_lines: &mut T) { 49 | if self.pending_cr { 50 | return; 51 | } 52 | if !self.buf.is_empty() { 53 | (accept_lines)(String::from_utf8_lossy(&self.buf).into()); 54 | } 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | pub mod tests { 60 | use super::*; 61 | 62 | fn test(s: &str, expected: Vec<&str>) { 63 | let mut lines = Vec::new(); 64 | let mut buf = LineBuf::new(40); 65 | buf.accept(&s.bytes().collect::>(), &mut |line| lines.push(line)); 66 | buf.close(&mut |line| lines.push(line)); 67 | assert_eq!(lines, expected); 68 | } 69 | 70 | #[test] 71 | fn test_short_lines() { 72 | test( 73 | "hello world\nbye world\n", 74 | vec!["hello world\n", "bye world\n"], 75 | ); 76 | } 77 | 78 | #[test] 79 | fn test_long_lines() { 80 | test( 81 | "01234567890123456789012345678901234567890123456789012345678901234567890123456789", 82 | vec![ 83 | "0123456789012345678901234567890123456789", 84 | "0123456789012345678901234567890123456789", 85 | ], 86 | ); 87 | test( 88 | "012345678901234567890123456789012345678901234567890123456789012345678901234567890", 89 | vec![ 90 | "0123456789012345678901234567890123456789", 91 | "0123456789012345678901234567890123456789", 92 | "0", 93 | ], 94 | ); 95 | test( 96 | "0123456789012345678901234567890123456789\n0123456789012345678901234567890123456789", 97 | vec![ 98 | "0123456789012345678901234567890123456789\n", 99 | "0123456789012345678901234567890123456789", 100 | ], 101 | ); 102 | test( 103 | "0123456789012345678901234567890123456789\n0123456789012345678901234567890123456789\n", 104 | vec![ 105 | "0123456789012345678901234567890123456789\n", 106 | "0123456789012345678901234567890123456789\n", 107 | ], 108 | ); 109 | test( 110 | "01234567890123456789012345678901234567890123456789012345678901234567890123456789\n", 111 | vec![ 112 | "0123456789012345678901234567890123456789", 113 | "0123456789012345678901234567890123456789\n", 114 | ], 115 | ); 116 | } 117 | 118 | #[test] 119 | fn test_empty() { 120 | test("", vec![]); 121 | } 122 | 123 | #[test] 124 | fn test_no_lf() { 125 | test("hello world", vec!["hello world"]); 126 | } 127 | 128 | #[test] 129 | fn test_cr_lf() { 130 | // DOS-style CR+LF 131 | test("hello world\r\n", vec!["hello world\n"]); 132 | test( 133 | "hello world\r\nbye world", 134 | vec!["hello world\n", "bye world"], 135 | ); 136 | test( 137 | "hello world\r\nbye world\r\n", 138 | vec!["hello world\n", "bye world\n"], 139 | ); 140 | } 141 | 142 | #[test] 143 | fn test_cr() { 144 | // cURL-style CR 145 | test("test1\rtest2\rtest3\n", vec!["test3\n"]); 146 | test("test1\rtest2\rtest3", vec!["test3"]); 147 | test("test1\rtest2\rtest3\r", vec![]); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/worker/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::ffi::{OsStr, OsString}; 3 | use std::path::Path; 4 | use std::sync::atomic::{AtomicBool, Ordering}; 5 | use std::thread; 6 | use std::time::{Duration, Instant}; 7 | 8 | use subprocess::{Exec, ExitStatus, Popen, Redirection}; 9 | 10 | use self::linebuf::LineBuf; 11 | use crate::config::*; 12 | 13 | mod linebuf; 14 | 15 | #[derive(Debug, Default, Display, Error)] 16 | pub struct ShuttingDown {} 17 | 18 | #[derive(Debug)] 19 | pub enum LogStream { 20 | StdOut, 21 | StdErr, 22 | } 23 | 24 | #[derive(Debug)] 25 | pub enum WorkerMessage { 26 | Starting, 27 | LogMessage(LogStream, String), 28 | Metadata(String), 29 | Termination(i64), 30 | AbnormalTermination(String), 31 | } 32 | 33 | pub fn monitor_thread Result<(), Box>>( 34 | monitor: MonitorDirConfig, 35 | mut sender: T, 36 | ) { 37 | loop { 38 | let (interval, res) = monitor_run(&monitor, &mut sender); 39 | if let Err(err) = res { 40 | // Break the loop on a task failure (but don't log ShuttingDown errors) 41 | if err.downcast_ref::().is_none() { 42 | error!("[{}] Task failure: {}", monitor.id, err); 43 | } 44 | if sender( 45 | &monitor.id, 46 | WorkerMessage::AbnormalTermination(err.to_string()), 47 | ) 48 | .is_err() 49 | { 50 | return; 51 | } 52 | } 53 | 54 | trace!("[{}] Sleeping {}ms", monitor.id, interval.as_millis()); 55 | thread::sleep(interval); 56 | } 57 | } 58 | 59 | pub fn monitor_run Result<(), Box>>( 60 | monitor: &MonitorDirConfig, 61 | sender: &mut T, 62 | ) -> (Duration, Result<(), Box>) { 63 | let args: Option<&[OsString]> = None; 64 | let test = monitor.root.test(); 65 | ( 66 | test.interval, 67 | monitor_thread_impl( 68 | &monitor.id, 69 | &test.command, 70 | &monitor.base_path, 71 | args, 72 | test.timeout, 73 | sender, 74 | ), 75 | ) 76 | } 77 | 78 | fn append( 79 | id: &str, 80 | f: &mut T, 81 | out: &mut Option>, 82 | stdout: &mut LineBuf, 83 | err: &mut Option>, 84 | stderr: &mut LineBuf, 85 | ) -> bool { 86 | if let (Some(out), Some(err)) = (out, err) { 87 | // Termination condition: "until read() returns all-empty data, which marks EOF." 88 | let done = out.is_empty() && err.is_empty(); 89 | if !done { 90 | // This is pretty noisy, so only trace if we have data 91 | trace!("[{}] read out={} err={}", id, out.len(), err.len()); 92 | } 93 | stdout.accept(out, &mut |s| f(LogStream::StdOut, s)); 94 | stderr.accept(err, &mut |s| f(LogStream::StdErr, s)); 95 | done 96 | } else { 97 | error!("[{}] null reader?", id); 98 | debug_assert!(false, "Unexpectedly null reads"); 99 | false 100 | } 101 | } 102 | 103 | enum DeathResult { 104 | ExitStatus(ExitStatus), 105 | Wedged(Popen), 106 | } 107 | 108 | fn aggressively_wait_for_death(id: &str, mut popen: Popen, duration: Duration) -> DeathResult { 109 | let r = popen.wait_timeout(duration); 110 | if let Ok(Some(status)) = r { 111 | // Easy, status was available right await 112 | debug!("[{}] Normal exit: {:?}", id, status); 113 | return DeathResult::ExitStatus(status); 114 | } 115 | 116 | // If we didn't get a result OR there was an error, let's try to terminate the process, ignoring any errors 117 | info!("[{}] Terminating process...", id); 118 | let _ = popen.terminate(); 119 | 120 | // Now give it 5 seconds to exit for good 121 | let r = popen.wait_timeout(Duration::from_millis(5000)); 122 | if let Ok(Some(_)) = r { 123 | // Always return the signal 124 | return DeathResult::ExitStatus(ExitStatus::Signaled(1)); 125 | } 126 | 127 | // Kill with prejudice 128 | info!("[{}] Killing process...", id); 129 | let _ = popen.kill(); 130 | 131 | // Give it another 5 seconds 132 | let r = popen.wait_timeout(Duration::from_millis(5000)); 133 | if let Ok(Some(_)) = r { 134 | // Always return the signal 135 | return DeathResult::ExitStatus(ExitStatus::Signaled(9)); 136 | } 137 | 138 | // This process is probably wedged and will become a zombie 139 | error!("[{}] Process wedged, bad things may happen", id); 140 | DeathResult::Wedged(popen) 141 | } 142 | 143 | fn process_log_message Result<(), Box>>( 144 | id: &str, 145 | failed: &AtomicBool, 146 | stream: LogStream, 147 | s: String, 148 | sender: &mut T, 149 | ) { 150 | const META_PREFIX: &str = "@@STYLUS@@"; 151 | 152 | let msg = if s.starts_with(META_PREFIX) { 153 | let s = s.split_at(META_PREFIX.len()).1; 154 | WorkerMessage::Metadata(s.trim().to_owned()) 155 | } else { 156 | WorkerMessage::LogMessage(stream, s) 157 | }; 158 | 159 | if sender(id, msg).is_err() { 160 | failed.store(true, Ordering::SeqCst) 161 | } 162 | } 163 | 164 | fn monitor_thread_impl Result<(), Box>>( 165 | id: &str, 166 | cmd: &Path, 167 | base_path: &Path, 168 | args: Option<&[impl AsRef]>, 169 | timeout: Duration, 170 | sender: &mut T, 171 | ) -> Result<(), Box> { 172 | // This will fail if we're supposed to shut down 173 | sender(id, WorkerMessage::Starting)?; 174 | 175 | debug!("[{}] Starting {:?}", id, cmd); 176 | 177 | let mut exec = Exec::cmd(cmd) 178 | .cwd(base_path) 179 | .env("STYLUS_MONITOR_ID", id) 180 | .stdout(Redirection::Pipe) 181 | .stderr(Redirection::Pipe); 182 | if let Some(args) = args { 183 | exec = exec.args(args); 184 | } 185 | let mut popen = exec.popen()?; 186 | 187 | let failed = AtomicBool::new(false); 188 | let mut f = |stream, s| { 189 | process_log_message(id, &failed, stream, s, sender); 190 | }; 191 | let mut stdout = LineBuf::new(80); 192 | let mut stderr = LineBuf::new(80); 193 | 194 | let start = Instant::now(); 195 | let mut comms = popen 196 | .communicate_start(None) 197 | .limit_time(Duration::from_millis(250)); 198 | 199 | while start.elapsed() < timeout { 200 | let mut r = comms.read(); 201 | if let Err(ref mut err) = r { 202 | if err.error.kind() == std::io::ErrorKind::TimedOut { 203 | if append( 204 | id, 205 | &mut f, 206 | &mut err.capture.0, 207 | &mut stdout, 208 | &mut err.capture.1, 209 | &mut stderr, 210 | ) { 211 | // We *might* have a completed process: need to check whether the return value is available or not 212 | if popen.poll().is_some() { 213 | debug!("[{}] Early completion", id); 214 | break; 215 | } 216 | } 217 | continue; 218 | } 219 | } 220 | let mut r = r?; 221 | if append(id, &mut f, &mut r.0, &mut stdout, &mut r.1, &mut stderr) { 222 | break; 223 | } 224 | } 225 | 226 | stdout.close(&mut |s| f(LogStream::StdOut, s)); 227 | stderr.close(&mut |s| f(LogStream::StdErr, s)); 228 | 229 | debug!("[{}] Finished read, waiting for status...", id); 230 | 231 | // Give the process reaper at least 250ms to get the exit code (or longer if the test timeout is still not elapsed) 232 | let timeout = Duration::max( 233 | Duration::from_millis(250), 234 | timeout 235 | .checked_sub(start.elapsed()) 236 | .unwrap_or(Duration::from_secs(0)), 237 | ); 238 | match aggressively_wait_for_death(id, popen, timeout) { 239 | DeathResult::ExitStatus(ExitStatus::Exited(code)) => { 240 | sender(id, WorkerMessage::Termination(code as i64))?; 241 | } 242 | DeathResult::ExitStatus(ExitStatus::Signaled(code)) => { 243 | sender( 244 | id, 245 | WorkerMessage::AbnormalTermination(format!("Process exited with signal {}", code)), 246 | )?; 247 | } 248 | DeathResult::ExitStatus(ExitStatus::Other(code)) => { 249 | sender( 250 | id, 251 | WorkerMessage::AbnormalTermination(format!( 252 | "Process exited for unknown reason {:x}", 253 | code 254 | )), 255 | )?; 256 | } 257 | DeathResult::ExitStatus(ExitStatus::Undetermined) => { 258 | sender( 259 | id, 260 | WorkerMessage::AbnormalTermination("Process exited for unknown reason".into()), 261 | )?; 262 | } 263 | DeathResult::Wedged(mut popen) => { 264 | sender( 265 | id, 266 | WorkerMessage::AbnormalTermination("Process timed out".into()), 267 | )?; 268 | // We can wait here after we notify the monitor system 269 | let _ = popen.wait(); 270 | } 271 | } 272 | 273 | Ok(()) 274 | } 275 | 276 | #[cfg(test)] 277 | mod tests { 278 | use super::*; 279 | use std::sync::mpsc::*; 280 | 281 | #[test] 282 | fn test_timeout() { 283 | let (tx, rx) = channel(); 284 | monitor_thread_impl( 285 | &"test".to_owned(), 286 | Path::new("/bin/sleep"), 287 | Path::new("/tmp"), 288 | Some(&["10"]), 289 | Duration::from_millis(250), 290 | &mut |_, m| { 291 | tx.send(m)?; 292 | Ok(()) 293 | }, 294 | ) 295 | .expect("Failed to monitor"); 296 | drop(tx); 297 | loop { 298 | if let Ok(msg) = rx.recv() { 299 | if let WorkerMessage::AbnormalTermination(_) = msg { 300 | return; 301 | } 302 | } else { 303 | panic!("Never got the abnormal termination error") 304 | } 305 | } 306 | } 307 | } 308 | --------------------------------------------------------------------------------