From e598a1db69ddf2d91d1ab24cafb6b1b7f8523253 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:53:52 -0800 Subject: [PATCH 01/31] Update .gitignore for tree-sitter files --- .gitignore | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index e10bb0704..c7021dcb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ -target +build/ +target/ bin/ .DS_Store *.msix -# Node.js generated files for tree-sitter -build/ -node_modules/ +# Generated files for tree-sitter grammars/**/bindings/ grammars/**/src/ grammars/**/parser.* +tree-sitter-ssh-server-config/ +tree-sitter-dscexpression/ From bde1fc7c8e896eea87819232dbe43c59f5b20dc6 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:55:45 -0800 Subject: [PATCH 02/31] Add bicep.proto file This is imported from: https://github.com/Azure/bicep/blob/main/src/Bicep.Local.Rpc/extension.proto There may be a better way to sync this dependency, but as far as I can tell they're usually just copied like this. --- dsc/src/bicep/bicep.proto | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 dsc/src/bicep/bicep.proto diff --git a/dsc/src/bicep/bicep.proto b/dsc/src/bicep/bicep.proto new file mode 100644 index 000000000..c02b28d90 --- /dev/null +++ b/dsc/src/bicep/bicep.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +option csharp_namespace = "Bicep.Local.Rpc"; + +package extension; + +service BicepExtension { + rpc CreateOrUpdate (ResourceSpecification) returns (LocalExtensibilityOperationResponse); + rpc Preview (ResourceSpecification) returns (LocalExtensibilityOperationResponse); + rpc Get (ResourceReference) returns (LocalExtensibilityOperationResponse); + rpc Delete (ResourceReference) returns (LocalExtensibilityOperationResponse); + rpc GetTypeFiles(Empty) returns (TypeFilesResponse); + rpc Ping(Empty) returns (Empty); +} + +message Empty {} + +message ResourceSpecification { + optional string config = 1; + string type = 2; + optional string apiVersion = 3; + string properties = 4; +} + +message ResourceReference { + string identifiers = 1; + optional string config = 2; + string type = 3; + optional string apiVersion = 4; +} + +message LocalExtensibilityOperationResponse { + optional Resource resource = 1; + optional ErrorData errorData = 2; +} + +message Resource { + string type = 1; + optional string apiVersion = 2; + string identifiers = 3; + string properties = 4; + optional string status = 5; +} + +message ErrorData { + Error error = 1; +} + +message Error { + string code = 1; + optional string target = 2; + string message = 3; + repeated ErrorDetail details = 4; + optional string innerError = 5; +} + +message ErrorDetail { + string code = 1; + optional string target = 2; + string message = 3; +} + +message TypeFilesResponse { + string indexFile = 1; + map typeFiles = 2; +} From 92b6e01e8d2b368ebfc14cc9d38bfb5d60826c40 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:56:28 -0800 Subject: [PATCH 03/31] Basic CLI plumbing for Bicep gRPC server --- dsc/locales/en-us.toml | 1 + dsc/src/args.rs | 2 ++ dsc/src/bicep/mod.rs | 7 +++++++ dsc/src/main.rs | 9 +++++++++ dsc/src/util.rs | 1 + 5 files changed, 20 insertions(+) create mode 100644 dsc/src/bicep/mod.rs diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index 61d5946b6..62d3525b2 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -36,6 +36,7 @@ functionAbout = "Operations on DSC functions" listFunctionAbout = "List or find functions" version = "The version of the resource to invoke in semver format" mcpAbout = "Use DSC as a MCP server" +bicepAbout = "Use DSC as a Bicep server over gRPC" [main] ctrlCReceived = "Ctrl-C received" diff --git a/dsc/src/args.rs b/dsc/src/args.rs index e7e514b7e..e365b9baa 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -93,6 +93,8 @@ pub enum SubCommand { }, #[clap(name = "mcp", about = t!("args.mcpAbout").to_string())] Mcp, + #[clap(name = "bicep", about = t!("args.bicepAbout").to_string())] + Bicep, #[clap(name = "resource", about = t!("args.resourceAbout").to_string())] Resource { #[clap(subcommand)] diff --git a/dsc/src/bicep/mod.rs b/dsc/src/bicep/mod.rs new file mode 100644 index 000000000..2e59321a2 --- /dev/null +++ b/dsc/src/bicep/mod.rs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub fn start_bicep_server() -> Result<(), Box> { + // TODO: Start a gRPC server to handle Bicep requests. + Ok(()) +} diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 83fbbac95..64ac6862c 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -6,6 +6,7 @@ use clap::{CommandFactory, Parser}; use clap_complete::generate; use dsc_lib::progress::ProgressFormat; use mcp::start_mcp_server; +use bicep::start_bicep_server; use rust_i18n::{i18n, t}; use std::{io, process::exit}; use sysinfo::{Process, RefreshKind, System, get_current_pid, ProcessRefreshKind}; @@ -20,6 +21,7 @@ use std::env; pub mod args; pub mod mcp; +pub mod bicep; pub mod resolve; pub mod resource_command; pub mod subcommand; @@ -92,6 +94,13 @@ fn main() { } exit(util::EXIT_SUCCESS); } + SubCommand::Bicep => { + if let Err(err) = start_bicep_server() { + error!("{}", t!("main.failedToStartBicepServer", error = err)); + exit(util::EXIT_BICEP_FAILED); + } + exit(util::EXIT_SUCCESS); + } SubCommand::Resource { subcommand } => { subcommand::resource(&subcommand, progress_format); }, diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 6e4ec31a0..049a886fa 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -73,6 +73,7 @@ pub const EXIT_CTRL_C: i32 = 6; pub const EXIT_DSC_RESOURCE_NOT_FOUND: i32 = 7; pub const EXIT_DSC_ASSERTION_FAILED: i32 = 8; pub const EXIT_MCP_FAILED: i32 = 9; +pub const EXIT_BICEP_FAILED: i32 = 10; pub const DSC_CONFIG_ROOT: &str = "DSC_CONFIG_ROOT"; pub const DSC_TRACE_LEVEL: &str = "DSC_TRACE_LEVEL"; From 1868e58230ce4d90d3cb86c50013553f35b2cb10 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:54:29 -0800 Subject: [PATCH 04/31] Add Rust tonic and prost packages for gRPC And tonic-prost, and tonic-prost-build... Adds a simple build.rs script for compiling the Protobuf files. Requires the protoc binary: https://protobuf.dev/installation/ --- Cargo.lock | 320 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 ++ dsc/Cargo.toml | 6 + dsc/build.rs | 9 ++ 4 files changed, 343 insertions(+) create mode 100644 dsc/build.rs diff --git a/Cargo.lock b/Cargo.lock index 4388cf58d..2643796f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arc-swap" version = "1.7.1" @@ -131,6 +137,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base32" version = "0.5.1" @@ -647,6 +696,7 @@ dependencies = [ "indicatif", "jsonschema", "path-absolutize", + "prost", "regex", "rmcp", "rust-i18n", @@ -660,6 +710,9 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", + "tonic", + "tonic-prost", + "tonic-prost-build", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -957,6 +1010,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.4" @@ -1171,6 +1230,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -1228,6 +1306,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.7.0" @@ -1238,9 +1322,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1266,6 +1352,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.17" @@ -1622,12 +1721,24 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1650,6 +1761,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "murmurhash64" version = "0.3.1" @@ -2059,6 +2176,36 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2124,6 +2271,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -2133,6 +2290,80 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.4", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quick-xml" version = "0.38.3" @@ -3120,6 +3351,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -3174,6 +3416,74 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", + "tempfile", + "tonic-build", +] + [[package]] name = "tower" version = "0.5.2" @@ -3182,11 +3492,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3378,6 +3692,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-general-category" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index cf640b638..42861e86d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,8 @@ num-traits = { version = "0.2" } os_info = { version = "3.14" } # dsc, dsc-lib path-absolutize = { version = "3.1" } +# dsc +prost = { version = "0.14" } # dsc-lib-jsonschema-macros proc-macro2 = { version = "1.0" } # dsc-lib-jsonschema-macros @@ -198,6 +200,10 @@ thiserror = { version = "2.0" } tokio = { version = "1.49" } # dsc tokio-util = { version = "0.7" } +# dsc +tonic = { version = "*" } +# dsc +tonic-prost = { version = "*" } # dsc, dsc-lib, registry, dsc-lib-registry, runcommandonset, sshdconfig tracing = { version = "0.1" } # dsc, dsc-lib @@ -234,6 +240,8 @@ windows = { version = "0.62", features = [ # build-only dependencies # dsc-lib, dsc-lib-registry, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config cc = { version = "1.2" } +# dsc +tonic-prost-build = { version = "*" } # test-only dependencies # dsc-lib-jsonschema diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index 1f48be821..6eeb6e373 100644 --- a/dsc/Cargo.toml +++ b/dsc/Cargo.toml @@ -12,6 +12,7 @@ ctrlc = { workspace = true } indicatif = { workspace = true } jsonschema = { workspace = true } path-absolutize = { workspace = true } +prost = { workspace = true } regex = { workspace = true } rmcp = { workspace = true, features = [ "server", @@ -32,8 +33,13 @@ sysinfo = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } +tonic = { workspace = true } +tonic-prost = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-indicatif = { workspace = true } # workspace crate dependencies dsc-lib = { workspace = true } + +[build-dependencies] +tonic-prost-build = { workspace = true } diff --git a/dsc/build.rs b/dsc/build.rs new file mode 100644 index 000000000..e49a0d27d --- /dev/null +++ b/dsc/build.rs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +fn main() -> Result<(), Box> { + // TODO: We can save the compiled code so not every build needs protoc. + // See: https://github.com/hyperium/tonic/blob/master/tonic-build/README.md + tonic_prost_build::compile_protos("src/bicep/bicep.proto")?; + Ok(()) +} From 4b79f4f20c20ce008ebbaa5826882ca8c1c54397 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:25:32 -0800 Subject: [PATCH 05/31] Implement stub Bicep gRPC server Copilot was helpful, then reconciled against this example: https://github.com/hyperium/tonic/blob/master/examples/src/routeguide/server.rs --- dsc/src/bicep/mod.rs | 122 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/dsc/src/bicep/mod.rs b/dsc/src/bicep/mod.rs index 2e59321a2..56e0d8762 100644 --- a/dsc/src/bicep/mod.rs +++ b/dsc/src/bicep/mod.rs @@ -1,7 +1,127 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use tonic::{transport::Server, Request, Response, Status}; + +// Include the generated protobuf code +pub mod proto { + tonic::include_proto!("extension"); +} + +use proto::bicep_extension_server::{BicepExtension, BicepExtensionServer}; +use proto::{ + Empty, ResourceSpecification, ResourceReference, LocalExtensibilityOperationResponse, + TypeFilesResponse, +}; + +#[derive(Debug, Default)] +pub struct BicepExtensionService; + +#[tonic::async_trait] +impl BicepExtension for BicepExtensionService { + async fn create_or_update( + &self, + request: Request, + ) -> Result, Status> { + let spec = request.into_inner(); + tracing::debug!( + "CreateOrUpdate called for type: {}, apiVersion: {:?}", + spec.r#type, + spec.api_version + ); + + // TODO: Implement actual resource creation/update logic + Err(Status::unimplemented("CreateOrUpdate not yet implemented")) + } + + async fn preview( + &self, + request: Request, + ) -> Result, Status> { + let spec = request.into_inner(); + tracing::debug!( + "Preview called for type: {}, apiVersion: {:?}", + spec.r#type, + spec.api_version + ); + + // TODO: Implement preview/what-if logic + Err(Status::unimplemented("Preview not yet implemented")) + } + + async fn get( + &self, + request: Request, + ) -> Result, Status> { + let reference = request.into_inner(); + tracing::debug!( + "Get called for type: {}, identifiers: {}", + reference.r#type, + reference.identifiers + ); + + // TODO: Implement resource retrieval logic + Err(Status::unimplemented("Get not yet implemented")) + } + + async fn delete( + &self, + request: Request, + ) -> Result, Status> { + let reference = request.into_inner(); + tracing::debug!( + "Delete called for type: {}, identifiers: {}", + reference.r#type, + reference.identifiers + ); + + // TODO: Implement resource deletion logic + Err(Status::unimplemented("Delete not yet implemented")) + } + + async fn get_type_files( + &self, + _request: Request, + ) -> Result, Status> { + tracing::debug!("GetTypeFiles called"); + + // TODO: Return actual Bicep type definitions + Err(Status::unimplemented("GetTypeFiles not yet implemented")) + } + + async fn ping( + &self, + _request: Request, + ) -> Result, Status> { + tracing::debug!("Ping called"); + Ok(Response::new(Empty {})) + } +} + +async fn start_bicep_server_async(addr: impl Into) -> Result<(), Box> { + let addr = addr.into(); + + tracing::info!("Starting Bicep gRPC server on {addr}"); + + let route_guide = BicepExtensionService; + let svc = BicepExtensionServer::new(route_guide); + + Server::builder().add_service(svc).serve(addr).await?; + + Ok(()) +} + +/// Synchronous wrapper to start the Bicep gRPC server +/// +/// # Errors +/// +/// This function will return an error if the Bicep server fails to start or if the tokio runtime cannot be created. pub fn start_bicep_server() -> Result<(), Box> { - // TODO: Start a gRPC server to handle Bicep requests. + let rt = tokio::runtime::Runtime::new()?; + + // Default to localhost:50051 (standard gRPC port) + let addr: std::net::SocketAddr = "127.0.0.1:50051".parse()?; + + rt.block_on(start_bicep_server_async(addr))?; Ok(()) } From 8039b6cb062f2b066345501151a713307b58fd39 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:42:41 -0800 Subject: [PATCH 06/31] Refactor to stand-alone dscbicep binary Since the Bicep extension is very unforgiving on the CLI. --- Cargo.lock | 19 +++- Cargo.toml | 7 ++ dsc/Cargo.toml | 6 - dsc/build.rs | 9 -- dsc/src/args.rs | 2 - dsc/src/main.rs | 9 -- dscbicep/Cargo.toml | 21 ++++ dscbicep/build.rs | 7 ++ {dsc/src/bicep => dscbicep/proto}/bicep.proto | 4 + dsc/src/bicep/mod.rs => dscbicep/src/main.rs | 104 +++++++++++++++--- 10 files changed, 141 insertions(+), 47 deletions(-) delete mode 100644 dsc/build.rs create mode 100644 dscbicep/Cargo.toml create mode 100644 dscbicep/build.rs rename {dsc/src/bicep => dscbicep/proto}/bicep.proto (87%) rename dsc/src/bicep/mod.rs => dscbicep/src/main.rs (51%) diff --git a/Cargo.lock b/Cargo.lock index 2643796f5..1ff14bf23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -696,7 +696,6 @@ dependencies = [ "indicatif", "jsonschema", "path-absolutize", - "prost", "regex", "rmcp", "rust-i18n", @@ -710,9 +709,6 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", - "tonic", - "tonic-prost", - "tonic-prost-build", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -917,6 +913,21 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "dscbicep" +version = "0.1.0" +dependencies = [ + "clap", + "prost", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tracing", + "tracing-subscriber", +] + [[package]] name = "dsctest" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 42861e86d..204f4161c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" # the path to a crate. members = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -29,6 +30,7 @@ members = [ # avoid unintentionally modifying this value. default-members = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -56,6 +58,7 @@ default-members = [ # current operating system to enable faster builds. Windows = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -78,6 +81,7 @@ Windows = [ ] macOS = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -96,6 +100,7 @@ macOS = [ ] Linux = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -198,6 +203,8 @@ tempfile = { version = "3.24" } thiserror = { version = "2.0" } # dsc, dsc-lib tokio = { version = "1.49" } +# dscbicep +tokio-stream = { version = "0.1" } # dsc tokio-util = { version = "0.7" } # dsc diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index 6eeb6e373..1f48be821 100644 --- a/dsc/Cargo.toml +++ b/dsc/Cargo.toml @@ -12,7 +12,6 @@ ctrlc = { workspace = true } indicatif = { workspace = true } jsonschema = { workspace = true } path-absolutize = { workspace = true } -prost = { workspace = true } regex = { workspace = true } rmcp = { workspace = true, features = [ "server", @@ -33,13 +32,8 @@ sysinfo = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } -tonic = { workspace = true } -tonic-prost = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-indicatif = { workspace = true } # workspace crate dependencies dsc-lib = { workspace = true } - -[build-dependencies] -tonic-prost-build = { workspace = true } diff --git a/dsc/build.rs b/dsc/build.rs deleted file mode 100644 index e49a0d27d..000000000 --- a/dsc/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -fn main() -> Result<(), Box> { - // TODO: We can save the compiled code so not every build needs protoc. - // See: https://github.com/hyperium/tonic/blob/master/tonic-build/README.md - tonic_prost_build::compile_protos("src/bicep/bicep.proto")?; - Ok(()) -} diff --git a/dsc/src/args.rs b/dsc/src/args.rs index e365b9baa..e7e514b7e 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -93,8 +93,6 @@ pub enum SubCommand { }, #[clap(name = "mcp", about = t!("args.mcpAbout").to_string())] Mcp, - #[clap(name = "bicep", about = t!("args.bicepAbout").to_string())] - Bicep, #[clap(name = "resource", about = t!("args.resourceAbout").to_string())] Resource { #[clap(subcommand)] diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 64ac6862c..83fbbac95 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -6,7 +6,6 @@ use clap::{CommandFactory, Parser}; use clap_complete::generate; use dsc_lib::progress::ProgressFormat; use mcp::start_mcp_server; -use bicep::start_bicep_server; use rust_i18n::{i18n, t}; use std::{io, process::exit}; use sysinfo::{Process, RefreshKind, System, get_current_pid, ProcessRefreshKind}; @@ -21,7 +20,6 @@ use std::env; pub mod args; pub mod mcp; -pub mod bicep; pub mod resolve; pub mod resource_command; pub mod subcommand; @@ -94,13 +92,6 @@ fn main() { } exit(util::EXIT_SUCCESS); } - SubCommand::Bicep => { - if let Err(err) = start_bicep_server() { - error!("{}", t!("main.failedToStartBicepServer", error = err)); - exit(util::EXIT_BICEP_FAILED); - } - exit(util::EXIT_SUCCESS); - } SubCommand::Resource { subcommand } => { subcommand::resource(&subcommand, progress_format); }, diff --git a/dscbicep/Cargo.toml b/dscbicep/Cargo.toml new file mode 100644 index 000000000..ce3a7c759 --- /dev/null +++ b/dscbicep/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dscbicep" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "dscbicep" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +prost = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "io-std"] } +tokio-stream = { workspace = true, features = ["net"] } +tonic = { workspace = true } +tonic-prost = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[build-dependencies] +tonic-prost-build = { workspace = true } diff --git a/dscbicep/build.rs b/dscbicep/build.rs new file mode 100644 index 000000000..ee032c0cf --- /dev/null +++ b/dscbicep/build.rs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +fn main() -> Result<(), Box> { + tonic_prost_build::compile_protos("proto/bicep.proto")?; + Ok(()) +} diff --git a/dsc/src/bicep/bicep.proto b/dscbicep/proto/bicep.proto similarity index 87% rename from dsc/src/bicep/bicep.proto rename to dscbicep/proto/bicep.proto index c02b28d90..c977015a5 100644 --- a/dsc/src/bicep/bicep.proto +++ b/dscbicep/proto/bicep.proto @@ -1,3 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Imported from https://github.com/Azure/bicep/blob/9a9b56e15f4acb6fa1382f92ad69e4650fab4ee3/src/Bicep.Local.Rpc/extension.proto syntax = "proto3"; option csharp_namespace = "Bicep.Local.Rpc"; diff --git a/dsc/src/bicep/mod.rs b/dscbicep/src/main.rs similarity index 51% rename from dsc/src/bicep/mod.rs rename to dscbicep/src/main.rs index 56e0d8762..11222c7b6 100644 --- a/dsc/src/bicep/mod.rs +++ b/dscbicep/src/main.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use clap::Parser; use tonic::{transport::Server, Request, Response, Status}; // Include the generated protobuf code @@ -98,30 +99,99 @@ impl BicepExtension for BicepExtensionService { } } -async fn start_bicep_server_async(addr: impl Into) -> Result<(), Box> { - let addr = addr.into(); +#[derive(Parser, Debug)] +#[command(name = "dscbicep")] +#[command(about = "DSC Bicep Local Deploy Extension", long_about = None)] +struct Args { + /// The path to the domain socket to connect on (Unix-like systems) + #[arg(long)] + socket: Option, + + /// The named pipe to connect on (Windows) + #[arg(long)] + pipe: Option, + + /// Wait for debugger to attach before starting + #[arg(long)] + wait_for_debugger: bool, +} - tracing::info!("Starting Bicep gRPC server on {addr}"); +#[allow(unused_variables)] +async fn run_server( + socket: Option, + pipe: Option, +) -> Result<(), Box> { + let service = BicepExtensionService; - let route_guide = BicepExtensionService; - let svc = BicepExtensionServer::new(route_guide); + #[cfg(unix)] + if let Some(socket_path) = socket { + use tokio::net::UnixListener; + use tokio_stream::wrappers::UnixListenerStream; - Server::builder().add_service(svc).serve(addr).await?; + tracing::info!("Starting Bicep gRPC server on Unix socket: {}", socket_path); - Ok(()) + // Remove the socket file if it exists + let _ = std::fs::remove_file(&socket_path); + + let uds = UnixListener::bind(&socket_path)?; + let uds_stream = UnixListenerStream::new(uds); + + Server::builder() + .add_service(BicepExtensionServer::new(service)) + .serve_with_incoming(uds_stream) + .await?; + + return Ok(()); + } + + #[cfg(windows)] + if let Some(pipe_name) = pipe { + tracing::info!("Starting Bicep gRPC server on named pipe: {}", pipe_name); + + // TODO: Implement Windows named pipe transport + // This requires additional dependencies and platform-specific code + return Err("Windows named pipe support not yet implemented".into()); + } + + Err("Either --socket (Unix) or --pipe (Windows) must be specified".into()) } -/// Synchronous wrapper to start the Bicep gRPC server -/// -/// # Errors -/// -/// This function will return an error if the Bicep server fails to start or if the tokio runtime cannot be created. -pub fn start_bicep_server() -> Result<(), Box> { - let rt = tokio::runtime::Runtime::new()?; +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing + tracing_subscriber::fmt() + .with_target(false) + .with_level(true) + .init(); + + let args = Args::parse(); + + if args.wait_for_debugger { + tracing::info!("Waiting for debugger to attach..."); + tracing::info!("Press Enter to continue after attaching debugger"); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + } - // Default to localhost:50051 (standard gRPC port) - let addr: std::net::SocketAddr = "127.0.0.1:50051".parse()?; + // Set up graceful shutdown on SIGTERM/SIGINT + let shutdown_signal = async { + tokio::signal::ctrl_c() + .await + .expect("Failed to listen for shutdown signal"); + tracing::info!("Received shutdown signal, terminating gracefully..."); + }; + + tokio::select! { + result = run_server(args.socket, args.pipe) => { + if let Err(e) = result { + tracing::error!("Server error: {}", e); + return Err(e); + } + } + _ = shutdown_signal => { + tracing::info!("Shutdown complete"); + } + } - rt.block_on(start_bicep_server_async(addr))?; Ok(()) } From 6d74da505d7ad3c5ea17eb5a03807ee0f69c8736 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:09:33 -0800 Subject: [PATCH 07/31] Always attach debugger Until Bicep will actually send this. --- dscbicep/src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 11222c7b6..e84ded092 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -166,9 +166,10 @@ async fn main() -> Result<(), Box> { let args = Args::parse(); - if args.wait_for_debugger { + // TODO: Find out if there is any actual way to get bicep local-deploy to send the --wait-for-debugger command. + if true { tracing::info!("Waiting for debugger to attach..."); - tracing::info!("Press Enter to continue after attaching debugger"); + tracing::info!("Press any key to continue after attaching to PID: {}", std::process::id()); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; } From 379311703edf553a3c7c89730137578033533af8 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:58:00 -0800 Subject: [PATCH 08/31] Start to implement create_or_update gRPC method Upgraded to a "Resource not found" error! --- Cargo.lock | 1 + Cargo.toml | 2 +- dscbicep/Cargo.toml | 2 ++ dscbicep/src/main.rs | 29 +++++++++++++++++++++++++++-- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ff14bf23..e5c7ac529 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -918,6 +918,7 @@ name = "dscbicep" version = "0.1.0" dependencies = [ "clap", + "dsc-lib", "prost", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 204f4161c..284948e94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -248,7 +248,7 @@ windows = { version = "0.62", features = [ # dsc-lib, dsc-lib-registry, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config cc = { version = "1.2" } # dsc -tonic-prost-build = { version = "*" } +tonic-prost-build = { version = "0.14" } # test-only dependencies # dsc-lib-jsonschema diff --git a/dscbicep/Cargo.toml b/dscbicep/Cargo.toml index ce3a7c759..0771944fa 100644 --- a/dscbicep/Cargo.toml +++ b/dscbicep/Cargo.toml @@ -8,6 +8,8 @@ name = "dscbicep" path = "src/main.rs" [dependencies] +dsc-lib = { workspace = true } + clap = { workspace = true } prost = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "io-std"] } diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index e84ded092..8ac104c51 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -3,6 +3,11 @@ use clap::Parser; use tonic::{transport::Server, Request, Response, Status}; +use dsc_lib::{ + configure::config_doc::ExecutionKind, + dscresources::dscresource::Invoke, + DscManager, +}; // Include the generated protobuf code pub mod proto { @@ -31,8 +36,28 @@ impl BicepExtension for BicepExtensionService { spec.api_version ); - // TODO: Implement actual resource creation/update logic - Err(Status::unimplemented("CreateOrUpdate not yet implemented")) + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&spec.r#type, None) else { + return Err(Status::invalid_argument("Resource not found")); + }; + + let _result = match resource.set(&spec.properties, false, &ExecutionKind::Actual) { + Ok(res) => res, + Err(e) => return Err(Status::internal(format!("DSC set operation failed: {}", e))), + }; + + let response = LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: spec.r#type, + api_version: spec.api_version, + identifiers: String::new(), + properties: spec.properties, + status: None, + }), + error_data: None, + }; + + Ok(Response::new(response)) } async fn preview( From 34d5875f7f8e8565a7b72f25c4ec68a4b397f65d Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:35:56 -0800 Subject: [PATCH 09/31] Place the dscbicep binary --- dscbicep/.project.data.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 dscbicep/.project.data.json diff --git a/dscbicep/.project.data.json b/dscbicep/.project.data.json new file mode 100644 index 000000000..1f0ee7c11 --- /dev/null +++ b/dscbicep/.project.data.json @@ -0,0 +1,6 @@ +{ + "Name": "dscbicep", + "Kind": "CLI", + "IsRust": true, + "Binaries": ["dscbicep"] +} From 8382304852f614b52c08c08e68997e4dcbdb0100 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:37:47 -0800 Subject: [PATCH 10/31] Reuse Tokio runtime in invoke_command Since the gRPC (and MCP) servers already start one. --- .../src/dscresources/command_resource.rs | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 082895ccf..b899299d0 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -765,24 +765,41 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option let exit_codes = convert_hashmap_string_keys_to_i32(exit_codes)?; let executable = canonicalize_which(executable, cwd)?; - tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( - async { - trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); - if let Some(cwd) = cwd { - trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd.display())); - } + let run_async = async { + trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); + if let Some(cwd) = cwd { + trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd.display())); + } - match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await { - Ok((code, stdout, stderr)) => { - Ok((code, stdout, stderr)) - }, - Err(err) => { - error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err)); - Err(err) - } + match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await { + Ok((code, stdout, stderr)) => { + Ok((code, stdout, stderr)) + }, + Err(err) => { + error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err)); + Err(err) } } - ) + }; + + // Try to use existing runtime first (e.g. from gRPC or MCP server) + match tokio::runtime::Handle::try_current() { + Ok(handle) => { + std::thread::scope(|s| { + s.spawn(|| { + handle.block_on(run_async) + }).join().unwrap() + }) + }, + // Otherwise create a new runtime + Err(_) => { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(run_async) + } + } } /// Process the arguments for a command resource. From 403c2faa3972c722f2398afd3acc933ef2048f3b Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:54:58 -0800 Subject: [PATCH 11/31] Add tracing --- dscbicep/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 8ac104c51..e923ba295 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -187,6 +187,8 @@ async fn main() -> Result<(), Box> { tracing_subscriber::fmt() .with_target(false) .with_level(true) + // TODO: Plumb tracing env var support. + .with_max_level(tracing::Level::TRACE) .init(); let args = Args::parse(); From 169eeb62d1769d5e9d7bd3900897d6026a61a867 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:11:25 -0800 Subject: [PATCH 12/31] Don't build gRPC client --- dscbicep/build.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dscbicep/build.rs b/dscbicep/build.rs index ee032c0cf..7c763c9f3 100644 --- a/dscbicep/build.rs +++ b/dscbicep/build.rs @@ -2,6 +2,10 @@ // Licensed under the MIT License. fn main() -> Result<(), Box> { - tonic_prost_build::compile_protos("proto/bicep.proto")?; + tonic_prost_build::configure() + .build_client(false) + // TODO: Configure and commit the out_dir to avoid dependency on protoc + // .out_dir(out_dir) + .compile_protos(&["proto/bicep.proto"], &["proto"])?; Ok(()) } From 90b7b1f7de87065bc72472caee2d175b1c2371f7 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:49:30 -0800 Subject: [PATCH 13/31] Basic implementation of all methods --- dscbicep/src/main.rs | 155 ++++++++++++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 40 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index e923ba295..1b724b286 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -2,12 +2,10 @@ // Licensed under the MIT License. use clap::Parser; -use tonic::{transport::Server, Request, Response, Status}; use dsc_lib::{ - configure::config_doc::ExecutionKind, - dscresources::dscresource::Invoke, - DscManager, + configure::config_doc::ExecutionKind, dscresources::dscresource::Invoke, DscManager, }; +use tonic::{transport::Server, Request, Response, Status}; // Include the generated protobuf code pub mod proto { @@ -16,7 +14,7 @@ pub mod proto { use proto::bicep_extension_server::{BicepExtension, BicepExtensionServer}; use proto::{ - Empty, ResourceSpecification, ResourceReference, LocalExtensibilityOperationResponse, + Empty, LocalExtensibilityOperationResponse, ResourceReference, ResourceSpecification, TypeFilesResponse, }; @@ -30,28 +28,29 @@ impl BicepExtension for BicepExtensionService { request: Request, ) -> Result, Status> { let spec = request.into_inner(); - tracing::debug!( - "CreateOrUpdate called for type: {}, apiVersion: {:?}", - spec.r#type, - spec.api_version - ); + let resource_type = spec.r#type; + let version = spec.api_version; + let properties = spec.properties; + + tracing::debug!("CreateOrUpdate called for {resource_type}@{version:?}: {properties}"); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&spec.r#type, None) else { + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { return Err(Status::invalid_argument("Resource not found")); }; - let _result = match resource.set(&spec.properties, false, &ExecutionKind::Actual) { + let _result = match resource.set(&properties, false, &ExecutionKind::Actual) { Ok(res) => res, - Err(e) => return Err(Status::internal(format!("DSC set operation failed: {}", e))), + Err(e) => return Err(Status::internal(format!("DSC set operation failed: {e}"))), }; + // TODO: Use '_result'. let response = LocalExtensibilityOperationResponse { resource: Some(proto::Resource { - r#type: spec.r#type, - api_version: spec.api_version, + r#type: resource_type, + api_version: version, identifiers: String::new(), - properties: spec.properties, + properties: properties, status: None, }), error_data: None, @@ -65,14 +64,39 @@ impl BicepExtension for BicepExtensionService { request: Request, ) -> Result, Status> { let spec = request.into_inner(); - tracing::debug!( - "Preview called for type: {}, apiVersion: {:?}", - spec.r#type, - spec.api_version - ); + let resource_type = spec.r#type; + let version = spec.api_version; + let properties = spec.properties; + + tracing::debug!("Preview called for {resource_type}@{version:?}: {properties}"); + + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + return Err(Status::invalid_argument("Resource not found")); + }; + + let _result = match resource.set(&properties, false, &ExecutionKind::WhatIf) { + Ok(res) => res, + Err(e) => { + return Err(Status::internal(format!( + "DSC whatif operation failed: {e}" + ))) + } + }; - // TODO: Implement preview/what-if logic - Err(Status::unimplemented("Preview not yet implemented")) + // TODO: Use '_result'. + let response = LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: String::new(), + properties: properties, + status: None, + }), + error_data: None, + }; + + Ok(Response::new(response)) } async fn get( @@ -80,14 +104,36 @@ impl BicepExtension for BicepExtensionService { request: Request, ) -> Result, Status> { let reference = request.into_inner(); - tracing::debug!( - "Get called for type: {}, identifiers: {}", - reference.r#type, - reference.identifiers - ); + let resource_type = reference.r#type.clone(); + let version = reference.api_version.clone(); + let identifiers = reference.identifiers.clone(); + + tracing::debug!("Get called for {resource_type}@{version:?}: {identifiers}"); + + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + return Err(Status::invalid_argument("Resource not found")); + }; + + // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. + let _result = match resource.get(&identifiers) { + Ok(res) => res, + Err(e) => return Err(Status::internal(format!("DSC get operation failed: {e}"))), + }; + + // TODO: Use '_result'. + let response = LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: identifiers, + properties: String::new(), + status: None, + }), + error_data: None, + }; - // TODO: Implement resource retrieval logic - Err(Status::unimplemented("Get not yet implemented")) + Ok(Response::new(response)) } async fn delete( @@ -95,14 +141,45 @@ impl BicepExtension for BicepExtensionService { request: Request, ) -> Result, Status> { let reference = request.into_inner(); + let resource_type = reference.r#type.clone(); + let version = reference.api_version.clone(); + let identifiers = reference.identifiers.clone(); + tracing::debug!( - "Delete called for type: {}, identifiers: {}", - reference.r#type, - reference.identifiers + "Delete called for {}@{:?}: {}", + resource_type, + version, + identifiers ); - // TODO: Implement resource deletion logic - Err(Status::unimplemented("Delete not yet implemented")) + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + return Err(Status::invalid_argument("Resource not found")); + }; + + // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. + let _result = match resource.delete(&identifiers) { + Ok(res) => res, + Err(e) => { + return Err(Status::internal(format!( + "DSC delete operation failed: {e}" + ))) + } + }; + + // TODO: Use '_result'. + let response = LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: identifiers, + properties: String::new(), + status: None, + }), + error_data: None, + }; + + Ok(Response::new(response)) } async fn get_type_files( @@ -111,14 +188,12 @@ impl BicepExtension for BicepExtensionService { ) -> Result, Status> { tracing::debug!("GetTypeFiles called"); - // TODO: Return actual Bicep type definitions + // TODO: Return actual Bicep type definitions...yet the extension already has these? + // Perhaps this is where we can dynamically get them from the current system. Err(Status::unimplemented("GetTypeFiles not yet implemented")) } - async fn ping( - &self, - _request: Request, - ) -> Result, Status> { + async fn ping(&self, _request: Request) -> Result, Status> { tracing::debug!("Ping called"); Ok(Response::new(Empty {})) } From b93d59822afe122ffcc0ab5b67623fda7794c752 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:27:53 -0800 Subject: [PATCH 14/31] Check environment for tracing and debug settings --- dscbicep/src/main.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 1b724b286..5e021ec0c 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -258,20 +258,31 @@ async fn run_server( #[tokio::main] async fn main() -> Result<(), Box> { - // Initialize tracing + let trace_level = std::env::var("DSC_TRACE_LEVEL") + .ok() + .and_then(|level| match level.to_uppercase().as_str() { + "TRACE" => Some(tracing::Level::TRACE), + "DEBUG" => Some(tracing::Level::DEBUG), + "INFO" => Some(tracing::Level::INFO), + "WARN" => Some(tracing::Level::WARN), + "ERROR" => Some(tracing::Level::ERROR), + _ => None, + }) + .unwrap_or(tracing::Level::WARN); + tracing_subscriber::fmt() .with_target(false) .with_level(true) - // TODO: Plumb tracing env var support. - .with_max_level(tracing::Level::TRACE) + .with_max_level(trace_level) .init(); let args = Args::parse(); - // TODO: Find out if there is any actual way to get bicep local-deploy to send the --wait-for-debugger command. - if true { - tracing::info!("Waiting for debugger to attach..."); - tracing::info!("Press any key to continue after attaching to PID: {}", std::process::id()); + if args.wait_for_debugger || std::env::var_os("DSC_GRPC_DEBUG").is_some() { + tracing::warn!( + "Press any key to continue after attaching to PID: {}", + std::process::id() + ); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; } From 9900616953dd7c0b42b51893ddd6322193192b74 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:32:58 -0800 Subject: [PATCH 15/31] Add a default HTTP server for debugging with grpcurl --- dscbicep/src/main.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 5e021ec0c..0c953c399 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -211,6 +211,10 @@ struct Args { #[arg(long)] pipe: Option, + /// The HTTP address to listen on (e.g., 127.0.0.1:50051) + #[arg(long)] + http: Option, + /// Wait for debugger to attach before starting #[arg(long)] wait_for_debugger: bool, @@ -220,6 +224,7 @@ struct Args { async fn run_server( socket: Option, pipe: Option, + http: Option, ) -> Result<(), Box> { let service = BicepExtensionService; @@ -253,7 +258,16 @@ async fn run_server( return Err("Windows named pipe support not yet implemented".into()); } - Err("Either --socket (Unix) or --pipe (Windows) must be specified".into()) + // Default to HTTP server on [::1]:50051 if no transport specified + let addr = http.unwrap_or_else(|| "[::1]:50051".to_string()).parse()?; + tracing::info!("Starting Bicep gRPC server on HTTP: {addr}"); + + Server::builder() + .add_service(BicepExtensionServer::new(service)) + .serve(addr) + .await?; + + Ok(()) } #[tokio::main] @@ -296,9 +310,9 @@ async fn main() -> Result<(), Box> { }; tokio::select! { - result = run_server(args.socket, args.pipe) => { + result = run_server(args.socket, args.pipe, args.http) => { if let Err(e) = result { - tracing::error!("Server error: {}", e); + tracing::error!("Server error: {e}"); return Err(e); } } From c693ecc16de2ab081f974fc7cd5db144a407ca26 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:38:22 -0800 Subject: [PATCH 16/31] Add tonic-reflection package --- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 20 +++++++++++--------- dscbicep/Cargo.toml | 1 + 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5c7ac529..24095894a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -925,6 +925,7 @@ dependencies = [ "tonic", "tonic-prost", "tonic-prost-build", + "tonic-reflection", "tracing", "tracing-subscriber", ] @@ -3496,6 +3497,20 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "tonic-reflection" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34da53e8387581d66db16ff01f98a70b426b091fdf76856e289d5c1bd386ed7b" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", +] + [[package]] name = "tower" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 284948e94..f542a86c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,7 +135,7 @@ base32 = { version = "0.5" } base64 = { version = "0.22" } # dsc-lib, sshdconfig chrono = { version = "0.4" } -# dsc, dsc-lib, dscecho, registry, runcommandonset, sshdconfig, dsctest, test_group_resource +# dsc, dsc-lib, dscbicep, dscecho, registry, runcommandonset, sshdconfig, dsctest, test_group_resource clap = { version = "4.5", features = ["derive"] } # dsc clap_complete = { version = "4.5" } @@ -165,7 +165,7 @@ num-traits = { version = "0.2" } os_info = { version = "3.14" } # dsc, dsc-lib path-absolutize = { version = "3.1" } -# dsc +# dscbicep prost = { version = "0.14" } # dsc-lib-jsonschema-macros proc-macro2 = { version = "1.0" } @@ -201,21 +201,23 @@ sysinfo = { version = "0.37" } tempfile = { version = "3.24" } # dsc, dsc-lib, registry, dsc-lib-registry, sshdconfig thiserror = { version = "2.0" } -# dsc, dsc-lib +# dsc, dsc-lib, dscbicep tokio = { version = "1.49" } # dscbicep tokio-stream = { version = "0.1" } # dsc tokio-util = { version = "0.7" } -# dsc -tonic = { version = "*" } -# dsc -tonic-prost = { version = "*" } -# dsc, dsc-lib, registry, dsc-lib-registry, runcommandonset, sshdconfig +# dscbicep +tonic = { version = "0.14" } +# dscbicep +tonic-prost = { version = "0.14" } +# dscbicep +tonic-reflection = { version = "0.14" } +# dsc, dsc-lib, dscbicep, registry, dsc-lib-registry, runcommandonset, sshdconfig tracing = { version = "0.1" } # dsc, dsc-lib tracing-indicatif = { version = "0.3" } -# dsc, registry, dsc-lib-registry, runcommandonset, sshdconfig +# dsc, dscbicep, registry, dsc-lib-registry, runcommandonset, sshdconfig tracing-subscriber = { version = "0.3", features = ["ansi", "env-filter", "json"] } # dsc-lib, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config tree-sitter = { version = "0.26" } diff --git a/dscbicep/Cargo.toml b/dscbicep/Cargo.toml index 0771944fa..ab4355eb8 100644 --- a/dscbicep/Cargo.toml +++ b/dscbicep/Cargo.toml @@ -16,6 +16,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", " tokio-stream = { workspace = true, features = ["net"] } tonic = { workspace = true } tonic-prost = { workspace = true } +tonic-reflection = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } From 841cd13518791ae75e208c294241ca2f37d73965 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:01:28 -0800 Subject: [PATCH 17/31] Add tonic reflection service --- dscbicep/build.rs | 13 +++++++++---- dscbicep/src/main.rs | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/dscbicep/build.rs b/dscbicep/build.rs index 7c763c9f3..0a7482414 100644 --- a/dscbicep/build.rs +++ b/dscbicep/build.rs @@ -1,11 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::{env, path::PathBuf}; + fn main() -> Result<(), Box> { + let descriptor_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bicep.bin"); + tonic_prost_build::configure() - .build_client(false) - // TODO: Configure and commit the out_dir to avoid dependency on protoc - // .out_dir(out_dir) - .compile_protos(&["proto/bicep.proto"], &["proto"])?; + .build_client(false) + .file_descriptor_set_path(&descriptor_path) + // TODO: Configure and commit the out_dir to avoid dependency on protoc + // .out_dir(out_dir) + .compile_protos(&["proto/bicep.proto"], &["proto"])?; Ok(()) } diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 0c953c399..9dc033b00 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -10,6 +10,7 @@ use tonic::{transport::Server, Request, Response, Status}; // Include the generated protobuf code pub mod proto { tonic::include_proto!("extension"); + pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("bicep"); } use proto::bicep_extension_server::{BicepExtension, BicepExtensionServer}; @@ -227,6 +228,10 @@ async fn run_server( http: Option, ) -> Result<(), Box> { let service = BicepExtensionService; + let reflection_service = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET) + .build_v1() + .unwrap(); #[cfg(unix)] if let Some(socket_path) = socket { @@ -242,6 +247,7 @@ async fn run_server( let uds_stream = UnixListenerStream::new(uds); Server::builder() + .add_service(reflection_service) .add_service(BicepExtensionServer::new(service)) .serve_with_incoming(uds_stream) .await?; @@ -263,6 +269,7 @@ async fn run_server( tracing::info!("Starting Bicep gRPC server on HTTP: {addr}"); Server::builder() + .add_service(reflection_service) .add_service(BicepExtensionServer::new(service)) .serve(addr) .await?; From 093a165f1c04ffcb83488a40b780a7fb49ccdc76 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:13:20 -0800 Subject: [PATCH 18/31] Refactor use std:: --- dscbicep/src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 9dc033b00..68f9784a6 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -5,6 +5,7 @@ use clap::Parser; use dsc_lib::{ configure::config_doc::ExecutionKind, dscresources::dscresource::Invoke, DscManager, }; +use std::{env, fs, io, process}; use tonic::{transport::Server, Request, Response, Status}; // Include the generated protobuf code @@ -241,7 +242,7 @@ async fn run_server( tracing::info!("Starting Bicep gRPC server on Unix socket: {}", socket_path); // Remove the socket file if it exists - let _ = std::fs::remove_file(&socket_path); + let _ = fs::remove_file(&socket_path); let uds = UnixListener::bind(&socket_path)?; let uds_stream = UnixListenerStream::new(uds); @@ -279,7 +280,7 @@ async fn run_server( #[tokio::main] async fn main() -> Result<(), Box> { - let trace_level = std::env::var("DSC_TRACE_LEVEL") + let trace_level = env::var("DSC_TRACE_LEVEL") .ok() .and_then(|level| match level.to_uppercase().as_str() { "TRACE" => Some(tracing::Level::TRACE), @@ -299,13 +300,13 @@ async fn main() -> Result<(), Box> { let args = Args::parse(); - if args.wait_for_debugger || std::env::var_os("DSC_GRPC_DEBUG").is_some() { + if args.wait_for_debugger || env::var_os("DSC_GRPC_DEBUG").is_some() { tracing::warn!( "Press any key to continue after attaching to PID: {}", - std::process::id() + process::id() ); let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; + io::stdin().read_line(&mut input)?; } // Set up graceful shutdown on SIGTERM/SIGINT From 621a34dd9d458fbd35857443f69f326d7d19d048 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:18:05 -0800 Subject: [PATCH 19/31] Implement fuller return responses --- dscbicep/src/main.rs | 124 ++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 72 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 68f9784a6..57e535343 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -41,24 +41,21 @@ impl BicepExtension for BicepExtensionService { return Err(Status::invalid_argument("Resource not found")); }; - let _result = match resource.set(&properties, false, &ExecutionKind::Actual) { - Ok(res) => res, - Err(e) => return Err(Status::internal(format!("DSC set operation failed: {e}"))), + let result = match resource.set(&properties, false, &ExecutionKind::Actual) { + Ok(r) => LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: String::new(), + properties: serde_json::to_string(&r).unwrap(), + status: None, + }), + error_data: None, + }, + Err(e) => return Err(Status::internal(format!("DSC set operation failed: {e}"))) }; - // TODO: Use '_result'. - let response = LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: String::new(), - properties: properties, - status: None, - }), - error_data: None, - }; - - Ok(Response::new(response)) + Ok(Response::new(result)) } async fn preview( @@ -77,28 +74,21 @@ impl BicepExtension for BicepExtensionService { return Err(Status::invalid_argument("Resource not found")); }; - let _result = match resource.set(&properties, false, &ExecutionKind::WhatIf) { - Ok(res) => res, - Err(e) => { - return Err(Status::internal(format!( - "DSC whatif operation failed: {e}" - ))) - } - }; - - // TODO: Use '_result'. - let response = LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: String::new(), - properties: properties, - status: None, - }), - error_data: None, + let result = match resource.set(&properties, false, &ExecutionKind::WhatIf) { + Ok(r) => LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: String::new(), + properties: serde_json::to_string(&r).unwrap(), + status: None, + }), + error_data: None, + }, + Err(e) => return Err(Status::internal(format!("DSC whatif operation failed: {e}"))) }; - Ok(Response::new(response)) + Ok(Response::new(result)) } async fn get( @@ -118,24 +108,21 @@ impl BicepExtension for BicepExtensionService { }; // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. - let _result = match resource.get(&identifiers) { - Ok(res) => res, - Err(e) => return Err(Status::internal(format!("DSC get operation failed: {e}"))), + let result = match resource.get(&identifiers) { + Ok(r) => LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: String::new(), + properties: serde_json::to_string(&r).unwrap(), + status: None, + }), + error_data: None, + }, + Err(e) => return Err(Status::internal(format!("DSC get operation failed: {e}"))) }; - // TODO: Use '_result'. - let response = LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: identifiers, - properties: String::new(), - status: None, - }), - error_data: None, - }; - - Ok(Response::new(response)) + Ok(Response::new(result)) } async fn delete( @@ -160,28 +147,21 @@ impl BicepExtension for BicepExtensionService { }; // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. - let _result = match resource.delete(&identifiers) { - Ok(res) => res, - Err(e) => { - return Err(Status::internal(format!( - "DSC delete operation failed: {e}" - ))) - } - }; - - // TODO: Use '_result'. - let response = LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: identifiers, - properties: String::new(), - status: None, - }), - error_data: None, + let result = match resource.delete(&identifiers) { + Ok(r) => LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: String::new(), + properties: serde_json::to_string(&r).unwrap(), + status: None, + }), + error_data: None, + }, + Err(e) => return Err(Status::internal(format!("DSC delete operation failed: {e}"))) }; - Ok(Response::new(response)) + Ok(Response::new(result)) } async fn get_type_files( From df36a0a5d75b20b07b24ba0fcb3952033afcca46 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 17 Dec 2025 16:03:15 -0800 Subject: [PATCH 20/31] Fix bugs --- dscbicep/src/main.rs | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 57e535343..1f0f00d0b 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -46,13 +46,13 @@ impl BicepExtension for BicepExtensionService { resource: Some(proto::Resource { r#type: resource_type, api_version: version, - identifiers: String::new(), + identifiers: properties, properties: serde_json::to_string(&r).unwrap(), status: None, }), error_data: None, }, - Err(e) => return Err(Status::internal(format!("DSC set operation failed: {e}"))) + Err(e) => return Err(Status::internal(format!("DSC set operation failed: {e}"))), }; Ok(Response::new(result)) @@ -79,13 +79,17 @@ impl BicepExtension for BicepExtensionService { resource: Some(proto::Resource { r#type: resource_type, api_version: version, - identifiers: String::new(), + identifiers: properties, properties: serde_json::to_string(&r).unwrap(), status: None, }), error_data: None, }, - Err(e) => return Err(Status::internal(format!("DSC whatif operation failed: {e}"))) + Err(e) => { + return Err(Status::internal(format!( + "DSC whatif operation failed: {e}" + ))) + } }; Ok(Response::new(result)) @@ -113,13 +117,13 @@ impl BicepExtension for BicepExtensionService { resource: Some(proto::Resource { r#type: resource_type, api_version: version, - identifiers: String::new(), + identifiers: identifiers, properties: serde_json::to_string(&r).unwrap(), status: None, }), error_data: None, }, - Err(e) => return Err(Status::internal(format!("DSC get operation failed: {e}"))) + Err(e) => return Err(Status::internal(format!("DSC get operation failed: {e}"))), }; Ok(Response::new(result)) @@ -152,13 +156,17 @@ impl BicepExtension for BicepExtensionService { resource: Some(proto::Resource { r#type: resource_type, api_version: version, - identifiers: String::new(), + identifiers: identifiers, properties: serde_json::to_string(&r).unwrap(), status: None, }), error_data: None, }, - Err(e) => return Err(Status::internal(format!("DSC delete operation failed: {e}"))) + Err(e) => { + return Err(Status::internal(format!( + "DSC delete operation failed: {e}" + ))) + } }; Ok(Response::new(result)) @@ -209,10 +217,6 @@ async fn run_server( http: Option, ) -> Result<(), Box> { let service = BicepExtensionService; - let reflection_service = tonic_reflection::server::Builder::configure() - .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET) - .build_v1() - .unwrap(); #[cfg(unix)] if let Some(socket_path) = socket { @@ -228,7 +232,7 @@ async fn run_server( let uds_stream = UnixListenerStream::new(uds); Server::builder() - .add_service(reflection_service) + // .add_service(reflection_service) .add_service(BicepExtensionServer::new(service)) .serve_with_incoming(uds_stream) .await?; @@ -249,6 +253,11 @@ async fn run_server( let addr = http.unwrap_or_else(|| "[::1]:50051".to_string()).parse()?; tracing::info!("Starting Bicep gRPC server on HTTP: {addr}"); + let reflection_service = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET) + .build_v1() + .unwrap(); + Server::builder() .add_service(reflection_service) .add_service(BicepExtensionServer::new(service)) @@ -280,7 +289,9 @@ async fn main() -> Result<(), Box> { let args = Args::parse(); - if args.wait_for_debugger || env::var_os("DSC_GRPC_DEBUG").is_some() { + if args.wait_for_debugger + || env::var_os("DSC_GRPC_DEBUG").is_some_and(|v| v.eq_ignore_ascii_case("true")) + { tracing::warn!( "Press any key to continue after attaching to PID: {}", process::id() From 1f37f5d8e2e66316b4b4cc6706f8bdd841e70a09 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 18 Dec 2025 00:51:43 -0800 Subject: [PATCH 21/31] Unwrap DSC result structs to property bags Now output works! --- dscbicep/src/main.rs | 108 ++++++++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 1f0f00d0b..bfbbcb61b 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -3,7 +3,9 @@ use clap::Parser; use dsc_lib::{ - configure::config_doc::ExecutionKind, dscresources::dscresource::Invoke, DscManager, + configure::config_doc::ExecutionKind, + dscresources::{dscresource::Invoke, invoke_result}, + DscManager, }; use std::{env, fs, io, process}; use tonic::{transport::Server, Request, Response, Status}; @@ -42,20 +44,27 @@ impl BicepExtension for BicepExtensionService { }; let result = match resource.set(&properties, false, &ExecutionKind::Actual) { - Ok(r) => LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: properties, - properties: serde_json::to_string(&r).unwrap(), - status: None, - }), - error_data: None, + Ok(r) => match r { + invoke_result::SetResult::Resource(set_result) => { + serde_json::to_string(&set_result.after_state).map_err(|e| { + Status::internal(format!("Failed to serialize actual state: {e}")) + })? + } + _ => return Err(Status::unimplemented("Group resources not yet supported")), }, Err(e) => return Err(Status::internal(format!("DSC set operation failed: {e}"))), }; - Ok(Response::new(result)) + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: properties, + properties: result, + status: None, + }), + error_data: None, + })) } async fn preview( @@ -75,15 +84,13 @@ impl BicepExtension for BicepExtensionService { }; let result = match resource.set(&properties, false, &ExecutionKind::WhatIf) { - Ok(r) => LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: properties, - properties: serde_json::to_string(&r).unwrap(), - status: None, - }), - error_data: None, + Ok(r) => match r { + invoke_result::SetResult::Resource(set_result) => { + serde_json::to_string(&set_result.after_state).map_err(|e| { + Status::internal(format!("Failed to serialize actual state: {e}")) + })? + } + _ => return Err(Status::unimplemented("Group resources not yet supported")), }, Err(e) => { return Err(Status::internal(format!( @@ -92,7 +99,16 @@ impl BicepExtension for BicepExtensionService { } }; - Ok(Response::new(result)) + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: properties, + properties: result, + status: None, + }), + error_data: None, + })) } async fn get( @@ -113,20 +129,27 @@ impl BicepExtension for BicepExtensionService { // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. let result = match resource.get(&identifiers) { - Ok(r) => LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: identifiers, - properties: serde_json::to_string(&r).unwrap(), - status: None, - }), - error_data: None, + Ok(r) => match r { + invoke_result::GetResult::Resource(get_result) => { + serde_json::to_string(&get_result.actual_state).map_err(|e| { + Status::internal(format!("Failed to serialize actual state: {e}")) + })? + } + _ => return Err(Status::unimplemented("Group resources not yet supported")), }, Err(e) => return Err(Status::internal(format!("DSC get operation failed: {e}"))), }; - Ok(Response::new(result)) + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: identifiers, + properties: result, + status: None, + }), + error_data: None, + })) } async fn delete( @@ -152,16 +175,8 @@ impl BicepExtension for BicepExtensionService { // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. let result = match resource.delete(&identifiers) { - Ok(r) => LocalExtensibilityOperationResponse { - resource: Some(proto::Resource { - r#type: resource_type, - api_version: version, - identifiers: identifiers, - properties: serde_json::to_string(&r).unwrap(), - status: None, - }), - error_data: None, - }, + // Successful deletion returns () so we return an empty JSON object. + Ok(_) => "{}".to_string(), Err(e) => { return Err(Status::internal(format!( "DSC delete operation failed: {e}" @@ -169,7 +184,16 @@ impl BicepExtension for BicepExtensionService { } }; - Ok(Response::new(result)) + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: identifiers, + properties: result, + status: None, + }), + error_data: None, + })) } async fn get_type_files( From 4266b52d97f4c1ede7725fca067bfbf814f58b3d Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:19:17 -0800 Subject: [PATCH 22/31] Simplify and remove direct serde dependency --- dscbicep/src/main.rs | 61 ++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index bfbbcb61b..1685d963a 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -4,7 +4,10 @@ use clap::Parser; use dsc_lib::{ configure::config_doc::ExecutionKind, - dscresources::{dscresource::Invoke, invoke_result}, + dscresources::{ + dscresource::Invoke, + invoke_result::{GetResult, SetResult}, + }, DscManager, }; use std::{env, fs, io, process}; @@ -43,16 +46,11 @@ impl BicepExtension for BicepExtensionService { return Err(Status::invalid_argument("Resource not found")); }; - let result = match resource.set(&properties, false, &ExecutionKind::Actual) { - Ok(r) => match r { - invoke_result::SetResult::Resource(set_result) => { - serde_json::to_string(&set_result.after_state).map_err(|e| { - Status::internal(format!("Failed to serialize actual state: {e}")) - })? - } - _ => return Err(Status::unimplemented("Group resources not yet supported")), - }, - Err(e) => return Err(Status::internal(format!("DSC set operation failed: {e}"))), + let SetResult::Resource(result) = resource + .set(&properties, false, &ExecutionKind::Actual) + .map_err(|e| Status::internal(format!("DSC set operation failed: {e}")))? + else { + return Err(Status::unimplemented("Group resources not supported")); }; Ok(Response::new(LocalExtensibilityOperationResponse { @@ -60,7 +58,7 @@ impl BicepExtension for BicepExtensionService { r#type: resource_type, api_version: version, identifiers: properties, - properties: result, + properties: result.after_state.to_string(), status: None, }), error_data: None, @@ -83,20 +81,11 @@ impl BicepExtension for BicepExtensionService { return Err(Status::invalid_argument("Resource not found")); }; - let result = match resource.set(&properties, false, &ExecutionKind::WhatIf) { - Ok(r) => match r { - invoke_result::SetResult::Resource(set_result) => { - serde_json::to_string(&set_result.after_state).map_err(|e| { - Status::internal(format!("Failed to serialize actual state: {e}")) - })? - } - _ => return Err(Status::unimplemented("Group resources not yet supported")), - }, - Err(e) => { - return Err(Status::internal(format!( - "DSC whatif operation failed: {e}" - ))) - } + let SetResult::Resource(result) = resource + .set(&properties, false, &ExecutionKind::WhatIf) + .map_err(|e| Status::internal(format!("DSC whatif operation failed: {e}")))? + else { + return Err(Status::unimplemented("Group resources not supported")); }; Ok(Response::new(LocalExtensibilityOperationResponse { @@ -104,7 +93,7 @@ impl BicepExtension for BicepExtensionService { r#type: resource_type, api_version: version, identifiers: properties, - properties: result, + properties: result.after_state.to_string(), status: None, }), error_data: None, @@ -127,17 +116,11 @@ impl BicepExtension for BicepExtensionService { return Err(Status::invalid_argument("Resource not found")); }; - // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. - let result = match resource.get(&identifiers) { - Ok(r) => match r { - invoke_result::GetResult::Resource(get_result) => { - serde_json::to_string(&get_result.actual_state).map_err(|e| { - Status::internal(format!("Failed to serialize actual state: {e}")) - })? - } - _ => return Err(Status::unimplemented("Group resources not yet supported")), - }, - Err(e) => return Err(Status::internal(format!("DSC get operation failed: {e}"))), + let GetResult::Resource(result) = resource + .get(&identifiers) + .map_err(|e| Status::internal(format!("DSC get operation failed: {e}")))? + else { + return Err(Status::unimplemented("Group resources not supported")); }; Ok(Response::new(LocalExtensibilityOperationResponse { @@ -145,7 +128,7 @@ impl BicepExtension for BicepExtensionService { r#type: resource_type, api_version: version, identifiers: identifiers, - properties: result, + properties: result.actual_state.to_string(), status: None, }), error_data: None, From 177d0b04645bb97274d2e97d2290242af833267f Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:33:32 -0800 Subject: [PATCH 23/31] Clean up error handling --- dscbicep/src/main.rs | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 1685d963a..d9fd4c650 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -43,12 +43,12 @@ impl BicepExtension for BicepExtensionService { let mut dsc = DscManager::new(); let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { - return Err(Status::invalid_argument("Resource not found")); + return Err(Status::not_found("Resource not found")); }; let SetResult::Resource(result) = resource .set(&properties, false, &ExecutionKind::Actual) - .map_err(|e| Status::internal(format!("DSC set operation failed: {e}")))? + .map_err(|e| Status::aborted(e.to_string()))? else { return Err(Status::unimplemented("Group resources not supported")); }; @@ -78,12 +78,12 @@ impl BicepExtension for BicepExtensionService { let mut dsc = DscManager::new(); let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { - return Err(Status::invalid_argument("Resource not found")); + return Err(Status::not_found("Resource not found")); }; let SetResult::Resource(result) = resource .set(&properties, false, &ExecutionKind::WhatIf) - .map_err(|e| Status::internal(format!("DSC whatif operation failed: {e}")))? + .map_err(|e| Status::aborted(e.to_string()))? else { return Err(Status::unimplemented("Group resources not supported")); }; @@ -113,12 +113,12 @@ impl BicepExtension for BicepExtensionService { let mut dsc = DscManager::new(); let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { - return Err(Status::invalid_argument("Resource not found")); + return Err(Status::not_found("Resource not found")); }; let GetResult::Resource(result) = resource .get(&identifiers) - .map_err(|e| Status::internal(format!("DSC get operation failed: {e}")))? + .map_err(|e| Status::aborted(e.to_string()))? else { return Err(Status::unimplemented("Group resources not supported")); }; @@ -153,26 +153,19 @@ impl BicepExtension for BicepExtensionService { let mut dsc = DscManager::new(); let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { - return Err(Status::invalid_argument("Resource not found")); + return Err(Status::not_found("Resource not found")); }; - // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. - let result = match resource.delete(&identifiers) { - // Successful deletion returns () so we return an empty JSON object. - Ok(_) => "{}".to_string(), - Err(e) => { - return Err(Status::internal(format!( - "DSC delete operation failed: {e}" - ))) - } - }; + resource + .delete(&identifiers) + .map_err(|e| Status::aborted(e.to_string()))?; Ok(Response::new(LocalExtensibilityOperationResponse { resource: Some(proto::Resource { r#type: resource_type, api_version: version, identifiers: identifiers, - properties: result, + properties: "{}".to_string(), status: None, }), error_data: None, From fd310e6522b22cafb00482e7f91ff190bb672b76 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:24:40 -0800 Subject: [PATCH 24/31] Slight socket code clean up --- dscbicep/src/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index d9fd4c650..9639f59ed 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -10,7 +10,7 @@ use dsc_lib::{ }, DscManager, }; -use std::{env, fs, io, process}; +use std::{env, io, process}; use tonic::{transport::Server, Request, Response, Status}; // Include the generated protobuf code @@ -223,16 +223,15 @@ async fn run_server( use tokio::net::UnixListener; use tokio_stream::wrappers::UnixListenerStream; - tracing::info!("Starting Bicep gRPC server on Unix socket: {}", socket_path); + tracing::info!("Starting Bicep gRPC server on Unix socket: {socket_path}"); // Remove the socket file if it exists - let _ = fs::remove_file(&socket_path); + let _ = std::fs::remove_file(&socket_path); let uds = UnixListener::bind(&socket_path)?; let uds_stream = UnixListenerStream::new(uds); Server::builder() - // .add_service(reflection_service) .add_service(BicepExtensionServer::new(service)) .serve_with_incoming(uds_stream) .await?; From c7c1d06e38455846c6491164baf1578aca95347e Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:19:20 -0800 Subject: [PATCH 25/31] Hacky support for named pipes --- Cargo.lock | 23 ++++++++++ Cargo.toml | 2 + dscbicep/Cargo.toml | 3 +- dscbicep/src/main.rs | 107 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 130 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24095894a..db4075e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,28 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -917,6 +939,7 @@ dependencies = [ name = "dscbicep" version = "0.1.0" dependencies = [ + "async-stream", "clap", "dsc-lib", "prost", diff --git a/Cargo.toml b/Cargo.toml index f542a86c1..b896c316c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,8 @@ darling = { version = "0.23" } derive_builder = { version = "0.20" } # dsc, dsc-lib indicatif = { version = "0.18" } +# dscbicep +async-stream = { version = "0.3" } # dsc-lib-security_context::windows is_elevated = { version = "0.1" } # dsc, dsc-lib diff --git a/dscbicep/Cargo.toml b/dscbicep/Cargo.toml index ab4355eb8..2140e2900 100644 --- a/dscbicep/Cargo.toml +++ b/dscbicep/Cargo.toml @@ -10,9 +10,10 @@ path = "src/main.rs" [dependencies] dsc-lib = { workspace = true } +async-stream = { workspace = true } clap = { workspace = true } prost = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "io-std"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "io-std", "net"] } tokio-stream = { workspace = true, features = ["net"] } tonic = { workspace = true } tonic-prost = { workspace = true } diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 9639f59ed..36bcdc36b 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -241,11 +241,109 @@ async fn run_server( #[cfg(windows)] if let Some(pipe_name) = pipe { - tracing::info!("Starting Bicep gRPC server on named pipe: {}", pipe_name); + // TODO: This named pipe code is messy and honestly mostly generated. It + // does work, but most of the problem lies in minimal Windows support + // inside the Tokio library (and no support for UDS). + use std::pin::Pin; + use std::task::{Context, Poll}; + use tokio::io::{AsyncRead, AsyncWrite}; + use tokio::net::windows::named_pipe::ServerOptions; + use tonic::transport::server::Connected; + + // Wrapper to implement Connected trait for NamedPipeServer + struct NamedPipeConnection(tokio::net::windows::named_pipe::NamedPipeServer); + + impl Connected for NamedPipeConnection { + type ConnectInfo = (); + + fn connect_info(&self) -> Self::ConnectInfo { + () + } + } + + impl AsyncRead for NamedPipeConnection { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_read(cx, buf) + } + } + + impl AsyncWrite for NamedPipeConnection { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.0).poll_write(cx, buf) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_shutdown(cx) + } + } + + // Windows named pipes must be in the format \\.\pipe\{name} + let full_pipe_path = format!(r"\\.\pipe\{}", pipe_name); + tracing::info!("Starting Bicep gRPC server on named pipe: {full_pipe_path}"); + + // Create a stream that accepts connections on the named pipe + let incoming = async_stream::stream! { + // Track whether this is the first instance + let mut is_first = true; + + loop { + let pipe = if is_first { + ServerOptions::new() + .first_pipe_instance(true) + .create(&full_pipe_path) + } else { + ServerOptions::new() + .create(&full_pipe_path) + }; + + let server = match pipe { + Ok(server) => server, + Err(e) => { + tracing::error!("Failed to create named pipe: {}", e); + break; + } + }; + + is_first = false; + + tracing::debug!("Waiting for client to connect to named pipe..."); + match server.connect().await { + Ok(()) => { + tracing::info!("Client connected to named pipe"); + yield Ok::<_, std::io::Error>(NamedPipeConnection(server)); + } + Err(e) => { + tracing::error!("Failed to accept connection: {}", e); + break; + } + } + } + }; - // TODO: Implement Windows named pipe transport - // This requires additional dependencies and platform-specific code - return Err("Windows named pipe support not yet implemented".into()); + Server::builder() + .add_service(BicepExtensionServer::new(service)) + .serve_with_incoming(incoming) + .await?; + + return Ok(()); } // Default to HTTP server on [::1]:50051 if no transport specified @@ -287,6 +385,7 @@ async fn main() -> Result<(), Box> { .init(); let args = Args::parse(); + tracing::debug!("Args are {args:#?}"); if args.wait_for_debugger || env::var_os("DSC_GRPC_DEBUG").is_some_and(|v| v.eq_ignore_ascii_case("true")) From cd87700ea88220f0a2d04fd7f5319041cc6db7c5 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:55:31 -0800 Subject: [PATCH 26/31] Use tokio correctly by informing it we're about to block --- lib/dsc-lib/src/dscresources/command_resource.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index b899299d0..b255f8255 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -785,10 +785,8 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option // Try to use existing runtime first (e.g. from gRPC or MCP server) match tokio::runtime::Handle::try_current() { Ok(handle) => { - std::thread::scope(|s| { - s.spawn(|| { - handle.block_on(run_async) - }).join().unwrap() + tokio::task::block_in_place(|| { + handle.block_on(run_async) }) }, // Otherwise create a new runtime From 01629a6c22aeec81ad4194435e01a371aa6a2b31 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:06:55 -0800 Subject: [PATCH 27/31] Update to use DiscoveryFilter --- dscbicep/src/main.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 36bcdc36b..60fe37929 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -4,6 +4,7 @@ use clap::Parser; use dsc_lib::{ configure::config_doc::ExecutionKind, + discovery::discovery_trait::DiscoveryFilter, dscresources::{ dscresource::Invoke, invoke_result::{GetResult, SetResult}, @@ -42,7 +43,7 @@ impl BicepExtension for BicepExtensionService { tracing::debug!("CreateOrUpdate called for {resource_type}@{version:?}: {properties}"); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { return Err(Status::not_found("Resource not found")); }; @@ -77,7 +78,7 @@ impl BicepExtension for BicepExtensionService { tracing::debug!("Preview called for {resource_type}@{version:?}: {properties}"); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { return Err(Status::not_found("Resource not found")); }; @@ -112,7 +113,7 @@ impl BicepExtension for BicepExtensionService { tracing::debug!("Get called for {resource_type}@{version:?}: {identifiers}"); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { return Err(Status::not_found("Resource not found")); }; @@ -152,7 +153,7 @@ impl BicepExtension for BicepExtensionService { ); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { return Err(Status::not_found("Resource not found")); }; From aeed8832e358a8e0a580960ef1da7c94f4d2cd5a Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:24:00 -0800 Subject: [PATCH 28/31] Ensure Protobuf is available in build --- .pipelines/DSC-Official.yml | 3 ++- build.helpers.psm1 | 45 ++++++++++++++++++++++++++++++++++++- build.ps1 | 11 ++++----- dscbicep/build.rs | 2 -- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/.pipelines/DSC-Official.yml b/.pipelines/DSC-Official.yml index dd04a277c..6ed3e6035 100644 --- a/.pipelines/DSC-Official.yml +++ b/.pipelines/DSC-Official.yml @@ -321,7 +321,7 @@ extends: ob_restore_phase: true - pwsh: | apt update - apt -y install musl-tools rpm dpkg build-essential + apt -y install musl-tools rpm dpkg build-essential protobuf-compiler $header = "Bearer $(AzToken)" $env:CARGO_REGISTRIES_POWERSHELL_TOKEN = $header $env:CARGO_REGISTRIES_POWERSHELL_CREDENTIAL_PROVIDER = 'cargo:token' @@ -381,6 +381,7 @@ extends: apt -y install rpm apt -y install dpkg apt -y install build-essential + apt install -y protobuf-compiler msrustup default stable-aarch64-unknown-linux-musl if ((openssl version -d) -match 'OPENSSLDIR: "(?.*?)"') { $env:OPENSSL_LIB_DIR = $matches['dir'] diff --git a/build.helpers.psm1 b/build.helpers.psm1 index ab32cd4ec..58a07f678 100644 --- a/build.helpers.psm1 +++ b/build.helpers.psm1 @@ -611,7 +611,6 @@ function Install-NodeJS { } } elseif ($IsWindows) { if (Get-Command 'winget' -ErrorAction Ignore) { - Write-Warning "WHY WHAT IS HAPPENING HERE" Write-Verbose -Verbose "Using winget to install Node.js" winget install OpenJS.NodeJS --accept-source-agreements --accept-package-agreements --source winget --silent } else { @@ -627,6 +626,50 @@ function Install-NodeJS { } } +function Install-Protobuf { + <# + .SYNOPSIS + Installs Protobuf for the protoc executable. + #> + + [cmdletbinding()] + param() + + process { + if ((Get-Command 'protoc' -ErrorAction Ignore)) { + Write-Verbose "Protobuf already installed." + return + } + + Write-Verbose -Verbose "Protobuf not found, installing..." + if ($IsMacOS) { + if (Get-Command 'brew' -ErrorAction Ignore) { + brew install protobuf + } else { + Write-Warning "Homebrew not found, please install Protobuf manually" + } + } elseif ($IsWindows) { + if (Get-Command 'winget' -ErrorAction Ignore) { + Write-Verbose -Verbose "Using winget to install Protobuf" + winget install Google.Protobuf --accept-source-agreements --accept-package-agreements --source winget --silent + } else { + Write-Warning "winget not found, please install Protobuf manually" + } + } else { + if (Get-Command 'apt' -ErrorAction Ignore) { + Write-Verbose -Verbose "Using apt to install Protobuf" + sudo apt install -y protobuf-compiler + } else { + Write-Warning "apt not found, please install Protobuf manually" + } + } + + if ($LASTEXITCODE -ne 0) { + throw "Failed to install Protobuf" + } + } +} + function Install-PowerShellTestPrerequisite { [cmdletbinding()] param( diff --git a/build.ps1 b/build.ps1 index ffbc72ac5..9350f5a67 100755 --- a/build.ps1 +++ b/build.ps1 @@ -203,11 +203,12 @@ process { } if (-not ($SkipBuild -and $Test -and $ExcludeRustTests)) { - # Install Node if needed + Write-BuildProgress @progressParams -Status 'Ensuring Protobuf is available' + Install-Protobuf @VerboseParam + Write-BuildProgress @progressParams -Status 'Ensuring Node.JS is available' Install-NodeJS @VerboseParam - - # Ensure tree-sitter is installed + Write-BuildProgress @progressParams -Status 'Ensuring tree-sitter is available' Install-TreeSitter -UseCFS:$UseCFS @VerboseParam } @@ -259,7 +260,7 @@ process { if ($Test) { $progressParams.Activity = 'Testing projects' Write-BuildProgress @progressParams - + if (-not $ExcludeRustTests) { $rustTestParams = @{ Project = $BuildData.Projects @@ -319,4 +320,4 @@ clean { } Write-BuildProgress -Completed -} \ No newline at end of file +} diff --git a/dscbicep/build.rs b/dscbicep/build.rs index 0a7482414..ae8c430da 100644 --- a/dscbicep/build.rs +++ b/dscbicep/build.rs @@ -9,8 +9,6 @@ fn main() -> Result<(), Box> { tonic_prost_build::configure() .build_client(false) .file_descriptor_set_path(&descriptor_path) - // TODO: Configure and commit the out_dir to avoid dependency on protoc - // .out_dir(out_dir) .compile_protos(&["proto/bicep.proto"], &["proto"])?; Ok(()) } From d9d2b9eafb1016e9e270e844a806e7b89d4d8070 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:20:11 -0800 Subject: [PATCH 29/31] Use Test-CommandAvailable for protoc and node --- build.helpers.psm1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.helpers.psm1 b/build.helpers.psm1 index 58a07f678..5dad87bfe 100644 --- a/build.helpers.psm1 +++ b/build.helpers.psm1 @@ -393,7 +393,7 @@ function Install-Rust { param() process { - if ((Test-CommandAvailable -Name 'cargo')) { + if (Test-CommandAvailable -Name 'cargo') { Write-Verbose "Rust already installed" return } @@ -597,20 +597,20 @@ function Install-NodeJS { param() process { - if ((Get-Command 'node' -ErrorAction Ignore)) { + if (Test-CommandAvailable -Name 'node') { Write-Verbose "Node.js already installed." return } Write-Verbose -Verbose "Node.js not found, installing..." if ($IsMacOS) { - if (Get-Command 'brew' -ErrorAction Ignore) { + if (Test-CommandAvailable -Name 'brew') { brew install node@24 } else { Write-Warning "Homebrew not found, please install Node.js manually" } } elseif ($IsWindows) { - if (Get-Command 'winget' -ErrorAction Ignore) { + if (Test-CommandAvailable -Name 'winget') { Write-Verbose -Verbose "Using winget to install Node.js" winget install OpenJS.NodeJS --accept-source-agreements --accept-package-agreements --source winget --silent } else { @@ -636,27 +636,27 @@ function Install-Protobuf { param() process { - if ((Get-Command 'protoc' -ErrorAction Ignore)) { + if (Test-CommandAvailable -Name 'protoc') { Write-Verbose "Protobuf already installed." return } Write-Verbose -Verbose "Protobuf not found, installing..." if ($IsMacOS) { - if (Get-Command 'brew' -ErrorAction Ignore) { + if (Test-CommandAvailable -Name 'brew') { brew install protobuf } else { Write-Warning "Homebrew not found, please install Protobuf manually" } } elseif ($IsWindows) { - if (Get-Command 'winget' -ErrorAction Ignore) { + if (Test-CommandAvailable -Name 'winget') { Write-Verbose -Verbose "Using winget to install Protobuf" winget install Google.Protobuf --accept-source-agreements --accept-package-agreements --source winget --silent } else { Write-Warning "winget not found, please install Protobuf manually" } } else { - if (Get-Command 'apt' -ErrorAction Ignore) { + if (Test-CommandAvailable -Name 'apt') { Write-Verbose -Verbose "Using apt to install Protobuf" sudo apt install -y protobuf-compiler } else { From 254eed3eecfdb511767a3a11588133efb6aff976 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:29:49 -0800 Subject: [PATCH 30/31] Add WinGet links to PATH on Windows --- .github/workflows/rust.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 082b19003..e41df6b93 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -26,6 +26,10 @@ jobs: runs-on: ${{matrix.platform}} steps: - uses: actions/checkout@v5 + - name: Add WinGet links to PATH + if: matrix.platform == 'windows-latest' + run: | + "$env:LOCALAPPDATA\\Microsoft\\WinGet\\Links" | Out-File -Append -Encoding utf8 -FilePath $env:GITHUB_PATH - name: Install prerequisites run: ./build.ps1 -SkipBuild -Clippy -Verbose - name: Generate documentation @@ -129,6 +133,9 @@ jobs: windows-build: runs-on: windows-latest steps: + - name: Add WinGet links to PATH + run: | + "$env:LOCALAPPDATA\\Microsoft\\WinGet\\Links" | Out-File -Append -Encoding utf8 -FilePath $env:GITHUB_PATH - uses: actions/checkout@v5 - name: Install prerequisites run: ./build.ps1 -SkipBuild -Clippy -Verbose From 873c479f4a913792931ac1303510b925162e9b0d Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:10:45 -0800 Subject: [PATCH 31/31] Localize strings in dscbicep --- Cargo.toml | 2 +- dscbicep/Cargo.toml | 1 + dscbicep/locales/en-us.toml | 7 ++ dscbicep/src/main.rs | 173 +++++++++++++++++++++++++++--------- 4 files changed, 138 insertions(+), 45 deletions(-) create mode 100644 dscbicep/locales/en-us.toml diff --git a/Cargo.toml b/Cargo.toml index b896c316c..4aab9da8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -181,7 +181,7 @@ registry = { version = "1.3" } rmcp = { version = "0.12" } # dsc_lib rt-format = { version = "0.3" } -# dsc, dsc-lib, dscecho, registry, dsc-lib-registry, runcommandonset, sshdconfig +# dsc, dsc-lib, dscbicep, dscecho, registry, dsc-lib-registry, runcommandonset, sshdconfig rust-i18n = { version = "3.1" } # dsc, dsc-lib, dscecho, registry, dsc-lib-registry, sshdconfig, dsctest, test_group_resource schemars = { version = "1.2", features = ["preserve_order"] } diff --git a/dscbicep/Cargo.toml b/dscbicep/Cargo.toml index 2140e2900..529c48f87 100644 --- a/dscbicep/Cargo.toml +++ b/dscbicep/Cargo.toml @@ -13,6 +13,7 @@ dsc-lib = { workspace = true } async-stream = { workspace = true } clap = { workspace = true } prost = { workspace = true } +rust-i18n = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "io-std", "net"] } tokio-stream = { workspace = true, features = ["net"] } tonic = { workspace = true } diff --git a/dscbicep/locales/en-us.toml b/dscbicep/locales/en-us.toml new file mode 100644 index 000000000..37c07566e --- /dev/null +++ b/dscbicep/locales/en-us.toml @@ -0,0 +1,7 @@ +_version = 1 + +[dscbicep] +functionCalled = "%{function} called for %{resourceType}@%{version}: %{properties}" +starting = "Starting Bicep gRPC server on %{transport}: %{address}" +waitForDebugger = "Press any key to continue after attaching to PID: {pid}" +serverError = "Server error: %{error}" diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 60fe37929..cf0c9e3ca 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -11,6 +11,7 @@ use dsc_lib::{ }, DscManager, }; +use rust_i18n::{i18n, t}; use std::{env, io, process}; use tonic::{transport::Server, Request, Response, Status}; @@ -26,6 +27,8 @@ use proto::{ TypeFilesResponse, }; +i18n!("locales", fallback = "en-us"); + #[derive(Debug, Default)] pub struct BicepExtensionService; @@ -40,18 +43,38 @@ impl BicepExtension for BicepExtensionService { let version = spec.api_version; let properties = spec.properties; - tracing::debug!("CreateOrUpdate called for {resource_type}@{version:?}: {properties}"); + tracing::debug!( + "{}", + t!( + "dscbicep.functionCalled", + function = "CreateOrUpdate", + resourceType = resource_type, + version = format!("{version:?}"), + properties = properties + ) + ); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { - return Err(Status::not_found("Resource not found")); + let Some(resource) = dsc + .find_resource(&DiscoveryFilter::new( + &resource_type, + version.as_deref(), + None, + )) + .unwrap_or(None) + else { + return Err(Status::not_found( + t!("dscerror.resourceNotFound").to_string(), + )); }; let SetResult::Resource(result) = resource .set(&properties, false, &ExecutionKind::Actual) .map_err(|e| Status::aborted(e.to_string()))? else { - return Err(Status::unimplemented("Group resources not supported")); + return Err(Status::unimplemented( + t!("dscerror.notSupported").to_string(), + )); }; Ok(Response::new(LocalExtensibilityOperationResponse { @@ -75,18 +98,38 @@ impl BicepExtension for BicepExtensionService { let version = spec.api_version; let properties = spec.properties; - tracing::debug!("Preview called for {resource_type}@{version:?}: {properties}"); + tracing::debug!( + "{}", + t!( + "dscbicep.functionCalled", + function = "Preview", + resourceType = resource_type, + version = format!("{version:?}"), + properties = properties + ) + ); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { - return Err(Status::not_found("Resource not found")); + let Some(resource) = dsc + .find_resource(&DiscoveryFilter::new( + &resource_type, + version.as_deref(), + None, + )) + .unwrap_or(None) + else { + return Err(Status::not_found( + t!("dscerror.resourceNotFound").to_string(), + )); }; let SetResult::Resource(result) = resource .set(&properties, false, &ExecutionKind::WhatIf) .map_err(|e| Status::aborted(e.to_string()))? else { - return Err(Status::unimplemented("Group resources not supported")); + return Err(Status::unimplemented( + t!("dscerror.notSupported").to_string(), + )); }; Ok(Response::new(LocalExtensibilityOperationResponse { @@ -110,18 +153,38 @@ impl BicepExtension for BicepExtensionService { let version = reference.api_version.clone(); let identifiers = reference.identifiers.clone(); - tracing::debug!("Get called for {resource_type}@{version:?}: {identifiers}"); + tracing::debug!( + "{}", + t!( + "dscbicep.functionCalled", + function = "Get", + resourceType = resource_type, + version = format!("{version:?}"), + properties = identifiers + ) + ); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { - return Err(Status::not_found("Resource not found")); + let Some(resource) = dsc + .find_resource(&DiscoveryFilter::new( + &resource_type, + version.as_deref(), + None, + )) + .unwrap_or(None) + else { + return Err(Status::not_found( + t!("dscerror.resourceNotFound").to_string(), + )); }; let GetResult::Resource(result) = resource .get(&identifiers) .map_err(|e| Status::aborted(e.to_string()))? else { - return Err(Status::unimplemented("Group resources not supported")); + return Err(Status::unimplemented( + t!("dscerror.notSupported").to_string(), + )); }; Ok(Response::new(LocalExtensibilityOperationResponse { @@ -146,15 +209,28 @@ impl BicepExtension for BicepExtensionService { let identifiers = reference.identifiers.clone(); tracing::debug!( - "Delete called for {}@{:?}: {}", - resource_type, - version, - identifiers + "{}", + t!( + "dscbicep.functionCalled", + function = "Delete", + resourceType = resource_type, + version = format!("{version:?}"), + properties = identifiers + ) ); let mut dsc = DscManager::new(); - let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else { - return Err(Status::not_found("Resource not found")); + let Some(resource) = dsc + .find_resource(&DiscoveryFilter::new( + &resource_type, + version.as_deref(), + None, + )) + .unwrap_or(None) + else { + return Err(Status::not_found( + t!("dscerror.resourceNotFound").to_string(), + )); }; resource @@ -177,15 +253,13 @@ impl BicepExtension for BicepExtensionService { &self, _request: Request, ) -> Result, Status> { - tracing::debug!("GetTypeFiles called"); - - // TODO: Return actual Bicep type definitions...yet the extension already has these? - // Perhaps this is where we can dynamically get them from the current system. - Err(Status::unimplemented("GetTypeFiles not yet implemented")) + // TODO: Dynamically return type definitions for DSC resources found on the system. + Err(Status::unimplemented( + t!("dscerror.notImplemented").to_string(), + )) } async fn ping(&self, _request: Request) -> Result, Status> { - tracing::debug!("Ping called"); Ok(Response::new(Empty {})) } } @@ -224,7 +298,14 @@ async fn run_server( use tokio::net::UnixListener; use tokio_stream::wrappers::UnixListenerStream; - tracing::info!("Starting Bicep gRPC server on Unix socket: {socket_path}"); + tracing::info!( + "{}", + t!( + "dscbicep.starting", + transport = "socket", + address = socket_path + ) + ); // Remove the socket file if it exists let _ = std::fs::remove_file(&socket_path); @@ -298,7 +379,14 @@ async fn run_server( // Windows named pipes must be in the format \\.\pipe\{name} let full_pipe_path = format!(r"\\.\pipe\{}", pipe_name); - tracing::info!("Starting Bicep gRPC server on named pipe: {full_pipe_path}"); + tracing::info!( + "{}", + t!( + "dscbicep.starting", + transport = "named pipe", + address = full_pipe_path + ) + ); // Create a stream that accepts connections on the named pipe let incoming = async_stream::stream! { @@ -318,21 +406,19 @@ async fn run_server( let server = match pipe { Ok(server) => server, Err(e) => { - tracing::error!("Failed to create named pipe: {}", e); + tracing::error!("{}", t!("dscbicep.serverError", error = e.to_string())); break; } }; is_first = false; - tracing::debug!("Waiting for client to connect to named pipe..."); match server.connect().await { Ok(()) => { - tracing::info!("Client connected to named pipe"); yield Ok::<_, std::io::Error>(NamedPipeConnection(server)); } Err(e) => { - tracing::error!("Failed to accept connection: {}", e); + tracing::error!("{}", t!("dscbicep.serverError", error = e.to_string())); break; } } @@ -348,8 +434,15 @@ async fn run_server( } // Default to HTTP server on [::1]:50051 if no transport specified - let addr = http.unwrap_or_else(|| "[::1]:50051".to_string()).parse()?; - tracing::info!("Starting Bicep gRPC server on HTTP: {addr}"); + let addr = http.unwrap_or_else(|| "[::1]:50051".to_string()); + tracing::info!( + "{}", + t!( + "dscbicep.starting", + transport = "HTTP", + address = addr.to_string() + ) + ); let reflection_service = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET) @@ -359,7 +452,7 @@ async fn run_server( Server::builder() .add_service(reflection_service) .add_service(BicepExtensionServer::new(service)) - .serve(addr) + .serve(addr.parse()?) .await?; Ok(()) @@ -386,36 +479,28 @@ async fn main() -> Result<(), Box> { .init(); let args = Args::parse(); - tracing::debug!("Args are {args:#?}"); if args.wait_for_debugger || env::var_os("DSC_GRPC_DEBUG").is_some_and(|v| v.eq_ignore_ascii_case("true")) { - tracing::warn!( - "Press any key to continue after attaching to PID: {}", - process::id() - ); + tracing::warn!("{}", t!("dscbicep.waitForDebugger", pid = process::id())); let mut input = String::new(); io::stdin().read_line(&mut input)?; } // Set up graceful shutdown on SIGTERM/SIGINT let shutdown_signal = async { - tokio::signal::ctrl_c() - .await - .expect("Failed to listen for shutdown signal"); - tracing::info!("Received shutdown signal, terminating gracefully..."); + tokio::signal::ctrl_c().await.unwrap(); }; tokio::select! { result = run_server(args.socket, args.pipe, args.http) => { if let Err(e) = result { - tracing::error!("Server error: {e}"); + tracing::error!("{}", t!("dscbicep.serverError", error = e.to_string())); return Err(e); } } _ = shutdown_signal => { - tracing::info!("Shutdown complete"); } }