diff --git a/Cargo.lock b/Cargo.lock index c0874ff..45e8a56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -100,9 +106,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "itoa", "matchit", "memchr", @@ -111,8 +117,8 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", - "tower", + "sync_wrapper 0.1.2", + "tower 0.4.13", "tower-layer", "tower-service", ] @@ -126,8 +132,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -140,6 +146,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bindgen" version = "0.71.1" @@ -218,6 +230,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -254,6 +272,16 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -289,7 +317,7 @@ dependencies = [ "chrono", "libc", "pkg-config", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -368,6 +396,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -392,6 +435,23 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -411,9 +471,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -433,8 +498,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -444,9 +511,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -466,7 +535,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", "indexmap 2.12.1", "slab", "tokio", @@ -533,6 +621,16 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -540,7 +638,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -566,9 +687,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -580,18 +701,99 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.12", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.32", "pin-project-lite", "tokio", "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -738,6 +940,22 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.12.1" @@ -812,6 +1030,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "match_cfg" version = "0.1.0" @@ -868,6 +1092,23 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -902,6 +1143,50 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -1002,12 +1287,14 @@ dependencies = [ "prost", "prost-types", "protoc-bin-vendored", + "reqwest", "serde", "serde_json", "serde_yaml", "sha2", + "snmp", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tonic", @@ -1143,6 +1430,61 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -1165,8 +1507,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1176,7 +1528,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "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.3", ] [[package]] @@ -1188,6 +1550,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "regex" version = "1.12.2" @@ -1217,6 +1588,67 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.12", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[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.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1236,6 +1668,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1248,6 +1715,38 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -1291,6 +1790,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1351,6 +1862,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "snmp" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2a575449a5c487091e541c0cb4ccd83620167fd52363f816fe28f6f357fc00" + [[package]] name = "socket2" version = "0.5.10" @@ -1377,6 +1894,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[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.111" @@ -1394,6 +1917,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[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" @@ -1405,6 +1937,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -1424,7 +1977,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1438,6 +2000,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1457,6 +2030,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.48.0" @@ -1494,6 +2082,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[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.17" @@ -1527,19 +2135,19 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64", + "base64 0.21.7", "bytes", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", "hyper-timeout", "percent-encoding", "pin-project", "prost", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -1569,7 +2177,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -1578,6 +2186,39 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1675,6 +2316,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[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.7" @@ -1699,6 +2346,12 @@ 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_check" version = "0.9.5" @@ -1742,6 +2395,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -1774,6 +2440,48 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +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-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1837,6 +2545,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2098,6 +2817,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 90b6480..035addd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ tokio-stream = "0.1" hostname = "0.3" sha2 = "0.10" url = "2" +snmp = "0.2" +reqwest = { version = "0.12", features = ["rustls-tls", "stream"] } [build-dependencies] tonic-build = "0.11" diff --git a/build.rs b/build.rs index 1f9149e..0dadc28 100644 --- a/build.rs +++ b/build.rs @@ -4,6 +4,8 @@ fn main() { std::env::set_var("PROTOC", protoc); } + println!("cargo:rerun-if-changed=proto/control.proto"); + tonic_build::configure() .build_server(false) .compile(&["proto/control.proto"], &["proto"]) diff --git a/proto/control.proto b/proto/control.proto index e7039b9..fb9cc21 100644 --- a/proto/control.proto +++ b/proto/control.proto @@ -39,8 +39,12 @@ message Pong { message PrintInstruction { string request_id = 1; - bytes pdf_data = 2; + bytes pdf_data = 2; // 兼容旧字段,仍可用 PrintParams params = 3; + string url = 4; // 新:远程文件 URL + bytes raw_data = 5; // 新:任意二进制(预期 PDF) + string local_path = 6; // 新:本地路径(客户端可读) + string content_type = 7; // 可选 MIME 类型,默认 application/pdf } message PrintParams { @@ -78,5 +82,7 @@ message PrinterInfo { uint32 ppm = 9; uint32 ppm_color = 10; repeated string reasons = 11; + int32 paper_percent = 12; // -1 或缺省表示未知 + uint64 page_count = 13; // 总页数,0 或缺省表示未知 } diff --git a/src/bin/cups_smoke.rs b/src/bin/cups_smoke.rs new file mode 100644 index 0000000..59f5cbe --- /dev/null +++ b/src/bin/cups_smoke.rs @@ -0,0 +1,271 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; +use print_backend::{ + config::{AppConfig, LoadBalanceStrategy}, + models::{ColorModeSetting, DuplexSetting, OrientationSetting, PrintParams, QualitySetting}, + printer::{CupsClient, SharedPrinterAdapter}, + scheduler::Scheduler, +}; + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(std::env::args().skip(1))?; + + let config_path = std::env::var("CONFIG_PATH").ok(); + let config = AppConfig::load(config_path.as_deref())?; + + configure_cups(&config)?; + + let adapter: SharedPrinterAdapter = Arc::new(CupsClient::with_snmp(Some(config.snmp.clone()))); + let scheduler = Arc::new(Scheduler::new( + config.scheduler.strategy, + config.scheduler.refresh_interval_secs, + adapter, + config.scheduler.default_ppm_bw, + config.scheduler.default_ppm_color, + config.scheduler.target_wait_minutes, + config.scheduler.printer_speeds.clone(), + )); + + if args.list { + list_printers(&scheduler).await?; + return Ok(()); + } + + let file = args.file.clone(); + if let Some(file) = file { + let params = args.into_print_params(); + let retry = &config.retry; + let defaults = &config.defaults; + let target = params.printer.as_deref(); + + let (job, retries_used) = scheduler + .dispatch_with_target(file.as_path(), ¶ms, defaults, retry, target) + .await + .context("提交打印任务失败")?; + + println!( + "打印成功: printer={}, job_id={}, retries_used={}", + job.printer, job.job_id, retries_used + ); + return Ok(()); + } + + Args::print_usage(); + Err(anyhow!("未指定操作 (--list 或 --print)")) +} + +fn configure_cups(config: &AppConfig) -> Result<()> { + if let Some(server) = &config.cups.server { + let normalized = normalize_server(server); + unsafe { + std::env::set_var("CUPS_SERVER", &normalized); + } + cups_rs::config::set_server(Some(&normalized)).context("设置 CUPS 服务器失败")?; + } + if let Some(user) = &config.cups.user { + unsafe { + std::env::set_var("CUPS_USER", user); + } + cups_rs::config::set_user(Some(user)).context("设置 CUPS 用户失败")?; + } + if let Some(password) = &config.cups.password { + unsafe { + std::env::set_var("CUPS_PASSWORD", password); + } + } + Ok(()) +} + +fn normalize_server(input: &str) -> String { + if let Some((_, rest)) = input.split_once("://") { + rest.split('/').next().unwrap_or(rest).to_string() + } else { + input.split('/').next().unwrap_or(input).to_string() + } +} + +async fn list_printers(scheduler: &Scheduler) -> Result<()> { + let printers = scheduler + .refresh_snapshot() + .await + .context("获取打印机列表失败")?; + + if printers.is_empty() { + println!("未发现打印机"); + return Ok(()); + } + + println!("发现 {} 台打印机:", printers.len()); + for p in printers { + println!( + "- name: {}\n id: {}\n state: {:?}\n accepting_jobs: {}\n active_jobs: {}\n color_supported: {}\n ppm: {:?}, ppm_color: {:?}\n paper_percent: {:?}\n page_count: {:?}\n reasons: {:?}\n", + p.dest.name, + p.id, + p.state, + p.accepting_jobs, + p.active_jobs, + p.color_supported, + p.ppm, + p.ppm_color, + p.paper_percent, + p.page_count, + p.reasons + ); + } + Ok(()) +} + +#[derive(Debug, Default)] +struct Args { + list: bool, + file: Option, + printer: Option, + copies: Option, + color: Option, + duplex: Option, + media: Option, + quality: Option, + orientation: Option, + job_name: Option, +} + +impl Args { + fn parse>(mut iter: I) -> Result { + let mut args = Args::default(); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--list" => args.list = true, + "--print" | "-f" | "--file" => { + let v = iter.next().ok_or_else(|| anyhow!("--print 需要文件路径"))?; + args.file = Some(PathBuf::from(v)); + } + "--printer" | "-p" => { + args.printer = + Some(iter.next().ok_or_else(|| anyhow!("--printer 需要值"))?); + } + "--copies" => { + let v = iter.next().ok_or_else(|| anyhow!("--copies 需要值"))?; + args.copies = Some(v.parse()?); + } + "--color" => { + let v = iter.next().ok_or_else(|| anyhow!("--color 需要值"))?; + args.color = Some(parse_color(&v)?); + } + "--duplex" => { + let v = iter.next().ok_or_else(|| anyhow!("--duplex 需要值"))?; + args.duplex = Some(parse_duplex(&v)?); + } + "--media" => { + args.media = Some(iter.next().ok_or_else(|| anyhow!("--media 需要值"))?); + } + "--quality" => { + let v = iter.next().ok_or_else(|| anyhow!("--quality 需要值"))?; + args.quality = Some(parse_quality(&v)?); + } + "--orientation" => { + let v = iter.next().ok_or_else(|| anyhow!("--orientation 需要值"))?; + args.orientation = Some(parse_orientation(&v)?); + } + "--job-name" => { + args.job_name = + Some(iter.next().ok_or_else(|| anyhow!("--job-name 需要值"))?); + } + "--strategy" => { + // keep compatibility with config; no-op but accept for clarity + let v = iter.next().ok_or_else(|| anyhow!("--strategy 需要值"))?; + if v.to_lowercase() == "round_robin" { + args.set_strategy(LoadBalanceStrategy::RoundRobin)?; + } else if v.to_lowercase() == "least_queued" { + args.set_strategy(LoadBalanceStrategy::LeastQueued)?; + } else { + return Err(anyhow!( + "不支持的策略: {} (可选: round_robin, least_queued)", + v + )); + } + } + "--help" | "-h" => { + Args::print_usage(); + std::process::exit(0); + } + other => { + return Err(anyhow!("未知参数: {}", other)); + } + } + } + Ok(args) + } + + fn into_print_params(mut self) -> PrintParams { + let mut params = PrintParams { + copies: self.copies, + duplex: self.duplex, + color: self.color, + media: self.media.take(), + quality: self.quality, + orientation: self.orientation, + job_name: self.job_name.take(), + printer: self.printer.take(), + }; + // convenience: allow -p to set printer + if params.printer.is_none() { + params.printer = self.printer.take(); + } + params + } + + fn set_strategy(&mut self, strategy: LoadBalanceStrategy) -> Result<()> { + // Currently strategy is only configurable via config; accept CLI flag but ensure config matches. + if strategy != LoadBalanceStrategy::LeastQueued { + // Most users rely on config; warn if they expect change. + println!("提示:调度策略请在 config.app.yaml 的 scheduler.strategy 中配置"); + } + Ok(()) + } + + fn print_usage() { + println!( + "用法:\n cups_smoke --list\n cups_smoke --print <文件路径> [选项]\n\n\ + 选项:\n --printer, -p <名称> 指定打印机名称\n --copies 份数\n --color \n --duplex \n --media <介质>\n --quality \n --orientation \n --job-name <名称>\n --help, -h 显示帮助" + ); + } +} + +fn parse_color(input: &str) -> Result { + match input.to_ascii_lowercase().as_str() { + "auto" => Ok(ColorModeSetting::Auto), + "color" => Ok(ColorModeSetting::Color), + "monochrome" | "mono" | "bw" => Ok(ColorModeSetting::Monochrome), + other => Err(anyhow!("无效的 color: {}", other)), + } +} + +fn parse_duplex(input: &str) -> Result { + match input.to_ascii_lowercase().as_str() { + "one_sided" | "single" => Ok(DuplexSetting::OneSided), + "two_sided_long_edge" | "long" => Ok(DuplexSetting::TwoSidedLongEdge), + "two_sided_short_edge" | "short" => Ok(DuplexSetting::TwoSidedShortEdge), + other => Err(anyhow!("无效的 duplex: {}", other)), + } +} + +fn parse_quality(input: &str) -> Result { + match input.to_ascii_lowercase().as_str() { + "draft" => Ok(QualitySetting::Draft), + "normal" => Ok(QualitySetting::Normal), + "high" => Ok(QualitySetting::High), + other => Err(anyhow!("无效的 quality: {}", other)), + } +} + +fn parse_orientation(input: &str) -> Result { + match input.to_ascii_lowercase().as_str() { + "portrait" => Ok(OrientationSetting::Portrait), + "landscape" => Ok(OrientationSetting::Landscape), + other => Err(anyhow!("无效的 orientation: {}", other)), + } +} + diff --git a/src/config.rs b/src/config.rs index 78011ef..8c12279 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,8 @@ pub struct AppConfig { #[serde(default)] pub cups: CupsConfig, #[serde(default)] + pub snmp: SnmpConfig, + #[serde(default)] pub scheduler: SchedulerConfig, #[serde(default)] pub retry: RetryConfig, @@ -38,6 +40,22 @@ pub struct CupsConfig { pub password: Option, } +#[derive(Debug, Clone, Deserialize)] +pub struct SnmpConfig { + /// 是否启用 SNMP 查询耗材/纸量 + #[serde(default = "default_snmp_enabled")] + pub enabled: bool, + /// SNMP community,默认 public + #[serde(default = "default_snmp_community")] + pub community: String, + /// 查询超时(毫秒) + #[serde(default = "default_snmp_timeout_ms")] + pub timeout_ms: u64, + /// 重试次数 + #[serde(default = "default_snmp_retries")] + pub retries: u32, +} + #[derive(Debug, Clone, Deserialize)] pub struct SchedulerConfig { #[serde(default = "default_strategy")] @@ -59,7 +77,7 @@ pub struct SchedulerConfig { pub printer_speeds: std::collections::HashMap, } -#[derive(Debug, Clone, Deserialize, Copy)] +#[derive(Debug, Clone, Deserialize, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LoadBalanceStrategy { RoundRobin, @@ -104,6 +122,17 @@ impl Default for CupsConfig { } } +impl Default for SnmpConfig { + fn default() -> Self { + Self { + enabled: default_snmp_enabled(), + community: default_snmp_community(), + timeout_ms: default_snmp_timeout_ms(), + retries: default_snmp_retries(), + } + } +} + impl Default for SchedulerConfig { fn default() -> Self { Self { @@ -161,6 +190,22 @@ fn default_refresh_secs() -> u64 { 10 } +fn default_snmp_enabled() -> bool { + false +} + +fn default_snmp_community() -> String { + "public".to_string() +} + +fn default_snmp_timeout_ms() -> u64 { + 1500 +} + +fn default_snmp_retries() -> u32 { + 1 +} + fn default_ppm_bw() -> u32 { 30 } diff --git a/src/control_client.rs b/src/control_client.rs index dbcb150..2c7f452 100644 --- a/src/control_client.rs +++ b/src/control_client.rs @@ -1,12 +1,15 @@ -use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, + io::Write, + path::PathBuf, + sync::Arc, + time::Duration, +}; use anyhow::{anyhow, Context, Result}; use tempfile::NamedTempFile; -use tokio::{ - io::AsyncWriteExt, - task::JoinHandle, - time::{sleep, timeout}, -}; +use tokio::io::AsyncReadExt; +use tokio::time::sleep; use tokio_stream::wrappers::ReceiverStream; use tracing::{info, warn}; @@ -22,6 +25,7 @@ use crate::{ scheduler::Scheduler, }; use cups_rs::PrinterState; +use reqwest::StatusCode; const RECONNECT_BASE_MS: u64 = 1000; const RECONNECT_MAX_MS: u64 = 10000; @@ -73,7 +77,11 @@ impl ControlService { hostname_value.clone() }; - // send hello + info!( + client_id = %client_id_value, + printers = printer_payload.len(), + "发送 hello 到控制端" + ); let hello = ClientMessage { msg: Some(client_message::Msg::Hello(Hello { client_id: client_id_value, @@ -138,7 +146,7 @@ async fn handle_print( .ok_or_else(|| anyhow!("缺少打印参数"))?; let params = map_params(params_proto)?; - let temp = TempPdf::new(instr.pdf_data).await?; + let temp = materialize_pdf(&instr).await?; let result = scheduler .dispatch( temp.path(), @@ -265,6 +273,8 @@ fn to_proto_printer(p: &PrinterInfo) -> ProtoPrinter { ppm: p.ppm.unwrap_or_default(), ppm_color: p.ppm_color.unwrap_or_default(), reasons: p.reasons.clone(), + paper_percent: p.paper_percent.unwrap_or(-1), + page_count: p.page_count.unwrap_or(0), } } @@ -325,37 +335,17 @@ fn map_params(params: &ProtoPrintParams) -> Result { struct TempPdf { path: PathBuf, - _handle: JoinHandle>, + _file: NamedTempFile, } impl TempPdf { - async fn new(data: Vec) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = tokio::spawn(async move { - let file = NamedTempFile::new().context("创建临时文件失败")?; - let mut writer = tokio::fs::File::from_std(file.reopen().context("打开临时文件失败")?); - writer - .write_all(&data) - .await - .context("写入临时文件失败")?; - writer.flush().await.context("刷新临时文件失败")?; - let path = file.path().to_path_buf(); - // Hold file until drop to keep on disk - tx.send(path).ok(); - // keep file alive - sleep(Duration::from_secs(300)).await; - Ok(()) - }); - - let path = timeout(Duration::from_secs(5), rx) - .await - .context("等待临时文件路径超时")? - .context("获取临时文件路径失败")?; - - Ok(Self { - path, - _handle: handle, - }) + async fn from_bytes(data: &[u8]) -> Result { + let mut file = NamedTempFile::new().context("创建临时文件失败")?; + file.write_all(data) + .context("写入临时文件失败")?; + file.flush().context("刷新临时文件失败")?; + let path = file.path().to_path_buf(); + Ok(Self { path, _file: file }) } fn path(&self) -> &std::path::Path { @@ -368,3 +358,48 @@ fn hostname() -> Result { .map(|s| s.to_string_lossy().to_string()) .context("读取主机名失败") } + +async fn materialize_pdf(instr: &PrintInstruction) -> Result { + if !instr.pdf_data.is_empty() { + return TempPdf::from_bytes(&instr.pdf_data).await; + } + if !instr.raw_data.is_empty() { + return TempPdf::from_bytes(&instr.raw_data).await; + } + if !instr.url.is_empty() { + return TempPdf::from_url(&instr.url).await; + } + if !instr.local_path.is_empty() { + return TempPdf::from_path(&instr.local_path).await; + } + Err(anyhow!("未提供可用的打印数据(pdf_data/raw_data/url/local_path)")) +} + +impl TempPdf { + async fn from_url(url: &str) -> Result { + let resp = reqwest::get(url).await.context("下载打印文件失败")?; + if resp.status() == StatusCode::NO_CONTENT { + return Err(anyhow!("下载文件为空")); + } + let resp = resp.error_for_status().context("下载文件返回错误状态")?; + let bytes = resp.bytes().await.context("读取下载内容失败")?; + if bytes.is_empty() { + return Err(anyhow!("下载文件为空")); + } + Self::from_bytes(&bytes).await + } + + async fn from_path(path: &str) -> Result { + let mut file = tokio::fs::File::open(path) + .await + .with_context(|| format!("打开打印文件失败: {}", path))?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf) + .await + .with_context(|| format!("读取打印文件失败: {}", path))?; + if buf.is_empty() { + return Err(anyhow!("读取文件为空")); + } + Self::from_bytes(&buf).await + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eeb5902 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +pub mod config; +pub mod control_client; +pub mod models; +pub mod printer; +pub mod proto; +pub mod scheduler; + + diff --git a/src/main.rs b/src/main.rs index e94d1b5..b1d38b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,12 @@ -mod config; -mod control_client; -mod models; -mod printer; -mod proto; -mod scheduler; - use std::sync::Arc; use anyhow::Context; -use control_client::ControlService; -use printer::{CupsClient, SharedPrinterAdapter}; -use scheduler::Scheduler; +use print_backend::{ + config, + control_client::ControlService, + printer::{CupsClient, SharedPrinterAdapter}, + scheduler::Scheduler, +}; use tracing_subscriber::{fmt, EnvFilter}; #[tokio::main] @@ -41,7 +37,7 @@ async fn main() -> Result<(), anyhow::Error> { } } - let adapter: SharedPrinterAdapter = Arc::new(CupsClient::default()); + let adapter: SharedPrinterAdapter = Arc::new(CupsClient::with_snmp(Some(config.snmp.clone()))); let scheduler = Arc::new(Scheduler::new( config.scheduler.strategy, config.scheduler.refresh_interval_secs, diff --git a/src/printer.rs b/src/printer.rs index 19db387..2a1a4b4 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,4 +1,4 @@ -use std::{path::Path, sync::Arc}; +use std::{collections::HashMap, path::Path, sync::Arc, time::Duration}; use anyhow::{Context, Result}; use async_trait::async_trait; @@ -12,6 +12,7 @@ use tokio::task::spawn_blocking; use tracing::warn; use url::Url; +use crate::config::SnmpConfig; use crate::models::{PrintDefaults, PrintParams}; #[derive(Debug, Clone)] @@ -27,6 +28,8 @@ pub struct PrinterInfo { pub device_uri: Option, pub host: Option, pub accepting_jobs: bool, + pub paper_percent: Option, + pub page_count: Option, } impl PrinterInfo { @@ -65,12 +68,21 @@ pub trait PrintAdapter: Send + Sync { } #[derive(Debug, Default)] -pub struct CupsClient; +pub struct CupsClient { + snmp: Option, +} + +impl CupsClient { + pub fn with_snmp(snmp: Option) -> Self { + Self { snmp } + } +} #[async_trait] impl PrintAdapter for CupsClient { async fn list_printers(&self) -> Result> { - spawn_blocking(|| { + let snmp_cfg = self.snmp.clone(); + spawn_blocking(move || { let destinations = get_all_destinations().context("获取打印机列表失败")?; let mut infos = Vec::with_capacity(destinations.len()); @@ -78,14 +90,7 @@ impl PrintAdapter for CupsClient { let state = dest.state(); let reasons = dest.state_reasons(); let options = dest.get_options(); - let color_supported = options - .get("print-color-mode-supported") - .map(|modes| { - modes - .split(',') - .any(|m| m.trim().eq_ignore_ascii_case("color")) - }) - .unwrap_or(true); + let color_supported = detect_color_supported(&options); let ppm = parse_speed_option(options.get("printer-speed")); let ppm_color = parse_speed_option( options @@ -107,6 +112,17 @@ impl PrintAdapter for CupsClient { let id = derive_printer_id(&dest, device_uri.as_deref(), host.as_deref()); let accepting_jobs = dest.is_accepting_jobs(); + let (paper_percent, page_count) = match (&snmp_cfg, host.as_deref()) { + (Some(snmp_cfg), Some(h)) if snmp_cfg.enabled => match poll_snmp_paper_page(h, snmp_cfg) { + Ok(v) => v, + Err(e) => { + warn!("SNMP 查询 {} 失败: {}", h, e); + (None, None) + } + }, + _ => (None, None), + }; + infos.push(PrinterInfo { id, dest, @@ -119,6 +135,8 @@ impl PrintAdapter for CupsClient { device_uri, host, accepting_jobs, + paper_percent, + page_count, }); } Ok::<_, anyhow::Error>(infos) @@ -213,8 +231,195 @@ impl PartialEq for PrinterInfo { && self.device_uri == other.device_uri && self.host == other.host && self.accepting_jobs == other.accepting_jobs + && self.paper_percent == other.paper_percent + && self.page_count == other.page_count } } impl Eq for PrinterInfo {} +fn detect_color_supported(options: &std::collections::HashMap) -> bool { + // Prefer explicit color mode indicators; otherwise be conservative (default false). + for key in [ + "print-color-mode-supported", + "printer-color-mode-supported", + "color-supported", + ] { + if let Some(v) = options.get(key) { + let has_color = v + .split(|c| c == ',' || c == ' ' || c == ';') + .any(|m| { + let l = m.trim().to_ascii_lowercase(); + l == "color" || l.contains("rgb") || l.contains("cmyk") + }); + return has_color; + } + } + + if let Some(v) = options.get("ColorModel").or_else(|| options.get("color-models")) { + let has_color = v + .split(|c| c == ',' || c == ' ' || c == ';') + .any(|m| { + let l = m.trim().to_ascii_lowercase(); + l.contains("rgb") || l.contains("cmyk") || l == "color" + }); + return has_color; + } + + false +} + +fn poll_snmp_paper_page(host: &str, cfg: &SnmpConfig) -> Result<(Option, Option)> { + use snmp::SyncSession; + let addr = format!("{host}:161"); + let timeout = Duration::from_millis(cfg.timeout_ms); + let mut session = SyncSession::new( + addr.as_str(), + cfg.community.as_bytes(), + Some(timeout), + cfg.retries as i32, + ) + .context("创建 SNMP 会话失败")?; + + let paper = compute_percent( + &mut session, + &[1, 3, 6, 1, 2, 1, 43, 8, 2, 1, 9, 1], // max: prtInputMaxCapacity + &[1, 3, 6, 1, 2, 1, 43, 8, 2, 1, 10, 1], // level: prtInputCurrentLevel + ); + let page_count = read_counter_first( + &mut session, + &[1, 3, 6, 1, 2, 1, 43, 10, 2, 1, 4, 1], // prtMarkerLifeCount + ); + Ok((paper, page_count)) +} + +fn compute_percent( + session: &mut snmp::SyncSession, + max_oid: &[u32], + level_oid: &[u32], +) -> Option { + use snmp::{ObjIdBuf, Value}; + + let max_map = walk_i32(session, max_oid); + if max_map.is_empty() { + return None; + } + + let mut best: Option = None; + let mut cursor = level_oid.to_vec(); + loop { + let pdu = match session.getbulk(&[cursor.as_slice()], 0, 10) { + Ok(p) => p, + Err(_) => break, + }; + let mut progressed = false; + for (oid, val) in pdu.varbinds { + let mut buf: ObjIdBuf = [0; 128]; + let name = match oid.read_name(&mut buf) { + Ok(n) => n.to_vec(), + Err(_) => continue, + }; + if !name.starts_with(level_oid) { + return best; + } + progressed = true; + cursor = name.clone(); + if let Value::Integer(level) = val { + if level <= 0 { + continue; + } + // find matching max by full oid or last index + let max_v = max_map + .get(&name) + .or_else(|| { + name.last().and_then(|last| { + max_map + .iter() + .find(|(k, _)| k.last() == Some(last)) + .map(|(_, v)| v) + }) + }); + if let Some(max_v) = max_v { + if *max_v > 0 { + let pct = ((level as f64) * 100.0 / (*max_v as f64)).round() as i32; + best = Some(best.map_or(pct, |b| b.min(pct))); + } + } + } + } + if !progressed { + break; + } + } + best +} + +fn walk_i32(session: &mut snmp::SyncSession, base: &[u32]) -> HashMap, i32> { + use snmp::{ObjIdBuf, Value}; + + let mut results = HashMap::new(); + let mut cursor = base.to_vec(); + loop { + let pdu = match session.getbulk(&[cursor.as_slice()], 0, 10) { + Ok(p) => p, + Err(_) => break, + }; + let mut progressed = false; + for (oid, val) in pdu.varbinds.clone() { + let mut buf: ObjIdBuf = [0; 128]; + let name = match oid.read_name(&mut buf) { + Ok(n) => n.to_vec(), + Err(_) => continue, + }; + if !name.starts_with(base) { + return results; + } + progressed = true; + cursor = name.clone(); + if let Value::Integer(v) = val { + if v > 0 && v <= i32::MAX as i64 { + results.insert(name, v as i32); + } + } + } + if !progressed { + break; + } + } + results +} + +fn read_counter_first(session: &mut snmp::SyncSession, base: &[u32]) -> Option { + use snmp::{ObjIdBuf, Value}; + let mut cursor = base.to_vec(); + loop { + let pdu = match session.getbulk(&[cursor.as_slice()], 0, 10) { + Ok(p) => p, + Err(_) => break, + }; + let mut progressed = false; + for (oid, val) in pdu.varbinds.clone() { + let mut buf: ObjIdBuf = [0; 128]; + let name = match oid.read_name(&mut buf) { + Ok(n) => n.to_vec(), + Err(_) => continue, + }; + if !name.starts_with(base) { + return None; + } + progressed = true; + cursor = name.clone(); + match val { + Value::Integer(v) if v >= 0 => return Some(v as u64), + Value::Counter32(v) => return Some(v as u64), + Value::Counter64(v) => return Some(v), + _ => continue, + } + } + if !progressed { + break; + } + } + None +} + diff --git a/src/scheduler.rs b/src/scheduler.rs index 1e550b3..e056d1c 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -352,6 +352,8 @@ mod tests { device_uri: None, host: None, accepting_jobs: true, + paper_percent: None, + page_count: None, } } diff --git a/yipe/grpc_summary.md b/yipe/grpc_summary.md new file mode 100644 index 0000000..b6c210b --- /dev/null +++ b/yipe/grpc_summary.md @@ -0,0 +1,132 @@ +# gRPC 控制流协议摘要(ControlService) + +## 接口 +- 服务:`control.v1.Control` +- RPC:`ControlStream(stream ClientMessage) returns (stream ServerMessage)` +- 双向长连接,客户端与控制端可任意时序发消息。 + +## 消息结构(摘自 proto/control.proto) +- `ClientMessage` oneof: + - `Hello`:client_id、version、hostname、printers[] + - `PrintResult`:request_id、ok、printer、message、retries_used + - `Pong`:nonce(对应 Ping) + - `PrinterUpdate`:printers[] +- `ServerMessage` oneof: + - `PrintInstruction`:request_id、pdf_data(bytes)、PrintParams + - `Ping`:nonce +- `PrintParams`:copies、duplex(one_sided/long_edge/short_edge)、color(auto/color/monochrome)、media、quality(draft/normal/high)、orientation(portrait/landscape)、job_name、printer(optional 指定目标打印机) +- `PrinterInfo`:id、name、host、uri、state、accepting_jobs、color_supported、active_jobs、ppm、ppm_color、reasons[] + +## 客户端行为(当前实现) +- 建立流后立即发送 `Hello`,携带当前打印机快照。 +- 后台按配置刷新(默认 10s,最小 5s)检测变更,发送 `PrinterUpdate`。 +- 收到 `Ping{nonce}` 立即回 `Pong{nonce}`。 +- 收到 `PrintInstruction`: + - 将 pdf_data 落盘临时文件。 + - 按调度策略(默认 least_queued,支持指定 printer;尊重彩色需求和可用性)分发到 CUPS。 + - 成功/失败后返回 `PrintResult{ok, printer, message, retries_used}`。 +- 连接出错自动指数退避重连(1s 起,最大 10s),控制端无需主动重连。 + +## 字段语义补充 +- `color_supported`:客户端基于 CUPS 选项判定,保守为 false 以避免误判。 +- `state`:CUPS 状态字符串(Idle/Processing/Stopped 等)。 +- `reasons`:CUPS 状态原因列表(offline/jam/paper-out 等)。 +- `ppm`/`ppm_color`:若 CUPS 未提供则为缺省/0。 + +## 控制端最小交互建议 +1) 等待首条 `Hello`,登记客户端与打印机列表。 +2) 定期/按需发送 `Ping` 保活。 +3) 下发打印用 `PrintInstruction`(带 request_id 以关联结果)。 +4) 接收 `PrintResult` 做业务回执;可订阅 `PrinterUpdate` 更新状态。 + +## 开发/测试命令 +- 生成/查看 proto:文件位于 `proto/control.proto`,由 `tonic-build` 在 `build.rs` 生成绑定。 +- 本地 smoke(无控制端):`LIBCLANG_PATH=/usr/lib CONFIG_PATH=config/app.yaml cargo run --bin cups_smoke -- --list` / `--print -p `。 + +## 原始 proto 文件 +路径:`proto/control.proto` + +``` +syntax = "proto3"; + +package control.v1; + +service Control { + rpc ControlStream(stream ClientMessage) returns (stream ServerMessage); +} + +message ClientMessage { + oneof msg { + Hello hello = 1; + PrintResult result = 2; + Pong pong = 3; + PrinterUpdate printers = 4; + } +} + +message ServerMessage { + oneof msg { + PrintInstruction print = 1; + Ping ping = 2; + } +} + +message Hello { + string client_id = 1; + string version = 2; + string hostname = 3; + repeated PrinterInfo printers = 4; +} + +message Ping { + string nonce = 1; +} + +message Pong { + string nonce = 1; +} + +message PrintInstruction { + string request_id = 1; + bytes pdf_data = 2; + PrintParams params = 3; +} + +message PrintParams { + uint32 copies = 1; + string duplex = 2; // one_sided, long_edge, short_edge + string color = 3; // auto, color, monochrome + string media = 4; + string quality = 5; // draft, normal, high + string orientation = 6; // portrait, landscape + string job_name = 7; + string printer = 8; +} + +message PrintResult { + string request_id = 1; + bool ok = 2; + string printer = 3; + string message = 4; + uint32 retries_used = 5; +} + +message PrinterUpdate { + repeated PrinterInfo printers = 1; +} + +message PrinterInfo { + string id = 1; + string name = 2; + string host = 3; + string uri = 4; + string state = 5; + bool accepting_jobs = 6; + bool color_supported = 7; + uint32 active_jobs = 8; + uint32 ppm = 9; + uint32 ppm_color = 10; + repeated string reasons = 11; +} +``` +