Compare commits

...

28 Commits

Author SHA1 Message Date
wendrul
9a16e3f337 ADd feature flag 2024-12-12 19:26:08 +01:00
wendrul
d311592c5a Merge branch 'main' into win-444-add-support-for-csharp-c 2024-12-12 18:50:19 +01:00
wendrul
16712bd1c8 Disable csharp test 2024-12-12 18:41:35 +01:00
wendrul
e688ec6104 Windows compatibility 2024-12-12 18:41:21 +01:00
wendrul
02dccdb773 Add setting to set nuget package 2024-12-12 18:28:07 +01:00
wendrul
5d7240e2ba Add csharp to backend image for tests 2024-12-12 16:45:44 +01:00
wendrul
40344316d1 Update ee repo ref 2024-12-12 16:18:44 +01:00
wendrul
a3fad2cdcf Add opt mount in nsjail 2024-12-12 16:02:41 +01:00
wendrul
8a6a879948 npm install c# parser 2024-12-12 15:34:10 +01:00
wendrul
e1d373767f Touch Dockerfile 2024-12-12 15:28:39 +01:00
wendrul
f3a4031e0a Update dockerfile again 2024-12-12 15:28:04 +01:00
wendrul
5575899aa3 Merge remote-tracking branch 'origin/main' into win-444-add-support-for-csharp-c 2024-12-12 14:38:49 +01:00
wendrul
ac6b937b42 Update dockerfiles 2024-12-12 13:12:45 +01:00
wendrul
7befa69e05 Add dotnet to the images 2024-12-12 13:03:37 +01:00
wendrul
94f455690c Fix c# test 2024-12-12 11:58:19 +01:00
wendrul
dd04566f32 Add c_variadic only for wasm32 assembly 2024-12-12 11:57:44 +01:00
wendrul
6bae455f86 Remove unused imports 2024-12-12 11:40:02 +01:00
wendrul
4d541d7ae6 Remove c_variadic feature 2024-12-12 11:32:22 +01:00
wendrul
e66ee4f5a1 Merge remote-tracking branch 'origin/main' into win-444-add-support-for-csharp-c 2024-12-12 11:09:24 +01:00
wendrul
dc055604d8 Nsjail for c# 2024-12-12 00:26:11 +01:00
wendrul
044ce292b3 Update init code for c# 2024-12-11 16:55:20 +01:00
wendrul
935a915617 Add default parsing using serde_json::from_str 2024-12-11 16:41:36 +01:00
wendrul
07f08cb9ec Lockfile generation 2024-12-11 15:03:31 +01:00
wendrul
dc8b2b02a5 Csharp editor resource and var buttons 2024-12-11 13:02:30 +01:00
wendrul
48da52568e Parse async and void to fit the wrapper to the main sig 2024-12-11 12:22:03 +01:00
wendrul
c322c3400c Add logo 2024-12-10 17:08:45 +01:00
wendrul
cd2791d09e More types and req parsing 2024-12-10 17:06:30 +01:00
wendrul
f94fed5ee7 Add csharp stuff and merge after the rewrite
* Add csharp, boilerplate + minimal execution
    * Add initial parser
    * Frontend + wasm export of the tree sitter parser
    * Arg spread and use cache
2024-12-05 16:28:57 +01:00
67 changed files with 1678 additions and 122 deletions

View File

@@ -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
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

View File

@@ -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 }}

View File

@@ -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: |

View File

@@ -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: |

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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: |

42
backend/Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -1 +1 @@
dddc8d60d483a2ce8d78233a25a3899a2b1224ca
dddc8d60d483a2ce8d78233a25a3899a2b1224ca

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -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;

View File

@@ -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

View File

@@ -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<String>,
pub main_sig: MainArgSignature,
}
fn csharp_param_default_value<'a>(def: Node<'a>, code: &str) -> Option<serde_json::Value> {
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<CsharpMainSigMeta> {
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<MainArgSignature> {
Ok(parse_csharp_sig_meta(code)?.main_sig)
}
fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result<Typ> {
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<String>, 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<String>)> {
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<String>)>, Vec<usize>) {
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<String>)> {
// 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()))
);
}
}

View File

@@ -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::<Layout>()
.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::<Layout>()
.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");
}

View File

@@ -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"] }

View File

@@ -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

View File

@@ -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

View File

@@ -24,3 +24,6 @@ popd
pushd "pkg-yaml" && npm publish ${args}
popd
pushd "pkg-csharp" && npm publish ${args}
popd

View File

@@ -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))
}

View File

@@ -0,0 +1,4 @@
#pragma once
#define assert(ignore) ((void)0)
#define static_assert(cnd, msg) assert(cnd && msg)

View File

@@ -0,0 +1,3 @@
#pragma once
int isprint(int c);

View File

@@ -0,0 +1,3 @@
#pragma once
#define PRId32 "d"

View File

