From 5789a4346a727f0fe7ff0364dfb1eba9781164cb Mon Sep 17 00:00:00 2001 From: lan Date: Sun, 14 Dec 2025 18:25:26 +0800 Subject: [PATCH] Update dependencies and refactor application structure - Updated various dependencies in `Cargo.toml` to their latest versions, including `tonic`, `tokio`, and `url`. - Refactored `ServerConfig` to include a new `grpc_target` field, replacing the previous bind and port fields. - Removed the `routes.rs` file and integrated its functionality into the main application logic. - Enhanced the `PrinterInfo` struct to include additional fields for printer details. - Simplified the main application flow by directly using the `ControlService` for handling requests. - Cleaned up unused code and improved overall organization of the project structure. --- Cargo.lock | 1007 +++++++++++++++++++++++++++++++++++------ Cargo.toml | 14 +- build.rs | 12 + proto/control.proto | 82 ++++ src/config.rs | 26 +- src/control_client.rs | 370 +++++++++++++++ src/main.rs | 43 +- src/models.rs | 14 - src/printer.rs | 110 ++++- src/proto.rs | 4 + src/routes.rs | 502 -------------------- src/scheduler.rs | 68 ++- 12 files changed, 1510 insertions(+), 742 deletions(-) create mode 100644 build.rs create mode 100644 proto/control.proto create mode 100644 src/control_client.rs create mode 100644 src/proto.rs delete mode 100644 src/routes.rs diff --git a/Cargo.lock b/Cargo.lock index ea2cf1b..c0874ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,28 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -61,12 +83,6 @@ 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" @@ -75,58 +91,47 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.7.9" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", + "bitflags 1.3.2", "bytes", "futures-util", "http", "http-body", - "http-body-util", "hyper", - "hyper-util", "itoa", "matchit", "memchr", "mime", - "multer", "percent-encoding", "pin-project-lite", "rustversion", "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", "sync_wrapper", - "tokio", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] name = "axum-core" -version = "0.4.5" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", "futures-util", "http", "http-body", - "http-body-util", "mime", - "pin-project-lite", "rustversion", - "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -141,10 +146,10 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -155,12 +160,27 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[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 = "bumpalo" version = "3.19.0" @@ -240,6 +260,25 @@ 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 = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cups_rs" version = "0.3.0" @@ -253,6 +292,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -296,6 +356,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -320,6 +392,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -338,6 +416,27 @@ dependencies = [ "pin-utils", ] +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -356,6 +455,31 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -382,35 +506,41 @@ dependencies = [ ] [[package]] -name = "http" -version = "1.4.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", + "fnv", "itoa", ] [[package]] name = "http-body" -version = "1.0.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", -] - -[[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", - "http-body", "pin-project-lite", ] @@ -428,39 +558,38 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "atomic-waker", "bytes", "futures-channel", "futures-core", + "futures-util", + "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "pin-utils", - "smallvec", + "socket2 0.5.10", "tokio", + "tower-service", + "tracing", + "want", ] [[package]] -name = "hyper-util" -version = "0.1.19" +name = "hyper-timeout" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", "hyper", "pin-project-lite", "tokio", - "tower-service", + "tokio-io-timeout", ] [[package]] @@ -487,6 +616,118 @@ 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 = "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 = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -497,6 +738,15 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -550,12 +800,24 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.2.0" @@ -601,21 +863,10 @@ dependencies = [ ] [[package]] -name = "multer" -version = "3.1.0" +name = "multimap" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "nom" @@ -663,6 +914,36 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.12.1", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -681,6 +962,24 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[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" @@ -697,19 +996,25 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "axum", - "base64", "config", "cups_rs", + "hostname", + "prost", + "prost-types", + "protoc-bin-vendored", "serde", "serde_json", "serde_yaml", + "sha2", "tempfile", "thiserror", "tokio", - "tower-http", + "tokio-stream", + "tonic", + "tonic-build", "tracing", "tracing-subscriber", + "url", ] [[package]] @@ -721,6 +1026,123 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes", + "heck", + "itertools 0.12.1", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +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 = "quote" version = "1.0.42" @@ -736,6 +1158,36 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + [[package]] name = "regex" version = "1.12.2" @@ -777,7 +1229,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -839,42 +1291,30 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.12.1", "itoa", "ryu", "serde", "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -899,12 +1339,28 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.1" @@ -916,10 +1372,10 @@ dependencies = [ ] [[package]] -name = "spin" -version = "0.9.8" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" @@ -934,9 +1390,20 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "1.0.2" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "tempfile" @@ -945,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -980,21 +1447,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.6.0" @@ -1007,33 +1495,84 @@ dependencies = [ ] [[package]] -name = "tower" -version = "0.5.2" +name = "tokio-stream" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", - "futures-util", "pin-project-lite", - "sync_wrapper", "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", "tower-layer", "tower-service", "tracing", ] [[package]] -name = "tower-http" -version = "0.5.2" +name = "tonic-build" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" dependencies = [ - "bitflags", - "bytes", - "http", - "http-body", - "http-body-util", + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -1057,7 +1596,6 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1113,6 +1651,18 @@ dependencies = [ "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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -1125,6 +1675,24 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "valuable" version = "0.1.1" @@ -1137,6 +1705,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[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" @@ -1197,6 +1774,28 @@ dependencies = [ "unicode-ident", ] +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -1256,13 +1855,22 @@ dependencies = [ "windows-link", ] +[[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.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1274,6 +1882,22 @@ dependencies = [ "windows-link", ] +[[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" @@ -1281,58 +1905,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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_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" @@ -1345,6 +2017,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yaml-rust2" version = "0.8.1" @@ -1356,6 +2034,29 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.31" @@ -1375,3 +2076,57 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 1572e85..90b6480 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,6 @@ edition = "2024" [dependencies] anyhow = "1" async-trait = "0.1" -axum = { version = "0.7", features = ["multipart", "json"] } -base64 = "0.21" config = { version = "0.14", default-features = false, features = ["yaml"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -16,6 +14,16 @@ thiserror = "1" tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs", "signal", "net"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } -tower-http = { version = "0.5", features = ["trace"] } cups_rs = "0.3" tempfile = "3" +tonic = { version = "0.11", features = ["transport"] } +prost = "0.12" +prost-types = "0.12" +tokio-stream = "0.1" +hostname = "0.3" +sha2 = "0.10" +url = "2" + +[build-dependencies] +tonic-build = "0.11" +protoc-bin-vendored = "3" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..1f9149e --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +fn main() { + let protoc = protoc_bin_vendored::protoc_bin_path().expect("failed to fetch protoc"); + unsafe { + std::env::set_var("PROTOC", protoc); + } + + tonic_build::configure() + .build_server(false) + .compile(&["proto/control.proto"], &["proto"]) + .expect("failed to compile protobuf"); +} + diff --git a/proto/control.proto b/proto/control.proto new file mode 100644 index 0000000..e7039b9 --- /dev/null +++ b/proto/control.proto @@ -0,0 +1,82 @@ +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; +} + diff --git a/src/config.rs b/src/config.rs index 72fd9e5..78011ef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,13 +22,9 @@ pub struct AppConfig { #[derive(Debug, Clone, Deserialize)] pub struct ServerConfig { - #[serde(default = "default_bind")] - pub bind: String, - #[serde(default = "default_port")] - pub port: u16, - /// 请求体大小上限(字节) - #[serde(default = "default_body_limit")] - pub body_limit_bytes: u64, + /// gRPC 控制端地址,例如 http://127.0.0.1:50051 + #[serde(default = "default_grpc_target")] + pub grpc_target: String, } #[derive(Debug, Clone, Deserialize)] @@ -93,9 +89,7 @@ impl RetryConfig { impl Default for ServerConfig { fn default() -> Self { Self { - bind: default_bind(), - port: default_port(), - body_limit_bytes: default_body_limit(), + grpc_target: default_grpc_target(), } } } @@ -147,16 +141,8 @@ impl AppConfig { } } -fn default_bind() -> String { - "0.0.0.0".to_string() -} - -fn default_port() -> u16 { - 8080 -} - -fn default_body_limit() -> u64 { - 20 * 1024 * 1024 // 20MB +fn default_grpc_target() -> String { + "http://127.0.0.1:50051".to_string() } fn default_attempts() -> usize { diff --git a/src/control_client.rs b/src/control_client.rs new file mode 100644 index 0000000..dbcb150 --- /dev/null +++ b/src/control_client.rs @@ -0,0 +1,370 @@ +use std::{collections::HashMap, 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_stream::wrappers::ReceiverStream; +use tracing::{info, warn}; + +use crate::{ + config::AppConfig, + models::{ColorModeSetting, DuplexSetting, OrientationSetting, PrintParams, QualitySetting}, + printer::PrinterInfo, + proto::control::{ + client_message, control_client::ControlClient, server_message, ClientMessage, Hello, Ping, + Pong, PrintInstruction, PrintParams as ProtoPrintParams, PrintResult, PrinterInfo as ProtoPrinter, + PrinterUpdate, + }, + scheduler::Scheduler, +}; +use cups_rs::PrinterState; + +const RECONNECT_BASE_MS: u64 = 1000; +const RECONNECT_MAX_MS: u64 = 10000; + +pub struct ControlService { + scheduler: Arc, + config: AppConfig, +} + +impl ControlService { + pub fn new(scheduler: Arc, config: AppConfig) -> Self { + Self { scheduler, config } + } + + pub async fn run(&self) -> Result<()> { + let mut backoff = RECONNECT_BASE_MS; + loop { + match self.connect_and_run().await { + Ok(()) => return Ok(()), + Err(e) => { + warn!("控制流连接失败: {e}"); + sleep(Duration::from_millis(backoff)).await; + backoff = (backoff * 2).min(RECONNECT_MAX_MS); + } + } + } + } + + async fn connect_and_run(&self) -> Result<()> { + let mut client = ControlClient::connect(self.config.server.grpc_target.clone()) + .await + .context("连接 gRPC 控制端失败")?; + + let (tx, rx) = tokio::sync::mpsc::channel::(32); + let outbound = ReceiverStream::new(rx); + let response = client + .control_stream(outbound) + .await + .context("建立控制流失败")?; + let mut stream = response.into_inner(); + + let printers = collect_printers(&self.scheduler).await; + let printer_payload: Vec = + printers.iter().map(to_proto_printer).collect(); + let hostname_value = hostname().unwrap_or_default(); + let client_id_value = if hostname_value.is_empty() { + "unknown-client".to_string() + } else { + hostname_value.clone() + }; + + // send hello + let hello = ClientMessage { + msg: Some(client_message::Msg::Hello(Hello { + client_id: client_id_value, + version: env!("CARGO_PKG_VERSION").to_string(), + hostname: hostname_value, + printers: printer_payload.clone(), + })), + }; + let _ = tx.send(hello).await; + + let tx_for_print = tx.clone(); + let scheduler = self.scheduler.clone(); + let config = self.config.clone(); + let watch_tx = tx.clone(); + let watch_scheduler = self.scheduler.clone(); + let watch_interval = + Duration::from_secs(self.config.scheduler.refresh_interval_secs.max(5)); + tokio::spawn(async move { + if let Err(e) = + watch_printer_changes(watch_scheduler, watch_tx, printers, watch_interval).await + { + warn!("打印机变更上报失败: {e}"); + } + }); + + while let Some(msg) = stream.message().await? { + match msg.msg { + Some(server_message::Msg::Print(print)) => { + let tx = tx_for_print.clone(); + let scheduler = scheduler.clone(); + let config = config.clone(); + tokio::spawn(async move { + if let Err(e) = handle_print(print, scheduler, config, tx).await { + warn!("处理打印指令失败: {e}"); + } + }); + } + Some(server_message::Msg::Ping(Ping { nonce })) => { + let _ = tx_for_print + .send(ClientMessage { + msg: Some(client_message::Msg::Pong(Pong { nonce })), + }) + .await; + } + None => warn!("收到空指令"), + } + } + + Err(anyhow!("控制流结束")) + } +} + +async fn handle_print( + instr: PrintInstruction, + scheduler: Arc, + config: AppConfig, + tx: tokio::sync::mpsc::Sender, +) -> Result<()> { + let params_proto = instr + .params + .as_ref() + .ok_or_else(|| anyhow!("缺少打印参数"))?; + let params = map_params(params_proto)?; + + let temp = TempPdf::new(instr.pdf_data).await?; + let result = scheduler + .dispatch( + temp.path(), + ¶ms, + &config.defaults, + &config.retry, + ) + .await; + + match result { + Ok((job, retries_used)) => { + info!( + printer = %job.printer, + job_id = job.job_id, + retries = retries_used, + "打印任务完成" + ); + send_result( + tx, + PrintResult { + request_id: instr.request_id, + ok: true, + printer: job.printer, + message: "".to_string(), + retries_used: retries_used as u32, + }, + ) + .await; + } + Err(e) => { + warn!(error = %e, "打印任务失败"); + send_result( + tx, + PrintResult { + request_id: instr.request_id, + ok: false, + printer: "".to_string(), + message: e.to_string(), + retries_used: 0, + }, + ) + .await; + } + } + + Ok(()) +} + +async fn send_result( + tx: tokio::sync::mpsc::Sender, + result: PrintResult, +) { + if let Err(e) = tx + .send(ClientMessage { + msg: Some(client_message::Msg::Result(result)), + }) + .await + { + warn!("发送打印结果失败: {e}"); + } +} + +async fn collect_printers(scheduler: &Scheduler) -> Vec { + match scheduler.refresh_snapshot().await { + Ok(list) => list, + Err(e) => { + warn!("刷新打印机列表失败: {e}"); + scheduler.snapshot().await + } + } +} + +async fn watch_printer_changes( + scheduler: Arc, + tx: tokio::sync::mpsc::Sender, + mut last: Vec, + interval: Duration, +) -> Result<()> { + loop { + sleep(interval).await; + let current = collect_printers(&scheduler).await; + if printers_changed(&last, ¤t) { + let payload: Vec = current.iter().map(to_proto_printer).collect(); + tx.send(ClientMessage { + msg: Some(client_message::Msg::Printers(PrinterUpdate { + printers: payload, + })), + }) + .await + .map_err(|e| anyhow!("发送打印机更新失败: {e}"))?; + last = current; + } + } +} + +fn printers_changed(prev: &[PrinterInfo], curr: &[PrinterInfo]) -> bool { + if prev.len() != curr.len() { + return true; + } + + let prev_map: HashMap<&str, &PrinterInfo> = + prev.iter().map(|p| (p.id.as_str(), p)).collect(); + + for printer in curr { + match prev_map.get(printer.id.as_str()) { + Some(existing) if *existing == printer => continue, + _ => return true, + } + } + + false +} + +fn to_proto_printer(p: &PrinterInfo) -> ProtoPrinter { + ProtoPrinter { + id: p.id.clone(), + name: p.dest.name.clone(), + host: p.host.clone().unwrap_or_default(), + uri: p.device_uri.clone().unwrap_or_default(), + state: printer_state_label(&p.state), + accepting_jobs: p.accepting_jobs, + color_supported: p.color_supported, + active_jobs: p.active_jobs as u32, + ppm: p.ppm.unwrap_or_default(), + ppm_color: p.ppm_color.unwrap_or_default(), + reasons: p.reasons.clone(), + } +} + +fn printer_state_label(state: &PrinterState) -> String { + match state { + PrinterState::Idle => "idle", + PrinterState::Processing => "processing", + PrinterState::Stopped => "stopped", + PrinterState::Unknown => "unknown", + } + .to_string() +} + +fn map_params(params: &ProtoPrintParams) -> Result { + Ok(PrintParams { + copies: Some(params.copies.max(1)), + duplex: Some(match params.duplex.as_str() { + "one_sided" | "" => DuplexSetting::OneSided, + "long_edge" => DuplexSetting::TwoSidedLongEdge, + "short_edge" => DuplexSetting::TwoSidedShortEdge, + other => return Err(anyhow!("未知双面选项: {other}")), + }), + color: Some(match params.color.as_str() { + "auto" | "" => ColorModeSetting::Auto, + "color" => ColorModeSetting::Color, + "monochrome" => ColorModeSetting::Monochrome, + other => return Err(anyhow!("未知色彩选项: {other}")), + }), + media: if params.media.is_empty() { + None + } else { + Some(params.media.clone()) + }, + quality: Some(match params.quality.as_str() { + "draft" => QualitySetting::Draft, + "high" => QualitySetting::High, + "" | "normal" => QualitySetting::Normal, + other => return Err(anyhow!("未知质量选项: {other}")), + }), + orientation: match params.orientation.as_str() { + "" => None, + "portrait" => Some(OrientationSetting::Portrait), + "landscape" => Some(OrientationSetting::Landscape), + other => return Err(anyhow!("未知方向选项: {other}")), + }, + job_name: if params.job_name.is_empty() { + None + } else { + Some(params.job_name.clone()) + }, + printer: if params.printer.is_empty() { + None + } else { + Some(params.printer.clone()) + }, + }) +} + +struct TempPdf { + path: PathBuf, + _handle: JoinHandle>, +} + +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, + }) + } + + fn path(&self) -> &std::path::Path { + &self.path + } +} + +fn hostname() -> Result { + hostname::get() + .map(|s| s.to_string_lossy().to_string()) + .context("读取主机名失败") +} diff --git a/src/main.rs b/src/main.rs index 189c091..e94d1b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,16 @@ mod config; -mod error; +mod control_client; mod models; mod printer; -mod routes; +mod proto; mod scheduler; -use std::{net::SocketAddr, sync::Arc}; +use std::sync::Arc; use anyhow::Context; -use axum::serve; +use control_client::ControlService; use printer::{CupsClient, SharedPrinterAdapter}; -use routes::{build_router, AppState}; use scheduler::Scheduler; -use tower_http::{limit::RequestBodyLimitLayer, trace::TraceLayer}; use tracing_subscriber::{fmt, EnvFilter}; #[tokio::main] @@ -54,33 +52,14 @@ async fn main() -> Result<(), anyhow::Error> { config.scheduler.printer_speeds.clone(), )); - let state = AppState { - scheduler, - config: config.clone(), - }; + let service = ControlService::new(scheduler, config.clone()); - let app = build_router(state) - .layer(TraceLayer::new_for_http()) - .layer(RequestBodyLimitLayer::new( - config.server - .body_limit_bytes - .try_into() - .unwrap_or(usize::MAX), - )); - - let addr: SocketAddr = format!("{}:{}", config.server.bind, config.server.port) - .parse() - .context("解析监听地址失败")?; - let listener = tokio::net::TcpListener::bind(addr) - .await - .context("监听端口失败")?; - - tracing::info!("服务启动于 http://{}", listener.local_addr()?); - - serve(listener, app) - .with_graceful_shutdown(shutdown_signal()) - .await - .context("HTTP 服务异常")?; + tokio::select! { + res = service.run() => res.context("控制客户端运行失败")?, + _ = shutdown_signal() => { + tracing::info!("收到退出信号,退出"); + } + } Ok(()) } diff --git a/src/models.rs b/src/models.rs index d8e13e9..f25db0a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -175,17 +175,3 @@ impl PrintParams { } } -#[derive(Debug, Deserialize)] -pub struct JsonPrintRequest { - pub pdf_base64: String, - pub params: PrintParams, -} - -#[derive(Debug, Serialize)] -pub struct PrintJobResponse { - pub job_id: i32, - pub printer: String, - pub strategy: String, - pub retries_used: usize, -} - diff --git a/src/printer.rs b/src/printer.rs index 73ca818..19db387 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -7,22 +7,31 @@ use cups_rs::{ job::{cancel_job, create_job_with_options, FORMAT_PDF}, Destination, PrinterState, }; +use sha2::{Digest, Sha256}; use tokio::task::spawn_blocking; -use tracing::{debug, warn}; +use tracing::warn; +use url::Url; use crate::models::{PrintDefaults, PrintParams}; #[derive(Debug, Clone)] pub struct PrinterInfo { + pub id: String, pub dest: Destination, pub state: PrinterState, pub reasons: Vec, pub active_jobs: usize, + pub color_supported: bool, + pub ppm: Option, + pub ppm_color: Option, + pub device_uri: Option, + pub host: Option, + pub accepting_jobs: bool, } impl PrinterInfo { pub fn healthy(&self) -> bool { - self.dest.is_accepting_jobs() && self.state.is_available() && !self.has_error_reason() + self.accepting_jobs && self.state.is_available() && !self.has_error_reason() } fn has_error_reason(&self) -> bool { @@ -53,7 +62,6 @@ pub trait PrintAdapter: Send + Sync { params: &PrintParams, defaults: &PrintDefaults, ) -> Result; - async fn cancel(&self, job_id: i32) -> Result<()>; } #[derive(Debug, Default)] @@ -69,15 +77,48 @@ impl PrintAdapter for CupsClient { for dest in destinations { 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 ppm = parse_speed_option(options.get("printer-speed")); + let ppm_color = parse_speed_option( + options + .get("printer-speed-color-supported") + .or_else(|| options.get("printer-color-ppm-supported")), + ); let active_jobs = get_active_jobs(Some(dest.name.as_str())) .map(|jobs| jobs.len()) .unwrap_or(0); + let device_uri = options + .get("device-uri") + .cloned() + .or_else(|| options.get("printer-uri-supported").cloned()); + let host = device_uri + .as_deref() + .and_then(extract_host_from_uri) + .or_else(|| options.get("printer-host").map(|s| s.to_string())); + let id = derive_printer_id(&dest, device_uri.as_deref(), host.as_deref()); + let accepting_jobs = dest.is_accepting_jobs(); + infos.push(PrinterInfo { + id, dest, state, reasons, active_jobs, + color_supported, + ppm, + ppm_color, + device_uri, + host, + accepting_jobs, }); } Ok::<_, anyhow::Error>(infos) @@ -116,13 +157,64 @@ impl PrintAdapter for CupsClient { .await .context("提交打印任务失败")? } - - async fn cancel(&self, job_id: i32) -> Result<()> { - spawn_blocking(move || cancel_job(job_id).context("取消打印作业失败")) - .await - .context("取消任务失败")? - } } pub type SharedPrinterAdapter = Arc; +fn parse_speed_option(input: Option<&String>) -> Option { + input.and_then(|s| s.trim().parse::().ok()) +} + +fn derive_printer_id( + dest: &Destination, + device_uri: Option<&str>, + host: Option<&str>, +) -> String { + let mut hasher = Sha256::new(); + hasher.update(dest.name.as_bytes()); + if let Some(uri) = device_uri { + hasher.update(uri.as_bytes()); + } + if let Some(host) = host { + hasher.update(host.as_bytes()); + } + + let hash = hasher.finalize(); + let mut hex = format!("{:x}", hash); + hex.truncate(12); + format!("prn-{hex}") +} + +fn extract_host_from_uri(uri: &str) -> Option { + Url::parse(uri) + .ok() + .and_then(|u| u.host_str().map(|h| h.to_string())) + .or_else(|| { + uri.split("//") + .nth(1) + .and_then(|rest| { + rest.split(['/', ':', '?']) + .filter(|s| !s.is_empty()) + .next() + }) + .map(|s| s.to_string()) + }) +} + +impl PartialEq for PrinterInfo { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.state == other.state + && self.reasons == other.reasons + && self.active_jobs == other.active_jobs + && self.color_supported == other.color_supported + && self.ppm == other.ppm + && self.ppm_color == other.ppm_color + && self.device_uri == other.device_uri + && self.host == other.host + && self.accepting_jobs == other.accepting_jobs + } +} + +impl Eq for PrinterInfo {} + diff --git a/src/proto.rs b/src/proto.rs new file mode 100644 index 0000000..4bfa83f --- /dev/null +++ b/src/proto.rs @@ -0,0 +1,4 @@ +pub mod control { + tonic::include_proto!("control.v1"); +} + diff --git a/src/routes.rs b/src/routes.rs deleted file mode 100644 index 342ca88..0000000 --- a/src/routes.rs +++ /dev/null @@ -1,502 +0,0 @@ -use std::{io::Write, sync::Arc}; - -use axum::{ - extract::{Multipart, Path, Query, State}, - http::StatusCode, - routing::{get, post}, - Json, Router, -}; -use anyhow::anyhow; -use base64::engine::general_purpose::STANDARD as BASE64_STD; -use base64::Engine; -use serde::Serialize; -use tempfile::NamedTempFile; -use tracing::info; - -use crate::{ - config::AppConfig, - error::AppError, - models::{JsonPrintRequest, PrintJobResponse, PrintParams}, - scheduler::Scheduler, -}; - -#[derive(Clone)] -pub struct AppState { - pub scheduler: Arc, - pub config: AppConfig, -} - -pub fn build_router(state: AppState) -> Router { - Router::new() - .route("/health", get(health)) - .route("/printers", get(printers)) - .route("/printers/:name/detail", get(printer_detail)) - .route("/print", post(print_base64)) - .route("/print/upload", post(print_multipart)) - .with_state(state) -} - -async fn health(State(state): State) -> Result, AppError> { - let printers = state.scheduler.healthy_snapshot().await?; - let details = printers - .into_iter() - .map(|p| { - let healthy = p.healthy(); - PrinterStatus { - name: p.dest.name.clone(), - state: format!("{}", p.state), - accepting_jobs: p.dest.is_accepting_jobs(), - active_jobs: p.active_jobs, - reasons: p.reasons, - healthy, - } - }) - .collect::>(); - Ok(Json(HealthResp { - status: "ok".to_string(), - available_printers: details.len(), - printers: details, - })) -} - -async fn printers(State(state): State) -> Result, AppError> { - let printers = state.scheduler.healthy_snapshot().await?; - let details = printers - .into_iter() - .map(|p| { - let healthy = p.healthy(); - PrinterStatus { - name: p.dest.name.clone(), - state: format!("{}", p.state), - accepting_jobs: p.dest.is_accepting_jobs(), - active_jobs: p.active_jobs, - reasons: p.reasons, - healthy, - } - }) - .collect::>(); - - Ok(Json(PrintersResp { printers: details })) -} - -async fn print_base64( - State(state): State, - Json(payload): Json, -) -> Result<(StatusCode, Json), AppError> { - let data = BASE64_STD - .decode(payload.pdf_base64) - .map_err(|e| AppError::BadRequest(format!("PDF base64 解码失败: {}", e)))?; - - let response = process_print(&state, data, payload.params).await?; - Ok((StatusCode::ACCEPTED, Json(response))) -} - -async fn print_multipart( - State(state): State, - mut multipart: Multipart, -) -> Result<(StatusCode, Json), AppError> { - let mut pdf: Option> = None; - let mut params: Option = None; - - while let Some(field) = multipart - .next_field() - .await - .map_err(|e| AppError::BadRequest(format!("解析表单失败: {}", e)))? - { - match field.name() { - Some("file") => { - pdf = Some( - field - .bytes() - .await - .map_err(|e| AppError::BadRequest(format!("读取文件失败: {}", e)))? - .to_vec(), - ); - } - Some("params") => { - let text = field - .text() - .await - .map_err(|e| AppError::BadRequest(format!("读取参数失败: {}", e)))?; - params = Some( - serde_json::from_str(&text) - .map_err(|e| AppError::BadRequest(format!("参数 JSON 无效: {}", e)))?, - ); - } - _ => {} - } - } - - let pdf = pdf.ok_or_else(|| AppError::BadRequest("缺少 file 字段".into()))?; - let params = params.ok_or_else(|| AppError::BadRequest("缺少 params 字段".into()))?; - - let response = process_print(&state, pdf, params).await?; - Ok((StatusCode::ACCEPTED, Json(response))) -} - -async fn process_print( - state: &AppState, - pdf_data: Vec, - params: PrintParams, -) -> Result { - if !is_pdf(&pdf_data) { - return Err(AppError::BadRequest("仅支持 PDF 文件".into())); - } - - let temp = TempPdf::new(pdf_data).await?; - let (job, retries_used) = state - .scheduler - .dispatch( - temp.path(), - ¶ms, - &state.config.defaults, - &state.config.retry, - ) - .await?; - - info!( - printer = %job.printer, - job_id = job.job_id, - retries = retries_used, - "打印任务已提交" - ); - - Ok(PrintJobResponse { - job_id: job.job_id, - printer: job.printer, - strategy: format!("{:?}", state.config.scheduler.strategy), - retries_used, - }) -} - -struct TempPdf { - file: NamedTempFile, -} - -impl TempPdf { - async fn new(data: Vec) -> Result { - let file = tokio::task::spawn_blocking(move || -> Result { - let mut file = NamedTempFile::new() - .map_err(|e| AppError::Internal(anyhow!("创建临时文件失败: {}", e)))?; - file.write_all(&data) - .map_err(|e| AppError::Internal(anyhow!(e)))?; - file.flush() - .map_err(|e| AppError::Internal(anyhow!(e)))?; - Ok(file) - }) - .await - .map_err(|e| AppError::Internal(anyhow!(e)))??; - - Ok(Self { file }) - } - - fn path(&self) -> &std::path::Path { - self.file.path() - } -} - -#[derive(Serialize)] -struct HealthResp { - status: String, - available_printers: usize, - printers: Vec, -} - -#[derive(Serialize)] -struct PrinterStatus { - name: String, - state: String, - accepting_jobs: bool, - active_jobs: usize, - reasons: Vec, - healthy: bool, -} - -fn is_pdf(data: &[u8]) -> bool { - data.starts_with(b"%PDF") -} - -#[derive(Serialize)] -struct PrintersResp { - printers: Vec, -} - -#[derive(Serialize)] -struct PrinterDetailResp { - name: String, - state: String, - accepting_jobs: bool, - active_jobs: usize, - reasons: Vec, - healthy: bool, - info: Option, - location: Option, - make_and_model: Option, - uri: Option, - device_uri: Option, - options: std::collections::HashMap, - media: MediaInfo, - alerts: Vec, -} - -async fn printer_detail( - State(state): State, - Path(name): Path, - Query(query): Query>, -) -> Result, AppError> { - let refresh = query.get("refresh").map(|s| { - let lower = s.to_ascii_lowercase(); - lower == "1" || lower == "true" || lower == "yes" || lower == "y" - }).unwrap_or(false); - let p = state - .scheduler - .printer_detail(&name, refresh) - .await - .map_err(|e| AppError::Printer(e.to_string()))?; - - let healthy = p.healthy(); - let (media_opt, alerts) = parse_media_and_alerts(p.dest.get_options()); - let media = if let Some(m) = media_opt { - m - } else { - let from_ipptool = fetch_media_via_ipptool( - &state.config, - &p.dest.uri().cloned().unwrap_or_default(), - ) - .await; - match from_ipptool { - Some(m) => m, - None => fetch_media_via_lpoptions(&state.config, &p.dest.name) - .await - .unwrap_or_default(), - } - }; - let resp = PrinterDetailResp { - name: p.dest.name.clone(), - state: format!("{}", p.state), - accepting_jobs: p.dest.is_accepting_jobs(), - active_jobs: p.active_jobs, - reasons: p.reasons, - healthy, - info: p.dest.info().cloned(), - location: p.dest.location().cloned(), - make_and_model: p.dest.make_and_model().cloned(), - uri: p.dest.uri().cloned(), - device_uri: p.dest.device_uri().cloned(), - options: p.dest.get_options().clone(), - media, - alerts, - }; - - Ok(Json(resp)) -} - -#[derive(Serialize, Default, Clone)] -struct MediaInfo { - ready: Vec, - supported: Vec, - default: Option, - sources: Vec, - types: Vec, - raw_ready_col: Option, -} - -fn parse_media_and_alerts( - options: &std::collections::HashMap, -) -> (Option, Vec) { - let mut media = None; - let mut m = MediaInfo::default(); - - if let Some(r) = options.get("media-ready") { - m.ready = split_list(r); - media = Some(m.clone()); - } - if let Some(r) = options.get("media-supported") { - m.supported = split_list(r); - media = Some(m.clone()); - } - if let Some(r) = options.get("media-default") { - m.default = Some(r.clone()); - media = Some(m.clone()); - } - if let Some(r) = options.get("media-source-supported") { - m.sources = split_list(r); - media = Some(m.clone()); - } - if let Some(r) = options.get("media-type-supported") { - m.types = split_list(r); - media = Some(m.clone()); - } - if let Some(r) = options.get("media-col-ready") { - m.raw_ready_col = Some(r.clone()); - media = Some(m.clone()); - } - - let mut alerts = Vec::new(); - if let Some(a) = options.get("printer-alert") { - alerts.extend(split_list(a)); - } - if let Some(a) = options.get("printer-alert-description") { - alerts.extend(split_list(a)); - } - - (media, alerts) -} - -fn split_list(s: &str) -> Vec { - s.split(',') - .map(|v| v.trim().to_string()) - .filter(|v| !v.is_empty()) - .collect() -} - -async fn fetch_media_via_lpoptions(config: &AppConfig, printer: &str) -> Option { - let server_env = std::env::var("CUPS_SERVER").ok(); - let server = config - .cups - .server - .as_ref() - .map(|s| s.as_str()) - .or_else(|| server_env.as_deref()) - .unwrap_or("localhost:631"); - - let server_arg = format!("-h{}", server); - let printer_arg = format!("-p{}", printer); - - let output_detail = tokio::task::spawn_blocking({ - let server_arg = server_arg.clone(); - let printer_arg = printer_arg.clone(); - move || { - std::process::Command::new("lpoptions") - .args([server_arg.as_str(), printer_arg.as_str(), "-l"]) - .output() - } - }) - .await; - - let output_defaults = tokio::task::spawn_blocking(move || { - std::process::Command::new("lpoptions") - .args([server_arg.as_str(), printer_arg.as_str()]) - .output() - }) - .await; - - let detail = match output_detail { - Ok(Ok(o)) => o, - _ => return None, - }; - if !detail.status.success() { - return None; - } - - let defaults_opt = match output_defaults { - Ok(Ok(o)) if o.status.success() => Some(o), - _ => None, - }; - - let text = String::from_utf8_lossy(&detail.stdout); - let mut media = MediaInfo::default(); - - for line in text.lines() { - if line.starts_with("PageSize/") { - let tokens: Vec<&str> = line.split(':').nth(1).unwrap_or("").split_whitespace().collect(); - let mut supported = Vec::new(); - let mut default = None; - for t in tokens { - if t.starts_with('*') { - default = Some(t.trim_start_matches('*').to_string()); - supported.push(default.as_ref().unwrap().clone()); - } else { - supported.push(t.to_string()); - } - } - media.supported = supported; - media.default = default; - } else if line.starts_with("InputSlot/") { - let tokens: Vec<&str> = line.split(':').nth(1).unwrap_or("").split_whitespace().collect(); - let mut sources = Vec::new(); - for t in tokens { - sources.push(t.trim_start_matches('*').to_string()); - } - media.sources = sources; - } else if line.starts_with("MediaType/") { - let tokens: Vec<&str> = line.split(':').nth(1).unwrap_or("").split_whitespace().collect(); - let mut types = Vec::new(); - for t in tokens { - types.push(t.trim_start_matches('*').to_string()); - } - media.types = types; - } - } - - if let Some(o) = defaults_opt { - let defaults_line = String::from_utf8_lossy(&o.stdout); - for token in defaults_line.split_whitespace() { - if let Some(rest) = token.strip_prefix("InputSlot=") { - media.ready = vec![rest.to_string()]; - } - } - } - - Some(media) -} - -async fn fetch_media_via_ipptool(_config: &AppConfig, uri: &str) -> Option { - if uri.is_empty() { - return None; - } - - let uri = uri.to_string(); - let output = tokio::task::spawn_blocking(move || { - std::process::Command::new("ipptool") - .args(["-v", "-t", uri.as_str(), "get-printer-attributes.test"]) - .output() - }) - .await - .ok()?; - - let output = match output { - Ok(o) => o, - Err(_) => return None, - }; - - if !output.status.success() { - return None; - } - - let mut text = String::new(); - text.push_str(&String::from_utf8_lossy(&output.stdout)); - text.push_str(&String::from_utf8_lossy(&output.stderr)); - let mut media = MediaInfo::default(); - - for line in text.lines() { - let trimmed = line.trim(); - if let Some(rest) = trimmed.strip_prefix("media-default (keyword) = ") { - media.default = Some(rest.to_string()); - } else if let Some(rest) = trimmed.strip_prefix("media-ready (1setOf keyword) = ") { - media.ready = split_list(rest); - } else if let Some(rest) = trimmed.strip_prefix("media-supported (1setOf keyword) = ") { - media.supported = split_list(rest); - } else if let Some(rest) = trimmed.strip_prefix("media-source-supported (1setOf keyword) = ") { - media.sources = split_list(rest); - } else if let Some(rest) = trimmed.strip_prefix("media-type-supported (1setOf keyword) = ") { - media.types = split_list(rest); - } else if let Some(rest) = trimmed.strip_prefix("media-col-ready (1setOf collection) = ") { - media.raw_ready_col = Some(rest.to_string()); - } - } - - if media.ready.is_empty() - && media.supported.is_empty() - && media.default.is_none() - && media.sources.is_empty() - && media.types.is_empty() - && media.raw_ready_col.is_none() - { - None - } else { - Some(media) - } -} - - diff --git a/src/scheduler.rs b/src/scheduler.rs index a37de65..1e550b3 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -13,14 +13,13 @@ use tracing::{info, warn}; use crate::{ config::{LoadBalanceStrategy, RetryConfig}, models::{PrintDefaults, PrintParams}, - printer::{PrintAdapter, PrintJobResult, PrinterInfo, SharedPrinterAdapter}, + printer::{PrintJobResult, PrinterInfo, SharedPrinterAdapter}, }; pub struct Scheduler { adapter: SharedPrinterAdapter, strategy: LoadBalanceStrategy, rr_cursor: AtomicUsize, - refresh_interval: Duration, cache: std::sync::Arc>>, default_ppm_bw: u32, default_ppm_color: u32, @@ -44,7 +43,6 @@ impl Scheduler { adapter: adapter.clone(), strategy, rr_cursor: AtomicUsize::new(0), - refresh_interval, cache: cache.clone(), default_ppm_bw, default_ppm_color, @@ -75,6 +73,23 @@ impl Scheduler { this } + pub async fn snapshot(&self) -> Vec { + self.cache.read().await.clone() + } + + pub async fn refresh_snapshot(&self) -> Result> { + match self.adapter.list_printers().await { + Ok(list) => { + *self.cache.write().await = list.clone(); + Ok(list) + } + Err(e) => { + warn!("刷新打印机列表失败: {}", e); + Ok(self.cache.read().await.clone()) + } + } + } + async fn healthy_printers(&self) -> Result> { let mut printers = self.cache.read().await.clone(); @@ -121,7 +136,7 @@ impl Scheduler { sorted } } -} + } pub async fn dispatch( &self, @@ -220,32 +235,6 @@ impl Scheduler { )) } - pub async fn healthy_snapshot(&self) -> Result> { - let printers = self.cache.read().await.clone(); - Ok(printers) - } - - pub async fn printer_detail(&self, name: &str, refresh: bool) -> Result { - if refresh { - if let Ok(list) = self.adapter.list_printers().await { - *self.cache.write().await = list; - } - } - - { - let cache = self.cache.read().await; - if let Some(p) = cache.iter().find(|p| p.dest.name == name).cloned() { - return Ok(p); - } - } - - let list = self.adapter.list_printers().await?; - *self.cache.write().await = list.clone(); - list.into_iter() - .find(|p| p.dest.name == name) - .ok_or_else(|| anyhow!("未找到打印机: {}", name)) - } - fn state_rank(state: &PrinterState) -> u8 { match state { PrinterState::Idle => 0, @@ -304,7 +293,14 @@ impl Scheduler { fn cost_estimate(&self, printer: &PrinterInfo, color: crate::models::ColorModeSetting) -> f64 { // 简化成本:活跃作业数 / 速度;假设每个作业页数相近 let ppm = self.effective_ppm(printer, color); - (printer.active_jobs as f64 + 1.0) / ppm + let estimated_wait = (printer.active_jobs as f64 + 1.0) / ppm; + + if let Some(target) = self.target_wait_minutes { + let normalized_target = target.max(0.1); + return estimated_wait / normalized_target; + } + + estimated_wait } } @@ -314,6 +310,7 @@ mod tests { use anyhow::Result; use async_trait::async_trait; use cups_rs::{Destination, PrinterState}; + use crate::printer::PrintAdapter; use std::sync::Arc; use tokio::runtime::Runtime; @@ -335,14 +332,11 @@ mod tests { ) -> Result { unreachable!() } - - async fn cancel(&self, _job_id: i32) -> Result<()> { - Ok(()) - } } fn mock_printer(name: &str, active_jobs: usize) -> PrinterInfo { PrinterInfo { + id: format!("id-{name}"), dest: Destination { name: name.to_string(), instance: None, @@ -353,9 +347,11 @@ mod tests { reasons: vec![], active_jobs, color_supported: true, - color_modes_supported: vec!["color".to_string(), "monochrome".to_string()], ppm: Some(30), ppm_color: Some(20), + device_uri: None, + host: None, + accepting_jobs: true, } }