From 8478a09b3e0e63f554d6921b249fcb45865954aa 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/26] 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 949df819943c1ebefcf178d3f2996a23c22e1ffb 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/26] 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 66c4d4d879b4609a6987f018e9a9b504d5be265a 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/26] 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 87ea3e5f3fab2ed8cf24ef92a9ed0551181e7d13 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/26] 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 2f8011dcb..75551577e 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" @@ -682,6 +731,7 @@ dependencies = [ "indicatif", "jsonschema", "path-absolutize", + "prost", "regex", "rmcp", "rust-i18n", @@ -695,6 +745,9 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", + "tonic", + "tonic-prost", + "tonic-prost-build", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -972,6 +1025,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +[[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" @@ -1186,6 +1245,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" @@ -1243,6 +1321,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" @@ -1253,9 +1337,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1281,6 +1367,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" @@ -1637,12 +1736,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" @@ -1665,6 +1776,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" @@ -2074,6 +2191,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" @@ -2139,6 +2286,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" @@ -2148,6 +2305,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" @@ -3135,6 +3366,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" @@ -3189,6 +3431,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" @@ -3197,11 +3507,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3393,6 +3707,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 106a4945b..25caf234e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,6 +157,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 @@ -195,6 +197,10 @@ thiserror = { version = "2.0" } tokio = { version = "1.48" } # 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 @@ -223,6 +229,8 @@ ipnetwork = { version = "0.21" } # 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 432acba92..85733e1d8 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 39c9c44ac625ed706c190f727ff699f7bf3b1b55 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/26] 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 de1cb2970dad9e8e0f511badd210e2c64e27ad37 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/26] 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 | 0 dsc/src/bicep/mod.rs => dscbicep/src/main.rs | 104 +++++++++++++++--- 10 files changed, 137 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 (100%) rename dsc/src/bicep/mod.rs => dscbicep/src/main.rs (51%) diff --git a/Cargo.lock b/Cargo.lock index 75551577e..a42522e6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,7 +731,6 @@ dependencies = [ "indicatif", "jsonschema", "path-absolutize", - "prost", "regex", "rmcp", "rust-i18n", @@ -745,9 +744,6 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", - "tonic", - "tonic-prost", - "tonic-prost-build", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -943,6 +939,21 @@ dependencies = [ "tree-sitter-ssh-server-config", ] +[[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 25caf234e..990b57c68 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", @@ -28,6 +29,7 @@ members = [ # avoid unintentionally modifying this value. default-members = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -54,6 +56,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", @@ -75,6 +78,7 @@ Windows = [ ] macOS = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -93,6 +97,7 @@ macOS = [ ] Linux = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -195,6 +200,8 @@ tempfile = { version = "3.23" } thiserror = { version = "2.0" } # dsc, dsc-lib tokio = { version = "1.48" } +# dscbicep +tokio-stream = { version = "0.1" } # dsc tokio-util = { version = "0.7" } # dsc diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index 85733e1d8..432acba92 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 100% rename from dsc/src/bicep/bicep.proto rename to dscbicep/proto/bicep.proto 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 47404d61f893f6ba176d9e4e71557b808f7d1946 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/26] 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 bbff6e7b187b8ddf494718fe5f1d45ce22afd096 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/26] 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 a42522e6e..5931a2ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -944,6 +944,7 @@ name = "dscbicep" version = "0.1.0" dependencies = [ "clap", + "dsc-lib", "prost", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 990b57c68..7d620cd2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -237,7 +237,7 @@ ipnetwork = { version = "0.21" } # 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 1b0d53af0b210953668b137d8fbef57b51cd20bf 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/26] 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 9c09eb6a51fe09fc4cb0f5d3e505e559fa04d1da 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/26] 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 8d7001c03..7bf3b3c83 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 c931e1382b57e86994b9594961e492824728baae 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/26] 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 84225086b9346ad3c62fded656d616bc70a05b79 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/26] 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 68602c18427425bc8787166b13d0fa2eea48c60b 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/26] 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 79df4590449a0bfc40101685af3ad3e06033d262 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/26] 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 53685ccc89de98005695e486d3823f0484023dd1 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/26] 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 f9b7325f80075c55044bc4ce90696a533d21acbd 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/26] 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 5931a2ad5..5ada9b274 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -951,6 +951,7 @@ dependencies = [ "tonic", "tonic-prost", "tonic-prost-build", + "tonic-reflection", "tracing", "tracing-subscriber", ] @@ -3511,6 +3512,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 7d620cd2c..cb3128de7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,7 +132,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" } @@ -162,7 +162,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" } @@ -198,21 +198,23 @@ sysinfo = { version = "0.37" } tempfile = { version = "3.23" } # dsc, dsc-lib, registry, dsc-lib-registry, sshdconfig thiserror = { version = "2.0" } -# dsc, dsc-lib +# dsc, dsc-lib, dscbicep tokio = { version = "1.48" } # 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.25" } 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 fa9b8f4501ff26cbe0e6bc3e0586fbe96af8d3af 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/26] 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 51e32a60d4bce6a3ad2fd1a9c03d93b1edf6decc 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/26] 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 d81ef65800717b70558746afad38b182d6b8d6b5 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/26] 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 4cf735fd68befbe64d50de77d8b866ea0d74bfaa 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/26] 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 cd964397328a0ff2a3bcaa1abc693beb443eeaac 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/26] 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 edac41a3e8010cff3af32d9d31479d818f6ae244 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/26] Simplify and remove direct serde dependency --- dscbicep/src/main.rs | 60 ++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index bfbbcb61b..9b75b34c8 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, @@ -128,16 +117,11 @@ 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) => 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 +129,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 c3a918b50f7ab3e0285160b679ce3cc043c6ec0f 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/26] Clean up error handling --- dscbicep/src/main.rs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs index 9b75b34c8..c9b5d08f0 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,13 +113,13 @@ 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 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")); }; @@ -154,26 +154,20 @@ 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 5c2f2ab522253dd88e6fb001917303b872c0487d Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:29:23 -0800 Subject: [PATCH 24/26] Add CWD to resource paths Not just exe_home. --- lib/dsc-lib/src/discovery/command_discovery.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 8479b8cb6..1234fe080 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -185,6 +185,20 @@ impl CommandDiscovery { } } } + + // if current working directory is not already in PATH env var then add it to env var and list of searched paths + if let Ok(cwd) = env::current_dir() { + if paths.contains(&cwd) { + trace!("Current working directory already in path: {}", cwd.to_string_lossy()); + } else { + trace!("Adding current working directory to path: {}", cwd.to_string_lossy()); + paths.push(cwd); + + if let Ok(new_path) = env::join_paths(paths.clone()) { + env::set_var("PATH", new_path); + } + } + } } if let Ok(final_resource_path) = env::join_paths(paths.clone()) { From a1fd5f15dd1f04b3934e9466dbcd56e4bd103f31 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 25/26] 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 c9b5d08f0..66ecfe347 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 @@ -225,16 +225,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 5fdbbabcf262fc3240b54c6a6cffee393dee260d 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 26/26] 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 5ada9b274..440ad54c4 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" @@ -943,6 +965,7 @@ dependencies = [ name = "dscbicep" version = "0.1.0" dependencies = [ + "async-stream", "clap", "dsc-lib", "prost", diff --git a/Cargo.toml b/Cargo.toml index cb3128de7..dbebbc33d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,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 66ecfe347..726dc6e13 100644 --- a/dscbicep/src/main.rs +++ b/dscbicep/src/main.rs @@ -243,11 +243,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 @@ -289,6 +387,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"))