@@ -0,0 +1,5 @@
#pragma once
#define bool _Bool
#define true 1
#define false 0

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,12 @@
#pragma once
#include <stdint.h>
#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);

View File

@@ -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);

View File

@@ -0,0 +1,5 @@
#pragma once
typedef unsigned long clock_t;
#define CLOCKS_PER_SEC ((clock_t)1000000)
clock_t clock(void);

View File

@@ -0,0 +1,3 @@
#pragma once
int dup(int);

View File

@@ -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);

View File

@@ -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,
] {

View File

@@ -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,

View File

@@ -1798,6 +1798,48 @@ fn main(world: String) -> Result<String, String> {
assert_eq!(result, serde_json::json!("Hello Hyrule!"));
}
// #[sqlx::test(fixtures("base"))]
// async fn test_csharp_job(db: Pool<Postgres>) {
// 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<Postgres>) {
initialize_tracing().await;

View File

@@ -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

View File

@@ -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())

View File

@@ -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))

View File

@@ -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";

View File

@@ -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",
}
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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}

View File

@@ -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<String>,
job_dir: &str,
@@ -219,7 +200,7 @@ pub async fn handle_ansible_job(
envs: HashMap<String, String>,
occupancy_metrics: &mut OccupancyMetrics,
) -> windmill_common::error::Result<Box<RawValue>> {
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;

View File

@@ -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,

View File

@@ -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<CanceledBy>,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut OccupancyMetrics,
) -> error::Result<String> {
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<CanceledBy>,
_job_dir: &str,
_db: &sqlx::Pool<sqlx::Postgres>,
_worker_name: &str,
_w_id: &str,
_occupancy_metrics: &mut OccupancyMetrics,
) -> error::Result<String> {
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<String>)>,
lines_to_remove: Vec<usize>,
) -> 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!(" <PackageReference Include=\"{pkg}\" {version}/>")
})
.join("\n");
write_file(
job_dir,
"Main.csproj",
&format!(
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<StartupObject>WindmillScriptCSharpInternal.Wrapper</StartupObject>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
{pkgs}
</ItemGroup>
</Project>
"#
),
)?;
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::<Result<Vec<String>, 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<Args>(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<CanceledBy>,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
w_id: &str,
base_internal_url: &str,
hash: &str,
occupancy_metrics: &mut OccupancyMetrics,
) -> error::Result<String> {
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<usize>) -> 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<CanceledBy>,
_job: &QueuedJob,
_db: &sqlx::Pool<sqlx::Postgres>,
_client: &AuthedClientBackgroundTask,
_inner_content: &str,
_job_dir: &str,
_requirements_o: Option<String>,
_shared_mount: &str,
_base_internal_url: &str,
_worker_name: &str,
_envs: HashMap<String, String>,
_occupancy_metrics: &mut OccupancyMetrics,
) -> Result<Box<RawValue>, 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<CanceledBy>,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
inner_content: &str,
job_dir: &str,
requirements_o: Option<String>,
shared_mount: &str,
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
occupancy_metrics: &mut OccupancyMetrics,
) -> Result<Box<RawValue>, 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
}

View File

@@ -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;

View File

