From 06db90587c79d76b9b56dfe435709ddebb01b329 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sun, 26 Apr 2026 19:53:35 -0700 Subject: [PATCH 1/3] cli: clean up and update dependencies --- cli/Cargo.lock | 609 +++++++++++++---------------- cli/Cargo.toml | 19 +- cli/src/async_pipe.rs | 84 +--- cli/src/auth.rs | 66 +++- cli/src/bin/code/main.rs | 5 +- cli/src/commands/agent_host.rs | 53 +-- cli/src/commands/serve_web.rs | 155 +++++--- cli/src/commands/tunnels.rs | 4 +- cli/src/constants.rs | 38 +- cli/src/desktop/version_manager.rs | 6 +- cli/src/lib.rs | 1 + cli/src/log.rs | 62 +-- cli/src/self_update.rs | 6 +- cli/src/tunnels/agent_host.rs | 57 +-- cli/src/tunnels/code_server.rs | 29 +- cli/src/tunnels/control_server.rs | 58 +-- cli/src/tunnels/dev_tunnels.rs | 182 ++++----- cli/src/tunnels/legal.rs | 8 +- cli/src/tunnels/protocol.rs | 10 +- cli/src/tunnels/service.rs | 4 - cli/src/tunnels/service_linux.rs | 2 - cli/src/tunnels/service_macos.rs | 3 - cli/src/tunnels/service_windows.rs | 2 - cli/src/update_service.rs | 14 +- cli/src/util/errors.rs | 2 +- cli/src/util/http.rs | 93 +++-- cli/src/util/prereqs.rs | 28 +- cli/src/util/sync.rs | 94 +++-- 28 files changed, 752 insertions(+), 942 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 93862f3137794..c8df4ebde2314 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -243,6 +243,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-vec" version = "0.6.3" @@ -342,15 +348,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "chrono" -version = "0.4.43" +name = "chacha20" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -403,11 +408,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" name = "code-cli" version = "0.1.0" dependencies = [ - "async-trait", - "base64", + "base64 0.21.7", "bytes", "cfg-if", - "chrono", "clap", "clap_lex 0.7.7", "console", @@ -418,16 +421,18 @@ dependencies = [ "flate2", "futures", "gethostname", + "http", + "http-body-util", "hyper", + "hyper-util", "indicatif", + "jiff", "keyring", - "lazy_static", "libc", "log", "open", - "opentelemetry", "pin-project", - "rand 0.9.3", + "rand 0.10.1", "regex", "reqwest", "rmp-serde", @@ -439,7 +444,7 @@ dependencies = [ "sysinfo", "tar", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", "tunnels", @@ -526,21 +531,21 @@ dependencies = [ ] [[package]] -name = "crc32fast" -version = "1.5.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ - "cfg-if", + "libc", ] [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "crossbeam-utils", + "cfg-if", ] [[package]] @@ -803,12 +808,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -1016,35 +1015,11 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", ] -[[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.13.0", - "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.15.5" @@ -1101,23 +1076,34 @@ dependencies = [ [[package]] name = "http" -version = "0.2.12" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 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", ] @@ -1135,63 +1121,64 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.32" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "hyper-util" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "cc", + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1302,16 +1289,6 @@ dependencies = [ "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.13.0" @@ -1373,6 +1350,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -1404,6 +1391,30 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1442,9 +1453,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" @@ -1565,9 +1576,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -1936,51 +1947,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "opentelemetry" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4b8347cc26099d3aeee044065ecc3ae11469796b4d65d065a23a584ed92a6f" -dependencies = [ - "opentelemetry_api", - "opentelemetry_sdk", -] - -[[package]] -name = "opentelemetry_api" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed41783a5bf567688eb38372f2b7a8530f5a607a4b49d38dd7573236c23ca7e2" -dependencies = [ - "futures-channel", - "futures-util", - "indexmap 1.9.3", - "once_cell", - "pin-project-lite", - "thiserror", - "urlencoding", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3a2a91fdbfdd4d212c0dcc2ab540de2c2bcbbd90be17de7a7daf8822d010c1" -dependencies = [ - "async-trait", - "crossbeam-channel", - "futures-channel", - "futures-executor", - "futures-util", - "once_cell", - "opentelemetry_api", - "percent-encoding", - "rand 0.8.5", - "thiserror", - "tokio", - "tokio-stream", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -2138,6 +2104,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -2232,14 +2207,25 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2297,6 +2283,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_hc" version = "0.2.0" @@ -2332,7 +2324,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2366,44 +2358,42 @@ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-tls", - "ipnet", + "hyper-util", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-util", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg 0.50.0", ] [[package]] @@ -2449,7 +2439,7 @@ dependencies = [ "sha1", "sha2", "subtle", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", ] @@ -2484,7 +2474,7 @@ dependencies = [ "russh-cryptovec", "serde", "sha2", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "yasna", @@ -2531,12 +2521,12 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-pki-types" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "base64", + "zeroize", ] [[package]] @@ -2545,12 +2535,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "schannel" version = "0.1.28" @@ -2684,18 +2668,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2703,7 +2675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2714,7 +2686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2786,12 +2758,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2842,9 +2814,12 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2873,20 +2848,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -2922,7 +2897,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2936,6 +2920,17 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "time" version = "0.3.47" @@ -2967,9 +2962,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -2977,7 +2972,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -2985,9 +2980,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3017,9 +3012,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", @@ -3049,7 +3044,7 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.13.0", + "indexmap", "serde_core", "serde_spanned", "toml_datetime 0.7.5+spec-1.1.0", @@ -3079,7 +3074,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.13.0", + "indexmap", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -3099,6 +3094,45 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3144,48 +3178,45 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ - "byteorder", "bytes", "data-encoding", "http", "httparse", "log", "native-tls", - "rand 0.8.5", + "rand 0.9.4", "sha1", - "thiserror", - "url", - "utf-8", + "thiserror 2.0.18", ] [[package]] name = "tunnels" version = "0.1.0" -source = "git+https://github.com/microsoft/dev-tunnels?rev=8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30#8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30" dependencies = [ "async-trait", - "chrono", - "futures", + "futures-util", + "http-body-util", "hyper", + "hyper-util", + "jiff", "log", "os_info", + "percent-encoding", "rand 0.8.5", "reqwest", "russh", "russh-keys", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-tungstenite", "tokio-util", - "tungstenite", "url", - "urlencoding", "uuid", "winreg 0.8.0", ] @@ -3237,18 +3268,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3406,16 +3425,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap", "wasm-encoder", "wasmparser", ] [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -3432,7 +3451,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap", "semver", ] @@ -3479,46 +3498,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.115", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-interface" -version = "0.59.3" +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.115", + "windows-link", + "windows-result", + "windows-strings", ] -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.4.1" @@ -3564,15 +3559,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -3606,30 +3592,13 @@ 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_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "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.48.5" @@ -3642,12 +3611,6 @@ 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.48.5" @@ -3660,12 +3623,6 @@ 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.48.5" @@ -3678,24 +3635,12 @@ 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.48.5" @@ -3708,12 +3653,6 @@ 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.48.5" @@ -3726,12 +3665,6 @@ 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.48.5" @@ -3744,12 +3677,6 @@ 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.48.5" @@ -3762,12 +3689,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.5.40" @@ -3840,7 +3761,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap", "prettyplease", "syn 2.0.115", "wasm-metadata", @@ -3871,7 +3792,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.10.0", - "indexmap 2.13.0", + "indexmap", "log", "serde", "serde_derive", @@ -3890,7 +3811,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap", "log", "semver", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0389746bbeca0..c89eb27f57c03 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -15,34 +15,35 @@ name = "code" futures = "0.3.28" clap = { version = "4.3.0", features = ["derive", "env"] } open = "4.1.0" -reqwest = { version = "0.11.22", default-features = false, features = ["json", "stream", "native-tls"] } -tokio = { version = "1.38.2", features = ["full"] } +reqwest = { version = "0.13", default-features = false, features = ["json", "stream", "native-tls"] } +tokio = { version = "1.52", features = ["full"] } tokio-util = { version = "0.7.8", features = ["compat", "codec"] } flate2 = { version = "1.0.26", default-features = false, features = ["zlib"] } zip = { version = "0.6.6", default-features = false, features = ["time", "deflate-zlib"] } regex = "1.8.3" -lazy_static = "1.4.0" sysinfo = { version = "0.29.0", default-features = false } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" rmp-serde = "1.1.1" uuid = { version = "1.4", features = ["serde", "v4"] } dirs = "5.0.1" -rand = "0.9.3" -opentelemetry = { version = "0.19.0", features = ["rt-tokio"] } +rand = "0.10" serde_bytes = "0.11.9" -chrono = { version = "0.4.26", features = ["serde", "std", "clock"], default-features = false } +jiff = { version = "0.2", default-features = false, features = ["std", "serde"] } +http = "1" gethostname = "0.4.3" libc = "0.2.144" -tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30", default-features = false, features = ["connections"] } +# tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30", default-features = false, features = ["connections"] } +tunnels = { path = "C:\\Users\\conno\\Github\\dev-tunnels\\rs", default-features = false, features = ["connections"] } keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl", "platform-windows", "platform-macos", "linux-keyutils"] } dialoguer = "0.10.4" -hyper = { version = "0.14.26", features = ["server", "http1", "runtime"] } +hyper = { version = "1", features = ["server", "http1", "client"] } +hyper-util = { version = "0.1", features = ["tokio"] } +http-body-util = "0.1" indicatif = "0.17.4" tempfile = "3.5.0" clap_lex = "0.7.0" url = "2.5.4" -async-trait = "0.1.68" log = "0.4.18" const_format = "0.2.31" sha2 = "0.10.6" diff --git a/cli/src/async_pipe.rs b/cli/src/async_pipe.rs index 78aed6fe3e769..efa8a77008272 100644 --- a/cli/src/async_pipe.rs +++ b/cli/src/async_pipe.rs @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ use crate::{constants::APPLICATION_NAME, util::errors::CodeError}; -use async_trait::async_trait; +use std::future::Future; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -176,57 +176,6 @@ cfg_if::cfg_if! { } } -impl AsyncPipeListener { - pub fn into_pollable(self) -> PollableAsyncListener { - PollableAsyncListener { - listener: Some(self), - write_fut: tokio_util::sync::ReusableBoxFuture::new(make_accept_fut(None)), - } - } -} - -pub struct PollableAsyncListener { - listener: Option, - write_fut: tokio_util::sync::ReusableBoxFuture< - 'static, - (AsyncPipeListener, Result), - >, -} - -async fn make_accept_fut( - data: Option, -) -> (AsyncPipeListener, Result) { - match data { - Some(mut l) => { - let c = l.accept().await; - (l, c) - } - None => unreachable!("this future should not be pollable in this state"), - } -} - -impl hyper::server::accept::Accept for PollableAsyncListener { - type Conn = AsyncPipe; - type Error = CodeError; - - fn poll_accept( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - if let Some(l) = self.listener.take() { - self.write_fut.set(make_accept_fut(Some(l))) - } - - match self.write_fut.poll(cx) { - Poll::Ready((l, cnx)) => { - self.listener = Some(l); - Poll::Ready(Some(cnx)) - } - Poll::Pending => Poll::Pending, - } - } -} - /// Gets a random name for a pipe/socket on the platform pub fn get_socket_name() -> PathBuf { cfg_if::cfg_if! { @@ -243,28 +192,29 @@ pub type AcceptedRW = ( Box, ); -#[async_trait] pub trait AsyncRWAccepter { - async fn accept_rw(&mut self) -> Result; + fn accept_rw(&mut self) -> Pin> + Send + '_>>; } -#[async_trait] impl AsyncRWAccepter for AsyncPipeListener { - async fn accept_rw(&mut self) -> Result { - let pipe = self.accept().await?; - let (read, write) = socket_stream_split(pipe); - Ok((Box::new(read), Box::new(write))) + fn accept_rw(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { + let pipe = self.accept().await?; + let (read, write) = socket_stream_split(pipe); + Ok((Box::new(read) as Box, Box::new(write) as Box)) + }) } } -#[async_trait] impl AsyncRWAccepter for TcpListener { - async fn accept_rw(&mut self) -> Result { - let (stream, _) = self - .accept() - .await - .map_err(CodeError::AsyncPipeListenerFailed)?; - let (read, write) = tokio::io::split(stream); - Ok((Box::new(read), Box::new(write))) + fn accept_rw(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { + let (stream, _) = self + .accept() + .await + .map_err(CodeError::AsyncPipeListenerFailed)?; + let (read, write) = tokio::io::split(stream); + Ok((Box::new(read) as Box, Box::new(write) as Box)) + }) } } diff --git a/cli/src/auth.rs b/cli/src/auth.rs index d4d62a8bf104e..cd8492065b266 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -10,18 +10,20 @@ use crate::{ trace, util::{ errors::{ - wrap, AnyError, CodeError, OAuthError, RefreshTokenNotAvailableError, StatusError, + wrap, AnyError, OAuthError, RefreshTokenNotAvailableError, StatusError, WrappedError, }, input::prompt_options, }, warning, }; -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use gethostname::gethostname; +use jiff::{SignedDuration, Timestamp}; +#[cfg(target_os = "linux")] +use crate::util::errors::CodeError; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{cell::Cell, fmt::Display, path::PathBuf, sync::Arc, thread}; +use std::{cell::Cell, fmt::Display, future::Future, path::PathBuf, pin::Pin, sync::Arc}; +#[cfg(target_os = "linux")] +use std::thread; use tokio::time::sleep; use tunnels::{ contracts::PROD_FIRST_PARTY_APP_ID, @@ -110,7 +112,7 @@ pub struct StoredCredential { #[serde(rename = "r")] refresh_token: Option, #[serde(rename = "e")] - expires_at: Option>, + expires_at: Option, } const GH_USER_ENDPOINT: &str = "https://api.github.com/user"; @@ -132,7 +134,7 @@ impl StoredCredential { match self.provider { AuthProvider::Microsoft => self .expires_at - .map(|e| Utc::now() + chrono::Duration::minutes(5) > e) + .map(|e| Timestamp::now() + SignedDuration::from_secs(5 * 60) > e) .unwrap_or(false), // Make an auth request to Github. Mark the credential as expired @@ -166,7 +168,7 @@ impl StoredCredential { refresh_token: auth.refresh_token, expires_at: auth .expires_in - .map(|e| Utc::now() + chrono::Duration::seconds(e)), + .map(|e| Timestamp::now() + SignedDuration::from_secs(e)), } } } @@ -226,10 +228,12 @@ const CONTINUE_MARKER: &str = ""; /// Implementation that wraps the KeyringStorage on Linux to avoid /// https://github.com/hwchen/keyring-rs/issues/132 +#[cfg(target_os = "linux")] struct ThreadKeyringStorage { s: Option, } +#[cfg(target_os = "linux")] impl ThreadKeyringStorage { fn thread_op(&mut self, f: Fn) -> Result where @@ -262,6 +266,7 @@ impl ThreadKeyringStorage { } } +#[cfg(target_os = "linux")] impl Default for ThreadKeyringStorage { fn default() -> Self { Self { @@ -270,6 +275,7 @@ impl Default for ThreadKeyringStorage { } } +#[cfg(target_os = "linux")] impl StorageImplementation for ThreadKeyringStorage { fn read(&mut self) -> Result, AnyError> { self.thread_op(|s| s.read()) @@ -494,7 +500,7 @@ impl Auth { // soon in order to get the real expiry time. expires_at: refresh_token .as_ref() - .map(|_| Utc::now() + chrono::Duration::minutes(5)), + .map(|_| Timestamp::now() + SignedDuration::from_secs(5 * 60)), refresh_token, }, None => self.do_device_code_flow_with_provider(provider).await?, @@ -718,7 +724,8 @@ impl Auth { } let init_code_json = init_code.json::().await?; - let expires_at = Utc::now() + chrono::Duration::seconds(init_code_json.expires_in); + let expires_at = + Timestamp::now() + SignedDuration::from_secs(init_code_json.expires_in); match &init_code_json.message { Some(m) => self.log.result(m), @@ -735,7 +742,7 @@ impl Auth { ); let mut interval_s = 5; - while Utc::now() < expires_at { + while Timestamp::now() < expires_at { sleep(std::time::Duration::from_secs(interval_s)).await; match self.do_grant(provider, body.clone()).await { @@ -772,7 +779,19 @@ impl Auth { min_refresh } else { match credential.expires_at { - Some(d) => ((d - Utc::now()) * 2 / 3).to_std().unwrap_or(min_refresh), + Some(d) => { + let dur = d.duration_since(Timestamp::now()); + let nanos = dur.as_nanos() * 2 / 3; + let scaled = SignedDuration::new( + (nanos / 1_000_000_000) as i64, + (nanos % 1_000_000_000) as i32, + ); + if scaled.is_negative() { + min_refresh + } else { + scaled.unsigned_abs() + } + } None => default_refresh, } }; @@ -807,18 +826,25 @@ impl Auth { } } -#[async_trait] impl AuthorizationProvider for Auth { - async fn get_authorization(&self) -> Result { - self.get_tunnel_authentication() - .await - .map_err(|e| HttpError::AuthorizationError(e.to_string())) + fn get_authorization( + &self, + ) -> Pin> + Send + '_>> { + Box::pin(async move { + self.get_tunnel_authentication() + .await + .map_err(|e| HttpError::AuthorizationError(e.to_string())) + }) } } -lazy_static::lazy_static! { - static ref HOSTNAME: Vec = gethostname().to_string_lossy().bytes().collect(); -} +#[cfg(feature = "vscode-encrypt")] +static HOSTNAME: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + gethostname::gethostname() + .to_string_lossy() + .bytes() + .collect() +}); #[cfg(feature = "vscode-encrypt")] fn encrypt(value: &str) -> String { diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index 6c301ca9502be..fda5fc36145ad 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -19,8 +19,6 @@ use cli::{ }, }; use legacy_args::try_parse_legacy; -use opentelemetry::sdk::trace::TracerProvider as SdkTracerProvider; -use opentelemetry::trace::TracerProvider; #[tokio::main] async fn main() -> Result<(), std::convert::Infallible> { @@ -143,8 +141,7 @@ fn make_logger(core: &args::CliCore) -> log::Logger { core.global_options.log.unwrap_or(log::Level::Info) }; - let tracer = SdkTracerProvider::builder().build().tracer("codecli"); - let mut log = log::Logger::new(tracer, log_level); + let mut log = log::Logger::new(log_level); if let Some(f) = &core.global_options.log_to_file { log = log .with_sink(log::FileLogSink::new(log_level, f).expect("expected to make file logger")) diff --git a/cli/src/commands/agent_host.rs b/cli/src/commands/agent_host.rs index bfa0644b2ec81..d3c4a11ee777a 100644 --- a/cli/src/commands/agent_host.rs +++ b/cli/src/commands/agent_host.rs @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use std::convert::Infallible; use std::fs; use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::sync::Arc; -use hyper::service::{make_service_fn, service_fn}; -use hyper::Server; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; use crate::log; use crate::tunnels::agent_host::{handle_request, AgentHostConfig, AgentHostManager}; @@ -88,8 +89,8 @@ pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result< Some(h) => SocketAddr::new(h.parse().map_err(CodeError::InvalidHostAddress)?, args.port), None => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port), }; - let builder = Server::try_bind(&addr).map_err(CodeError::CouldNotListenOnInterface)?; - let bound_addr = builder.local_addr(); + let listener = TcpListener::bind(addr).await.map_err(CodeError::CouldNotListenOnInterface)?; + let bound_addr = listener.local_addr().map_err(CodeError::CouldNotListenOnInterface)?; let mut url = format!("ws://{bound_addr}"); if let Some(ct) = &args.connection_token { @@ -98,25 +99,33 @@ pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result< ctx.log .result(format!("Agent host proxy listening on {url}")); - let manager_for_svc = manager.clone(); - let make_svc = move || { - let mgr = manager_for_svc.clone(); - let service = service_fn(move |req| { - let mgr = mgr.clone(); - async move { handle_request(mgr, req).await } - }); - async move { Ok::<_, Infallible>(service) } - }; - - let server_future = builder - .serve(make_service_fn(|_| make_svc())) - .with_graceful_shutdown(async { - let _ = shutdown.wait().await; - }); + loop { + tokio::select! { + result = listener.accept() => { + let (stream, _) = match result { + Ok(r) => r, + Err(_) => continue, + }; + let mgr = manager.clone(); + tokio::spawn(async move { + let svc = service_fn(move |req| { + let mgr = mgr.clone(); + async move { handle_request(mgr, req).await } + }); + if let Err(e) = http1::Builder::new() + .serve_connection(TokioIo::new(stream), svc) + .with_upgrades() + .await + { + let _ = e; // connection closed + } + }); + } + _ = shutdown.wait() => break, + } + } - let r = server_future.await; manager.kill_running_server().await; - r.map_err(CodeError::CouldNotListenOnInterface)?; Ok(0) } diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index d3a9a88a87dd4..58fdbabf9d10b 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -12,8 +12,14 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Request, Response, Server}; +use ::http::{Request, Response}; +use http_body_util::BodyExt; +use hyper::body::Incoming; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; +use crate::util::http::{HyperBody, full_body, empty_body}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::{pin, time}; @@ -100,30 +106,33 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result(service) } - }; let mut shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]); - let r = if let Some(s) = args.socket_path { + if let Some(s) = args.socket_path { let s = PathBuf::from(&s); - let socket = listen_socket_rw_stream(&s).await?; + let mut socket = listen_socket_rw_stream(&s).await?; ctx.log .result(format!("Web UI available on {}", s.display())); - let r = Server::builder(socket.into_pollable()) - .serve(make_service_fn(|_| make_svc())) - .with_graceful_shutdown(async { - let _ = shutdown.wait().await; - }) - .await; + loop { + tokio::select! { + result = socket.accept() => { + let conn = match result { + Ok(c) => c, + Err(_) => continue, + }; + let ctx = HandleContext { cm: cm.clone(), log: cm.log.clone(), server_secret_key: key.clone() }; + tokio::spawn(async move { + let svc = service_fn(move |req| handle(ctx.clone(), req)); + let _ = http1::Builder::new() + .serve_connection(TokioIo::new(conn), svc) + .with_upgrades() + .await; + }); + } + _ = shutdown.wait() => break, + } + } let _ = std::fs::remove_file(&s); // cleanup - r } else { let addr: SocketAddr = match &args.host { Some(h) => { @@ -131,10 +140,10 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port), }; - let builder = Server::try_bind(&addr).map_err(CodeError::CouldNotListenOnInterface)?; + let listener = TcpListener::bind(addr).await.map_err(CodeError::CouldNotListenOnInterface)?; // Get the actual bound address (important when port 0 is used for random port assignment) - let bound_addr = builder.local_addr(); + let bound_addr = listener.local_addr().map_err(CodeError::CouldNotListenOnInterface)?; let mut listening = format!("Web UI available at http://{bound_addr}"); if let Some(base) = args.server_base_path { if !base.starts_with('/') { @@ -147,16 +156,27 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result { + let (stream, _) = match result { + Ok(r) => r, + Err(_) => continue, + }; + let ctx = HandleContext { cm: cm.clone(), log: cm.log.clone(), server_secret_key: key.clone() }; + tokio::spawn(async move { + let svc = service_fn(move |req| handle(ctx.clone(), req)); + let _ = http1::Builder::new() + .serve_connection(TokioIo::new(stream), svc) + .with_upgrades() + .await; + }); + } + _ = shutdown.wait() => break, + } + } }; - r.map_err(CodeError::CouldNotListenOnInterface)?; - Ok(0) } @@ -168,7 +188,7 @@ struct HandleContext { } /// Handler function for an inbound request -async fn handle(ctx: HandleContext, req: Request) -> Result, Infallible> { +async fn handle(ctx: HandleContext, req: Request) -> Result, Infallible> { let client_key_half = get_client_key_half(&req); let path = req.uri().path(); @@ -185,7 +205,7 @@ async fn handle(ctx: HandleContext, req: Request) -> Result Ok(res) } -async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response { +async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response { let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), ctx.cm.platform) { r } else { @@ -200,7 +220,7 @@ async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response { - if req.headers().contains_key(hyper::header::UPGRADE) { + if req.headers().contains_key(::http::header::UPGRADE) { forward_ws_req_to_server(ctx.log.clone(), rw, req).await } else { forward_http_req_to_server(rw, req).await @@ -211,7 +231,7 @@ async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response) -> Response { +fn handle_secret_mint(ctx: &HandleContext, req: Request) -> Response { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); @@ -227,18 +247,18 @@ fn handle_secret_mint(ctx: &HandleContext, req: Request) -> Response /// and maintains the http-only cookie the client will use for cookies. fn append_secret_headers( base_path: &str, - res: &mut Response, + res: &mut Response, client_key_half: &SecretKeyPart, ) { let headers = res.headers_mut(); headers.append( - hyper::header::SET_COOKIE, + ::http::header::SET_COOKIE, format!("{PATH_COOKIE_NAME}={base_path}{SECRET_KEY_MINT_PATH}; SameSite=Strict; Path=/",) .parse() .unwrap(), ); headers.append( - hyper::header::SET_COOKIE, + ::http::header::SET_COOKIE, format!( "{}={}; SameSite=Strict; HttpOnly; Max-Age=2592000; Path=/", SECRET_KEY_COOKIE_NAME, @@ -284,20 +304,20 @@ fn get_release_from_path(path: &str, platform: Platform) -> Option<(Release, Str /// Proxies the standard HTTP request to the async pipe, returning the piped response async fn forward_http_req_to_server( (rw, handle): (AsyncPipe, ConnectionHandle), - req: Request, -) -> Response { + req: Request, +) -> Response { let (mut request_sender, connection) = - match hyper::client::conn::Builder::new().handshake(rw).await { + match hyper::client::conn::http1::handshake(TokioIo::new(rw)).await { Ok(r) => r, Err(e) => return response::connection_err(e), }; tokio::spawn(connection); - let res = request_sender - .send_request(req) - .await - .unwrap_or_else(response::connection_err); + let res = match request_sender.send_request(req).await { + Ok(res) => res.map(|b| b.boxed()), + Err(e) => response::connection_err(e), + }; // technically, we should buffer the body into memory since it may not be // read at this point, but because the keepalive time is very large @@ -312,11 +332,11 @@ async fn forward_http_req_to_server( async fn forward_ws_req_to_server( log: log::Logger, (rw, handle): (AsyncPipe, ConnectionHandle), - mut req: Request, -) -> Response { + mut req: Request, +) -> Response { // splicing of client and servers inspired by https://github.com/hyperium/hyper/blob/fece9f7f50431cf9533cfe7106b53a77b48db699/examples/upgrades.rs let (mut request_sender, connection) = - match hyper::client::conn::Builder::new().handshake(rw).await { + match hyper::client::conn::http1::handshake(TokioIo::new(rw)).await { Ok(r) => r, Err(e) => return response::connection_err(e), }; @@ -328,19 +348,22 @@ async fn forward_ws_req_to_server( proxied_req = proxied_req.header(k, v); } - let mut res = request_sender - .send_request(proxied_req.body(Body::empty()).unwrap()) + let mut res = match request_sender + .send_request(proxied_req.body(http_body_util::Empty::::new()).unwrap()) .await - .unwrap_or_else(response::connection_err); + { + Ok(r) => r, + Err(e) => return response::connection_err(e), + }; - let mut proxied_res = Response::new(Body::empty()); + let mut proxied_res = Response::new(empty_body()); *proxied_res.status_mut() = res.status(); for (k, v) in res.headers() { proxied_res.headers_mut().insert(k, v.clone()); } // only start upgrade at this point in case the server decides to deny socket - if res.status() == hyper::StatusCode::SWITCHING_PROTOCOLS { + if res.status() == ::http::StatusCode::SWITCHING_PROTOCOLS { tokio::spawn(async move { let (s_req, s_res) = tokio::join!(hyper::upgrade::on(&mut req), hyper::upgrade::on(&mut res)); @@ -352,8 +375,10 @@ async fn forward_ws_req_to_server( ), (Err(e1), _) => debug!(log, "client ({}) websocket upgrade failed", e1), (_, Err(e2)) => debug!(log, "server ({}) websocket upgrade failed", e2), - (Ok(mut s_req), Ok(mut s_res)) => { + (Ok(s_req), Ok(s_res)) => { trace!(log, "websocket upgrade succeeded"); + let mut s_req = TokioIo::new(s_req); + let mut s_res = TokioIo::new(s_res); let r = tokio::io::copy_bidirectional(&mut s_req, &mut s_res).await; trace!(log, "websocket closed (error: {:?})", r.err()); } @@ -372,8 +397,8 @@ fn is_commit_hash(s: &str) -> bool { } /// Gets a cookie from the request by name. -fn extract_cookie(req: &Request, name: &str) -> Option { - for h in req.headers().get_all(hyper::header::COOKIE) { +fn extract_cookie(req: &Request, name: &str) -> Option { + for h in req.headers().get_all(::http::header::COOKIE) { if let Ok(str) = h.to_str() { for pair in str.split("; ") { let i = match pair.find('=') { @@ -432,7 +457,7 @@ fn get_server_key_half(paths: &LauncherPaths) -> SecretKeyPart { } /// Gets the client's half of the secret key. -fn get_client_key_half(req: &Request) -> SecretKeyPart { +fn get_client_key_half(req: &Request) -> SecretKeyPart { if let Some(c) = extract_cookie(req, SECRET_KEY_COOKIE_NAME) { if let Ok(sk) = SecretKeyPart::decode(&c) { return sk; @@ -450,33 +475,33 @@ mod response { use super::*; - pub fn connection_err(err: hyper::Error) -> Response { + pub fn connection_err(err: hyper::Error) -> Response { Response::builder() .status(503) - .body(Body::from(format!("Error connecting to server: {err:?}"))) + .body(full_body(format!("Error connecting to server: {err:?}"))) .unwrap() } - pub fn code_err(err: CodeError) -> Response { + pub fn code_err(err: CodeError) -> Response { Response::builder() .status(500) - .body(Body::from(format!("Error serving request: {err}"))) + .body(full_body(format!("Error serving request: {err}"))) .unwrap() } - pub fn wait_for_download() -> Response { + pub fn wait_for_download() -> Response { Response::builder() .status(202) .header("Content-Type", "text/html") // todo: get latest - .body(Body::from(concatcp!("The latest version of the ", QUALITYLESS_SERVER_NAME, " is downloading, please wait a moment...", ))) + .body(full_body(concatcp!("The latest version of the ", QUALITYLESS_SERVER_NAME, " is downloading, please wait a moment...", ))) .unwrap() } - pub fn secret_key(hash: Vec) -> Response { + pub fn secret_key(hash: Vec) -> Response { Response::builder() .status(200) .header("Content-Type", "application/octet-stream") // todo: get latest - .body(Body::from(hash)) + .body(full_body(hash)) .unwrap() } } @@ -634,7 +659,7 @@ impl ConnectionManager { let target_kind = TargetKind::Web; let quality = VSCODE_CLI_QUALITY - .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .ok_or(CodeError::UpdatesNotConfigured("no configured quality")) .and_then(|q| { Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) })?; diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 8f99f8d37d660..3a9fa375f99ac 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use async_trait::async_trait; use base64::{engine::general_purpose as b64, Engine as _}; use futures::{stream::FuturesUnordered, StreamExt}; use serde::Serialize; @@ -118,7 +117,6 @@ impl TunnelServiceContainer { } } -#[async_trait] impl ServiceContainer for TunnelServiceContainer { async fn run_service( &mut self, @@ -639,7 +637,7 @@ async fn serve_with_csa( let mut server = make_singleton_server(log_broadcast.clone(), log.clone(), server, shutdown.clone()); - let platform = spanf!(log, log.span("prereq"), PreReqChecker::new().verify())?; + let platform = PreReqChecker::new().verify().await?; let _lock = app_mutex_name.map(AppMutex::new); let auth = Auth::new(&paths, log.clone()); diff --git a/cli/src/constants.rs b/cli/src/constants.rs index 9e2b066d74139..f6979db0157fa 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -7,7 +7,7 @@ use serde::Deserialize; use std::{collections::HashMap, io::IsTerminal}; use const_format::concatcp; -use lazy_static::lazy_static; +use std::sync::LazyLock; use crate::options::Quality; @@ -107,27 +107,25 @@ pub struct ServerQualityInfo { pub server_application_name: String, } -lazy_static! { - pub static ref TUNNEL_SERVICE_USER_AGENT: String = - match std::env::var(TUNNEL_SERVICE_USER_AGENT_ENV_VAR) { - Ok(ua) if !ua.is_empty() => format!("{} {}", ua, get_default_user_agent()), - _ => get_default_user_agent(), - }; +pub static TUNNEL_SERVICE_USER_AGENT: LazyLock = + LazyLock::new(|| match std::env::var(TUNNEL_SERVICE_USER_AGENT_ENV_VAR) { + Ok(ua) if !ua.is_empty() => format!("{} {}", ua, get_default_user_agent()), + _ => get_default_user_agent(), + }); - /// Map of qualities to the server name - pub static ref SERVER_NAME_MAP: Option> = - option_env!("VSCODE_CLI_TUNNEL_SERVER_QUALITIES").and_then(|s| serde_json::from_str(s).unwrap()); +/// Map of qualities to the server name +pub static SERVER_NAME_MAP: LazyLock>> = + LazyLock::new(|| option_env!("VSCODE_CLI_TUNNEL_SERVER_QUALITIES").and_then(|s| serde_json::from_str(s).unwrap())); - /// Whether i/o interactions are allowed in the current CLI. - pub static ref IS_A_TTY: bool = std::io::stdin().is_terminal(); +/// Whether i/o interactions are allowed in the current CLI. +pub static IS_A_TTY: LazyLock = LazyLock::new(|| std::io::stdin().is_terminal()); - /// Whether i/o interactions are allowed in the current CLI. - pub static ref COLORS_ENABLED: bool = *IS_A_TTY && std::env::var(NO_COLOR_ENV).is_err(); +/// Whether i/o interactions are allowed in the current CLI. +pub static COLORS_ENABLED: LazyLock = LazyLock::new(|| *IS_A_TTY && std::env::var(NO_COLOR_ENV).is_err()); - /// Whether i/o interactions are allowed in the current CLI. - pub static ref IS_INTERACTIVE_CLI: bool = *IS_A_TTY && std::env::var(NONINTERACTIVE_VAR).is_err(); +/// Whether i/o interactions are allowed in the current CLI. +pub static IS_INTERACTIVE_CLI: LazyLock = LazyLock::new(|| *IS_A_TTY && std::env::var(NONINTERACTIVE_VAR).is_err()); - /// Map of quality names to arrays of app IDs used for them, for example, `{"stable":["ABC123"]}` - pub static ref WIN32_APP_IDS: Option> = - option_env!("VSCODE_CLI_WIN32_APP_IDS").map(|s| s.split(',').map(|s| s.to_string()).collect()); -} +/// Map of quality names to arrays of app IDs used for them, for example, `{"stable":["ABC123"]}` +pub static WIN32_APP_IDS: LazyLock>> = + LazyLock::new(|| option_env!("VSCODE_CLI_WIN32_APP_IDS").map(|s| s.split(',').map(|s| s.to_string()).collect())); diff --git a/cli/src/desktop/version_manager.rs b/cli/src/desktop/version_manager.rs index e9cd1a1045009..5eb3ea6049048 100644 --- a/cli/src/desktop/version_manager.rs +++ b/cli/src/desktop/version_manager.rs @@ -9,8 +9,8 @@ use std::{ path::{Path, PathBuf}, }; -use lazy_static::lazy_static; use regex::Regex; +use std::sync::LazyLock; use serde::{Deserialize, Serialize}; use crate::{ @@ -33,9 +33,7 @@ pub enum RequestedVersion { Path(String), } -lazy_static! { - static ref COMMIT_RE: Regex = Regex::new(r"(?i)^[0-9a-f]{40}$").unwrap(); -} +static COMMIT_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?i)^[0-9a-f]{40}$").unwrap()); impl RequestedVersion { pub fn get_command(&self) -> String { diff --git a/cli/src/lib.rs b/cli/src/lib.rs index b2e23cb4d6994..860025337949a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +#![allow(async_fn_in_trait)] // todo: we should reduce the exported surface area over time as things are // moved into a common CLI diff --git a/cli/src/log.rs b/cli/src/log.rs index f58f49b21764e..3b2bca5ce17b9 100644 --- a/cli/src/log.rs +++ b/cli/src/log.rs @@ -3,11 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use chrono::Local; -use opentelemetry::{ - sdk::trace::{Tracer, TracerProvider}, - trace::{SpanBuilder, Tracer as TraitTracer, TracerProvider as TracerProviderTrait}, -}; +use jiff::Zoned; use serde::{Deserialize, Serialize}; use std::fmt; use std::{ @@ -103,7 +99,6 @@ pub fn new_rpc_prefix() -> String { // Base logger implementation #[derive(Clone)] pub struct Logger { - tracer: Arc, sink: Vec>, prefix: Option, } @@ -199,28 +194,18 @@ impl LogSink for FileLogSink { impl Logger { pub fn test() -> Self { Self { - tracer: Arc::new(TracerProvider::builder().build().tracer("codeclitest")), sink: vec![], prefix: None, } } - pub fn new(tracer: Tracer, level: Level) -> Self { + pub fn new(level: Level) -> Self { Self { - tracer: Arc::new(tracer), sink: vec![Box::new(StdioLogSink { level })], prefix: None, } } - pub fn span(&self, name: &str) -> SpanBuilder { - self.tracer.span_builder(format!("serverlauncher/{name}")) - } - - pub fn tracer(&self) -> &Tracer { - &self.tracer - } - pub fn emit(&self, level: Level, message: &str) { let prefix = self.prefix.as_deref().unwrap_or(""); for sink in &self.sink { @@ -305,8 +290,8 @@ impl crate::util::io::ReportCopyProgress for DownloadLogger<'_> { } fn format(level: Level, prefix: &str, message: &str, use_colors: bool) -> String { - let current = Local::now(); - let timestamp = current.format("%Y-%m-%d %H:%M:%S").to_string(); + let current = Zoned::now(); + let timestamp = current.strftime("%Y-%m-%d %H:%M:%S").to_string(); let name = level.name().unwrap(); @@ -421,42 +406,3 @@ macro_rules! warning { $logger.emit(log::Level::Warn, &format!($($fmt),+)) }; } - -#[macro_export] -macro_rules! span { - ($logger:expr, $span:expr, $func:expr) => {{ - use opentelemetry::trace::TraceContextExt; - - let span = $span.start($logger.tracer()); - let cx = opentelemetry::Context::current_with_span(span); - let guard = cx.clone().attach(); - let t = $func; - - if let Err(e) = &t { - cx.span().record_error(e); - } - - std::mem::drop(guard); - - t - }}; -} - -#[macro_export] -macro_rules! spanf { - ($logger:expr, $span:expr, $func:expr) => {{ - use opentelemetry::trace::{FutureExt, TraceContextExt}; - - let span = $span.start($logger.tracer()); - let cx = opentelemetry::Context::current_with_span(span); - let t = $func.with_context(cx.clone()).await; - - if let Err(e) = &t { - cx.span().record_error(e); - } - - cx.span().end(); - - t - }}; -} diff --git a/cli/src/self_update.rs b/cli/src/self_update.rs index 45d661e5af98b..21d6d6c756669 100644 --- a/cli/src/self_update.rs +++ b/cli/src/self_update.rs @@ -30,15 +30,15 @@ static OLD_UPDATE_EXTENSION: &str = "Updating CLI"; impl<'a> SelfUpdate<'a> { pub fn new(update_service: &'a UpdateService) -> Result { let commit = VSCODE_CLI_COMMIT - .ok_or_else(|| CodeError::UpdatesNotConfigured("unknown build commit"))?; + .ok_or(CodeError::UpdatesNotConfigured("unknown build commit"))?; let quality = VSCODE_CLI_QUALITY - .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .ok_or(CodeError::UpdatesNotConfigured("no configured quality")) .and_then(|q| { Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) })?; - let platform = Platform::env_default().ok_or_else(|| { + let platform = Platform::env_default().ok_or({ CodeError::UpdatesNotConfigured("Unknown platform, please report this error") })?; diff --git a/cli/src/tunnels/agent_host.rs b/cli/src/tunnels/agent_host.rs index 9d1f240c5099e..46786ba4f5a49 100644 --- a/cli/src/tunnels/agent_host.rs +++ b/cli/src/tunnels/agent_host.rs @@ -8,11 +8,15 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant}; -use hyper::{Body, Request, Response}; +use hyper::body::Incoming; +use ::http::{Request, Response}; +use http_body_util::BodyExt; +use hyper_util::rt::TokioIo; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::sync::Mutex; use crate::async_pipe::{get_socket_name, get_socket_rw_stream, AsyncPipe}; +use crate::util::http::{HyperBody, full_body, empty_body}; use crate::constants::VSCODE_CLI_QUALITY; use crate::download_cache::DownloadCache; use crate::log; @@ -318,7 +322,7 @@ impl AgentHostManager { } let quality = VSCODE_CLI_QUALITY - .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .ok_or(CodeError::UpdatesNotConfigured("no configured quality")) .and_then(|q| { Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) })?; @@ -389,7 +393,7 @@ impl AgentHostManager { let now = Instant::now(); let quality = VSCODE_CLI_QUALITY - .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .ok_or(CodeError::UpdatesNotConfigured("no configured quality")) .and_then(|q| { Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) })?; @@ -472,20 +476,20 @@ impl AgentHostManager { /// Proxies an incoming HTTP/WebSocket request to the agent host's Unix socket. pub async fn handle_request( manager: Arc, - req: Request, -) -> Result, Infallible> { + req: Request, +) -> Result, Infallible> { let socket_path = match manager.ensure_server().await { Ok(p) => p, Err(e) => { error!(manager.log, "Error starting agent host: {:?}", e); return Ok(Response::builder() .status(503) - .body(Body::from(format!("Error starting agent host: {e:?}"))) + .body(full_body(format!("Error starting agent host: {e:?}"))) .unwrap()); } }; - let is_upgrade = req.headers().contains_key(hyper::header::UPGRADE); + let is_upgrade = req.headers().contains_key(::http::header::UPGRADE); let rw = match get_socket_rw_stream(&socket_path).await { Ok(rw) => rw, @@ -496,7 +500,7 @@ pub async fn handle_request( ); return Ok(Response::builder() .status(503) - .body(Body::from(format!("Error connecting to agent host: {e:?}"))) + .body(full_body(format!("Error connecting to agent host: {e:?}"))) .unwrap()); } }; @@ -509,25 +513,25 @@ pub async fn handle_request( } /// Proxies a standard HTTP request through the socket. -async fn forward_http_to_server(rw: AsyncPipe, req: Request) -> Response { +async fn forward_http_to_server(rw: AsyncPipe, req: Request) -> Response { let (mut request_sender, connection) = - match hyper::client::conn::Builder::new().handshake(rw).await { + match hyper::client::conn::http1::handshake(TokioIo::new(rw)).await { Ok(r) => r, Err(e) => return connection_err(e), }; tokio::spawn(connection); - request_sender - .send_request(req) - .await - .unwrap_or_else(connection_err) + match request_sender.send_request(req).await { + Ok(res) => res.map(|b| b.boxed()), + Err(e) => connection_err(e), + } } /// Proxies a WebSocket upgrade request through the socket. -async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Response { +async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Response { let (mut request_sender, connection) = - match hyper::client::conn::Builder::new().handshake(rw).await { + match hyper::client::conn::http1::handshake(TokioIo::new(rw)).await { Ok(r) => r, Err(e) => return connection_err(e), }; @@ -539,23 +543,28 @@ async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Response proxied_req = proxied_req.header(k, v); } - let mut res = request_sender - .send_request(proxied_req.body(Body::empty()).unwrap()) + let mut res = match request_sender + .send_request(proxied_req.body(http_body_util::Empty::::new()).unwrap()) .await - .unwrap_or_else(connection_err); + { + Ok(r) => r, + Err(e) => return connection_err(e), + }; - let mut proxied_res = Response::new(Body::empty()); + let mut proxied_res = Response::new(empty_body()); *proxied_res.status_mut() = res.status(); for (k, v) in res.headers() { proxied_res.headers_mut().insert(k, v.clone()); } - if res.status() == hyper::StatusCode::SWITCHING_PROTOCOLS { + if res.status() == ::http::StatusCode::SWITCHING_PROTOCOLS { tokio::spawn(async move { let (s_req, s_res) = tokio::join!(hyper::upgrade::on(&mut req), hyper::upgrade::on(&mut res)); - if let (Ok(mut s_req), Ok(mut s_res)) = (s_req, s_res) { + if let (Ok(s_req), Ok(s_res)) = (s_req, s_res) { + let mut s_req = TokioIo::new(s_req); + let mut s_res = TokioIo::new(s_res); let _ = tokio::io::copy_bidirectional(&mut s_req, &mut s_res).await; } }); @@ -564,10 +573,10 @@ async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Response proxied_res } -fn connection_err(err: hyper::Error) -> Response { +fn connection_err(err: hyper::Error) -> Response { Response::builder() .status(503) - .body(Body::from(format!( + .body(full_body(format!( "Error connecting to agent host: {err:?}" ))) .unwrap() diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index ffabbad19c433..6f4e6f6dbce21 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -8,6 +8,7 @@ use crate::constants::{ APPLICATION_NAME, EDITOR_WEB_URL, QUALITYLESS_PRODUCT_NAME, QUALITYLESS_SERVER_NAME, }; use crate::download_cache::DownloadCache; +use crate::log; use crate::options::{Quality, TelemetryLevel}; use crate::state::LauncherPaths; use crate::tunnels::paths::{get_server_folder_name, SERVER_FOLDER_NAME}; @@ -23,9 +24,6 @@ use crate::util::http::{self, BoxedHttp}; use crate::util::io::SilentCopyProgress; use crate::util::machine::process_exists; use crate::util::prereqs::skip_requirements_check; -use crate::log; -use lazy_static::lazy_static; -use opentelemetry::KeyValue; use regex::Regex; use serde::Deserialize; use std::fs; @@ -33,6 +31,7 @@ use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::sync::LazyLock; use std::time::Duration; use tokio::fs::remove_file; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -40,11 +39,10 @@ use tokio::process::{Child, Command}; use tokio::sync::oneshot::Receiver; use tokio::time::{interval, timeout}; -lazy_static! { - static ref LISTENING_PORT_RE: Regex = - Regex::new(r"Extension host agent listening on (.+)").unwrap(); - static ref WEB_UI_RE: Regex = Regex::new(r"Web UI available at (.+)").unwrap(); -} +static LISTENING_PORT_RE: LazyLock = + LazyLock::new(|| Regex::new(r"Extension host agent listening on (.+)").unwrap()); +static WEB_UI_RE: LazyLock = + LazyLock::new(|| Regex::new(r"Web UI available at (.+)").unwrap()); #[derive(Clone, Debug, Default)] pub struct CodeServerArgs { @@ -551,14 +549,7 @@ impl<'a> ServerBuilder<'a> { } pub async fn listen_on_socket(&self, socket: &Path) -> Result { - Ok(spanf!( - self.logger, - self.logger.span("server.start").with_attributes(vec! { - KeyValue::new("commit_id", self.server_params.release.commit.to_string()), - KeyValue::new("quality", format!("{}", self.server_params.release.quality)), - }), - self._listen_on_socket(socket) - )?) + self._listen_on_socket(socket).await } async fn _listen_on_socket(&self, socket: &Path) -> Result { @@ -612,10 +603,8 @@ impl<'a> ServerBuilder<'a> { let cmd = cmd.creation_flags( winapi::um::winbase::CREATE_NO_WINDOW | winapi::um::winbase::CREATE_NEW_PROCESS_GROUP - | get_should_use_breakaway_from_job() - .await - .then_some(winapi::um::winbase::CREATE_BREAKAWAY_FROM_JOB) - .unwrap_or_default(), + | if get_should_use_breakaway_from_job() + .await { winapi::um::winbase::CREATE_BREAKAWAY_FROM_JOB } else { Default::default() }, ); let child = cmd diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index 614c05efd9000..a9372b5471f5f 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -28,8 +28,6 @@ use crate::util::sync::{new_barrier, Barrier, BarrierOpener}; use futures::stream::FuturesUnordered; use futures::FutureExt; -use opentelemetry::trace::SpanKind; -use opentelemetry::KeyValue; use std::collections::HashMap; use std::path::PathBuf; use std::process::Stdio; @@ -40,7 +38,6 @@ use tokio_util::codec::Decoder; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::sync::Arc; -use std::time::Instant; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, DuplexStream}; use tokio::sync::{mpsc, Mutex}; @@ -282,8 +279,8 @@ pub async fn serve( let mgr = mgr.clone(); async move { handle_agent_host_request(mgr, req).await } }); - if let Err(e) = hyper::server::conn::Http::new() - .serve_connection(rw, svc) + if let Err(e) = hyper::server::conn::http1::Builder::new() + .serve_connection(hyper_util::rt::TokioIo::new(rw), svc) .with_upgrades() .await { @@ -311,33 +308,17 @@ pub async fn serve( let own_forwarding = forwarding.handle(); tokio::spawn(async move { - use opentelemetry::trace::{FutureExt, TraceContextExt}; - - let span = own_log.span("server.socket").with_kind(SpanKind::Consumer).start(own_log.tracer()); - let cx = opentelemetry::Context::current_with_span(span); - let serve_at = Instant::now(); - debug!(own_log, "Serving new connection"); let (writehalf, readhalf) = socket.into_split(); - let stats = process_socket(readhalf, writehalf, own_tx, Some(own_forwarding), ServeStreamParams { + let _stats = process_socket(readhalf, writehalf, own_tx, Some(own_forwarding), ServeStreamParams { log: own_log, launcher_paths: own_paths, code_server_args: own_code_server_args, platform, exit_barrier: own_exit, requires_auth: AuthRequired::None, - }).with_context(cx.clone()).await; - - cx.span().add_event( - "socket.bandwidth", - vec![ - KeyValue::new("tx", stats.tx as f64), - KeyValue::new("rx", stats.rx as f64), - KeyValue::new("duration_ms", serve_at.elapsed().as_millis() as f64), - ], - ); - cx.span().end(); + }).await; }); } } @@ -375,8 +356,8 @@ pub async fn serve_stream( } pub struct SocketStats { - rx: usize, - tx: usize, + pub rx: usize, + pub tx: usize, } #[allow(clippy::too_many_arguments)] @@ -1190,7 +1171,9 @@ async fn handle_call_server_http( code_server: Option, params: CallServerHttpParams, ) -> Result { - use hyper::{body, client::conn::Builder, Body, Request}; + use ::http::Request; + use http_body_util::{BodyExt, Full}; + use hyper_util::rt::TokioIo; // We use Hyper directly here since reqwest doesn't support sockets/pipes. // See https://github.com/seanmonstar/reqwest/issues/39 @@ -1202,8 +1185,7 @@ async fn handle_call_server_http( let rw = get_socket_rw_stream(socket).await?; - let (mut request_sender, connection) = Builder::new() - .handshake(rw) + let (mut request_sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(rw)) .await .map_err(|e| wrap(e, "error establishing connection"))?; @@ -1219,7 +1201,7 @@ async fn handle_call_server_http( request_builder = request_builder.header(k, v); } let request = request_builder - .body(Body::from(params.body.unwrap_or_default())) + .body(Full::new(bytes::Bytes::from(params.body.unwrap_or_default()))) .map_err(|e| wrap(e, "invalid request"))?; let response = request_sender @@ -1227,17 +1209,19 @@ async fn handle_call_server_http( .await .map_err(|e| wrap(e, "error sending request"))?; + let (parts, body) = response.into_parts(); + let body_bytes = body.collect() + .await + .map_err(|e| wrap(e, "error reading response body"))? + .to_bytes(); + Ok(CallServerHttpResult { - status: response.status().as_u16(), - headers: response - .headers() - .into_iter() + status: parts.status.as_u16(), + headers: parts.headers + .iter() .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) .collect(), - body: body::to_bytes(response) - .await - .map_err(|e| wrap(e, "error reading response body"))? - .to_vec(), + body: body_bytes.to_vec(), }) } diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index bc043cd62af3b..2facd45c18312 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -5,22 +5,20 @@ use super::protocol::{self, PortPrivacy, PortProtocol}; use crate::auth; use crate::constants::{IS_INTERACTIVE_CLI, PROTOCOL_VERSION_TAG, TUNNEL_SERVICE_USER_AGENT}; +use crate::log; use crate::state::{LauncherPaths, PersistedState}; use crate::util::errors::{ wrap, AnyError, CodeError, DevTunnelError, InvalidTunnelName, TunnelCreationFailed, WrappedError, }; use crate::util::input::prompt_placeholder; -use crate::log; -use async_trait::async_trait; use futures::future::BoxFuture; use futures::{FutureExt, TryFutureExt}; -use lazy_static::lazy_static; use rand::prelude::IteratorRandom; use regex::Regex; -use reqwest::StatusCode; use serde::{Deserialize, Serialize}; -use std::sync::{Arc, Mutex}; +use std::future::Future; +use std::sync::{Arc, LazyLock, Mutex}; use std::time::Duration; use tokio::sync::{mpsc, watch}; use tunnels::connections::{ForwardedPortConnection, RelayTunnelHost}; @@ -91,10 +89,10 @@ impl PersistedTunnel { } } -#[async_trait] +#[allow(clippy::manual_async_fn)] trait AccessTokenProvider: Send + Sync { /// Gets the current access token. - async fn refresh_token(&self) -> Result; + fn refresh_token(&self) -> impl Future> + Send; /// Maintains the stored credential by refreshing it against the service /// to ensure its stays current. Returns a future that should be polled and @@ -111,10 +109,10 @@ impl StaticAccessTokenProvider { } } -#[async_trait] +#[allow(clippy::manual_async_fn)] impl AccessTokenProvider for StaticAccessTokenProvider { - async fn refresh_token(&self) -> Result { - Ok(self.0.clone()) + fn refresh_token(&self) -> impl Future> + Send { + async move { Ok(self.0.clone()) } } fn keep_alive(&self) -> BoxFuture<'static, Result<(), AnyError>> { @@ -149,30 +147,31 @@ impl LookupAccessTokenProvider { } } -#[async_trait] +#[allow(clippy::manual_async_fn)] impl AccessTokenProvider for LookupAccessTokenProvider { - async fn refresh_token(&self) -> Result { - if let Some(token) = self.initial_token.lock().unwrap().take() { - return Ok(token); - } + fn refresh_token(&self) -> impl Future> + Send { + async move { + if let Some(token) = self.initial_token.lock().unwrap().take() { + return Ok(token); + } - let tunnel_lookup = spanf!( - self.log, - self.log.span("dev-tunnel.tag.get"), - self.client.get_tunnel( - &self.locator, - &TunnelRequestOptions { - token_scopes: vec!["host".to_string()], - ..Default::default() - } - ) - ); + let tunnel_lookup = self + .client + .get_tunnel( + &self.locator, + &TunnelRequestOptions { + token_scopes: vec!["host".to_string()], + ..Default::default() + }, + ) + .await; - trace!(self.log, "Successfully refreshed access token"); + trace!(self.log, "Successfully refreshed access token"); - match tunnel_lookup { - Ok(tunnel) => Ok(get_host_token_from_tunnel(&tunnel)), - Err(e) => Err(wrap(e, "failed to lookup tunnel for host token")), + match tunnel_lookup { + Ok(tunnel) => Ok(get_host_token_from_tunnel(&tunnel)), + Err(e) => Err(wrap(e, "failed to lookup tunnel for host token")), + } } } @@ -297,13 +296,12 @@ fn is_valid_name(name: &str) -> Result<(), InvalidTunnelName> { Ok(()) } -lazy_static! { - static ref HOST_TUNNEL_REQUEST_OPTIONS: TunnelRequestOptions = TunnelRequestOptions { +static HOST_TUNNEL_REQUEST_OPTIONS: LazyLock = + LazyLock::new(|| TunnelRequestOptions { include_ports: true, token_scopes: vec!["host".to_string()], ..Default::default() - }; -} + }); /// Structure optionally passed into `start_existing_tunnel` to forward an existing tunnel. #[derive(Clone, Debug)] @@ -366,13 +364,10 @@ impl DevTunnels { } }; - spanf!( - self.log, - self.log.span("dev-tunnel.delete"), - self.client - .delete_tunnel(&tunnel.into_locator(), NO_REQUEST_OPTIONS) - ) - .map_err(|e| wrap(e, "failed to execute `tunnel delete`"))?; + self.client + .delete_tunnel(&tunnel.into_locator(), NO_REQUEST_OPTIONS) + .await + .map_err(|e| wrap(e, "failed to execute `tunnel delete`"))?; self.launcher_tunnel.save(None)?; Ok(()) @@ -420,12 +415,11 @@ impl DevTunnels { full_tunnel.labels = desired_tags; - let updated_tunnel = spanf!( - self.log, - self.log.span("dev-tunnel.tag.update"), - self.client.update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS) - ) - .map_err(|e| wrap(e, "failed to rename tunnel"))?; + let updated_tunnel = self + .client + .update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS) + .await + .map_err(|e| wrap(e, "failed to rename tunnel"))?; persisted.name = name; self.launcher_tunnel.save(Some(persisted.clone()))?; @@ -442,17 +436,12 @@ impl DevTunnels { create_with_new_name: Option<&str>, options: &TunnelRequestOptions, ) -> Result<(Tunnel, PersistedTunnel, /* is_new */ bool), AnyError> { - let tunnel_lookup = spanf!( - self.log, - self.log.span("dev-tunnel.tag.get"), - self.client.get_tunnel(&persisted.locator(), options) - ); + let tunnel_lookup = self.client.get_tunnel(&persisted.locator(), options).await; match tunnel_lookup { Ok(ft) => Ok((ft, persisted, false)), Err(HttpError::ResponseError(e)) - if e.status_code == StatusCode::NOT_FOUND - || e.status_code == StatusCode::FORBIDDEN => + if e.status_code.as_u16() == 404 || e.status_code.as_u16() == 403 => { let (persisted, tunnel) = self .create_tunnel(create_with_new_name.unwrap_or(&persisted.name), options) @@ -520,12 +509,9 @@ impl DevTunnels { port_to_delete.port_number, NO_REQUEST_OPTIONS, ); - spanf!( - self.log, - self.log.span("dev-tunnel.port.delete"), - output_fut - ) - .map_err(|e| wrap(e, "failed to delete port"))?; + output_fut + .await + .map_err(|e| wrap(e, "failed to delete port"))?; } // cleanup any old trailing tunnel endpoints @@ -536,7 +522,7 @@ impl DevTunnels { NO_REQUEST_OPTIONS, ); - spanf!(self.log, self.log.span("dev-tunnel.endpoint.prune"), fut) + fut.await .map_err(|e| wrap(e, "failed to prune tunnel endpoint"))?; } @@ -570,30 +556,25 @@ impl DevTunnels { let loc = TunnelLocator::try_from(&e).unwrap(); info!(self.log, "Adopting existing tunnel (ID={:?})", loc); - spanf!( - self.log, - self.log.span("dev-tunnel.tag.get"), - self.client.get_tunnel(&loc, &HOST_TUNNEL_REQUEST_OPTIONS) - ) - .map_err(|e| wrap(e, "failed to lookup tunnel"))? + self.client + .get_tunnel(&loc, &HOST_TUNNEL_REQUEST_OPTIONS) + .await + .map_err(|e| wrap(e, "failed to lookup tunnel"))? } None => loop { - let result = spanf!( - self.log, - self.log.span("dev-tunnel.create"), - self.client.create_tunnel( + let result = self + .client + .create_tunnel( Tunnel { labels: self.get_labels(name), ..Default::default() }, - options + options, ) - ); + .await; match result { - Err(HttpError::ResponseError(e)) - if e.status_code == StatusCode::TOO_MANY_REQUESTS => - { + Err(HttpError::ResponseError(e)) if e.status_code.as_u16() == 429 => { if let Some(d) = e.get_details() { let detail = d.detail.unwrap_or_else(|| "unknown".to_string()); if detail.contains(TUNNEL_COUNT_LIMIT_NAME) @@ -672,11 +653,7 @@ impl DevTunnels { ..Default::default() }; - let result = spanf!( - self.log, - self.log.span("dev-tunnel.protocol-tag-update"), - client.update_tunnel(&tunnel_update, options) - ); + let result = client.update_tunnel(&tunnel_update, options).await; result.map_err(|e| wrap(e, "tunnel tag update failed").into()) } @@ -699,13 +676,10 @@ impl DevTunnels { match recyclable { Some(tunnel) => { trace!(self.log, "Recycling tunnel ID {:?}", tunnel.tunnel_id); - spanf!( - self.log, - self.log.span("dev-tunnel.delete"), - self.client - .delete_tunnel(&tunnel.try_into().unwrap(), NO_REQUEST_OPTIONS) - ) - .map_err(|e| wrap(e, "failed to execute `tunnel delete`"))?; + self.client + .delete_tunnel(&tunnel.try_into().unwrap(), NO_REQUEST_OPTIONS) + .await + .map_err(|e| wrap(e, "failed to execute `tunnel delete`"))?; Ok(true) } None => { @@ -719,24 +693,22 @@ impl DevTunnels { &mut self, tags: &[&'static str], ) -> Result, AnyError> { - let tunnels = spanf!( - self.log, - self.log.span("dev-tunnel.listall"), - self.client.list_all_tunnels(&TunnelRequestOptions { + let tunnels = self + .client + .list_all_tunnels(&TunnelRequestOptions { labels: tags.iter().map(|t| t.to_string()).collect(), ..Default::default() }) - ) - .map_err(|e| wrap(e, "error listing current tunnels"))?; + .await + .map_err(|e| wrap(e, "error listing current tunnels"))?; Ok(tunnels) } async fn get_existing_tunnel_with_name(&self, name: &str) -> Result, AnyError> { - let existing: Vec = spanf!( - self.log, - self.log.span("dev-tunnel.rename.search"), - self.client.list_all_tunnels(&TunnelRequestOptions { + let existing: Vec = self + .client + .list_all_tunnels(&TunnelRequestOptions { labels: vec![self.tag.to_string(), name.to_string()], require_all_labels: true, limit: 1, @@ -744,8 +716,8 @@ impl DevTunnels { token_scopes: vec!["host".to_string()], ..Default::default() }) - ) - .map_err(|e| wrap(e, "failed to list existing tunnels"))?; + .await + .map_err(|e| wrap(e, "failed to list existing tunnels"))?; Ok(existing.into_iter().next()) } @@ -871,11 +843,7 @@ impl DevTunnels { ) -> Result { let mut manager = ActiveTunnelManager::new(self.log.clone(), client, locator, access_token); - let endpoint_result = spanf!( - self.log, - self.log.span("dev-tunnel.serve.callback"), - manager.get_endpoint() - ); + let endpoint_result = manager.get_endpoint().await; let endpoint = match endpoint_result { Ok(endpoint) => endpoint, @@ -903,13 +871,13 @@ impl StatusLock { fn succeed(&self) { let mut status = self.0.lock().unwrap(); status.tunnel = protocol::singleton::TunnelState::Connected; - status.last_connected_at = Some(chrono::Utc::now()); + status.last_connected_at = Some(jiff::Timestamp::now()); } fn fail(&self, reason: String) { let mut status = self.0.lock().unwrap(); if let protocol::singleton::TunnelState::Connected = status.tunnel { - status.last_disconnected_at = Some(chrono::Utc::now()); + status.last_disconnected_at = Some(jiff::Timestamp::now()); status.tunnel = protocol::singleton::TunnelState::Disconnected; } status.last_fail_reason = Some(reason); diff --git a/cli/src/tunnels/legal.rs b/cli/src/tunnels/legal.rs index 35316af4fde9a..225cf08f4069e 100644 --- a/cli/src/tunnels/legal.rs +++ b/cli/src/tunnels/legal.rs @@ -6,13 +6,11 @@ use crate::constants::IS_INTERACTIVE_CLI; use crate::state::{LauncherPaths, PersistedState}; use crate::util::errors::{AnyError, CodeError}; use crate::util::input::prompt_yn; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; -lazy_static! { - static ref LICENSE_TEXT: Option> = - option_env!("VSCODE_CLI_SERVER_LICENSE").and_then(|s| serde_json::from_str(s).unwrap()); -} +static LICENSE_TEXT: LazyLock>> = + LazyLock::new(|| option_env!("VSCODE_CLI_SERVER_LICENSE").and_then(|s| serde_json::from_str(s).unwrap())); const LICENSE_PROMPT: Option<&'static str> = option_env!("VSCODE_CLI_REMOTE_LICENSE_PROMPT"); diff --git a/cli/src/tunnels/protocol.rs b/cli/src/tunnels/protocol.rs index 0c6329f30439f..482845965d15f 100644 --- a/cli/src/tunnels/protocol.rs +++ b/cli/src/tunnels/protocol.rs @@ -353,7 +353,7 @@ pub mod forward_singleton { pub mod singleton { use crate::log; - use chrono::{DateTime, Utc}; + use jiff::Timestamp; use serde::{Deserialize, Serialize}; pub const METHOD_RESTART: &str = "restart"; @@ -385,17 +385,17 @@ pub mod singleton { #[derive(Serialize, Deserialize, Clone)] pub struct Status { - pub started_at: DateTime, + pub started_at: Timestamp, pub tunnel: TunnelState, - pub last_connected_at: Option>, - pub last_disconnected_at: Option>, + pub last_connected_at: Option, + pub last_disconnected_at: Option, pub last_fail_reason: Option, } impl Default for Status { fn default() -> Self { Self { - started_at: Utc::now(), + started_at: Timestamp::now(), tunnel: TunnelState::Disconnected, last_connected_at: None, last_disconnected_at: None, diff --git a/cli/src/tunnels/service.rs b/cli/src/tunnels/service.rs index 66bdf7a8e63a6..e8b752253abff 100644 --- a/cli/src/tunnels/service.rs +++ b/cli/src/tunnels/service.rs @@ -5,8 +5,6 @@ use std::path::{Path, PathBuf}; -use async_trait::async_trait; - use crate::log; use crate::state::LauncherPaths; use crate::util::errors::{wrap, AnyError}; @@ -14,7 +12,6 @@ use crate::util::io::{tailf, TailEvent}; pub const SERVICE_LOG_FILE_NAME: &str = "tunnel-service.log"; -#[async_trait] pub trait ServiceContainer: Send { async fn run_service( &mut self, @@ -23,7 +20,6 @@ pub trait ServiceContainer: Send { ) -> Result<(), AnyError>; } -#[async_trait] pub trait ServiceManager { /// Registers the current executable as a service to run with the given set /// of arguments. diff --git a/cli/src/tunnels/service_linux.rs b/cli/src/tunnels/service_linux.rs index 0a3e2df6ea2e4..4de6ba24d83b7 100644 --- a/cli/src/tunnels/service_linux.rs +++ b/cli/src/tunnels/service_linux.rs @@ -10,7 +10,6 @@ use std::{ process::Command, }; -use async_trait::async_trait; use zbus::{dbus_proxy, zvariant, Connection}; use crate::{ @@ -66,7 +65,6 @@ impl SystemdService { } } -#[async_trait] impl ServiceManager for SystemdService { async fn register( &self, diff --git a/cli/src/tunnels/service_macos.rs b/cli/src/tunnels/service_macos.rs index 2a51681de1db3..938c71e9ead01 100644 --- a/cli/src/tunnels/service_macos.rs +++ b/cli/src/tunnels/service_macos.rs @@ -9,8 +9,6 @@ use std::{ path::{Path, PathBuf}, }; -use async_trait::async_trait; - use crate::{ constants::APPLICATION_NAME, log, @@ -37,7 +35,6 @@ impl LaunchdService { } } -#[async_trait] impl ServiceManager for LaunchdService { async fn register( &self, diff --git a/cli/src/tunnels/service_windows.rs b/cli/src/tunnels/service_windows.rs index 395a707f3513b..e88943569a565 100644 --- a/cli/src/tunnels/service_windows.rs +++ b/cli/src/tunnels/service_windows.rs @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use async_trait::async_trait; use shell_escape::windows::escape as shell_escape; use std::os::windows::process::CommandExt; use std::{path::PathBuf, process::Stdio}; @@ -46,7 +45,6 @@ impl WindowsService { } } -#[async_trait] impl CliServiceManager for WindowsService { async fn register(&self, exe: std::path::PathBuf, args: &[&str]) -> Result<(), AnyError> { let key = WindowsService::open_key()?; diff --git a/cli/src/update_service.rs b/cli/src/update_service.rs index de977b736b20a..1070dac00c4bb 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -64,7 +64,7 @@ fn get_update_endpoint() -> Result { } VSCODE_CLI_UPDATE_ENDPOINT .map(|s| s.to_string()) - .ok_or_else(|| CodeError::UpdatesNotConfigured("no service url")) + .ok_or(CodeError::UpdatesNotConfigured("no service url")) } impl UpdateService { @@ -91,11 +91,7 @@ impl UpdateService { quality_download_segment(quality), ); - let mut response = spanf!( - self.log, - self.log.span("server.version.resolve"), - self.client.make_request("GET", download_url) - )?; + let mut response = self.client.make_request("GET", download_url).await?; if !response.status_code.is_success() { return Err(response.into_err().await.into()); @@ -131,11 +127,7 @@ impl UpdateService { quality_download_segment(quality), ); - let mut response = spanf!( - self.log, - self.log.span("server.version.resolve"), - self.client.make_request("GET", download_url) - )?; + let mut response = self.client.make_request("GET", download_url).await?; if !response.status_code.is_success() { return Err(response.into_err().await.into()); diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index b7ed029bb98d4..b8dd84a3f9fbf 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -493,7 +493,7 @@ pub enum CodeError { #[error("could not parse `host`: {0}")] InvalidHostAddress(std::net::AddrParseError), #[error("could not start server on the given host/port: {0}")] - CouldNotListenOnInterface(hyper::Error), + CouldNotListenOnInterface(std::io::Error), #[error( "Run this command again with --accept-server-license-terms to indicate your agreement." )] diff --git a/cli/src/util/http.rs b/cli/src/util/http.rs index 9658ec1fcbd62..223d1689116fb 100644 --- a/cli/src/util/http.rs +++ b/cli/src/util/http.rs @@ -7,16 +7,16 @@ use crate::{ log, util::errors::{self, WrappedError}, }; -use async_trait::async_trait; +use bytes::Bytes; use core::panic; use futures::stream::TryStreamExt; -use hyper::{ - header::{HeaderName, CONTENT_LENGTH}, - http::HeaderValue, +use http::{ + header::{HeaderName, HeaderValue, CONTENT_LENGTH}, HeaderMap, StatusCode, }; +use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use serde::de::DeserializeOwned; -use std::{io, pin::Pin, str::FromStr, sync::Arc, task::Poll}; +use std::{future::Future, io, pin::Pin, str::FromStr, sync::Arc, task::Poll}; use tokio::{ fs, io::{AsyncRead, AsyncReadExt}, @@ -29,6 +29,19 @@ use super::{ io::{copy_async_progress, ReadBuffer, ReportCopyProgress}, }; +/// Boxed body type used across the HTTP layer. +pub type HyperBody = BoxBody; + +/// Creates a body from some data (string, bytes, etc.) +pub fn full_body(data: impl Into) -> HyperBody { + Full::new(data.into()).map_err(|never| match never {}).boxed() +} + +/// Creates an empty body. +pub fn empty_body() -> HyperBody { + Empty::::new().map_err(|never| match never {}).boxed() +} + pub async fn download_into_file( filename: &std::path::Path, progress: T, @@ -119,13 +132,12 @@ impl SimpleResponse { /// the request library on the server (i.e. `reqwest`) but it can also be used /// to make update/download requests on the client rather than the server, /// similar to SSH's `remote.SSH.localServerDownload` setting. -#[async_trait] pub trait SimpleHttp { - async fn make_request( + fn make_request( &self, method: &'static str, url: String, - ) -> Result; + ) -> Pin> + Send + '_>>; } pub type BoxedHttp = Arc; @@ -157,29 +169,30 @@ impl Default for ReqwestSimpleHttp { } } -#[async_trait] impl SimpleHttp for ReqwestSimpleHttp { - async fn make_request( + fn make_request( &self, method: &'static str, url: String, - ) -> Result { - let res = self - .client - .request(reqwest::Method::try_from(method).unwrap(), &url) - .send() - .await?; - - Ok(SimpleResponse { - status_code: res.status(), - headers: res.headers().clone(), - url: Some(res.url().clone()), - read: Box::pin( - res.bytes_stream() - .map_err(futures::io::Error::other) - .into_async_read() - .compat(), - ), + ) -> Pin> + Send + '_>> { + Box::pin(async move { + let res = self + .client + .request(reqwest::Method::try_from(method).unwrap(), &url) + .send() + .await?; + + Ok(SimpleResponse { + status_code: res.status(), + headers: res.headers().clone(), + url: Some(res.url().clone()), + read: Box::pin( + res.bytes_stream() + .map_err(futures::io::Error::other) + .into_async_read() + .compat(), + ), + }) }) } } @@ -243,13 +256,13 @@ impl DelegatedSimpleHttp { } } -#[async_trait] impl SimpleHttp for DelegatedSimpleHttp { - async fn make_request( + fn make_request( &self, method: &'static str, url: String, - ) -> Result { + ) -> Pin> + Send + '_>> { + Box::pin(async move { trace!(self.log, "making delegated request to {}", url); let (tx, mut rx) = mpsc::unbounded_channel(); let sent = self @@ -298,6 +311,7 @@ impl SimpleHttp for DelegatedSimpleHttp { Some(_) => panic!("expected initresponse as first message from delegated http"), None => Ok(SimpleResponse::generic_error(&url)), // sender shut down } + }) } } @@ -357,20 +371,21 @@ impl FallbackSimpleHttp { } } -#[async_trait] impl SimpleHttp for FallbackSimpleHttp { - async fn make_request( + fn make_request( &self, method: &'static str, url: String, - ) -> Result { - let r1 = self.native.make_request(method, url.clone()).await; - if let Ok(res) = r1 { - if !res.status_code.is_server_error() { - return Ok(res); + ) -> Pin> + Send + '_>> { + Box::pin(async move { + let r1 = self.native.make_request(method, url.clone()).await; + if let Ok(res) = r1 { + if !res.status_code.is_server_error() { + return Ok(res); + } } - } - self.delegated.make_request(method, url).await + self.delegated.make_request(method, url).await + }) } } diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index 44c859772e383..7c14d2d45b083 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -6,31 +6,29 @@ use std::cmp::Ordering; use crate::constants::QUALITYLESS_SERVER_NAME; use crate::update_service::Platform; -use lazy_static::lazy_static; use regex::bytes::Regex as BinRegex; use regex::Regex; +use std::sync::LazyLock; use tokio::fs; use super::errors::CodeError; -lazy_static! { - static ref LDCONFIG_STDC_RE: Regex = Regex::new(r"libstdc\+\+.* => (.+)").unwrap(); - static ref LDD_VERSION_RE: BinRegex = BinRegex::new(r"^ldd.*\s(\d+)\.(\d+)(?:\.(\d+))?\s").unwrap(); - static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap(); - static ref LIBSTD_CXX_VERSION_RE: BinRegex = - BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); - static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 28, 0); -} +static LDCONFIG_STDC_RE: LazyLock = LazyLock::new(|| Regex::new(r"libstdc\+\+.* => (.+)").unwrap()); +static LDD_VERSION_RE: LazyLock = LazyLock::new(|| BinRegex::new(r"^ldd.*\s(\d+)\.(\d+)(?:\.(\d+))?\s").unwrap()); +static GENERIC_VERSION_RE: LazyLock = LazyLock::new(|| Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap()); +#[cfg(target_os = "linux")] +static LIBSTD_CXX_VERSION_RE: LazyLock = + LazyLock::new(|| BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap()); +#[cfg(target_os = "linux")] +static MIN_LDD_VERSION: LazyLock = LazyLock::new(|| SimpleSemver::new(2, 28, 0)); +#[cfg(target_os = "linux")] #[cfg(target_arch = "arm")] -lazy_static! { - static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 26); -} +static MIN_CXX_VERSION: LazyLock = LazyLock::new(|| SimpleSemver::new(3, 4, 26)); +#[cfg(target_os = "linux")] #[cfg(not(target_arch = "arm"))] -lazy_static! { - static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); -} +static MIN_CXX_VERSION: LazyLock = LazyLock::new(|| SimpleSemver::new(3, 4, 25)); const NIXOS_TEST_PATH: &str = "/etc/NIXOS"; diff --git a/cli/src/util/sync.rs b/cli/src/util/sync.rs index 67c777b75ed21..3eba4e6b1a279 100644 --- a/cli/src/util/sync.rs +++ b/cli/src/util/sync.rs @@ -2,8 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use async_trait::async_trait; -use std::{marker::PhantomData, sync::Arc}; +use std::{future::Future, marker::PhantomData, pin::Pin, sync::Arc}; use tokio::sync::{ broadcast, mpsc, watch::{self, error::RecvError}, @@ -35,10 +34,9 @@ where } } -#[async_trait] impl Receivable for Barrier { - async fn recv_msg(&mut self) -> Option { - self.wait().await.ok() + fn recv_msg(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { self.wait().await.ok() }) } } @@ -70,37 +68,35 @@ where } /// Type that can receive messages in an async way. -#[async_trait] pub trait Receivable { - async fn recv_msg(&mut self) -> Option; + fn recv_msg(&mut self) -> Pin> + Send + '_>>; } // todo: ideally we would use an Arc in the broadcast::Receiver to avoid having // to clone bytes everywhere, requires updating rpc consumers as well. -#[async_trait] impl Receivable for broadcast::Receiver { - async fn recv_msg(&mut self) -> Option { - loop { - match self.recv().await { - Ok(v) => return Some(v), - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => return None, + fn recv_msg(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { + loop { + match self.recv().await { + Ok(v) => return Some(v), + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => return None, + } } - } + }) } } -#[async_trait] impl Receivable for mpsc::UnboundedReceiver { - async fn recv_msg(&mut self) -> Option { - self.recv().await + fn recv_msg(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { self.recv().await }) } } -#[async_trait] impl Receivable for () { - async fn recv_msg(&mut self) -> Option { - futures::future::pending().await + fn recv_msg(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { futures::future::pending().await }) } } @@ -120,21 +116,22 @@ impl, B: Receivable> ConcatReceivable { } } -#[async_trait] impl, B: Send + Receivable> Receivable for ConcatReceivable { - async fn recv_msg(&mut self) -> Option { - if let Some(left) = &mut self.left { - match left.recv_msg().await { - Some(v) => return Some(v), - None => { - self.left = None; + fn recv_msg(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { + if let Some(left) = &mut self.left { + match left.recv_msg().await { + Some(v) => return Some(v), + None => { + self.left = None; + } } } - } - return self.right.recv_msg().await; + return self.right.recv_msg().await; + }) } } @@ -154,30 +151,31 @@ impl, B: Receivable> MergedReceivable { } } -#[async_trait] impl, B: Send + Receivable> Receivable for MergedReceivable { - async fn recv_msg(&mut self) -> Option { - loop { - match (&mut self.left, &mut self.right) { - (Some(left), Some(right)) => { - tokio::select! { - left = left.recv_msg() => match left { - Some(v) => return Some(v), - None => { self.left = None; continue; }, - }, - right = right.recv_msg() => match right { - Some(v) => return Some(v), - None => { self.right = None; continue; }, - }, + fn recv_msg(&mut self) -> Pin> + Send + '_>> { + Box::pin(async move { + loop { + match (&mut self.left, &mut self.right) { + (Some(left), Some(right)) => { + tokio::select! { + left = left.recv_msg() => match left { + Some(v) => return Some(v), + None => { self.left = None; continue; }, + }, + right = right.recv_msg() => match right { + Some(v) => return Some(v), + None => { self.right = None; continue; }, + }, + } } + (Some(a), None) => break a.recv_msg().await, + (None, Some(b)) => break b.recv_msg().await, + (None, None) => break None, } - (Some(a), None) => break a.recv_msg().await, - (None, Some(b)) => break b.recv_msg().await, - (None, None) => break None, } - } + }) } } From 2c36ddb55ea8dbce6299ee1046046365e30fd1ad Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sun, 26 Apr 2026 19:53:49 -0700 Subject: [PATCH 2/3] cli: implement client AHP control functionality --- cli/Cargo.lock | 237 ++++++++- cli/Cargo.toml | 12 +- cli/src/async_pipe.rs | 22 +- cli/src/auth.rs | 105 +++- cli/src/bin/code/main.rs | 24 +- cli/src/commands.rs | 6 + cli/src/commands/agent.rs | 282 +++++++++++ cli/src/commands/agent_host.rs | 212 ++++++-- cli/src/commands/agent_kill.rs | 56 +++ cli/src/commands/agent_logs.rs | 181 +++++++ cli/src/commands/agent_ps.rs | 124 +++++ cli/src/commands/agent_stop.rs | 63 +++ cli/src/commands/args.rs | 101 +++- cli/src/commands/output.rs | 455 ++++++++++++++---- cli/src/commands/serve_web.rs | 23 +- cli/src/commands/tunnels.rs | 2 +- cli/src/constants.rs | 16 +- cli/src/desktop/version_manager.rs | 2 +- cli/src/self_update.rs | 4 +- cli/src/state.rs | 8 + cli/src/tunnels/agent_host.rs | 46 +- cli/src/tunnels/code_server.rs | 31 +- cli/src/tunnels/control_server.rs | 10 +- cli/src/tunnels/dev_tunnels.rs | 100 +++- cli/src/tunnels/legal.rs | 5 +- cli/src/util/errors.rs | 2 + cli/src/util/http.rs | 94 ++-- cli/src/util/prereqs.rs | 18 +- .../agentHost/node/copilot/copilotAgent.ts | 2 +- 29 files changed, 1967 insertions(+), 276 deletions(-) create mode 100644 cli/src/commands/agent.rs create mode 100644 cli/src/commands/agent_kill.rs create mode 100644 cli/src/commands/agent_logs.rs create mode 100644 cli/src/commands/agent_ps.rs create mode 100644 cli/src/commands/agent_stop.rs diff --git a/cli/Cargo.lock b/cli/Cargo.lock index c8df4ebde2314..44e14d6f9191c 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -17,6 +17,46 @@ dependencies = [ "memchr", ] +[[package]] +name = "ahp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c974ae2da70e455e9e72de59ccc5279afc660ce267ab994474734d3310401f0" +dependencies = [ + "ahp-types", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "ahp-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299233ec34afadd8dd8c24fc63c50e24eb376c9588dd5b48804e33f60b1ecaf5" +dependencies = [ + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "ahp-ws" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516267592f4f77df2cecb21b4945ae6eb5a7cf2b2d07062e6423fa2e56a1caa6" +dependencies = [ + "ahp", + "futures-util", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "url", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -408,6 +448,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" name = "code-cli" version = "0.1.0" dependencies = [ + "ahp", + "ahp-types", + "ahp-ws", "base64 0.21.7", "bytes", "cfg-if", @@ -429,6 +472,7 @@ dependencies = [ "jiff", "keyring", "libc", + "local-ip-address", "log", "open", "pin-project", @@ -446,11 +490,13 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "tokio", + "tokio-tungstenite", "tokio-util", "tunnels", "url", "uuid", "winapi", + "windows-sys 0.59.0", "winreg 0.50.0", "winresource", "zbus", @@ -564,6 +610,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.115", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.115", +] + [[package]] name = "data-encoding" version = "2.10.0" @@ -590,6 +671,37 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.115", +] + [[package]] name = "dialoguer" version = "0.10.4" @@ -675,6 +787,12 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -808,6 +926,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -1020,6 +1144,37 @@ dependencies = [ "wasip3", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1129,6 +1284,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1173,7 +1329,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -1268,6 +1424,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1513,6 +1675,17 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "local-ip-address" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b0187df4e614e42405b49511b82ff7a1774fbd9a816060ee465067847cac22" +dependencies = [ + "libc", + "neli", + "windows-sys 0.61.2", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -1602,6 +1775,35 @@ dependencies = [ "tempfile", ] +[[package]] +name = "neli" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" +dependencies = [ + "bitflags 2.10.0", + "byteorder", + "derive_builder", + "getset", + "libc", + "log", + "neli-proc-macros", + "parking_lot", +] + +[[package]] +name = "neli-proc-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 2.0.115", +] + [[package]] name = "nix" version = "0.26.4" @@ -2157,6 +2359,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2746,16 +2970,6 @@ dependencies = [ "winapi", ] -[[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.3" @@ -3196,6 +3410,7 @@ dependencies = [ [[package]] name = "tunnels" version = "0.1.0" +source = "git+https://github.com/connor4312/dev-tunnels?rev=4be50b3cc5ade8cb6beec4038c53ea4f2cdac5a2#4be50b3cc5ade8cb6beec4038c53ea4f2cdac5a2" dependencies = [ "async-trait", "futures-util", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c89eb27f57c03..081e0f30814b6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,13 +33,14 @@ jiff = { version = "0.2", default-features = false, features = ["std", "serde"] http = "1" gethostname = "0.4.3" libc = "0.2.144" -# tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30", default-features = false, features = ["connections"] } -tunnels = { path = "C:\\Users\\conno\\Github\\dev-tunnels\\rs", default-features = false, features = ["connections"] } +# temporary fork pending https://github.com/microsoft/dev-tunnels/pull/626 +tunnels = { git = "https://github.com/connor4312/dev-tunnels", rev = "4be50b3cc5ade8cb6beec4038c53ea4f2cdac5a2", default-features = false, features = ["connections"] } keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl", "platform-windows", "platform-macos", "linux-keyutils"] } dialoguer = "0.10.4" hyper = { version = "1", features = ["server", "http1", "client"] } -hyper-util = { version = "0.1", features = ["tokio"] } +hyper-util = { version = "0.1", features = ["tokio", "server-auto"] } http-body-util = "0.1" +tokio-tungstenite = { version = "0.29", features = ["native-tls"] } indicatif = "0.17.4" tempfile = "3.5.0" clap_lex = "0.7.0" @@ -55,6 +56,10 @@ pin-project = "1.1.0" console = "0.15.7" bytes = "1.11.1" tar = "0.4.45" +local-ip-address = "0.6" +ahp = "0.1" +ahp-types = "0.1" +ahp-ws = "0.1" [build-dependencies] serde = { version="1.0.163", features = ["derive"] } @@ -64,6 +69,7 @@ winresource = "0.1" [target.'cfg(windows)'.dependencies] winreg = "0.50.0" winapi = "0.3.9" +windows-sys = { version = "0.59.0", features = ["Win32_System_Console", "Win32_UI_Input_KeyboardAndMouse"] } [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.3" diff --git a/cli/src/async_pipe.rs b/cli/src/async_pipe.rs index efa8a77008272..8e10681843456 100644 --- a/cli/src/async_pipe.rs +++ b/cli/src/async_pipe.rs @@ -193,28 +193,40 @@ pub type AcceptedRW = ( ); pub trait AsyncRWAccepter { - fn accept_rw(&mut self) -> Pin> + Send + '_>>; + fn accept_rw( + &mut self, + ) -> Pin> + Send + '_>>; } impl AsyncRWAccepter for AsyncPipeListener { - fn accept_rw(&mut self) -> Pin> + Send + '_>> { + fn accept_rw( + &mut self, + ) -> Pin> + Send + '_>> { Box::pin(async move { let pipe = self.accept().await?; let (read, write) = socket_stream_split(pipe); - Ok((Box::new(read) as Box, Box::new(write) as Box)) + Ok(( + Box::new(read) as Box, + Box::new(write) as Box, + )) }) } } impl AsyncRWAccepter for TcpListener { - fn accept_rw(&mut self) -> Pin> + Send + '_>> { + fn accept_rw( + &mut self, + ) -> Pin> + Send + '_>> { Box::pin(async move { let (stream, _) = self .accept() .await .map_err(CodeError::AsyncPipeListenerFailed)?; let (read, write) = tokio::io::split(stream); - Ok((Box::new(read) as Box, Box::new(write) as Box)) + Ok(( + Box::new(read) as Box, + Box::new(write) as Box, + )) }) } } diff --git a/cli/src/auth.rs b/cli/src/auth.rs index cd8492065b266..14eff97dfd79c 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -10,20 +10,15 @@ use crate::{ trace, util::{ errors::{ - wrap, AnyError, OAuthError, RefreshTokenNotAvailableError, StatusError, - WrappedError, + wrap, AnyError, OAuthError, RefreshTokenNotAvailableError, StatusError, WrappedError, }, input::prompt_options, }, warning, }; use jiff::{SignedDuration, Timestamp}; -#[cfg(target_os = "linux")] -use crate::util::errors::CodeError; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{cell::Cell, fmt::Display, future::Future, path::PathBuf, pin::Pin, sync::Arc}; -#[cfg(target_os = "linux")] -use std::thread; use tokio::time::sleep; use tunnels::{ contracts::PROD_FIRST_PARTY_APP_ID, @@ -130,6 +125,11 @@ async fn get_github_user( } impl StoredCredential { + /// Returns the raw access token string. + pub fn access_token(&self) -> &str { + &self.access_token + } + pub async fn is_expired(&self, log: &log::Logger, client: &reqwest::Client) -> bool { match self.provider { AuthProvider::Microsoft => self @@ -185,6 +185,8 @@ pub struct Auth { log: log::Logger, file_storage_path: PathBuf, storage: Arc>>, + /// Prefix for keyring entries, derived from the namespace. + keyring_prefix: String, } trait StorageImplementation: Send + Sync { @@ -235,11 +237,20 @@ struct ThreadKeyringStorage { #[cfg(target_os = "linux")] impl ThreadKeyringStorage { + fn new(prefix: String) -> Self { + Self { + s: Some(KeyringStorage::new(prefix)), + } + } + fn thread_op(&mut self, f: Fn) -> Result where Fn: 'static + Send + FnOnce(&mut KeyringStorage) -> Result, R: 'static + Send, { + use crate::util::errors::CodeError; + use std::thread; + let mut s = match self.s.take() { Some(s) => s, None => return Err(CodeError::KeyringTimeout.into()), @@ -266,15 +277,6 @@ impl ThreadKeyringStorage { } } -#[cfg(target_os = "linux")] -impl Default for ThreadKeyringStorage { - fn default() -> Self { - Self { - s: Some(KeyringStorage::default()), - } - } -} - #[cfg(target_os = "linux")] impl StorageImplementation for ThreadKeyringStorage { fn read(&mut self) -> Result, AnyError> { @@ -290,19 +292,27 @@ impl StorageImplementation for ThreadKeyringStorage { } } -#[derive(Default)] struct KeyringStorage { - // keyring storage can be split into multiple entries due to entry length limits - // on Windows https://github.com/microsoft/vscode-cli/issues/358 + prefix: String, entries: Vec, } +impl KeyringStorage { + fn new(prefix: String) -> Self { + Self { + prefix, + entries: vec![], + } + } +} + macro_rules! get_next_entry { ($self: expr, $i: expr) => { match $self.entries.get($i) { Some(e) => e, None => { - let e = keyring::Entry::new("vscode-cli", &format!("vscode-cli-{}", $i)).unwrap(); + let e = keyring::Entry::new(&$self.prefix, &format!("{}-{}", $self.prefix, $i)) + .unwrap(); $self.entries.push(e); $self.entries.last().unwrap() } @@ -388,11 +398,31 @@ impl StorageImplementation for FileStorage { impl Auth { pub fn new(paths: &LauncherPaths, log: log::Logger) -> Auth { + Self::with_namespace(paths, log, None) + } + + /// Creates an `Auth` instance with an isolated credential namespace. + /// Credentials are stored separately from the global CLI credentials, + /// so logging in here does not affect tunnel or other global auth. + pub fn with_namespace( + paths: &LauncherPaths, + log: log::Logger, + namespace: Option, + ) -> Auth { + let filename = match &namespace { + None => "token.json".to_string(), + Some(ns) => format!("token-{ns}.json"), + }; + let keyring_prefix = match &namespace { + None => "vscode-cli".to_string(), + Some(ns) => format!("vscode-cli-{ns}"), + }; Auth { log, client: reqwest::Client::new(), - file_storage_path: paths.root().join("token.json"), + file_storage_path: paths.root().join(filename), storage: Arc::new(std::sync::Mutex::new(None)), + keyring_prefix, } } @@ -406,9 +436,9 @@ impl Auth { } #[cfg(not(target_os = "linux"))] - let mut keyring_storage = KeyringStorage::default(); + let mut keyring_storage = KeyringStorage::new(self.keyring_prefix.clone()); #[cfg(target_os = "linux")] - let mut keyring_storage = ThreadKeyringStorage::default(); + let mut keyring_storage = ThreadKeyringStorage::new(self.keyring_prefix.clone()); let mut file_storage = FileStorage(PersistedState::new_with_mode( self.file_storage_path.clone(), 0o600, @@ -510,6 +540,22 @@ impl Auth { Ok(credentials) } + /// Runs the device-flow login for a specific provider with custom OAuth + /// scopes. Unlike [`login`], this is purpose-built for agent host auth + /// where the scopes are dictated by the server's protected resource + /// metadata rather than hardcoded defaults. + pub async fn login_with_scopes( + &self, + provider: AuthProvider, + scopes: Option, + ) -> Result { + let credentials = self + .do_device_code_flow_with_scopes(provider, scopes) + .await?; + self.store_credentials(credentials.clone()); + Ok(credentials) + } + /// Gets the currently stored credentials, or asks the user to log in. pub async fn get_credential(&self) -> Result { let entry = match self.get_current_credential() { @@ -681,7 +727,7 @@ impl Auth { /// Implements the device code flow, returning the credentials upon success. async fn do_device_code_flow(&self) -> Result { let provider = self.prompt_for_provider().await?; - self.do_device_code_flow_with_provider(provider).await + self.do_device_code_flow_with_scopes(provider, None).await } async fn prompt_for_provider(&self) -> Result { @@ -706,6 +752,17 @@ impl Auth { &self, provider: AuthProvider, ) -> Result { + self.do_device_code_flow_with_scopes(provider, None).await + } + + /// Runs the OAuth device code flow with optional custom scopes. + /// If `scopes` is `None`, falls back to the provider's default scopes. + pub async fn do_device_code_flow_with_scopes( + &self, + provider: AuthProvider, + scopes: Option, + ) -> Result { + let scopes = scopes.unwrap_or_else(|| provider.get_default_scopes()); loop { let init_code = self .client @@ -714,7 +771,7 @@ impl Auth { .body(format!( "client_id={}&scope={}", provider.client_id(), - provider.get_default_scopes(), + scopes, )) .send() .await?; diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index fda5fc36145ad..8a9c38f3afd9b 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -8,7 +8,10 @@ use std::process::Command; use clap::Parser; use cli::{ - commands::{agent_host, args, serve_web, tunnels, update, version, CommandContext}, + commands::{ + agent_host, agent_kill, agent_logs, agent_ps, agent_stop, args, serve_web, tunnels, update, + version, CommandContext, + }, constants::get_default_user_agent, desktop, log, state::LauncherPaths, @@ -101,9 +104,22 @@ async fn main() -> Result<(), std::convert::Infallible> { serve_web::serve_web(context!(), sw_args).await } - Some(args::Commands::AgentHost(ah_args)) => { - agent_host::agent_host(context!(), ah_args).await - } + Some(args::Commands::Agent(agent_args)) => match agent_args.subcommand { + Some(args::AgentSubcommand::Ps(ps_args)) => { + agent_ps::agent_ps(context!(), ps_args).await + } + Some(args::AgentSubcommand::Host(ah_args)) => { + agent_host::agent_host(context!(), ah_args).await + } + Some(args::AgentSubcommand::Stop(stop_args)) => { + agent_stop::agent_stop(context!(), stop_args).await + } + Some(args::AgentSubcommand::Kill) => agent_kill::agent_kill(context!()).await, + Some(args::AgentSubcommand::Logs(logs_args)) => { + agent_logs::agent_logs(context!(), logs_args).await + } + None => agent_host::agent_host(context!(), agent_args.host_args).await, + }, Some(args::Commands::Tunnel(mut tunnel_args)) => match tunnel_args.subcommand.take() { Some(args::TunnelSubcommand::Prune) => tunnels::prune(context!()).await, diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 1b706653c6e04..eeb8fc53336bb 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -5,8 +5,14 @@ mod context; +pub mod agent; pub mod agent_host; +pub mod agent_kill; +pub mod agent_logs; +pub mod agent_ps; +pub mod agent_stop; pub mod args; +pub mod output; pub mod serve_web; pub mod tunnels; pub mod update; diff --git a/cli/src/commands/agent.rs b/cli/src/commands/agent.rs new file mode 100644 index 0000000000000..39d5e4e2ad090 --- /dev/null +++ b/cli/src/commands/agent.rs @@ -0,0 +1,282 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use std::fs; + +use ahp::{Client, Transport, TransportError, TransportMessage}; +use ahp_types::commands::{AuthenticateParams, AuthenticateResult}; +use ahp_types::errors::ahp_error_codes; +use ahp_types::state::ProtectedResourceMetadata; +use ahp_types::PROTOCOL_VERSION; +use futures::{SinkExt, StreamExt}; +use tokio_tungstenite::tungstenite::Message; + +use crate::auth::{Auth, AuthProvider}; +use crate::constants::AGENT_HOST_PORT; +use crate::log; +use crate::tunnels::dev_tunnels::DevTunnels; +use crate::util::errors::{wrap, AnyError, CodeError}; +use crate::util::machine::process_exists; + +use super::agent_host::AgentHostLockData; +use super::CommandContext; + +/// Connects to an agent host, initializes the AHP session, and returns +/// the ready-to-use client. If an explicit `address` is given it is used +/// directly; if `tunnel_name` is given, the tunnel is looked up via the +/// dev tunnels API; otherwise the lockfile written by `code agent host` +/// is read to discover the local instance. +/// +/// The returned client has been initialized but **not** authenticated. +/// Use [`request_with_auth`] to issue commands that may require auth. +pub async fn connect( + ctx: &CommandContext, + address: Option<&str>, + tunnel_name: Option<&str>, +) -> Result { + let client = match (address, tunnel_name) { + (Some(addr), _) => connect_ws(addr).await?, + (None, Some(name)) => connect_via_tunnel(ctx, name).await?, + (None, None) => { + let addr = resolve_address_from_lockfile(ctx)?; + connect_ws(&addr).await? + } + }; + + client + .initialize("code-cli".into(), PROTOCOL_VERSION as i64, vec![]) + .await + .map_err(|e| wrap(e, "AHP initialize failed"))?; + + Ok(client) +} + +/// Opens a WebSocket connection and creates an AHP client. +async fn connect_ws(address: &str) -> Result { + let transport = ahp_ws::WebSocketTransport::connect(address) + .await + .map_err(|e| wrap(e, format!("Failed to connect to agent host at {address}")))?; + + Client::connect(transport, ahp::ClientConfig::default()) + .await + .map_err(|e| wrap(e, "Failed to establish AHP session").into()) +} + +/// Connects to an agent host over a dev tunnel relay. Looks up the tunnel +/// by name, opens a direct-tcpip channel to the agent host port, performs +/// a WebSocket handshake over the raw stream, then creates an AHP client. +async fn connect_via_tunnel(ctx: &CommandContext, name: &str) -> Result { + let auth = Auth::new(&ctx.paths, ctx.log.clone()); + let mut dt = DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths); + + let (port_conn, _relay_handle) = dt.connect_to_tunnel_port(name, AGENT_HOST_PORT).await?; + + let rw = port_conn.into_rw(); + let (ws_stream, _) = tokio_tungstenite::client_async("ws://localhost/", rw) + .await + .map_err(|e| wrap(e, "WebSocket handshake over tunnel failed"))?; + + let transport = TunnelWsTransport { + inner: ws_stream, + // Keep the relay handle alive so the SSH session isn't dropped. + _relay_handle, + }; + + Client::connect(transport, ahp::ClientConfig::default()) + .await + .map_err(|e| wrap(e, "Failed to establish AHP session over tunnel").into()) +} + +/// A [`Transport`] backed by a WebSocket stream running over a tunnel +/// relay channel (via `PortConnectionRW`). +struct TunnelWsTransport { + inner: tokio_tungstenite::WebSocketStream, + /// Prevent the relay handle from being dropped, which would close the + /// underlying SSH session. + _relay_handle: tunnels::connections::ClientRelayHandle, +} + +impl Transport for TunnelWsTransport { + async fn send(&mut self, msg: TransportMessage) -> Result<(), TransportError> { + let frame = match msg { + TransportMessage::Parsed(m) => { + let s = serde_json::to_string(&m) + .map_err(|e| TransportError::Protocol(e.to_string()))?; + Message::Text(s.into()) + } + TransportMessage::Text(s) => Message::Text(s.into()), + TransportMessage::Binary(b) => Message::Binary(b.into()), + }; + self.inner + .send(frame) + .await + .map_err(|e| TransportError::Io(e.to_string())) + } + + async fn recv(&mut self) -> Result, TransportError> { + loop { + match self.inner.next().await { + None => return Ok(None), + Some(Err(e)) => return Err(TransportError::Io(e.to_string())), + Some(Ok(Message::Text(s))) => { + return Ok(Some(TransportMessage::Text(s.to_string()))) + } + Some(Ok(Message::Binary(b))) => { + return Ok(Some(TransportMessage::Binary(b.to_vec()))) + } + Some(Ok(Message::Close(_))) => return Ok(None), + Some(Ok(_)) => continue, + } + } + } + + async fn close(&mut self) -> Result<(), TransportError> { + self.inner + .close(None) + .await + .map_err(|e| TransportError::Io(e.to_string())) + } +} + +/// Issues a JSON-RPC request, automatically handling `-32007` auth errors +/// by running the device-flow login and retrying once. +pub async fn request_with_auth( + ctx: &CommandContext, + client: &Client, + method: &str, + params: P, +) -> Result +where + P: serde::Serialize + Clone, + R: serde::de::DeserializeOwned, +{ + match client.request::(method, params.clone()).await { + Ok(r) => Ok(r), + Err(ref e) if is_auth_required(e) => { + debug!( + ctx.log, + "Server requires authentication, starting login flow..." + ); + authenticate_from_error(ctx, client, e).await?; + client + .request::(method, params) + .await + .map_err(|e| wrap(e, format!("Failed after authentication: {method}")).into()) + } + Err(e) => Err(wrap(e, format!("Request failed: {method}")).into()), + } +} + +fn is_auth_required(err: &ahp::ClientError) -> bool { + matches!(err, ahp::ClientError::Rpc(e) if e.code == ahp_error_codes::AUTH_REQUIRED) +} + +fn parse_protected_resources(err: &ahp::ClientError) -> Vec { + if let ahp::ClientError::Rpc(e) = err { + if let Some(data) = &e.data { + if let Ok(resources) = + serde_json::from_value::>(data.clone()) + { + return resources; + } + } + } + Vec::new() +} + +fn provider_for_resource(resource: &ProtectedResourceMetadata) -> Option { + for server in resource + .authorization_servers + .as_deref() + .unwrap_or_default() + { + if server.contains("github.com") { + return Some(AuthProvider::Github); + } + if server.contains("microsoftonline.com") || server.contains("login.microsoft.com") { + return Some(AuthProvider::Microsoft); + } + } + None +} + +async fn authenticate_from_error( + ctx: &CommandContext, + client: &Client, + err: &ahp::ClientError, +) -> Result<(), AnyError> { + let resources = parse_protected_resources(err); + if resources.is_empty() { + return Err(wrap( + "Server returned AuthRequired but did not include protected resource metadata", + "Cannot determine authentication provider", + ) + .into()); + } + + let auth = Auth::with_namespace(&ctx.paths, ctx.log.clone(), Some("agent-host".into())); + + for resource in &resources { + let provider = provider_for_resource(resource); + let scopes = resource.scopes_supported.as_ref().map(|s| s.join("+")); + + // Reuse a stored credential from the namespace if one exists; only + // start a device-flow login when there is nothing cached. + let credential = match auth.get_current_credential() { + Ok(Some(existing)) => existing, + _ => match provider { + Some(p) => auth.login_with_scopes(p, scopes).await?, + None => auth.get_credential().await?, + }, + }; + + let _: AuthenticateResult = client + .request( + "authenticate", + AuthenticateParams { + resource: resource.resource.clone(), + token: credential.access_token().to_string(), + }, + ) + .await + .map_err(|e| { + wrap( + e, + format!("AHP authenticate failed for {}", resource.resource), + ) + })?; + } + + Ok(()) +} + +fn resolve_address_from_lockfile(ctx: &CommandContext) -> Result { + let lockfile_path = ctx.paths.agent_host_lockfile(); + + let data = fs::read_to_string(&lockfile_path).map_err(|e| { + wrap( + e, + "No running agent host found. Start one with `code agent host` or specify --address", + ) + })?; + + let lock: AgentHostLockData = serde_json::from_str(&data).map_err(|e| { + wrap( + e, + format!("Corrupt agent host lockfile at {}", lockfile_path.display()), + ) + })?; + + if !process_exists(lock.pid) { + let _ = fs::remove_file(&lockfile_path); + return Err(CodeError::NoRunningAgentHost.into()); + } + + let mut url = lock.address; + if let Some(token) = &lock.connection_token { + url.push_str(&format!("?tkn={token}")); + } + Ok(url) +} diff --git a/cli/src/commands/agent_host.rs b/cli/src/commands/agent_host.rs index d3c4a11ee777a..42c0a78033994 100644 --- a/cli/src/commands/agent_host.rs +++ b/cli/src/commands/agent_host.rs @@ -3,28 +3,53 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +use std::convert::Infallible; use std::fs; use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::time::Instant; -use hyper::server::conn::http1; +use ::http::{Request, Response}; +use hyper::body::Incoming; use hyper::service::service_fn; -use hyper_util::rt::TokioIo; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::server::conn::auto::Builder as ServerBuilder; +use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; +use crate::auth::Auth; +use crate::constants::{self, AGENT_HOST_PORT}; use crate::log; use crate::tunnels::agent_host::{handle_request, AgentHostConfig, AgentHostManager}; -use crate::tunnels::legal; +use crate::tunnels::dev_tunnels::DevTunnels; use crate::tunnels::shutdown_signal::ShutdownRequest; use crate::update_service::Platform; -use crate::util::errors::AnyError; -use crate::util::errors::CodeError; -use crate::util::http::ReqwestSimpleHttp; +use crate::util::errors::{AnyError, CodeError}; +use crate::util::http::{full_body, HyperBody, ReqwestSimpleHttp}; use crate::util::prereqs::PreReqChecker; -use super::{args::AgentHostArgs, CommandContext}; +use super::args::AgentHostArgs; +use super::output; +use super::tunnels::fulfill_existing_tunnel_args; +use super::CommandContext; + +/// Bookkeeping data written to the agent host lockfile so that other CLI +/// commands (e.g. `code agent ps`) can discover a running agent host. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentHostLockData { + /// WebSocket address the agent host is listening on (e.g. `ws://127.0.0.1:4567`). + pub address: String, + /// PID of the CLI process running the agent host. + pub pid: u32, + /// Connection token, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub connection_token: Option, + /// Tunnel name, if `--tunnel` was used. + #[serde(skip_serializing_if = "Option::is_none")] + pub tunnel_name: Option, +} /// Runs a local agent host server. Downloads the latest VS Code server on /// demand, starts it with `--enable-remote-auto-shutdown`, and proxies @@ -32,7 +57,7 @@ use super::{args::AgentHostArgs, CommandContext}; /// socket. The server auto-shuts down when idle; the CLI checks for updates /// in the background and starts the latest version on the next connection. pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result { - legal::require_consent(&ctx.paths, args.accept_server_license_terms)?; + let started = Instant::now(); let platform: Platform = PreReqChecker::new().verify().await?; @@ -57,9 +82,13 @@ pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result< Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), AgentHostConfig { server_data_dir: args.server_data_dir.clone(), - without_connection_token: args.without_connection_token, - connection_token: args.connection_token.clone(), - connection_token_file: args.connection_token_file.clone(), + // The CLI proxy enforces the connection token itself, so the + // underlying server always runs without one. This lets tunnel + // connections (which bypass the proxy token check) reach the + // server without needing a token at all. + without_connection_token: true, + connection_token: None, + connection_token_file: None, }, ); @@ -89,47 +118,174 @@ pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result< Some(h) => SocketAddr::new(h.parse().map_err(CodeError::InvalidHostAddress)?, args.port), None => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port), }; - let listener = TcpListener::bind(addr).await.map_err(CodeError::CouldNotListenOnInterface)?; - let bound_addr = listener.local_addr().map_err(CodeError::CouldNotListenOnInterface)?; + let listener = TcpListener::bind(addr) + .await + .map_err(CodeError::CouldNotListenOnInterface)?; + let bound_addr = listener + .local_addr() + .map_err(CodeError::CouldNotListenOnInterface)?; let mut url = format!("ws://{bound_addr}"); if let Some(ct) = &args.connection_token { url.push_str(&format!("?tkn={ct}")); } - ctx.log - .result(format!("Agent host proxy listening on {url}")); - loop { + let product = constants::QUALITYLESS_PRODUCT_NAME; + let token_suffix = args + .connection_token + .as_deref() + .map(|t| format!("?tkn={t}")) + .unwrap_or_default(); + + // If --tunnel is set, create a dev tunnel and serve connections directly. + let mut _tunnel_handle: Option = None; + let mut tunnel_name: Option = None; + if args.tunnel { + let auth = Auth::new(&ctx.paths, ctx.log.clone()); + let mut dt = DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths); + + let mut tunnel = if let Some(existing) = + fulfill_existing_tunnel_args(args.existing_tunnel.clone(), &args.name) + { + dt.start_existing_tunnel(existing).await + } else { + dt.start_new_launcher_tunnel(args.name.as_deref(), args.random_name, &[]) + .await + }?; + + // Receive tunnel connections directly (no TCP forwarding) and serve + // them without connection-token enforcement — the tunnel relay + // provides its own authentication. + let mut tunnel_port = tunnel.add_port_direct(AGENT_HOST_PORT).await?; + let mgr_for_tunnel = manager.clone(); + let tunnel_log = ctx.log.clone(); + tokio::spawn(async move { + while let Some(socket) = tunnel_port.recv().await { + let mgr = mgr_for_tunnel.clone(); + let log = tunnel_log.clone(); + tokio::spawn(async move { + debug!(log, "Serving tunnel agent host connection"); + let rw = socket.into_rw(); + let svc = service_fn(move |req| { + let mgr = mgr.clone(); + async move { handle_request(mgr, req).await } + }); + let io = TokioIo::new(rw); + if let Err(e) = ServerBuilder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(io, svc) + .await + { + debug!(log, "Tunnel agent host connection ended: {:?}", e); + } + }); + } + }); + + tunnel_name = Some(tunnel.name.clone()); + _tunnel_handle = Some(tunnel); + } + + output::print_banner_header(&format!("{product} Agent Host"), started.elapsed()); + if let Some(name) = &tunnel_name { + let tunnel_url = match constants::EDITOR_WEB_URL { + Some(base) => format!("{base}/agents?tunnel={name}"), + None => format!("(set VSCODE_CLI_TUNNEL_EDITOR_WEB_URL)/agents?tunnel={name}"), + }; + output::print_banner_line("Tunnel", &tunnel_url); + } + output::print_network_lines(bound_addr.port(), addr.ip(), &token_suffix); + output::print_banner_footer(); + + // Write lockfile so `code agent ps` can discover this instance. + let lockfile_path = ctx.paths.agent_host_lockfile(); + let lock_data = AgentHostLockData { + address: format!("ws://{bound_addr}/"), + pid: std::process::id(), + connection_token: args.connection_token.clone(), + tunnel_name: tunnel_name.clone(), + }; + if let Err(e) = fs::write(&lockfile_path, serde_json::to_string(&lock_data).unwrap()) { + warning!(ctx.log, "Failed to write agent host lockfile: {}", e); + } + + let manager_for_svc = manager.clone(); + let expected_token = args.connection_token.clone(); + + // Accept loop: for each incoming TCP connection, serve it with hyper. + let accept_result: Result<(), AnyError> = loop { tokio::select! { - result = listener.accept() => { - let (stream, _) = match result { - Ok(r) => r, - Err(_) => continue, + _ = shutdown.wait() => break Ok(()), + accepted = listener.accept() => { + let (stream, _) = match accepted { + Ok(v) => v, + Err(e) => { + warning!(ctx.log, "Failed to accept connection: {}", e); + continue; + } }; - let mgr = manager.clone(); + let mgr = manager_for_svc.clone(); + let token = expected_token.clone(); tokio::spawn(async move { + let io = TokioIo::new(stream); let svc = service_fn(move |req| { let mgr = mgr.clone(); - async move { handle_request(mgr, req).await } + let token = token.clone(); + async move { handle_request_with_auth(mgr, req, token).await } }); - if let Err(e) = http1::Builder::new() - .serve_connection(TokioIo::new(stream), svc) - .with_upgrades() + if let Err(e) = ServerBuilder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(io, svc) .await { - let _ = e; // connection closed + // Connection-level errors are normal (client disconnect, etc.) + let _ = e; } }); } - _ = shutdown.wait() => break, } - } + }; manager.kill_running_server().await; + // Close the tunnel if one was created. + if let Some(mut tunnel) = _tunnel_handle.take() { + tunnel.close().await.ok(); + } + + // Clean up the lockfile. + let _ = fs::remove_file(&lockfile_path); + + accept_result?; + Ok(0) } +/// Wraps [`handle_request`] with connection-token enforcement. +/// +/// When `expected_token` is `Some`, the proxy requires `?tkn=` on +/// the request URI. This only applies to the local TCP listener; tunnel +/// connections are served directly via `add_port_direct` and bypass this +/// function entirely. +async fn handle_request_with_auth( + manager: Arc, + req: Request, + expected_token: Option, +) -> Result, Infallible> { + if let Some(ref token) = expected_token { + let uri_query = req.uri().query().unwrap_or(""); + let has_valid_token = url::form_urlencoded::parse(uri_query.as_bytes()) + .any(|(k, v)| k == "tkn" && v == token.as_str()); + + if !has_valid_token { + return Ok(Response::builder() + .status(403) + .body(full_body("Forbidden: missing or invalid connection token")) + .unwrap()); + } + } + + handle_request(manager, req).await +} + fn mint_connection_token(path: &Path, prefer_token: Option) -> std::io::Result { #[cfg(not(windows))] use std::os::unix::fs::OpenOptionsExt; diff --git a/cli/src/commands/agent_kill.rs b/cli/src/commands/agent_kill.rs new file mode 100644 index 0000000000000..2a916374443d3 --- /dev/null +++ b/cli/src/commands/agent_kill.rs @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use std::fs; + +use crate::log; +use crate::util::command::kill_tree; +use crate::util::errors::{wrap, AnyError}; +use crate::util::machine::process_exists; + +use super::agent_host::AgentHostLockData; +use super::CommandContext; + +/// Forcefully kills the running agent host process tree and cleans up. +pub async fn agent_kill(ctx: CommandContext) -> Result { + let lockfile_path = ctx.paths.agent_host_lockfile(); + + let data = fs::read_to_string(&lockfile_path).map_err(|e| { + wrap( + e, + "No running agent host found. Start one with `code agent host`", + ) + })?; + + let lock: AgentHostLockData = serde_json::from_str(&data).map_err(|e| { + wrap( + e, + format!("Corrupt agent host lockfile at {}", lockfile_path.display()), + ) + })?; + + if !process_exists(lock.pid) { + let _ = fs::remove_file(&lockfile_path); + ctx.log + .result("Agent host is not running (stale lockfile cleaned up).".to_string()); + return Ok(0); + } + + debug!( + ctx.log, + "Killing agent host process tree (pid {})", lock.pid + ); + + kill_tree(lock.pid) + .await + .map_err(|e| wrap(e, "Failed to kill agent host process tree"))?; + + let _ = fs::remove_file(&lockfile_path); + + ctx.log + .result(format!("Killed agent host (pid {}).", lock.pid)); + + Ok(0) +} diff --git a/cli/src/commands/agent_logs.rs b/cli/src/commands/agent_logs.rs new file mode 100644 index 0000000000000..c52a08a81b50d --- /dev/null +++ b/cli/src/commands/agent_logs.rs @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use ahp::SubscriptionEvent; +use ahp_types::actions::StateAction; +use ahp_types::commands::{SubscribeParams, SubscribeResult}; +use ahp_types::state::{SnapshotState, TurnState}; +use console::Style; + +use crate::tunnels::shutdown_signal::ShutdownRequest; +use crate::util::errors::AnyError; + +use super::agent; +use super::args::AgentLogsArgs; +use super::output::Styles; +use super::CommandContext; + +/// Subscribes to a session and streams actions/notifications in real time. +pub async fn agent_logs(ctx: CommandContext, args: AgentLogsArgs) -> Result { + let client = agent::connect(&ctx, args.address.as_deref(), args.tunnel.as_deref()).await?; + + let (result, mut sub): (SubscribeResult, _) = { + let r: SubscribeResult = agent::request_with_auth( + &ctx, + &client, + "subscribe", + SubscribeParams { + resource: args.session.clone(), + }, + ) + .await?; + let s = client.attach_subscription(&args.session).await; + (r, s) + }; + + // Print initial state summary. + print_initial_state(&args.session, &result); + + let header = Styles::muted(); + println!( + "\n{}", + header.apply_to("Streaming events (Ctrl+C to quit)...") + ); + println!("{}", header.apply_to("─".repeat(50))); + + // Stream events until Ctrl+C or the subscription closes. + let mut shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]); + + loop { + tokio::select! { + ev = sub.recv() => match ev { + Some(SubscriptionEvent::Action(envelope)) => { + print_action(envelope.server_seq, &envelope.action); + } + Some(SubscriptionEvent::Notification(notif)) => { + let notif_style = Style::new().magenta(); + println!("{}", notif_style.apply_to(format!("notification: {notif:?}"))); + } + None => { + println!("{}", Styles::muted().apply_to("Subscription closed.")); + break; + } + }, + _ = shutdown.wait() => { + println!("\n{}", Styles::muted().apply_to("Interrupted.")); + break; + } + } + } + + client.shutdown().await; + Ok(0) +} + +fn print_initial_state(uri: &str, result: &SubscribeResult) { + let title = Styles::title(); + let label = Styles::label(); + let uri_style = Styles::uri(); + + println!( + "\n{} {}", + title.apply_to("Session"), + uri_style.apply_to(uri) + ); + + if let SnapshotState::Session(ref session) = result.snapshot.state { + let s = &session.summary; + if !s.title.is_empty() { + println!(" {} {}", label.apply_to("title:"), s.title); + } + println!(" {} {}", label.apply_to("provider:"), s.provider); + if let Some(ref activity) = s.activity { + if !activity.is_empty() { + println!(" {} {}", label.apply_to("activity:"), activity); + } + } + println!(" {} {}", label.apply_to("turns:"), session.turns.len()); + + // Print a brief summary of past turns. + for turn in &session.turns { + let state_str = match turn.state { + TurnState::Complete => Styles::success().apply_to("✓"), + TurnState::Cancelled => Styles::warning().apply_to("⊘"), + TurnState::Error => Styles::error().apply_to("✗"), + }; + let msg = truncate(&turn.user_message.text, 80); + println!(" {} {}", state_str, Styles::muted().apply_to(msg)); + } + + // Print active turn if any. + if let Some(ref active) = session.active_turn { + let msg = truncate(&active.user_message.text, 80); + println!(" {} {}", Style::new().green().bold().apply_to("►"), msg); + } + } + + println!(" {} {}", label.apply_to("seq:"), result.snapshot.from_seq); +} + +fn print_action(seq: u64, action: &StateAction) { + let seq_str = Styles::muted().apply_to(format!("[{seq:>6}]")); + + // Serialize the action to extract the "type" tag and remaining fields. + let value = serde_json::to_value(action).unwrap_or_default(); + let type_name = value + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + + // Build a compact params string from all fields except "type". + let params = if let Some(obj) = value.as_object() { + let parts: Vec = obj + .iter() + .filter(|(k, _)| k.as_str() != "type") + .map(|(k, v)| { + let v_str = match v { + serde_json::Value::String(s) => truncate(s, 80), + other => truncate(&other.to_string(), 80), + }; + format!("{}={}", Styles::label().apply_to(k), v_str) + }) + .collect(); + parts.join(" ") + } else { + String::new() + }; + + let style = action_style(type_name); + println!("{} {} {}", seq_str, style.apply_to(type_name), params); +} + +/// Picks a color for the action type name. +fn action_style(type_name: &str) -> Style { + if type_name.contains("error") || type_name.contains("Failed") { + Styles::error() + } else if type_name.contains("Complete") || type_name.contains("complete") { + Styles::success() + } else if type_name.contains("Cancel") || type_name.contains("cancel") { + Styles::warning() + } else if type_name.contains("oolCall") || type_name.contains("oolcall") { + Style::new().blue() + } else if type_name.contains("delta") + || type_name.contains("Delta") + || type_name.contains("reasoning") + { + Styles::muted() + } else { + Style::new().cyan() + } +} + +fn truncate(s: &str, max: usize) -> String { + let s = s.replace('\n', " "); + if s.len() <= max { + s + } else { + format!("{}…", &s[..max - 1]) + } +} diff --git a/cli/src/commands/agent_ps.rs b/cli/src/commands/agent_ps.rs new file mode 100644 index 0000000000000..edac3ffc711dc --- /dev/null +++ b/cli/src/commands/agent_ps.rs @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use ahp_types::commands::{ListSessionsParams, ListSessionsResult}; +use ahp_types::state::{SessionStatus, SessionSummary}; + +use crate::util::errors::AnyError; + +use super::agent; +use super::args::AgentPsArgs; +use super::output::{self, Styles}; +use super::CommandContext; + +/// Lists active sessions on a running agent host. +pub async fn agent_ps(ctx: CommandContext, args: AgentPsArgs) -> Result { + let client = agent::connect(&ctx, args.address.as_deref(), args.tunnel.as_deref()).await?; + + let result: ListSessionsResult = + agent::request_with_auth(&ctx, &client, "listSessions", ListSessionsParams::default()) + .await?; + + client.shutdown().await; + + let mut items: Vec<&SessionSummary> = if args.all { + result.items.iter().collect() + } else { + result + .items + .iter() + .filter(|s| is_active(s.status)) + .collect() + }; + + // Most-recently-modified first. + items.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + + if args.json { + let json = serde_json::to_string_pretty(&items) + .map_err(|e| crate::util::errors::wrap(e, "Failed to serialize sessions"))?; + output::print_paged(&json); + } else if items.is_empty() { + ctx.log.result("No active sessions.".to_string()); + } else { + let out = format_sessions_list(&items); + output::print_paged(&out); + } + + Ok(0) +} + +/// A session is "active" if it is in-progress, needs input, or errored +/// (i.e. not just idle/archived). +fn is_active(status: u32) -> bool { + let dominated = SessionStatus::IsRead as u32 + | SessionStatus::IsArchived as u32 + | SessionStatus::Idle as u32; + status & !dominated != 0 +} + +fn format_sessions_list(sessions: &[&SessionSummary]) -> String { + let title_style = Styles::title(); + let label_style = Styles::label(); + let uri_style = Styles::uri(); + + let mut out = String::new(); + + for (i, s) in sessions.iter().enumerate() { + if i > 0 { + out.push('\n'); + } + + let status = status_styled(s.status); + let title = if s.title.is_empty() { + "(untitled)".to_string() + } else { + s.title.clone() + }; + out.push_str(&format!(" {} {}\n", title_style.apply_to(&title), status)); + + out.push_str(&format!( + " {} {}\n", + label_style.apply_to("uri:"), + uri_style.apply_to(&s.resource), + )); + + out.push_str(&format!( + " {} {}\n", + label_style.apply_to("provider:"), + s.provider, + )); + + if let Some(activity) = &s.activity { + if !activity.is_empty() { + out.push_str(&format!( + " {} {}\n", + label_style.apply_to("activity:"), + activity, + )); + } + } + + if let Some(wd) = &s.working_directory { + out.push_str(&format!(" {} {}\n", label_style.apply_to("cwd:"), wd,)); + } + } + + out +} + +fn status_styled(status: u32) -> console::StyledObject { + if status & (SessionStatus::InputNeeded as u32) == (SessionStatus::InputNeeded as u32) { + Styles::warning().apply_to("● input needed".to_string()) + } else if status & (SessionStatus::InProgress as u32) != 0 { + Styles::success().apply_to("● in progress".to_string()) + } else if status & (SessionStatus::Error as u32) != 0 { + Styles::error().apply_to("● error".to_string()) + } else if status & (SessionStatus::Idle as u32) != 0 { + Styles::muted().apply_to("○ idle".to_string()) + } else { + Styles::muted().apply_to(format!("? unknown ({status})")) + } +} diff --git a/cli/src/commands/agent_stop.rs b/cli/src/commands/agent_stop.rs new file mode 100644 index 0000000000000..6773ac7bd56dc --- /dev/null +++ b/cli/src/commands/agent_stop.rs @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use ahp_types::actions::{SessionTurnCancelledAction, StateAction}; +use ahp_types::commands::{SubscribeParams, SubscribeResult}; +use ahp_types::state::SnapshotState; + +use crate::log; +use crate::util::errors::{wrap, AnyError}; + +use super::agent; +use super::args::AgentStopArgs; +use super::CommandContext; + +/// Cancels the active turn of a session on a running agent host. +pub async fn agent_stop(ctx: CommandContext, args: AgentStopArgs) -> Result { + let client = agent::connect(&ctx, args.address.as_deref(), args.tunnel.as_deref()).await?; + + // Subscribe to the session to get its current state. + let result: SubscribeResult = agent::request_with_auth( + &ctx, + &client, + "subscribe", + SubscribeParams { + resource: args.session.clone(), + }, + ) + .await?; + + let turn_id = match result.snapshot.state { + SnapshotState::Session(session) => session.active_turn.map(|t| t.id), + _ => None, + }; + + let turn_id = match turn_id { + Some(id) => id, + None => { + ctx.log.result("No active turn to cancel.".to_string()); + client.shutdown().await; + return Ok(0); + } + }; + + debug!(ctx.log, "Cancelling turn {} on {}", turn_id, args.session); + + client + .dispatch(StateAction::SessionTurnCancelled( + SessionTurnCancelledAction { + session: args.session.clone(), + turn_id: turn_id.clone(), + }, + )) + .await + .map_err(|e| wrap(e, "Failed to dispatch turn cancellation"))?; + + ctx.log + .result(format!("Cancelled turn {turn_id} on {}", args.session)); + + client.shutdown().await; + Ok(0) +} diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 51e7347c4ea5c..01844c65a544d 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -186,9 +186,9 @@ pub enum Commands { #[clap(hide = true)] CommandShell(CommandShellArgs), - /// Runs a local agent host server. - #[clap(name = "agent-host")] - AgentHost(AgentHostArgs), + /// Manage agent host sessions. + #[clap(name = "agent")] + Agent(AgentArgs), } #[derive(Args, Debug, Clone)] @@ -251,12 +251,101 @@ pub struct AgentHostArgs { /// Run without a connection token. Only use this if the connection is secured by other means. #[clap(long)] pub without_connection_token: bool, - /// If set, the user accepts the server license terms and the server will be started without a user prompt. - #[clap(long)] - pub accept_server_license_terms: bool, /// Specifies the directory that server data is kept in. #[clap(long)] pub server_data_dir: Option, + + /// Expose the agent host over a dev tunnel. + #[clap(long)] + pub tunnel: bool, + /// Sets the machine name for the tunnel. + #[clap(long)] + pub name: Option, + /// Randomly name the machine for the tunnel. + #[clap(long)] + pub random_name: bool, + + /// Optional details to connect to an existing tunnel. + #[clap(flatten, next_help_heading = Some("ADVANCED TUNNEL OPTIONS"))] + pub existing_tunnel: ExistingTunnelArgs, +} + +#[derive(Args, Debug, Clone)] +pub struct AgentArgs { + #[clap(subcommand)] + pub subcommand: Option, + + /// Agent host arguments used when no subcommand is given. + #[clap(flatten)] + pub host_args: AgentHostArgs, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum AgentSubcommand { + /// Start a local agent host server. + Host(AgentHostArgs), + + /// List active sessions on a running agent host. + Ps(AgentPsArgs), + + /// Cancel the active turn of a session. + Stop(AgentStopArgs), + + /// Forcefully kill the running agent host process tree. + Kill, + + /// Stream live session events. + Logs(AgentLogsArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct AgentPsArgs { + /// WebSocket address of a running agent host (e.g. ws://127.0.0.1:1234?tkn=secret). + /// If omitted, the CLI discovers a locally running agent host automatically. + #[clap(long)] + pub address: Option, + + /// Connect via a named dev tunnel instead of the local address. + #[clap(long)] + pub tunnel: Option, + + /// Output results as JSON instead of a human-readable table. + #[clap(long)] + pub json: bool, + + /// Show all sessions, including idle and archived ones. + #[clap(long, short)] + pub all: bool, +} + +#[derive(Args, Debug, Clone)] +pub struct AgentStopArgs { + /// Session URI to cancel the active turn of (e.g. copilot:/). + pub session: String, + + /// WebSocket address of a running agent host. + /// If omitted, the CLI discovers a locally running agent host automatically. + #[clap(long)] + pub address: Option, + + /// Connect via a named dev tunnel instead of the local address. + #[clap(long)] + pub tunnel: Option, +} + +#[derive(Args, Debug, Clone)] +pub struct AgentLogsArgs { + /// Session URI to stream events for (e.g. copilot:/). + pub session: String, + + /// WebSocket address of a running agent host. + /// If omitted, the CLI discovers a locally running agent host automatically. + #[clap(long)] + pub address: Option, + + /// Connect via a named dev tunnel instead of the local address. + #[clap(long)] + pub tunnel: Option, } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/output.rs b/cli/src/commands/output.rs index 8747457889b18..e438e86a198c7 100644 --- a/cli/src/commands/output.rs +++ b/cli/src/commands/output.rs @@ -3,133 +3,392 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use std::fmt::Display; +//! Console output helpers: styled text, paging, and structured display. -use std::io::{BufWriter, Write}; +use std::io::Write; +use std::net::IpAddr; +use std::process::{Command, Stdio}; +use std::time::Duration; -use super::args::OutputFormat; +use console::{style, Style, Term}; -pub struct Column { - max_width: usize, - heading: &'static str, - data: Vec, +use crate::constants; + +// ---- Styles ----------------------------------------------------------------- + +/// Predefined styles for consistent CLI output. +pub struct Styles; + +impl Styles { + /// Bold text for headings / titles. + pub fn title() -> Style { + Style::new().bold() + } + + /// Dim text for field labels. + pub fn label() -> Style { + Style::new().dim() + } + + /// Cyan text for URIs and identifiers. + pub fn uri() -> Style { + Style::new().cyan() + } + + /// Green + bold for success / active indicators. + pub fn success() -> Style { + Style::new().green().bold() + } + + /// Yellow + bold for warnings / attention-needed indicators. + pub fn warning() -> Style { + Style::new().yellow().bold() + } + + /// Red + bold for error indicators. + pub fn error() -> Style { + Style::new().red().bold() + } + + /// Dim text for inactive / secondary information. + pub fn muted() -> Style { + Style::new().dim() + } } -impl Column { - pub fn new(heading: &'static str) -> Self { - Column { - max_width: heading.len(), - heading, - data: vec![], +// ---- Pager ------------------------------------------------------------------ + +/// Outputs `text` through a pager when stdout is a terminal and the +/// content is taller than the terminal window. +/// +/// Resolution order: +/// 1. `$PAGER` environment variable (any platform) +/// 2. `less -R` on Unix +/// 3. Built-in interactive pager (works everywhere, including Windows) +/// +/// When stdout is not a terminal (e.g. piped), the text is written +/// directly without paging. +pub fn print_paged(text: &str) { + let term = Term::stdout(); + if !term.is_term() { + print!("{text}"); + return; + } + + let (term_height, _) = term.size(); + let term_height = term_height as usize; + let line_count = text.lines().count(); + if line_count <= term_height.saturating_sub(1) { + print!("{text}"); + return; + } + + // Prefer $PAGER if explicitly set. + if let Ok(pager) = std::env::var("PAGER") { + if !pager.is_empty() && try_external_pager(&pager, text) { + return; } } - pub fn add_row(&mut self, row: String) { - self.max_width = std::cmp::max(self.max_width, row.len()); - self.data.push(row); + // On Unix, try `less -R` before falling back to built-in. + #[cfg(not(windows))] + if try_external_pager("less -R", text) { + return; } + + builtin_pager(&term, text, term_height); } -impl OutputFormat { - pub fn print_table(&self, table: OutputTable) -> Result<(), std::io::Error> { - match *self { - OutputFormat::Json => JsonTablePrinter().print(table, &mut std::io::stdout()), - OutputFormat::Text => TextTablePrinter().print(table, &mut std::io::stdout()), +/// Attempts to spawn an external pager. Returns `true` if successful. +fn try_external_pager(pager: &str, text: &str) -> bool { + let mut parts = pager.split_whitespace(); + let program = match parts.next() { + Some(p) => p, + None => return false, + }; + let pager_args: Vec<&str> = parts.collect(); + + match Command::new(program) + .args(&pager_args) + .stdin(Stdio::piped()) + .spawn() + { + Ok(mut child) => { + if let Some(mut stdin) = child.stdin.take() { + let _ = stdin.write_all(text.as_bytes()); + } + let _ = child.wait(); + true } + Err(_) => false, } } -pub struct OutputTable { - cols: Vec, +/// Built-in interactive pager with scroll support. +/// +/// Controls: +/// - **↓** / **j** / **Enter** — scroll down one line +/// - **↑** / **k** — scroll up one line +/// - **Page Down** / **Space** / **f** — scroll down one page +/// - **Page Up** / **b** — scroll up one page +/// - **Home** / **g** — jump to top +/// - **End** / **G** — jump to bottom +/// - **q** / **Escape** / **Ctrl+C** — quit +fn builtin_pager(term: &Term, text: &str, term_height: usize) { + let lines: Vec<&str> = text.lines().collect(); + let total = lines.len(); + let page = term_height.saturating_sub(1); // reserve one line for the status bar + let mut offset: usize = 0; + + // Switch to the alternate screen buffer so quitting restores the + // original terminal content (like `less` does). + let _ = term.write_str("\x1b[?1049h"); + + // Draw the initial screen. + draw_page(term, &lines, offset, page); + draw_status_bar(term, offset, total, page); + + loop { + let key = read_pager_key(term); + let new_offset = match key { + // Down one line + PagerKey::Down => offset.saturating_add(1), + // Up one line + PagerKey::Up => offset.saturating_sub(1), + // Page down + PagerKey::PageDown => offset.saturating_add(page), + // Page up + PagerKey::PageUp => offset.saturating_sub(page), + // Home + PagerKey::Home => 0, + // End + PagerKey::End => total.saturating_sub(page), + // Quit + PagerKey::Quit => { + break; + } + PagerKey::Other => offset, + }; + + // Clamp to valid range. + let max_offset = total.saturating_sub(page); + let new_offset = new_offset.min(max_offset); + + if new_offset != offset { + offset = new_offset; + // Redraw: clear the page area and status bar, then repaint. + let _ = term.clear_last_lines(page + 1); + draw_page(term, &lines, offset, page); + draw_status_bar(term, offset, total, page); + } + } + + // Leave the alternate screen buffer, restoring prior content. + let _ = term.write_str("\x1b[?1049l"); } -impl OutputTable { - pub fn new(cols: Vec) -> Self { - OutputTable { cols } +/// Writes `page` lines starting from `offset`. +fn draw_page(term: &Term, lines: &[&str], offset: usize, page: usize) { + let end = (offset + page).min(lines.len()); + for line in &lines[offset..end] { + let _ = term.write_line(line); + } + // Pad with empty lines if at the end. + for _ in (end - offset)..page { + let _ = term.write_line(""); } } -trait TablePrinter { - fn print(&self, table: OutputTable, out: &mut dyn std::io::Write) - -> Result<(), std::io::Error>; +/// Draws the status bar at the bottom showing scroll position. +fn draw_status_bar(term: &Term, offset: usize, total: usize, page: usize) { + let end = (offset + page).min(total); + let pct = if total == 0 { 100 } else { (end * 100) / total }; + let bar = format!( + " lines {}-{} of {} ({pct}%) ↑↓ scroll PgUp/PgDn page q quit ", + offset + 1, + end, + total, + ); + let _ = term.write_str(&format!("{}", Style::new().reverse().apply_to(bar))); } -pub struct JsonTablePrinter(); - -impl TablePrinter for JsonTablePrinter { - fn print( - &self, - table: OutputTable, - out: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let mut bw = BufWriter::new(out); - bw.write_all(b"[")?; - - if !table.cols.is_empty() { - let data_len = table.cols[0].data.len(); - for i in 0..data_len { - if i > 0 { - bw.write_all(b",{")?; - } else { - bw.write_all(b"{")?; - } - for col in &table.cols { - serde_json::to_writer(&mut bw, col.heading)?; - bw.write_all(b":")?; - serde_json::to_writer(&mut bw, &col.data[i])?; - } - } - } +// ---- Key reading ------------------------------------------------------------ - bw.write_all(b"]")?; - bw.flush() +enum PagerKey { + Up, + Down, + PageUp, + PageDown, + Home, + End, + Quit, + Other, +} + +/// Reads a single key press and maps it to a pager action. +/// +/// On Windows, uses `ReadConsoleInputW` directly because the `console` +/// crate does not map `VK_PRIOR` (Page Up) or `VK_NEXT` (Page Down). +#[cfg(not(windows))] +fn read_pager_key(term: &Term) -> PagerKey { + use console::Key; + match term.read_key() { + Ok(Key::ArrowDown) | Ok(Key::Char('j')) | Ok(Key::Enter) => PagerKey::Down, + Ok(Key::ArrowUp) | Ok(Key::Char('k')) => PagerKey::Up, + Ok(Key::PageDown) | Ok(Key::Char(' ')) | Ok(Key::Char('f')) => PagerKey::PageDown, + Ok(Key::PageUp) | Ok(Key::Char('b')) => PagerKey::PageUp, + Ok(Key::Home) | Ok(Key::Char('g')) => PagerKey::Home, + Ok(Key::End) | Ok(Key::Char('G')) => PagerKey::End, + Ok(Key::Escape) | Ok(Key::Char('q')) | Ok(Key::Char('Q')) | Ok(Key::CtrlC) => { + PagerKey::Quit + } + _ => PagerKey::Other, } } -/// Type that prints the output as an ASCII, markdown-style table. -pub struct TextTablePrinter(); - -impl TablePrinter for TextTablePrinter { - fn print( - &self, - table: OutputTable, - out: &mut dyn std::io::Write, - ) -> Result<(), std::io::Error> { - let mut bw = BufWriter::new(out); - - let sizes = table.cols.iter().map(|c| c.max_width).collect::>(); - - // print headers - write_columns(&mut bw, table.cols.iter().map(|c| c.heading), &sizes)?; - // print --- separators - write_columns( - &mut bw, - table.cols.iter().map(|c| "-".repeat(c.max_width)), - &sizes, - )?; - // print each column - if !table.cols.is_empty() { - let data_len = table.cols[0].data.len(); - for i in 0..data_len { - write_columns(&mut bw, table.cols.iter().map(|c| &c.data[i]), &sizes)?; - } +#[cfg(windows)] +fn read_pager_key(_term: &Term) -> PagerKey { + use windows_sys::Win32::System::Console::{ + GetStdHandle, ReadConsoleInputW, INPUT_RECORD, KEY_EVENT, STD_INPUT_HANDLE, + }; + use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + VK_DOWN, VK_END, VK_ESCAPE, VK_HOME, VK_NEXT, VK_PRIOR, VK_RETURN, VK_UP, + }; + + loop { + let mut record: INPUT_RECORD = unsafe { std::mem::zeroed() }; + let mut count: u32 = 0; + + let ok = unsafe { + ReadConsoleInputW(GetStdHandle(STD_INPUT_HANDLE), &mut record, 1, &mut count) + }; + if ok == 0 || count == 0 { + return PagerKey::Other; + } + + // Only handle key-down events. + if record.EventType as u32 != KEY_EVENT { + continue; } + let key_event = unsafe { record.Event.KeyEvent }; + if key_event.bKeyDown == 0 { + continue; + } + + let vk = key_event.wVirtualKeyCode; + let ch = unsafe { key_event.uChar.UnicodeChar }; - bw.flush() + // Map virtual key codes first (handles keys with no unicode char). + return match vk { + VK_UP => PagerKey::Up, + VK_DOWN => PagerKey::Down, + VK_PRIOR => PagerKey::PageUp, + VK_NEXT => PagerKey::PageDown, + VK_HOME => PagerKey::Home, + VK_END => PagerKey::End, + VK_ESCAPE => PagerKey::Quit, + VK_RETURN => PagerKey::Down, + _ => { + // Fall through to character matching. + if let Some(c) = char::from_u32(ch as u32) { + match c { + 'j' => PagerKey::Down, + 'k' => PagerKey::Up, + ' ' | 'f' => PagerKey::PageDown, + 'b' => PagerKey::PageUp, + 'g' => PagerKey::Home, + 'G' => PagerKey::End, + 'q' | 'Q' => PagerKey::Quit, + '\x03' => PagerKey::Quit, // Ctrl+C + _ => PagerKey::Other, + } + } else { + PagerKey::Other + } + } + }; } } -fn write_columns( - mut w: impl Write, - cols: impl Iterator, - sizes: &[usize], -) -> Result<(), std::io::Error> -where - T: Display, -{ - w.write_all(b"|")?; - for (i, col) in cols.enumerate() { - write!(w, " {:width$} |", col, width = sizes[i])?; - } - w.write_all(b"\r\n") +// ---- Server banner ---------------------------------------------------------- + +/// Prints a styled startup header line: +/// +/// ```text +/// Code Agent Host vX.Y.Z ready in 123ms +/// ``` +pub fn print_banner_header(title: &str, elapsed: Duration) { + let version = constants::VSCODE_CLI_VERSION.unwrap_or("dev"); + let elapsed_ms = elapsed.as_millis(); + + println!(); + println!( + " {} {} {}", + style(title).cyan().bold(), + style(format!("v{version}")).dim(), + style(format!("ready in {elapsed_ms}ms")).dim(), + ); + println!(); +} + +/// Minimum label width so values align across banner lines. +const BANNER_LABEL_WIDTH: usize = 9; + +/// Prints a single `➜ Label: value` line inside a banner. +pub fn print_banner_line(label: &str, value: &str) { + println!( + " {} {} {}", + style("➜").green().bold(), + style(format!( + "{label}:{:>pad$}", + "", + pad = BANNER_LABEL_WIDTH.saturating_sub(label.len() + 1) + )) + .bold(), + style(value).cyan(), + ); +} + +/// Prints a dimmed `➜ Label: hint` line inside a banner. +pub fn print_banner_line_dim(label: &str, hint: &str) { + println!( + " {} {} {}", + style("➜").green().bold(), + style(format!( + "{label}:{:>pad$}", + "", + pad = BANNER_LABEL_WIDTH.saturating_sub(label.len() + 1) + )) + .bold(), + style(hint).dim(), + ); +} + +/// Prints a trailing blank line to close the banner. +pub fn print_banner_footer() { + println!(); +} + +/// Prints the Local / Network lines for a WebSocket server. +/// +/// When `listen_ip` is loopback, shows `use --host to expose`. +/// When it is unspecified (`0.0.0.0` / `::`), enumerates all routable +/// IPv4 interfaces. Otherwise shows the single bound address. +pub fn print_network_lines(port: u16, listen_ip: IpAddr, token_suffix: &str) { + print_banner_line("Local", &format!("ws://localhost:{port}{token_suffix}")); + + if listen_ip.is_loopback() { + print_banner_line_dim("Network", "use --host to expose"); + } else if listen_ip.is_unspecified() { + if let Ok(ifas) = local_ip_address::list_afinet_netifas() { + for (_, addr) in &ifas { + if addr.is_loopback() || addr.is_ipv6() { + continue; + } + print_banner_line("Network", &format!("ws://{addr}:{port}{token_suffix}")); + } + } + } else { + print_banner_line("Network", &format!("ws://{listen_ip}:{port}{token_suffix}")); + } } diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 58fdbabf9d10b..74f2abdbcea8e 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -12,15 +12,15 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use crate::util::http::{empty_body, full_body, HyperBody}; use ::http::{Request, Response}; use http_body_util::BodyExt; use hyper::body::Incoming; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::TokioIo; -use tokio::net::TcpListener; -use crate::util::http::{HyperBody, full_body, empty_body}; use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::net::TcpListener; use tokio::{pin, time}; use crate::async_pipe::{ @@ -140,10 +140,14 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port), }; - let listener = TcpListener::bind(addr).await.map_err(CodeError::CouldNotListenOnInterface)?; + let listener = TcpListener::bind(addr) + .await + .map_err(CodeError::CouldNotListenOnInterface)?; // Get the actual bound address (important when port 0 is used for random port assignment) - let bound_addr = listener.local_addr().map_err(CodeError::CouldNotListenOnInterface)?; + let bound_addr = listener + .local_addr() + .map_err(CodeError::CouldNotListenOnInterface)?; let mut listening = format!("Web UI available at http://{bound_addr}"); if let Some(base) = args.server_base_path { if !base.starts_with('/') { @@ -188,7 +192,10 @@ struct HandleContext { } /// Handler function for an inbound request -async fn handle(ctx: HandleContext, req: Request) -> Result, Infallible> { +async fn handle( + ctx: HandleContext, + req: Request, +) -> Result, Infallible> { let client_key_half = get_client_key_half(&req); let path = req.uri().path(); @@ -349,7 +356,11 @@ async fn forward_ws_req_to_server( } let mut res = match request_sender - .send_request(proxied_req.body(http_body_util::Empty::::new()).unwrap()) + .send_request( + proxied_req + .body(http_body_util::Empty::::new()) + .unwrap(), + ) .await { Ok(r) => r, diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 3a9fa375f99ac..a3b2056faed85 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -75,7 +75,7 @@ impl From for crate::auth::AuthProvider { } } -fn fulfill_existing_tunnel_args( +pub(super) fn fulfill_existing_tunnel_args( d: ExistingTunnelArgs, name_arg: &Option, ) -> Option { diff --git a/cli/src/constants.rs b/cli/src/constants.rs index f6979db0157fa..f95cc77a4bfb9 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -115,17 +115,23 @@ pub static TUNNEL_SERVICE_USER_AGENT: LazyLock = /// Map of qualities to the server name pub static SERVER_NAME_MAP: LazyLock>> = - LazyLock::new(|| option_env!("VSCODE_CLI_TUNNEL_SERVER_QUALITIES").and_then(|s| serde_json::from_str(s).unwrap())); + LazyLock::new(|| { + option_env!("VSCODE_CLI_TUNNEL_SERVER_QUALITIES") + .and_then(|s| serde_json::from_str(s).unwrap()) + }); /// Whether i/o interactions are allowed in the current CLI. pub static IS_A_TTY: LazyLock = LazyLock::new(|| std::io::stdin().is_terminal()); /// Whether i/o interactions are allowed in the current CLI. -pub static COLORS_ENABLED: LazyLock = LazyLock::new(|| *IS_A_TTY && std::env::var(NO_COLOR_ENV).is_err()); +pub static COLORS_ENABLED: LazyLock = + LazyLock::new(|| *IS_A_TTY && std::env::var(NO_COLOR_ENV).is_err()); /// Whether i/o interactions are allowed in the current CLI. -pub static IS_INTERACTIVE_CLI: LazyLock = LazyLock::new(|| *IS_A_TTY && std::env::var(NONINTERACTIVE_VAR).is_err()); +pub static IS_INTERACTIVE_CLI: LazyLock = + LazyLock::new(|| *IS_A_TTY && std::env::var(NONINTERACTIVE_VAR).is_err()); /// Map of quality names to arrays of app IDs used for them, for example, `{"stable":["ABC123"]}` -pub static WIN32_APP_IDS: LazyLock>> = - LazyLock::new(|| option_env!("VSCODE_CLI_WIN32_APP_IDS").map(|s| s.split(',').map(|s| s.to_string()).collect())); +pub static WIN32_APP_IDS: LazyLock>> = LazyLock::new(|| { + option_env!("VSCODE_CLI_WIN32_APP_IDS").map(|s| s.split(',').map(|s| s.to_string()).collect()) +}); diff --git a/cli/src/desktop/version_manager.rs b/cli/src/desktop/version_manager.rs index 5eb3ea6049048..18d0a459d0e28 100644 --- a/cli/src/desktop/version_manager.rs +++ b/cli/src/desktop/version_manager.rs @@ -10,8 +10,8 @@ use std::{ }; use regex::Regex; -use std::sync::LazyLock; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use crate::{ constants::{PRODUCT_DOWNLOAD_URL, QUALITY, QUALITYLESS_PRODUCT_NAME}, diff --git a/cli/src/self_update.rs b/cli/src/self_update.rs index 21d6d6c756669..8f5377ced5706 100644 --- a/cli/src/self_update.rs +++ b/cli/src/self_update.rs @@ -29,8 +29,8 @@ static OLD_UPDATE_EXTENSION: &str = "Updating CLI"; impl<'a> SelfUpdate<'a> { pub fn new(update_service: &'a UpdateService) -> Result { - let commit = VSCODE_CLI_COMMIT - .ok_or(CodeError::UpdatesNotConfigured("unknown build commit"))?; + let commit = + VSCODE_CLI_COMMIT.ok_or(CodeError::UpdatesNotConfigured("unknown build commit"))?; let quality = VSCODE_CLI_QUALITY .ok_or(CodeError::UpdatesNotConfigured("no configured quality")) diff --git a/cli/src/state.rs b/cli/src/state.rs index 5bc655ef2c171..e6b7a4ffe592f 100644 --- a/cli/src/state.rs +++ b/cli/src/state.rs @@ -237,6 +237,14 @@ impl LauncherPaths { }) } + /// Lockfile for the running agent host + pub fn agent_host_lockfile(&self) -> PathBuf { + self.root.join(format!( + "agent-host-{}.lock", + VSCODE_CLI_QUALITY.unwrap_or("oss") + )) + } + /// Suggested path for web server storage pub fn web_server_storage(&self) -> PathBuf { self.root.join("serve-web") diff --git a/cli/src/tunnels/agent_host.rs b/cli/src/tunnels/agent_host.rs index 46786ba4f5a49..640de39632a45 100644 --- a/cli/src/tunnels/agent_host.rs +++ b/cli/src/tunnels/agent_host.rs @@ -8,15 +8,14 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant}; -use hyper::body::Incoming; use ::http::{Request, Response}; use http_body_util::BodyExt; +use hyper::body::Incoming; use hyper_util::rt::TokioIo; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::sync::Mutex; use crate::async_pipe::{get_socket_name, get_socket_rw_stream, AsyncPipe}; -use crate::util::http::{HyperBody, full_body, empty_body}; use crate::constants::VSCODE_CLI_QUALITY; use crate::download_cache::DownloadCache; use crate::log; @@ -27,6 +26,7 @@ use crate::update_service::{ use crate::util::command::new_script_command; use crate::util::errors::CodeError; use crate::util::http::{self, BoxedHttp}; +use crate::util::http::{empty_body, full_body, HyperBody}; use crate::util::io::SilentCopyProgress; use crate::util::sync::{new_barrier, Barrier, BarrierOpener}; @@ -506,7 +506,7 @@ pub async fn handle_request( }; if is_upgrade { - Ok(forward_ws_to_server(rw, req).await) + Ok(forward_ws_to_server(manager.log.clone(), rw, req).await) } else { Ok(forward_http_to_server(rw, req).await) } @@ -529,14 +529,18 @@ async fn forward_http_to_server(rw: AsyncPipe, req: Request) -> Respon } /// Proxies a WebSocket upgrade request through the socket. -async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Response { +async fn forward_ws_to_server( + log: log::Logger, + rw: AsyncPipe, + mut req: Request, +) -> Response { let (mut request_sender, connection) = match hyper::client::conn::http1::handshake(TokioIo::new(rw)).await { Ok(r) => r, Err(e) => return connection_err(e), }; - tokio::spawn(connection); + tokio::spawn(connection.with_upgrades()); let mut proxied_req = Request::builder().uri(req.uri()); for (k, v) in req.headers() { @@ -544,7 +548,11 @@ async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Resp } let mut res = match request_sender - .send_request(proxied_req.body(http_body_util::Empty::::new()).unwrap()) + .send_request( + proxied_req + .body(http_body_util::Empty::::new()) + .unwrap(), + ) .await { Ok(r) => r, @@ -562,10 +570,28 @@ async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Resp let (s_req, s_res) = tokio::join!(hyper::upgrade::on(&mut req), hyper::upgrade::on(&mut res)); - if let (Ok(s_req), Ok(s_res)) = (s_req, s_res) { - let mut s_req = TokioIo::new(s_req); - let mut s_res = TokioIo::new(s_res); - let _ = tokio::io::copy_bidirectional(&mut s_req, &mut s_res).await; + match (s_req, s_res) { + (Ok(s_req), Ok(s_res)) => { + let mut s_req = TokioIo::new(s_req); + let mut s_res = TokioIo::new(s_res); + if let Err(e) = tokio::io::copy_bidirectional(&mut s_req, &mut s_res).await { + debug!(log, "Agent host WebSocket proxy ended with error: {:?}", e); + } + } + (Err(e), _) => { + warning!( + log, + "Agent host client-side WebSocket upgrade failed: {:?}", + e + ); + } + (_, Err(e)) => { + warning!( + log, + "Agent host server-side WebSocket upgrade failed: {:?}", + e + ); + } } }); } diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 6f4e6f6dbce21..3e4bb9eb90017 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -603,8 +603,11 @@ impl<'a> ServerBuilder<'a> { let cmd = cmd.creation_flags( winapi::um::winbase::CREATE_NO_WINDOW | winapi::um::winbase::CREATE_NEW_PROCESS_GROUP - | if get_should_use_breakaway_from_job() - .await { winapi::um::winbase::CREATE_BREAKAWAY_FROM_JOB } else { Default::default() }, + | if get_should_use_breakaway_from_job().await { + winapi::um::winbase::CREATE_BREAKAWAY_FROM_JOB + } else { + Default::default() + }, ); let child = cmd @@ -792,6 +795,9 @@ fn parse_port_from(text: &str) -> Option { } pub fn print_listening(log: &log::Logger, tunnel_name: &str) { + use crate::commands::output; + use console::style; + debug!( log, "{} is listening for incoming connections", QUALITYLESS_SERVER_NAME @@ -824,8 +830,25 @@ pub fn print_listening(log: &log::Logger, tunnel_name: &str) { } } - let message = &format!("\nOpen this link in your browser {addr}\n"); - log.result(message); + let arrow = style("➜").green().bold(); + let product = QUALITYLESS_PRODUCT_NAME; + let version = crate::constants::VSCODE_CLI_VERSION.unwrap_or("dev"); + + println!(); + println!( + " {} {}", + style(format!("{product} Tunnel")).cyan().bold(), + style(format!("v{version}")).dim(), + ); + println!(); + output::print_banner_line("Tunnel", tunnel_name); + println!( + " {} {} {}", + arrow, + style("Open:").bold(), + style(&addr).cyan(), + ); + output::print_banner_footer(); } pub async fn download_cli_into_cache( diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index a9372b5471f5f..4e19344e12ac7 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -1201,7 +1201,9 @@ async fn handle_call_server_http( request_builder = request_builder.header(k, v); } let request = request_builder - .body(Full::new(bytes::Bytes::from(params.body.unwrap_or_default()))) + .body(Full::new(bytes::Bytes::from( + params.body.unwrap_or_default(), + ))) .map_err(|e| wrap(e, "invalid request"))?; let response = request_sender @@ -1210,14 +1212,16 @@ async fn handle_call_server_http( .map_err(|e| wrap(e, "error sending request"))?; let (parts, body) = response.into_parts(); - let body_bytes = body.collect() + let body_bytes = body + .collect() .await .map_err(|e| wrap(e, "error reading response body"))? .to_bytes(); Ok(CallServerHttpResult { status: parts.status.as_u16(), - headers: parts.headers + headers: parts + .headers .iter() .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) .collect(), diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index 2facd45c18312..1454158b22a2b 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -14,6 +14,7 @@ use crate::util::errors::{ use crate::util::input::prompt_placeholder; use futures::future::BoxFuture; use futures::{FutureExt, TryFutureExt}; +use http::StatusCode; use rand::prelude::IteratorRandom; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -21,9 +22,11 @@ use std::future::Future; use std::sync::{Arc, LazyLock, Mutex}; use std::time::Duration; use tokio::sync::{mpsc, watch}; -use tunnels::connections::{ForwardedPortConnection, RelayTunnelHost}; +use tunnels::connections::{ + ClientRelayHandle, ForwardedPortConnection, PortConnection, RelayTunnelClient, RelayTunnelHost, +}; use tunnels::contracts::{ - Tunnel, TunnelAccessControl, TunnelPort, TunnelRelayTunnelEndpoint, PORT_TOKEN, + Tunnel, TunnelAccessControl, TunnelEndpoint, TunnelPort, PORT_TOKEN, TUNNEL_ACCESS_SCOPES_CONNECT, TUNNEL_PROTOCOL_AUTO, }; use tunnels::management::{ @@ -240,8 +243,7 @@ impl ActiveTunnel { return details .as_ref() .map(|r| { - r.base - .port_uri_format + r.port_uri_format .clone() .expect("expected to have port format") }) @@ -380,6 +382,83 @@ impl DevTunnels { .map(|_| ()) } + /// Connects to a tunnel by name as a client, returning a raw connection + /// to the tunnel's agent host port. The caller is responsible for doing + /// the WebSocket upgrade over the returned stream. + /// + /// The returned [`ClientRelayHandle`] must be kept alive for the duration + /// of the connection; dropping it closes the underlying SSH session. + pub async fn connect_to_tunnel_port( + &mut self, + name: &str, + port: u16, + ) -> Result<(PortConnection, ClientRelayHandle), AnyError> { + let tunnel = self.get_tunnel_with_connect_scope(name).await?; + + let endpoint = tunnel.endpoints.first().ok_or_else(|| { + DevTunnelError(format!( + "Tunnel '{name}' has no active endpoint (is the host running?)", + )) + })?; + + let connect_token = tunnel + .access_tokens + .as_ref() + .and_then(|t| t.get("connect")) + .ok_or_else(|| { + DevTunnelError(format!( + "No connect-scoped access token for tunnel '{name}'", + )) + })?; + + let client = RelayTunnelClient::new(self.client.clone()); + let handle = client + .connect(&endpoint, connect_token) + .await + .map_err(|e| wrap(e, "failed to connect to tunnel relay"))?; + + let port_conn = handle + .connect_to_port(port) + .await + .map_err(|e| wrap(e, format!("failed to connect to port {port} on tunnel")))?; + + Ok((port_conn, handle)) + } + + /// Looks up a tunnel by name with connect-scoped access token. + async fn get_tunnel_with_connect_scope(&self, name: &str) -> Result { + let existing: Vec = self + .client + .list_all_tunnels(&TunnelRequestOptions { + labels: vec![self.tag.to_string(), name.to_string()], + require_all_labels: true, + limit: 1, + ..Default::default() + }) + .await + .map_err(|e| wrap(e, "failed to list tunnels"))?; + + let tunnel = match existing.into_iter().next() { + Some(t) => t, + None => { + return Err(DevTunnelError(format!("No tunnel found with name '{name}'")).into()) + } + }; + + let loc = TunnelLocator::try_from(&tunnel).unwrap(); + self.client + .get_tunnel( + &loc, + &TunnelRequestOptions { + include_ports: true, + token_scopes: vec!["connect".to_string()], + ..Default::default() + }, + ) + .await + .map_err(|e| wrap(e, "failed to lookup tunnel").into()) + } + /// Updates the name of the existing persisted tunnel to the new name. /// Gracefully creates a new tunnel if the previous one was deleted. async fn update_tunnel_name( @@ -441,7 +520,8 @@ impl DevTunnels { match tunnel_lookup { Ok(ft) => Ok((ft, persisted, false)), Err(HttpError::ResponseError(e)) - if e.status_code.as_u16() == 404 || e.status_code.as_u16() == 403 => + if e.status_code.as_u16() == StatusCode::NOT_FOUND.as_u16() + || e.status_code.as_u16() == StatusCode::FORBIDDEN.as_u16() => { let (persisted, tunnel) = self .create_tunnel(create_with_new_name.unwrap_or(&persisted.name), options) @@ -574,7 +654,9 @@ impl DevTunnels { .await; match result { - Err(HttpError::ResponseError(e)) if e.status_code.as_u16() == 429 => { + Err(HttpError::ResponseError(e)) + if e.status_code.as_u16() == StatusCode::TOO_MANY_REQUESTS.as_u16() => + { if let Some(d) = e.get_details() { let detail = d.detail.unwrap_or_else(|| "unknown".to_string()); if detail.contains(TUNNEL_COUNT_LIMIT_NAME) @@ -891,7 +973,7 @@ impl StatusLock { struct ActiveTunnelManager { close_tx: Option>, - endpoint_rx: watch::Receiver>>, + endpoint_rx: watch::Receiver>>, relay: Arc>, status: StatusLock, } @@ -988,7 +1070,7 @@ impl ActiveTunnelManager { /// Gets the most recent details from the tunnel process. Returns None if /// the process exited before providing details. - pub async fn get_endpoint(&mut self) -> Result { + pub async fn get_endpoint(&mut self) -> Result { loop { if let Some(details) = &*self.endpoint_rx.borrow() { return details.clone().map_err(AnyError::from); @@ -1023,7 +1105,7 @@ impl ActiveTunnelManager { log: log::Logger, relay: Arc>, mut close_rx: mpsc::Receiver<()>, - endpoint_tx: watch::Sender>>, + endpoint_tx: watch::Sender>>, access_token_provider: impl AccessTokenProvider + 'static, status: StatusLock, ) { diff --git a/cli/src/tunnels/legal.rs b/cli/src/tunnels/legal.rs index 225cf08f4069e..90aec54337794 100644 --- a/cli/src/tunnels/legal.rs +++ b/cli/src/tunnels/legal.rs @@ -9,8 +9,9 @@ use crate::util::input::prompt_yn; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; -static LICENSE_TEXT: LazyLock>> = - LazyLock::new(|| option_env!("VSCODE_CLI_SERVER_LICENSE").and_then(|s| serde_json::from_str(s).unwrap())); +static LICENSE_TEXT: LazyLock>> = LazyLock::new(|| { + option_env!("VSCODE_CLI_SERVER_LICENSE").and_then(|s| serde_json::from_str(s).unwrap()) +}); const LICENSE_PROMPT: Option<&'static str> = option_env!("VSCODE_CLI_REMOTE_LICENSE_PROMPT"); diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index b8dd84a3f9fbf..72e82307bdb73 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -451,6 +451,8 @@ pub enum CodeError { SingletonLockedProcessExited(u32), #[error("no tunnel process is currently running")] NoRunningTunnel, + #[error("no agent host process is currently running")] + NoRunningAgentHost, #[error("rpc call failed: {0:?}")] TunnelRpcCallFailed(ResponseError), #[cfg(windows)] diff --git a/cli/src/util/http.rs b/cli/src/util/http.rs index 223d1689116fb..89190a1bb88bd 100644 --- a/cli/src/util/http.rs +++ b/cli/src/util/http.rs @@ -34,12 +34,16 @@ pub type HyperBody = BoxBody; /// Creates a body from some data (string, bytes, etc.) pub fn full_body(data: impl Into) -> HyperBody { - Full::new(data.into()).map_err(|never| match never {}).boxed() + Full::new(data.into()) + .map_err(|never| match never {}) + .boxed() } /// Creates an empty body. pub fn empty_body() -> HyperBody { - Empty::::new().map_err(|never| match never {}).boxed() + Empty::::new() + .map_err(|never| match never {}) + .boxed() } pub async fn download_into_file( @@ -263,54 +267,54 @@ impl SimpleHttp for DelegatedSimpleHttp { url: String, ) -> Pin> + Send + '_>> { Box::pin(async move { - trace!(self.log, "making delegated request to {}", url); - let (tx, mut rx) = mpsc::unbounded_channel(); - let sent = self - .start_request - .send(DelegatedHttpRequest { - method, - url: url.clone(), - ch: tx, - }) - .await; + trace!(self.log, "making delegated request to {}", url); + let (tx, mut rx) = mpsc::unbounded_channel(); + let sent = self + .start_request + .send(DelegatedHttpRequest { + method, + url: url.clone(), + ch: tx, + }) + .await; - if sent.is_err() { - return Ok(SimpleResponse::generic_error(&url)); // sender shut down - } + if sent.is_err() { + return Ok(SimpleResponse::generic_error(&url)); // sender shut down + } - match rx.recv().await { - Some(DelegatedHttpEvent::InitResponse { - status_code, - headers, - }) => { - trace!( - self.log, - "delegated request to {} resulted in status = {}", - url, - status_code - ); - let mut headers_map = HeaderMap::with_capacity(headers.len()); - for (k, v) in &headers { - if let (Ok(key), Ok(value)) = ( - HeaderName::from_str(&k.to_lowercase()), - HeaderValue::from_str(v), - ) { - headers_map.insert(key, value); + match rx.recv().await { + Some(DelegatedHttpEvent::InitResponse { + status_code, + headers, + }) => { + trace!( + self.log, + "delegated request to {} resulted in status = {}", + url, + status_code + ); + let mut headers_map = HeaderMap::with_capacity(headers.len()); + for (k, v) in &headers { + if let (Ok(key), Ok(value)) = ( + HeaderName::from_str(&k.to_lowercase()), + HeaderValue::from_str(v), + ) { + headers_map.insert(key, value); + } } - } - Ok(SimpleResponse { - url: url::Url::parse(&url).ok(), - status_code: StatusCode::from_u16(status_code) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - headers: headers_map, - read: Box::pin(DelegatedReader::new(rx)), - }) + Ok(SimpleResponse { + url: url::Url::parse(&url).ok(), + status_code: StatusCode::from_u16(status_code) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + headers: headers_map, + read: Box::pin(DelegatedReader::new(rx)), + }) + } + Some(DelegatedHttpEvent::End) => Ok(SimpleResponse::generic_error(&url)), + Some(_) => panic!("expected initresponse as first message from delegated http"), + None => Ok(SimpleResponse::generic_error(&url)), // sender shut down } - Some(DelegatedHttpEvent::End) => Ok(SimpleResponse::generic_error(&url)), - Some(_) => panic!("expected initresponse as first message from delegated http"), - None => Ok(SimpleResponse::generic_error(&url)), // sender shut down - } }) } } diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index 7c14d2d45b083..83dd024b181ad 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -13,9 +13,12 @@ use tokio::fs; use super::errors::CodeError; -static LDCONFIG_STDC_RE: LazyLock = LazyLock::new(|| Regex::new(r"libstdc\+\+.* => (.+)").unwrap()); -static LDD_VERSION_RE: LazyLock = LazyLock::new(|| BinRegex::new(r"^ldd.*\s(\d+)\.(\d+)(?:\.(\d+))?\s").unwrap()); -static GENERIC_VERSION_RE: LazyLock = LazyLock::new(|| Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap()); +static LDCONFIG_STDC_RE: LazyLock = + LazyLock::new(|| Regex::new(r"libstdc\+\+.* => (.+)").unwrap()); +static LDD_VERSION_RE: LazyLock = + LazyLock::new(|| BinRegex::new(r"^ldd.*\s(\d+)\.(\d+)(?:\.(\d+))?\s").unwrap()); +static GENERIC_VERSION_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap()); #[cfg(target_os = "linux")] static LIBSTD_CXX_VERSION_RE: LazyLock = LazyLock::new(|| BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap()); @@ -185,10 +188,10 @@ async fn check_is_nixos() -> bool { /// minimum requirements. #[cfg(not(windows))] pub async fn skip_requirements_check() -> bool { - std::env::var("VSCODE_SERVER_CUSTOM_GLIBC_LINKER").is_ok() || - fs::metadata("/tmp/vscode-skip-server-requirements-check") - .await - .is_ok() + std::env::var("VSCODE_SERVER_CUSTOM_GLIBC_LINKER").is_ok() + || fs::metadata("/tmp/vscode-skip-server-requirements-check") + .await + .is_ok() } #[cfg(windows)] @@ -400,5 +403,4 @@ mod tests { Some(SimpleSemver::new(2, 40, 0)), ); } - } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index de102fdedde7f..0161f80f2eb59 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -265,7 +265,7 @@ export class CopilotAgent extends Disposable implements IAgent { private async _ensureClient(): Promise { const tokenAtStartup = this._githubToken; if (!tokenAtStartup) { - throw new ProtocolError(AHP_AUTH_REQUIRED, 'Authentication is required to use Copilot'); + throw new ProtocolError(AHP_AUTH_REQUIRED, 'Authentication is required to use Copilot', this.getProtectedResources()); } if (this._client) { return this._client; From ed85ff7a6c676cdd29560d722757c8d96da330f3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 28 Apr 2026 08:56:30 -0700 Subject: [PATCH 3/3] comments and more deps --- cli/Cargo.lock | 34 +++++++--------------- cli/Cargo.toml | 10 +++---- cli/src/commands/agent_host.rs | 53 +++++++++++++++++++++------------- cli/src/util/io.rs | 9 ++++-- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 236d8aec0ee9d..2981d038f2c37 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -277,12 +277,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -416,7 +410,7 @@ checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", - "clap_lex 1.0.0", + "clap_lex", "strsim", ] @@ -432,12 +426,6 @@ dependencies = [ "syn 2.0.115", ] -[[package]] -name = "clap_lex" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" - [[package]] name = "clap_lex" version = "1.0.0" @@ -451,11 +439,11 @@ dependencies = [ "ahp", "ahp-types", "ahp-ws", - "base64 0.21.7", + "base64", "bytes", "cfg-if", "clap", - "clap_lex 0.7.7", + "clap_lex", "console", "const_format", "core-foundation", @@ -488,7 +476,7 @@ dependencies = [ "sysinfo", "tar", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tokio-tungstenite", "tokio-util", @@ -496,8 +484,8 @@ dependencies = [ "url", "uuid", "winapi", - "windows-sys 0.59.0", - "winreg 0.50.0", + "windows-sys 0.61.2", + "winreg 0.56.0", "winresource", "zbus", "zip", @@ -1318,7 +1306,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", @@ -2586,7 +2574,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-core", @@ -3930,12 +3918,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.50.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 081e0f30814b6..c06404b7afd19 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -43,14 +43,14 @@ http-body-util = "0.1" tokio-tungstenite = { version = "0.29", features = ["native-tls"] } indicatif = "0.17.4" tempfile = "3.5.0" -clap_lex = "0.7.0" +clap_lex = "1" url = "2.5.4" log = "0.4.18" const_format = "0.2.31" sha2 = "0.10.6" -base64 = "0.21.2" +base64 = "0.22" shell-escape = "0.1.5" -thiserror = "1.0.40" +thiserror = "2" cfg-if = "1.0.0" pin-project = "1.1.0" console = "0.15.7" @@ -67,9 +67,9 @@ serde_json = "1.0.96" winresource = "0.1" [target.'cfg(windows)'.dependencies] -winreg = "0.50.0" +winreg = "0.56" winapi = "0.3.9" -windows-sys = { version = "0.59.0", features = ["Win32_System_Console", "Win32_UI_Input_KeyboardAndMouse"] } +windows-sys = { version = "0.61", features = ["Win32_System_Console", "Win32_UI_Input_KeyboardAndMouse"] } [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.3" diff --git a/cli/src/commands/agent_host.rs b/cli/src/commands/agent_host.rs index 42c0a78033994..58826d98c039c 100644 --- a/cli/src/commands/agent_host.rs +++ b/cli/src/commands/agent_host.rs @@ -39,7 +39,7 @@ use super::CommandContext; /// commands (e.g. `code agent ps`) can discover a running agent host. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentHostLockData { - /// WebSocket address the agent host is listening on (e.g. `ws://127.0.0.1:4567`). + /// WebSocket address the agent host is listening on (e.g. `ws://127.0.0.1:4567/`). pub address: String, /// PID of the CLI process running the agent host. pub pid: u32, @@ -125,10 +125,7 @@ pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result< .local_addr() .map_err(CodeError::CouldNotListenOnInterface)?; - let mut url = format!("ws://{bound_addr}"); - if let Some(ct) = &args.connection_token { - url.push_str(&format!("?tkn={ct}")); - } + let local_agent_host_url = format!("ws://{bound_addr}/"); let product = constants::QUALITYLESS_PRODUCT_NAME; let token_suffix = args @@ -199,12 +196,12 @@ pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result< // Write lockfile so `code agent ps` can discover this instance. let lockfile_path = ctx.paths.agent_host_lockfile(); let lock_data = AgentHostLockData { - address: format!("ws://{bound_addr}/"), + address: local_agent_host_url, pid: std::process::id(), connection_token: args.connection_token.clone(), tunnel_name: tunnel_name.clone(), }; - if let Err(e) = fs::write(&lockfile_path, serde_json::to_string(&lock_data).unwrap()) { + if let Err(e) = write_agent_host_lockfile(&lockfile_path, &lock_data) { warning!(ctx.log, "Failed to write agent host lockfile: {}", e); } @@ -290,29 +287,45 @@ fn mint_connection_token(path: &Path, prefer_token: Option) -> std::io:: #[cfg(not(windows))] use std::os::unix::fs::OpenOptionsExt; - let mut f = fs::OpenOptions::new(); - f.create(true); - f.write(true); - f.read(true); + let mut file_options = fs::OpenOptions::new(); + file_options.create(true); + file_options.write(true); + file_options.read(true); #[cfg(not(windows))] - f.mode(0o600); - let mut f = f.open(path)?; + file_options.mode(0o600); + let mut file = file_options.open(path)?; if prefer_token.is_none() { - let mut t = String::new(); - f.read_to_string(&mut t)?; - let t = t.trim(); - if !t.is_empty() { - return Ok(t.to_string()); + let mut token = String::new(); + file.read_to_string(&mut token)?; + let token = token.trim(); + if !token.is_empty() { + return Ok(token.to_string()); } } - f.set_len(0)?; + file.set_len(0)?; let prefer_token = prefer_token.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - f.write_all(prefer_token.as_bytes())?; + file.write_all(prefer_token.as_bytes())?; Ok(prefer_token) } +fn write_agent_host_lockfile(path: &Path, lock_data: &AgentHostLockData) -> std::io::Result<()> { + #[cfg(not(windows))] + use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; + + let mut file_options = fs::OpenOptions::new(); + file_options.create(true); + file_options.write(true); + file_options.truncate(true); + #[cfg(not(windows))] + file_options.mode(0o600); + let mut file = file_options.open(path)?; + #[cfg(not(windows))] + file.set_permissions(fs::Permissions::from_mode(0o600))?; + file.write_all(serde_json::to_string(lock_data).unwrap().as_bytes()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/src/util/io.rs b/cli/src/util/io.rs index 2de2a72583ea9..17786463eea11 100644 --- a/cli/src/util/io.rs +++ b/cli/src/util/io.rs @@ -68,7 +68,6 @@ where Ok(bytes_so_far) } - /// Helper used when converting Future interfaces to poll-based interfaces. /// Stores excess data that can be reused on future polls. #[derive(Default)] @@ -221,7 +220,7 @@ pub fn tailf(file: File, n: usize) -> mpsc::UnboundedReceiver { #[cfg(test)] mod tests { - use rand::Rng; + use rand::RngExt; use std::{fs::OpenOptions, io::Write}; use super::*; @@ -315,7 +314,11 @@ mod tests { let mut written = vec![]; let base_line = "Elit ipsum cillum ex cillum. Adipisicing consequat cupidatat do proident ut in sunt Lorem ipsum tempor. Eiusmod ipsum Lorem labore exercitation sunt pariatur excepteur fugiat cillum velit cillum enim. Nisi Lorem cupidatat ad enim velit officia eiusmod esse tempor aliquip. Deserunt pariatur tempor in duis culpa esse sit nulla irure ullamco ipsum voluptate non laboris. Occaecat officia nulla officia mollit do aliquip reprehenderit ad incididunt."; for i in 0..100 { - let line = format!("{}: {}", i, &base_line[..rng.gen_range(0..base_line.len())]); + let line = format!( + "{}: {}", + i, + &base_line[..rng.random_range(0..base_line.len())] + ); writeln!(&mut read_file, "{line}").unwrap(); written.push(line); }