diff --git a/.github/DockerfileBackendTests b/.github/DockerfileBackendTests index e20d023bdc8a1..979d2112dc0b7 100644 --- a/.github/DockerfileBackendTests +++ b/.github/DockerfileBackendTests @@ -52,4 +52,6 @@ RUN unzip deno.zip && rm deno.zip && mv deno /usr/bin/deno RUN apt-get update \ && apt-get install -y postgresql-client --allow-unauthenticated -RUN rustup component add rustfmt \ No newline at end of file +RUN rustup component add rustfmt +COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk +RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet diff --git a/.github/workflows/build-publish-rh-image.yml b/.github/workflows/build-publish-rh-image.yml index eb904758f056d..67b84f66acb4f 100644 --- a/.github/workflows/build-publish-rh-image.yml +++ b/.github/workflows/build-publish-rh-image.yml @@ -64,7 +64,7 @@ jobs: platforms: linux/amd64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql,csharp secrets: | rh_username=${{ secrets.RH_USERNAME }} rh_password=${{ secrets.RH_PASSWORD }} @@ -81,7 +81,7 @@ jobs: platforms: linux/arm64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql,csharp secrets: | rh_username=${{ secrets.RH_USERNAME }} rh_password=${{ secrets.RH_PASSWORD }} diff --git a/.github/workflows/build-staging-image.yml b/.github/workflows/build-staging-image.yml index 8f54411c1ad66..cbb6d77572d02 100644 --- a/.github/workflows/build-staging-image.yml +++ b/.github/workflows/build-staging-image.yml @@ -62,7 +62,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql,csharp tags: | ${{ steps.meta-ee-public.outputs.tags }} labels: | diff --git a/.github/workflows/build_windows_worker_.yml b/.github/workflows/build_windows_worker_.yml index d093f99f01c08..0fa179eaeb3c8 100644 --- a/.github/workflows/build_windows_worker_.yml +++ b/.github/workflows/build_windows_worker_.yml @@ -45,7 +45,7 @@ jobs: $env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static" mkdir frontend/build && cd backend New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force - cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,php,mysql + cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,php,mysql,csharp - name: Rename binary with corresponding architecture run: | diff --git a/.github/workflows/docker-image-rpi4.yml b/.github/workflows/docker-image-rpi4.yml index f7e8358a1738b..8e8f43b118549 100644 --- a/.github/workflows/docker-image-rpi4.yml +++ b/.github/workflows/docker-image-rpi4.yml @@ -67,7 +67,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true build-args: | - features=embedding,parquet,openidconnect,deno_core,php,mysql + features=embedding,parquet,openidconnect,deno_core,php,mysql,csharp tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev ${{ steps.meta-public.outputs.tags }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 69bc432e68b47..a85aaac967a3b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -76,7 +76,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true build-args: | - features=embedding,parquet,openidconnect,jemalloc,deno_core,dind,php,mysql + features=embedding,parquet,openidconnect,jemalloc,deno_core,dind,php,mysql,csharp tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }} ${{ steps.meta-public.outputs.tags }} @@ -138,7 +138,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind,php,mysql + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind,php,mysql,csharp tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }} ${{ steps.meta-ee-public.outputs.tags }} @@ -200,7 +200,7 @@ jobs: platforms: linux/amd64 push: true build-args: | - features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind,php,mysql + features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind,php,mysql,csharp PYTHON_IMAGE=python:3.12.2-slim-bookworm tags: | ${{ steps.meta-ee-public-py312.outputs.tags }} diff --git a/.github/workflows/publish_windows_worker.yml b/.github/workflows/publish_windows_worker.yml index 50aff4fc19686..67d3d3f0bbfb0 100644 --- a/.github/workflows/publish_windows_worker.yml +++ b/.github/workflows/publish_windows_worker.yml @@ -47,7 +47,7 @@ jobs: $env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static" mkdir frontend/build && cd backend New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force - cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,php,mysql + cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,php,mysql,csharp - name: Rename binary with corresponding architecture run: | diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 0cbf2534487b0..d20737ba51491 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -10024,6 +10024,34 @@ dependencies = [ "tracing-serde 0.2.0", ] +[[package]] +name = "tree-sitter" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca" +dependencies = [ + "cc", + "regex", + "regex-syntax 0.8.5", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c0f6d2209a3cd6d0bb9d2934715da15a15710d3c09c7c1ecd4c9804c3ecd10" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600" + [[package]] name = "triomphe" version = "0.1.14" @@ -11037,6 +11065,18 @@ dependencies = [ "windmill-parser", ] +[[package]] +name = "windmill-parser-csharp" +version = "1.437.1" +dependencies = [ + "anyhow", + "serde_json", + "tree-sitter", + "tree-sitter-c-sharp", + "wasm-bindgen", + "windmill-parser", +] + [[package]] name = "windmill-parser-go" version = "1.437.1" @@ -11159,6 +11199,7 @@ dependencies = [ "wasm-bindgen-test", "windmill-parser", "windmill-parser-bash", + "windmill-parser-csharp", "windmill-parser-go", "windmill-parser-graphql", "windmill-parser-php", @@ -11290,6 +11331,7 @@ dependencies = [ "windmill-git-sync", "windmill-parser", "windmill-parser-bash", + "windmill-parser-csharp", "windmill-parser-go", "windmill-parser-graphql", "windmill-parser-php", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 891954337974d..7b68ba9f62426 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -21,6 +21,7 @@ members = [ "./parsers/windmill-parser-wasm", "./parsers/windmill-parser-go", "./parsers/windmill-parser-rust", + "./parsers/windmill-parser-csharp", "./parsers/windmill-parser-bash", "./parsers/windmill-parser-py", "./parsers/windmill-parser-py-imports", @@ -67,6 +68,7 @@ otel = ["windmill-common/otel", "windmill-worker/otel"] dind = ["windmill-worker/dind"] php = ["windmill-worker/php"] mysql = ["windmill-worker/mysql"] +csharp = ["windmill-worker/csharp"] [dependencies] anyhow.workspace = true @@ -131,6 +133,7 @@ windmill-parser-py-imports = { path = "./parsers/windmill-parser-py-imports" } windmill-parser-go = { path = "./parsers/windmill-parser-go" } windmill-parser-rust = { path = "./parsers/windmill-parser-rust" } windmill-parser-yaml = { path = "./parsers/windmill-parser-yaml" } +windmill-parser-csharp = { path = "./parsers/windmill-parser-csharp" } windmill-parser-bash = { path = "./parsers/windmill-parser-bash" } windmill-parser-sql = { path = "./parsers/windmill-parser-sql" } windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" } @@ -314,3 +317,5 @@ quote = "1.0.36" regex-lite = "0.1.6" yaml-rust = "0.4.5" tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] } +tree-sitter = {version = "0.23.0", features = []} +tree-sitter-c-sharp = "0.23.0" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index aad90fa017135..e545b436cc864 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -dddc8d60d483a2ce8d78233a25a3899a2b1224ca \ No newline at end of file +dddc8d60d483a2ce8d78233a25a3899a2b1224ca diff --git a/backend/migrations/20241029132207_add-csharp-support.down.sql b/backend/migrations/20241029132207_add-csharp-support.down.sql new file mode 100644 index 0000000000000..d2f607c5b8bd6 --- /dev/null +++ b/backend/migrations/20241029132207_add-csharp-support.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20241029132207_add-csharp-support.up.sql b/backend/migrations/20241029132207_add-csharp-support.up.sql new file mode 100644 index 0000000000000..128cae9e77ab3 --- /dev/null +++ b/backend/migrations/20241029132207_add-csharp-support.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'csharp'; +UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["csharp"]'::jsonb) where name = 'worker__default' and config @> '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "php", "rust", "ansible"]}'::jsonb AND NOT config->'worker_tags' @> '"csharp"'::jsonb; diff --git a/backend/parsers/windmill-parser-csharp/Cargo.toml b/backend/parsers/windmill-parser-csharp/Cargo.toml new file mode 100644 index 0000000000000..07b025922470d --- /dev/null +++ b/backend/parsers/windmill-parser-csharp/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "windmill-parser-csharp" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[lib] +name = "windmill_parser_csharp" +path = "./src/lib.rs" + +[dependencies] +windmill-parser.workspace = true +tree-sitter.workspace = true +tree-sitter-c-sharp.workspace = true +anyhow.workspace = true +wasm-bindgen.workspace = true +serde_json.workspace = true +# convert_case.workspace = true +# lazy_static.workspace = true +# regex.workspace = true + diff --git a/backend/parsers/windmill-parser-csharp/src/lib.rs b/backend/parsers/windmill-parser-csharp/src/lib.rs new file mode 100644 index 0000000000000..0a74273a6cba0 --- /dev/null +++ b/backend/parsers/windmill-parser-csharp/src/lib.rs @@ -0,0 +1,314 @@ +#![cfg_attr(target_arch = "wasm32", feature(c_variadic))] + +#[cfg(target_arch = "wasm32")] +pub mod wasm_libc; + +use anyhow::anyhow; +use tree_sitter::Node; +use windmill_parser::Arg; +use windmill_parser::MainArgSignature; +use windmill_parser::Typ; + +#[derive(Debug)] +pub struct CsharpMainSigMeta { + pub is_async: bool, + pub is_public: bool, + pub returns_void: bool, + pub class_name: Option, + pub main_sig: MainArgSignature, +} + +fn csharp_param_default_value<'a>(def: Node<'a>, code: &str) -> Option { + def.utf8_text(code.as_bytes()) + .ok() + .and_then(|content| serde_json::from_str(content).ok()) +} + +pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result { + let mut parser = tree_sitter::Parser::new(); + let language = tree_sitter_c_sharp::LANGUAGE; + parser + .set_language(&language.into()) + .map_err(|e| anyhow!("Error setting c# as language: {e}"))?; + + // Parse code + let tree = parser.parse(code, None).expect("Failed to parse code"); + let root_node = tree.root_node(); + + // Traverse the AST to find the Main method signature + let main_sig = find_main_signature(root_node, code); + let no_main_func = Some(main_sig.is_none()); + let mut is_async = false; + let mut is_public = false; + let mut returns_void = false; + let mut class_name = None; + + let mut args = vec![]; + if let Some((sig, name)) = main_sig { + class_name = name; + for sig_node in sig.children(&mut sig.walk()) { + if sig_node.kind() == "modifier" && sig_node.utf8_text(code.as_bytes())? == "async" { + is_async = true; + } + if sig_node.kind() == "modifier" && sig_node.utf8_text(code.as_bytes())? == "public" { + is_public = true; + } + } + if let Some(return_type) = sig.child_by_field_name("returns") { + let return_type = return_type.utf8_text(code.as_bytes())?; + + if return_type == "void" || (is_async && return_type == "Task") { + returns_void = true; + } + } + if let Some(param_list) = sig.child_by_field_name("parameters") { + for p_list_node in param_list.children(&mut param_list.walk()) { + if p_list_node.kind() == "parameter" { + let mut default = None; + for a in p_list_node.children(&mut p_list_node.walk()) { + if a.kind() == "=" { + if let Some(node) = a.next_sibling() { + default = csharp_param_default_value(node, code); + } + } + } + let (otyp, typ, name) = parse_csharp_typ(p_list_node, code)?; + args.push(Arg { name, otyp, typ, default, has_default: false, oidx: None }); + } + } + } + } + + let main_sig = MainArgSignature { + star_args: false, + star_kwargs: false, + args, + has_preprocessor: None, + no_main_func, + }; + + Ok(CsharpMainSigMeta { is_async, returns_void, class_name, main_sig, is_public }) +} + +pub fn parse_csharp_signature(code: &str) -> anyhow::Result { + Ok(parse_csharp_sig_meta(code)?.main_sig) +} + +fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result { + match typ_node.kind() { + "predefined_type" => { + match typ_node.utf8_text(code.as_bytes()) { + Ok("string") => Ok(Typ::Str(None)), + Ok("sbyte") | Ok("System.SByte") => Ok(Typ::Bytes), + Ok("byte") | Ok("System.Byte") => Ok(Typ::Bytes), + Ok("short") | Ok("System.Int16") => Ok(Typ::Int), + Ok("ushort") | Ok("System.UInt16") => Ok(Typ::Int), + Ok("int") | Ok("System.Int32") => Ok(Typ::Int), + Ok("uint") | Ok("System.UInt32") => Ok(Typ::Int), + Ok("long") | Ok("System.Int64") => Ok(Typ::Int), + Ok("ulong") | Ok("System.UInt64") => Ok(Typ::Int), + Ok("char") | Ok("System.Char") => Ok(Typ::Str(None)), + Ok("float") | Ok("System.Single") => Ok(Typ::Float), + Ok("double") | Ok("System.Double") => Ok(Typ::Float), + Ok("bool") | Ok("System.Boolean") => Ok(Typ::Bool), + Ok("decimal") | Ok("System.Decimal") => Ok(Typ::Float), + Ok("object") => Ok(Typ::Object(vec![])), // TODO: Complete the object type + Ok(s) => Err(anyhow!("Unknown type `{s}`")), + Err(e) => Err(anyhow!("Error getting type name: {}", e)), + } + } + "array_type" => { + let new_typ_node = typ_node + .child_by_field_name("type") + .ok_or(anyhow!("Failed to find inner type of array type"))?; + Ok(Typ::List(Box::new(find_typ(new_typ_node, code)?))) + } + "identifier" => Ok(Typ::Unknown), + "generic_name" => Ok(Typ::Unknown), + "pointer_type" => Ok(Typ::Int), + "nullable_type" => { + let new_typ_node = typ_node + .child_by_field_name("type") + .ok_or(anyhow!("Failed to find inner type of nullable_type"))?; + Ok(find_typ(new_typ_node, code)?) + } + wc => Err(anyhow!( + "Unexpected C# type node kind: {} for '{}'. This type is not handeled by Windmill, please open an issue if this seems to be an error", + wc, + typ_node.utf8_text(code.as_bytes())? + )), + } +} + +fn parse_csharp_typ<'a>( + param_node: Node<'a>, + code: &str, +) -> anyhow::Result<(Option, Typ, String)> { + let name = param_node + .child_by_field_name("name") + .and_then(|n| n.utf8_text(code.as_bytes()).ok()) + .unwrap_or(""); + let otyp_node = param_node.child_by_field_name("type"); + let otyp = otyp_node + .and_then(|n| n.utf8_text(code.as_bytes()).ok()) + .map(|s| s.to_string()); + + let typ = find_typ(otyp_node.unwrap(), code)?; + + Ok((otyp, typ, name.to_string())) +} + +// Function to find the Main method's signature +fn find_main_signature<'a>(root_node: Node<'a>, code: &str) -> Option<(Node<'a>, Option)> { + let mut cursor = root_node.walk(); + for x in root_node.children(&mut cursor) { + if x.kind() == "class_declaration" { + let class_name = x + .child_by_field_name("name") + .and_then(|n| n.utf8_text(code.as_bytes()).ok().map(|s| s.to_string())); + for c in x.children(&mut x.walk()) { + if c.kind() == "declaration_list" { + for w in c.children(&mut c.walk()) { + if w.kind() == "method_declaration" { + for child in w.children(&mut w.walk()) { + if child + .utf8_text(code.as_bytes()) + .map(|name| name == "Main") + .unwrap_or(false) + { + return Some((w, class_name)); + } + } + } + } + } + } + } + } + return None; +} + +pub fn parse_csharp_reqs(code: &str) -> (Vec<(String, Option)>, Vec) { + let mut nuget_reqs = Vec::new(); + let mut pkg_lines = Vec::new(); + + for (i, line) in code.split("\n").enumerate() { + if line.starts_with('#') { + if let Some(req) = parse_nuget_req(&line) { + pkg_lines.push(i); + nuget_reqs.push(req); + } + } else { + break; // Stop processing after the first non-comment line + } + } + + (nuget_reqs, pkg_lines) +} + +fn parse_nuget_req(line: &str) -> Option<(String, Option)> { + // Check if the line starts with `#r "nuget:` + if let Some(start) = line.find("#r \"nuget:") { + // Extract the content after `#r "nuget:` + let start_idx = start + 10; + let end_idx = line[start_idx..].find('"')?; + let line = &line[start_idx..start_idx + end_idx]; + let mut splitted = line.split(","); + if let Some(pkg) = splitted.next() { + return Some(( + pkg.trim().to_string(), + splitted.next().map(|s| s.trim().to_string()), + )); + } + } + None +} + +#[cfg(test)] +mod test { + + use super::*; + #[test] + fn test_parse_csharp_sig_and_meta() { + let code = r#" +using System; + +class LilProgram +{ + + public async static string Main(string myString = "World", int myInt = 2, string[] jj = ["asd", "ss"]) + { + Console.Writeline("Hello!!"); + return "yeah"; + } + +}"#; + let sig_meta = parse_csharp_sig_meta(code).unwrap(); + + assert_eq!(sig_meta.class_name, Some("LilProgram".to_string())); + assert_eq!(sig_meta.is_async, true); + + let ret = sig_meta.main_sig; + assert_eq!(ret.args.len(), 3); + } + + #[test] + fn test_parse_csharp_sig() { + let code = r#" +using System; + +class LilProgram +{ + + public static string Main(string myString = "World", int myInt, string[] jj) + { + Console.Writeline("Hello!!"); + return "yeah"; + } + +}"#; + let ret = parse_csharp_signature(code).unwrap(); + + assert_eq!(ret.args.len(), 3); + + assert_eq!(ret.args[0].name, "myString"); + assert_eq!(ret.args[0].otyp, Some("string".to_string())); + assert_eq!(ret.args[0].typ, Typ::Str(None)); + + assert_eq!(ret.args[1].name, "myInt"); + assert_eq!(ret.args[1].otyp, Some("int".to_string())); + assert_eq!(ret.args[1].typ, Typ::Int); + + assert_eq!(ret.args[2].name, "jj"); + assert_eq!(ret.args[2].otyp, Some("string[]".to_string())); + assert_eq!(ret.args[2].typ, Typ::List(Box::new(Typ::Str(None)))); + } + + #[test] + fn test_parse_csharp_reqs() { + let file_content = r#"#r "nuget: AutoMapper, 6.1.0" +#r "nuget: Newtonsoft.Json, 13.0.1" +#r "nuget: Serilog, 2.10.0" + +#r "nuget: Serilog, 2.10.0" + +using System; +"#; + + let requirements = parse_csharp_reqs(file_content).0; + + assert_eq!(requirements.len(), 3); + assert_eq!( + requirements[0], + ("AutoMapper".to_string(), Some("6.1.0".to_string())) + ); + assert_eq!( + requirements[1], + ("Newtonsoft.Json".to_string(), Some("13.0.1".to_string())) + ); + assert_eq!( + requirements[2], + ("Serilog".to_string(), Some("2.10.0".to_string())) + ); + } +} diff --git a/backend/parsers/windmill-parser-csharp/src/wasm_libc.rs b/backend/parsers/windmill-parser-csharp/src/wasm_libc.rs new file mode 100644 index 0000000000000..bb703fb1310ec --- /dev/null +++ b/backend/parsers/windmill-parser-csharp/src/wasm_libc.rs @@ -0,0 +1,208 @@ +use std::{ + alloc::{self, Layout}, + ffi::{c_char, c_int, c_void}, + mem::align_of, + ptr, +}; +use std::collections::BTreeMap; +use std::sync::{Mutex, OnceLock}; +use wasm_bindgen::prelude::*; + +/* -------------------------------- stdlib.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn abort() { + panic!("Aborted from C"); +} + +macro_rules! console_log { + ($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string()) }) +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(a: &str); +} + +#[no_mangle] +pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void { + if size == 0 { + return ptr::null_mut(); + } + + let (layout, offset_to_data) = layout_for_size_prepended(size); + let buf = alloc::alloc(layout); + store_layout(buf, layout, offset_to_data) +} + +#[no_mangle] +pub unsafe extern "C" fn calloc(count: usize, size: usize) -> *mut c_void { + if count == 0 || size == 0 { + return ptr::null_mut(); + } + + let (layout, offset_to_data) = layout_for_size_prepended(size * count); + let buf = alloc::alloc_zeroed(layout); + store_layout(buf, layout, offset_to_data) +} + +#[no_mangle] +pub unsafe extern "C" fn realloc(buf: *mut c_void, new_size: usize) -> *mut c_void { + if buf.is_null() { + malloc(new_size) + } else if new_size == 0 { + free(buf); + ptr::null_mut() + } else { + let (old_buf, old_layout) = retrieve_layout(buf); + let (new_layout, offset_to_data) = layout_for_size_prepended(new_size); + let new_buf = alloc::realloc(old_buf, old_layout, new_layout.size()); + store_layout(new_buf, new_layout, offset_to_data) + } +} + +#[no_mangle] +pub unsafe extern "C" fn free(buf: *mut c_void) { + if buf.is_null() { + return; + } + let (buf, layout) = retrieve_layout(buf); + alloc::dealloc(buf, layout); +} + + +// In all these allocations, we store the layout before the data for later retrieval. +// This is because we need to know the layout when deallocating the memory. +// Here are some helper methods for that: + +/// Given a pointer to the data, retrieve the layout and the pointer to the layout. +unsafe fn retrieve_layout(buf: *mut c_void) -> (*mut u8, Layout) { + let (_, layout_offset) = Layout::new::() + .extend(Layout::from_size_align(0, align_of::<*const u8>() * 2).unwrap()) + .unwrap(); + + let buf = (buf as *mut u8).offset(-(layout_offset as isize)); + let layout = *(buf as *mut Layout); + + (buf, layout) +} + +/// Calculate a layout for a given size with space for storing a layout at the start. +/// Returns the layout and the offset to the data. +fn layout_for_size_prepended(size: usize) -> (Layout, usize) { + Layout::new::() + .extend(Layout::from_size_align(size, align_of::<*const u8>() * 2).unwrap()) + .unwrap() +} + +/// Store a layout in the pointer, returning a pointer to where the data should be stored. +unsafe fn store_layout(buf: *mut u8, layout: Layout, offset_to_data: usize) -> *mut c_void { + *(buf as *mut Layout) = layout; + (buf as *mut u8).offset(offset_to_data as isize) as *mut c_void +} + +/* -------------------------------- string.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn strncmp(ptr1: *const c_void, ptr2: *const c_void, n: usize) -> c_int { + let s1 = std::slice::from_raw_parts(ptr1 as *const u8, n); + let s2 = std::slice::from_raw_parts(ptr2 as *const u8, n); + + for (a, b) in s1.iter().zip(s2.iter()) { + if *a != *b || *a == 0 { + return (*a as i32) - (*b as i32); + } + } + + 0 +} + +/* -------------------------------- wctype.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn iswspace(c: c_int) -> bool { + char::from_u32(c as u32).map_or(false, |c| c.is_whitespace()) +} + +#[no_mangle] +pub unsafe extern "C" fn iswalnum(c: c_int) -> bool { + char::from_u32(c as u32).map_or(false, |c| c.is_alphanumeric()) +} + +/* --------------------------------- time.h --------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn clock() -> u64 { + panic!("clock is not supported"); +} + +/* --------------------------------- ctype.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn isprint(c: c_int) -> bool { + c >= 32 && c <= 126 +} + +/* --------------------------------- stdio.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn fprintf(_file: *mut c_void, _format: *const c_void, _args: ...) -> c_int { + panic!("fprintf is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fputs(_s: *const c_void, _file: *mut c_void) -> c_int { + panic!("fputs is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fputc(_c: c_int, _file: *mut c_void) -> c_int { + panic!("fputc is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fdopen(_fd: c_int, _mode: *const c_void) -> *mut c_void { + panic!("fdopen is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fclose(_file: *mut c_void) -> c_int { + panic!("fclose is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fwrite( + _ptr: *const c_void, + _size: usize, + _nmemb: usize, + _stream: *mut c_void, +) -> usize { + panic!("fwrite is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn vsnprintf( + _buf: *mut c_char, + _size: usize, + _format: *const c_char, + _args: ... +) -> c_int { + panic!("vsnprintf is not supported"); +} + +#[no_mangle] +pub extern "C" fn clock_gettime(ptr: usize, new_size: usize) { + panic!("asdasd"); +} + +// int snprintf( char* restrict buffer, size_t bufsz, const char* restrict format, ... ); +#[no_mangle] +pub extern "C" fn snprintf() { + panic!("snprintf is not supported"); +} + +#[no_mangle] +pub extern "C" fn __assert_fail(_: *const i32, _: *const i32, _: *const i32, _: *const i32) { + panic!("oh no"); +} diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 2513ece67bf51..c78a9da69ffef 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -26,6 +26,7 @@ php-parser = [ "dep:windmill-parser-php"] rust-parser = [ "dep:windmill-parser-rust"] graphql-parser = [ "dep:windmill-parser-graphql"] ansible-parser = [ "dep:windmill-parser-yaml"] +csharp-parser = [ "dep:windmill-parser-csharp"] [dependencies] anyhow.workspace = true @@ -39,6 +40,7 @@ windmill-parser-php = { workspace = true, optional = true } windmill-parser-graphql = { workspace = true, optional = true } windmill-parser-rust = { workspace = true, optional = true } windmill-parser-yaml = { workspace = true, optional = true } +windmill-parser-csharp = { workspace = true, optional = true } wasm-bindgen.workspace = true serde_json.workspace = true getrandom = { workspace = true, features = ["js"] } diff --git a/backend/parsers/windmill-parser-wasm/build-pkgs-mac.sh b/backend/parsers/windmill-parser-wasm/build-pkgs-mac.sh index 7803fe22f0faf..dcd5a78275c63 100755 --- a/backend/parsers/windmill-parser-wasm/build-pkgs-mac.sh +++ b/backend/parsers/windmill-parser-wasm/build-pkgs-mac.sh @@ -48,3 +48,9 @@ OUT_DIR="pkg-yaml" wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \ -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json + +# C# (needs some more stuff to compile C tree sitter into wasm) +# TODO: hasn't been tested on mac, might need fixing +OUT_DIR="pkg-csharp" +CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser" +sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json diff --git a/backend/parsers/windmill-parser-wasm/build-pkgs.sh b/backend/parsers/windmill-parser-wasm/build-pkgs.sh index 66dafe3cccbc3..ec7a1f5ee412b 100755 --- a/backend/parsers/windmill-parser-wasm/build-pkgs.sh +++ b/backend/parsers/windmill-parser-wasm/build-pkgs.sh @@ -48,3 +48,8 @@ OUT_DIR="pkg-yaml" wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \ -Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json + +# C# (needs some more stuff to compile C tree sitter into wasm) +OUT_DIR="pkg-csharp" +CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser" +sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json diff --git a/backend/parsers/windmill-parser-wasm/publish-pkgs.sh b/backend/parsers/windmill-parser-wasm/publish-pkgs.sh index c2f2c0d7ad960..a4af8bb684421 100755 --- a/backend/parsers/windmill-parser-wasm/publish-pkgs.sh +++ b/backend/parsers/windmill-parser-wasm/publish-pkgs.sh @@ -24,3 +24,6 @@ popd pushd "pkg-yaml" && npm publish ${args} popd + +pushd "pkg-csharp" && npm publish ${args} +popd diff --git a/backend/parsers/windmill-parser-wasm/src/lib.rs b/backend/parsers/windmill-parser-wasm/src/lib.rs index a5dd03ad918b4..50ebf232acd79 100644 --- a/backend/parsers/windmill-parser-wasm/src/lib.rs +++ b/backend/parsers/windmill-parser-wasm/src/lib.rs @@ -135,3 +135,9 @@ pub fn parse_rust(code: &str) -> String { pub fn parse_ansible(code: &str) -> String { wrap_sig(windmill_parser_yaml::parse_ansible_sig(code)) } + +#[cfg(feature = "csharp-parser")] +#[wasm_bindgen] +pub fn parse_csharp(code: &str) -> String { + wrap_sig(windmill_parser_csharp::parse_csharp_signature(code)) +} diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/assert.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/assert.h new file mode 100644 index 0000000000000..8a17e8dc6ff8b --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/assert.h @@ -0,0 +1,4 @@ +#pragma once + +#define assert(ignore) ((void)0) +#define static_assert(cnd, msg) assert(cnd && msg) diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/ctype.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/ctype.h new file mode 100644 index 0000000000000..14175419abf46 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/ctype.h @@ -0,0 +1,3 @@ +#pragma once + +int isprint(int c); diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/inttypes.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/inttypes.h new file mode 100644 index 0000000000000..cfc0873a81c4d --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/inttypes.h @@ -0,0 +1,3 @@ +#pragma once + +#define PRId32 "d" diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdbool.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdbool.h new file mode 100644 index 0000000000000..97287ba6e93e0 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdbool.h @@ -0,0 +1,5 @@ +#pragma once + +#define bool _Bool +#define true 1 +#define false 0 diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdint.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdint.h new file mode 100644 index 0000000000000..d575a081b4f6e --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdint.h @@ -0,0 +1,19 @@ +#pragma once + +typedef signed char int8_t; +typedef short int16_t; +typedef long int32_t; +typedef long long int64_t; + +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef unsigned long uint32_t; +typedef unsigned long long uint64_t; + +typedef unsigned long size_t; + +typedef unsigned int uintptr_t; + +#define UINT8_MAX 0xff +#define UINT16_MAX 0xffff +#define UINT32_MAX 0xffffffff diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdio.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdio.h new file mode 100644 index 0000000000000..6e19a95aa31e6 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdio.h @@ -0,0 +1,19 @@ +#pragma once + +// just some filler type +#define FILE void + +#define stdin NULL +#define stdout NULL +#define stderr NULL + +int fprintf(FILE *__restrict__, const char *__restrict__, ...); +int fputs(const char *__restrict, FILE *__restrict); +int fputc(int, FILE *); +FILE *fdopen(int, const char *); +int fclose(FILE *); + +int vsnprintf(char *s, unsigned long n, const char *format, ...); + +#define sprintf(str, ...) 0 +#define snprintf(str, len, ...) 0 diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdlib.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdlib.h new file mode 100644 index 0000000000000..8c8df2a8fd683 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/stdlib.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#define NULL ((void*)0) + +void* malloc(size_t size); +void* calloc(size_t nmemb, size_t size); +void free(void* ptr); +void* realloc(void* ptr, size_t size); + +void abort(void); diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/string.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/string.h new file mode 100644 index 0000000000000..c0687e9c88993 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/string.h @@ -0,0 +1,7 @@ +#pragma once + +void *memcpy(void *dest, const void *src, unsigned long n); +void *memmove(void *dest, const void *src, unsigned long n); +void *memset(void *s, int c, unsigned long n); +int memcmp(const void *ptr1, const void *ptr2, unsigned long n); +int strncmp(const char *s1, const char *s2, unsigned long n); diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/time.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/time.h new file mode 100644 index 0000000000000..3a1583bdaf439 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/time.h @@ -0,0 +1,5 @@ +#pragma once + +typedef unsigned long clock_t; +#define CLOCKS_PER_SEC ((clock_t)1000000) +clock_t clock(void); diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/unistd.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/unistd.h new file mode 100644 index 0000000000000..b3727dd3efce1 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/unistd.h @@ -0,0 +1,3 @@ +#pragma once + +int dup(int); diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/wctype.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/wctype.h new file mode 100644 index 0000000000000..517a954be5e28 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/wctype.h @@ -0,0 +1,7 @@ +#pragma once + +typedef __WCHAR_TYPE__ wchar_t; +typedef __WINT_TYPE__ wint_t; + +int iswspace(wchar_t ch); +int iswalnum(wint_t _wc); diff --git a/backend/src/main.rs b/backend/src/main.rs index e76dfa79f67c5..c9977886705e2 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -8,9 +8,7 @@ use anyhow::Context; use monitor::{ - load_base_url, load_otel, reload_delete_logs_periodically_setting, reload_indexer_config, - reload_timeout_wait_result_setting, send_current_log_file_to_object_store, - send_logs_to_object_store, + load_base_url, load_otel, reload_delete_logs_periodically_setting, reload_indexer_config, reload_nuget_config_setting, reload_timeout_wait_result_setting, send_current_log_file_to_object_store, send_logs_to_object_store }; use rand::Rng; use sqlx::{postgres::PgListener, Pool, Postgres}; @@ -31,15 +29,7 @@ use windmill_common::ee::{maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICE use windmill_common::{ global_settings::{ - BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING, - CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, - DEFAULT_TAGS_WORKSPACES_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, - EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING, - JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, - LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, - OAUTH_SETTING, OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, - REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, - SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING, + BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING }, scripts::ScriptLang, stats_ee::schedule_stats, @@ -66,10 +56,10 @@ use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING; use windmill_worker::{ get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, - BUN_DEPSTAR_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM, - GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, POWERSHELL_CACHE_DIR, - PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR, TMP_LOGS_DIR, - UV_CACHE_DIR, + BUN_DEPSTAR_CACHE_DIR, CSHARP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, + DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, + POWERSHELL_CACHE_DIR, PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR, + TMP_LOGS_DIR, UV_CACHE_DIR, }; use crate::monitor::{ @@ -769,6 +759,9 @@ Windmill Community Edition {GIT_VERSION} BUNFIG_INSTALL_SCOPES_SETTING => { reload_bunfig_install_scopes_setting(&db).await }, + NUGET_CONFIG_SETTING => { + reload_nuget_config_setting(&db).await + }, KEEP_JOB_DIR_SETTING => { load_keep_job_dir(&db).await; }, @@ -1023,6 +1016,7 @@ pub async fn run_workers( GO_CACHE_DIR, GO_BIN_CACHE_DIR, RUST_CACHE_DIR, + CSHARP_CACHE_DIR, HUB_CACHE_DIR, POWERSHELL_CACHE_DIR, ] { diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index c11d60621efe2..5d50d7e82dd24 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -34,15 +34,7 @@ use windmill_common::{ error, flow_status::FlowStatusModule, global_settings::{ - BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING, - CRITICAL_ERROR_CHANNELS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, - DEFAULT_TAGS_WORKSPACES_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, - EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, - JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, - MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, - OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, - REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, - SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING, + BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING }, indexer::load_indexer_config, jobs::QueuedJob, @@ -63,9 +55,7 @@ use windmill_common::{ }; use windmill_queue::cancel_job; use windmill_worker::{ - create_token_for_owner, handle_job_error, AuthedClient, SameWorkerPayload, SameWorkerSender, - SendResult, BUNFIG_INSTALL_SCOPES, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, - PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, SCRIPT_TOKEN_EXPIRY, + create_token_for_owner, handle_job_error, AuthedClient, SameWorkerPayload, SameWorkerSender, SendResult, BUNFIG_INSTALL_SCOPES, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, NUGET_CONFIG, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, SCRIPT_TOKEN_EXPIRY }; #[cfg(feature = "parquet")] @@ -188,6 +178,7 @@ pub async fn initial_load( reload_pip_index_url_setting(&db).await; reload_npm_config_registry_setting(&db).await; reload_bunfig_install_scopes_setting(&db).await; + reload_nuget_config_setting(&db).await; } } @@ -916,6 +907,16 @@ pub async fn reload_bunfig_install_scopes_setting(db: &DB) { .await; } +pub async fn reload_nuget_config_setting(db: &DB) { + reload_option_setting_with_tracing( + db, + NUGET_CONFIG_SETTING, + "NUGET_CONFIG", + NUGET_CONFIG.clone(), + ) + .await; +} + pub async fn reload_retention_period_setting(db: &DB) { if let Err(e) = reload_setting( db, diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 571b71cd8f367..712c8c2762e6d 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -1798,6 +1798,48 @@ fn main(world: String) -> Result { assert_eq!(result, serde_json::json!("Hello Hyrule!")); } +// #[sqlx::test(fixtures("base"))] +// async fn test_csharp_job(db: Pool) { +// initialize_tracing().await; +// let server = ApiServer::start(db.clone()).await; +// let port = server.addr.port(); +// +// let content = r#" +// using System; +// +// class Script +// { +// public static string Main(string world, int b = 2) +// { +// Console.WriteLine($"Hello {world} - {b}. This is a log line"); +// return $"Hello {world} - {b}"; +// } +// } +// "# +// .to_owned(); +// +// let result = RunJob::from(JobPayload::Code(RawCode { +// hash: None, +// content, +// path: None, +// lock: None, +// language: ScriptLang::CSharp, +// custom_concurrency_key: None, +// concurrent_limit: None, +// concurrency_time_window_s: None, +// cache_ttl: None, +// dedicated_worker: None, +// })) +// .arg("world", json!("Arakis")) +// .arg("b", json!(3)) +// .run_until_complete(&db, port) +// .await +// .json_result() +// .unwrap(); +// +// assert_eq!(result, serde_json::json!("Hello Arakis - 3")); +// } + #[sqlx::test(fixtures("base"))] async fn test_bash_job(db: Pool) { initialize_tracing().await; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 7de22e3248eaa..a6d25a27adca9 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -10692,6 +10692,7 @@ components: php, rust, ansible, + csharp, ] kind: type: string @@ -10795,6 +10796,7 @@ components: php, rust, ansible, + csharp, ] kind: type: string @@ -11019,6 +11021,7 @@ components: php, rust, ansible, + csharp, ] email: type: string @@ -11141,6 +11144,7 @@ components: php, rust, ansible, + csharp, ] is_skipped: type: boolean @@ -11696,6 +11700,7 @@ components: php, rust, ansible, + csharp, ] tag: type: string @@ -13312,6 +13317,7 @@ components: php, rust, ansible, + csharp, ] required: - raw_code diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index ba21c688e4b43..090bff24aa355 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -583,6 +583,7 @@ async fn create_script_internal<'c>( || ns.language == ScriptLang::Deno || ns.language == ScriptLang::Rust || ns.language == ScriptLang::Ansible + || ns.language == ScriptLang::CSharp || ns.language == ScriptLang::Php) { Some(String::new()) diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 0552a6c94bce8..370c1c51395c1 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -2364,6 +2364,7 @@ async fn tarball_workspace( ScriptLang::Php => "php", ScriptLang::Rust => "rs", ScriptLang::Ansible => "playbook.yml", + ScriptLang::CSharp => "cs", }; archive .write_to_archive(&script.content, &format!("{}.{}", script.path, ext)) diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 9dbfd03b0eb10..9e5c308e653d5 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -10,6 +10,7 @@ pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb"; pub const LICENSE_KEY_SETTING: &str = "license_key"; pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry"; pub const BUNFIG_INSTALL_SCOPES_SETTING: &str = "bunfig_install_scopes"; +pub const NUGET_CONFIG_SETTING: &str = "nuget_config"; pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url"; pub const PIP_INDEX_URL_SETTING: &str = "pip_index_url"; diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index 9d51bbb830647..75e4e1e0c0bcc 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -45,6 +45,7 @@ pub enum ScriptLang { Php, Rust, Ansible, + CSharp, } impl ScriptLang { @@ -67,6 +68,7 @@ impl ScriptLang { ScriptLang::Php => "php", ScriptLang::Rust => "rust", ScriptLang::Ansible => "ansible", + ScriptLang::CSharp => "csharp", } } } diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 73205f6620037..ba8ed8aa52c0b 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -47,6 +47,7 @@ lazy_static::lazy_static! { "php".to_string(), "rust".to_string(), "ansible".to_string(), + "csharp".to_string(), "dependency".to_string(), "flow".to_string(), "other".to_string() diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index 7b800eee8ff64..dd6099cc9b5fc 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -24,6 +24,7 @@ otel = ["windmill-common/otel", "dep:opentelemetry"] dind = ["dep:bollard"] php = ["dep:windmill-parser-php"] mysql = ["dep:mysql_async"] +csharp = ["dep:windmill-parser-csharp"] [dependencies] windmill-queue.workspace = true @@ -33,6 +34,7 @@ windmill-parser.workspace = true windmill-parser-ts.workspace = true windmill-parser-go.workspace = true windmill-parser-rust.workspace = true +windmill-parser-csharp = { workspace = true, optional = true } windmill-parser-py.workspace = true windmill-parser-yaml.workspace = true windmill-parser-py-imports.workspace = true diff --git a/backend/windmill-worker/nsjail/run.csharp.config.proto b/backend/windmill-worker/nsjail/run.csharp.config.proto new file mode 100644 index 0000000000000..4a6de952a70fe --- /dev/null +++ b/backend/windmill-worker/nsjail/run.csharp.config.proto @@ -0,0 +1,114 @@ +name: "csharp run script" + +mode: ONCE +hostname: "csharp" +log_level: ERROR + +disable_rl: true + +cwd: "/tmp" + +clone_newnet: false +clone_newuser: {CLONE_NEWUSER} + +keep_caps: false +keep_env: true +mount_proc: true + +mount { + src: "/bin" + dst: "/bin" + is_bind: true +} + +mount { + src: "/lib" + dst: "/lib" + is_bind: true +} + + +mount { + src: "/lib64" + dst: "/lib64" + is_bind: true + mandatory: false +} + + +mount { + src: "/usr" + dst: "/usr" + is_bind: true +} + +mount { + src: "/opt/dotnet-sdk/bin" + dst: "/opt/dotnet-sdk/bin" + is_bind: true +} + +mount { + src: "/dev/null" + dst: "/dev/null" + is_bind: true + rw: true +} + +mount { + dst: "/tmp" + fstype: "tmpfs" + rw: true + options: "size=500000000" +} + + +mount { + src: "{JOB_DIR}/Main" + dst: "/tmp/main" + is_bind: true + mandatory: false +} + +mount { + src: "{JOB_DIR}/args.json" + dst: "/tmp/args.json" + is_bind: true +} + +mount { + src: "{JOB_DIR}/result.json" + dst: "/tmp/result.json" + rw: true + is_bind: true +} + +mount { + src: "/etc" + dst: "/etc" + is_bind: true +} + +mount { + src: "/dev/random" + dst: "/dev/random" + is_bind: true +} + +mount { + src: "/dev/urandom" + dst: "/dev/urandom" + is_bind: true +} + +iface_no_lo: true + +mount { + src: "{CACHE_DIR}" + dst: "/tmp/.cache/csharp" + is_bind: true + rw: true + mandatory: false +} + +{SHARED_MOUNT} diff --git a/backend/windmill-worker/src/ansible_executor.rs b/backend/windmill-worker/src/ansible_executor.rs index c2d917ad8677c..068e5baa315e0 100644 --- a/backend/windmill-worker/src/ansible_executor.rs +++ b/backend/windmill-worker/src/ansible_executor.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, os::unix::fs::PermissionsExt, - path::{Path, PathBuf}, + path::PathBuf, process::Stdio, }; @@ -29,8 +29,7 @@ use windmill_queue::{append_logs, CanceledBy}; use crate::{ bash_executor::BIN_BASH, common::{ - get_reserved_variables, read_and_check_result, start_child_process, transform_json, - OccupancyMetrics, + check_executor_binary_exists, get_reserved_variables, read_and_check_result, start_child_process, transform_json, OccupancyMetrics }, handle_child::handle_child, python_executor::{create_dependencies_dir, handle_python_reqs, uv_pip_compile}, @@ -185,24 +184,6 @@ async fn install_galaxy_collections( Ok(()) } -#[cfg(not(feature = "enterprise"))] -fn check_ansible_exists() -> Result<(), error::Error> { - if !Path::new(ANSIBLE_PLAYBOOK_PATH.as_str()).exists() { - let msg = format!("Couldn't find ansible-playbook at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full` for your instance in order to run Ansible jobs.", ANSIBLE_PLAYBOOK_PATH.as_str()); - return Err(error::Error::NotFound(msg)); - } - Ok(()) -} - -#[cfg(feature = "enterprise")] -fn check_ansible_exists() -> Result<(), error::Error> { - if !Path::new(ANSIBLE_PLAYBOOK_PATH.as_str()).exists() { - let msg = format!("Couldn't find ansible-playbook at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-ee-full` for your instance in order to run Ansible jobs.", ANSIBLE_PLAYBOOK_PATH.as_str()); - return Err(error::Error::NotFound(msg)); - } - Ok(()) -} - pub async fn handle_ansible_job( requirements_o: Option, job_dir: &str, @@ -219,7 +200,7 @@ pub async fn handle_ansible_job( envs: HashMap, occupancy_metrics: &mut OccupancyMetrics, ) -> windmill_common::error::Result> { - check_ansible_exists()?; + check_executor_binary_exists("ansible-playbook", ANSIBLE_PLAYBOOK_PATH.as_str(), "ansible")?; let (logs, reqs, playbook) = windmill_parser_yaml::parse_ansible_reqs(inner_content)?; append_logs(&job.id, &job.workspace_id, logs, db).await; diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 147ba1bdae0c0..51385ff3672f2 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -31,7 +31,13 @@ use windmill_common::{ use anyhow::{anyhow, Result}; -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::path::Path; +use std::{ + collections::HashMap, + sync::Arc, + time::Duration, +}; + use uuid::Uuid; use windmill_common::{variables, DB}; @@ -53,6 +59,23 @@ pub async fn build_args_map<'a>( return Ok(None); } +pub fn check_executor_binary_exists( + executor: &str, + executor_path: &str, + language: &str, +) -> Result<(), Error> { + if !Path::new(executor_path).exists() { + #[cfg(feature = "enterprise")] + let msg = format!("Couldn't find {executor} at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full-ee` for your instance in order to run {language} jobs.", executor_path); + + #[cfg(not(feature = "enterprise"))] + let msg = format!("Couldn't find {executor} at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full` for your instance in order to run {language} jobs.", executor_path); + return Err(Error::NotFound(msg)); + } + + Ok(()) +} + pub async fn build_args_values( job: &QueuedJob, client: &AuthedClientBackgroundTask, diff --git a/backend/windmill-worker/src/csharp_executor.rs b/backend/windmill-worker/src/csharp_executor.rs new file mode 100644 index 0000000000000..e880a0fa81f70 --- /dev/null +++ b/backend/windmill-worker/src/csharp_executor.rs @@ -0,0 +1,514 @@ +use anyhow::anyhow; +use serde_json::value::RawValue; +use std::{collections::HashMap, io, path::Path, process::Stdio}; +use uuid::Uuid; +#[cfg(feature = "csharp")] +use windmill_parser_csharp::parse_csharp_reqs; + +use itertools::Itertools; +use tokio::{fs::File, io::AsyncReadExt, process::Command}; +use windmill_common::{ + error::{self, Error}, + jobs::QueuedJob, + utils::calculate_hash, + worker::{save_cache, write_file}, +}; +use windmill_queue::{append_logs, CanceledBy}; + +use crate::{ + common::{ + check_executor_binary_exists, create_args_and_out_file, get_reserved_variables, + read_result, start_child_process, OccupancyMetrics, + }, + handle_child::handle_child, + AuthedClientBackgroundTask, CSHARP_CACHE_DIR, DISABLE_NSJAIL, DISABLE_NUSER, DOTNET_PATH, + HOME_ENV, NSJAIL_PATH, NUGET_CONFIG, PATH_ENV, TZ_ENV, +}; + +#[cfg(windows)] +use crate::SYSTEM_ROOT; + +const NSJAIL_CONFIG_RUN_CSHARP_CONTENT: &str = include_str!("../nsjail/run.csharp.config.proto"); + +lazy_static::lazy_static! { + static ref HOME_DIR: String = std::env::var("HOME").expect("Could not find the HOME environment variable"); +} + +const CSHARP_OBJECT_STORE_PREFIX: &str = "csharpbin/"; + +#[cfg(feature = "csharp")] +pub async fn generate_nuget_lockfile( + job_id: &Uuid, + code: &str, + mem_peak: &mut i32, + canceled_by: &mut Option, + job_dir: &str, + db: &sqlx::Pool, + worker_name: &str, + w_id: &str, + occupancy_metrics: &mut OccupancyMetrics, +) -> error::Result { + check_executor_binary_exists("dotnet", DOTNET_PATH.as_str(), "C#")?; + + if let Some(nuget_config) = NUGET_CONFIG.read().await.clone() { + write_file(job_dir, "nuget.config", &nuget_config)?; + } + + let (reqs, lines_to_remove) = parse_csharp_reqs(code); + + gen_cs_proj(code, job_dir, reqs, lines_to_remove)?; + + let mut gen_lockfile_cmd = Command::new(DOTNET_PATH.as_str()); + gen_lockfile_cmd + .current_dir(job_dir) + .args(vec!["restore", "--use-lock-file"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let gen_lockfile_process = start_child_process(gen_lockfile_cmd, DOTNET_PATH.as_str()).await?; + handle_child( + job_id, + db, + mem_peak, + canceled_by, + gen_lockfile_process, + false, + worker_name, + w_id, + "dotnet restore", + None, + false, + &mut Some(occupancy_metrics), + ) + .await?; + + if let Err(e) = std::fs::remove_file(Path::new(job_dir).join("nuget.config")) { + if e.kind() != io::ErrorKind::NotFound { + Err(anyhow!("Error erasing nuget.config: {}", e))?; + } + } + + let path_lock = format!("{job_dir}/packages.lock.json"); + let mut file = File::open(path_lock).await?; + let mut req_content = String::new(); + file.read_to_string(&mut req_content).await?; + Ok(req_content) +} + +#[cfg(not(feature = "csharp"))] +pub async fn generate_nuget_lockfile( + _job_id: &Uuid, + _code: &str, + _mem_peak: &mut i32, + _canceled_by: &mut Option, + _job_dir: &str, + _db: &sqlx::Pool, + _worker_name: &str, + _w_id: &str, + _occupancy_metrics: &mut OccupancyMetrics, +) -> error::Result { + Err(anyhow!("C# is not available because the feature is not enabled").into()) +} + + +#[cfg(feature = "csharp")] +fn gen_cs_proj( + code: &str, + job_dir: &str, + reqs: Vec<(String, Option)>, + lines_to_remove: Vec, +) -> anyhow::Result<()> { + let code = remove_lines_from_text(code, lines_to_remove); + + let pkgs = reqs + .into_iter() + .map(|(pkg, vrsion_o)| { + let version = vrsion_o + .map(|v| format!("Version=\"{v}\"")) + .unwrap_or("".to_string()); + format!(" ") + }) + .join("\n"); + + write_file( + job_dir, + "Main.csproj", + &format!( + r#" + + Exe + net7.0 + enable + WindmillScriptCSharpInternal.Wrapper + true + + +{pkgs} + + + +"# + ), + )?; + + write_file(job_dir, "Script.cs", code.as_str())?; + + let sig_meta = windmill_parser_csharp::parse_csharp_sig_meta(code.as_str())?; + let sig = sig_meta.main_sig; + let spread = &sig + .args + .clone() + .into_iter() + .map(|x| format!("parsedArgs.{}", &x.name)) + .join(", "); + let args_class_body = &sig + .args + .into_iter() + .map(|x| { + Ok(format!( + " public {} {} {{ get; set; }}", + x.otyp + .ok_or(anyhow!("Type not found for argument {}", x.name))?, + &x.name, + )) + }) + .collect::, anyhow::Error>>()? + .join("\n"); + + let class_name = sig_meta.class_name.unwrap_or("Script".to_string()); + + let script_call = match (sig_meta.is_async, sig_meta.returns_void) { + (true, true) => format!( + r#" + {class_name}.Main({spread}).Wait();"# + ), + (false, true) => format!( + r#" + {class_name}.Main({spread});"# + ), + + (false, false) => format!( + r#" + var result = {class_name}.Main({spread}); + + var jsonResult = JsonSerializer.Serialize(result); + + File.WriteAllText("result.json", jsonResult);"# + ), + (true, false) => format!( + r#" + var result = {class_name}.Main({spread}).Result; + + var jsonResult = JsonSerializer.Serialize(result); + + File.WriteAllText("result.json", jsonResult);"# + ), + }; + + write_file( + job_dir, + "Wrapper.cs", + &format!( + r#"using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +namespace WindmillScriptCSharpInternal {{ + + struct Args {{ +{args_class_body} + }} + + class Wrapper + {{ + static void Main(string[] args) + {{ + using FileStream fs = File.OpenRead("args.json"); + Args parsedArgs = JsonSerializer.Deserialize(fs); + + File.WriteAllText("result.json", "null"); + + {script_call} + }} + }} +}} +"#, + ), + )?; + + Ok(()) +} + +#[cfg(feature = "csharp")] +async fn build_cs_proj( + job_id: &Uuid, + mem_peak: &mut i32, + canceled_by: &mut Option, + job_dir: &str, + db: &sqlx::Pool, + worker_name: &str, + w_id: &str, + base_internal_url: &str, + hash: &str, + occupancy_metrics: &mut OccupancyMetrics, +) -> error::Result { + if let Some(nuget_config) = NUGET_CONFIG.read().await.clone() { + write_file(job_dir, "nuget.config", &nuget_config)?; + } + + let mut build_cs_cmd = Command::new(DOTNET_PATH.as_str()); + + build_cs_cmd + .current_dir(job_dir) + .env_clear() + .env("PATH", PATH_ENV.as_str()) + .env("BASE_INTERNAL_URL", base_internal_url) + .env("HOME", HOME_ENV.as_str()) + .args(vec![ + "publish", + "--configuration", + "Release", + "-o", + job_dir, + "--no-self-contained", + "-p:PublishSingleFile=true", + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + #[cfg(windows)] + { + build_cs_cmd.env("SystemRoot", SYSTEM_ROOT.as_str()); + build_cs_cmd.env( + "TMP", + std::env::var("TMP").unwrap_or_else(|_| "C:\\tmp".to_string()), + ); + } + + let build_cs_process = start_child_process(build_cs_cmd, DOTNET_PATH.as_str()).await?; + handle_child( + job_id, + db, + mem_peak, + canceled_by, + build_cs_process, + false, + worker_name, + w_id, + "dotnet publish", + None, + false, + &mut Some(occupancy_metrics), + ) + .await?; + append_logs(job_id, w_id, "\n\n", db).await; + + if let Err(e) = std::fs::remove_file(Path::new(job_dir).join("nuget.config")) { + if e.kind() != io::ErrorKind::NotFound { + Err(anyhow!("Error erasing nuget.config: {}", e))?; + } + } + + let bin_path = format!("{}/{hash}", CSHARP_CACHE_DIR); + + match save_cache( + &bin_path, + &format!("{CSHARP_OBJECT_STORE_PREFIX}{hash}"), + &format!("{job_dir}/Main"), + ) + .await + { + Err(e) => { + let em = format!("could not save {job_dir}/Main to C# cache: {e:?}",); + tracing::error!(em); + Ok(em) + } + Ok(logs) => Ok(logs), + } +} + +fn remove_lines_from_text(contents: &str, indices_to_remove: Vec) -> String { + let mut result = Vec::new(); + + for (i, line) in contents.lines().enumerate() { + if !indices_to_remove.contains(&i) { + result.push(line); + } + } + + result.join("\n") +} + +#[cfg(not(feature = "csharp"))] +pub async fn handle_csharp_job( + _mem_peak: &mut i32, + _canceled_by: &mut Option, + _job: &QueuedJob, + _db: &sqlx::Pool, + _client: &AuthedClientBackgroundTask, + _inner_content: &str, + _job_dir: &str, + _requirements_o: Option, + _shared_mount: &str, + _base_internal_url: &str, + _worker_name: &str, + _envs: HashMap, + _occupancy_metrics: &mut OccupancyMetrics, +) -> Result, Error> { + Err(anyhow!("C# is not available because the feature is not enabled").into()) +} + +#[cfg(feature = "csharp")] +pub async fn handle_csharp_job( + mem_peak: &mut i32, + canceled_by: &mut Option, + job: &QueuedJob, + db: &sqlx::Pool, + client: &AuthedClientBackgroundTask, + inner_content: &str, + job_dir: &str, + requirements_o: Option, + shared_mount: &str, + base_internal_url: &str, + worker_name: &str, + envs: HashMap, + occupancy_metrics: &mut OccupancyMetrics, +) -> Result, Error> { + check_executor_binary_exists("dotnet", DOTNET_PATH.as_str(), "C#")?; + + let hash = calculate_hash(&format!( + "{}{}", + inner_content, + requirements_o + .as_ref() + .map(|x| x.to_string()) + .unwrap_or_default() + )); + let bin_path = format!("{}/{hash}", CSHARP_CACHE_DIR); + let remote_path = format!("{CSHARP_OBJECT_STORE_PREFIX}{hash}"); + + let (cache, cache_logs) = windmill_common::worker::load_cache(&bin_path, &remote_path).await; + + let cache_logs = if cache { + let target = format!("{job_dir}/Main"); + + #[cfg(unix)] + let symlink = std::os::unix::fs::symlink(&bin_path, &target); + #[cfg(windows)] + let symlink = std::os::windows::fs::symlink_dir(&bin_path, &target); + + symlink.map_err(|e| { + Error::ExecutionErr(format!( + "could not copy cached binary from {bin_path} to {job_dir}/main: {e:?}" + )) + })?; + + cache_logs + } else { + let logs1 = format!("{cache_logs}\n\n--- DOTNET BUILD ---\n"); + append_logs(&job.id, &job.workspace_id, logs1, db).await; + + let (reqs, lines_to_remove) = parse_csharp_reqs(inner_content); + for req in &reqs { + append_logs( + &job.id, + &job.workspace_id, + format!( + "Requirement detected: {} {}\n", + req.0, + req.1.as_ref().unwrap_or(&"".to_string()) + ), + db, + ) + .await; + } + + gen_cs_proj(inner_content, job_dir, reqs, lines_to_remove)?; + + if let Some(reqs) = requirements_o { + if !reqs.is_empty() { + write_file(job_dir, "packages.lock.json", &reqs)?; + } + } + + build_cs_proj( + &job.id, + mem_peak, + canceled_by, + job_dir, + db, + worker_name, + &job.workspace_id, + base_internal_url, + &hash, + occupancy_metrics, + ) + .await? + }; + + create_args_and_out_file(client, job, job_dir, db).await?; + + let logs2 = format!("{cache_logs}\n\n--- C# CODE EXECUTION ---\n"); + append_logs(&job.id, &job.workspace_id, format!("{}\n", logs2), db).await; + + let client = &client.get_authed().await; + let reserved_variables = get_reserved_variables(job, &client.token, db).await?; + + let child = if !*DISABLE_NSJAIL { + write_file( + job_dir, + "run.config.proto", + &NSJAIL_CONFIG_RUN_CSHARP_CONTENT + .replace("{JOB_DIR}", job_dir) + .replace("{CACHE_DIR}", CSHARP_CACHE_DIR) + .replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()) + .replace("{SHARED_MOUNT}", shared_mount), + )?; + let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str()); + nsjail_cmd + .current_dir(job_dir) + .env_clear() + .envs(envs) + .envs(reserved_variables) + .env("PATH", PATH_ENV.as_str()) + .env("TZ", TZ_ENV.as_str()) + .env("BASE_INTERNAL_URL", base_internal_url) + .args(vec!["--config", "run.config.proto", "--", "/tmp/main"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await? + } else { + let compiled_executable_name = "./Main"; + let mut run_csharp = Command::new(compiled_executable_name); + run_csharp + .current_dir(job_dir) + .env_clear() + .envs(envs) + .envs(reserved_variables) + .env("PATH", PATH_ENV.as_str()) + .env("TZ", TZ_ENV.as_str()) + .env("BASE_INTERNAL_URL", base_internal_url) + .env("HOME", HOME_ENV.as_str()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + start_child_process(run_csharp, compiled_executable_name).await? + }; + + handle_child( + &job.id, + db, + mem_peak, + canceled_by, + child, + !*DISABLE_NSJAIL, + worker_name, + &job.workspace_id, + "csharp run", + job.timeout, + false, + &mut Some(occupancy_metrics), + ) + .await?; + read_result(job_dir).await +} diff --git a/backend/windmill-worker/src/lib.rs b/backend/windmill-worker/src/lib.rs index a940bef7715cb..c68f77917f1b9 100644 --- a/backend/windmill-worker/src/lib.rs +++ b/backend/windmill-worker/src/lib.rs @@ -31,6 +31,7 @@ mod worker; mod worker_flow; mod worker_lockfiles; mod job_logger_ee; +mod csharp_executor; pub use worker::*; pub use result_processor::handle_job_error; diff --git a/backend/windmill-worker/src/php_executor.rs b/backend/windmill-worker/src/php_executor.rs index aceca99cc2ca4..cb6b08f4e83b6 100644 --- a/backend/windmill-worker/src/php_executor.rs +++ b/backend/windmill-worker/src/php_executor.rs @@ -2,7 +2,7 @@ use convert_case::{Case, Casing}; use itertools::Itertools; use regex::Regex; use serde_json::value::RawValue; -use std::{collections::HashMap, path::Path, process::Stdio}; +use std::{collections::HashMap, process::Stdio}; use tokio::{fs::File, io::AsyncReadExt, process::Command}; use uuid::Uuid; use windmill_common::{ @@ -15,8 +15,7 @@ use windmill_queue::{append_logs, CanceledBy}; use crate::{ common::{ - create_args_and_out_file, get_main_override, get_reserved_variables, read_result, - start_child_process, OccupancyMetrics, + check_executor_binary_exists, create_args_and_out_file, get_main_override, get_reserved_variables, read_result, start_child_process, OccupancyMetrics }, handle_child::handle_child, AuthedClientBackgroundTask, COMPOSER_CACHE_DIR, COMPOSER_PATH, DISABLE_NSJAIL, DISABLE_NUSER, @@ -73,7 +72,7 @@ pub async fn composer_install( lock: Option, occupancy_metrics: &mut OccupancyMetrics, ) -> Result { - check_php_exists()?; + check_executor_binary_exists("php", PHP_PATH.as_str(), "php")?; write_file(job_dir, "composer.json", &requirements)?; @@ -131,24 +130,6 @@ $args->{arg_name} = new {rt_name}($args->{arg_name});" ) } -#[cfg(not(feature = "enterprise"))] -fn check_php_exists() -> error::Result<()> { - if !Path::new(PHP_PATH.as_str()).exists() { - let msg = format!("Couldn't find php at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full` for your instance in order to run php jobs.", PHP_PATH.as_str()); - return Err(error::Error::NotFound(msg)); - } - Ok(()) -} - -#[cfg(feature = "enterprise")] -fn check_php_exists() -> error::Result<()> { - if !Path::new(PHP_PATH.as_str()).exists() { - let msg = format!("Couldn't find php at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-ee-full` for your instance in order to run php jobs.", PHP_PATH.as_str()); - return Err(error::Error::NotFound(msg)); - } - Ok(()) -} - #[tracing::instrument(level = "trace", skip_all)] pub async fn handle_php_job( requirements_o: Option, @@ -165,7 +146,7 @@ pub async fn handle_php_job( shared_mount: &str, occupancy_metrics: &mut OccupancyMetrics, ) -> error::Result> { - check_php_exists()?; + check_executor_binary_exists("php", PHP_PATH.as_str(), "php")?; let (composer_json, composer_lock) = match requirements_o { Some(reqs_and_lock) if !reqs_and_lock.is_empty() => { diff --git a/backend/windmill-worker/src/rust_executor.rs b/backend/windmill-worker/src/rust_executor.rs index c6f258d6acc4a..c26d4d5823a71 100644 --- a/backend/windmill-worker/src/rust_executor.rs +++ b/backend/windmill-worker/src/rust_executor.rs @@ -1,5 +1,5 @@ use serde_json::value::RawValue; -use std::{collections::HashMap, path::Path, process::Stdio}; +use std::{collections::HashMap, process::Stdio}; use uuid::Uuid; use windmill_parser_rust::parse_rust_deps_into_manifest; @@ -15,8 +15,7 @@ use windmill_queue::{append_logs, CanceledBy}; use crate::{ common::{ - create_args_and_out_file, get_reserved_variables, read_result, start_child_process, - OccupancyMetrics, + check_executor_binary_exists, create_args_and_out_file, get_reserved_variables, read_result, start_child_process, OccupancyMetrics }, handle_child::handle_child, AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV, @@ -132,7 +131,7 @@ pub async fn generate_cargo_lockfile( w_id: &str, occupancy_metrics: &mut OccupancyMetrics, ) -> error::Result { - check_cargo_exists()?; + check_executor_binary_exists("cargo", CARGO_PATH.as_str(), "rust")?; gen_cargo_crate(code, job_dir)?; @@ -271,24 +270,6 @@ pub fn compute_rust_hash(code: &str, requirements_o: Option<&String>) -> String )) } -#[cfg(not(feature = "enterprise"))] -fn check_cargo_exists() -> Result<(), Error> { - if !Path::new(CARGO_PATH.as_str()).exists() { - let msg = format!("Couldn't find cargo at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full` for your instance in order to run rust jobs.", CARGO_PATH.as_str()); - return Err(Error::NotFound(msg)); - } - Ok(()) -} - -#[cfg(feature = "enterprise")] -fn check_cargo_exists() -> Result<(), Error> { - if !Path::new(CARGO_PATH.as_str()).exists() { - let msg = format!("Couldn't find cargo at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-ee-full` for your instance in order to run rust jobs.", CARGO_PATH.as_str()); - return Err(Error::NotFound(msg)); - } - Ok(()) -} - #[tracing::instrument(level = "trace", skip_all)] pub async fn handle_rust_job( mem_peak: &mut i32, @@ -305,7 +286,7 @@ pub async fn handle_rust_job( envs: HashMap, occupancy_metrics: &mut OccupancyMetrics, ) -> Result, Error> { - check_cargo_exists()?; + check_executor_binary_exists("cargo", CARGO_PATH.as_str(), "rust")?; let hash = compute_rust_hash(inner_content, requirements_o.as_ref()); let bin_path = format!("{}/{hash}", RUST_CACHE_DIR); diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 58a5c1f43bb64..396607505b9f7 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -97,6 +97,7 @@ use crate::{ build_args_map, cached_result_path, get_cached_resource_value_if_valid, get_reserved_variables, update_worker_ping_for_failed_init_script, OccupancyMetrics, }, + csharp_executor::handle_csharp_job, deno_executor::handle_deno_job, go_executor::handle_go_job, graphql_executor::do_graphql, @@ -268,6 +269,7 @@ pub const DENO_CACHE_DIR_NPM: &str = concatcp!(ROOT_CACHE_DIR, "deno/npm"); pub const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go"); pub const RUST_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "rust"); +pub const CSHARP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "csharp"); pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "bun"); pub const BUN_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun"); pub const BUN_DEPSTAR_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "buntar"); @@ -370,6 +372,7 @@ lazy_static::lazy_static! { pub static ref POWERSHELL_PATH: String = std::env::var("POWERSHELL_PATH").unwrap_or_else(|_| "/usr/bin/pwsh".to_string()); pub static ref PHP_PATH: String = std::env::var("PHP_PATH").unwrap_or_else(|_| "/usr/bin/php".to_string()); pub static ref COMPOSER_PATH: String = std::env::var("COMPOSER_PATH").unwrap_or_else(|_| "/usr/bin/composer".to_string()); + pub static ref DOTNET_PATH: String = std::env::var("DOTNET_PATH").unwrap_or_else(|_| "/usr/bin/dotnet".to_string()); pub static ref NSJAIL_PATH: String = std::env::var("NSJAIL_PATH").unwrap_or_else(|_| "nsjail".to_string()); pub static ref PATH_ENV: String = std::env::var("PATH").unwrap_or_else(|_| String::new()); pub static ref HOME_ENV: String = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); @@ -384,6 +387,7 @@ lazy_static::lazy_static! { pub static ref NPM_CONFIG_REGISTRY: Arc>> = Arc::new(RwLock::new(None)); pub static ref BUNFIG_INSTALL_SCOPES: Arc>> = Arc::new(RwLock::new(None)); + pub static ref NUGET_CONFIG: Arc>> = Arc::new(RwLock::new(None)); pub static ref PIP_EXTRA_INDEX_URL: Arc>> = Arc::new(RwLock::new(None)); pub static ref PIP_INDEX_URL: Arc>> = Arc::new(RwLock::new(None)); @@ -2710,6 +2714,24 @@ mount {{ ) .await } + Some(ScriptLang::CSharp) => { + handle_csharp_job( + mem_peak, + canceled_by, + job, + db, + client, + &inner_content, + job_dir, + requirements_o, + &shared_mount, + base_internal_url, + worker_name, + envs, + occupancy_metrics, + ) + .await + } _ => panic!("unreachable, language is not supported: {language:#?}"), }; tracing::info!( diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 0f66376d8f291..82baf71030504 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -28,10 +28,11 @@ use windmill_parser_ts::parse_expr_for_imports; use windmill_queue::{append_logs, CanceledBy, PushIsolationLevel}; use crate::common::OccupancyMetrics; +use crate::csharp_executor::generate_nuget_lockfile; use crate::python_executor::{ create_dependencies_dir, handle_python_reqs, uv_pip_compile, USE_PIP_COMPILE, USE_PIP_INSTALL, }; -use crate::rust_executor::{build_rust_crate, compute_rust_hash, generate_cargo_lockfile}; +use crate::rust_executor::generate_cargo_lockfile; use crate::{ bun_executor::gen_bun_lockfile, deno_executor::generate_deno_lock, @@ -1869,20 +1870,26 @@ async fn capture_dependency_job( ) .await?; - build_rust_crate( + Ok(lockfile) + } + ScriptLang::CSharp => { + if raw_deps { + return Err(Error::ExecutionErr( + "Raw dependencies not supported for C#".to_string(), + )); + } + + generate_nuget_lockfile( job_id, + job_raw_code, mem_peak, canceled_by, job_dir, db, worker_name, w_id, - base_internal_url, - &compute_rust_hash(&job_raw_code, Some(&lockfile)), occupancy_metrics, - ) - .await?; - Ok(lockfile) + ).await } ScriptLang::Postgresql => Ok("".to_owned()), ScriptLang::Mysql => Ok("".to_owned()), diff --git a/docker/DockerfileFull b/docker/DockerfileFull index c28b2b256a9ce..3678b243b4665 100644 --- a/docker/DockerfileFull +++ b/docker/DockerfileFull @@ -4,3 +4,6 @@ COPY --from=rust:1.80.1 /usr/local/cargo /usr/local/cargo COPY --from=rust:1.80.1 /usr/local/rustup /usr/local/rustup RUN pip3 install ansible + +COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk +RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet diff --git a/docker/DockerfileFullEe b/docker/DockerfileFullEe index bdf8c5fff2315..b697e87d21b31 100644 --- a/docker/DockerfileFullEe +++ b/docker/DockerfileFullEe @@ -4,3 +4,6 @@ COPY --from=rust:1.80.1 /usr/local/cargo /usr/local/cargo COPY --from=rust:1.80.1 /usr/local/rustup /usr/local/rustup RUN pip3 install ansible + +COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk +RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 50063aeeb9081..71ea2846b842f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -69,6 +69,7 @@ "vscode-languageclient": "~9.0.1", "vscode-uri": "~3.0.8", "vscode-ws-jsonrpc": "~3.3.2", + "windmill-parser-wasm-csharp": "^1.437.1", "windmill-parser-wasm-go": "^1.429.0", "windmill-parser-wasm-php": "^1.429.0", "windmill-parser-wasm-py": "^1.429.0", @@ -142,6 +143,11 @@ "svelte": "^4.0.0" } }, + "../backend/parsers/windmill-parser-wasm/pkg-csharp": { + "name": "windmill-parser-wasm-csharp", + "version": "1.411.1", + "extraneous": true + }, "../svelte-dnd-action": { "extraneous": true }, @@ -13644,6 +13650,11 @@ "node": ">= 8" } }, + "node_modules/windmill-parser-wasm-csharp": { + "version": "1.437.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-csharp/-/windmill-parser-wasm-csharp-1.437.1.tgz", + "integrity": "sha512-qzB/kUE9JCf1CYFDz+50AI+SUVaZYn3lSgqmJ11Iuibl41AC4EIvfH4zrsOU53lcTOb9b4ZH3D6z9FjCCfqWsw==" + }, "node_modules/windmill-parser-wasm-go": { "version": "1.429.0", "resolved": "https://registry.npmjs.org/windmill-parser-wasm-go/-/windmill-parser-wasm-go-1.429.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7427139167ad8..c0da3a8393c75 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -142,6 +142,7 @@ "vscode-languageclient": "~9.0.1", "vscode-uri": "~3.0.8", "vscode-ws-jsonrpc": "~3.3.2", + "windmill-parser-wasm-csharp": "^1.437.1", "windmill-parser-wasm-go": "^1.429.0", "windmill-parser-wasm-php": "^1.429.0", "windmill-parser-wasm-py": "^1.429.0", diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index a894d1359ccff..1f3dcb83b2bbe 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -191,6 +191,7 @@ | 'javascript' | 'rust' | 'yaml' + | 'csharp' export let code: string = '' export let cmdEnterAction: (() => void) | undefined = undefined export let formatAction: (() => void) | undefined = undefined diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index e3d09ee007bdc..15b5f0a2d7787 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -91,7 +91,8 @@ 'bunnative', 'nativets', 'php', - 'rust' + 'rust', + 'csharp' ].includes(lang ?? '') $: showVarPicker = [ 'python3', @@ -103,7 +104,8 @@ 'bunnative', 'nativets', 'php', - 'rust' + 'rust', + 'csharp' ].includes(lang ?? '') $: showResourcePicker = [ 'python3', @@ -115,7 +117,8 @@ 'bunnative', 'nativets', 'php', - 'rust' + 'rust', + 'csharp' ].includes(lang ?? '') $: showResourceTypePicker = ['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) || @@ -283,6 +286,25 @@ } } + function windmillPathToCamelCaseName(path: string): string { + const parts = path.split('/') + const lastPart = parts[parts.length - 1] + + const words = lastPart.split('_') + + return words + .map((word, index) => { + if (index === 0) { + // Lowercase the first word + return word.toLowerCase() + } else { + // Capitalize the first letter of subsequent words + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + } + }) + .join('') + } + let historyBrowserDrawerOpen = false @@ -342,6 +364,10 @@ editor.insertAtCursor(`$Env:${name}`) } else if (lang == 'php') { editor.insertAtCursor(`getenv('${name}');`) + } else if (lang == 'rust') { + editor.insertAtCursor(`std::env::var("${name}").unwrap();`) + } else if (lang == 'csharp') { + editor.insertAtCursor(`Environment.GetEnvironmentVariable("${name}");`) } sendUserToast(`${name} inserted at cursor`) }} @@ -398,6 +424,15 @@ curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN'))); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $var = json_decode(curl_exec($ch));`) + } else if (lang == 'csharp') { + editor.insertAtCursor(`var baseUrl = Environment.GetEnvironmentVariable("BASE_INTERNAL_URL"); +var workspace = Environment.GetEnvironmentVariable("WM_WORKSPACE"); +var uri = $"{baseUrl}/api/w/{workspace}/variables/get_value/${path}"; +using var client = new HttpClient(); +client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("WM_TOKEN")}"); + +string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri); +`) } sendUserToast(`${name} inserted at cursor`) }} @@ -468,6 +503,18 @@ $var = json_decode(curl_exec($ch));`) curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN'))); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $res = json_decode(curl_exec($ch));`) + } else if (lang == 'csharp') { + if (!editor.getCode().includes(`using System.Text.Json.Nodes;`)) { + editor.insertAtBeginning(`using System.Text.Json.Nodes;\n`) + } + editor.insertAtCursor(`var baseUrl = Environment.GetEnvironmentVariable("BASE_INTERNAL_URL"); +var workspace = Environment.GetEnvironmentVariable("WM_WORKSPACE"); +var uri = $"{baseUrl}/api/w/{workspace}/resources/get_value_interpolated/${path}"; +using var client = new HttpClient(); +client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("WM_TOKEN")}"); + +JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetStringAsync(uri)); +`) } sendUserToast(`${path} inserted at cursor`) }} diff --git a/frontend/src/lib/components/HighlightCode.svelte b/frontend/src/lib/components/HighlightCode.svelte index 9f9d525625128..80857ef0b0fd3 100644 --- a/frontend/src/lib/components/HighlightCode.svelte +++ b/frontend/src/lib/components/HighlightCode.svelte @@ -10,6 +10,7 @@ import powershell from 'svelte-highlight/languages/powershell' import php from 'svelte-highlight/languages/php' import rust from 'svelte-highlight/languages/rust' + import csharp from 'svelte-highlight/languages/csharp' import yaml from 'svelte-highlight/languages/yaml' import type { Script } from '$lib/gen' import { Button } from './common' @@ -55,6 +56,8 @@ return php case 'rust': return rust + case 'csharp': + return csharp case 'ansible': return yaml; default: diff --git a/frontend/src/lib/components/InstanceSetting.svelte b/frontend/src/lib/components/InstanceSetting.svelte index 68ff3e1edf3ac..ef81b7f32a01e 100644 --- a/frontend/src/lib/components/InstanceSetting.svelte +++ b/frontend/src/lib/components/InstanceSetting.svelte @@ -26,6 +26,7 @@ import { createEventDispatcher } from 'svelte' import { fade } from 'svelte/transition' import { base } from '$lib/base' + import SimpleEditor from './SimpleEditor.svelte' export let setting: Setting export let version: string @@ -179,6 +180,14 @@ > {/if} + {:else if setting.fieldType == 'codearea'} + {:else if setting.fieldType == 'license_key'} {@const { valid, expiration } = parseLicenseKey($values[setting.key] ?? '')}
diff --git a/frontend/src/lib/components/WorkerGroup.svelte b/frontend/src/lib/components/WorkerGroup.svelte index b87513cb8accb..c0c34e1bd5f1f 100644 --- a/frontend/src/lib/components/WorkerGroup.svelte +++ b/frontend/src/lib/components/WorkerGroup.svelte @@ -121,7 +121,8 @@ 'bun', 'php', 'rust', - 'ansible' + 'ansible', + 'csharp' ] const nativeTags = [ 'nativets', diff --git a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte index 881d8ba72fb35..9bf4241891ade 100644 --- a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte +++ b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte @@ -18,6 +18,7 @@ import PHPIcon from '$lib/components/icons/PHPIcon.svelte' import RustIcon from '$lib/components/icons/RustIcon.svelte' import AnsibleIcon from '$lib/components/icons/AnsibleIcon.svelte' + import CSharpIcon from '$lib/components/icons/CSharpIcon.svelte' export let lang: | SupportedLanguage @@ -50,7 +51,8 @@ bun: 'TypeScript', php: 'PHP', rust: 'Rust', - ansible: 'Ansible Playbook' + ansible: 'Ansible Playbook', + csharp: 'C sharpo' } const langToComponent: Record< @@ -78,7 +80,8 @@ graphql: GraphqlIcon, php: PHPIcon, rust: RustIcon, - ansible: AnsibleIcon + ansible: AnsibleIcon, + csharp: CSharpIcon } let subIconScale = width === 30 ? 0.6 : 0.8 diff --git a/frontend/src/lib/components/icons/CSharpIcon.svelte b/frontend/src/lib/components/icons/CSharpIcon.svelte new file mode 100644 index 0000000000000..36cd1777f37ec --- /dev/null +++ b/frontend/src/lib/components/icons/CSharpIcon.svelte @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index e5693e971742d..dfebf1f292444 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -13,6 +13,7 @@ export interface Setting { | 'password' | 'select' | 'textarea' + | 'codearea' | 'seconds' | 'email' | 'license_key' @@ -33,6 +34,7 @@ export interface Setting { isValid?: (value: any) => boolean error?: string defaultValue?: () => any + codeAreaLang?: string, } export type SettingStorage = 'setting' @@ -253,6 +255,16 @@ export const settings: Record = { placeholder: '"@myorg3" = { token = "mytoken", url = "https://registry.myorg.com/" }', storage: 'setting', ee_only: '' + }, + { + label: 'Nuget Config', + description: + 'Write a nuget.config file to set custom package sources and credentials', + key: 'nuget_config', + fieldType: 'codearea', + codeAreaLang: 'xml', + storage: 'setting', + ee_only: '' } ], Alerts: [ diff --git a/frontend/src/lib/editorUtils.ts b/frontend/src/lib/editorUtils.ts index 03ce7420ca6f6..33c54c4a0f026 100644 --- a/frontend/src/lib/editorUtils.ts +++ b/frontend/src/lib/editorUtils.ts @@ -80,6 +80,8 @@ export function langToExt(lang: string): string { return 'css' case 'ansible': return 'yml' + case 'csharp': + return 'cs' default: return 'unknown' } diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index 0bb53957f56a3..604ee9802c8be 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -21,6 +21,7 @@ import initGoParser, { parse_go } from 'windmill-parser-wasm-go' import initPhpParser, { parse_php } from 'windmill-parser-wasm-php' import initRustParser, { parse_rust } from 'windmill-parser-wasm-rust' import initYamlParser, { parse_ansible } from 'windmill-parser-wasm-yaml' +import initCSharpParser, { parse_csharp } from 'windmill-parser-wasm-csharp' import wasmUrlTs from 'windmill-parser-wasm-ts/windmill_parser_wasm_bg.wasm?url' import wasmUrlRegex from 'windmill-parser-wasm-regex/windmill_parser_wasm_bg.wasm?url' @@ -29,6 +30,7 @@ import wasmUrlGo from 'windmill-parser-wasm-go/windmill_parser_wasm_bg.wasm?url' import wasmUrlPhp from 'windmill-parser-wasm-php/windmill_parser_wasm_bg.wasm?url' import wasmUrlRust from 'windmill-parser-wasm-rust/windmill_parser_wasm_bg.wasm?url' import wasmUrlYaml from 'windmill-parser-wasm-yaml/windmill_parser_wasm_bg.wasm?url' +import wasmUrlCSharp from 'windmill-parser-wasm-csharp/windmill_parser_wasm_bg.wasm?url' import { workspaceStore } from './stores.js' import { argSigToJsonSchemaType } from './inferArgSig.js' @@ -60,6 +62,9 @@ async function initWasmGo() { async function initWasmYaml() { await initYamlParser(wasmUrlYaml) } +async function initWasmCSharp() { + await initCSharpParser(wasmUrlCSharp) +} export async function inferArgs( language: SupportedLanguage | 'bunnative' | undefined, @@ -161,6 +166,9 @@ export async function inferArgs( } else if (language == 'ansible') { await initWasmYaml() inferedSchema = JSON.parse(parse_ansible(code)) + } else if (language == 'csharp') { + await initWasmCSharp() + inferedSchema = JSON.parse(parse_csharp(code)) } else { return null } diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index 3cac89df8c5ec..ef82340a399e4 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -348,6 +348,56 @@ fn main(who_to_greet: String, numbers: Vec) -> anyhow::Result { } ` +const CSHARP_INIT_CODE = ` +#r "nuget: Humanizer, 2.14.1" + +using System; +using System.Linq; +using Humanizer; + + +class Script +{ + public static int Main(string[] extraWords, string word = "clue", int highNumberThreshold = 50) + { + Console.WriteLine("Hello, World!"); + + Console.WriteLine("Your chosen words are pluralized here:"); + + string[] newWordArray = extraWords.Concat(new[] { word }).ToArray(); + + foreach (var s in newWordArray) + { + Console.WriteLine($" {s.Pluralize()}"); + } + + var random = new Random(); + int randomNumber = random.Next(1, 101); + + Console.WriteLine($"Random number: {randomNumber}"); + + string greeting = randomNumber > highNumberThreshold ? "High number!" : "Low number!"; + greeting += " (according to the threshold parameter)"; + Console.WriteLine(greeting); + // Humanize a timespan + var timespan = TimeSpan.FromMinutes(90); + Console.WriteLine($"Timespan: {timespan.Humanize()}"); + + // Humanize numbers into words + int number = 123; + Console.WriteLine($"Number: {number.ToWords()}"); + + // Pluralize words + string singular = "apple"; + + // Humanize date difference + var date = DateTime.UtcNow.AddDays(-3); + Console.WriteLine($"Date: {date.Humanize()}"); + return 2; + } +} +` + const FETCH_INIT_CODE = `export async function main( url: string | undefined, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' = 'GET', @@ -772,6 +822,9 @@ export const INITIAL_CODE = { ansible: { script: ANSIBLE_PLAYBOOK_INIT_CODE }, + csharp: { + script: CSHARP_INIT_CODE + }, docker: { script: DOCKER_INIT_CODE }, @@ -876,6 +929,8 @@ export function initialCode( return INITIAL_CODE.rust.script } else if (language == 'ansible') { return INITIAL_CODE.ansible.script + } else if (language == 'csharp') { + return INITIAL_CODE.csharp.script } else if (language == 'bun' || language == 'bunnative') { if (kind == 'trigger') { return INITIAL_CODE.bun.trigger diff --git a/frontend/src/lib/scripts.ts b/frontend/src/lib/scripts.ts index 36c6a439da448..f741b71775bcd 100644 --- a/frontend/src/lib/scripts.ts +++ b/frontend/src/lib/scripts.ts @@ -39,6 +39,8 @@ export function scriptLangToEditorLang( return 'graphql' } else if (lang == 'ansible') { return 'yaml' + } else if (lang == 'csharp') { + return 'csharp' } else if (lang == undefined) { return 'typescript' } else { @@ -116,6 +118,7 @@ const scriptLanguagesArray: [SupportedLanguage | 'docker' | 'bunnative', string] ['php', 'PHP'], ['rust', 'Rust'], ['ansible', 'Ansible Playbook'], + ['csharp', 'C#'], ['docker', 'Docker'] ] export function processLangs(selected: string | undefined, langs: string[]): string[] { diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index c87cfedeb1178..02bc163746d68 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -730,7 +730,7 @@ priority: {job.priority}
{/if} - {#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'nativets', 'bash', 'powershell', 'php', 'rust', 'other', 'dependency'].includes(job.tag)} + {#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'nativets', 'bash', 'powershell', 'php', 'rust', 'other', 'ansible', 'csharp', 'dependency'].includes(job.tag)}
Tag: {job.tag}