diff --git a/Cargo.lock b/Cargo.lock index fa2c2bda..1e3901e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,87 +4,74 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" -version = "0.6.13" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_matches" @@ -94,22 +81,20 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bindgen" -version = "0.69.4" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools", - "lazy_static", - "lazycell", "proc-macro2", "quote", "regex", @@ -126,15 +111,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" - -[[package]] -name = "bumpalo" -version = "3.15.4" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "byteorder" @@ -144,9 +123,13 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.90" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cexpr" @@ -159,9 +142,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -169,20 +152,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -196,9 +165,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.16" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -206,9 +175,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -218,9 +187,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -230,21 +199,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "crossbeam-channel" @@ -257,9 +220,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "daemonize" @@ -272,15 +235,15 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -295,12 +258,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -315,22 +278,33 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -348,32 +322,42 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets", + "r-efi", + "wasip2", + "wasip3", ] [[package]] name = "glob" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -383,44 +367,29 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.60" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", + "windows-sys 0.59.0", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.2.5" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -452,35 +421,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" - -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -488,11 +454,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] @@ -503,38 +469,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "lazycell" -version = "1.3.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets", + "windows-link", ] [[package]] name = "libproc" -version = "0.14.8" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9ea4b75e1a81675429dafe43441df1caea70081e82246a8cccf514884a88bb" +checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" dependencies = [ "bindgen", - "errno 0.3.10", + "errno 0.3.14", "libc", ] +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.5", +] + [[package]] name = "libshpool" version = "0.9.8" @@ -542,7 +520,6 @@ dependencies = [ "anyhow", "assert_matches", "byteorder", - "chrono", "clap", "crossbeam-channel", "daemonize", @@ -575,15 +552,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -596,15 +573,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -623,14 +600,14 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -647,11 +624,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -674,7 +651,7 @@ version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.11.1", "crossbeam-channel", "filetime", "fsevent-sys", @@ -699,9 +676,9 @@ dependencies = [ [[package]] name = "ntest" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb183f0a1da7a937f672e5ee7b7edb727bf52b8a52d531374ba8ebb9345c0330" +checksum = "54d1aa56874c2152c24681ed0df95ee155cc06c5c61b78e2d1e8c0cae8bc5326" dependencies = [ "ntest_test_cases", "ntest_timeout", @@ -709,9 +686,9 @@ dependencies = [ [[package]] name = "ntest_test_cases" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d0d3f2a488592e5368ebbe996e7f1d44aa13156efad201f5b4d84e150eaa93" +checksum = "6913433c6319ef9b2df316bb8e3db864a41724c2bb8f12555e07dc4ec69d3db1" dependencies = [ "proc-macro2", "quote", @@ -720,9 +697,9 @@ dependencies = [ [[package]] name = "ntest_timeout" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc7c92f190c97f79b4a332f5e81dcf68c8420af2045c936c9be0bc9de6f63b5" +checksum = "9224be3459a0c1d6e9b0f42ab0e76e98b29aef5aba33c0487dfcf47ea08b5150" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -732,18 +709,24 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking_lot" @@ -769,16 +752,16 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-project-lite" -version = "0.2.13" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "ppv-lite86" @@ -789,13 +772,23 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.21.1", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -809,18 +802,24 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -848,27 +847,27 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", ] [[package]] name = "redox_syscall" -version = "0.5.18" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.11.1", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -878,9 +877,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -889,19 +888,17 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] @@ -916,42 +913,36 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.4.2", - "errno 0.3.10", + "bitflags 2.11.1", + "errno 0.3.14", "libc", - "linux-raw-sys 0.4.14", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.4.2", - "errno 0.3.10", + "bitflags 2.11.1", + "errno 0.3.14", "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] -[[package]] -name = "ryu" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" - [[package]] name = "same-file" version = "1.0.6" @@ -967,6 +958,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -998,21 +995,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -1028,9 +1026,9 @@ dependencies = [ [[package]] name = "shell-words" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" @@ -1101,15 +1099,15 @@ checksum = "e839aaacba63b9c8fba16affed9507042eb33d802cf8040c4a6f37e3945029c3" dependencies = [ "itoa", "log", - "unicode-width 0.1.11", + "unicode-width 0.1.14", "vte 0.15.0", ] [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -1117,18 +1115,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno 0.3.14", "libc", ] [[package]] name = "smallvec" -version = "1.13.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "static_assertions" @@ -1138,18 +1137,18 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strip-ansi-escapes" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" dependencies = [ - "vte 0.11.1", + "vte 0.14.1", ] [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" @@ -1175,15 +1174,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", - "windows-sys 0.59.0", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -1197,64 +1196,89 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "toml" -version = "0.8.12" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit 0.22.12", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] -name = "toml_edit" -version = "0.21.1" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "indexmap", - "toml_datetime", - "winnow 0.5.40", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", - "winnow 0.6.13", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", ] +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1263,9 +1287,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1274,9 +1298,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1295,9 +1319,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "sharded-slab", "smallvec", @@ -1308,15 +1332,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" @@ -1324,26 +1348,31 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vte" -version = "0.11.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ - "utf8parse", - "vte_generate_state_changes", + "memchr", ] [[package]] @@ -1356,16 +1385,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "vte_generate_state_changes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -1378,84 +1397,72 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen 0.46.0", ] [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen 0.51.0", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", + "leb128fmt", + "wasmparser", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-backend", - "wasm-bindgen-shared", + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", ] -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - [[package]] name = "which" -version = "6.0.0" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "once_cell", - "rustix 0.38.42", - "windows-sys 0.52.0", + "rustix 0.38.44", + "winsafe", ] [[package]] @@ -1476,11 +1483,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1489,15 +1496,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -1522,6 +1520,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1588,47 +1595,144 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "winnow" -version = "0.6.13" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ - "bitflags 2.4.2", + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "zerocopy" -version = "0.8.38" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.38" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/deny.toml b/deny.toml index 877b3c70..57ea956b 100644 --- a/deny.toml +++ b/deny.toml @@ -19,7 +19,7 @@ allow = [ "ISC", # notice "Apache-2.0", "MIT", - "Unicode-DFS-2016", + "Unicode-3.0", # notice "BSD-3-Clause", # notice ] confidence-threshold = 1.0 diff --git a/libshpool/Cargo.toml b/libshpool/Cargo.toml index f7bb985a..5c11a06b 100644 --- a/libshpool/Cargo.toml +++ b/libshpool/Cargo.toml @@ -21,7 +21,6 @@ test_hooks = [] # for internal testing only, don't enable this feature [dependencies] clap = { version = "4", features = ["derive"] } # cli parsing anyhow = "1" # dynamic, unstructured errors -chrono = "0.4" # getting current time and formatting it serde = "1" # config parsing, connection header formatting serde_derive = "1" # config parsing, connection header formatting serde_json = "1" # JSON output for list command diff --git a/libshpool/src/attach.rs b/libshpool/src/attach.rs index 816f584d..a5c614d8 100644 --- a/libshpool/src/attach.rs +++ b/libshpool/src/attach.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2023-2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,17 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{env, fmt, io, path::PathBuf, thread, time}; +use std::{ + collections::HashMap, + env, fmt, io, + os::fd::AsFd, + path::PathBuf, + sync::{Arc, Mutex}, + thread, time, +}; use anyhow::{anyhow, bail, Context}; +use nix::unistd; use shpool_protocol::{ - AttachHeader, AttachReplyHeader, ConnectHeader, DetachReply, DetachRequest, ResizeReply, - ResizeRequest, SessionMessageReply, SessionMessageRequest, SessionMessageRequestPayload, - TtySize, + AttachHeader, AttachReplyHeader, ConnectHeader, DetachReply, DetachRequest, MaybeSwitch, + ResizeReply, ResizeRequest, SessionMessageReply, SessionMessageRequest, + SessionMessageRequestPayload, TtySize, }; use tracing::{debug, error, info, warn}; -use super::{config, duration, protocol, protocol::ClientResult, test_hooks, tty::TtySizeExt as _}; +use crate::{ + config, duration, protocol, + protocol::{ClientResult, PipeBytesResult}, + template, test_hooks, + tty::TtySizeExt as _, +}; const MAX_FORCE_RETRIES: usize = 20; @@ -40,18 +53,7 @@ pub fn run( info!("\n\n======================== STARTING ATTACH ============================\n\n"); test_hooks::emit("attach-startup"); - if name.is_empty() { - eprintln!("blank session names are not allowed"); - return Ok(()); - } - if name.contains(char::is_whitespace) { - eprintln!("whitespace is not allowed in session names"); - return Ok(()); - } - - if !background { - SignalHandler::new(name.clone(), socket.clone()).spawn()?; - } + let session_name_tmpl = template::Template::new(&name).context("parsing session name tmpl")?; let ttl = match &ttl { Some(src) => match duration::parse(src.as_str()) { @@ -63,224 +65,312 @@ pub fn run( None => None, }; - let mut detached = false; - let mut tries = 0; - let attach_client = loop { - match do_attach(&config_manager, name.as_str(), background, &ttl, &cmd, &dir, &socket) { - Ok(client) => break client, - Err(err) => match err.downcast() { - Ok(BusyError) if !force => { - eprintln!("session '{name}' already has a terminal attached"); - return Ok(()); - } - Ok(BusyError) => { - if !detached { - let mut client = dial_client(&socket, background)?; - client - .write_connect_header(ConnectHeader::Detach(DetachRequest { - sessions: vec![name.clone()], - })) - .context("writing detach request header")?; - let detach_reply: DetachReply = - client.read_reply().context("reading reply")?; - if !detach_reply.not_found_sessions.is_empty() { - warn!("could not find session '{}' to detach it", name); - } + let attach = + Attach { config_manager, session_name_tmpl, force, background, ttl, cmd, dir, socket }; - detached = true; - } - thread::sleep(time::Duration::from_millis(100)); + attach.run() +} - if tries > MAX_FORCE_RETRIES { - eprintln!("session '{name}' already has a terminal which remains attached even after attempting to detach it"); - return Err(anyhow!("could not detach session, forced attach failed")); - } - tries += 1; - } - Err(err) => return Err(err), - }, - } - }; +struct Attach { + config_manager: config::Manager, + session_name_tmpl: template::Template, + force: bool, + background: bool, + ttl: Option, + cmd: Option, + dir: Option, + socket: PathBuf, +} - if background { - // Close the attached connection first so the daemon can observe EOF. - // We still send an explicit Detach on a fresh connection as a best-effort - // fallback in case EOF processing is delayed. - drop(attach_client); - let mut client = dial_client(&socket, true)?; - client - .write_connect_header(ConnectHeader::Detach(DetachRequest { - sessions: vec![name.clone()], - })) - .context("writing detach request header")?; - let detach_reply: DetachReply = client.read_reply().context("reading reply")?; - if !detach_reply.not_found_sessions.is_empty() { - warn!("could not find session '{}' to detach it", name); - } - if !detach_reply.not_attached_sessions.is_empty() { - debug!( - "session '{}' was already detached while processing background detach request (expected)", - name - ); +impl Attach { + fn run(self) -> anyhow::Result<()> { + // This is the first time we dial the daemon, so we do want to show + // warnings. After this we shouldn't show them again. + let mut client = self.dial_client(false).context("dialing daemon")?; + info!("dialed initial conn for GetVars"); + + client.write_connect_header(ConnectHeader::GetVars).context("getting vars")?; + let mut maybe_switch: MaybeSwitch = client.read_reply().context("reading reply")?; + + let var_map = maybe_switch.vars.iter().cloned().collect(); + let mut resolved_name = self.session_name_tmpl.apply(&var_map); + + let sig_handler_session_name_slot = if !self.background { + Some(SignalHandler::new(resolved_name.clone(), self.socket.clone()).spawn()?) + } else { + None + }; + + info!("looping on attach_with_name"); + loop { + match self.attach_with_name(resolved_name) { + Ok(AttachResult::Done) => return Ok(()), + Ok(AttachResult::Switch(s)) => maybe_switch = s, + Err(e) => return Err(e), + } + + let var_map = maybe_switch.vars.iter().cloned().collect(); + resolved_name = self.session_name_tmpl.apply(&var_map); + + if let Some(ref slot) = sig_handler_session_name_slot { + let mut slot = slot.lock().unwrap(); + *slot = resolved_name.clone(); + } } - return Ok(()); } - match attach_client.pipe_bytes() { - Ok(exit_status) => std::process::exit(exit_status), - Err(e) => Err(e), - } -} + /// Attach with the given resolved name. This will run until exit or until + /// we need to reconnect due to + pub fn attach_with_name(&self, resolved_name: String) -> anyhow::Result { + if resolved_name.is_empty() { + eprintln!("blank session names are not allowed"); + return Ok(AttachResult::Done); + } + if resolved_name.contains(char::is_whitespace) { + eprintln!("session name '{}' may not have whitespace", resolved_name); + return Ok(AttachResult::Done); + } -#[derive(Debug)] -struct BusyError; -impl fmt::Display for BusyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BusyError") - } -} -impl std::error::Error for BusyError {} + let mut detached = false; + let mut tries = 0; + let attach_client = loop { + match self.dial_attach(resolved_name.as_str()) { + Ok(client) => break client, + Err(err) => match err.downcast() { + Ok(BusyError) if !self.force => { + eprintln!("session '{resolved_name}' already has a terminal attached"); + return Ok(AttachResult::Done); + } + Ok(BusyError) => { + if !detached { + let mut client = self.dial_client(true)?; + client + .write_connect_header(ConnectHeader::Detach(DetachRequest { + sessions: vec![resolved_name.clone()], + })) + .context("writing detach request header")?; + let detach_reply: DetachReply = + client.read_reply().context("reading reply")?; + if !detach_reply.not_found_sessions.is_empty() { + warn!("could not find session '{}' to detach it", resolved_name); + } + + detached = true; + } + thread::sleep(time::Duration::from_millis(100)); -/// Attach to a session and return the connected client without piping stdio. -/// -/// `background` is forwarded to `dial_client` to suppress the interactive -/// version-mismatch prompt on stdin; no other behavior changes. -fn do_attach( - config: &config::Manager, - name: &str, - background: bool, - ttl: &Option, - cmd: &Option, - dir: &Option, - socket: &PathBuf, -) -> anyhow::Result { - let mut client = dial_client(socket, background)?; - - let tty_size = match TtySize::from_fd(0) { - Ok(s) => s, - Err(e) => { - warn!("stdin is not a tty, using default size (err: {e:?})"); - TtySize { rows: 24, cols: 80, xpixel: 0, ypixel: 0 } + if tries > MAX_FORCE_RETRIES { + eprintln!("session '{resolved_name}' already has a terminal which remains attached even after attempting to detach it"); + return Err(anyhow!("could not detach session, forced attach failed")); + } + tries += 1; + } + Err(err) => return Err(err), + }, + } + }; + info!("got attach client"); + + if self.background { + // Close the attached connection first so the daemon can observe EOF. + // We still send an explicit Detach on a fresh connection as a best-effort + // fallback in case EOF processing is delayed. + drop(attach_client); + let mut client = self.dial_client(true)?; + client + .write_connect_header(ConnectHeader::Detach(DetachRequest { + sessions: vec![resolved_name.clone()], + })) + .context("writing detach request header")?; + let detach_reply: DetachReply = client.read_reply().context("reading reply")?; + if !detach_reply.not_found_sessions.is_empty() { + warn!("could not find session '{}' to detach it", resolved_name); + } + if !detach_reply.not_attached_sessions.is_empty() { + debug!( + "session '{}' was already detached while processing background detach request (expected)", + resolved_name + ); + } + return Ok(AttachResult::Done); } - }; - let forward_env = config.get().forward_env.clone(); - let mut local_env_keys = vec!["TERM", "DISPLAY", "LANG", "SSH_AUTH_SOCK"]; - if let Some(fenv) = &forward_env { - for var in fenv.iter() { - local_env_keys.push(var); + info!("entering bidi streaming mode"); + let session_name_tmpl = self.session_name_tmpl.clone(); + match attach_client.pipe_bytes(move |maybe_switch: &MaybeSwitch| { + let var_map: HashMap = maybe_switch.vars.iter().cloned().collect(); + session_name_tmpl.apply(&var_map) != resolved_name + }) { + Ok(PipeBytesResult::Exit(exit_status)) => std::process::exit(exit_status), + Ok(PipeBytesResult::MaybeSwitch(s)) => Ok(AttachResult::Switch(s)), + Err(e) => Err(e), } } - info!("local env keys: {local_env_keys:?}"); - - let cwd = String::from(env::current_dir().context("getting cwd")?.to_string_lossy()); - let default_dir = config.get().default_dir.clone().unwrap_or(String::from("$HOME")); - let start_dir = match (default_dir.as_str(), dir.as_deref()) { - (".", None) => Some(cwd), - ("$HOME", None) => None, - (d, None) => Some(String::from(d)), - (_, Some(".")) => Some(cwd), - (_, Some(d)) => Some(String::from(d)), - }; - client - .write_connect_header(ConnectHeader::Attach(AttachHeader { - name: String::from(name), - local_tty_size: tty_size, - local_env: local_env_keys - .into_iter() - .filter_map(|var| { - let val = env::var(var).context("resolving var").ok()?; - Some((String::from(var), val)) - }) - .collect::>(), - ttl_secs: ttl.map(|d| d.as_secs()), - cmd: cmd.clone(), - dir: start_dir, - })) - .context("writing attach header")?; - - let attach_resp: AttachReplyHeader = client.read_reply().context("reading attach reply")?; - info!("attach_resp.status={:?}", attach_resp.status); - - { - use shpool_protocol::AttachStatus::*; - match attach_resp.status { - Busy => { - return Err(BusyError.into()); + /// Attach to a session and return the connected client without piping + /// stdio. + fn dial_attach(&self, name: &str) -> anyhow::Result { + let mut client = self.dial_client(true)?; + + let tty_size = match TtySize::from_fd(0) { + Ok(s) => s, + Err(e) => { + warn!("stdin is not a tty, using default size (err: {e:?})"); + TtySize { rows: 24, cols: 80, xpixel: 0, ypixel: 0 } } - Forbidden(reason) => { - eprintln!("forbidden: {reason}"); - return Err(anyhow!("forbidden: {reason}")); + }; + + let forward_env = self.config_manager.get().forward_env.clone(); + let mut local_env_keys = vec!["TERM", "DISPLAY", "LANG", "SSH_AUTH_SOCK"]; + if let Some(fenv) = &forward_env { + for var in fenv.iter() { + local_env_keys.push(var); } - Attached { warnings } => { - for warning in warnings.into_iter() { - eprintln!("shpool: warn: {warning}"); + } + info!("local env keys: {local_env_keys:?}"); + + let cwd = String::from(env::current_dir().context("getting cwd")?.to_string_lossy()); + let default_dir = + self.config_manager.get().default_dir.clone().unwrap_or(String::from("$HOME")); + let start_dir = match (default_dir.as_str(), self.dir.as_deref()) { + (".", None) => Some(cwd), + ("$HOME", None) => None, + (d, None) => Some(String::from(d)), + (_, Some(".")) => Some(cwd), + (_, Some(d)) => Some(String::from(d)), + }; + + client + .write_connect_header(ConnectHeader::Attach(AttachHeader { + name: String::from(name), + local_tty_size: tty_size, + local_env: local_env_keys + .into_iter() + .filter_map(|var| { + let val = env::var(var).context("resolving var").ok()?; + Some((String::from(var), val)) + }) + .collect::>(), + ttl_secs: self.ttl.map(|d| d.as_secs()), + cmd: self.cmd.clone(), + dir: start_dir, + })) + .context("writing attach header")?; + + let attach_resp: AttachReplyHeader = client.read_reply().context("reading attach reply")?; + info!("attach_resp.status={:?}", attach_resp.status); + + { + use shpool_protocol::AttachStatus::*; + match attach_resp.status { + Busy => { + return Err(BusyError.into()); } - info!("attached to an existing session: '{}'", name); - } - Created { warnings } => { - for warning in warnings.into_iter() { - eprintln!("shpool: warn: {warning}"); + Forbidden(reason) => { + eprintln!("forbidden: {reason}"); + return Err(anyhow!("forbidden: {reason}")); + } + Attached { warnings } => { + for warning in warnings.into_iter() { + eprintln!("shpool: warn: {warning}"); + } + info!("attached to an existing session: '{}'", name); + } + Created { warnings } => { + for warning in warnings.into_iter() { + eprintln!("shpool: warn: {warning}"); + } + info!("created a new session: '{}'", name); + } + UnexpectedError(err) => { + return Err(anyhow!("BUG: unexpected error attaching to '{}': {}", name, err)); } - info!("created a new session: '{}'", name); - } - UnexpectedError(err) => { - return Err(anyhow!("BUG: unexpected error attaching to '{}': {}", name, err)); } } + + Ok(client) } - Ok(client) -} + // Dial the daemon. If silent is true, don't attempt to warn the user. + // After the first dial, silent should always be true. + fn dial_client(&self, silent: bool) -> anyhow::Result { + match protocol::Client::new(&self.socket) { + Ok(ClientResult::JustClient(c)) => Ok(c), + Ok(ClientResult::VersionMismatch { warning, client }) => { + if silent { + return Ok(client); + } -fn dial_client(socket: &PathBuf, background: bool) -> anyhow::Result { - match protocol::Client::new(socket) { - Ok(ClientResult::JustClient(c)) => Ok(c), - Ok(ClientResult::VersionMismatch { warning, client }) => { - if background { - eprintln!( - "warning: {warning}, proceeding in background mode; try restarting your daemon" - ); - } else { - eprintln!("warning: {warning}, try restarting your daemon"); - eprintln!("hit enter to continue anyway or ^C to exit"); - - let _ = io::stdin() - .lines() - .next() - .context("waiting for a continue through a version mismatch")?; - } + if self.background { + eprintln!( + "warning: {warning}, proceeding in background mode; try restarting your daemon" + ); + } else { + eprintln!("warning: {warning}, try restarting your daemon"); + eprintln!("hit enter to continue anyway or ^C to exit"); + + let mut buf = [0u8; 1]; + loop { + match unistd::read(io::stdin().as_fd(), &mut buf) { + Ok(0) => break, + Ok(1) if buf[0] == b'\n' => break, + Ok(_) => continue, + Err(nix::errno::Errno::EINTR) => continue, + Err(e) => { + return Err(anyhow::Error::from(e)) + .context("waiting for a continue through a version mismatch") + } + } + } + info!("user continued through version mismatch"); + } - Ok(client) - } - Err(err) => { - let io_err = err.downcast::()?; - if io_err.kind() == io::ErrorKind::NotFound { - eprintln!("could not connect to daemon"); + Ok(client) + } + Err(err) => { + let io_err = err.downcast::()?; + if io_err.kind() == io::ErrorKind::NotFound { + eprintln!("could not connect to daemon"); + } + Err(io_err).context("connecting to daemon") } - Err(io_err).context("connecting to daemon") } } } +#[derive(Debug)] +struct BusyError; +impl fmt::Display for BusyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BusyError") + } +} +impl std::error::Error for BusyError {} + +enum AttachResult { + Done, + Switch(MaybeSwitch), +} + // // Signal Handling // struct SignalHandler { - session_name: String, + session_name: Arc>, socket: PathBuf, } impl SignalHandler { fn new(session_name: String, socket: PathBuf) -> Self { - SignalHandler { session_name, socket } + SignalHandler { session_name: Arc::new(Mutex::new(session_name)), socket } } - fn spawn(self) -> anyhow::Result<()> { + fn spawn(self) -> anyhow::Result>> { use signal_hook::{consts::*, iterator::*}; + let session_name_slot = Arc::clone(&self.session_name); + let sigs = vec![SIGWINCH]; let mut signals = Signals::new(sigs).context("creating signal iterator")?; @@ -299,7 +389,7 @@ impl SignalHandler { } }); - Ok(()) + Ok(session_name_slot) } fn handle_sigwinch(&self) -> anyhow::Result<()> { @@ -318,7 +408,7 @@ impl SignalHandler { // write the request on a new, seperate connection client .write_connect_header(ConnectHeader::SessionMessage(SessionMessageRequest { - session_name: self.session_name.clone(), + session_name: self.get_session_name(), payload: SessionMessageRequestPayload::Resize(ResizeRequest { tty_size: tty_size.clone(), }), @@ -331,11 +421,15 @@ impl SignalHandler { SessionMessageReply::NotFound => { warn!( "handle_sigwinch: sent resize for session '{}', but the daemon has no record of that session", - self.session_name + self.get_session_name() ); } SessionMessageReply::Resize(ResizeReply::Ok) => { - info!("handle_sigwinch: resized session '{}' to {:?}", self.session_name, tty_size); + info!( + "handle_sigwinch: resized session '{}' to {:?}", + self.get_session_name(), + tty_size + ); } reply => { warn!("handle_sigwinch: unexpected resize reply: {:?}", reply); @@ -344,4 +438,9 @@ impl SignalHandler { Ok(()) } + + fn get_session_name(&self) -> String { + let session_name = self.session_name.lock().unwrap(); + session_name.clone() + } } diff --git a/libshpool/src/daemon/server.rs b/libshpool/src/daemon/server.rs index ce73a8d0..475cec2a 100644 --- a/libshpool/src/daemon/server.rs +++ b/libshpool/src/daemon/server.rs @@ -36,9 +36,10 @@ use nix::unistd; use parking_lot::{ArcMutexGuard, Mutex, RawMutex}; use shpool_protocol::{ AttachHeader, AttachReplyHeader, AttachStatus, ConnectHeader, DetachReply, DetachRequest, - KillReply, KillRequest, ListReply, LogLevel, ResizeReply, Session, SessionMessageDetachReply, - SessionMessageReply, SessionMessageRequest, SessionMessageRequestPayload, SessionStatus, - SetLogLevelReply, SetLogLevelRequest, VersionHeader, + KillReply, KillRequest, ListReply, LogLevel, MaybeSwitch, ModifyVarReply, ModifyVarRequest, + ResizeReply, Session, SessionMessageDetachReply, SessionMessageReply, SessionMessageRequest, + SessionMessageRequestPayload, SessionStatus, SetLogLevelReply, SetLogLevelRequest, + VersionHeader, }; use tracing::{debug, error, info, instrument, span, warn, Level}; @@ -80,6 +81,7 @@ pub struct Server { tracing_subscriber::filter::LevelFilter, tracing_subscriber::registry::Registry, >, + vars: Mutex>, } impl Server { @@ -113,6 +115,7 @@ impl Server { hooks, daily_messenger, log_level_handle, + vars: HashMap::new().into(), })) } @@ -210,6 +213,8 @@ impl Server { ConnectHeader::List => self.handle_list(stream), ConnectHeader::SessionMessage(header) => self.handle_session_message(stream, header), ConnectHeader::SetLogLevel(r) => self.handle_set_log_level(stream, r), + ConnectHeader::GetVars => self.handle_get_vars(stream), + ConnectHeader::ModifyVar(r) => self.handle_modify_var(stream, r), } } @@ -619,6 +624,57 @@ impl Server { Ok(()) } + #[instrument(skip_all)] + fn handle_get_vars(&self, mut stream: UnixStream) -> anyhow::Result<()> { + let maybe_switch = { + let var_map = self.vars.lock(); + let vars: Vec<(String, String)> = + var_map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + shpool_protocol::MaybeSwitch { switch_to: None, vars } + }; + + write_reply(&mut stream, maybe_switch).context("writing maybe_switch reply")?; + Ok(()) + } + + #[instrument(skip_all)] + fn handle_modify_var( + &self, + mut stream: UnixStream, + request: ModifyVarRequest, + ) -> anyhow::Result<()> { + let maybe_switch = { + let mut vars = self.vars.lock(); + if let Some(val) = request.val { + vars.insert(request.var, val); + } else { + vars.remove(&request.var); + } + + MaybeSwitch { + switch_to: None, + vars: vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect(), + } + }; + + let mut ctls = Vec::new(); + { + let shells = self.shells.lock(); + for session in shells.values() { + ctls.push(Arc::clone(&session.shell_to_client_ctl)); + } + } + for ctl in ctls.into_iter() { + let ctl = ctl.lock(); + ctl.maybe_switch + .send_timeout(maybe_switch.clone(), SESSION_MSG_TIMEOUT) + .context("broadcasting maybe_switch")?; + } + + write_reply(&mut stream, ModifyVarReply {}).context("writing modify var reply")?; + Ok(()) + } + #[instrument(skip_all)] fn handle_kill(&self, mut stream: UnixStream, request: KillRequest) -> anyhow::Result<()> { let mut not_found_sessions = vec![]; @@ -993,13 +1049,18 @@ impl Server { let (heartbeat_tx, heartbeat_rx) = crossbeam_channel::bounded(0); let (heartbeat_ack_tx, heartbeat_ack_rx) = crossbeam_channel::bounded(0); - let shell_to_client_ctl = Arc::new(Mutex::new(shell::ReaderCtl { + // We make this buffered to avoid blocking during a broadcast. There is + // no ack chan so we can afford to buffer a bit. + let (maybe_switch_tx, maybe_switch_rx) = crossbeam_channel::bounded(10); + + let shell_to_client_ctl = Arc::new(Mutex::new(shell::ShellToClientCtl { client_connection: client_connection_tx, client_connection_ack: client_connection_ack_rx, tty_size_change: tty_size_change_tx, tty_size_change_ack: tty_size_change_ack_rx, heartbeat: heartbeat_tx, heartbeat_ack: heartbeat_ack_rx, + maybe_switch: maybe_switch_tx, })); let mut session_inner = shell::SessionInner { @@ -1033,6 +1094,7 @@ impl Server { tty_size_change_ack: tty_size_change_ack_tx, heartbeat: heartbeat_rx, heartbeat_ack: heartbeat_ack_tx, + maybe_switch: maybe_switch_rx, child_exit_notifier: shell_to_client_child_exit_notifier, })?); diff --git a/libshpool/src/daemon/shell.rs b/libshpool/src/daemon/shell.rs index 7c40ae76..c4062949 100644 --- a/libshpool/src/daemon/shell.rs +++ b/libshpool/src/daemon/shell.rs @@ -29,12 +29,13 @@ use std::{ use anyhow::{anyhow, Context}; use nix::{poll, poll::PollFlags, sys::signal, unistd::Pid}; use parking_lot::Mutex; -use shpool_protocol::{Chunk, ChunkKind, TtySize}; +use shpool_protocol::{Chunk, ChunkKind, MaybeSwitch, TtySize}; use tracing::{debug, error, info, instrument, span, trace, warn, Level}; use crate::{ common, consts, daemon::{config, exit_notify::ExitNotifier, keybindings, pager::PagerCtl, prompt, show_motd}, + protocol, protocol::ChunkExt as _, session_restore, test_hooks, tty::TtySizeExt as _, @@ -75,7 +76,7 @@ pub struct Session { pub lifecycle_timestamps: Mutex, pub child_pid: libc::pid_t, pub child_exit_notifier: Arc, - pub shell_to_client_ctl: Arc>, + pub shell_to_client_ctl: Arc>, pub pager_ctl: Arc>>, /// Mutable state with the lock held by the servicing handle_attach thread /// while a tty is attached to the session. Probing the mutex can be used @@ -110,7 +111,7 @@ impl Session { #[derive(Debug)] pub struct SessionInner { pub name: String, // to improve logging - pub shell_to_client_ctl: Arc>, + pub shell_to_client_ctl: Arc>, pub pty_master: shpool_pty::fork::Fork, pub client_stream: Option, pub config: config::Manager, @@ -208,6 +209,7 @@ pub struct ShellToClientArgs { pub tty_size_change: crossbeam_channel::Receiver, pub tty_size_change_ack: crossbeam_channel::Sender<()>, pub heartbeat: crossbeam_channel::Receiver<()>, + pub maybe_switch: crossbeam_channel::Receiver, // true if the client is still live, false if it has hung up on us pub heartbeat_ack: crossbeam_channel::Sender, pub child_exit_notifier: Arc, @@ -394,6 +396,42 @@ impl SessionInner { args.heartbeat_ack.send(client_present) .context("sending heartbeat ack")?; } + recv(args.maybe_switch) -> maybe_switch => { + let maybe_switch = match maybe_switch { + Ok(ms) => ms, + Err(e) => { + error!("error recving MaybeSwitch: {:?}", e); + continue; + }, + }; + + let conn = if let ClientConnectionMsg::New(c) = &mut client_conn { + c + } else { + info!("got MaybeSwitch, but no attached client, dropping"); + continue; + }; + + let mut encoded = Vec::new(); + if let Err(e) = protocol::encode_to(&maybe_switch, &mut encoded) { + error!("error encoding MaybeSwitch: {:?}", e); + continue; + } + + let chunk = Chunk { kind: ChunkKind::MaybeSwitch, buf: &encoded[..] }; + match chunk.write_to(&mut conn.sink).and_then(|_| conn.sink.flush()) { + Ok(_) => { + trace!("wrote MaybeSwitch"); + } + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => { + trace!("writing MaybeSwitch: client hangup: {:?}", e); + } + Err(e) => { + error!("unexpected IO error while writing heartbeat: {}", e); + return Err(e).context("writing MaybeSwitch")?; + } + } + } // make this select non-blocking so we spend most of our time parked // in poll @@ -1004,7 +1042,7 @@ impl SessionInner { /// Shared between the session struct (for calls originating with the cli) /// and the session inner struct (for calls resulting from keybindings). #[derive(Debug)] -pub struct ReaderCtl { +pub struct ShellToClientCtl { /// A control channel for the shell->client thread. Whenever a new client /// dials in, the output stream for that client must be attached to the /// shell->client thread by sending it down this channel. A disconnect @@ -1029,6 +1067,12 @@ pub struct ReaderCtl { // True if the client is still listening, false if it has hung up // on us. pub heartbeat_ack: crossbeam_channel::Receiver, + + /// A control channel telling the shell->client thread to + /// broadcast the given MaybeSwitch. There is no ack channel + /// because we just blast this out and the caller doesn't need + /// to know about completion. + pub maybe_switch: crossbeam_channel::Sender, } /// Given a buffer, a length after which the data is not valid, a list of diff --git a/libshpool/src/daemon/show_motd.rs b/libshpool/src/daemon/show_motd.rs index a31f4618..750f111f 100644 --- a/libshpool/src/daemon/show_motd.rs +++ b/libshpool/src/daemon/show_motd.rs @@ -183,14 +183,12 @@ impl Debouncer { fn should_fire(&self) -> anyhow::Result { let mut last_fired = self.last_fired.lock(); if last_fired.elapsed()? >= self.dur { - let old_ts: chrono::DateTime = (*last_fired).into(); + let old: time::SystemTime = *last_fired; *last_fired = time::SystemTime::now(); - let new_ts: chrono::DateTime = (*last_fired).into(); - info!("last_fired: old = {}, new = {}", old_ts, new_ts); + info!("last_fired: old = {:?}, new = {:?}", old, *last_fired); Ok(true) } else { - let ts: chrono::DateTime = (*last_fired).into(); - info!("not firing yet (last_fired = {})", ts); + info!("not firing yet (last_fired = {:?})", *last_fired); Ok(false) } } diff --git a/libshpool/src/lib.rs b/libshpool/src/lib.rs index 03758038..aad548d6 100644 --- a/libshpool/src/lib.rs +++ b/libshpool/src/lib.rs @@ -43,9 +43,11 @@ mod list; mod protocol; mod session_restore; mod set_log_level; +mod template; mod test_hooks; mod tty; mod user; +mod var; /// The command line arguments that shpool expects. /// These can be directly parsed with clap or manually @@ -210,6 +212,54 @@ needs debugging, but would be clobbered by a restart.")] #[clap(help = "new log level")] level: shpool_protocol::LogLevel, }, + + #[clap(about = "Manipulate template variables + +shpool session names can include {variables} which are resolved via +an environment stored globally in the shpool daemon. This command +manipulates that environment. + +The main usecase for templated session names is the ability to switch +multiple shpool sessions to new targets at the same time. For example, +you might have a `shpool attach -f '{workspace}-edit'` session and +a `shpool attach -f '{workspace}-term'` session. To switch both +sessions from the fun-feature workspace to the key-bugfix workspace, +you could just do `shpool var set workspace key-bugfix`. +")] + #[non_exhaustive] + Var { + #[clap(subcommand)] + command: VarCommands, + }, +} + +/// The subcommds of the var command. +#[derive(Subcommand, Debug)] +#[non_exhaustive] +pub enum VarCommands { + #[clap(about = "List the variables + +This command dumps out the whole variable list with +both vars and values in a JSON object using vars as keys.")] + List { + #[clap(short, long, help = "Output as JSON")] + json: bool, + }, + #[clap(about = "Get a variable + +This returns the raw value of the given variable.")] + #[non_exhaustive] + Get { var: String }, + #[clap(about = "Set a variable + +This updates the value of the given variable.")] + #[non_exhaustive] + Set { var: String, val: String }, + #[clap(about = "Unset a variable + +This removes the given variable from the environment.")] + #[non_exhaustive] + Unset { var: String }, } impl Args { @@ -382,6 +432,7 @@ pub fn run(args: Args, hooks: Option>) -> an Commands::Kill { sessions } => kill::run(sessions, socket), Commands::List { json } => list::run(socket, json), Commands::SetLogLevel { level } => set_log_level::run(level, socket), + Commands::Var { command } => var::run(socket, command), }; if let Err(err) = res { diff --git a/libshpool/src/list.rs b/libshpool/src/list.rs index 2492ab05..3aaeebae 100644 --- a/libshpool/src/list.rs +++ b/libshpool/src/list.rs @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{io, path::PathBuf, time}; +use std::{io, path::PathBuf}; use anyhow::Context; -use chrono::{DateTime, Utc}; use shpool_protocol::{ConnectHeader, ListReply}; use crate::{protocol, protocol::ClientResult}; @@ -42,12 +41,16 @@ pub fn run(socket: PathBuf, json_output: bool) -> anyhow::Result<()> { if json_output { println!("{}", serde_json::to_string_pretty(&reply)?); } else { - println!("NAME\tSTARTED_AT\tSTATUS"); + let longest_session_name_len = + reply.sessions.iter().map(|s| s.name.len()).max().unwrap_or(0); + + println!("NAME\tSTATUS"); for session in reply.sessions.iter() { - let started_at = - time::UNIX_EPOCH + time::Duration::from_millis(session.started_at_unix_ms as u64); - let started_at = DateTime::::from(started_at); - println!("{}\t{}\t{}", session.name, started_at.to_rfc3339(), session.status); + let mut name = session.name.clone(); + while name.len() < longest_session_name_len { + name.push(' '); + } + println!("{}\t{}", name, session.status); } } diff --git a/libshpool/src/protocol.rs b/libshpool/src/protocol.rs index e4f64a4a..109a2e14 100644 --- a/libshpool/src/protocol.rs +++ b/libshpool/src/protocol.rs @@ -15,27 +15,29 @@ use std::{ cmp, io::{self, Read, Write}, - os::unix::net::UnixStream, + os::{fd::AsFd, unix::net::UnixStream}, path::Path, - sync::atomic::{AtomicI32, Ordering}, + sync::Mutex, thread, time, }; use anyhow::{anyhow, Context}; use byteorder::{LittleEndian, ReadBytesExt as _, WriteBytesExt as _}; +use nix::poll; use serde::{Deserialize, Serialize}; use shpool_protocol::{Chunk, ChunkKind, ConnectHeader, VersionHeader}; use tracing::{debug, error, info, instrument, span, trace, warn, Level}; use super::{common, consts, tty}; -const DETACH_DISCONNECT_FAST_WAIT_DUR: time::Duration = time::Duration::from_millis(10); const MAX_DETACH_WAIT_DUR: time::Duration = time::Duration::from_millis(300); const DETACH_BACKOFF_INITIAL_DUR: time::Duration = time::Duration::from_millis(1); // Cap backoff steps so slow-path stays responsive while still avoiding busy // waits. const DETACH_BACKOFF_MAX_STEP_DUR: time::Duration = time::Duration::from_millis(25); +const STDIN_READ_POLL_MS: u16 = 50; + /// The centralized encoding function that should be used for all protocol /// serialization. pub fn encode_to(d: &T, w: W) -> anyhow::Result<()> @@ -241,16 +243,23 @@ impl Client { /// socket and back again. It is the main loop of /// `shpool attach`. /// - /// Return value: the exit status that `shpool attach` should - /// exit with. + /// The on_maybe_switch callback should return true if pipe_bytes + /// should exit with a PipeBytesResult::MaybeSwitch because the + /// attach process needs to reattach. #[instrument(skip_all)] - pub fn pipe_bytes(self) -> anyhow::Result { + pub fn pipe_bytes( + self, + on_maybe_switch: OnMaybeSwitchF, + ) -> anyhow::Result + where + OnMaybeSwitchF: Fn(&shpool_protocol::MaybeSwitch) -> bool + Send + Sync + 'static, + { let tty_guard = tty::set_attach_flags()?; let mut read_client_stream = self.stream.try_clone().context("cloning read stream")?; let mut write_client_stream = self.stream.try_clone().context("cloning read stream")?; - let exit_status = AtomicI32::new(1); + let result_slot = Mutex::new(None); thread::scope(|s| { // stdin -> sock let stdin_to_sock_h = s.spawn(|| -> anyhow::Result<()> { @@ -259,6 +268,24 @@ impl Client { let mut buf = vec![0; consts::BUF_SIZE]; loop { + { + let res = result_slot.lock().unwrap(); + if res.is_some() { + return Ok(()); + } + } + + { + let mut poll_fds = + [poll::PollFd::new(stdin.as_fd(), poll::PollFlags::POLLIN)]; + let nready = poll::poll(&mut poll_fds, STDIN_READ_POLL_MS) + .context("polling stdin")?; + if nready == 0 { + // timeout + continue; + } + } + let nread = stdin.read(&mut buf).context("reading stdin from user")?; if nread == 0 { return Ok(()); @@ -281,6 +308,26 @@ impl Client { let mut buf = vec![0; consts::BUF_SIZE]; loop { + { + let res = result_slot.lock().unwrap(); + if res.is_some() { + return Ok(()); + } + } + + { + let mut poll_fds = [poll::PollFd::new( + read_client_stream.as_fd(), + poll::PollFlags::POLLIN, + )]; + let nready = poll::poll(&mut poll_fds, STDIN_READ_POLL_MS) + .context("polling stdin")?; + if nready == 0 { + // timeout + continue; + } + } + let chunk = match Chunk::read_into(&mut read_client_stream, &mut buf) { Ok(c) => c, Err(err) => { @@ -323,46 +370,48 @@ impl Client { .read_i32::() .context("reading exit status from exit status chunk")?; info!("got exit status frame (status={})", stat); - exit_status.store(stat, Ordering::Release); + { + let mut res = result_slot.lock().unwrap(); + *res = Some(PipeBytesResult::Exit(stat)); + } + } + ChunkKind::MaybeSwitch => { + let maybe_switch_reader = io::Cursor::new(chunk.buf); + let maybe_switch: shpool_protocol::MaybeSwitch = + decode_from(maybe_switch_reader).context("decoding vars list")?; + + info!("got vars update (maybe_switch={:?})", maybe_switch); + if on_maybe_switch(&maybe_switch) { + let mut res = result_slot.lock().unwrap(); + *res = Some(PipeBytesResult::MaybeSwitch(maybe_switch)); + } } } } }); loop { - let mut nfinished_threads = 0; - if stdin_to_sock_h.is_finished() { - nfinished_threads += 1; - } - if sock_to_stdout_h.is_finished() { - nfinished_threads += 1; - } + let mut nfinished_threads = (stdin_to_sock_h.is_finished() as usize) + + (sock_to_stdout_h.is_finished() as usize); if nfinished_threads > 0 { - if nfinished_threads < 2 { - // Fast-path: when sock->stdout already ended (detach/disconnect), - // stdin->sock can stay blocked on stdin. In that case, do a very - // short grace wait and then exit quickly. This is independent - // of stdin being a TTY or a pipe. - // Slow-path: for other shutdown orders, keep compatibility by - // waiting up to 300ms with backoff. - let mut stdin_done = stdin_to_sock_h.is_finished(); - let mut stdout_done = sock_to_stdout_h.is_finished(); - - // Keep max_wait fixed for this detach sequence. Recomputing it inside - // the loop could accidentally switch paths mid-cleanup. - let max_wait = if stdout_done && !stdin_done { - DETACH_DISCONNECT_FAST_WAIT_DUR - } else { - MAX_DETACH_WAIT_DUR - }; + // If one of the threads has exited, but not the other + // make sure that the exit result slot has some contents + // so the other thread will exit the next time it wakes + // from its poll(). + { + let mut res = result_slot.lock().unwrap(); + if res.is_none() { + *res = Some(PipeBytesResult::Exit(1)); + } + } + if nfinished_threads < 2 { let finished_waiting = common::sleep_unless( - max_wait, + MAX_DETACH_WAIT_DUR, || { - stdin_done = stdin_to_sock_h.is_finished(); - stdout_done = sock_to_stdout_h.is_finished(); - nfinished_threads = (stdin_done as usize) + (stdout_done as usize); + nfinished_threads = (stdin_to_sock_h.is_finished() as usize) + + (sock_to_stdout_h.is_finished() as usize); nfinished_threads >= 2 }, common::PollStrategy::Backoff { @@ -375,12 +424,17 @@ impl Client { if !finished_waiting { // Re-probe after timeout because thread state can change // during the final sleep inside sleep_unless. - stdin_done = stdin_to_sock_h.is_finished(); - stdout_done = sock_to_stdout_h.is_finished(); - nfinished_threads = (stdin_done as usize) + (stdout_done as usize); + nfinished_threads = (stdin_to_sock_h.is_finished() as usize) + + (sock_to_stdout_h.is_finished() as usize); } if nfinished_threads < 2 { + // It should be impossible to get here because both + // loops use poll() to wake up every so often to + // check if they need to exit. Nevertheless, if + // we somehow still have a stuck thread at this + // point, we'll just exit. + // If one of the worker threads is done and the // other is not exiting, we are likely blocked on // some IO. Fortunately, since there isn't much else @@ -389,15 +443,20 @@ impl Client { // by just hard-exiting the whole process. This allows // us to use simple blocking IO. warn!( - "exiting due to a stuck IO thread stdin_to_sock_finished={} sock_to_stdout_finished={}", - stdin_done, - stdout_done + "internal error: exiting due to a stuck IO thread stdin_to_sock_finished={} sock_to_stdout_finished={}", + stdin_to_sock_h.is_finished(), + sock_to_stdout_h.is_finished(), ); // make sure that we restore the tty flags on the input // tty before exiting the process. drop(tty_guard); - std::process::exit(exit_status.load(Ordering::Acquire)); + let res = result_slot.lock().unwrap(); + if let Some(PipeBytesResult::Exit(stat)) = *res { + std::process::exit(stat); + } else { + std::process::exit(1); + } } } break; @@ -425,11 +484,29 @@ impl Client { } } - Ok(exit_status.load(Ordering::Acquire)) + let mut ret = PipeBytesResult::Exit(1); + { + let res = result_slot.lock().unwrap(); + if let Some(r) = res.clone() { + ret = r; + } + } + Ok(ret) }) } } +#[derive(Clone)] +pub enum PipeBytesResult { + /// The attach proc should exit with the given exit status. + Exit(i32), + /// The on_maybe_switch callback has requested the client stop streaming + /// due to some change that requires a reconnect (almost certainly a + /// changed var in the session name template). The attach proc + /// should recompute any relevant templates and re-attach. + MaybeSwitch(shpool_protocol::MaybeSwitch), +} + #[cfg(test)] mod test { use super::*; diff --git a/libshpool/src/template.rs b/libshpool/src/template.rs new file mode 100644 index 00000000..c03dd62f --- /dev/null +++ b/libshpool/src/template.rs @@ -0,0 +1,202 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use anyhow::anyhow; + +// A guess at how large the values of variables will be on average. +// This is intended as a slight over-estimate as we use it to compute +// the buffer size we should pre-allocate for instantiation. +const VAR_SIZE_GUESS: usize = 40; + +/// A template is a simple variable substitution string template used +/// by the templated session name feature to allow automatic client +/// switching. +/// +/// The template syntax is that variable subsitutions look like +/// `#{var_name}`, where var_name must be some alphanumeric string. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Template { + chunks: Vec, + instantiated_size_guess: usize, +} + +/// A chunk is either a raw hunk of text or a variable substitution. +#[derive(Debug, Clone, Eq, PartialEq)] +enum Chunk { + Raw(String), + Var(String), +} + +impl Template { + pub fn new(src: &str) -> anyhow::Result