diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66ecb85..7b5588e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,18 +105,14 @@ jobs: run: rustup toolchain install 1.85 --profile minimal - # NOTE: no `--locked`, and `Cargo.lock` is not tracked. Three things cannot all be true at once: - # 1. a committed lockfile must record the pinned GIT source, or it lies to a fresh clone; - # 2. local dev builds through the gitignored `.cargo/config.toml` `[patch]`, and a patched - # build REWRITES the lockfile to a PATH source; - # 3. simulators/run-rtsp-native-coverage.ps1 mounts the workspace READ-ONLY, so a git-sourced - # lock plus the patch means cargo must rewrite it and cannot -- the build dies outright. - # Tracking the lockfile satisfies (1) and breaks (2) and (3); a patched lockfile satisfies (2) - # and (3) and lies about (1). `file-replicator` resolves this by not tracking it. So do we. + # `--locked` against the committed `Cargo.lock`, which records the pinned GIT source (it is + # regenerated on a fresh clone with the local `.cargo` `[patch]` inactive, so it never carries a + # path override, and the MSRV-aware resolver held every dependency to a 1.85-compatible version). + # CI has no `.cargo` override, so the lock resolves the pinned rev reproducibly. - name: Check on the declared MSRV env: CARGO_NET_GIT_FETCH_WITH_CLI: "true" - run: cargo +1.85 check --all-targets + run: cargo +1.85 check --locked --all-targets rtsp-backend: # The `rtsp` (GStreamer) backend was compiled by NOTHING. The jobs above build default features @@ -178,25 +174,21 @@ jobs: image: # The Dockerfile was built by NOTHING. Not by CI, not by any script -- the only automated docker - # builds in this repo are the simulator validation images, which are different files. So when - # 3c0d83d stopped tracking `Cargo.lock`, the `COPY camera-adapter/Cargo.lock` on line 35 and the - # two `cargo build --locked` invocations became a build that CANNOT SUCCEED in a clean clone -- - # `"/camera-adapter/Cargo.lock": not found` -- and it stayed that way, because a developer with a - # stale lock still lying around in their working tree never saw it. The shipped container image - # was unbuildable by anyone who cloned the repo, and nothing said a word. + # builds in this repo are the simulator validation images, which are different files. This job + # builds the shipped image so it cannot silently rot. # - # This job builds it. Both targets, from a clean checkout, and it runs the binary it produced -- - # compiling is not the same as producing a working image, and the RTSP target exists precisely to - # carry a GStreamer runtime the ONVIF one does not. + # It builds both targets from a clean checkout `--locked` against the committed `Cargo.lock` (which + # records the pinned git source; the image context carries no `.cargo` override) and runs the + # binary it produced -- compiling is not the same as producing a working image, and the RTSP target + # exists precisely to carry a GStreamer runtime the ONVIF one does not. runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 - # A clean checkout has no `Cargo.lock` (it is untracked -- see the note in the `msrv` job). If a - # future change reintroduces `--locked` or copies the lock, the build dies here, which is the - # entire point of this job. + # A clean checkout carries the committed `Cargo.lock`; the Dockerfile copies it and builds + # `--locked` for a reproducible image. This job proves the image builds from a clean clone. - name: Build the default image (onvif) run: docker build --target onvif -t camera-adapter:onvif . diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..4f44451 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,25 @@ +name: deploy-docs + +# Doc-only pushes skip the build CI (see paths-ignore in ci.yml) but should still refresh the +# EdgeCommons docs site, which aggregates this repo's docs/ at build time. This hits the Cloudflare +# Workers Builds deploy hook for the edgecommons-docs project to trigger a rebuild (a few seconds of +# Actions, not the full build). Enable by setting the repo Actions secret CLOUDFLARE_DEPLOY_HOOK to +# the deploy-hook URL; without it this is a no-op (docs rebuild on the next edgecommons main push). +on: + push: + branches: [main] + paths: ['docs/**', '**.md'] + +jobs: + trigger-docs-rebuild: + runs-on: ubuntu-latest + steps: + - name: Trigger Cloudflare docs rebuild + env: + HOOK: ${{ secrets.CLOUDFLARE_DEPLOY_HOOK }} + run: | + if [ -n "$HOOK" ]; then + curl -fsS -X POST "$HOOK" && echo "triggered edgecommons-docs rebuild" + else + echo "CLOUDFLARE_DEPLOY_HOOK not set; skipping (docs rebuild on the next edgecommons main push)" + fi diff --git a/.gitignore b/.gitignore index 962d886..39aa449 100644 --- a/.gitignore +++ b/.gitignore @@ -12,11 +12,9 @@ simulators/generated/ __pycache__/ *.py[cod] -# Cargo.lock is NOT tracked. See .github/workflows/ci.yml — a local build through the gitignored -# .cargo/config.toml [patch] rewrites it to a path source, and the RTSP validation container mounts -# the workspace read-only, so a git-sourced lock cannot be rewritten there either. The three cannot -# all be true. file-replicator does the same. -/Cargo.lock +# Cargo.lock IS committed (git-sourced; regenerate with the .cargo [patch] inactive so it records the +# pinned git rev, not a path source). A local build through the gitignored .cargo/config.toml [patch] +# rewrites it to a path source — do not commit that churn. # The ONVIF simulator TLS material is MINTED, never committed: simulators/generate-tls-fixtures.{sh,ps1}. # A private key in a public repository is a private key in a public repository, however worthless. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..18ffc7a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# camera-adapter — component notes + +EdgeCommons **southbound camera adapter** (Rust). Crate/binary `camera-adapter`, GG component +`com.mbreissi.edgecommons.CameraAdapter`. Depends on the `edgecommons` Rust library. If this repo +lives inside the EdgeCommons org umbrella workspace, read its root `AGENTS.md` first (org repo map, +design-fidelity contract, validation matrix, platform/transport model); everything below is this +component's own detail. + +## What it is + +Connects to cameras (ONVIF/RTSP, bare RTSP, GenICam/Aravis, plus an in-process simulator), captures +still images on demand and on schedules, and publishes capture announcements onto the Unified +Namespace. It is a **camera**, not a signal adapter: a data point is an image on disk, not a +`SouthboundSignalUpdate`. The image bytes are the data plane (files, delivered by `file-replicator`); +the bus carries control and terminal metadata (`app/image/*` announcements, `evt` operator events). + +It serves the canonical `southbound_health` metric plus operational families (`camera_captures`, +`camera_queue`, `CameraCommand`), the standardized lifecycle verbs `sb/pause` / `sb/resume` / +`sb/reconnect`, and 16 domain `sb/*` verbs (`sb/capture` and friends, `sb/ptz*`, queue verbs) on the +D-U28 component command inbox — SOUTHBOUND.md §2.2 sanctions `sb/capture`-style domain verbs. Runs on +`GREENGRASS` / `HOST` / `KUBERNETES` via `edgecommons`, with no platform branching in this component. + +## The seam + +`src/backend/`'s `CameraBackendFactory` / `CameraSession` trait pair is the one place protocol +knowledge lives; the `SimBackend` implementation is compiled into every build and drives the +deterministic test suite. Everything above the seam — the durable job catalog (`src/catalog.rs`), the +fleet capture scheduler (`src/dispatch.rs`), the command plane (`src/runtime/command.rs`), the metric +families (`src/observability.rs`) — is written against the trait and does not change when a protocol +is added. The boundary rule: a backend knows protocols; it does not know EdgeCommons topics, the UNS, +envelopes, or metrics. + +## Config location + +This component's settings live under `component.global` / `component.instances[]` in the EdgeCommons +config document (`config.schema.json` is the contract; `src/config.rs` is the parser); the sibling +sections (`tags`, `hierarchy`, `identity`, `messaging`, `metricEmission`, `logging`, `heartbeat`) are +the standard `edgecommons` envelope, owned by the canonical schema and not redeclared here. +`test-configs/` and `docs/sample-configurations.md` carry runnable examples. + +## Validation expectations + +- `cargo test` covers every module against the simulator and a mocked device-control channel — no + network, no broker, no camera required. +- The coverage gate is **90% line + a 95% diff gate** (`.github/workflows/ci.yml`'s `coverage` job): + `cargo llvm-cov report --summary-only --fail-under-lines 90` plus a `diff-cover --fail-under=95` on + the changed lines. Do not lower a gate or exclude testable code to pass it — add tests. +- The `rtsp` (GStreamer) and `genicam` (Aravis) backends need system libraries; they are compiled and + exercised in the simulator-backed containers (`simulators/rtsp_validation.Dockerfile`, + `simulators/native_all_validation.Dockerfile`) and the `rtsp-backend` CI job, not the default build. + Their live-fixture suites are `#[ignore]`d and run against MediaMTX / real devices. +- `Cargo.lock` **is committed** (git-sourced), so a fresh clone and CI resolve reproducibly. +- `edgecommons component validate` checks this repo's config against `config.schema.json`. + +## Org conventions this component inherits + +- Southbound routing/availability error codes are the standardized `BAD_ARGS` / `NO_SUCH_INSTANCE` / + `DEVICE_UNAVAILABLE` (SOUTHBOUND.md §2.2); domain codes (`CAPTURE_*`, `PTZ_*`, …) are camera-specific. +- Instance routing is D-EIP-13/D-U28: body `instance`, optional iff exactly one camera is configured. +- Builders/facades are the construction path (`app()`, `events()`, `commands()`, `MetricBuilder`) — + never hand-built topics or envelopes. +- Runtime artifacts (durable state DBs, captured images, TLS fixtures, logs, build output) stay out of + Git. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..08c1916 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# camera-adapter (Claude Code) + +EdgeCommons southbound camera adapter (Rust), `com.mbreissi.edgecommons.CameraAdapter`. The full +picture — what this component is, the backend seam, config location, and the org conventions it +inherits — lives in `AGENTS.md` and is shared with every agent tool. It is imported here in full: + +@AGENTS.md + +## Local-dev notes + +- **Sibling library override.** `Cargo.toml` pins the `edgecommons` dependency by git `rev`. For local + development a gitignored `.cargo/config.toml` `[patch]` block redirects that pin at your sibling + `core/libs/rust` checkout, so a plain `cargo build` tracks your working copy without touching the + committed pin CI uses. `Cargo.lock` is committed and records the git source; a `[patch]`-ed build + rewrites it to a path source locally — do not commit that churn. +- **Simulators.** `SimBackend` is compiled into every build and drives the deterministic suite with no + hardware. The ONVIF/RTSP/GenICam simulators under `simulators/` back the live validation containers; + `simulators/run-rtsp-native-coverage.ps1` runs the RTSP native-coverage harness (run it with the + `.cargo` `[patch]` inactive so cargo never needs to rewrite the read-only-mounted committed lock). +- **Feature builds.** Default features are `standalone,onvif` (pure Rust, Windows-buildable). `rtsp` + needs GStreamer and `genicam` needs Aravis ≥ 0.8.36 — build those in the simulator containers or WSL. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dd52417 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4630 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "aravis" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ab70a6f0e178b4331dfbce9572d3c6303b0a847edb9612d74c0735b1ccdad4" +dependencies = [ + "aravis-sys", + "gio", + "gio-sys 0.20.10", + "glib 0.20.12", + "glib-sys 0.20.10", + "gobject-sys 0.20.10", + "rustc_version", +] + +[[package]] +name = "aravis-scoped" +version = "0.1.0" +dependencies = [ + "glib-sys 0.20.10", + "pkg-config", +] + +[[package]] +name = "aravis-sys" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3840c4d1b935f3e5a2612e791d24a894b12125023b4ef34a40309af06a2c71" +dependencies = [ + "gio-sys 0.20.10", + "glib-sys 0.20.10", + "gobject-sys 0.20.10", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic_refcell" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-greengrass-component-sdk" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27784b53a82e3063993a5825e2c317624b6aa92e1cd5b0fbafb1bfcab9e59e2f" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camera-adapter" +version = "0.1.0" +dependencies = [ + "anyhow", + "aravis", + "aravis-scoped", + "async-trait", + "base64", + "bytes", + "cap-fs-ext", + "cap-std", + "chrono", + "chrono-tz", + "croner", + "edgecommons", + "fs4", + "futures", + "gstreamer", + "gstreamer-app", + "hex", + "image", + "ipnet", + "known-folders", + "md-5", + "network-interface", + "png", + "proptest", + "quick-xml", + "rand 0.8.7", + "reqwest", + "rusqlite", + "rustix", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "sha1", + "sha2", + "tempfile", + "thiserror", + "tiff", + "time", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "unicode-general-category", + "url", + "uuid", + "windows-permissions", + "zeroize", +] + +[[package]] +name = "cap-fs-ext" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78e5a3368ae89b7cb68186411452b4b9fac8b41be9c19bf3f47c2d2c8e36e6b" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes 3.0.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "cap-primitives" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-std" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 3.0.1", + "rustix", +] + +[[package]] +name = "cc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", + "serde", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "croner" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa42bcd3d846ebf66e15bd528d1087f75d1c6c1c66ebff626178a106353c576" +dependencies = [ + "chrono", + "derive_builder", + "strum", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "edgecommons" +version = "0.2.0" +source = "git+https://github.com/edgecommons/edgecommons.git?rev=36a70c48b65b35f77bfab70d3a73869debdfc407#36a70c48b65b35f77bfab70d3a73869debdfc407" +dependencies = [ + "aes-gcm", + "arc-swap", + "async-trait", + "aws-greengrass-component-sdk", + "base64", + "clap", + "fs4", + "getrandom 0.2.17", + "hkdf", + "hmac", + "jsonschema", + "notify", + "prost", + "prost-build", + "protoc-bin-vendored", + "rand 0.8.7", + "regex", + "rumqttc", + "serde", + "serde_json", + "sha2", + "sysinfo", + "thiserror", + "time", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "windows-sys 0.61.2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fancy-regex" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gio" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys 0.20.10", + "glib 0.20.12", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys 0.20.10", + "gobject-sys 0.20.10", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + +[[package]] +name = "gio-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" +dependencies = [ + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "libc", + "system-deps", + "windows-sys 0.61.2", +] + +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.20.10", + "glib-macros 0.20.12", + "glib-sys 0.20.10", + "gobject-sys 0.20.10", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.21.5", + "glib-macros 0.21.5", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-macros" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glib-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys 0.20.10", + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" +dependencies = [ + "glib-sys 0.21.5", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bed73742c5d54cb48533be608b67d89f96e1ebbba280be7823f1ef995e3a9d7" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib 0.21.5", + "gstreamer-sys", + "itertools 0.14.0", + "kstring", + "libc", + "muldiv", + "num-integer", + "num-rational", + "option-operations", + "pastey", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gstreamer-app" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0af5d403738faf03494dfd502d223444b4b44feb997ba28ab3f118ee6d40a0b2" +dependencies = [ + "futures-core", + "futures-sink", + "glib 0.21.5", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "libc", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa9f1b12b546aea543c15a0fdbc5a53617902a74f6d357a32b6a9fb4bc4725c" +dependencies = [ + "glib-sys 0.21.5", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-base" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9375f9a12120a8ee17b765c816c9b23861ce258def77b0ee40a05acb00c74972" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib 0.21.5", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b844f3559b6ab0379b4b771261643783ae4e0ffa71d5f5f46e33b7acf66b752" +dependencies = [ + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-sys" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d37c1a599ae57b8186948bd5699f2dbfc044baea9d400228b489a85bcf2759" +dependencies = [ + "cfg-if", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "libc", + "system-deps", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inotify" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +dependencies = [ + "bitflags 2.13.1", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05" +dependencies = [ + "ahash", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna", + "itoa", + "jsonschema-regex", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "reqwest", + "rustls", + "serde", + "serde_json", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "known-folders" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "network-interface" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" +dependencies = [ + "cc", + "libc", + "thiserror", + "winapi", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.13.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-operations" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aca39cf52b03268400c16eeb9b56382ea3c3353409309b63f5c8f0b1faf42754" +dependencies = [ + "pastey", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "referencing" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rumqttc" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0feff8d882bff0b2fddaf99355a10336d43dd3ed44204f85ece28cf9626ab519" +dependencies = [ + "bytes", + "fixedbitset", + "flume", + "futures-util", + "log", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.1", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "serde_json", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5edbec4ed188954a10c12c038215f8ce7606b2d5c973cd8dc43e8795065c5f2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sysinfo" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-permissions" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2ccdc3c6bf4d4a094e031b63fadd08d8e42abd259940eb8aa5fdc09d4bf9be" +dependencies = [ + "bitflags 1.3.2", + "winapi", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.13.1", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.1", +] diff --git a/Cargo.toml b/Cargo.toml index 91eb6c6..e25b334 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "camera-adapter" version = "0.1.0" edition = "2024" rust-version = "1.85" -license = "Apache-2.0" +license = "BUSL-1.1" description = "EdgeCommons southbound adapter for GenICam and ONVIF cameras" publish = false diff --git a/DESIGN.md b/DESIGN.md index 6a10a63..f0fc464 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -139,6 +139,12 @@ The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are no | D-CAM-19 | Outbox acknowledgement | Withdrawn | There is no outbox and no acknowledgement to wait for. Terminal announcements publish once, best effort. Durable, acknowledged delivery is a generic messaging concern and belongs in the EdgeCommons messaging service as an opt-in augmentation across all four languages, available to any component, rather than being reimplemented inside one. | | D-CAM-20 | Group capture | `sb/capture-group` fans one request out as independent per-camera capture jobs sharing an adapter-generated `captureGroupId`; the single deferred reply aggregates every member's terminal result | One operator action often needs an evidence set from several cameras. Aggregation fits the shipped single-reply command model plus the D-CAM-10 deferred reply; a core scatter-gather exchange (one request, multiple replies) is not required for v1 and is raised as a core question (§27). | | D-CAM-21 | Capture thumbnail | Opt-in per capture profile (`thumbnail.size` = `small` 160px / `medium` 320px / `large` 640px, longest edge, aspect preserved, never upscaled); JPEG; carried in the ANNOUNCEMENT only, as native protobuf bytes; never in the durable record | A consumer on the bus can see the picture without fetching the file. It is bounded by the longest edge because cameras are 4:3 and 16:9 and a fixed W×H would distort or letterbox. It is announcement-only because the terminal body IS the committed document — the catalog's `terminal_result`, the metadata sidecar verbatim, and the body group replies embed — and a lossy, derived, disposable preview must not be durably stored per capture (D-CAM-13). It carries NO digest: a thumbnail is a lossy re-encode and a `sha256` beside the artifact's would invite a consumer to believe it is verifiable against it. A thumbnail that cannot be rendered, or will not fit the byte ceiling, is dropped and counted — it never fails a capture. The ceiling is the TRANSPORT's, resolved at startup: the Greengrass IPC client encodes a whole message into a static 10,000-byte buffer, so IPC carries `small` only (6 KiB budget) and a larger configured size is clamped down rather than rejected — the same config ships to Greengrass and to Kubernetes; MQTT carries all three (60 KiB budget, bounded by the library's 64 KiB binary-value cap, not by the broker). And a preview NEVER outranks the result: if an announcement carrying one cannot be published, the result is announced again without it. | +| D-CAM-23 | Standardized southbound error codes | Rename the routing/availability codes to the SOUTHBOUND.md §2.2 family: `INVALID_REQUEST` and `INSTANCE_REQUIRED` → `BAD_ARGS`, `UNKNOWN_INSTANCE` → `NO_SUCH_INSTANCE`, `CAMERA_UNAVAILABLE` → `DEVICE_UNAVAILABLE`. Domain codes (`CAPTURE_*`, `PTZ_*`, `STORAGE_PRESSURE`, `QUEUE_FULL`, …) are unchanged. **Hard rename, no aliases.** | The adapter's routing/availability codes were adapter-local while the rest of the ecosystem standardized on `BAD_ARGS`/`NO_SUCH_INSTANCE`/`DEVICE_UNAVAILABLE`, so a fleet-wide alarm rule keyed on the standard names could not match this adapter. This is a **wire-visible breaking change** for any consumer keying on the old strings; the component is pre-1.0/experimental and a documented breaking change is acceptable where a silent one is not. `INSTANCE_REQUIRED` and `INVALID_REQUEST` both collapse to `BAD_ARGS` because both are argument faults. Consumer grep at implementation time found no matches in `edge-console` or `bottling-company-test` for the old strings. | +| D-CAM-24 | Lifecycle verbs `sb/pause` / `sb/resume`; `repoll` N/A | Serve the standardized `sb/pause` / `sb/resume` lifecycle verbs: they suspend/admit a camera's new capture work (scheduled and commanded), leave in-flight captures to run to completion, refuse new capture work while paused with the stable `INSTANCE_PAUSED` code, are idempotent (`{ paused, changed }`), and surface in `sb/status`. Pause is process-local (a restart begins unpaused) and scoped to capture workload — `sb/reconnect`, PTZ, and read-only verbs are unaffected. **`repoll` is recorded N/A.** | Pause/resume are the standard instance-control family a real adapter serves (SOUTHBOUND.md §2.2). `repoll` triggers an immediate poll cycle for poll-loop adapters; this adapter is capture-on-demand and schedule-driven, not poll-based, so there is no poll cycle to trigger — `sb/capture` / `sb/capture-submit` are the on-demand trigger and schedules drive the periodic work. Adding a redundant `repoll` alias would only duplicate `sb/capture` while needing its own idempotency key, so it is not implemented; the decision is recorded here and in `docs/reference/messaging-interface.md` per the issue. | +| D-CAM-25 | Operational metric families | `camera_captures` becomes a `(Total, Interval)` counter family accumulated on the capture hooks and drained on the 30 s sampler tick (the interval counter preserves the every-event guarantee — the drain is a reset, not a sample). Add a `CameraCommand` family dimensioned `instance` × `verb` × `result` with `commandRequests`/`commandErrors` pairs and a `commandLatencyMs` sum, its cells defined lazily on first use. `camera_queue` stays a set of sampled gauges. **Recorded cardinality decision:** `camera_captures` / `camera_queue` stay **fleet-scoped** (no per-camera dimension); only the lower-frequency `CameraCommand` carries `instance`. | The two capture families lacked the `(Total, Interval)` pattern the rest of the ecosystem uses, so a fleet dashboard could not read them the same way as the other adapters. Per-camera dimensions on the capture families are deliberately avoided: a 256-camera fleet would mint 256 streams per measure on the highest-frequency families (the same reasoning that kept the fleet-scope originally), and pre-populating `instance × verb × result` for the command family would cold-define 256 × 16 × 2 streams — so `CameraCommand` is defined lazily and only the families whose frequency justifies it carry `instance`. Per-camera capture state remains answerable through `sb/queue-status` and the per-instance connectivity keepalive. | +| D-CAM-26 | Edge-console panel trio | Register `overview` / `signals` / `diagnostics` panels via `register_panel` (order 10/20/30, `scope: "instance"`), bound to the verbs the adapter serves: overview → `sb/status`/`sb/reconnect`/`sb/pause`/`sb/resume`; signals → `sb/list`/`sb/status`/`sb/capture`/`sb/capture-status`; diagnostics → `sb/discover`/`sb/queue-status`. | The baseline advertises a descriptor-driven panel surface to edge-console; registering the trio on the same inbox as the verbs, before the acknowledged subscription begins, advertises the panels atomically with the command surface they drive. | +| D-CAM-27 | Committed `Cargo.lock`; `data-types.md` N/A | Commit `Cargo.lock` (drop the `/Cargo.lock` gitignore + rationale), regenerated with the local `.cargo` `[patch]` override inactive so it records the pinned git source and is valid on a fresh clone / in CI. The RTSP native-coverage harness (`simulators/run-rtsp-native-coverage.ps1`), which mounts the workspace read-only, keeps its **writable-overlay** lockfile — a single-file bind mount from outside the read-only source tree that masks the committed lock and is (re)generated in-container by a prep run — so cargo never has to rewrite the committed git-sourced lock on the read-only mount. The adapter's signal `data-types.md` page is **N/A**: this component publishes capture announcements (`app/image/*`), not `SouthboundSignalUpdate` envelopes, so the signal value-mapping page does not apply; the published shapes are documented in `messaging-interface.md`. | The three-way constraint (a committed lock must record the git source ⊕ a `[patch]`-ed local build rewrites it to a path source ⊕ the read-only validation mount cannot rewrite it) is resolved deliberately rather than by suppressing the `edgecommons component validate` warning: the committed lock is git-sourced and reproducible on a fresh clone/CI, and the two local frictions (a `git status` that shows lock churn while the patch is active, and the read-only harness) are handled by simply not committing that churn and by running the harness with the patch inactive. The manifest `license` field is reconciled to `BUSL-1.1` to match the `LICENSE` file (`Fixes #7`). | +| D-CAM-28 | `config.schema.json` | Author a Draft 2020-12 `config.schema.json` whose **root models `component.global`** (with `$defs/camera` for one `component.instances[]` entry and `$defs` for every backend/profile/schedule/enum), derived field-for-field from `src/config.rs`. Strict (`additionalProperties:false`) everywhere the parser is (`deny_unknown_fields`); permissive only where the parser is (`featureOverrides`, `resourceGroups`). | `edgecommons component validate` could not check camera configs and warned on the missing schema. The CLI validates `component.global` against the schema root (`ec-validate/schema.rs` checks `/component/global` only), so the root is `component.global`; `$defs/camera` documents and lets external Draft 2020-12 validators check `instances[]`, matching the scaffold's `$defs/device`. Validated: the shipped `deploy/docker/simulator-config.json` and the §10.1 full example ACCEPT; a malformed global key, an ONVIF backend missing `mediaProfile`, a bad enum, an unknown backend field, and a camera missing `backend` all REJECT. `edgecommons component validate` reports **no findings** (schema valid, config clean, lockfile warning resolved). | | D-CAM-22 | Bare-RTSP backend | A distinct `rtsp` backend addresses a camera by a raw `rtsp://`/`rtsps://` URL, with no ONVIF. It is still-image only, reuses the shared RTSP engine (`RtspCaptureController`) and the network/credential/TLS primitives, and advertises `capture_modes=[rtsp-frame]` with all PTZ/snapshot/discovery capabilities off. To make it buildable without ONVIF, the protocol-neutral net/auth primitives and the credential-resolution seam are lifted from the `onvif` module into a shared `backend::net` module, and the `rtsp` cargo feature is decoupled from `onvif`. | ONVIF gives identity, capability discovery, media profiles, snapshot, PTZ, and the governed stream URI; a raw RTSP URL gives none of these, so it is a genuinely different camera kind rather than a mode of `onvif-rtsp` — a separate backend keeps the ONVIF backend's required-field invariants (`deviceServiceUrl`/`mediaProfile`) intact. `connect()` performs the RTSP `DESCRIBE`/`SETUP` + auth + SDP/codec validation so a dead URL, bad auth, or unsupported codec fails at connect (the supervisor keeps such a camera OFFLINE rather than falsely ONLINE, since reachability is inferred from a successful connect). The URL carries no credentials (userinfo is rejected); credentials are `$secret` references resolved through the same bounded EdgeCommons path as ONVIF, and the same host-allowlist / DNS-pin / RTSPS-SNI / forbidden-address policy applies to the user-supplied URL. Decoupling the feature lets an operator ship an RTSP-only binary without the ONVIF surface. | ## 5. System context @@ -1079,7 +1085,7 @@ command wrapper: { "ok": true, "result": { } } // error -{ "ok": false, "error": { "code": "CAMERA_UNAVAILABLE", "message": "..." } } +{ "ok": false, "error": { "code": "DEVICE_UNAVAILABLE", "message": "..." } } ``` Reply `header.name` remains the command verb, and `header.version` remains the core command contract @@ -1113,6 +1119,8 @@ camera adapter. | `sb/queue-status` | immediate | Report live admission capacity, per-camera queue depth, and the durable backlog. | | `sb/queue-clear` | immediate | Break-glass: cancel the durable backlog for one camera or, explicitly, the fleet. | | `sb/reconnect` | immediate acceptance | Close and reconnect a configured camera session. | +| `sb/pause` | immediate | Suspend a camera's new capture work (scheduled and commanded); idempotent `{ paused, changed }`. | +| `sb/resume` | immediate | Resume a paused camera; idempotent `{ paused, changed }`. | | `sb/ptz` | immediate | Execute or inspect a capability-gated PTZ operation. | | `sb/ptz-presets` | immediate | List, recall, create, or remove presets subject to policy. | @@ -1121,8 +1129,8 @@ camera adapter. Every command that targets a camera accepts `instance`: - When exactly one camera is configured, it MAY be omitted and defaults to that instance. -- With more than one camera, omission returns `INSTANCE_REQUIRED`. -- An unknown instance returns `UNKNOWN_INSTANCE`. +- With more than one camera, omission returns `BAD_ARGS`. +- An unknown instance returns `NO_SUCH_INSTANCE`. - A known but disabled camera returns status normally and rejects actuation with `CAMERA_DISABLED`. ### 13.2 `sb/list` @@ -1285,7 +1293,7 @@ Request (both verbs): - `instances` MUST name 2 to `limits.maxCamerasPerGroup` distinct configured cameras. Validation is all-or-nothing: an unknown, disabled, or duplicate member rejects the whole request before any job is - created (`UNKNOWN_INSTANCE`, `CAMERA_DISABLED`, or `INVALID_REQUEST`); an oversize list returns + created (`NO_SUCH_INSTANCE`, `CAMERA_DISABLED`, or `BAD_ARGS`); an oversize list returns `GROUP_TOO_LARGE`. - `captureProfile` applies to every member unless overridden per member in `profileOverrides`; every effective profile must exist on its camera. `timeoutMs` is a per-member job deadline, not a group @@ -1491,13 +1499,13 @@ must round-trip unchanged. | Code | Meaning | |---|---| -| `INSTANCE_REQUIRED` | More than one camera exists and no target was supplied. | -| `UNKNOWN_INSTANCE` | Target camera ID is not configured. | +| `BAD_ARGS` | Malformed request body, an out-of-range field, or instance routing — a missing `instance` when more than one camera is configured, or any other invalid argument. | +| `NO_SUCH_INSTANCE` | Target camera ID is not configured. | | `CAMERA_DISABLED` | The configured camera is disabled. | -| `CAMERA_UNAVAILABLE` | Camera is offline and the command policy does not wait. | +| `DEVICE_UNAVAILABLE` | Camera is offline (session down or unreachable) and the command policy does not wait. | +| `INSTANCE_PAUSED` | The camera is paused (`sb/pause`) and refuses new capture work until resumed. | | `CAMERA_MOVING` | Capture/PTZ interlock rejected capture while the camera was moving. | | `UNSUPPORTED_CAPABILITY` | Camera/backend cannot perform the requested operation. | -| `INVALID_REQUEST` | Body shape, range, or profile is invalid. | | `UNKNOWN_CAPTURE_PROFILE` | Named profile is not configured for the camera. | | `QUEUE_FULL` | Per-camera or global admission limit rejected the job. | | `GROUP_TOO_LARGE` | A group capture named more members than `limits.maxCamerasPerGroup`. | diff --git a/Dockerfile b/Dockerfile index 6518888..a132b37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,14 +34,13 @@ RUN rm -f /etc/apt/sources.list.d/debian.sources \ WORKDIR /build -# Copy only source inputs. +# Copy only source inputs, including the committed `Cargo.lock`. # -# `Cargo.lock` is deliberately absent: it is untracked and gitignored, because a local build goes -# through the `[patch]` override above, and a patched build rewrites the lock to a PATH source. A -# committed lock would therefore either lie to a fresh clone or break the read-only validation mounts. -# Copying it -- or building `--locked` against a lock that is not there -- fails outright in a clean -# clone, which is exactly what this file used to do. -COPY Cargo.toml /build/camera-adapter/ +# `Cargo.lock` is committed and records the git source `edgecommons` is pinned to by REV in Cargo.toml +# (it is regenerated on a fresh clone with the local `.cargo` `[patch]` inactive, so it never carries a +# path override). The image build sees no `.cargo` override, so `--locked` resolves the pinned rev +# reproducibly from the lock. +COPY Cargo.toml Cargo.lock /build/camera-adapter/ COPY src /build/camera-adapter/src COPY native /build/camera-adapter/native @@ -52,7 +51,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/build/target \ CARGO_TARGET_DIR=/build/target \ - cargo build --release --no-default-features --features standalone,onvif \ + cargo build --release --locked --no-default-features --features standalone,onvif \ && install -D -m 0755 /build/target/release/camera-adapter /build/artifacts/camera-adapter FROM build AS build-rtsp @@ -60,7 +59,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/build/target \ CARGO_TARGET_DIR=/build/target \ - cargo build --release --no-default-features --features standalone,onvif,rtsp \ + cargo build --release --locked --no-default-features --features standalone,onvif,rtsp \ && install -D -m 0755 /build/target/release/camera-adapter /build/artifacts/camera-adapter FROM docker.io/library/debian@sha256:1def178129dfb5f24db43afbf2fcac04530012e3264ba4ff81c71184e17a9ee4 AS runtime-base diff --git a/Dockerfile.dockerignore b/Dockerfile.dockerignore index bb462c5..67efca4 100644 --- a/Dockerfile.dockerignore +++ b/Dockerfile.dockerignore @@ -4,11 +4,12 @@ ** !Cargo.toml +!Cargo.lock !src/** !native/** -# `Cargo.lock` is NOT admitted, and deliberately so. It is untracked, so a developer's copy is -# whatever their gitignored `.cargo/config.toml` `[patch]` last wrote -- a PATH source pointing at a -# checkout that does not exist inside the image. Letting it into the context would put a local, -# unreviewable file in charge of what the image is built from. `edgecommons` is pinned by REV in -# Cargo.toml; that is the pin that decides the library source here. +# `Cargo.lock` IS admitted: it is committed and records the git source `edgecommons` is pinned to by +# REV in Cargo.toml (a fresh clone regenerates it with the local `.cargo/config.toml` `[patch]` +# inactive, so the committed lock never carries a developer's path override). The image is built +# `--locked` against it for reproducibility. The developer's local `.cargo` overrides are still kept +# out of the context. diff --git a/README.md b/README.md index e0b409e..b454d80 100644 --- a/README.md +++ b/README.md @@ -95,4 +95,4 @@ northbound without an explicit policy. ## License -Apache-2.0. +BUSL-1.1. See [`LICENSE`](LICENSE). diff --git a/config.schema.json b/config.schema.json new file mode 100644 index 0000000..4017dfd --- /dev/null +++ b/config.schema.json @@ -0,0 +1,574 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://docs.edgecommons.mbreissi.com/schemas/camera-adapter/config.schema.json", + "title": "camera-adapter component configuration", + "description": "The configuration camera-adapter itself understands: the object at `component.global` (this schema's root), with each entry of `component.instances[]` validated against `#/$defs/camera`. Derived from `src/config.rs`. camera-adapter is a camera, not a signal adapter: an instance is a camera and a profile produces an image. The library envelope (logging, messaging, metricEmission, heartbeat, health, hierarchy, identity, topic, tags, credentials, parameters) is owned by the canonical schema and is deliberately not redeclared here. `component.token` is the core component token and likewise belongs to the envelope.", + "type": "object", + "additionalProperties": false, + "required": ["output"], + "properties": { + "output": { + "type": "object", + "additionalProperties": false, + "required": ["rootDirectory"], + "description": "Output root, path templates, durability, and disk-pressure floors.", + "properties": { + "rootDirectory": { + "type": "string", + "minLength": 1, + "description": "Absolute output root. Every camera writes under it." + }, + "cameraDirectoryTemplate": { + "type": "string", + "default": "{cameraId}", + "description": "Relative per-camera directory template." + }, + "fileNameTemplate": { + "type": "string", + "default": "{captureId}.{extension}", + "description": "Relative filename template. Must contain `{captureId}` (in this field or the directory template) so two captures cannot resolve to the same file." + }, + "writeMetadataSidecar": { + "type": "boolean", + "default": false, + "description": "Whether a durable JSON sidecar is written before the final image is exposed." + }, + "minimumFreeBytes": { + "type": "integer", + "minimum": 0, + "description": "Absolute free bytes retained after outstanding reservations." + }, + "minimumFreePercent": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Free-space percentage retained after outstanding reservations." + }, + "directoryMode": { + "type": "string", + "default": "0755", + "description": "Unix mode (octal string) for new output directories." + }, + "fileMode": { + "type": "string", + "default": "0644", + "description": "Unix mode (octal string) for new images and sidecars." + } + } + }, + "state": { + "type": "object", + "additionalProperties": false, + "description": "Durable catalog location and retention policy.", + "properties": { + "directory": { + "type": "string", + "description": "Absolute state root, or a platform-bound default where permitted." + }, + "resultRetentionHours": { + "type": "integer", + "minimum": 0, + "default": 72, + "description": "Terminal-result and idempotency retention window." + }, + "maxResultRecords": { + "type": "integer", + "minimum": 0, + "default": 100000, + "description": "Soft terminal-record count cap." + }, + "queuedRecoveryPolicy": { + "$ref": "#/$defs/queuedRecoveryPolicy" + } + } + }, + "limits": { + "type": "object", + "additionalProperties": false, + "description": "Process-wide and per-camera capacity bounds.", + "properties": { + "maxConnectedCameras": { "type": "integer", "minimum": 1, "default": 256 }, + "maxConcurrentCaptures": { "type": "integer", "minimum": 1, "default": 32 }, + "maxConcurrentEncodes": { "type": "integer", "minimum": 1 }, + "maxConcurrentWrites": { "type": "integer", "minimum": 1, "default": 8 }, + "maxConcurrentConnects": { "type": "integer", "minimum": 1, "default": 16 }, + "maxInFlightBytes": { "type": "integer", "minimum": 1, "description": "Raw frame bytes reserved across active work. Must cover maxConcurrentCaptures x maxFrameBytesPerCamera." }, + "maxFrameBytesPerCamera": { "type": "integer", "minimum": 1, "description": "Default and hard per-camera frame ceiling." }, + "maxMetadataBytes": { "type": "integer", "minimum": 0, "default": 8192 }, + "maxQueuedCapturesPerCamera": { "type": "integer", "minimum": 1, "default": 4 }, + "maxQueuedControlsPerCamera": { "type": "integer", "minimum": 1, "default": 32 }, + "maxDeferredWaitersPerCapture": { "type": "integer", "minimum": 1, "default": 8 }, + "maxCamerasPerGroup": { "type": "integer", "minimum": 2, "default": 32 }, + "maxPendingCaptures": { "type": "integer", "minimum": 1, "default": 256, "description": "Fleet-wide backlog bound." }, + "maxQueueWaitMs": { "type": "integer", "minimum": 0, "default": 300000 }, + "resourceGroups": { + "type": "object", + "description": "Named shared NIC/USB transport admission bounds, keyed by group name.", + "additionalProperties": { "$ref": "#/$defs/resourceGroup" } + } + } + }, + "timeouts": { + "type": "object", + "additionalProperties": false, + "description": "Runtime deadlines, in milliseconds.", + "properties": { + "connectMs": { "type": "integer", "minimum": 1, "default": 10000 }, + "reconnectBackoffMinMs": { "type": "integer", "minimum": 1, "default": 1000 }, + "reconnectBackoffMaxMs": { "type": "integer", "minimum": 1, "default": 60000 }, + "jobTerminalMs": { "type": "integer", "minimum": 1, "default": 90000 }, + "captureMs": { "type": "integer", "minimum": 1, "default": 30000 }, + "encodeMs": { "type": "integer", "minimum": 1, "default": 30000 }, + "persistMs": { "type": "integer", "minimum": 1, "default": 30000 }, + "ptzMs": { "type": "integer", "minimum": 1, "default": 10000 }, + "replyMarginMs": { "type": "integer", "minimum": 0, "default": 5000 }, + "maxDeferredReplyLifetimeMs": { "type": "integer", "minimum": 1, "default": 95000 }, + "reloadDrainTimeoutMs": { "type": "integer", "minimum": 0, "default": 30000 }, + "shutdownGraceMs": { "type": "integer", "minimum": 0, "default": 30000 } + } + }, + "discovery": { + "type": "object", + "additionalProperties": false, + "description": "Protocol discovery policy.", + "properties": { + "enabled": { "type": "boolean", "default": false }, + "reportUnconfigured": { "type": "boolean", "default": false }, + "intervalSeconds": { "type": "integer", "minimum": 1, "default": 60 }, + "maxResults": { "type": "integer", "minimum": 0, "default": 1000 }, + "eligibleInterfaces": { + "type": "array", + "items": { "type": "string" }, + "default": [], + "description": "Exact OS interface names eligible for credential-free WS-Discovery multicast." + } + } + }, + "operatorEvents": { + "type": "object", + "additionalProperties": false, + "description": "Optional high-volume operator-event policy.", + "properties": { + "captureLifecycle": { + "type": "boolean", + "default": false, + "description": "Emit capture queued/started diagnostic events." + } + } + }, + "healthThresholds": { + "type": "object", + "additionalProperties": false, + "description": "Thresholds feeding the `southbound_health` metric (SOUTHBOUND.md §5).", + "properties": { + "staleSignalSecs": { + "type": "integer", + "minimum": 1, + "default": 300, + "description": "Seconds without a successful observation before a camera counts toward `southbound_health.staleSignals`." + } + } + }, + "security": { + "type": "object", + "additionalProperties": false, + "description": "HTTP/XML/decompression policy shared by ONVIF instances.", + "properties": { + "maxHeaderBytes": { "type": "integer", "minimum": 0, "default": 65536 }, + "maxDecompressionRatio": { "type": "integer", "minimum": 0, "default": 100 }, + "allowBasicOverPlaintext": { "type": "boolean", "default": false } + } + }, + "captureGroupSchedules": { + "type": "array", + "description": "Cron schedules that fire one synchronised capture across several cameras.", + "items": { "$ref": "#/$defs/captureGroupSchedule" } + } + }, + "$defs": { + "camera": { + "title": "One camera", + "description": "One entry of `component.instances[]`. Each camera gets its own supervisor and connection lifecycle.", + "type": "object", + "additionalProperties": false, + "required": ["id", "backend", "defaultCaptureProfile", "captureProfiles"], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9._-]+$", + "description": "Stable UNS instance token: the `{instance}` of this camera's topics and its health-metric `instance` dimension." + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether the camera accepts actuation." + }, + "resourceGroup": { + "type": "string", + "description": "Optional shared NIC/USB admission group (a key of `limits.resourceGroups`)." + }, + "backend": { "$ref": "#/$defs/backend" }, + "defaultCaptureProfile": { + "type": "string", + "minLength": 1, + "description": "Profile used for a request that names none. Must be a key of `captureProfiles`." + }, + "captureProfiles": { + "type": "object", + "minProperties": 1, + "description": "Named, immutable-at-acceptance capture profiles, keyed by profile name.", + "additionalProperties": { "$ref": "#/$defs/captureProfile" } + }, + "schedules": { + "type": "array", + "description": "Per-camera cron schedules; omission is command-only operation.", + "items": { "$ref": "#/$defs/schedule" } + }, + "ptz": { "$ref": "#/$defs/ptz" } + } + }, + "backend": { + "title": "Protocol backend", + "description": "Tagged by `type`. camera is a camera, not a signal adapter — `sim`, `genicam-aravis`, `onvif-rtsp`, or `rtsp`.", + "type": "object", + "required": ["type"], + "oneOf": [ + { "$ref": "#/$defs/backendSim" }, + { "$ref": "#/$defs/backendGenicam" }, + { "$ref": "#/$defs/backendOnvif" }, + { "$ref": "#/$defs/backendRtsp" } + ] + }, + "backendSim": { + "title": "Simulator backend", + "type": "object", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": { "const": "sim" }, + "simulatedId": { "type": "string", "description": "Stable simulated identity; defaults to the camera id." }, + "seed": { "type": "integer", "minimum": 0, "description": "Deterministic generator seed." }, + "frame": { "$ref": "#/$defs/simFrame" }, + "connectDelayMs": { "type": "integer", "minimum": 0, "default": 0 }, + "captureDelayMs": { "type": "integer", "minimum": 0 }, + "ptz": { "$ref": "#/$defs/simPtz" }, + "faults": { "$ref": "#/$defs/simFaults" } + } + }, + "simFrame": { + "type": "object", + "additionalProperties": false, + "description": "Simulator source-frame settings.", + "properties": { + "width": { "type": "integer", "minimum": 1, "default": 640 }, + "height": { "type": "integer", "minimum": 1, "default": 480 }, + "pixelFormat": { "$ref": "#/$defs/pixelFormat" }, + "pattern": { "$ref": "#/$defs/simPattern" } + } + }, + "simPtz": { + "type": "object", + "additionalProperties": false, + "description": "Simulated PTZ capability switches.", + "properties": { + "supported": { "type": "boolean", "default": false }, + "statusSupported": { "type": "boolean", "default": true }, + "presetsSupported": { "type": "boolean", "default": false } + } + }, + "simFaults": { + "type": "object", + "additionalProperties": false, + "description": "Simulator deterministic failure counters.", + "properties": { + "disconnectAfterCaptures": { "type": "integer", "minimum": 0 }, + "failEveryNthCapture": { "type": "integer", "minimum": 0 }, + "incompleteEveryNthCapture": { "type": "integer", "minimum": 0 } + } + }, + "backendGenicam": { + "title": "GenICam/Aravis backend", + "type": "object", + "additionalProperties": false, + "required": ["type", "selector"], + "properties": { + "type": { "const": "genicam-aravis" }, + "selector": { "$ref": "#/$defs/genicamSelector" }, + "transport": { "$ref": "#/$defs/genicamTransport" }, + "interface": { "type": "string", "description": "Explicit host interface when required." }, + "packetSize": { + "description": "`auto` or a numeric device packet size.", + "oneOf": [ { "const": "auto" }, { "type": "integer", "minimum": 1 } ] + }, + "packetDelayNs": { "type": "integer", "minimum": 0 }, + "bufferCount": { "type": "integer", "minimum": 1 }, + "featureOverrides": { + "type": "object", + "description": "Allowlisted standard GenICam feature values, keyed by feature name.", + "additionalProperties": true + } + } + }, + "genicamSelector": { + "type": "object", + "additionalProperties": false, + "description": "Stable GenICam selector; exactly one field must be set (enforced at runtime).", + "properties": { + "serial": { "type": "string" }, + "mac": { "type": "string" }, + "deviceId": { "type": "string" }, + "ip": { "type": "string" } + } + }, + "backendOnvif": { + "title": "ONVIF/RTSP backend", + "type": "object", + "additionalProperties": false, + "required": ["type", "mediaProfile"], + "description": "One of `deviceServiceUrl` or `selector` is also required (enforced at runtime).", + "properties": { + "type": { "const": "onvif-rtsp" }, + "deviceServiceUrl": { "type": "string", "description": "Explicit ONVIF Device service URL." }, + "selector": { "$ref": "#/$defs/onvifSelector" }, + "credentials": { "$ref": "#/$defs/secretRef" }, + "mediaProfile": { "type": "string", "minLength": 1, "description": "Opaque profile token or exact profile name." }, + "captureMode": { "$ref": "#/$defs/captureMode" }, + "rtspFallback": { "type": "boolean", "default": false }, + "rtspSessionPolicy": { "$ref": "#/$defs/rtspSessionPolicy" }, + "allowInsecure": { "type": "boolean", "default": false }, + "allowedUriHosts": { "type": "array", "items": { "type": "string" }, "default": [] }, + "allowedUriCidrs": { "type": "array", "items": { "$ref": "#/$defs/cidr" }, "default": [] }, + "maxSoapBytes": { "type": "integer", "minimum": 1 }, + "maxSnapshotBytes": { "type": "integer", "minimum": 1 }, + "maxXmlDepth": { "type": "integer", "minimum": 1 }, + "tls": { "$ref": "#/$defs/tls" }, + "mediaService": { "$ref": "#/$defs/mediaService" }, + "authenticationMode": { "$ref": "#/$defs/authenticationMode" } + } + }, + "onvifSelector": { + "type": "object", + "additionalProperties": false, + "required": ["endpointReference"], + "properties": { + "endpointReference": { "type": "string", "minLength": 1, "description": "Exact endpoint-reference URI." } + } + }, + "backendRtsp": { + "title": "Bare RTSP backend", + "description": "A camera addressed by a raw `rtsp`/`rtsps` URL, still-image only. No ONVIF control.", + "type": "object", + "additionalProperties": false, + "required": ["type", "url"], + "properties": { + "type": { "const": "rtsp" }, + "url": { "type": "string", "minLength": 1, "description": "Raw `rtsp`/`rtsps` stream URL. Userinfo in the URL is forbidden." }, + "credentials": { "$ref": "#/$defs/secretRef" }, + "tls": { "$ref": "#/$defs/tls" }, + "allowInsecure": { "type": "boolean", "default": false }, + "allowedUriHosts": { "type": "array", "items": { "type": "string" }, "default": [] }, + "allowedUriCidrs": { "type": "array", "items": { "$ref": "#/$defs/cidr" }, "default": [] }, + "rtspSessionPolicy": { "$ref": "#/$defs/rtspSessionPolicy" }, + "captureMode": { "$ref": "#/$defs/captureMode" }, + "maxFrameBytes": { "type": "integer", "minimum": 1 }, + "authenticationMode": { "$ref": "#/$defs/authenticationMode" } + } + }, + "captureProfile": { + "title": "Capture profile", + "type": "object", + "additionalProperties": false, + "required": ["output"], + "description": "Immutable-at-acceptance capture settings. Geometry/exposure fields apply to GenICam.", + "properties": { + "captureMode": { "$ref": "#/$defs/captureMode" }, + "offlinePolicy": { "$ref": "#/$defs/offlinePolicy" }, + "queueExpiryMs": { "type": "integer", "minimum": 0 }, + "timeoutMs": { "type": "integer", "minimum": 1 }, + "maximumFrameBytes": { "type": "integer", "minimum": 1 }, + "pixelFormat": { "$ref": "#/$defs/pixelFormat" }, + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 }, + "offsetX": { "type": "integer", "minimum": 0 }, + "offsetY": { "type": "integer", "minimum": 0 }, + "exposureMicros": { "type": "integer", "minimum": 0 }, + "gain": { "type": "number" }, + "output": { "$ref": "#/$defs/profileOutput" }, + "thumbnail": { "$ref": "#/$defs/thumbnail" }, + "captureInterlock": { "$ref": "#/$defs/captureInterlock" } + } + }, + "profileOutput": { + "type": "object", + "additionalProperties": false, + "required": ["encoding"], + "properties": { + "encoding": { "$ref": "#/$defs/outputEncoding" }, + "jpegQuality": { "type": "integer", "minimum": 1, "maximum": 100, "default": 85 } + } + }, + "thumbnail": { + "type": "object", + "additionalProperties": false, + "required": ["size"], + "description": "Opt-in preview of the captured frame. Presence is the opt-in.", + "properties": { + "size": { "$ref": "#/$defs/thumbnailSize" } + } + }, + "schedule": { + "title": "Per-camera schedule", + "type": "object", + "additionalProperties": false, + "required": ["id", "cron", "timezone", "captureProfile"], + "properties": { + "id": { "type": "string", "minLength": 1, "description": "Stable schedule token." }, + "enabled": { "type": "boolean", "default": true }, + "cron": { "type": "string", "description": "Six-field cron expression including seconds." }, + "timezone": { "type": "string", "description": "IANA timezone identifier." }, + "captureProfile": { "type": "string", "minLength": 1, "description": "A key of the camera's `captureProfiles`." }, + "misfirePolicy": { "$ref": "#/$defs/misfirePolicy" }, + "overlapPolicy": { "$ref": "#/$defs/overlapPolicy" }, + "jitterSeconds": { "type": "integer", "minimum": 0, "default": 0 } + } + }, + "captureGroupSchedule": { + "title": "Group schedule", + "type": "object", + "additionalProperties": false, + "required": ["id", "cron", "timezone", "instances"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "enabled": { "type": "boolean", "default": true }, + "cron": { "type": "string", "description": "Six-field cron expression including seconds." }, + "timezone": { "type": "string", "description": "IANA timezone identifier." }, + "instances": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 2, + "description": "The cameras captured together, in result order." + }, + "captureProfile": { "type": "string", "description": "Profile applied to every member without an override." }, + "profileOverrides": { + "type": "object", + "description": "Per-camera capture-profile overrides; every key must be a member.", + "additionalProperties": { "type": "string" } + }, + "misfirePolicy": { "$ref": "#/$defs/misfirePolicy" }, + "overlapPolicy": { "$ref": "#/$defs/overlapPolicy" }, + "jitterSeconds": { "type": "integer", "minimum": 0, "default": 0 }, + "timeoutMs": { "type": "integer", "minimum": 1 } + } + }, + "ptz": { + "title": "Per-camera PTZ policy", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean", "default": false }, + "maximumContinuousMoveMs": { "type": "integer", "minimum": 1, "default": 10000 }, + "captureInterlock": { "$ref": "#/$defs/captureInterlock" }, + "settleMs": { "type": "integer", "minimum": 0, "default": 750 }, + "allowPresetMutation": { "type": "boolean", "default": false } + } + }, + "resourceGroup": { + "type": "object", + "additionalProperties": false, + "required": ["maxConcurrentCaptures"], + "properties": { + "maxConcurrentCaptures": { "type": "integer", "minimum": 1, "description": "Simultaneous acquisitions using this resource." } + } + }, + "secretRef": { + "type": "object", + "additionalProperties": false, + "required": ["$secret"], + "description": "A reference resolved lazily through EdgeCommons credentials.", + "properties": { + "$secret": { "type": "string", "minLength": 1, "description": "Secret name/path." }, + "field": { "type": "string", "description": "Optional field within a structured secret." } + } + }, + "tls": { + "type": "object", + "additionalProperties": false, + "description": "TLS trust/hostname policy.", + "properties": { + "ca": { "$ref": "#/$defs/secretRef" }, + "verifyHostname": { "type": "boolean", "default": true } + } + }, + "cidr": { + "type": "string", + "description": "A CIDR block, e.g. `192.168.1.0/24` or `2001:db8::/32`." + }, + "queuedRecoveryPolicy": { + "enum": ["requeue", "interrupt"], + "default": "requeue", + "description": "Restart treatment for queued jobs." + }, + "pixelFormat": { + "enum": ["Mono8", "RGB8", "BGR8", "JPEG"], + "description": "Source-frame pixel format." + }, + "outputEncoding": { + "enum": ["passthrough", "jpeg", "png", "tiff", "raw"], + "description": "Final file encoding." + }, + "captureMode": { + "enum": ["simulated", "software-trigger", "snapshot-uri", "rtsp-frame"], + "description": "Capture mechanism." + }, + "simPattern": { + "enum": ["color-bars", "gradient", "checkerboard", "solid"], + "default": "color-bars", + "description": "Simulator frame pattern." + }, + "genicamTransport": { + "enum": ["auto", "gige-vision", "usb3-vision"], + "default": "auto", + "description": "GenICam transport selection." + }, + "rtspSessionPolicy": { + "enum": ["on-demand", "warm"], + "default": "on-demand", + "description": "RTSP connection lifecycle policy." + }, + "mediaService": { + "enum": ["auto", "media1", "media2"], + "default": "auto", + "description": "ONVIF Media service generation." + }, + "authenticationMode": { + "enum": ["auto", "http-digest", "wsse-digest", "basic"], + "default": "auto", + "description": "ONVIF/RTSP authentication negotiation mode." + }, + "thumbnailSize": { + "enum": ["small", "medium", "large"], + "description": "Thumbnail longest-edge bound: small=160px, medium=320px, large=640px." + }, + "offlinePolicy": { + "enum": ["failFast", "waitUntilDeadline", "queue"], + "description": "Camera-offline admission policy." + }, + "captureInterlock": { + "enum": ["reject", "stopAndSettle", "allow"], + "default": "reject", + "description": "Capture/PTZ interlock policy." + }, + "misfirePolicy": { + "enum": ["skip", "coalesce"], + "default": "skip", + "description": "Missed-occurrence treatment." + }, + "overlapPolicy": { + "enum": ["skip", "queue"], + "default": "skip", + "description": "Treatment when a schedule already has a nonterminal job/group." + } + } +} diff --git a/docs/reference/data-types.md b/docs/reference/data-types.md new file mode 100644 index 0000000..96d52e1 --- /dev/null +++ b/docs/reference/data-types.md @@ -0,0 +1,22 @@ +# Data types + +The baseline signal value-mapping page — how a protocol value becomes a `SouthboundSignalUpdate` — +does not apply to this adapter. camera-adapter is a **camera**, not a signal adapter: it produces +images, not signals. The image bytes are the data plane and travel as files (delivered by +`file-replicator`); the bus carries only control and terminal metadata. + +The data shapes this adapter publishes are documented in the +[messaging interface reference](messaging-interface.md): + +- **Capture announcement** — the terminal `app/image/*` application message for a completed capture: + the capture and group identifiers, the camera summary, the durable output paths (`absolutePath`, + `relativePath`, `fileUri`), the effective profile, per-stage durations, and any failure summary. See + [Terminal application messages](messaging-interface.md#terminal-application-messages). +- **Capture thumbnail** — an optional, bounded JPEG preview carried inside the announcement only, as + native protobuf bytes, never in the durable record. See + [Capture thumbnail](messaging-interface.md#capture-thumbnail). +- **Metadata sidecar** — the optional on-disk JSON companion written next to the image. See the + [configuration reference](configuration.md). + +Command request/reply bodies (the `sb/*` verbs), the `JobState` vocabulary, and the stable error +codes are likewise specified in the [messaging interface reference](messaging-interface.md). diff --git a/docs/reference/messaging-interface.md b/docs/reference/messaging-interface.md index 4f6d1a4..d2cdce9 100644 --- a/docs/reference/messaging-interface.md +++ b/docs/reference/messaging-interface.md @@ -15,10 +15,10 @@ application message, not the command reply — see [Terminal application message These rules apply to every verb below. - **Closed request schema.** Bodies are parsed with `deny_unknown_fields`: any field not listed for a - verb is rejected with `INVALID_REQUEST`. All field names are **camelCase** on the wire. + verb is rejected with `BAD_ARGS`. All field names are **camelCase** on the wire. - **Selecting a camera.** Actuation verbs take an optional `instance`. Omit it only when exactly one camera is configured — the sole camera is then used. With more than one camera, omission is - `INSTANCE_REQUIRED`; an unknown name is `UNKNOWN_INSTANCE`; a disabled camera is `CAMERA_DISABLED`. An + `BAD_ARGS`; an unknown name is `NO_SUCH_INSTANCE`; a disabled camera is `CAMERA_DISABLED`. An `instance` token is non-empty, ≤128 bytes, ASCII letters/digits/`.`/`_`/`-`. - **Idempotency.** Every *mutating* verb requires a caller-owned `requestId` (1–256 bytes, no control characters). A retry with the same `requestId` and the same arguments returns the original outcome; a @@ -33,7 +33,7 @@ These rules apply to every verb below. snapshot the first page captured, not a fresh read. - **Errors.** A failed command replies `{ "errorCode": "...", "errorMessage": "..." }`. Branch on `errorCode` (the stable [error codes](#stable-errors)), never the human-readable message. While a - configuration reload is draining, every verb replies `CAMERA_UNAVAILABLE`. + configuration reload is draining, every verb replies `DEVICE_UNAVAILABLE`. - **`JobState` vocabulary.** A capture's durable state is one of `ACCEPTED`, `QUEUED`, `ACQUIRING`, `ENCODING`, `PERSISTING`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `INTERRUPTED`. @@ -121,7 +121,7 @@ resolved, no durable work is written. | Field | Type | Required | Meaning | |---|---|---|---| | `requestId` | string | **yes** | Component-scoped durable key, 1–256 bytes. | -| `instances` | string[] | **yes** | Result-order camera list. 2 ≤ length ≤ `limits.maxCamerasPerGroup`, no duplicates. Fewer than 2 → `INVALID_REQUEST`; more than the cap → `GROUP_TOO_LARGE`. | +| `instances` | string[] | **yes** | Result-order camera list. 2 ≤ length ≤ `limits.maxCamerasPerGroup`, no duplicates. Fewer than 2 → `BAD_ARGS`; more than the cap → `GROUP_TOO_LARGE`. | | `captureProfile` | string | optional | Common profile applied to every member; defaults per camera. | | `profileOverrides` | object | optional | Per-camera profile override (`{ cameraId: profile }`); keys must be a subset of `instances`. | | `timeoutMs` | u64 | optional | 1000–1800000, per member. | @@ -184,7 +184,7 @@ one idempotency namespace on `requestId`.) **What it does.** Reads durable status for a capture, a group, or a filtered page of captures. It is the authority on what happened — a consumer that must not miss an outcome polls this rather than relying on the terminal announcement. It selects **exactly one** of five lookup modes from the field combination; an -ambiguous or empty combination is `INVALID_REQUEST`. +ambiguous or empty combination is `BAD_ARGS`. **Input payload / lookup modes** @@ -302,8 +302,8 @@ snapshot. **Input payload.** `{ instance? }` — optional camera token. **Response payload.** With `instance`: a single `CameraSnapshot` (`instance`, `enabled`, `backend`, -`generation`, `state`, `capabilities`, `capabilitiesDigest`, `connectedAt`, `lastError`, `updatedAt`). -Without: `{ cameras: [ CameraSnapshot, … ] }`. +`generation`, `state`, `capabilities`, `capabilitiesDigest`, `connectedAt`, `lastError`, `updatedAt`) plus +`paused` (the `sb/pause` lifecycle state). Without: `{ cameras: [ CameraSnapshot + paused, … ] }`. **Example — request / response** ```json @@ -311,6 +311,7 @@ Without: `{ cameras: [ CameraSnapshot, … ] }`. ``` ```json { "instance": "camera-a", "enabled": true, "backend": "onvif-rtsp", "generation": 7, "state": "ONLINE", + "paused": false, "capabilities": { "captureModes": ["snapshot-uri"], "ptz": true, "presets": true, "presetMutation": false, "vendor": "Acme", "model": "X9", "warnings": [] }, "capabilitiesDigest": "3f2a…", "connectedAt": "2026-07-18T11:59:00Z", @@ -432,6 +433,42 @@ session, not the camera); ledgered and settled immediately. { "operationId": "op_018f2c3d-…", "instance": "camera-a", "state": "ACCEPTED" } ``` +## `sb/pause` + +**What it does.** Suspends a camera's new capture work: while paused, scheduled captures are held and new +commanded captures (`sb/capture`, `sb/capture-submit`, `sb/capture-group`, `sb/capture-group-submit`) are +refused with `INSTANCE_PAUSED`. Captures already in flight run to completion. The verb is idempotent, and +`changed` reports whether the state moved. Pause is in-memory only — a restart begins unpaused. It applies +only to capture workload: `sb/reconnect`, `sb/ptz*`, and the read-only verbs are unaffected, and the paused +state is surfaced by `sb/status`. + +**Input payload.** `{ instance? }` (no `requestId` — the toggle is idempotent by nature). Omit `instance` +only when exactly one camera is configured. + +**Response payload.** `{ id, paused: true, changed }`. + +**Example — request / response** +```json +{ "instance": "camera-a" } +``` +```json +{ "id": "camera-a", "paused": true, "changed": true } +``` + +## `sb/resume` + +**What it does.** Clears a paused camera, so scheduled and commanded capture work is admitted again. +Idempotent; `changed` reports whether the state moved. + +**Input payload.** `{ instance? }`. + +**Response payload.** `{ id, paused: false, changed }`. + +> **`repoll` is not implemented (N/A).** The standardized `repoll` verb triggers an immediate poll cycle +> for poll-loop adapters. This adapter is capture-on-demand and schedule-driven, not poll-based, so there is +> no poll cycle to trigger: `sb/capture` / `sb/capture-submit` are the on-demand trigger, and schedules +> drive the periodic work. The applicability decision is recorded in `DESIGN.md`. + --- # PTZ @@ -561,14 +598,22 @@ lifecycle event. ## Stable errors -The public error `code` is one of `INSTANCE_REQUIRED`, `UNKNOWN_INSTANCE`, `CAMERA_DISABLED`, -`CAMERA_UNAVAILABLE`, `CAMERA_MOVING`, `UNSUPPORTED_CAPABILITY`, `INVALID_REQUEST`, +The public error `code` is one of `BAD_ARGS`, `NO_SUCH_INSTANCE`, `CAMERA_DISABLED`, +`DEVICE_UNAVAILABLE`, `INSTANCE_PAUSED`, `CAMERA_MOVING`, `UNSUPPORTED_CAPABILITY`, `UNKNOWN_CAPTURE_PROFILE`, `QUEUE_FULL`, `GROUP_TOO_LARGE`, `RESOURCE_LIMIT`, `CAPTURE_TIMEOUT`, `CAPTURE_CANCELLED`, `PROCESS_INTERRUPTED`, `CAPTURE_NOT_FOUND`, `IDEMPOTENCY_CONFLICT`, `PREVIOUS_OUTCOME_UNKNOWN`, `REPLY_REQUIRED`, `UNSUPPORTED_PIXEL_FORMAT`, `STORAGE_PRESSURE`, `PERSISTENCE_FAILED`, `PTZ_DISABLED`, `PTZ_RANGE_ERROR`, `PTZ_TIMEOUT`, `COMPONENT_STOPPING`, or `BACKEND_ERROR`. Branch on `code`, not the sanitized human-readable message. +`BAD_ARGS` covers a malformed request body, an out-of-range field, and instance routing: a missing +`instance` when more than one camera is configured is `BAD_ARGS`, and an unknown `instance` is +`NO_SUCH_INSTANCE`. `INSTANCE_PAUSED` is returned when a camera has been paused with `sb/pause` and is +asked to accept new capture work. The routing and availability codes (`BAD_ARGS`, `NO_SUCH_INSTANCE`, +`DEVICE_UNAVAILABLE`) are the standardized southbound names shared across EdgeCommons adapters +(`core/docs/SOUTHBOUND.md` §2.2); the domain codes (`CAPTURE_*`, `PTZ_*`, `STORAGE_PRESSURE`, …) are +camera-specific. + `STORAGE_PRESSURE` on a capture submission means the output or state root is below its configured free-space floor, or cannot be read. The component refuses new captures until that root recovers; a broker it cannot reach never has this effect, because messaging is not what a capture consumes. diff --git a/docs/reference/metrics.md b/docs/reference/metrics.md index 775e012..1ab9140 100644 --- a/docs/reference/metrics.md +++ b/docs/reference/metrics.md @@ -1,8 +1,12 @@ # Metrics and alarms reference -The adapter emits three metrics through the EdgeCommons metric subsystem, which routes them to the target +The adapter emits four metrics through the EdgeCommons metric subsystem, which routes them to the target selected by the component's `metricEmission` configuration. +The operational families follow the EdgeCommons `(Total, Interval)` counter convention: every counter is +emitted as a pair — `Total` is monotonic since start, and `Interval` is what accrued since the +previous emission and resets on each one. Levels (gauges) and latency sums are single measures. + `southbound_health` is the standard metric every adapter in the ecosystem emits, dimensioned by `instance` so each camera reports its own. It is sampled every 30 seconds, and emitted immediately when a camera connects or disconnects. @@ -16,8 +20,11 @@ a camera connects or disconnects. | `staleSignals` | Count | 1 when the camera has produced nothing within `healthThresholds.staleSignalSecs`, 0 otherwise. A camera can be connected and stale. | | `reconnects` | Count | Sessions re-established in the interval. A camera's first connection is not a reconnect. | -`camera_captures` counts captures as they happen. It is emitted at the moment of each event, never sampled, -so a capture that starts and finishes between two collection intervals is still counted. +`camera_captures` counts the capture lifecycle. Each measure below is a `(Total, Interval)` counter pair, +accumulated on the capture hooks and drained every 30 seconds. The interval counter carries every event +since the last drain, so a capture that starts and finishes between two drains is still counted — the drain +is a reset, not a sample, and nothing is lost. Each row names the base measure; the emitted names are +`Total` and `Interval`. | Measure | Unit | Counted when | |---|---|---| @@ -32,6 +39,17 @@ so a capture that starts and finishes between two collection intervals is still | `thumbnailDropped` | Count | A thumbnail rendered but exceeded the byte ceiling a message may carry, so it was left out. The capture succeeds and is announced without it. | | `announcementRetriedWithoutPreview` | Count | An announcement carrying a thumbnail could not be published, so it was re-published without the thumbnail. The capture succeeds; the preview is shed so the result is still announced. | +`CameraCommand` is the operational command family, dimensioned by `instance`, `verb` (the `sb/*` command), +and `result` (`success` or `error`). Its cells are drained every 30 seconds, and a cell is created the +first time an `(instance, verb, result)` combination is seen, so its cardinality tracks the commands an +operator actually issues. + +| Measure | Unit | Meaning | +|---|---|---| +| `commandRequests` | Count | Commands handled for this `(instance, verb, result)` — a `(Total, Interval)` pair. | +| `commandErrors` | Count | Of those, the ones that failed — a `(Total, Interval)` pair (the `error` result cell). | +| `commandLatencyMs` | Milliseconds | Summed command-handling latency over the interval. | + `camera_queue` samples what the component is currently holding, every 30 seconds. These are levels rather than events, so there is nothing to miss between samples. @@ -48,8 +66,11 @@ than events, so there is nothing to miss between samples. | `camerasOnline` | Count | Cameras whose session is online. | | `camerasConfigured` | Count | Cameras in the current configuration. | -Neither metric carries a per-camera dimension. A 256-camera fleet would otherwise mint 256 metric streams -per measure. Per-camera queue depth is answered by `sb/queue-status`. +`camera_captures` and `camera_queue` are fleet-scoped and carry no per-camera dimension: a 256-camera fleet +would otherwise mint 256 metric streams per measure on the highest-frequency families. Per-camera queue +depth is answered by `sb/queue-status`, and per-camera reachability by the state keepalive below. +`CameraCommand` does carry the `instance` dimension, because commands are operator-frequency rather than +capture-frequency. ## Per-camera presence diff --git a/simulators/run-rtsp-native-coverage.ps1 b/simulators/run-rtsp-native-coverage.ps1 index 2c2a4e4..5fd3b52 100644 --- a/simulators/run-rtsp-native-coverage.ps1 +++ b/simulators/run-rtsp-native-coverage.ps1 @@ -48,20 +48,18 @@ $registryMount = "${registryVolume}:/usr/local/cargo/registry" $gitMount = "${gitVolume}:/usr/local/cargo/git" $artifactMount = "${coverageRoot}:/coverage-artifacts" -# THE LOCKFILE. `Cargo.lock` is untracked and gitignored (see the note in .github/workflows/ci.yml), so -# a clean checkout has none -- and every run below passes `--locked`, which REFUSES to create one: +# THE LOCKFILE. `Cargo.lock` is committed and git-sourced, but this harness cannot build against the +# in-tree copy directly: the source is mounted `:ro` on purpose, and the workspace it mounts carries +# the developer's gitignored `.cargo/config.toml` `[patch]`, which redirects `edgecommons` to a local +# path -- so a build here would need to REWRITE the committed git-sourced lock to a path source, and +# cargo cannot write it on the read-only mount (`Read-only file system (os error 30)`). # -# error: cannot create the lock file ... because --locked was passed to prevent this -# -# Dropping `--locked` does not rescue it. The source is mounted `:ro` on purpose, and cargo writes the -# lock next to the workspace `Cargo.toml` (CARGO_TARGET_DIR does not move it), so cargo would then die -# on `Read-only file system (os error 30)` instead. Two walls, one behind the other. -# -# So the lock is bind-mounted as a single FILE from outside the source tree, and generated once, in a -# container, by the prep run below. The source tree itself is never written to -- the immutability this -# script depends on is preserved exactly -- and `--locked` goes back to meaning what it says. Generating -# it in the container also keeps the lockfile version inside what the pinned toolchain can read, which a -# host-side `cargo generate-lockfile` would not guarantee. +# So a WRITABLE OVERLAY lock is bind-mounted as a single FILE from outside the source tree, masking the +# committed one, and generated once, in a container, by the prep run below (with the patch active, it +# becomes a path-sourced lock the `:ro` runs then use consistently under `--locked`). The source tree +# itself is never written to -- the immutability this script depends on is preserved exactly. +# Generating it in the container also keeps the lockfile version inside what the pinned toolchain can +# read, which a host-side `cargo generate-lockfile` would not guarantee. $lockRoot = Join-Path $coverageRoot 'lock' New-Item -ItemType Directory -Force -Path $lockRoot | Out-Null $lockFile = Join-Path $lockRoot 'Cargo.lock' diff --git a/src/actor.rs b/src/actor.rs index ec5b1a7..a8ced11 100644 --- a/src/actor.rs +++ b/src/actor.rs @@ -104,7 +104,7 @@ impl CaptureDispatcher for CameraActorHandle { fn reserve(&self, camera_id: &str) -> Result> { if camera_id != self.shared.instance { return Err(CameraError::rejected( - ErrorCode::UnknownInstance, + ErrorCode::NoSuchInstance, "capture was dispatched to the wrong camera actor", )); } @@ -146,7 +146,7 @@ impl CameraActor { let instance = instance.into(); if instance.is_empty() || max_queued_captures == 0 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "actor instance and capture queue capacity must be non-empty", )); } @@ -482,7 +482,7 @@ impl DispatchReservation for ActorDispatchReservation { fn commit(mut self: Box, descriptor: CaptureDescriptor) -> Result { if descriptor.instance() != self.shared.instance { return Err(CameraError::rejected( - ErrorCode::UnknownInstance, + ErrorCode::NoSuchInstance, "capture descriptor was dispatched to the wrong camera actor", )); } @@ -627,7 +627,7 @@ mod tests { ) -> Result<()> { assert_eq!(policy, OfflinePolicy::WaitUntilDeadline); Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "injected offline camera", )) } @@ -1750,7 +1750,7 @@ mod tests { let record = terminal(&harness.catalog, "cap-offline").await; assert_eq!(record.state, JobState::Failed); - assert_eq!(record.error_code.as_deref(), Some("CAMERA_UNAVAILABLE")); + assert_eq!(record.error_code.as_deref(), Some("DEVICE_UNAVAILABLE")); assert_eq!( record.terminal_result.as_ref().unwrap()["failure"]["stage"], "QUEUED" @@ -1783,7 +1783,7 @@ mod tests { .reserve("cam-b") .err() .expect("a capture for another camera must never be run by this one"); - assert_eq!(misrouted.code(), ErrorCode::UnknownInstance); + assert_eq!(misrouted.code(), ErrorCode::NoSuchInstance); assert_eq!( harness.handle.queued_captures(), 0, diff --git a/src/admission.rs b/src/admission.rs index 65c96b8..bd4cd31 100644 --- a/src/admission.rs +++ b/src/admission.rs @@ -204,13 +204,13 @@ impl CaptureAdmissionQueue { ) -> Result { if max_global_pending == 0 || max_per_camera == 0 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "capture queue bounds must be non-zero", )); } if aging_interval.is_zero() { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "capture queue aging interval must be non-zero", )); } @@ -254,7 +254,7 @@ impl CaptureAdmissionQueue { let camera_id = camera_id.into(); if camera_id.is_empty() { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "camera id must not be empty", )); } @@ -498,7 +498,7 @@ impl ControlLanes { pub fn new(ordinary_capacity: usize) -> Result { if !(1..=1_024).contains(&ordinary_capacity) { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "ordinary control capacity must be between 1 and 1024", )); } @@ -529,7 +529,7 @@ impl ControlLanes { pub fn push_safety_stop(&self, stop: SafetyStop) -> Result<()> { if !stop.pan && !stop.tilt && !stop.zoom { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "safety stop must select at least one axis", )); } @@ -667,7 +667,7 @@ impl ByteBudget { fn new(capacity: u64) -> Result { if capacity == 0 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "byte budget must be non-zero", )); } @@ -784,7 +784,7 @@ impl DiskBudget { fn new(output: &OutputConfig, probe: Arc) -> Result { if output.minimum_free_percent > 100 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "minimum free percent must not exceed 100", )); } @@ -948,7 +948,7 @@ impl AdmissionController { || limits.max_concurrent_writes == 0 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "admission semaphore limits must be non-zero", )); } @@ -960,7 +960,7 @@ impl AdmissionController { || group.max_concurrent_captures > limits.max_concurrent_captures { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "resource-group capacity must be between 1 and the global capture limit", )); } diff --git a/src/backend/genicam_aravis.rs b/src/backend/genicam_aravis.rs index 4fde4c1..c5a7479 100644 --- a/src/backend/genicam_aravis.rs +++ b/src/backend/genicam_aravis.rs @@ -106,7 +106,7 @@ impl CameraBackendFactory for GenicamAravisBackendFactory { async fn discover(&self, request: DiscoveryRequest) -> Result> { if request.max_results == 0 || request.max_results > 10_000 { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam discovery result bound is outside 1..=10000", ); } @@ -142,7 +142,7 @@ impl CameraBackendFactory for GenicamAravisBackendFactory { async fn connect(&self, request: ConnectRequest) -> Result> { let BackendConfig::GenicamAravis(config) = request.backend else { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam factory received another backend configuration", ); }; @@ -1294,7 +1294,7 @@ fn requested_i32(value: Option, current: i32, label: &'static str) -> Resul value.map_or(Ok(current), |value| { i32::try_from(value).map_err(|_| { CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, format!("GenICam {label} exceeds the native range"), ) }) @@ -1313,7 +1313,7 @@ fn validate_axis( .max(1); if !(minimum..=maximum).contains(&value) || (value - minimum) % increment != 0 { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, format!("GenICam {label} violates device bounds or increment"), ); } @@ -1329,7 +1329,7 @@ fn validate_float_feature(camera: &aravis::Camera, feature: &str, value: f64) -> .map_err(|_| backend_error("failed to read GenICam numeric increment"))?; if !value.is_finite() || !(minimum..=maximum).contains(&value) { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam numeric feature violates device bounds", ); } @@ -1337,7 +1337,7 @@ fn validate_float_feature(camera: &aravis::Camera, feature: &str, value: f64) -> let steps = (value - minimum) / increment; if (steps - steps.round()).abs() > 1e-6 { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam numeric feature violates its device increment", ); } @@ -1397,7 +1397,7 @@ fn apply_connection_settings( Some(requested) => { let requested = i64::try_from(requested).map_err(|_| { CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam packetDelayNs exceeds the native range", ) })?; @@ -1590,7 +1590,7 @@ fn resolve_and_open( return Ok((camera, identity, WireTransport::Usb3Vision, None)); } Err(CameraError::Rejected { - code: ErrorCode::CameraUnavailable, + code: ErrorCode::DeviceUnavailable, .. }) => {} Err(error) => return Err(error), @@ -1610,7 +1610,7 @@ fn resolve_and_open( fn required_interface(config: &GenicamBackendConfig) -> Result<&str> { config.interface.as_deref().ok_or_else(|| { CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GigE GenICam connection requires an explicit OS interface", ) }) @@ -1631,7 +1631,7 @@ fn open_scoped( matches.retain(|device| device.transport == transport && selector_matches(config, device)); if matches.len() > 1 { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam stable selector is ambiguous on its configured interface", ); } @@ -1841,7 +1841,7 @@ fn validate_native_field(value: &str, label: &'static str) -> Result<()> { fn validate_interfaces(interfaces: &[String]) -> Result<()> { if interfaces.len() > 64 { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam discovery accepts at most 64 eligible interfaces", ); } @@ -1850,7 +1850,7 @@ fn validate_interfaces(interfaces: &[String]) -> Result<()> { validate_interface(interface)?; if !unique.insert(interface) { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam discovery interfaces must be distinct", ); } @@ -1861,7 +1861,7 @@ fn validate_interfaces(interfaces: &[String]) -> Result<()> { fn validate_interface(interface: &str) -> Result<()> { if interface.is_empty() || interface.len() > 256 || interface.chars().any(char::is_control) { return rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "GenICam interface must be 1..256 UTF-8 bytes without controls", ); } @@ -1913,7 +1913,7 @@ fn timeout(stage: &'static str) -> Result { } fn unavailable(message: &'static str) -> CameraError { - CameraError::rejected(ErrorCode::CameraUnavailable, message) + CameraError::rejected(ErrorCode::DeviceUnavailable, message) } fn backend_error(message: impl Into) -> CameraError { @@ -2678,7 +2678,7 @@ mod tests { requested_i32(Some(u32::MAX), 7, "width") .expect_err("u32 outside native range") .code(), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); assert!(validate_axis(Ok((2, 10)), Ok(2), 6, "width").is_ok()); assert!(validate_axis(Ok((2, 10)), Ok(2), 7, "width").is_err()); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 7954ffe..1d80f93 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -619,7 +619,7 @@ mod tests { Err(error) => error, Ok(_) => panic!("static ONVIF construction is intentionally unavailable"), }; - assert_eq!(static_error.code(), crate::ErrorCode::InvalidRequest); + assert_eq!(static_error.code(), crate::ErrorCode::BadArgs); assert_eq!( BackendRuntimeContext::new(None, &LimitsConfig::default()) diff --git a/src/backend/onvif.rs b/src/backend/onvif.rs index 8bfd7e5..925dbaf 100644 --- a/src/backend/onvif.rs +++ b/src/backend/onvif.rs @@ -2120,7 +2120,7 @@ impl OnvifBackendFactory { } let selected_identity = selected_identity.ok_or_else(|| { CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "selected ONVIF endpoint reference was not discovered", ) })?; @@ -2362,7 +2362,7 @@ impl CameraBackendFactory for OnvifBackendFactory { async fn discover(&self, request: DiscoveryRequest) -> Result> { if request.timeout.is_zero() || request.max_results == 0 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "ONVIF discovery requires positive timeout and result bounds", )); } @@ -2777,7 +2777,7 @@ impl OnvifSession { fn ensure_open(&self) -> Result<()> { if self.closed { Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "ONVIF camera session is closed", )) } else { @@ -4891,7 +4891,7 @@ wkWsh7u3nnr9fXRpWsamYEAKGzNo0istMB6rD6cMzNfRZCMk4rXuokYWOw== session.close().await.expect("idempotent close"); assert_eq!( session.status().await.expect_err("closed status").code(), - ErrorCode::CameraUnavailable + ErrorCode::DeviceUnavailable ); } @@ -6665,8 +6665,8 @@ wkWsh7u3nnr9fXRpWsamYEAKGzNo0istMB6rD6cMzNfRZCMk4rXuokYWOw== ErrorCode::PtzTimeout ); assert_eq!( - map_ptz_error(CameraError::rejected(ErrorCode::InvalidRequest, "invalid")).code(), - ErrorCode::InvalidRequest + map_ptz_error(CameraError::rejected(ErrorCode::BadArgs, "invalid")).code(), + ErrorCode::BadArgs ); assert_eq!( onvif_capture_modes(true, false), diff --git a/src/backend/rtsp.rs b/src/backend/rtsp.rs index 9cfff81..383c854 100644 --- a/src/backend/rtsp.rs +++ b/src/backend/rtsp.rs @@ -3166,7 +3166,7 @@ impl RtspCaptureController { closed.stop(deadline).await; } return Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "RTSP stream closed before a fresh complete frame", )); } @@ -3209,7 +3209,7 @@ impl RtspCaptureController { WaitOutcome::Changed(result) => { if result.is_err() { return Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "RTSP worker stopped before producing a complete frame", )); } diff --git a/src/backend/rtsp_backend.rs b/src/backend/rtsp_backend.rs index d035f86..ca964fd 100644 --- a/src/backend/rtsp_backend.rs +++ b/src/backend/rtsp_backend.rs @@ -298,7 +298,7 @@ impl RtspSession { fn ensure_open(&self) -> Result<()> { if self.closed { Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "RTSP camera session is closed", )) } else { diff --git a/src/backend/sim.rs b/src/backend/sim.rs index 8207c05..7ae81ae 100644 --- a/src/backend/sim.rs +++ b/src/backend/sim.rs @@ -122,7 +122,7 @@ impl SimSession { fn ensure_open(&self) -> Result<()> { if self.closed { Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "sim camera session is closed", )) } else { @@ -258,7 +258,7 @@ impl CameraSession for SimSession { { self.closed = true; return Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "simulated disconnect threshold reached", )); } @@ -427,7 +427,7 @@ impl SimSession { } PtzRequest::GotoPreset(token) => { let (_, position) = self.presets.get(&token).ok_or_else(|| { - CameraError::rejected(ErrorCode::InvalidRequest, "unknown preset token") + CameraError::rejected(ErrorCode::BadArgs, "unknown preset token") })?; self.position = *position; self.moving = false; @@ -454,7 +454,7 @@ impl SimSession { } if self.presets.remove(&token).is_none() { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "unknown preset token", )); } @@ -692,18 +692,18 @@ mod tests { }; assert!(matches!( incomplete.capture(request()).await.unwrap_err().code(), - ErrorCode::BackendError | ErrorCode::CameraUnavailable + ErrorCode::BackendError | ErrorCode::DeviceUnavailable )); let mut disconnect = session(json!({"type":"sim","faults":{"disconnectAfterCaptures":0}})).await; assert_eq!( disconnect.capture(request()).await.unwrap_err().code(), - ErrorCode::CameraUnavailable + ErrorCode::DeviceUnavailable ); disconnect.close().await.unwrap(); assert_eq!( disconnect.capture(request()).await.unwrap_err().code(), - ErrorCode::CameraUnavailable + ErrorCode::DeviceUnavailable ); } @@ -902,7 +902,7 @@ mod tests { .await .expect_err("removed preset cannot be removed twice") .code(), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); camera.ptz(PtzRequest::Home).await.expect("home command"); assert!(matches!( diff --git a/src/backend/ws_discovery.rs b/src/backend/ws_discovery.rs index 550a2e2..98d6a47 100644 --- a/src/backend/ws_discovery.rs +++ b/src/backend/ws_discovery.rs @@ -149,7 +149,7 @@ impl WsDiscovery for ExplicitInterfaceWsDiscovery { ) -> Result> { if max_results == 0 || max_results > 10_000 { return Err(rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "WS-Discovery result bound must be in 1..=10000", )); } @@ -1397,7 +1397,7 @@ xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\">\ .await .expect_err("result limit is a public input bound") .code(), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } } diff --git a/src/commands.rs b/src/commands.rs index 9ae9fbb..f9567dc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -26,7 +26,7 @@ pub const MAX_CURSOR_BYTES: usize = 4_096; pub fn parse_closed(body: Value) -> Result { serde_json::from_value(body).map_err(|error| { CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, format!("request body does not match the closed schema: {error}"), ) }) @@ -371,6 +371,25 @@ impl ReconnectRequest { } } +/// `sb/pause` / `sb/resume` request. +/// +/// The lifecycle verbs are idempotent in-memory toggles, so they carry no durable `requestId`: there +/// is nothing to make exactly-once. Only the optional instance target, resolved by the single-camera +/// omission rule. +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PauseResumeRequest { + /// Optional camera target; omission selects the sole configured camera. + pub instance: Option, +} + +impl PauseResumeRequest { + /// Validates the optional instance token. + pub fn validate(&self) -> Result<()> { + validate_optional_token(self.instance.as_deref(), "instance") + } +} + /// Normalized non-negative PTZ speed. #[derive(Debug, Clone, Copy, Deserialize, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -732,7 +751,7 @@ fn ptz_range(message: impl Into) -> Result { fn invalid(message: impl Into) -> Result { Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, message.into(), )) } @@ -908,7 +927,7 @@ mod tests { let request: ListRequest = parse_closed(body).unwrap(); assert_eq!( rejection_code(request.validate()), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } @@ -921,7 +940,7 @@ mod tests { let request: DiscoverRequest = parse_closed(body).unwrap(); assert_eq!( rejection_code(request.validate()), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } @@ -932,7 +951,7 @@ mod tests { let request: StatusRequest = parse_closed(body).unwrap(); assert_eq!( rejection_code(request.validate()), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } @@ -944,7 +963,7 @@ mod tests { let request: CaptureRequest = parse_closed(body).unwrap(); assert_eq!( rejection_code(request.validate(8 * 1024)), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } } @@ -958,7 +977,7 @@ mod tests { .unwrap(); assert_eq!( rejection_code(too_small.validate(8, 8 * 1024)), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); let too_large: GroupCaptureRequest = parse_closed(json!({ @@ -990,7 +1009,7 @@ mod tests { let request: CaptureStatusRequest = parse_closed(body).unwrap(); assert_eq!( rejection_code(request.validate()), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } } @@ -1042,7 +1061,7 @@ mod tests { .unwrap(); assert_eq!( rejection_code(empty_stop.validate(2_000)), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); let home: PtzCommandRequest = parse_closed(json!({ "operation": "home", "requestId": "home-1", "instance": "camera-a" @@ -1053,7 +1072,7 @@ mod tests { parse_closed(json!({"operation": "status", "instance": "bad/token"})).unwrap(); assert_eq!( rejection_code(bad_status.validate(2_000)), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } @@ -1061,7 +1080,7 @@ mod tests { fn preset_operations_validate_pagination_tokens_and_names() { let list: PtzPresetsRequest = parse_closed(json!({"operation": "list", "limit": 0})).unwrap(); - assert_eq!(rejection_code(list.validate()), ErrorCode::InvalidRequest); + assert_eq!(rejection_code(list.validate()), ErrorCode::BadArgs); for body in [ json!({"operation": "goto", "instance": "camera-a", "requestId": "goto-1", "token": ""}), @@ -1071,7 +1090,7 @@ mod tests { let request: PtzPresetsRequest = parse_closed(body).unwrap(); assert_eq!( rejection_code(request.validate()), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } @@ -1108,7 +1127,7 @@ mod tests { parse_closed(serde_json::json!({"requestId": "r-3"})).unwrap(); assert_eq!( bare.validate().unwrap_err().code(), - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "omitting the camera must not silently mean the whole fleet" ); @@ -1118,11 +1137,30 @@ mod tests { .unwrap(); assert_eq!( contradictory.validate().unwrap_err().code(), - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "naming a camera and asking for the whole fleet is a contradiction, not a preference" ); } + /// The lifecycle verbs take an optional camera and nothing else -- a closed, `requestId`-free body. + #[test] + fn pause_resume_takes_an_optional_camera_and_rejects_anything_else() { + let fleet: PauseResumeRequest = parse_closed(json!({})).unwrap(); + assert!(fleet.validate().is_ok()); + assert_eq!(fleet.instance, None); + + let one: PauseResumeRequest = parse_closed(json!({"instance": "camera-a"})).unwrap(); + assert!(one.validate().is_ok()); + + let bad_token: PauseResumeRequest = parse_closed(json!({"instance": "bad/token"})).unwrap(); + assert_eq!(rejection_code(bad_token.validate()), ErrorCode::BadArgs); + + assert!( + parse_closed::(json!({"requestId": "r-1"})).is_err(), + "the schema is closed: a field that does nothing must be rejected, not ignored" + ); + } + /// The read-only sibling carries no requestId, because there is nothing to make idempotent. #[test] fn queue_status_takes_an_optional_camera_and_nothing_else() { diff --git a/src/dispatch.rs b/src/dispatch.rs index 0222aec..b3dca76 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -446,7 +446,7 @@ mod tests { .unwrap_or_else(|| panic!("{bound} of zero must not produce a usable queue")); assert_eq!( error.code(), - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "{bound} of zero is a configuration fault, not a runtime rejection" ); } diff --git a/src/encoding.rs b/src/encoding.rs index aea8adf..67e9faa 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -61,7 +61,7 @@ pub fn encode_to( } if !(1..=100).contains(&request.jpeg_quality) { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "JPEG quality must be between 1 and 100", )); } @@ -718,7 +718,7 @@ mod tests { ) .unwrap_err() .code(), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); } diff --git a/src/error.rs b/src/error.rs index 1384cb3..76e2b43 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,20 +11,23 @@ pub type Result = std::result::Result; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ErrorCode { - /// A target is required because more than one camera is configured. - InstanceRequired, - /// The requested camera is not configured. - UnknownInstance, + /// The request body, a field value, or the instance routing (a missing instance when more than + /// one camera is configured, or an otherwise malformed argument) is invalid. Standardized + /// southbound routing/argument code (SOUTHBOUND.md §2.2). + BadArgs, + /// The requested camera is not configured. Standardized southbound routing code. + NoSuchInstance, /// The requested camera is disabled. CameraDisabled, - /// The camera cannot currently serve the request. - CameraUnavailable, + /// The camera cannot currently serve the request (session down / unreachable). Standardized + /// southbound availability code. + DeviceUnavailable, + /// The camera instance is paused (`sb/pause`) and refuses new capture work until resumed. + InstancePaused, /// The capture/PTZ interlock rejected the operation. CameraMoving, /// The backend or camera lacks the requested capability. UnsupportedCapability, - /// The request body or a value is invalid. - InvalidRequest, /// The named capture profile is not configured. UnknownCaptureProfile, /// A bounded camera/control queue is full. @@ -70,13 +73,13 @@ impl ErrorCode { #[must_use] pub const fn as_str(self) -> &'static str { match self { - Self::InstanceRequired => "INSTANCE_REQUIRED", - Self::UnknownInstance => "UNKNOWN_INSTANCE", + Self::BadArgs => "BAD_ARGS", + Self::NoSuchInstance => "NO_SUCH_INSTANCE", Self::CameraDisabled => "CAMERA_DISABLED", - Self::CameraUnavailable => "CAMERA_UNAVAILABLE", + Self::DeviceUnavailable => "DEVICE_UNAVAILABLE", + Self::InstancePaused => "INSTANCE_PAUSED", Self::CameraMoving => "CAMERA_MOVING", Self::UnsupportedCapability => "UNSUPPORTED_CAPABILITY", - Self::InvalidRequest => "INVALID_REQUEST", Self::UnknownCaptureProfile => "UNKNOWN_CAPTURE_PROFILE", Self::QueueFull => "QUEUE_FULL", Self::GroupTooLarge => "GROUP_TOO_LARGE", @@ -203,7 +206,7 @@ impl CameraError { Self::Rejected { code, .. } => *code, Self::Storage(_) | Self::Io(_) => ErrorCode::PersistenceFailed, Self::Backend { .. } => ErrorCode::BackendError, - Self::Config { .. } | Self::Json(_) => ErrorCode::InvalidRequest, + Self::Config { .. } | Self::Json(_) => ErrorCode::BadArgs, Self::Catalog(_) | Self::Messaging(_) | Self::Sqlite(_) => ErrorCode::BackendError, } } @@ -383,7 +386,7 @@ mod tests { message: "bad".to_string() } .code(), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); assert_eq!( CameraError::Storage("disk".to_string()).code(), @@ -398,13 +401,13 @@ mod tests { #[test] fn every_public_error_code_has_its_contract_wire_spelling() { let cases = [ - (ErrorCode::InstanceRequired, "INSTANCE_REQUIRED"), - (ErrorCode::UnknownInstance, "UNKNOWN_INSTANCE"), + (ErrorCode::BadArgs, "BAD_ARGS"), + (ErrorCode::NoSuchInstance, "NO_SUCH_INSTANCE"), (ErrorCode::CameraDisabled, "CAMERA_DISABLED"), - (ErrorCode::CameraUnavailable, "CAMERA_UNAVAILABLE"), + (ErrorCode::DeviceUnavailable, "DEVICE_UNAVAILABLE"), + (ErrorCode::InstancePaused, "INSTANCE_PAUSED"), (ErrorCode::CameraMoving, "CAMERA_MOVING"), (ErrorCode::UnsupportedCapability, "UNSUPPORTED_CAPABILITY"), - (ErrorCode::InvalidRequest, "INVALID_REQUEST"), (ErrorCode::UnknownCaptureProfile, "UNKNOWN_CAPTURE_PROFILE"), (ErrorCode::QueueFull, "QUEUE_FULL"), (ErrorCode::GroupTooLarge, "GROUP_TOO_LARGE"), @@ -463,7 +466,7 @@ mod tests { ), ( CameraError::Json(json_error), - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "JSON error", ), ( diff --git a/src/idempotency.rs b/src/idempotency.rs index 97ab5b1..79388ce 100644 --- a/src/idempotency.rs +++ b/src/idempotency.rs @@ -49,7 +49,7 @@ impl std::fmt::Debug for RequestHash { pub fn validate_request_id(request_id: &str) -> Result<()> { if request_id.is_empty() || request_id.len() > 256 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "requestId must contain 1 to 256 UTF-8 bytes", )); } @@ -61,7 +61,7 @@ pub fn validate_request_id(request_id: &str) -> Result<()> { ) }) { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "requestId must not contain control or format characters", )); } @@ -76,7 +76,7 @@ pub fn validate_request_id(request_id: &str) -> Result<()> { pub fn canonical_request_hash(request: &Value, sort_instances: bool) -> Result { if !request.is_object() { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "command arguments must be a JSON object", )); } @@ -95,7 +95,7 @@ pub fn canonical_request_hash(request: &Value, sort_instances: bool) -> Result Result> { if !request.is_object() { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "command arguments must be a JSON object", )); } @@ -181,13 +181,13 @@ fn encode_array(values: &[Value], output: &mut Vec) -> Result<()> { fn encode_sorted_instances(value: &Value, output: &mut Vec) -> Result<()> { let values = value.as_array().ok_or_else(|| { - CameraError::rejected(ErrorCode::InvalidRequest, "instances must be an array") + CameraError::rejected(ErrorCode::BadArgs, "instances must be an array") })?; let mut instances = Vec::with_capacity(values.len()); for value in values { let instance = value.as_str().ok_or_else(|| { CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "instances entries must be strings", ) })?; @@ -196,7 +196,7 @@ fn encode_sorted_instances(value: &Value, output: &mut Vec) -> Result<()> { instances.sort_unstable(); if instances.windows(2).any(|pair| pair[0] == pair[1]) { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "instances must not contain duplicates", )); } @@ -323,7 +323,7 @@ mod tests { ] { let error = canonical_request_hash(&invalid, true) .expect_err("invalid group targets must not acquire a durable ledger key"); - assert_eq!(error.code(), ErrorCode::InvalidRequest); + assert_eq!(error.code(), ErrorCode::BadArgs); } assert_eq!( diff --git a/src/jobs.rs b/src/jobs.rs index 562ca6e..9e3f1d6 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -692,7 +692,7 @@ impl JobEngine { }); } return Err(CameraError::rejected( - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, "capture is owned by recovery or an unavailable actor", )); }; @@ -2294,7 +2294,7 @@ fn validate_submission(submission: &JobSubmission) -> Result<()> { || spec.profile.maximum_frame_bytes == 0 { return Err(CameraError::rejected( - ErrorCode::InvalidRequest, + ErrorCode::BadArgs, "immutable capture snapshot contains an empty/zero required field", )); } @@ -2435,7 +2435,7 @@ fn cancelled_error(stage: &'static str) -> CameraError { fn is_retriable(code: ErrorCode) -> bool { matches!( code, - ErrorCode::CameraUnavailable + ErrorCode::DeviceUnavailable | ErrorCode::QueueFull | ErrorCode::ResourceLimit | ErrorCode::CaptureTimeout @@ -3341,7 +3341,7 @@ mod tests { empty.spec.capture_id.clear(); assert_eq!( validate_submission(&empty).unwrap_err().code(), - ErrorCode::InvalidRequest + ErrorCode::BadArgs ); let mut effective_profile = submission(); @@ -3456,7 +3456,7 @@ mod tests { ); for code in [ - ErrorCode::CameraUnavailable, + ErrorCode::DeviceUnavailable, ErrorCode::QueueFull, ErrorCode::ResourceLimit, ErrorCode::CaptureTimeout, @@ -3466,7 +3466,7 @@ mod tests { ] { assert!(is_retriable(code), "{code}"); } - assert!(!is_retriable(ErrorCode::InvalidRequest)); + assert!(!is_retriable(ErrorCode::BadArgs)); assert_eq!(bounded_detail("one\ntwo\u{7f}".to_string()), "one two "); let bounded = bounded_detail("é".repeat(600)); diff --git a/src/messages.rs b/src/messages.rs index fc1593c..f59d11e 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -535,7 +535,7 @@ fn invalid(message: impl Into) -> Result { } fn invalid_error(message: impl Into) -> CameraError { - CameraError::rejected(ErrorCode::InvalidRequest, message.into()) + CameraError::rejected(ErrorCode::BadArgs, message.into()) } #[cfg(test)] @@ -638,7 +638,7 @@ mod tests { let mut schema = base_body(); schema.schema_version = 2; let error = TerminalMessage::new(TerminalKind::Cancelled, schema).unwrap_err(); - assert_eq!(error.code(), ErrorCode::InvalidRequest); + assert_eq!(error.code(), ErrorCode::BadArgs); for field in [ "event_id", @@ -657,7 +657,7 @@ mod tests { _ => unreachable!("test field list is exhaustive"), } let error = TerminalMessage::new(TerminalKind::Cancelled, body).unwrap_err(); - assert_eq!(error.code(), ErrorCode::InvalidRequest, "{field}"); + assert_eq!(error.code(), ErrorCode::BadArgs, "{field}"); } } diff --git a/src/observability.rs b/src/observability.rs index 0af3375..c5f2932 100644 --- a/src/observability.rs +++ b/src/observability.rs @@ -316,37 +316,109 @@ impl FleetHealth { } } +/// A `Total` (monotonic since start) + `Interval` (since the previous emit; reset on +/// emit) counter pair — the operational-family convention every EdgeCommons adapter follows +/// (SOUTHBOUND.md §5), so a fleet dashboard reads every adapter the same way. +#[derive(Debug, Default, Clone, Copy)] +struct Pair { + total: f64, + interval: f64, +} + +impl Pair { + fn add(&mut self, value: f64) { + self.total += value; + self.interval += value; + } + + /// Writes both measures into `out` and RESETS the interval — the emit convention. + fn drain_into(&self, out: &mut std::collections::HashMap, prefix: &str) { + out.insert(format!("{prefix}Total"), self.total); + out.insert(format!("{prefix}Interval"), self.interval); + } + + fn reset_interval(&mut self) { + self.interval = 0.0; + } +} + +/// The `(verb, result)` command counters for one `(instance, verb, result)` cell of `CameraCommand`. +#[derive(Debug, Default)] +struct CmdCounters { + requests: Pair, + errors: Pair, + latency_ms: f64, +} + +/// The `camera_captures` counter set, one [`Pair`] per capture-lifecycle measure. Keyed by the +/// `&'static str` measure name the job hooks already pass to [`CaptureMetrics::count`]. +type CaptureCounters = std::collections::BTreeMap<&'static str, Pair>; + +/// A `(instance, verb, result)` key into the `CameraCommand` family. +type CommandKey = (String, &'static str, &'static str); + /// The component's metric surface. /// -/// The camera adapter emitted no metrics at all: there was not one call site for `metrics()`, -/// `MetricBuilder`, or `MetricService` anywhere in the crate. Every number an operator would want -- -/// how much work is queued, how much is in flight, how much succeeded, how much failed -- existed -/// somewhere inside the process and left no trace outside it. A capture that failed was a log line. +/// Three families, following the SOUTHBOUND.md §5 operational-family pattern: /// -/// Two metrics, and the split is deliberate: +/// * `camera_captures` COUNTS the capture lifecycle on the job hooks the runtime fires — every +/// measure is a `(Total, Interval)` counter pair, accumulated in-process and drained on the metric +/// sampler's tick. The interval counter is what preserves the "every event must be seen" guarantee +/// the immediate-emit design was reaching for: a capture that succeeded and one that failed between +/// two drains both land in the interval, so nothing is lost — the drain is a reset, not a sample. +/// * `camera_queue` SAMPLES what the component is holding right now — a level, not an event, so its +/// measures are single gauges (there is nothing to miss between samples). +/// * `CameraCommand` counts the `sb/*` command surface, dimensioned `instance` × `verb` × `result`, +/// with `(Total, Interval)` request/error pairs plus a latency sum — the reference command family. /// -/// * `camera_captures` is COUNTED as it happens, on the job hooks the runtime already fires. It -/// answers "what has this component done", and it must not be sampled: a capture that succeeded -/// and a capture that failed between two samples both have to be seen. -/// * `camera_queue` is SAMPLED on a timer. It answers "what is this component holding right now", -/// which is a level, not an event -- there is nothing to miss between samples. -/// -/// Deliberately free of per-camera dimensions. A 256-camera fleet would otherwise mint 256 metric -/// streams per measure, which is how a metrics bill and a Prometheus server both die. Per-camera -/// state is answered by `sb/queue-status` and by the per-instance connectivity the heartbeat -/// publishes. +/// **Recorded cardinality decision (DESIGN.md).** `camera_captures` / `camera_queue` stay +/// **fleet-scoped** (no per-instance dimension): a 256-camera fleet would otherwise mint 256 streams +/// PER MEASURE on the highest-frequency families, which is how a metrics bill and a Prometheus server +/// both die. Per-camera capture state is answered by `sb/queue-status` and the per-instance +/// connectivity the heartbeat publishes. `CameraCommand` DOES carry the `instance` dimension the +/// family pattern prescribes, because commands are operator-frequency, not capture-frequency; its +/// `(instance, verb, result)` cells are defined **lazily on first use** rather than pre-populated, so +/// its cardinality tracks what an operator actually commands rather than 256 × 16 × 2 cold streams. pub struct CaptureMetrics { metrics: Arc, /// Serializes the define-then-emit pair that `southbound_health` needs. See [`Self::emit_health`]. health: tokio::sync::Mutex<()>, + /// Accumulated `camera_captures` counter pairs, drained on the metric sampler's tick. + captures: std::sync::Mutex, + /// Accumulated `CameraCommand` cells, drained on the metric sampler's tick. + commands: std::sync::Mutex>, } -/// Counted as captures move: emitted at the moment, never sampled. +/// Capture-lifecycle counters, drained as `(Total, Interval)` pairs on the sampler tick. pub const CAPTURE_METRIC: &str = "camera_captures"; /// Sampled levels: what the component is holding right now. pub const QUEUE_METRIC: &str = "camera_queue"; /// The standard per-instance southbound metric every adapter in the ecosystem emits. pub const HEALTH_METRIC: &str = "southbound_health"; +/// The operational command family: `sb/*` request/error counts and latency, by `instance`/`verb`/`result`. +pub const COMMAND_METRIC: &str = "CameraCommand"; +/// The `result` dimension value for a command that succeeded. +pub const RESULT_SUCCESS: &str = "success"; +/// The `result` dimension value for a command that failed. +pub const RESULT_ERROR: &str = "error"; + +/// Every `camera_captures` measure. Each becomes a `Total` + `Interval` pair. +/// +/// The set is the parity anchor: `define_captures` and the drain both read it, so a measure the job +/// hooks count but this list omits fails to define, and one this list carries but nothing counts +/// simply reports zero — never a silent drop. +pub const CAPTURE_MEASURES: [&str; 10] = [ + "queued", + "started", + "succeeded", + "failed", + "cancelled", + "interrupted", + ANNOUNCEMENT_FAILED_MEASURE, + THUMBNAIL_FAILED_MEASURE, + THUMBNAIL_DROPPED_MEASURE, + ANNOUNCEMENT_RETRIED_WITHOUT_PREVIEW_MEASURE, +]; /// Terminal results that are durable but were never announced. /// /// The announcement is best-effort, so a broker that is down costs announcements and nothing else. @@ -378,23 +450,22 @@ pub const THUMBNAIL_DROPPED_MEASURE: &str = "thumbnailDropped"; pub const ANNOUNCEMENT_RETRIED_WITHOUT_PREVIEW_MEASURE: &str = "announcementRetriedWithoutPreview"; impl CaptureMetrics { - /// Defines both metrics against the component's metric service. + /// Defines the fleet-scoped metrics against the component's metric service. + /// + /// `camera_captures` and `camera_queue` are defined once (no dimensions vary); `CameraCommand` + /// carries the `instance`/`verb`/`result` dimensions, so its cells are defined lazily at emit time + /// like `southbound_health`. #[must_use] pub fn new(metrics: Arc) -> Self { - metrics.define_metric( - edgecommons::metrics::MetricBuilder::create(CAPTURE_METRIC) - .add_measure("queued", "Count", 60) - .add_measure("started", "Count", 60) - .add_measure("succeeded", "Count", 60) - .add_measure("failed", "Count", 60) - .add_measure("cancelled", "Count", 60) - .add_measure("interrupted", "Count", 60) - .add_measure(ANNOUNCEMENT_FAILED_MEASURE, "Count", 60) - .add_measure(THUMBNAIL_FAILED_MEASURE, "Count", 60) - .add_measure(THUMBNAIL_DROPPED_MEASURE, "Count", 60) - .add_measure(ANNOUNCEMENT_RETRIED_WITHOUT_PREVIEW_MEASURE, "Count", 60) - .build(), - ); + // camera_captures: every measure is a Total/Interval counter pair (SOUTHBOUND.md §5 pattern). + let mut captures = edgecommons::metrics::MetricBuilder::create(CAPTURE_METRIC); + for measure in CAPTURE_MEASURES { + captures = captures + .add_measure(format!("{measure}Total"), "Count", 60) + .add_measure(format!("{measure}Interval"), "Count", 60); + } + metrics.define_metric(captures.build()); + // camera_queue: sampled levels — single gauges, nothing to pair. metrics.define_metric( edgecommons::metrics::MetricBuilder::create(QUEUE_METRIC) .add_measure("dispatchQueued", "Count", 60) @@ -412,6 +483,8 @@ impl CaptureMetrics { Self { metrics, health: tokio::sync::Mutex::new(()), + captures: std::sync::Mutex::new(CaptureCounters::new()), + commands: std::sync::Mutex::new(std::collections::BTreeMap::new()), } } @@ -475,13 +548,125 @@ impl CaptureMetrics { } } - /// Counts one capture event. Best effort: a metric target that is unhappy must never be able to - /// fail a capture, so this reports and moves on. + /// The current monotonic total accumulated for a `camera_captures` measure, without emitting. + /// + /// A test seam only: it lets a test observe that a capture has been counted without draining + /// (which would emit and reset the interval), so the count-landed race can be polled cleanly. + #[cfg(test)] + #[must_use] + pub fn captured_total(&self, measure: &str) -> f64 { + self.captures + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .get(measure) + .map_or(0.0, |pair| pair.total) + } + + /// Records one `camera_captures` event into its `(Total, Interval)` accumulator. + /// + /// No longer emits at the call site: the interval counter accumulates every event since the last + /// drain, so nothing is lost between the sampler's ticks — a success and a failure in the same + /// window both land, which is the guarantee the old immediate-emit design was reaching for. Kept + /// `async` (and infallible) so the job-hook call sites are unchanged. + #[allow(clippy::unused_async)] pub async fn count(&self, measure: &'static str) { - let mut values = std::collections::HashMap::with_capacity(1); - values.insert(measure.to_owned(), 1.0); + self.captures + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .entry(measure) + .or_default() + .add(1.0); + } + + /// Records one `sb/*` command outcome into the `CameraCommand` family cell for its + /// `(instance, verb, result)`. Lazily creates the cell on first sight — see the cardinality note + /// on [`CaptureMetrics`]. + pub fn record_command(&self, instance: &str, verb: &'static str, ok: bool, latency_ms: u64) { + let result = if ok { RESULT_SUCCESS } else { RESULT_ERROR }; + let mut commands = self + .commands + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let cell = commands.entry((instance.to_owned(), verb, result)).or_default(); + cell.requests.add(1.0); + cell.latency_ms += latency_ms as f64; + if !ok { + cell.errors.add(1.0); + } + } + + /// Drains the `camera_captures` counters to the metric target and resets the intervals. + /// + /// Best effort: a metric target that is unhappy must never disturb the component, so this reports + /// and moves on. Called on the metric sampler's tick. + pub async fn drain_captures(&self) { + let values = { + let mut captures = self + .captures + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + // Always emit the full measure set (zeros for what has not happened), so a dashboard sees + // a stable schema rather than measures that blink in and out of existence. + let mut values = std::collections::HashMap::with_capacity(CAPTURE_MEASURES.len() * 2); + for measure in CAPTURE_MEASURES { + let pair = captures.entry(measure).or_default(); + pair.drain_into(&mut values, measure); + pair.reset_interval(); + } + values + }; if let Err(error) = self.metrics.emit_metric(CAPTURE_METRIC, values).await { - tracing::warn!(measure, error = %error, "camera capture metric could not be emitted"); + tracing::warn!(error = %error, "camera capture metrics could not be emitted"); + } + } + + /// Drains every accumulated `CameraCommand` cell to the metric target and resets the intervals. + /// + /// Each `(instance, verb, result)` cell is defined immediately before it is emitted — the same + /// define-then-emit pattern `southbound_health` uses, and for the same reason: the core metric API + /// carries dimensions on the definition. Called on the metric sampler's tick. + pub async fn drain_commands(&self) { + let cells: Vec<(CommandKey, std::collections::HashMap)> = { + let mut commands = self + .commands + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + commands + .iter_mut() + .map(|(key, cell)| { + let mut values = std::collections::HashMap::with_capacity(5); + cell.requests.drain_into(&mut values, "commandRequests"); + cell.errors.drain_into(&mut values, "commandErrors"); + values.insert("commandLatencyMs".to_owned(), cell.latency_ms); + cell.requests.reset_interval(); + cell.errors.reset_interval(); + cell.latency_ms = 0.0; + (key.clone(), values) + }) + .collect() + }; + let _serialized = self.health.lock().await; + for ((instance, verb, result), values) in cells { + let metric = edgecommons::metrics::MetricBuilder::create(COMMAND_METRIC) + .add_dimension("instance", &instance) + .add_dimension("verb", verb) + .add_dimension("result", result) + .add_measure("commandRequestsTotal", "Count", 60) + .add_measure("commandRequestsInterval", "Count", 60) + .add_measure("commandErrorsTotal", "Count", 60) + .add_measure("commandErrorsInterval", "Count", 60) + .add_measure("commandLatencyMs", "Milliseconds", 60) + .build(); + self.metrics.define_metric(metric); + if let Err(error) = self.metrics.emit_metric(COMMAND_METRIC, values).await { + tracing::warn!( + instance, + verb, + result, + error = %error, + "camera command metric could not be emitted" + ); + } } } @@ -983,4 +1168,137 @@ mod tests { assert_eq!(sample.reconnects, u64::MAX); assert_eq!(duration_millis(Duration::MAX), u64::MAX); } + + // --- the operational-family pattern: camera_captures pairs + CameraCommand ----------------------- + + /// A counter Pair is monotonic in total and resets its interval on drain. + #[test] + fn a_counter_pair_totals_monotonically_and_its_interval_resets() { + let mut pair = Pair::default(); + pair.add(3.0); + let mut out = std::collections::HashMap::new(); + pair.drain_into(&mut out, "x"); + assert_eq!(out["xTotal"], 3.0); + assert_eq!(out["xInterval"], 3.0); + pair.reset_interval(); + + pair.add(2.0); + let mut out2 = std::collections::HashMap::new(); + pair.drain_into(&mut out2, "x"); + assert_eq!(out2["xTotal"], 5.0, "the total is monotonic across drains"); + assert_eq!(out2["xInterval"], 2.0, "the interval carries only what accrued since the reset"); + } + + /// `count` accumulates on the job hooks; a drain emits every measure as a Total/Interval pair and + /// resets the intervals -- so a second drain with no new events reports Total unchanged, Interval 0. + #[tokio::test] + async fn camera_captures_accumulates_and_drains_as_total_interval_pairs() { + let target = Arc::new(RecordingMetrics::default()); + let metrics = + CaptureMetrics::new(Arc::clone(&target) as Arc); + + metrics.count("succeeded").await; + metrics.count("succeeded").await; + metrics.count("failed").await; + assert_eq!(metrics.captured_total("succeeded"), 2.0, "counts accumulate without emitting"); + assert!( + target.emitted.lock().unwrap().is_empty(), + "counting must not emit -- the pattern drains on the sampler tick" + ); + + metrics.drain_captures().await; + let (_, _, first) = target.last_emission(); + assert_eq!(first["succeededTotal"], 2.0); + assert_eq!(first["succeededInterval"], 2.0); + assert_eq!(first["failedTotal"], 1.0); + assert_eq!(first["queuedTotal"], 0.0, "the full schema is always emitted, zeros included"); + + metrics.drain_captures().await; + let (_, _, second) = target.last_emission(); + assert_eq!(second["succeededTotal"], 2.0, "total is monotonic across drains"); + assert_eq!(second["succeededInterval"], 0.0, "the interval reset on the previous drain"); + } + + /// A command outcome lands in its `(instance, verb, result)` cell; a drain defines and emits each + /// cell, a failure counts BOTH a request and an error, and the definitions carry the exact dims. + #[tokio::test] + async fn camera_command_records_by_instance_verb_result_and_drains_with_those_dimensions() { + use edgecommons::metrics::MetricService as _; + + let target = Arc::new(RecordingMetrics::default()); + let metrics = + CaptureMetrics::new(Arc::clone(&target) as Arc); + + metrics.record_command("camera-a", "sb/status", true, 4); + metrics.record_command("camera-a", "sb/status", false, 6); + metrics.drain_commands().await; + + // The success cell (errors = 0) and the error cell (a request AND an error, latency summed). + let cells: Vec> = target + .emitted + .lock() + .unwrap() + .iter() + .filter(|(name, _, _)| name == COMMAND_METRIC) + .map(|(_, _, values)| values.clone()) + .collect(); + assert_eq!(cells.len(), 2, "one cell per (verb, result) touched"); + let success = cells + .iter() + .find(|v| v["commandErrorsTotal"] == 0.0) + .expect("a success cell"); + assert_eq!(success["commandRequestsTotal"], 1.0); + let error = cells + .iter() + .find(|v| v["commandErrorsTotal"] == 1.0) + .expect("an error cell"); + assert_eq!(error["commandRequestsTotal"], 1.0, "a failure counts a request AND an error"); + assert_eq!(error["commandLatencyMs"], 6.0); + + // The definitions carry the low-cardinality dimensions the family pattern prescribes. + let defined_dims: Vec> = target + .defined + .lock() + .unwrap() + .iter() + .filter(|metric| metric.get_name() == COMMAND_METRIC) + .map(|metric| metric.get_dimensions().clone()) + .collect(); + assert!(!defined_dims.is_empty(), "each cell defines before it emits"); + for dims in &defined_dims { + assert_eq!(dims.get("instance").map(String::as_str), Some("camera-a")); + assert_eq!(dims.get("verb").map(String::as_str), Some("sb/status")); + assert!(dims.contains_key("result"), "the result dimension is present"); + } + assert!(target.is_metric_defined(COMMAND_METRIC)); + } + + /// A metric target that refuses every emission must never take a drain down with it — the drain is + /// best-effort, on a timer, and a briefly-unavailable backend must not stall the component. + #[tokio::test] + async fn a_drain_survives_a_metric_target_that_refuses_every_emission() { + let target = Arc::new(RecordingMetrics { + refuse: true, + ..RecordingMetrics::default() + }); + let metrics = + CaptureMetrics::new(Arc::clone(&target) as Arc); + + metrics.count("succeeded").await; + metrics.record_command("camera-a", "sb/status", false, 3); + + // Neither drain may panic or propagate, and both must still attempt the (failing) emission. + metrics.drain_captures().await; + metrics.drain_commands().await; + + let attempted: Vec = target + .emitted + .lock() + .unwrap() + .iter() + .map(|(name, _, _)| name.clone()) + .collect(); + assert!(attempted.iter().any(|n| n == CAPTURE_METRIC), "the capture drain still tried to emit"); + assert!(attempted.iter().any(|n| n == COMMAND_METRIC), "the command drain still tried to emit"); + } } diff --git a/src/registry.rs b/src/registry.rs index 5bbbbea..bca7b46 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -151,7 +151,7 @@ impl CameraRegistry { .map(|entry| entry.sender.borrow().clone()) .ok_or_else(|| { CameraError::rejected( - ErrorCode::UnknownInstance, + ErrorCode::NoSuchInstance, format!("camera instance '{instance}' is not configured"), ) }) @@ -164,12 +164,43 @@ impl CameraRegistry { .map(|entry| entry.config.clone()) .ok_or_else(|| { CameraError::rejected( - ErrorCode::UnknownInstance, + ErrorCode::NoSuchInstance, format!("camera instance '{instance}' is not configured"), ) }) } + /// Resolves the single-camera omission rule to a configured instance, WITHOUT enforcing enabled + /// state. + /// + /// The lifecycle verbs (`sb/pause` / `sb/resume`) address a camera that need not be enabled -- + /// pausing a camera an operator is about to disable, or resuming one after re-enabling it, are + /// both legitimate. So this is the routing-only sibling of [`Self::resolve_actuation_instance`]: + /// same D-EIP-13 omission rule and the same `BAD_ARGS` / `NO_SUCH_INSTANCE` codes, but no + /// `CAMERA_DISABLED` gate. + pub fn resolve_instance(&self, requested: Option<&str>) -> Result { + let entries = self.read_entries()?; + let instance = match requested { + Some(instance) => instance.to_string(), + None if entries.len() == 1 => entries.keys().next().cloned().ok_or_else(|| { + CameraError::Catalog("registry changed during lookup".to_string()) + })?, + None => { + return Err(CameraError::rejected( + ErrorCode::BadArgs, + "instance is required when more than one camera is configured", + )); + } + }; + if !entries.contains_key(&instance) { + return Err(CameraError::rejected( + ErrorCode::NoSuchInstance, + format!("camera instance '{instance}' is not configured"), + )); + } + Ok(instance) + } + /// Resolves the single-camera omission rule and enforces enabled state for actuation. pub fn resolve_actuation_instance(&self, requested: Option<&str>) -> Result { let entries = self.read_entries()?; @@ -180,14 +211,14 @@ impl CameraRegistry { })?, None => { return Err(CameraError::rejected( - ErrorCode::InstanceRequired, + ErrorCode::BadArgs, "instance is required when more than one camera is configured", )); } }; let entry = entries.get(&instance).ok_or_else(|| { CameraError::rejected( - ErrorCode::UnknownInstance, + ErrorCode::NoSuchInstance, format!("camera instance '{instance}' is not configured"), ) })?; @@ -207,7 +238,7 @@ impl CameraRegistry { .map(|entry| entry.sender.subscribe()) .ok_or_else(|| { CameraError::rejected( - ErrorCode::UnknownInstance, + ErrorCode::NoSuchInstance, format!("camera instance '{instance}' is not configured"), ) }) @@ -436,7 +467,7 @@ mod tests { let two = CameraRegistry::new(&config(true, false)).unwrap(); assert_eq!( two.resolve_actuation_instance(None).unwrap_err().code(), - ErrorCode::InstanceRequired + ErrorCode::BadArgs ); assert_eq!( two.resolve_actuation_instance(Some("camera-b")) @@ -450,6 +481,30 @@ mod tests { ); } + /// The routing-only resolver applies the same omission rule and codes as actuation, but does NOT + /// gate on enabled state -- a disabled camera can still be paused or resumed. + #[test] + fn resolve_instance_routes_without_the_enabled_gate() { + let one = CameraRegistry::new(&config(false, true)).unwrap(); + assert_eq!(one.resolve_instance(None).unwrap(), "camera-a"); + + let two = CameraRegistry::new(&config(true, false)).unwrap(); + assert_eq!( + two.resolve_instance(None).unwrap_err().code(), + ErrorCode::BadArgs, + "a missing instance with two cameras is BAD_ARGS" + ); + assert_eq!( + two.resolve_instance(Some("missing")).unwrap_err().code(), + ErrorCode::NoSuchInstance + ); + assert_eq!( + two.resolve_instance(Some("camera-b")).unwrap(), + "camera-b", + "a DISABLED camera still resolves -- pause/resume do not require it to be enabled" + ); + } + #[test] fn stale_generation_cannot_replace_new_capabilities() { let registry = CameraRegistry::new(&config(false, true)).unwrap(); @@ -567,15 +622,15 @@ mod tests { .resolve_actuation_instance(Some("missing")) .unwrap_err() .code(), - ErrorCode::UnknownInstance + ErrorCode::NoSuchInstance ); assert_eq!( registry.camera_config("missing").unwrap_err().code(), - ErrorCode::UnknownInstance + ErrorCode::NoSuchInstance ); assert_eq!( registry.subscribe("missing").unwrap_err().code(), - ErrorCode::UnknownInstance + ErrorCode::NoSuchInstance ); assert!( !registry diff --git a/src/runtime.rs b/src/runtime.rs index 460a27f..6c69506 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -454,7 +454,7 @@ impl CursorStore { let (cameras, unconfigured) = initial.ok_or_else(cursor_rejected)?; if cameras.len().saturating_add(unconfigured.len()) > MAX_RETAINED_SNAPSHOT_VALUES { return Err(crate::CameraError::rejected( - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "result exceeds the retained snapshot bound", )); } @@ -522,7 +522,7 @@ impl CursorStore { let values = initial.ok_or_else(cursor_rejected)?; if values.len() > MAX_RETAINED_SNAPSHOT_VALUES { return Err(crate::CameraError::rejected( - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "result exceeds the retained snapshot bound", )); } @@ -631,7 +631,7 @@ fn cursor_query_hash(query: &serde_json::Value) -> Result { fn cursor_rejected() -> crate::CameraError { crate::CameraError::rejected( - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "cursor is unknown, expired, or does not match this query", ) } @@ -872,6 +872,14 @@ pub struct CameraRuntime { cursors: CursorStore, reload_gate: tokio::sync::Mutex<()>, reloading: AtomicBool, + /// Cameras an operator has paused via `sb/pause`. + /// + /// A paused camera runs its in-flight captures to completion but accepts no new commanded or + /// scheduled capture work until `sb/resume` (SOUTHBOUND.md §2.2). The state is process-local and + /// deliberately not durable: a fresh start is unpaused, matching the scaffold's in-memory pause. + /// Keyed by instance id; a camera a reload removes leaves an inert entry that a restart clears and + /// that nothing reads (its schedules and capture paths no longer exist). + paused: RwLock>, self_reference: OnceLock>, } @@ -1406,6 +1414,38 @@ impl CameraRuntime { }) } + /// Whether an operator has paused this camera via `sb/pause`. + fn is_paused(&self, instance: &str) -> bool { + self.paused + .read() + .map(|paused| paused.contains(instance)) + .unwrap_or(false) + } + + /// Sets the pause state for one camera, returning whether it CHANGED (the `changed` reply field). + fn set_paused(&self, instance: &str, paused: bool) -> bool { + let mut set = self + .paused + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if paused { + set.insert(instance.to_owned()) + } else { + set.remove(instance) + } + } + + /// Refuses new capture work while the camera is paused. In-flight captures are untouched. + fn ensure_not_paused(&self, instance: &str) -> Result<()> { + if self.is_paused(instance) { + return Err(crate::CameraError::rejected( + crate::ErrorCode::InstancePaused, + "camera is paused; resume it before submitting new capture work", + )); + } + Ok(()) + } + fn lifecycle_events(&self, instance: &str) -> Option { let config = match self.config_snapshot() { Ok(config) => config, @@ -1623,6 +1663,7 @@ impl CameraRuntime { cursors: CursorStore::default(), reload_gate: tokio::sync::Mutex::new(()), reloading: AtomicBool::new(false), + paused: RwLock::new(std::collections::BTreeSet::new()), self_reference: OnceLock::new(), }); let _ = runtime.self_reference.set(Arc::downgrade(&runtime)); @@ -1658,7 +1699,7 @@ impl CameraRuntime { .map(|session| session.actor.clone()) .ok_or_else(|| { crate::CameraError::rejected( - crate::ErrorCode::CameraUnavailable, + crate::ErrorCode::DeviceUnavailable, format!("camera instance '{instance}' is not connected"), ) }) @@ -1673,7 +1714,7 @@ impl CameraRuntime { .map(|slot| slot.engine.clone()) .ok_or_else(|| { crate::CameraError::rejected( - crate::ErrorCode::UnknownInstance, + crate::ErrorCode::NoSuchInstance, format!("camera instance '{instance}' is not configured"), ) }) @@ -1764,7 +1805,7 @@ impl CameraCommandService for CameraRuntime { ) -> CommandOutcome { if self.reloading.load(Ordering::Acquire) { return CommandOutcome::ImmediateError(CommandError::new( - crate::ErrorCode::CameraUnavailable.as_str(), + crate::ErrorCode::DeviceUnavailable.as_str(), "the camera adapter is draining a configuration replacement", )); } @@ -1777,11 +1818,39 @@ impl CameraCommandService for CameraRuntime { "unsupported camera command", )); }; + // The CameraCommand operational family (instance × verb × result). `instance` is best-effort + // from the request body ("main" for a fleet/component-scoped command); the result is recorded + // once the outcome is known. The deferred capture verbs count acceptance (the capture's own + // success/failure lives in camera_captures). + let command_started = std::time::Instant::now(); + let command_instance = request + .body + .get("instance") + .and_then(serde_json::Value::as_str) + .unwrap_or("main") + .to_owned(); + let command_ms = |started: std::time::Instant| { + u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX) + }; if verb == CommandVerb::Capture { - return self.handle_deferred_capture(request, deferred).await; + let outcome = self.handle_deferred_capture(request, deferred).await; + self.metrics.record_command( + &command_instance, + verb.as_str(), + !matches!(outcome, CommandOutcome::ImmediateError(_)), + command_ms(command_started), + ); + return outcome; } if verb == CommandVerb::CaptureGroup { - return self.handle_deferred_group_capture(request, deferred).await; + let outcome = self.handle_deferred_group_capture(request, deferred).await; + self.metrics.record_command( + &command_instance, + verb.as_str(), + !matches!(outcome, CommandOutcome::ImmediateError(_)), + command_ms(command_started), + ); + return outcome; } let config = match self.config_snapshot() { Ok(config) => config, @@ -1845,8 +1914,32 @@ impl CameraCommandService for CameraRuntime { let body: StatusRequest = commands::parse_closed(request.body.clone())?; body.validate()?; match body.instance { - Some(instance) => Ok(serde_json::to_value(self.registry.snapshot(&instance)?)?), - None => Ok(serde_json::json!({ "cameras": self.registry.snapshots(1_000)? })), + // The `paused` flag is stamped onto the status view so the standardized + // lifecycle state is visible where an operator (and the overview panel) reads + // status, per SOUTHBOUND.md §2.2. + Some(instance) => { + let mut view = serde_json::to_value(self.registry.snapshot(&instance)?)?; + if let Some(object) = view.as_object_mut() { + object.insert("paused".to_owned(), self.is_paused(&instance).into()); + } + Ok(view) + } + None => { + let cameras = self + .registry + .snapshots(1_000)? + .into_iter() + .map(|snapshot| { + let paused = self.is_paused(&snapshot.instance); + let mut view = serde_json::to_value(snapshot)?; + if let Some(object) = view.as_object_mut() { + object.insert("paused".to_owned(), paused.into()); + } + Ok(view) + }) + .collect::>>()?; + Ok(serde_json::json!({ "cameras": cameras })) + } } } CommandVerb::CaptureSubmit => { @@ -1902,24 +1995,24 @@ impl CameraCommandService for CameraRuntime { let cursor = body.cursor.clone(); match body.validate()? { CaptureStatusMode::Capture => { - let id = body.capture_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::InvalidRequest, "captureId is required"))?; + let id = body.capture_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::BadArgs, "captureId is required"))?; let job = self.catalog.job(id).await?.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::CaptureNotFound, "capture was not found"))?; Ok(job_status_json(&job)) } CaptureStatusMode::Group => { - let id = body.capture_group_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::InvalidRequest, "captureGroupId is required"))?; + let id = body.capture_group_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::BadArgs, "captureGroupId is required"))?; let group = self.catalog.group(id).await?.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::CaptureNotFound, "capture group was not found"))?; self.group_status_page(group, usize::from(limit), cursor.as_deref()) } CaptureStatusMode::CameraRequest => { - let instance = body.instance.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::InvalidRequest, "instance is required"))?; - let request_id = body.request_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::InvalidRequest, "requestId is required"))?; + let instance = body.instance.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::BadArgs, "instance is required"))?; + let request_id = body.request_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::BadArgs, "requestId is required"))?; let job = self.catalog.job_by_ledger(crate::catalog::LedgerKey::new(instance, CommandVerb::Capture.as_str(), request_id)?).await? .ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::CaptureNotFound, "capture was not found"))?; Ok(job_status_json(&job)) } CaptureStatusMode::GroupRequest => { - let request_id = body.request_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::InvalidRequest, "requestId is required"))?; + let request_id = body.request_id.ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::BadArgs, "requestId is required"))?; let group = self.catalog.group_by_ledger(crate::catalog::LedgerKey::new("main", CommandVerb::CaptureGroup.as_str(), request_id)?).await? .ok_or_else(|| crate::CameraError::rejected(crate::ErrorCode::CaptureNotFound, "capture group was not found"))?; self.group_status_page(group, usize::from(limit), cursor.as_deref()) @@ -1955,6 +2048,22 @@ impl CameraCommandService for CameraRuntime { let body: PtzPresetsRequest = commands::parse_closed(request.body.clone())?; self.perform_presets(body).await } + CommandVerb::Pause => { + let body: commands::PauseResumeRequest = + commands::parse_closed(request.body.clone())?; + body.validate()?; + let instance = self.registry.resolve_instance(body.instance.as_deref())?; + let changed = self.set_paused(&instance, true); + Ok(serde_json::json!({ "id": instance, "paused": true, "changed": changed })) + } + CommandVerb::Resume => { + let body: commands::PauseResumeRequest = + commands::parse_closed(request.body.clone())?; + body.validate()?; + let instance = self.registry.resolve_instance(body.instance.as_deref())?; + let changed = self.set_paused(&instance, false); + Ok(serde_json::json!({ "id": instance, "paused": false, "changed": changed })) + } // The deferred verbs are answered above, before this match is reached. They are // named here rather than swept up by a `_`, because a `_` is what let a fifteenth // verb register with the inbox and then fall through to UNSUPPORTED_CAPABILITY at @@ -1967,6 +2076,12 @@ impl CameraCommandService for CameraRuntime { ), } }.await; + self.metrics.record_command( + &command_instance, + verb.as_str(), + outcome.is_ok(), + command_ms(command_started), + ); match outcome { Ok(value) => CommandOutcome::ImmediateSuccess(Some(value)), Err(error) => CommandOutcome::ImmediateError(command_error(&error)), @@ -2077,11 +2192,15 @@ pub enum CommandVerb { Ptz, /// `sb/ptz-presets` PtzPresets, + /// `sb/pause` -- suspend new capture work for the instance (standardized lifecycle verb). + Pause, + /// `sb/resume` -- resume a paused instance (standardized lifecycle verb). + Resume, } impl CommandVerb { /// Every verb the adapter answers, in the order the inbox registers them. - pub const ALL: [Self; 14] = [ + pub const ALL: [Self; 16] = [ Self::List, Self::Discover, Self::Status, @@ -2096,6 +2215,8 @@ impl CommandVerb { Self::Reconnect, Self::Ptz, Self::PtzPresets, + Self::Pause, + Self::Resume, ]; /// The exact wire spelling. This string is durable: it is part of the idempotency key. @@ -2116,6 +2237,8 @@ impl CommandVerb { Self::Reconnect => "sb/reconnect", Self::Ptz => "sb/ptz", Self::PtzPresets => "sb/ptz-presets", + Self::Pause => "sb/pause", + Self::Resume => "sb/resume", } } @@ -2132,6 +2255,35 @@ pub fn camera_command_verbs() -> Vec<&'static str> { CommandVerb::ALL.iter().map(|verb| verb.as_str()).collect() } +/// The three edge-console panel descriptors for the camera adapter. +/// +/// Core validates `id`/`title`/uniqueness; the widget kinds and bound verbs are console-interpreted, +/// so they ride verbatim. `order` 10/20/30, every panel `scope: "instance"`. Each binds only verbs +/// this adapter actually serves (SOUTHBOUND.md §2.2, the panel-trio baseline). +#[must_use] +pub fn camera_panels() -> Vec { + vec![ + serde_json::json!({ + "id": "overview", "title": "Overview", "order": 10, "scope": "instance", + "widgets": [ + { "kind": "summary", "fields": ["state", "connected", "paused", "backend"] }, + { "kind": "commandSummary", "actions": ["sb/reconnect", "sb/pause", "sb/resume"] } + ], + "verbs": ["sb/status", "sb/reconnect", "sb/pause", "sb/resume"] + }), + serde_json::json!({ + "id": "signals", "title": "Cameras", "order": 20, "scope": "instance", + "widgets": [ { "kind": "cameraRoster" }, { "kind": "captureSurface" } ], + "verbs": ["sb/list", "sb/status", "sb/capture", "sb/capture-status"] + }), + serde_json::json!({ + "id": "diagnostics", "title": "Diagnostics", "order": 30, "scope": "instance", + "widgets": [ { "kind": "treeBrowser" }, { "kind": "keyValueList" } ], + "verbs": ["sb/discover", "sb/queue-status"] + }), + ] +} + /// Durable states in which a capture still owes the operator an outcome. const NON_TERMINAL_JOB_STATES: [crate::model::JobState; 5] = [ crate::model::JobState::Accepted, @@ -2734,6 +2886,12 @@ impl RuntimeCommandRouter { }), )?; } + // The edge-console panel trio (overview / signals / diagnostics). Registered on the same inbox + // as the verbs, before the acknowledged subscription begins, so the descriptor surface is + // advertised atomically with the command surface it drives. + for panel in camera_panels() { + inbox.register_panel(panel)?; + } Ok(()) } @@ -2794,7 +2952,7 @@ impl RuntimeCommandRouter { match service { Some(service) => service.handle_camera_command(verb, request, deferred).await, None => CommandOutcome::ImmediateError(CommandError::new( - crate::ErrorCode::CameraUnavailable.as_str(), + crate::ErrorCode::DeviceUnavailable.as_str(), "the camera adapter is still starting", )), } diff --git a/src/runtime/command.rs b/src/runtime/command.rs index 7ce95c3..ba060af 100644 --- a/src/runtime/command.rs +++ b/src/runtime/command.rs @@ -177,6 +177,10 @@ impl CameraRuntime { crate::catalog::AcceptJobOutcome::Conflict }); } + // A paused camera runs its in-flight captures to completion but accepts no NEW work. Checked + // AFTER the idempotency lookup (so an operator can still retry-to-poll a capture accepted + // before the pause) and BEFORE any durable row or physical work (SOUTHBOUND.md §2.2). + self.ensure_not_paused(&instance)?; let ResolvedCapture { camera, @@ -415,9 +419,11 @@ impl CameraRuntime { config.global.limits.max_metadata_bytes, )?; // Resolve every member before creating any durable row. This gives the all-or-nothing - // error surface required by the group contract. + // error surface required by the group contract. A group is refused outright if ANY member is + // paused -- a partial group is not the contract (SOUTHBOUND.md §2.2). for instance in &body.instances { - let _ = self.registry.resolve_actuation_instance(Some(instance))?; + let resolved = self.registry.resolve_actuation_instance(Some(instance))?; + self.ensure_not_paused(&resolved)?; } let group_id = format!("grp_{}", uuid::Uuid::now_v7()); let mut submissions = Vec::with_capacity(body.instances.len()); @@ -990,7 +996,7 @@ impl CameraRuntime { let capture_group_id = capture_group_id.ok_or_else(|| { crate::CameraError::rejected( - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "captureGroupId is required", ) })?; diff --git a/src/runtime/reload.rs b/src/runtime/reload.rs index 5bd929f..0e8c839 100644 --- a/src/runtime/reload.rs +++ b/src/runtime/reload.rs @@ -20,7 +20,7 @@ impl CameraRuntime { ) -> Result { if self.reloading.swap(true, Ordering::AcqRel) { return Err(crate::CameraError::rejected( - crate::ErrorCode::CameraUnavailable, + crate::ErrorCode::DeviceUnavailable, "a configuration replacement is already draining camera work", )); } @@ -51,7 +51,7 @@ impl CameraRuntime { || previous.global.output.file_mode != replacement.global.output.file_mode { return Err(crate::CameraError::rejected( - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "state/output root security settings require component restart", )); } @@ -125,7 +125,7 @@ impl CameraRuntime { ) -> Result<()> { if self.reloading.swap(true, Ordering::AcqRel) { return Err(crate::CameraError::rejected( - crate::ErrorCode::CameraUnavailable, + crate::ErrorCode::DeviceUnavailable, "a configuration replacement is still active while restoring the prior generation", )); } @@ -484,7 +484,7 @@ impl CameraRuntime { } if tokio::time::Instant::now() >= deadline { return Err(crate::CameraError::rejected( - crate::ErrorCode::CameraUnavailable, + crate::ErrorCode::DeviceUnavailable, "camera supervisor did not stop within reloadDrainTimeoutMs", )); } diff --git a/src/runtime/schedule.rs b/src/runtime/schedule.rs index edc3b29..d2436bb 100644 --- a/src/runtime/schedule.rs +++ b/src/runtime/schedule.rs @@ -532,7 +532,7 @@ impl CameraRuntime { .find(|schedule| schedule.id == occurrence.schedule_id && schedule.enabled) .ok_or_else(|| { crate::CameraError::rejected( - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "group schedule is no longer enabled", ) })?; @@ -664,13 +664,23 @@ impl CameraRuntime { "camera was disabled before scheduled admission", )); } + // A paused camera's schedules are suspended: the occurrence is consumed (the one-occurrence + // guarantee holds) but no capture is admitted until the camera is resumed (SOUTHBOUND.md §2.2). + if self.is_paused(instance) { + tracing::info!( + instance = %instance, + schedule_id = %occurrence.schedule_id, + "scheduled capture suspended: camera is paused" + ); + return Ok(()); + } let schedule = camera .schedules .iter() .find(|schedule| schedule.id == occurrence.schedule_id && schedule.enabled) .ok_or_else(|| { crate::CameraError::rejected( - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "schedule is no longer enabled for this camera", ) })?; diff --git a/src/runtime/supervision.rs b/src/runtime/supervision.rs index 807ead1..99ae38a 100644 --- a/src/runtime/supervision.rs +++ b/src/runtime/supervision.rs @@ -171,6 +171,10 @@ impl CameraRuntime { } }; runtime.metrics.sample_queue(values).await; + // Drain the accumulated operational families on the same tick: the capture-lifecycle + // counters and the CameraCommand cells emit their Total/Interval pairs and reset. + runtime.metrics.drain_captures().await; + runtime.metrics.drain_commands().await; runtime.sample_southbound_health(false).await; } }) @@ -594,7 +598,7 @@ impl CameraRuntime { .get_mut(&instance) .ok_or_else(|| { crate::CameraError::rejected( - crate::ErrorCode::UnknownInstance, + crate::ErrorCode::NoSuchInstance, format!("camera instance '{instance}' is not configured"), ) })? diff --git a/src/runtime/tests/mod.rs b/src/runtime/tests/mod.rs index 7a612fb..e80217f 100644 --- a/src/runtime/tests/mod.rs +++ b/src/runtime/tests/mod.rs @@ -77,7 +77,7 @@ fn retained_snapshot_cursor_is_opaque_query_bound_and_stable() { .snapshot_page("list", &changed_query, Some(&cursor), None, None, 1) .unwrap_err() .code(), - crate::ErrorCode::InvalidRequest + crate::ErrorCode::BadArgs ); } diff --git a/src/runtime/tests/simulator_runtime.rs b/src/runtime/tests/simulator_runtime.rs index 2aefaa2..fd633d9 100644 --- a/src/runtime/tests/simulator_runtime.rs +++ b/src/runtime/tests/simulator_runtime.rs @@ -342,6 +342,7 @@ async fn runtime_with_everything( cursors: CursorStore::default(), reload_gate: tokio::sync::Mutex::new(()), reloading: AtomicBool::new(false), + paused: RwLock::new(std::collections::BTreeSet::new()), self_reference: OnceLock::new(), }); let _ = runtime.self_reference.set(Arc::downgrade(&runtime)); @@ -880,6 +881,21 @@ async fn wait_for_online(runtime: &CameraRuntime, instance: &str) { } } +/// Polls the `camera_captures` accumulator until a measure reaches `at_least`, without draining. +/// +/// The outcome counters land on the job hooks just after the durable terminal write, so a terminal +/// job does not yet imply a counted one; this waits for the count to accumulate. +async fn wait_for_capture_count(runtime: &CameraRuntime, measure: &str, at_least: f64) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(10); + while runtime.metrics.captured_total(measure) < at_least { + assert!( + tokio::time::Instant::now() < deadline, + "camera_captures.{measure} never reached {at_least} -- the count an operator watches was lost" + ); + tokio::time::sleep(Duration::from_millis(25)).await; + } +} + async fn wait_for_terminal( runtime: &CameraRuntime, capture_id: &str, @@ -2280,7 +2296,7 @@ async fn simulator_runtime_discovery_and_capture_status_use_stable_bounded_pages .await .unwrap_err() .code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "a cursor must be bound to its original state filter" ); runtime.shutdown().await; @@ -2727,7 +2743,7 @@ async fn command_service_dispatches_immediate_verbs_with_a_real_inbox_registry() .await { CommandOutcome::ImmediateError(error) => { - assert_eq!(error.code, crate::ErrorCode::InvalidRequest.as_str()); + assert_eq!(error.code, crate::ErrorCode::BadArgs.as_str()); } other => panic!("invalid request must return an immediate error, got {other:?}"), } @@ -2874,7 +2890,7 @@ async fn reload_timeout_preserves_prior_generation_until_a_safe_retry() { .apply_reloaded_config(replacement.clone(), BTreeMap::new(), BTreeMap::new()) .await .expect_err("a candidate must be vetoed while an old supervisor is unconfirmed"); - assert_eq!(error.code(), crate::ErrorCode::CameraUnavailable); + assert_eq!(error.code(), crate::ErrorCode::DeviceUnavailable); assert!(old_cancellation.is_cancelled()); assert!( runtime @@ -2969,7 +2985,7 @@ async fn supervisor_reports_unavailable_genicam_as_backoff_without_creating_an_a match runtime.actor("camera-a") { Err(error) => assert_eq!( error.code(), - crate::ErrorCode::CameraUnavailable, + crate::ErrorCode::DeviceUnavailable, "a rejected backend factory must never leave a live actor handle" ), Ok(_) => panic!("a rejected backend factory must never leave a live actor handle"), @@ -3237,7 +3253,7 @@ async fn reload_supervisor_barrier_cancels_before_timing_out_and_then_allows_ret .replace_supervisors(&["camera-a".to_string()], Duration::ZERO) .await .expect_err("an unconfirmed supervisor must veto the replacement"); - assert_eq!(error.code(), crate::ErrorCode::CameraUnavailable); + assert_eq!(error.code(), crate::ErrorCode::DeviceUnavailable); assert!( cancellation.is_cancelled(), "the old generation must be cancelled even when its drain deadline is already elapsed" @@ -4266,7 +4282,7 @@ fn cursor_store_preserves_page_boundaries_and_rejects_cross_query_reuse() { ) .unwrap_err() .code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "a retained list cursor is bound to its original capability view" ); @@ -4309,7 +4325,7 @@ fn cursor_store_preserves_page_boundaries_and_rejects_cross_query_reuse() { ) .unwrap_err() .code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "a retained snapshot cursor cannot be replayed under another operation" ); @@ -4330,7 +4346,7 @@ fn cursor_store_preserves_page_boundaries_and_rejects_cross_query_reuse() { ) .unwrap_err() .code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "a durable status cursor is bound to its original filter" ); } @@ -4406,7 +4422,7 @@ async fn simulator_runtime_rejects_invalid_requests_without_creating_durable_wor .await .unwrap_err() .code(), - crate::ErrorCode::UnknownInstance + crate::ErrorCode::NoSuchInstance ); assert_eq!( runtime @@ -4499,7 +4515,7 @@ async fn simulator_runtime_rejects_invalid_requests_without_creating_durable_wor .await .unwrap_err() .code(), - crate::ErrorCode::InstanceRequired, + crate::ErrorCode::BadArgs, "multi-camera reconnect requires an explicit target" ); assert_eq!( @@ -4859,7 +4875,7 @@ async fn runtime_startup_router_and_deferred_capture_flows_use_real_core_facades .await { CommandOutcome::ImmediateError(error) => { - assert_eq!(error.code, "CAMERA_UNAVAILABLE"); + assert_eq!(error.code, "DEVICE_UNAVAILABLE"); } other => panic!("uninstalled router must reject requests, got {other:?}"), } @@ -7174,18 +7190,29 @@ async fn captures_are_counted_even_when_every_event_slot_is_taken() { let terminal = wait_for_terminal(&runtime, &capture_id).await; assert_eq!(terminal.state, crate::model::JobState::Succeeded); + // The counters accumulate on the job hooks even with the event pipe starved; a drain emits the + // Total/Interval pairs. The outcome count lands just after the durable terminal write, so poll the + // accumulator (without emitting) before draining. + wait_for_capture_count(&runtime, "succeeded", 1.0).await; + runtime.metrics.drain_captures().await; + let counted = crate::observability::CAPTURE_METRIC; assert_eq!( - metrics.counts(counted, "queued"), + metrics.counts(counted, "queuedTotal"), 1.0, "the capture happened; a saturated EVENT pipe must not stop it being COUNTED" ); - assert_eq!(metrics.counts(counted, "started"), 1.0); + assert_eq!(metrics.counts(counted, "startedTotal"), 1.0); assert_eq!( - metrics.counts(counted, "succeeded"), + metrics.counts(counted, "succeededTotal"), 1.0, "and its outcome is the number an operator watches -- it must survive the overload that makes them look" ); + assert_eq!( + metrics.counts(counted, "succeededInterval"), + 1.0, + "the interval counter carries every event since the last drain -- nothing is sampled away" + ); runtime.shutdown().await; } @@ -7315,35 +7342,28 @@ async fn captures_are_counted_as_they_happen() { let counted = crate::observability::CAPTURE_METRIC; - // The outcome counter is emitted AFTER the durable terminal write, so a terminal job does not yet - // imply a counted one. Sampling here once made this test right almost always and wrong under load, - // which is the worst kind of test. Wait for the count instead of assuming it has landed. - let deadline = tokio::time::Instant::now() + Duration::from_secs(10); - while metrics.counts(counted, "succeeded") < 1.0 { - assert!( - tokio::time::Instant::now() < deadline, - "the capture succeeded but was never counted -- this is the number an operator watches" - ); - tokio::time::sleep(Duration::from_millis(25)).await; - } + // The outcome counter accumulates AFTER the durable terminal write, so a terminal job does not yet + // imply a counted one. Wait for it to land in the accumulator, then drain to emit the pairs. + wait_for_capture_count(&runtime, "succeeded", 1.0).await; + runtime.metrics.drain_captures().await; assert_eq!( - metrics.counts(counted, "queued"), + metrics.counts(counted, "queuedTotal"), 1.0, "an accepted capture must be counted when it is accepted" ); assert_eq!( - metrics.counts(counted, "started"), + metrics.counts(counted, "startedTotal"), 1.0, "and again when it actually starts doing physical work" ); assert_eq!( - metrics.counts(counted, "succeeded"), + metrics.counts(counted, "succeededTotal"), 1.0, "and its outcome must be counted -- this is the number an operator watches" ); assert_eq!( - metrics.counts(counted, "failed"), + metrics.counts(counted, "failedTotal"), 0.0, "a capture that succeeded must not also be counted as a failure" ); @@ -7727,7 +7747,7 @@ async fn an_occurrence_of_a_removed_group_schedule_is_not_submitted() { .submit_scheduled_group(&occurrence) .await .expect_err("a disabled schedule must not fire"); - assert_eq!(error.code(), crate::ErrorCode::InvalidRequest); + assert_eq!(error.code(), crate::ErrorCode::BadArgs); let unknown = ScheduleOccurrence { scope: crate::scheduler::ScheduleScope::Group("gone".to_string()), @@ -7872,7 +7892,7 @@ async fn queue_status_and_clear_reject_an_unknown_camera() { .await .expect_err("an unknown camera must be rejected, not reported as empty") .code(), - crate::ErrorCode::UnknownInstance + crate::ErrorCode::NoSuchInstance ); assert_eq!( runtime @@ -7880,7 +7900,7 @@ async fn queue_status_and_clear_reject_an_unknown_camera() { .await .expect_err("an unknown camera must be rejected") .code(), - crate::ErrorCode::UnknownInstance + crate::ErrorCode::NoSuchInstance ); } diff --git a/src/runtime/tests/simulator_runtime/coverage_command.rs b/src/runtime/tests/simulator_runtime/coverage_command.rs index 4c2ac4e..16ca3a5 100644 --- a/src/runtime/tests/simulator_runtime/coverage_command.rs +++ b/src/runtime/tests/simulator_runtime/coverage_command.rs @@ -118,7 +118,7 @@ async fn a_group_capture_of_one_camera_is_refused() { .unwrap_err(); assert_eq!( error.code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "a one-camera group is not a group" ); assert!( @@ -190,7 +190,7 @@ async fn a_group_naming_an_unknown_camera_creates_nothing_at_all() { .unwrap_err(); assert_eq!( error.code(), - crate::ErrorCode::UnknownInstance, + crate::ErrorCode::NoSuchInstance, "a group naming a camera that is not configured is refused whole" ); assert!( @@ -929,7 +929,7 @@ async fn a_deferred_verb_rejects_an_invalid_body_immediately() { { CommandOutcome::ImmediateError(error) => assert_eq!( error.code, - crate::ErrorCode::InvalidRequest.as_str(), + crate::ErrorCode::BadArgs.as_str(), "the closed schema must refuse an unknown field" ), other => panic!("an unparseable capture body must be refused, got {other:?}"), @@ -947,7 +947,7 @@ async fn a_deferred_verb_rejects_an_invalid_body_immediately() { { CommandOutcome::ImmediateError(error) => assert_eq!( error.code, - crate::ErrorCode::InvalidRequest.as_str(), + crate::ErrorCode::BadArgs.as_str(), "a one-camera group is refused before it is ever deferred" ), other => panic!("an invalid group body must be refused, got {other:?}"), @@ -1245,7 +1245,7 @@ async fn preset_mutation_is_refused_where_recall_is_not() { .unwrap(); assert_eq!( runtime.perform_presets(recall).await.unwrap_err().code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "recall is not a mutation: it reaches the camera, which rejects the unknown token itself" ); runtime.shutdown().await; @@ -1490,7 +1490,7 @@ async fn a_cancellation_whose_camera_is_gone_records_its_failure_and_replays_it( .cancel_capture(request.clone()) .await .expect_err("a capture whose camera is no longer configured cannot be cancelled"); - assert_eq!(error.code(), crate::ErrorCode::UnknownInstance); + assert_eq!(error.code(), crate::ErrorCode::NoSuchInstance); let replayed = runtime .cancel_capture(request) @@ -1498,7 +1498,7 @@ async fn a_cancellation_whose_camera_is_gone_records_its_failure_and_replays_it( .expect("the retry is answered from the ledger rather than run a second time"); assert_eq!( replayed["errorCode"], - crate::ErrorCode::UnknownInstance.as_str(), + crate::ErrorCode::NoSuchInstance.as_str(), "the recorded failure is what the retry gets back" ); assert!( @@ -1590,7 +1590,7 @@ async fn the_queue_verbs_refuse_a_malformed_or_unconsented_request() { .await .unwrap_err() .code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "an instance that is not a UNS token is refused before the catalog is touched" ); assert_eq!( @@ -1605,7 +1605,7 @@ async fn the_queue_verbs_refuse_a_malformed_or_unconsented_request() { .await .unwrap_err() .code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "draining every camera requires the caller to say allCameras=true" ); runtime.shutdown().await; @@ -2436,7 +2436,7 @@ async fn a_camera_that_is_down_reports_why_on_the_connectivity_surface() { .and_then(serde_json::Value::as_str); assert!( code == Some(crate::ErrorCode::UnsupportedCapability.as_str()) - || code == Some(crate::ErrorCode::InvalidRequest.as_str()), + || code == Some(crate::ErrorCode::BadArgs.as_str()), "a camera that is down must carry a stable error code an operator can act on without reading prose; got {code:?}" ); assert!( @@ -2529,7 +2529,7 @@ async fn a_discovery_cursor_that_belongs_to_no_snapshot_is_refused() { .expect_err("a cursor with no snapshot behind it cannot be paged"); assert_eq!( error.code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "an unresolvable cursor is refused, not silently turned into a new discovery pass" ); runtime.shutdown().await; @@ -2922,7 +2922,7 @@ async fn the_list_verb_pages_capabilities_on_demand_and_offers_only_unclaimed_di ); assert_eq!( stale.code, - crate::ErrorCode::InvalidRequest.as_str(), + crate::ErrorCode::BadArgs.as_str(), "a cursor that resolves to no retained result is refused, not answered from the head" ); assert_eq!( @@ -3213,7 +3213,7 @@ async fn the_capture_status_verb_answers_by_capture_group_request_and_list() { ); assert_eq!( bad_cursor.code, - crate::ErrorCode::InvalidRequest.as_str(), + crate::ErrorCode::BadArgs.as_str(), "a member cursor that belongs to no retained page is refused, not answered from the head" ); @@ -3574,7 +3574,7 @@ async fn a_group_capture_with_an_unusable_request_id_is_refused_before_anything_ .expect_err("an empty requestId cannot key a durable group"); assert_eq!( error.code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "the caller is told its key is unusable rather than given un-retryable work" ); assert!( @@ -4064,7 +4064,7 @@ async fn queue_status_for_a_camera_that_does_not_exist_is_refused() { .expect_err("an unknown camera has no queue to report"); assert_eq!( error.code(), - crate::ErrorCode::UnknownInstance, + crate::ErrorCode::NoSuchInstance, "the caller is told the camera does not exist, not handed an empty queue" ); @@ -4144,7 +4144,7 @@ async fn a_preset_cursor_that_belongs_to_no_snapshot_is_refused() { .expect_err("a cursor with no retained preset snapshot behind it cannot be paged"); assert_eq!( error.code(), - crate::ErrorCode::InvalidRequest, + crate::ErrorCode::BadArgs, "an unresolvable cursor is refused, not turned into a second read of the camera" ); runtime.shutdown().await; @@ -4720,10 +4720,13 @@ async fn a_thumbnail_that_fails_or_is_dropped_is_counted_on_the_capture_metric() runtime.waiters.thumbnail_dropped().await; runtime.waiters.thumbnail_dropped().await; + // The counts accumulate on the hooks; a drain emits their Total/Interval pairs. + runtime.metrics.drain_captures().await; + assert_eq!( recorder.counts( crate::observability::CAPTURE_METRIC, - crate::observability::THUMBNAIL_FAILED_MEASURE + &format!("{}Total", crate::observability::THUMBNAIL_FAILED_MEASURE) ), 1.0, "a thumbnail that could not be rendered must be visible outside the process" @@ -4731,7 +4734,7 @@ async fn a_thumbnail_that_fails_or_is_dropped_is_counted_on_the_capture_metric() assert_eq!( recorder.counts( crate::observability::CAPTURE_METRIC, - crate::observability::THUMBNAIL_DROPPED_MEASURE + &format!("{}Total", crate::observability::THUMBNAIL_DROPPED_MEASURE) ), 2.0, "and so must every thumbnail that did not fit the ceiling" @@ -4747,3 +4750,147 @@ async fn a_thumbnail_that_fails_or_is_dropped_is_counted_on_the_capture_metric() runtime.shutdown().await; } + +/// The lifecycle verbs suspend a camera's new capture work and surface the state in `sb/status`. +/// +/// `sb/pause` / `sb/resume` are the standardized lifecycle verbs (SOUTHBOUND.md §2.2). Pausing a +/// camera refuses NEW capture work with a stable `INSTANCE_PAUSED` code while leaving its sibling +/// untouched; the toggle is idempotent (`changed` reports whether it moved); and the paused flag is +/// visible where an operator and the overview panel read status. Resuming clears it. +#[tokio::test] +async fn pause_and_resume_suspend_new_capture_work_and_surface_in_status() { + let (port, _broker) = spawn_recording_mqtt_broker().await; + let directory = TempDir::new().unwrap(); + let runtime = runtime(config(directory.path(), &["camera-a", "camera-b"], false), &directory).await; + let (_app, deferred) = command_deferred_registry(&directory, port).await; + + // Pause camera-a — idempotent, and it names the instance it moved. + let paused = immediate_success( + runtime + .handle_camera_command( + "sb/pause", + command_message("sb/pause", "p1", json!({ "instance": "camera-a" })), + deferred.clone(), + ) + .await, + ); + assert_eq!(paused["id"], json!("camera-a")); + assert_eq!(paused["paused"], json!(true)); + assert_eq!(paused["changed"], json!(true)); + + let again = immediate_success( + runtime + .handle_camera_command( + "sb/pause", + command_message("sb/pause", "p2", json!({ "instance": "camera-a" })), + deferred.clone(), + ) + .await, + ); + assert_eq!(again["changed"], json!(false), "pausing an already-paused camera changes nothing"); + + // Status carries the paused flag, per instance and in the fleet listing. + let status = immediate_success( + runtime + .handle_camera_command( + "sb/status", + command_message("sb/status", "s1", json!({ "instance": "camera-a" })), + deferred.clone(), + ) + .await, + ); + assert_eq!(status["paused"], json!(true), "a paused camera says so in its status"); + + let fleet = immediate_success( + runtime + .handle_camera_command( + "sb/status", + command_message("sb/status", "s2", json!({})), + deferred.clone(), + ) + .await, + ); + let cameras = fleet["cameras"].as_array().expect("a fleet status lists every camera"); + let a = cameras.iter().find(|c| c["instance"] == json!("camera-a")).unwrap(); + let b = cameras.iter().find(|c| c["instance"] == json!("camera-b")).unwrap(); + assert_eq!(a["paused"], json!(true)); + assert_eq!(b["paused"], json!(false), "pausing one camera must not pause its siblings"); + + // New capture work for the paused camera is refused with the stable code; the sibling still works. + let refused = runtime + .submit_capture( + "camera-a".to_string(), + "while-paused".to_string(), + None, + None, + serde_json::Map::new(), + "while-paused-correlation".to_string(), + "sb/capture-submit", + crate::admission::CapturePriority::Submitted, + ) + .await + .expect_err("a paused camera must refuse new capture work"); + assert_eq!(refused.code(), crate::ErrorCode::InstancePaused); + + runtime + .submit_capture( + "camera-b".to_string(), + "sibling-ok".to_string(), + None, + None, + serde_json::Map::new(), + "sibling-ok-correlation".to_string(), + "sb/capture-submit", + crate::admission::CapturePriority::Submitted, + ) + .await + .expect("an un-paused sibling must still accept work"); + + // Resume clears it, and capture work is admitted again. + let resumed = immediate_success( + runtime + .handle_camera_command( + "sb/resume", + command_message("sb/resume", "r1", json!({ "instance": "camera-a" })), + deferred.clone(), + ) + .await, + ); + assert_eq!(resumed["paused"], json!(false)); + assert_eq!(resumed["changed"], json!(true)); + + runtime + .submit_capture( + "camera-a".to_string(), + "after-resume".to_string(), + None, + None, + serde_json::Map::new(), + "after-resume-correlation".to_string(), + "sb/capture-submit", + crate::admission::CapturePriority::Submitted, + ) + .await + .expect("a resumed camera accepts capture work again"); + + runtime.shutdown().await; +} + +/// The panel trio is registered with the ids, orders, and instance scope the baseline prescribes. +#[test] +fn the_panel_trio_is_registered_with_the_right_ids_orders_and_scope() { + let panels = crate::runtime::camera_panels(); + let ids: Vec<&str> = panels.iter().map(|p| p["id"].as_str().unwrap()).collect(); + assert_eq!(ids, vec!["overview", "signals", "diagnostics"]); + let orders: Vec = panels.iter().map(|p| p["order"].as_u64().unwrap()).collect(); + assert_eq!(orders, vec![10, 20, 30]); + for panel in &panels { + assert_eq!(panel["scope"], json!("instance"), "every panel is instance-scoped"); + } + // The overview panel binds the lifecycle verbs the adapter actually serves. + assert_eq!( + panels[0]["verbs"], + json!(["sb/status", "sb/reconnect", "sb/pause", "sb/resume"]) + ); + assert_eq!(panels[2]["verbs"], json!(["sb/discover", "sb/queue-status"])); +} diff --git a/src/runtime/tests/simulator_runtime/coverage_reload.rs b/src/runtime/tests/simulator_runtime/coverage_reload.rs index c7d0fd8..efa134f 100644 --- a/src/runtime/tests/simulator_runtime/coverage_reload.rs +++ b/src/runtime/tests/simulator_runtime/coverage_reload.rs @@ -513,7 +513,7 @@ async fn a_reload_whose_drain_budget_expires_while_a_capture_is_in_flight_vetoes .await .expect_err("an exhausted drain budget must veto the candidate rather than proceed"); - assert_eq!(error.code(), crate::ErrorCode::CameraUnavailable); + assert_eq!(error.code(), crate::ErrorCode::DeviceUnavailable); let crate::config::BackendConfig::Sim(sim) = &runtime.config_snapshot().unwrap().instances[0].backend else { @@ -550,13 +550,13 @@ async fn a_second_reload_is_rejected_while_one_is_already_draining() { .apply_reloaded_config(replacement, BTreeMap::new(), BTreeMap::new()) .await .expect_err("a replacement must not begin while another is draining camera work"); - assert_eq!(error.code(), crate::ErrorCode::CameraUnavailable); + assert_eq!(error.code(), crate::ErrorCode::DeviceUnavailable); let error = runtime .restore_reload_checkpoint(checkpoint) .await .expect_err("a rollback must not run while another reload is still draining"); - assert_eq!(error.code(), crate::ErrorCode::CameraUnavailable); + assert_eq!(error.code(), crate::ErrorCode::DeviceUnavailable); let crate::config::BackendConfig::Sim(sim) = &runtime.config_snapshot().unwrap().instances[0].backend @@ -597,7 +597,7 @@ async fn a_rollback_after_a_vetoed_reload_restores_a_working_camera_generation() .await .expect_err("a zero drain budget vetoes the candidate") .code(), - crate::ErrorCode::CameraUnavailable + crate::ErrorCode::DeviceUnavailable ); runtime @@ -923,6 +923,38 @@ async fn the_same_scheduled_occurrence_is_never_admitted_twice() { runtime.shutdown().await; } +/// A paused camera's schedules are suspended: the occurrence is consumed (no error, the one-occurrence +/// guarantee holds) but no durable job is written until the camera is resumed (SOUTHBOUND.md §2.2). +#[tokio::test] +async fn a_scheduled_occurrence_for_a_paused_camera_is_suspended_before_any_job_is_written() { + let directory = TempDir::new().unwrap(); + let runtime = runtime(config(directory.path(), &["camera-a"], true), &directory).await; + let occurrence = occurrence( + crate::scheduler::ScheduleScope::Camera("camera-a".to_string()), + "minute", + ); + + assert!(runtime.set_paused("camera-a", true), "pausing an unpaused camera reports a change"); + runtime + .submit_scheduled(&occurrence) + .await + .expect("a paused schedule occurrence is consumed, not an error"); + assert!( + jobs_for(&runtime, "camera-a").await.is_empty(), + "a paused camera's schedule must write no durable capture" + ); + + // Resume, and the same schedule admits work again. + assert!(runtime.set_paused("camera-a", false)); + runtime.submit_scheduled(&occurrence).await.unwrap(); + assert_eq!( + jobs_for(&runtime, "camera-a").await.len(), + 1, + "a resumed camera's schedule admits captures again" + ); + runtime.shutdown().await; +} + /// A camera disabled between the cron firing and the occurrence being admitted must not be given /// work. The schedule loop consumes the occurrence either way -- what it must not do is write a /// durable job for a camera the operator has taken out of service. @@ -989,7 +1021,7 @@ async fn a_scheduled_occurrence_for_a_schedule_that_was_disabled_is_rejected() { .await .expect_err("a disabled schedule cannot admit an occurrence"); - assert_eq!(error.code(), crate::ErrorCode::InvalidRequest); + assert_eq!(error.code(), crate::ErrorCode::BadArgs); assert!(jobs_for(&runtime, "camera-a").await.is_empty()); runtime.shutdown().await; } diff --git a/src/runtime/tests/simulator_runtime/coverage_supervision.rs b/src/runtime/tests/simulator_runtime/coverage_supervision.rs index 6fe0502..9aa4b5a 100644 --- a/src/runtime/tests/simulator_runtime/coverage_supervision.rs +++ b/src/runtime/tests/simulator_runtime/coverage_supervision.rs @@ -76,6 +76,7 @@ async fn runtime_with_component_events( cursors: CursorStore::default(), reload_gate: tokio::sync::Mutex::new(()), reloading: AtomicBool::new(false), + paused: RwLock::new(std::collections::BTreeSet::new()), self_reference: OnceLock::new(), }); let _ = runtime.self_reference.set(Arc::downgrade(&runtime)); @@ -629,7 +630,7 @@ async fn a_supervisor_cannot_be_started_for_a_camera_that_is_not_configured() { assert_eq!( error.code(), - crate::ErrorCode::UnknownInstance, + crate::ErrorCode::NoSuchInstance, "an unconfigured camera must be rejected by name" ); runtime.shutdown().await; @@ -692,7 +693,7 @@ async fn a_camera_whose_actor_cannot_be_built_is_held_in_backoff_and_retried() { .last_error .as_ref() .map(|error| error.code.as_str()), - Some("INVALID_REQUEST"), + Some("BAD_ARGS"), "the camera must carry the reason it cannot be served" ); assert!( diff --git a/src/storage.rs b/src/storage.rs index 8224167..a3d272b 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -2124,7 +2124,7 @@ mod tests { let error = root .prepare_capture(invalid_encoding, &CancellationToken::new()) .expect_err("invalid encoder settings must remove the exclusive partial"); - assert_eq!(error.code(), ErrorCode::InvalidRequest); + assert_eq!(error.code(), ErrorCode::BadArgs); let relative = "cam/2026/07/10/20260710T123456.000Z-cap-encoding.raw"; assert!(!temp.path().join(relative).exists());