@@ -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<String>,
occupancy_metrics: &mut OccupancyMetrics,
) -> Result<String> {
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<String>,
@@ -165,7 +146,7 @@ pub async fn handle_php_job(
shared_mount: &str,
occupancy_metrics: &mut OccupancyMetrics,
) -> error::Result<Box<RawValue>> {
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() => {

View File

@@ -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<String> {
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<String, String>,
occupancy_metrics: &mut OccupancyMetrics,
) -> Result<Box<RawValue>, 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);

View File

@@ -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<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref BUNFIG_INSTALL_SCOPES: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref NUGET_CONFIG: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref PIP_EXTRA_INDEX_URL: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref PIP_INDEX_URL: Arc<RwLock<Option<String>>> = 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!(

View File

@@ -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()),

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -191,6 +191,7 @@
| 'javascript'
| 'rust'
| 'yaml'
| 'csharp'
export let code: string = ''
export let cmdEnterAction: (() => void) | undefined = undefined
export let formatAction: (() => void) | undefined = undefined

View File

@@ -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
</script>
@@ -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`)
}}

View File

@@ -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:

View File

@@ -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 @@
>
</div>
{/if}
{:else if setting.fieldType == 'codearea'}
<SimpleEditor
autoHeight
class="editor"
lang={setting.codeAreaLang ?? "txt"}
bind:code={$values[setting.key]}
fixedOverflowWidgets={false}
/>
{:else if setting.fieldType == 'license_key'}
{@const { valid, expiration } = parseLicenseKey($values[setting.key] ?? '')}
<div class="flex gap-2">

View File

@@ -121,7 +121,8 @@
'bun',
'php',
'rust',
'ansible'
'ansible',
'csharp'
]
const nativeTags = [
'nativets',

View File

@@ -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

View File

@@ -0,0 +1,14 @@
<script>
export let height = '24px'
export let width = '24px'
</script>
<svg {width} {height} viewBox="0 0 256 288" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M255.569,84.452376 C255.567,79.622376 254.534,75.354376 252.445,71.691376 C250.393,68.089376 247.32,65.070376 243.198,62.683376 C209.173,43.064376 175.115,23.505376 141.101,3.86637605 C131.931,-1.42762395 123.04,-1.23462395 113.938,4.13537605 C100.395,12.122376 32.59,50.969376 12.385,62.672376 C4.064,67.489376 0.015,74.861376 0.013,84.443376 C0,123.898376 0.013,163.352376 0,202.808376 C0,207.532376 0.991,211.717376 2.988,215.325376 C5.041,219.036376 8.157,222.138376 12.374,224.579376 C32.58,236.282376 100.394,275.126376 113.934,283.115376 C123.04,288.488376 131.931,288.680376 141.104,283.384376 C175.119,263.744376 209.179,244.186376 243.209,224.567376 C247.426,222.127376 250.542,219.023376 252.595,215.315376 C254.589,211.707376 255.582,207.522376 255.582,202.797376 C255.582,202.797376 255.582,123.908376 255.569,84.452376" fill="#A179DC" fill-rule="nonzero"></path>
<path d="M128.182,143.241376 L2.988,215.325376 C5.041,219.036376 8.157,222.138376 12.374,224.579376 C32.58,236.282376 100.394,275.126376 113.934,283.115376 C123.04,288.488376 131.931,288.680376 141.104,283.384376 C175.119,263.744376 209.179,244.186376 243.209,224.567376 C247.426,222.127376 250.542,219.023376 252.595,215.315376 L128.182,143.241376" fill="#280068" fill-rule="nonzero"></path>
<path d="M255.569,84.452376 C255.567,79.622376 254.534,75.354376 252.445,71.691376 L128.182,143.241376 L252.595,215.315376 C254.589,211.707376 255.58,207.522376 255.582,202.797376 C255.582,202.797376 255.582,123.908376 255.569,84.452376" fill="#390091" fill-rule="nonzero"></path>
<path d="M201.892326,116.294008 L201.892326,129.767692 L215.36601,129.767692 L215.36601,116.294008 L222.102852,116.294008 L222.102852,129.767692 L235.576537,129.767692 L235.576537,136.504534 L222.102852,136.504534 L222.102852,149.978218 L235.576537,149.978218 L235.576537,156.71506 L222.102852,156.71506 L222.102852,170.188744 L215.36601,170.188744 L215.36601,156.71506 L201.892326,156.71506 L201.892326,170.188744 L195.155484,170.188744 L195.155484,156.71506 L181.6818,156.71506 L181.6818,149.978218 L195.155484,149.978218 L195.155484,136.504534 L181.6818,136.504534 L181.6818,129.767692 L195.155484,129.767692 L195.155484,116.294008 L201.892326,116.294008 Z M215.36601,136.504534 L201.892326,136.504534 L201.892326,149.978218 L215.36601,149.978218 L215.36601,136.504534 Z" fill="#FFFFFF"></path>
<path d="M128.456752,48.625876 C163.600523,48.625876 194.283885,67.7121741 210.718562,96.0819435 L210.558192,95.808876 L169.209615,119.617159 C161.062959,105.823554 146.128136,96.5150717 128.996383,96.3233722 L128.456752,96.3203544 C102.331178,96.3203544 81.1506705,117.499743 81.1506705,143.625316 C81.1506705,152.168931 83.4284453,160.17752 87.3896469,167.094792 C95.543745,181.330045 110.872554,190.931398 128.456752,190.931398 C146.149522,190.931398 161.565636,181.208041 169.67832,166.820563 L169.481192,167.165876 L210.767678,191.083913 C194.51328,219.21347 164.25027,238.240861 129.514977,238.620102 L128.456752,238.625876 C93.2021701,238.625876 62.4315028,219.422052 46.0382398,190.902296 C38.0352471,176.979327 33.4561922,160.837907 33.4561922,143.625316 C33.4561922,91.1592636 75.9884604,48.625876 128.456752,48.625876 Z" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

View File

@@ -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<string, Setting[]> = {
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: [

View File

@@ -80,6 +80,8 @@ export function langToExt(lang: string): string {
return 'css'
case 'ansible':
return 'yml'
case 'csharp':
return 'cs'
default:
return 'unknown'
}

View File

@@ -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
}

View File

@@ -348,6 +348,56 @@ fn main(who_to_greet: String, numbers: Vec<i8>) -> anyhow::Result<Ret> {
}
`
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

View File

@@ -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[] {

View File

@@ -730,7 +730,7 @@
<Badge color="blue">priority: {job.priority}</Badge>
</div>
{/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)}
<div>
<Badge color="indigo">Tag: {job.tag}</Badge>
</div>