mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
feat: add ruby support (#5939)
* implement single line pin * make panic-safe * use pin even if multiple modules imported withing single statement * add repins and make imports respect pins * keep all pins * Allow multiple pins * add comments + handle stuff more safely * fix fully qualified imports * remove ignore * sort nested * apply unique to output requirements list * fix typo * remove mut * update sqlx * sort imports * sort imports * fix formatter and format * feat: add ruby support * fix * update sqlx * add ruby to flake.nix Signed-off-by: pyranota <pyra@duck.com> * upgrade par_install_language_dependencies helper rename fields + allow passing custom payload Signed-off-by: pyranota <pyra@duck.com> * change migrations Signed-off-by: pyranota <pyra@duck.com> * make full pipeline work! Signed-off-by: pyranota <pyra@duck.com> * do a bit fixing * stage Signed-off-by: pyranota <pyra@duck.com> * add ruby in dockerfiles Signed-off-by: pyranota <pyra@duck.com> * implement auto-require + 'windmill/inline' Signed-off-by: pyranota <pyra@duck.com> * create 'windmill/inline' only if does not exist Signed-off-by: pyranota <pyra@duck.com> * add nsjail Signed-off-by: pyranota <pyra@duck.com> * update global settings Signed-off-by: pyranota <pyra@duck.com> * add ruby icon * generate lockfile on deploy (but broken) Signed-off-by: pyranota <pyra@duck.com> * fix(frontend): display deployed script lockfile and other info Signed-off-by: pyranota <pyra@duck.com> * feat: add mini-wmill client Methods: - get_resource - get_variable Additionally add shortcuts on frontend Signed-off-by: pyranota <pyra@duck.com> * add nit warning Signed-off-by: pyranota <pyra@duck.com> * support private repos credentials Signed-off-by: pyranota <pyra@duck.com> * fix compilation error Signed-off-by: pyranota <pyra@duck.com> * found rustc bug + refactor universal installer Signed-off-by: pyranota <pyra@duck.com> * more refactor + fixes Signed-off-by: pyranota <pyra@duck.com> * bug fixing + polishing Signed-off-by: pyranota <pyra@duck.com> * cleanup Signed-off-by: pyranota <pyra@duck.com> * change 'windmill/inline' Signed-off-by: pyranota <pyra@duck.com> * delegate env_clear to universal installer Signed-off-by: pyranota <pyra@duck.com> * update init script Signed-off-by: pyranota <pyra@duck.com> * add cli support Signed-off-by: pyranota <pyra@duck.com> * unhardcode frontend Signed-off-by: pyranota <pyra@duck.com> * fix cli Signed-off-by: pyranota <pyra@duck.com> * fix init script Signed-off-by: pyranota <pyra@duck.com> * make it work on ms windows Signed-off-by: pyranota <pyra@duck.com> * do not clean env in the end Signed-off-by: pyranota <pyra@duck.com> * fix windows (again) Signed-off-by: pyranota <pyra@duck.com> * include PROXY_ENVS (untested) Signed-off-by: pyranota <pyra@duck.com> * feat: self-signed certs + proxy support (tested) Signed-off-by: pyranota <pyra@duck.com> * flake: pin ruby to 3.4 * merge follow-ups Signed-off-by: pyranota <pyra@duck.com> * remove obsolete libc crate Signed-off-by: pyranota <pyra@duck.com> * cleanup parser Signed-off-by: pyranota <pyra@duck.com> * fix java ci Signed-off-by: pyranota <pyra@duck.com> * cleanup Signed-off-by: pyranota <pyra@duck.com> * fix nits Signed-off-by: pyranota <pyra@duck.com> * Update backend/windmill-worker/src/universal_pkg_installer.rs Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update backend/windmill-worker/src/ruby_executor.rs Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * add nsjail for lock stage Signed-off-by: pyranota <pyra@duck.com> * fix merge Signed-off-by: pyranota <pyra@duck.com> * Update common.rs * regen windmill-api-client Signed-off-by: pyranota <pyra@duck.com> * fix tests Signed-off-by: pyranota <pyra@duck.com> * fix tests again Signed-off-by: pyranota <pyra@duck.com> * reference published ruby parser Signed-off-by: pyranota <pyra@duck.com> --------- Signed-off-by: pyranota <pyra@duck.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
ellipsis-dev[bot]
parent
9b07d6d546
commit
11dd4118ce
+2
-1
@@ -62,7 +62,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -71,7 +71,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -138,7 +138,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4fb3881cdbb4b9e93e28f460a9b3715bdc6a52b76c89f3a3913023b13c4e085c"
|
||||
}
|
||||
+2
-1
@@ -62,7 +62,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -148,7 +148,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -42,7 +42,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -66,7 +66,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -71,7 +71,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n cj.id AS \"id!\",\n cj.workspace_id AS \"workspace_id!\",\n cj.parent_job,\n cj.created_by AS \"created_by!\",\n cj.duration_ms AS \"duration_ms!\",\n cj.success AS \"success!\",\n cj.script_hash AS \"script_hash!: Option<ScriptHash>\",\n cj.script_path,\n cj.args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n cj.result AS \"result: sqlx::types::Json<Box<RawValue>>\",\n cj.deleted AS \"deleted!\",\n cj.canceled AS \"canceled!\",\n cj.canceled_by,\n cj.canceled_reason,\n cj.job_kind AS \"job_kind!: JobKind\",\n cj.schedule_path,\n cj.permissioned_as AS \"permissioned_as!\",\n cj.is_flow_step AS \"is_flow_step!\",\n cj.language AS \"language: ScriptLang\",\n cj.is_skipped AS \"is_skipped!\",\n cj.email AS \"email!\",\n cj.visible_to_owner AS \"visible_to_owner!\",\n cj.mem_peak,\n cj.tag AS \"tag!\",\n cj.created_at AS \"created_at!\",\n cj.started_at,\n job_logs.logs,\n job_logs.log_offset AS \"log_offset?\",\n job_logs.log_file_index\n\n FROM v2_as_completed_job AS cj\n LEFT JOIN job_logs ON cj.id = job_logs.job_id\n WHERE cj.created_at < $1\n ORDER BY cj.created_at ASC LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_by!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "duration_ms!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "success!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_hash!: Option<ScriptHash>",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "args: sqlx::types::Json<HashMap<String, Box<RawValue>>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "deleted!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "canceled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "canceled_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "canceled_reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "job_kind!: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "schedule_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "permissioned_as!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "is_flow_step!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "language: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "is_skipped!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "email!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "visible_to_owner!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "mem_peak",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "tag!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "logs",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "log_offset?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "log_file_index",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7"
|
||||
}
|
||||
+2
-1
@@ -66,7 +66,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+26
@@ -13854,6 +13854,16 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8"
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-ruby"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.14"
|
||||
@@ -15214,6 +15224,20 @@ dependencies = [
|
||||
"windmill-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.523.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"tree-sitter",
|
||||
"tree-sitter-ruby",
|
||||
"wasm-bindgen",
|
||||
"windmill-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.526.1"
|
||||
@@ -15281,6 +15305,7 @@ dependencies = [
|
||||
"windmill-parser-nu",
|
||||
"windmill-parser-php",
|
||||
"windmill-parser-py",
|
||||
"windmill-parser-ruby",
|
||||
"windmill-parser-rust",
|
||||
"windmill-parser-sql",
|
||||
"windmill-parser-ts",
|
||||
@@ -15427,6 +15452,7 @@ dependencies = [
|
||||
"windmill-parser-php",
|
||||
"windmill-parser-py",
|
||||
"windmill-parser-py-imports",
|
||||
"windmill-parser-ruby",
|
||||
"windmill-parser-rust",
|
||||
"windmill-parser-sql",
|
||||
"windmill-parser-ts",
|
||||
|
||||
+6
-3
@@ -24,6 +24,7 @@ members = [
|
||||
"./parsers/windmill-parser-csharp",
|
||||
"./parsers/windmill-parser-nu",
|
||||
"./parsers/windmill-parser-java",
|
||||
"./parsers/windmill-parser-ruby",
|
||||
"./parsers/windmill-parser-bash",
|
||||
"./parsers/windmill-parser-py",
|
||||
"./parsers/windmill-parser-py-imports",
|
||||
@@ -48,7 +49,7 @@ incremental = true
|
||||
lto = "thin"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["ruby"]
|
||||
private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
|
||||
agent_worker_server = ["windmill-api/agent_worker_server"]
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
|
||||
@@ -95,7 +96,8 @@ php = ["windmill-worker/php"]
|
||||
csharp = ["windmill-worker/csharp"]
|
||||
nu = ["windmill-worker/nu"]
|
||||
java = ["windmill-worker/java"]
|
||||
all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
|
||||
ruby = ["windmill-worker/ruby"]
|
||||
all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java", "ruby"]
|
||||
# For windows we have another set of languages enabled
|
||||
# NOTE: DuckDB is ignored because of compilation problems
|
||||
all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
|
||||
@@ -145,7 +147,6 @@ systemstat.workspace = true
|
||||
size.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator = { optional = true, workspace = true }
|
||||
tikv-jemalloc-sys = { optional = true, workspace = true }
|
||||
@@ -180,6 +181,7 @@ 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-java = { path = "./parsers/windmill-parser-java" }
|
||||
windmill-parser-ruby = { path = "./parsers/windmill-parser-ruby" }
|
||||
windmill-parser-nu = { path = "./parsers/windmill-parser-nu" }
|
||||
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
|
||||
windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
|
||||
@@ -406,6 +408,7 @@ tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] }
|
||||
tree-sitter = { version = "0.23.0", features = [] }
|
||||
tree-sitter-c-sharp = "0.23.0"
|
||||
tree-sitter-java = "0.23.0"
|
||||
tree-sitter-ruby = "0.23.0"
|
||||
oracle = { version = "0.6.3", features = ["chrono"] }
|
||||
rumqttc = { version = "0.24.0", features = ["use-native-tls"]}
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'ruby';
|
||||
UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["ruby"]'::jsonb) where name = 'worker__default' and config @> '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "php", "rust", "ansible", "csharp", "nu", "java", "duckdb"]}'::jsonb AND NOT config->'worker_tags' @> '"ruby"'::jsonb;
|
||||
@@ -128,7 +128,7 @@ pub fn parse_python_signature(
|
||||
&& default.is_some()
|
||||
&& default != Some(json!(FUNCTION_CALL))
|
||||
{
|
||||
typ = json_to_typ(default.as_ref().unwrap());
|
||||
typ = json_to_typ(default.as_ref().unwrap(), false);
|
||||
}
|
||||
|
||||
// if the type is still a list of unknowns after checking the default, we set it to a list of strings to not break past behavior
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "windmill-parser-ruby"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_parser_ruby"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
windmill-parser.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-ruby.workspace = true
|
||||
anyhow.workspace = true
|
||||
wasm-bindgen.workspace = true
|
||||
serde_json.workspace = true
|
||||
regex.workspace = true
|
||||
lazy_static.workspace = true
|
||||
@@ -0,0 +1,410 @@
|
||||
#![cfg_attr(target_arch = "wasm32", feature(c_variadic))]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm_libc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use tree_sitter::Node;
|
||||
use tree_sitter::Range;
|
||||
use windmill_parser::json_to_typ;
|
||||
use windmill_parser::Arg;
|
||||
use windmill_parser::MainArgSignature;
|
||||
|
||||
pub fn parse_ruby_sig_meta(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let language = tree_sitter_ruby::LANGUAGE;
|
||||
parser
|
||||
.set_language(&language.into())
|
||||
.map_err(|e| anyhow!("Error setting Ruby as language: {e}"))?;
|
||||
|
||||
// Parse code
|
||||
let tree = parser
|
||||
.parse(code, None)
|
||||
.ok_or(anyhow!("Failed to parse code"))?;
|
||||
let root_node = tree.root_node();
|
||||
|
||||
root_node.clone().to_string();
|
||||
// Traverse the AST to find the Main method signature
|
||||
let args = find_main_signature(root_node, code)?;
|
||||
let no_main_func = Some(args.is_none());
|
||||
|
||||
let main_sig = MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: args.unwrap_or_default(),
|
||||
has_preprocessor: None,
|
||||
no_main_func,
|
||||
};
|
||||
|
||||
Ok(main_sig)
|
||||
}
|
||||
|
||||
pub fn parse_ruby_signature(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
Ok(parse_ruby_sig_meta(code)?)
|
||||
}
|
||||
|
||||
pub fn parse_ruby_requirements(code: &str) -> anyhow::Result<String> {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let language = tree_sitter_ruby::LANGUAGE;
|
||||
parser
|
||||
.set_language(&language.into())
|
||||
.map_err(|e| anyhow!("Error setting Ruby as language: {e}"))?;
|
||||
|
||||
// Parse code
|
||||
let tree = parser
|
||||
.parse(code, None)
|
||||
.ok_or(anyhow!("Failed to parse code"))?;
|
||||
let root_node = tree.root_node();
|
||||
|
||||
root_node.clone().to_string();
|
||||
let mut cursor = root_node.walk();
|
||||
'top_level: for x in root_node.children(&mut cursor) {
|
||||
if x.kind() == "call" {
|
||||
for (i, n) in x.children(&mut x.walk()).enumerate() {
|
||||
if i == 0
|
||||
&& n.kind() == "identifier"
|
||||
&& !n
|
||||
.utf8_text(code.as_bytes())
|
||||
.map(|ident| ident == "gemfile")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
continue 'top_level;
|
||||
} else if n.kind() == "do_block" {
|
||||
let req = n.utf8_text(code.as_bytes())?.to_owned();
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref WINDMILL_RE: Regex = Regex::new(r"(?m)^\s*require\s*'windmill/inline'").unwrap();
|
||||
}
|
||||
|
||||
if WINDMILL_RE.find(&code).is_none() {
|
||||
return Err(anyhow!(
|
||||
"`require 'windmill/inline'` is not detected - please add `require 'windmill/inline'` in order to use inline gemfile.
|
||||
Your Gemfile syntax will be compatible with bundler/inline."
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
return req
|
||||
// gemfile do_block comes with 'do' and 'end'
|
||||
// we want to omit these by taking slice
|
||||
.get(2..(req.len() - 3))
|
||||
.map(str::to_owned)
|
||||
.ok_or(anyhow!("Invalid gemfile do block"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
// Function to find the Main method's signature
|
||||
fn find_main_signature<'a>(root_node: Node<'a>, code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let mut cursor = root_node.walk();
|
||||
'top_level: for x in root_node.children(&mut cursor) {
|
||||
if x.kind() == "method" {
|
||||
let mut args = vec![];
|
||||
for (i, m) in x.children(&mut x.walk()).skip(1).enumerate() {
|
||||
if i == 0
|
||||
&& m.kind() == "identifier"
|
||||
&& !m
|
||||
.utf8_text(code.as_bytes())
|
||||
.map(|ident| ident == "main")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
continue 'top_level;
|
||||
} else if m.kind() == "method_parameters" {
|
||||
for a in m.children(&mut m.walk()) {
|
||||
if a.kind() == "identifier" {
|
||||
a.utf8_text(code.as_bytes()).inspect(|name| {
|
||||
args.push(Arg { name: (*name).to_owned(), ..Default::default() })
|
||||
})?;
|
||||
}
|
||||
if a.kind() == "optional_parameter" {
|
||||
let mut walk = a.walk();
|
||||
let mut it = a.children(&mut walk).into_iter();
|
||||
match (it.next().and_then(|n| n.utf8_text(code.as_bytes()).ok()), {
|
||||
// Skip `=`
|
||||
it.next();
|
||||
it.next().map(|n| n.range())
|
||||
}) {
|
||||
(Some(ident), Some(Range { start_byte, end_byte, .. })) => {
|
||||
let unparsed =
|
||||
&code[start_byte..end_byte].replace("nil", "null");
|
||||
let default: Value = serde_json::from_str(unparsed).map_err(
|
||||
|e|
|
||||
anyhow!("Cannot convert default value to json\n\tvalue: {unparsed}\n\terror: {e}{}",
|
||||
if e.to_string().contains("key must be a string") {
|
||||
"\n\nNOTE: If you are trying to declare default hash, use following syntax:\n { \"<key>\": <value> }"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))?;
|
||||
args.push(Arg {
|
||||
name: ident.to_owned(),
|
||||
typ: json_to_typ(&default, true),
|
||||
default: Some(default),
|
||||
has_default: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
let Range { start_byte, end_byte, .. } = a.range();
|
||||
bail!(
|
||||
"Cannot parse optional parameter: {}",
|
||||
&code.get(start_byte..end_byte).unwrap_or("CANNOT DISPLAY")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
let kind = a.kind();
|
||||
if matches!(
|
||||
kind,
|
||||
"keyword_parameter" | "splat_parameter" | "hash_splat_parameter"
|
||||
) {
|
||||
let Range { start_byte, end_byte, .. } = a.range();
|
||||
bail!(
|
||||
" - {}\n{}s are not supported",
|
||||
&code.get(start_byte..end_byte).unwrap_or("CANNOT DISPLAY"),
|
||||
kind.replace("_", " ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(Some(args));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use serde_json::json;
|
||||
use windmill_parser::{ObjectProperty, ObjectType, Typ};
|
||||
|
||||
use super::parse_ruby_sig_meta as parse;
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_parse_ruby_no_main() {
|
||||
let code = r#"
|
||||
def not_main end
|
||||
def private_fn end
|
||||
"#;
|
||||
let sig = parse(code).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature { no_main_func: Some(true), ..Default::default() }
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_ruby_no_args() {
|
||||
let code = r#"
|
||||
def main
|
||||
end
|
||||
"#;
|
||||
let sig = parse(code).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature { no_main_func: Some(false), ..Default::default() }
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_ruby_positional_args() {
|
||||
let sig = {
|
||||
let code = r#"def main(a, b, c) end"#;
|
||||
parse(code).unwrap()
|
||||
};
|
||||
let sig2 = {
|
||||
let code = r#"
|
||||
def main a, b, c
|
||||
end"#;
|
||||
parse(code).unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
args: vec![
|
||||
Arg { name: "a".into(), ..Default::default() },
|
||||
Arg { name: "b".into(), ..Default::default() },
|
||||
Arg { name: "c".into(), ..Default::default() }
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(sig2, sig);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_ruby_lists() {
|
||||
let sig = {
|
||||
let code = r#"def main(a = [ 1, 2, 3 ], b = [ 1, false, nil, "test"]) end"#;
|
||||
parse(code).unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
args: vec![
|
||||
Arg {
|
||||
name: "a".into(),
|
||||
has_default: true,
|
||||
typ: Typ::List(Box::new(Typ::Int)),
|
||||
default: Some(json!([1, 2, 3])),
|
||||
..Default::default()
|
||||
},
|
||||
Arg {
|
||||
name: "b".into(),
|
||||
has_default: true,
|
||||
typ: Typ::List(Box::new(Typ::Unknown)),
|
||||
default: Some(json!([1, false, null, "test"])),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_ruby_hashes() {
|
||||
let sig = {
|
||||
let code = r#"def main(a = { "1": 4, "2": [ 1, 2, 3] }) end"#;
|
||||
parse(code).unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
args: vec![Arg {
|
||||
name: "a".into(),
|
||||
has_default: true,
|
||||
typ: Typ::Object(ObjectType::new(
|
||||
None,
|
||||
Some(vec![
|
||||
ObjectProperty { key: "1".into(), typ: Box::new(Typ::Int) },
|
||||
ObjectProperty {
|
||||
key: "2".into(),
|
||||
typ: Box::new(Typ::List(Box::new(Typ::Int)))
|
||||
}
|
||||
],)
|
||||
)),
|
||||
default: Some(json!({"1": 4, "2": [ 1, 2, 3 ]})),
|
||||
..Default::default()
|
||||
},],
|
||||
no_main_func: Some(false),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_ruby_default_args() {
|
||||
let sig = {
|
||||
let code =
|
||||
r#"def main(a = 10, b = "hey", c = false, d = [ 1, 2, 3 ], e = { "a": 43 }) end"#;
|
||||
parse(code).unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
sig,
|
||||
MainArgSignature {
|
||||
args: vec![
|
||||
Arg {
|
||||
name: "a".into(),
|
||||
has_default: true,
|
||||
typ: Typ::Int,
|
||||
default: Some(json!(10)),
|
||||
..Default::default()
|
||||
},
|
||||
Arg {
|
||||
name: "b".into(),
|
||||
has_default: true,
|
||||
typ: Typ::Str(None),
|
||||
default: Some(json!("hey")),
|
||||
..Default::default()
|
||||
},
|
||||
Arg {
|
||||
name: "c".into(),
|
||||
has_default: true,
|
||||
typ: Typ::Bool,
|
||||
default: Some(json!(false)),
|
||||
..Default::default()
|
||||
},
|
||||
Arg {
|
||||
name: "d".into(),
|
||||
has_default: true,
|
||||
typ: Typ::List(Box::new(Typ::Int)),
|
||||
default: Some(json!([1, 2, 3])),
|
||||
..Default::default()
|
||||
},
|
||||
Arg {
|
||||
name: "e".into(),
|
||||
has_default: true,
|
||||
typ: Typ::Object(ObjectType::new(
|
||||
None,
|
||||
Some(vec![ObjectProperty {
|
||||
key: "a".into(),
|
||||
typ: Box::new(Typ::Int)
|
||||
}])
|
||||
)),
|
||||
default: Some(json!({"a": 43})),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ruby_unsupported() {
|
||||
assert_eq!(
|
||||
&parse("def main( a: ) end").unwrap_err().to_string(),
|
||||
" - a:\nkeyword parameters are not supported"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&parse("def main( *a ) end").unwrap_err().to_string(),
|
||||
" - *a\nsplat parameters are not supported"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&parse("def main( **a ) end").unwrap_err().to_string(),
|
||||
" - **a\nhash splat parameters are not supported"
|
||||
);
|
||||
|
||||
assert!(&parse("def main( a = Time.now ) end").is_err());
|
||||
assert!(&parse("def main( a = :symbol ) end").is_err());
|
||||
assert!(&parse("def main( a = { b: 2 } ) end").is_err());
|
||||
assert!(&parse("def main( a = { b => 2 } ) end").is_err());
|
||||
assert!(&parse(r#"def main( a = { "b" => 2 } ) end"#).is_err());
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_parse_ruby_requirements() {
|
||||
// assert_eq!(
|
||||
// parse_ruby_requirements(
|
||||
// "
|
||||
// require 'dep1'
|
||||
// require 'dep2'
|
||||
// require 'dep2/submod'
|
||||
// require 'u/username/script'
|
||||
// require 'f/folder/script'
|
||||
// "
|
||||
// )
|
||||
// .unwrap(),
|
||||
// vec![
|
||||
// "dep1".to_owned(),
|
||||
// "dep2".into(),
|
||||
// "dep2".into(),
|
||||
// "u/username/script".into(),
|
||||
// "f/folder/script".into(),
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::{
|
||||
alloc::{self, Layout},
|
||||
ffi::{c_char, c_int, c_void},
|
||||
mem::align_of,
|
||||
ptr,
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
// Implementation by AI:
|
||||
pub type size_t = usize;
|
||||
use std::slice;
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn memchr(
|
||||
haystack: *const c_void,
|
||||
needle: c_int,
|
||||
len: usize,
|
||||
) -> *mut c_void {
|
||||
if haystack.is_null() || len == 0 {
|
||||
return ptr::null_mut(); // Return null if the input pointer is null or length is zero
|
||||
}
|
||||
|
||||
let needle_byte = needle as u8; // Convert needle to a byte
|
||||
|
||||
// Create a pointer to the start of the haystack
|
||||
let mut current = haystack as *const u8;
|
||||
|
||||
// Iterate through the memory block
|
||||
for _ in 0..len {
|
||||
if *current == needle_byte {
|
||||
return current as *mut c_void; // Return the pointer to the found byte
|
||||
}
|
||||
current = current.add(1); // Move to the next byte
|
||||
}
|
||||
|
||||
ptr::null_mut() // Return null if the byte was not found
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn strchr(mut s: *const c_char, c: c_int) -> *mut c_char {
|
||||
if s.is_null() {
|
||||
return std::ptr::null_mut(); // Return null if the input string is null
|
||||
}
|
||||
|
||||
let target = c as u8 as char; // Convert c to a char
|
||||
let mut current = s;
|
||||
|
||||
// Iterate through the string until we find the character or reach the end
|
||||
while *current != 0 {
|
||||
if *current as u8 as char == target {
|
||||
return current as *mut c_char; // Return the pointer to the found character
|
||||
}
|
||||
current = current.add(1); // Move to the next character
|
||||
}
|
||||
|
||||
std::ptr::null_mut() // Return null if the character was not found
|
||||
}
|
||||
// End of AI implemetation
|
||||
/* -------------------------------- 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())
|
||||
}
|
||||
|
||||
// Implementation by AI:
|
||||
pub type wint_t = u32;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn iswdigit(wc: wint_t) -> c_int {
|
||||
// Check if the character is a digit ('0' to '9')
|
||||
if wc >= '0' as wint_t && wc <= '9' as wint_t {
|
||||
return 1; // Return true (1)
|
||||
}
|
||||
0 // Return false (0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn iswupper(wc: wint_t) -> c_int {
|
||||
// Check if the character is an uppercase letter ('A' to 'Z')
|
||||
if wc >= 'A' as wint_t && wc <= 'Z' as wint_t {
|
||||
return 1; // Return true (1)
|
||||
}
|
||||
0 // Return false (0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn iswalpha(wc: wint_t) -> c_int {
|
||||
// Check if the character is an alphabetic character ('A' to 'Z' or 'a' to 'z')
|
||||
if (wc >= 'A' as wint_t && wc <= 'Z' as wint_t) || (wc >= 'a' as wint_t && wc <= 'z' as wint_t) {
|
||||
return 1; // Return true (1)
|
||||
}
|
||||
0 // Return false (0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn iswlower(wc: wint_t) -> c_int {
|
||||
// Check if the character is a lowercase letter ('a' to 'z')
|
||||
if wc >= 'a' as wint_t && wc <= 'z' as wint_t {
|
||||
return 1; // Return true (1)
|
||||
}
|
||||
0 // Return false (0)
|
||||
}
|
||||
// End of AI implemetation
|
||||
|
||||
|
||||
/* --------------------------------- 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!("clock_gettime is not supported");
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
@@ -388,7 +388,7 @@ fn parse_param(
|
||||
};
|
||||
|
||||
if typ == Typ::Unknown && dflt.is_some() {
|
||||
typ = json_to_typ(dflt.as_ref().unwrap());
|
||||
typ = json_to_typ(dflt.as_ref().unwrap(), false);
|
||||
}
|
||||
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ ansible-parser = [ "dep:windmill-parser-yaml"]
|
||||
csharp-parser = [ "dep:windmill-parser-csharp"]
|
||||
nu-parser = [ "dep:windmill-parser-nu"]
|
||||
java-parser = [ "dep:windmill-parser-java"]
|
||||
ruby-parser = [ "dep:windmill-parser-ruby"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -45,6 +46,7 @@ windmill-parser-yaml = { workspace = true, optional = true }
|
||||
windmill-parser-csharp = { workspace = true, optional = true }
|
||||
windmill-parser-nu = { workspace = true, optional = true }
|
||||
windmill-parser-java = { workspace = true, optional = true }
|
||||
windmill-parser-ruby = { workspace = true, optional = true }
|
||||
wasm-bindgen.workspace = true
|
||||
serde_json.workspace = true
|
||||
getrandom = { workspace = true, features = ["js"] }
|
||||
|
||||
@@ -50,11 +50,11 @@ const targets = [
|
||||
desc: "Java",
|
||||
features: "java-parser",
|
||||
env: "tree-sitter",
|
||||
# }, {
|
||||
# ident: "ruby",
|
||||
# desc: "Ruby",
|
||||
# features: "ruby-parser",
|
||||
# env: "tree-sitter",
|
||||
}, {
|
||||
ident: "ruby",
|
||||
desc: "Ruby",
|
||||
features: "ruby-parser",
|
||||
env: "tree-sitter",
|
||||
},
|
||||
# ^^^ Add new entry here ^^^
|
||||
];
|
||||
@@ -130,3 +130,4 @@ def main [
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,11 @@ pub fn parse_java(code: &str) -> String {
|
||||
wrap_sig(windmill_parser_java::parse_java_signature(code))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ruby-parser")]
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_ruby(code: &str) -> String {
|
||||
wrap_sig(windmill_parser_ruby::parse_ruby_signature(code))
|
||||
}
|
||||
#[cfg(feature = "sql-parser")]
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_assets_sql(code: &str) -> String {
|
||||
|
||||
@@ -5,3 +5,7 @@ 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);
|
||||
|
||||
// Manually added (needed for Ruby)
|
||||
void *memchr (void *s, int c, size_t n);
|
||||
char *strchr (char *s, int c);
|
||||
|
||||
@@ -5,3 +5,9 @@ typedef __WINT_TYPE__ wint_t;
|
||||
|
||||
int iswspace(wchar_t ch);
|
||||
int iswalnum(wint_t _wc);
|
||||
|
||||
// Manually added (needed for Ruby)
|
||||
int iswdigit(wint_t _wc);
|
||||
int iswupper(wint_t _wc);
|
||||
int iswalpha(wint_t _wc);
|
||||
int iswlower(wint_t _wc);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::mem::discriminant;
|
||||
|
||||
use convert_case::{Boundary, Case, Casing};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
@@ -53,7 +55,7 @@ pub struct OneOfVariant {
|
||||
pub properties: Vec<ObjectProperty>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq)]
|
||||
#[derive(Serialize, Clone, Debug, PartialEq, Default)]
|
||||
#[serde(rename_all(serialize = "lowercase"))]
|
||||
pub enum Typ {
|
||||
Str(Option<Vec<String>>),
|
||||
@@ -69,10 +71,11 @@ pub enum Typ {
|
||||
DynSelect(String),
|
||||
Object(ObjectType),
|
||||
OneOf(Vec<OneOfVariant>),
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq)]
|
||||
#[derive(Serialize, Clone, Debug, PartialEq, Default)]
|
||||
pub struct Arg {
|
||||
pub name: String,
|
||||
pub otyp: Option<String>,
|
||||
@@ -82,7 +85,7 @@ pub struct Arg {
|
||||
pub oidx: Option<i32>,
|
||||
}
|
||||
|
||||
pub fn json_to_typ(js: &Value) -> Typ {
|
||||
pub fn json_to_typ(js: &Value, precise_arrays: bool) -> Typ {
|
||||
match js {
|
||||
Value::String(_) => Typ::Str(None),
|
||||
Value::Number(n) if n.is_i64() => Typ::Int,
|
||||
@@ -92,11 +95,28 @@ pub fn json_to_typ(js: &Value) -> Typ {
|
||||
None,
|
||||
Some(
|
||||
o.iter()
|
||||
.map(|(k, v)| ObjectProperty { key: k.to_string(), typ: Box::new(json_to_typ(v)) })
|
||||
.map(|(k, v)| ObjectProperty {
|
||||
key: k.to_string(),
|
||||
typ: Box::new(json_to_typ(v, precise_arrays)),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)),
|
||||
Value::Array(a) => Typ::List(Box::new(a.first().map(json_to_typ).unwrap_or(Typ::Unknown))),
|
||||
Value::Array(a) => Typ::List(Box::new({
|
||||
// Check if all variant types are the same
|
||||
if !precise_arrays
|
||||
|| a.windows(2).all(|pair| {
|
||||
let (l, r) = (&pair[0], &pair[1]);
|
||||
discriminant(l) == discriminant(r)
|
||||
})
|
||||
{
|
||||
a.first()
|
||||
.map(|js| json_to_typ(js, precise_arrays))
|
||||
.unwrap_or(Typ::Unknown)
|
||||
} else {
|
||||
Typ::Unknown
|
||||
}
|
||||
})),
|
||||
_ => Typ::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -10,7 +10,7 @@ use monitor::{
|
||||
load_base_url, load_otel, reload_critical_alerts_on_db_oversize,
|
||||
reload_delete_logs_periodically_setting, reload_indexer_config,
|
||||
reload_instance_python_version_setting, reload_maven_repos_setting,
|
||||
reload_no_default_maven_setting, reload_nuget_config_setting,
|
||||
reload_no_default_maven_setting, reload_nuget_config_setting, reload_ruby_repos_setting,
|
||||
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
|
||||
send_logs_to_object_store, WORKERS_NAMES,
|
||||
};
|
||||
@@ -47,7 +47,7 @@ use windmill_common::{
|
||||
NO_DEFAULT_MAVEN_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, TEAMS_SETTING,
|
||||
RUBY_REPOS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING,
|
||||
},
|
||||
scripts::ScriptLang,
|
||||
@@ -83,7 +83,8 @@ use windmill_worker::{
|
||||
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, CSHARP_CACHE_DIR,
|
||||
DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR,
|
||||
JAVA_CACHE_DIR, NU_CACHE_DIR, POWERSHELL_CACHE_DIR, PY310_CACHE_DIR, PY311_CACHE_DIR,
|
||||
PY312_CACHE_DIR, PY313_CACHE_DIR, RUST_CACHE_DIR, TAR_JAVA_CACHE_DIR, UV_CACHE_DIR,
|
||||
PY312_CACHE_DIR, PY313_CACHE_DIR, RUBY_CACHE_DIR, RUST_CACHE_DIR, TAR_JAVA_CACHE_DIR,
|
||||
UV_CACHE_DIR,
|
||||
};
|
||||
|
||||
use crate::monitor::{
|
||||
@@ -1014,6 +1015,9 @@ Windmill Community Edition {GIT_VERSION}
|
||||
NO_DEFAULT_MAVEN_SETTING => {
|
||||
reload_no_default_maven_setting(&conn).await
|
||||
},
|
||||
RUBY_REPOS_SETTING => {
|
||||
reload_ruby_repos_setting(&conn).await
|
||||
},
|
||||
KEEP_JOB_DIR_SETTING => {
|
||||
load_keep_job_dir(&conn).await;
|
||||
},
|
||||
@@ -1368,6 +1372,7 @@ pub async fn run_workers(
|
||||
HUB_CACHE_DIR,
|
||||
POWERSHELL_CACHE_DIR,
|
||||
JAVA_CACHE_DIR,
|
||||
RUBY_CACHE_DIR,
|
||||
TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG
|
||||
] {
|
||||
DirBuilder::new()
|
||||
|
||||
@@ -291,6 +291,7 @@ pub async fn initial_load(
|
||||
reload_nuget_config_setting(&conn).await;
|
||||
reload_maven_repos_setting(&conn).await;
|
||||
reload_no_default_maven_setting(&conn).await;
|
||||
reload_ruby_repos_setting(&conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,6 +1094,7 @@ pub async fn reload_nuget_config_setting(conn: &Connection) {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_maven_repos_setting(conn: &Connection) {
|
||||
reload_option_setting_with_tracing(
|
||||
conn,
|
||||
@@ -1102,6 +1104,7 @@ pub async fn reload_maven_repos_setting(conn: &Connection) {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_no_default_maven_setting(conn: &Connection) {
|
||||
let value = load_value_from_global_settings_with_conn(
|
||||
conn,
|
||||
@@ -1118,6 +1121,16 @@ pub async fn reload_no_default_maven_setting(conn: &Connection) {
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn reload_ruby_repos_setting(conn: &Connection) {
|
||||
reload_url_list_setting_with_tracing(
|
||||
conn,
|
||||
windmill_common::global_settings::RUBY_REPOS_SETTING,
|
||||
"RUBY_REPOS",
|
||||
windmill_worker::RUBY_REPOS.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_retention_period_setting(conn: &Connection) {
|
||||
if let Err(e) = reload_setting(
|
||||
conn,
|
||||
@@ -1287,6 +1300,89 @@ pub async fn reload_option_setting<T: FromStr + DeserializeOwned>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_url_list_setting_with_tracing(
|
||||
conn: &Connection,
|
||||
setting_name: &str,
|
||||
std_env_var: &str,
|
||||
lock: Arc<RwLock<Option<Vec<url::Url>>>>,
|
||||
) {
|
||||
if let Err(e) = reload_url_list_setting(conn, setting_name, std_env_var, lock.clone()).await {
|
||||
tracing::error!("Error reloading setting {}: {:?}", setting_name, e)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_url_list_setting(
|
||||
conn: &Connection,
|
||||
setting_name: &str,
|
||||
std_env_var: &str,
|
||||
lock: Arc<RwLock<Option<Vec<url::Url>>>>,
|
||||
) -> error::Result<()> {
|
||||
// Check for force environment variable
|
||||
if let Ok(force_value) = std::env::var(format!("FORCE_{}", std_env_var)) {
|
||||
let mut urls = Vec::new();
|
||||
for url_str in force_value.trim().split_whitespace() {
|
||||
match url::Url::parse(url_str) {
|
||||
Ok(url) => urls.push(url),
|
||||
Err(e) => {
|
||||
return Err(error::Error::BadRequest(format!("Invalid URL in FORCE_{}: '{}': {}", std_env_var, url_str, e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut l = lock.write().await;
|
||||
*l = if urls.is_empty() { None } else { Some(urls) };
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let q = load_value_from_global_settings_with_conn(conn, setting_name, true).await?;
|
||||
|
||||
// Check regular environment variable
|
||||
let mut value = if let Ok(env_value) = std::env::var(std_env_var) {
|
||||
let mut urls = Vec::new();
|
||||
for url_str in env_value.trim().split_whitespace() {
|
||||
match url::Url::parse(url_str) {
|
||||
Ok(url) => urls.push(url),
|
||||
Err(_) => {
|
||||
// Log error but continue, similar to force variable handling
|
||||
tracing::error!("Invalid URL in {}: '{}'", std_env_var, url_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Check database setting
|
||||
if let Some(q) = q {
|
||||
if let Ok(repos_str) = serde_json::from_value::<String>(q.clone()) {
|
||||
let mut urls = Vec::new();
|
||||
for url_str in repos_str.trim().split_whitespace() {
|
||||
match url::Url::parse(url_str) {
|
||||
Ok(url) => urls.push(url),
|
||||
Err(e) => {
|
||||
tracing::error!("Invalid URL in {}: '{}': {}", setting_name, url_str, e);
|
||||
// Continue with other URLs, just skip invalid ones
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::info!("Loaded setting {} from db config: {} URLs", setting_name, urls.len());
|
||||
value = if urls.is_empty() { None } else { Some(urls) };
|
||||
} else {
|
||||
tracing::error!("Could not parse {} found: {:#?}", setting_name, &q);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if value.is_none() {
|
||||
tracing::info!("Loaded {} setting to None", setting_name);
|
||||
}
|
||||
let mut l = lock.write().await;
|
||||
*l = value;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_setting<T: FromStr + DeserializeOwned + Display>(
|
||||
conn: &Connection,
|
||||
setting_name: &str,
|
||||
|
||||
@@ -3468,6 +3468,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
|
||||
tag: None,
|
||||
paused_until: None,
|
||||
cron_version: None,
|
||||
description: None,
|
||||
};
|
||||
|
||||
let _ = client.create_schedule("test-workspace", &schedule).await;
|
||||
@@ -3538,6 +3539,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
|
||||
tag: None,
|
||||
paused_until: None,
|
||||
cron_version: None,
|
||||
description: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -3621,6 +3623,7 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
|
||||
tag: None,
|
||||
paused_until: None,
|
||||
cron_version: None,
|
||||
description: None,
|
||||
};
|
||||
|
||||
let _ = client.create_schedule("test-workspace", &schedule).await;
|
||||
@@ -3692,6 +3695,7 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
|
||||
tag: None,
|
||||
paused_until: None,
|
||||
cron_version: None,
|
||||
description: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -3777,6 +3781,7 @@ async fn run_deployed_relative_imports(
|
||||
codebase: None,
|
||||
has_preprocessor: None,
|
||||
on_behalf_of_email: None,
|
||||
assets: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -4028,6 +4033,7 @@ async fn assert_lockfile(
|
||||
codebase: None,
|
||||
has_preprocessor: None,
|
||||
on_behalf_of_email: None,
|
||||
assets: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -8213,6 +8213,9 @@ the execution of this script will be permissioned_as and by extension its DT_TOK
|
||||
Csharp,
|
||||
#[serde(rename = "nu")]
|
||||
Nu,
|
||||
#[serde(rename = "ruby")]
|
||||
Ruby,
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
impl From<&ScriptLang> for ScriptLang {
|
||||
fn from(value: &ScriptLang) -> Self {
|
||||
@@ -8241,6 +8244,8 @@ the execution of this script will be permissioned_as and by extension its DT_TOK
|
||||
Self::Ansible => "ansible".to_string(),
|
||||
Self::Csharp => "csharp".to_string(),
|
||||
Self::Nu => "nu".to_string(),
|
||||
Self::Ruby => "ruby".to_string(),
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8267,6 +8272,8 @@ the execution of this script will be permissioned_as and by extension its DT_TOK
|
||||
"ansible" => Ok(Self::Ansible),
|
||||
"csharp" => Ok(Self::Csharp),
|
||||
"nu" => Ok(Self::Nu),
|
||||
"ruby" => Ok(Self::Ruby),
|
||||
// for related places search: ADD_NEW_LANG
|
||||
_ => Err("invalid value"),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15128,7 +15128,8 @@ components:
|
||||
csharp,
|
||||
nu,
|
||||
java,
|
||||
duckdb,
|
||||
ruby,
|
||||
duckdb
|
||||
# for related places search: ADD_NEW_LANG
|
||||
]
|
||||
|
||||
|
||||
@@ -734,6 +734,7 @@ async fn create_script_internal<'c>(
|
||||
|| ns.language == ScriptLang::Nu
|
||||
|| ns.language == ScriptLang::Php
|
||||
|| ns.language == ScriptLang::Java
|
||||
|| ns.language == ScriptLang::Ruby
|
||||
// for related places search: ADD_NEW_LANG
|
||||
) {
|
||||
Some(String::new())
|
||||
|
||||
@@ -377,6 +377,7 @@ pub(crate) async fn tarball_workspace(
|
||||
ScriptLang::Nu => "nu",
|
||||
ScriptLang::OracleDB => "odb.sql",
|
||||
ScriptLang::Java => "java",
|
||||
ScriptLang::Ruby => "rb",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
};
|
||||
archive
|
||||
|
||||
@@ -181,6 +181,13 @@ impl From<serde_json::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
#[track_caller]
|
||||
fn from(e: url::ParseError) -> Self {
|
||||
Self::ArgumentErr(format!("Cannot parse provided url. \ne: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// https://docs.rs/anyhow/1/anyhow/struct.Error.html#display-representations
|
||||
pub fn alt(&self) -> String {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const BUNFIG_INSTALL_SCOPES_SETTING: &str = "bunfig_install_scopes";
|
||||
pub const NUGET_CONFIG_SETTING: &str = "nuget_config";
|
||||
pub const MAVEN_REPOS_SETTING: &str = "maven_repos";
|
||||
pub const NO_DEFAULT_MAVEN_SETTING: &str = "no_default_maven";
|
||||
pub const RUBY_REPOS_SETTING: &str = "ruby_repos";
|
||||
|
||||
pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url";
|
||||
pub const PIP_INDEX_URL_SETTING: &str = "pip_index_url";
|
||||
@@ -66,6 +67,10 @@ pub const ENV_SETTINGS: &[&str] = &[
|
||||
"DENO_PATH",
|
||||
"GO_PATH",
|
||||
"JAVA_PATH",
|
||||
"RUBY_PATH",
|
||||
"BUNDLE_PATH",
|
||||
"GEM_PATH",
|
||||
"RUBY_CONCURRENT_DOWNLOADS",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
"GOPRIVATE",
|
||||
"GOPROXY",
|
||||
|
||||
@@ -348,7 +348,7 @@ pub fn should_validate_schema(code: &str, lang: &ScriptLang) -> bool {
|
||||
use ScriptLang::*;
|
||||
let comment = match lang {
|
||||
Nativets | Bun | Bunnative | Deno | Php | CSharp | Java => "//",
|
||||
Python3 | Go | Bash | Powershell | Graphql | Ansible | Nu => "#",
|
||||
Python3 | Go | Bash | Powershell | Graphql | Ansible | Nu | Ruby => "#",
|
||||
Postgresql | Mysql | Bigquery | Snowflake | Mssql | OracleDB | DuckDb => "--",
|
||||
Rust => "//!",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
|
||||
@@ -55,7 +55,9 @@ pub enum ScriptLang {
|
||||
Ansible,
|
||||
CSharp,
|
||||
Nu,
|
||||
Java, // for related places search: ADD_NEW_LANG
|
||||
Java,
|
||||
Ruby,
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
impl ScriptLang {
|
||||
@@ -83,6 +85,7 @@ impl ScriptLang {
|
||||
ScriptLang::CSharp => "csharp",
|
||||
ScriptLang::Nu => "nu",
|
||||
ScriptLang::Java => "java",
|
||||
ScriptLang::Ruby => "ruby",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
}
|
||||
@@ -113,6 +116,8 @@ impl FromStr for ScriptLang {
|
||||
"csharp" => ScriptLang::CSharp,
|
||||
"nu" => ScriptLang::Nu,
|
||||
"java" => ScriptLang::Java,
|
||||
"ruby" => ScriptLang::Ruby,
|
||||
// for related places search: ADD_NEW_LANG
|
||||
language => {
|
||||
return Err(anyhow::anyhow!("{} is currently not supported", language).into())
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ lazy_static::lazy_static! {
|
||||
"csharp".to_string(),
|
||||
"nu".to_string(),
|
||||
"java".to_string(),
|
||||
"ruby".to_string(),
|
||||
"duckdb".to_string(),
|
||||
// for related places search: ADD_NEW_LANG
|
||||
"dependency".to_string(),
|
||||
@@ -622,6 +623,12 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[annotations("#")]
|
||||
pub struct RubyAnnotations {
|
||||
pub verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[annotations("#")]
|
||||
pub struct PythonAnnotations {
|
||||
|
||||
@@ -32,6 +32,7 @@ csharp = ["dep:windmill-parser-csharp"]
|
||||
rust = ["dep:windmill-parser-rust"]
|
||||
nu = ["dep:windmill-parser-nu"]
|
||||
java = ["dep:windmill-parser-java"]
|
||||
ruby = ["dep:windmill-parser-ruby"]
|
||||
duckdb = ["dep:duckdb"]
|
||||
|
||||
[dependencies]
|
||||
@@ -46,6 +47,7 @@ windmill-parser-rust = { workspace = true, optional = true }
|
||||
windmill-parser-csharp = { workspace = true, optional = true }
|
||||
windmill-parser-nu = { workspace = true, optional = true }
|
||||
windmill-parser-java = { workspace = true, optional = true }
|
||||
windmill-parser-ruby = { workspace = true, optional = true }
|
||||
windmill-parser-py = { workspace = true, optional = true }
|
||||
windmill-parser-yaml.workspace = true
|
||||
windmill-parser-py-imports = { workspace = true, optional = true }
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
name: "gem install"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "ruby"
|
||||
log_level: ERROR
|
||||
time_limit: 900
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
|
||||
envar: "HOME=/user"
|
||||
envar: "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
|
||||
skip_setsid: true
|
||||
keep_caps: true
|
||||
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: "/etc"
|
||||
dst: "/etc"
|
||||
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: "{TARGET}"
|
||||
dst: "{TARGET}"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/urandom"
|
||||
dst: "/dev/urandom"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
{DEV}
|
||||
@@ -0,0 +1,79 @@
|
||||
name: "bundle lock"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "ruby"
|
||||
log_level: ERROR
|
||||
time_limit: 900
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
|
||||
envar: "HOME=/tmp"
|
||||
envar: "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
|
||||
skip_setsid: true
|
||||
keep_caps: true
|
||||
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: "/etc"
|
||||
dst: "/etc"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/null"
|
||||
dst: "/dev/null"
|
||||
is_bind: true
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}"
|
||||
dst: "/tmp"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/urandom"
|
||||
dst: "/dev/urandom"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
{DEV}
|
||||
@@ -0,0 +1,113 @@
|
||||
name: "ruby run script"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "ruby"
|
||||
log_level: ERROR
|
||||
|
||||
disable_rl: true
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
|
||||
skip_setsid: true
|
||||
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: "/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.rb"
|
||||
dst: "/tmp/main.rb"
|
||||
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: "{CACHE_DIR}"
|
||||
# dst: "{CACHE_DIR}"
|
||||
# is_bind: true
|
||||
# mandatory: false
|
||||
# }
|
||||
|
||||
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
|
||||
|
||||
{SHARED_MOUNT}
|
||||
|
||||
{SHARED_DEPENDENCIES}
|
||||
|
||||
|
||||
{DEV}
|
||||
@@ -12,7 +12,6 @@ use sha2::Digest;
|
||||
use sqlx::types::Json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::{RwLock, Semaphore};
|
||||
use tokio::{fs::File, io::AsyncReadExt};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -31,11 +30,10 @@ use windmill_common::{
|
||||
variables::ContextualVariable,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{anyhow, Result};
|
||||
use windmill_parser_sql::{s3_mode_extension, S3ModeArgs, S3ModeFormat};
|
||||
use windmill_queue::MiniPulledJob;
|
||||
|
||||
use std::ops::AsyncFn;
|
||||
use std::path::Path;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
@@ -45,7 +43,7 @@ use windmill_common::{variables, DB};
|
||||
use tokio::{io::AsyncWriteExt, time::Instant};
|
||||
|
||||
use crate::agent_workers::UPDATE_PING_URL;
|
||||
use crate::{DISABLE_NSJAIL, JOB_DEFAULT_TIMEOUT, MAX_RESULT_SIZE, MAX_TIMEOUT_DURATION, PATH_ENV};
|
||||
use crate::{JOB_DEFAULT_TIMEOUT, MAX_RESULT_SIZE, MAX_TIMEOUT_DURATION, PATH_ENV};
|
||||
use windmill_common::client::AuthedClient;
|
||||
|
||||
pub async fn build_args_map<'a>(
|
||||
@@ -1072,561 +1070,6 @@ pub fn build_http_client(timeout_duration: std::time::Duration) -> error::Result
|
||||
.map_err(|e| Error::internal_err(format!("Error building http client: {e:#}")))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RequiredDependency {
|
||||
/// Expected directory of dependency in cache
|
||||
/// For example:
|
||||
/// /tmp/windmill/cache/python_311/rich==0.0.0
|
||||
/// IMPORTANT!: path should not end with '/'
|
||||
pub path: String,
|
||||
/// Name to use for S3 tars
|
||||
/// If not specified will use top level directory of path.
|
||||
pub custom_name: Option<String>,
|
||||
/// Display name
|
||||
/// Name that will be used for console output and logging
|
||||
/// If not specified will either use custom_name or top level directory of path.
|
||||
pub short_name: Option<String>,
|
||||
}
|
||||
|
||||
pub enum InstallStrategy {
|
||||
/// Will invoke callback to install single dependency
|
||||
Single(Arc<dyn Fn(RequiredDependency) -> Result<Command, error::Error> + Send + Sync>),
|
||||
/// Will try to pull S3 first and will invoke closure to install the rest
|
||||
AllAtOnce(Arc<dyn Fn(Vec<RequiredDependency>) -> Result<Command, error::Error> + Send + Sync>),
|
||||
}
|
||||
/// # General
|
||||
///
|
||||
/// Languages that compile usually include dependencies in final executable.
|
||||
/// When dynamic languages do not and runtime dependencies provided separately.
|
||||
///
|
||||
/// This helper implies that the language is dynamic.
|
||||
/// Python, Ruby, Java are dynamic and they can use this helper.
|
||||
///
|
||||
/// # Features
|
||||
///
|
||||
/// This helper will install all specified dependencies in parallel and if it is EE, cache to S3
|
||||
/// It has atomic success file, allowing to distinguish failed installations from succesfull.
|
||||
///
|
||||
/// Besides that it provides console output and does logging.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// Most important arguments in this helper are `deps` and `install_fn`
|
||||
///
|
||||
/// In `deps` you specify all dependencies that are needed to be on worker in order to execute script.
|
||||
/// You don't know which are actually installed and which are not.
|
||||
///
|
||||
/// `deps` is a vector of RequiredDependency. Check [RequiredDependency] for more context.
|
||||
///
|
||||
/// After `deps` are provided helper will check each dependency and check if it is in cache, if not it will try to pull from S3
|
||||
/// and if it does not work either, it will invoke `install_fn` closure.
|
||||
/// Closure arguments has dependency name as well as it`s expected path in cache.
|
||||
/// Closure should return Command that will install dependency to asked place.
|
||||
pub async fn par_install_language_dependencies<'a>(
|
||||
deps: Vec<RequiredDependency>,
|
||||
language_name: &'a str,
|
||||
installer_executable_name: &'a str,
|
||||
platform_agnostic: bool,
|
||||
concurrent_downloads: usize,
|
||||
stdout_on_err: bool,
|
||||
install_fn: InstallStrategy,
|
||||
postinstall_cb: impl AsyncFn(Vec<RequiredDependency>) -> Result<(), error::Error>,
|
||||
job_id: &'a Uuid,
|
||||
w_id: &'a str,
|
||||
worker_name: &'a str,
|
||||
conn: &Connection,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
let _ = (platform_agnostic, language_name);
|
||||
|
||||
let total_time = std::time::Instant::now();
|
||||
|
||||
// Total to install
|
||||
let mut not_installed = vec![];
|
||||
let total_to_install;
|
||||
// let mut not_installed = vec![];
|
||||
let counter_arc = Arc::new(tokio::sync::Mutex::new(0));
|
||||
// Append logs with line like this:
|
||||
// [9/21] + requests==2.32.3 << (S3) | in 57ms
|
||||
#[allow(unused_assignments)]
|
||||
async fn print_success(
|
||||
mut s3_pull: bool,
|
||||
mut s3_push: bool,
|
||||
job_id: &Uuid,
|
||||
w_id: &str,
|
||||
req: &str,
|
||||
req_tl: usize,
|
||||
counter_arc: Arc<tokio::sync::Mutex<usize>>,
|
||||
total_to_install: usize,
|
||||
instant: std::time::Instant,
|
||||
conn: &Connection,
|
||||
) {
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
{
|
||||
(s3_pull, s3_push) = (false, false);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if windmill_common::s3_helpers::OBJECT_STORE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
(s3_pull, s3_push) = (false, false);
|
||||
}
|
||||
|
||||
let mut counter = counter_arc.lock().await;
|
||||
*counter += 1;
|
||||
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!(
|
||||
"\n{}+ {}{}{}| in {}ms",
|
||||
windmill_common::worker::pad_string(
|
||||
&format!("[{}/{total_to_install}]", counter),
|
||||
9
|
||||
),
|
||||
// Because we want to align to max len [999/999] we take 9
|
||||
// 123456789
|
||||
windmill_common::worker::pad_string(&req, req_tl + 1),
|
||||
// Margin to the right ^
|
||||
if s3_pull { "<< (S3) " } else { "" },
|
||||
if s3_push { " > (S3) " } else { "" },
|
||||
instant.elapsed().as_millis(),
|
||||
),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
// Drop lock, so next print success can fire
|
||||
}
|
||||
|
||||
let mut name_tl = 0;
|
||||
struct NotInstalledDependency {
|
||||
path: String,
|
||||
custom_name: Option<String>,
|
||||
short_name: Option<String>,
|
||||
display_name: String,
|
||||
}
|
||||
{
|
||||
let mut to_be_installed_is_used = false;
|
||||
for RequiredDependency {
|
||||
path, //
|
||||
custom_name,
|
||||
short_name,
|
||||
} in deps.into_iter()
|
||||
{
|
||||
if path.ends_with("/") {
|
||||
anyhow::bail!("Internal error: path should not end with '/'")
|
||||
}
|
||||
let display_name = short_name
|
||||
.as_ref()
|
||||
.or(custom_name.as_ref())
|
||||
.or(path.split("/").last().map(|e| e.to_owned()).as_ref())
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
workspace_id = %w_id,
|
||||
job_id = %job_id,
|
||||
"failed to parse top level directory name for {path}, fallback to full path.",
|
||||
);
|
||||
|
||||
&path
|
||||
})
|
||||
.to_owned();
|
||||
{
|
||||
// Later will help us align text in log console
|
||||
if display_name.len() > name_tl {
|
||||
name_tl = display_name.len();
|
||||
}
|
||||
}
|
||||
// Will look like: /tmp/windmill/cache/lang/dependency.valid.windmill
|
||||
if tokio::fs::metadata(path.clone() + ".valid.windmill")
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if !to_be_installed_is_used {
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!("\n--- INSTALLATION ---\n\nTo be installed:\n\n"),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
to_be_installed_is_used = true;
|
||||
}
|
||||
windmill_queue::append_logs(job_id, w_id, format!("- {display_name}\n"), conn)
|
||||
.await;
|
||||
not_installed.push(NotInstalledDependency {
|
||||
path,
|
||||
custom_name,
|
||||
short_name,
|
||||
display_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
total_to_install = not_installed.len();
|
||||
if total_to_install == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let is_not_pro = !matches!(
|
||||
windmill_common::ee_oss::get_license_plan().await,
|
||||
windmill_common::ee_oss::LicensePlan::Pro
|
||||
);
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if is_not_pro && matches!(install_fn, InstallStrategy::AllAtOnce(_)) {
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!("\nLooking for packages on S3:\n"),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Parallelism level (N)
|
||||
let parallel_limit = // Semaphore will panic if value less then 1
|
||||
concurrent_downloads.clamp(1, 30);
|
||||
|
||||
tracing::info!(
|
||||
workspace_id = %w_id,
|
||||
"Install parallel limit: {}, job: {}",
|
||||
parallel_limit,
|
||||
job_id
|
||||
);
|
||||
|
||||
let mut handles = vec![];
|
||||
let semaphore = Arc::new(Semaphore::new(parallel_limit));
|
||||
let not_pulled = Arc::new(RwLock::new(vec![]));
|
||||
// let mut handles = Vec::with_capacity(total_to_install);
|
||||
for NotInstalledDependency {
|
||||
//
|
||||
path,
|
||||
custom_name,
|
||||
short_name,
|
||||
display_name,
|
||||
} in not_installed
|
||||
{
|
||||
let permit = semaphore.clone().acquire_owned().await; // Acquire a permit
|
||||
|
||||
if let Err(_) = permit {
|
||||
tracing::error!(
|
||||
workspace_id = %w_id,
|
||||
"Cannot acquire permit on semaphore, that can only mean that semaphore has been closed."
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let permit = permit.unwrap();
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let s3_pull_future = if is_not_pro {
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
Some(crate::global_cache::pull_from_tar(
|
||||
os,
|
||||
path.clone(),
|
||||
language_name.to_owned(),
|
||||
custom_name.clone(),
|
||||
platform_agnostic,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let child = {
|
||||
if let InstallStrategy::Single(ref callback, ..) = install_fn {
|
||||
let cmd = callback(RequiredDependency {
|
||||
path: path.clone(),
|
||||
custom_name: custom_name.clone(),
|
||||
short_name: short_name.clone(),
|
||||
})?;
|
||||
tracing::debug!("{:?}", &cmd);
|
||||
Some(start_child_process(cmd, &installer_executable_name, false).await?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let (
|
||||
worker_name_2,
|
||||
path_2,
|
||||
display_name_2,
|
||||
custom_name,
|
||||
job_id_2,
|
||||
w_id_2,
|
||||
conn_2,
|
||||
counter_arc,
|
||||
language_name,
|
||||
installer_executable_name,
|
||||
not_pulled,
|
||||
) = (
|
||||
worker_name.to_owned(),
|
||||
path.clone(),
|
||||
display_name.clone(),
|
||||
custom_name.clone(),
|
||||
job_id.clone(),
|
||||
w_id.to_owned(),
|
||||
conn.clone(),
|
||||
counter_arc.clone(),
|
||||
language_name.to_owned(),
|
||||
installer_executable_name.to_owned(),
|
||||
not_pulled.clone(),
|
||||
);
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
let _ = language_name;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(s3_pull_future) = s3_pull_future {
|
||||
if let Err(e) = s3_pull_future.await {
|
||||
tracing::info!(
|
||||
workspace_id = %w_id_2,
|
||||
"No tarball was found for {:?} on S3 or different problem occured {job_id_2}:\n{e}",
|
||||
&custom_name.clone().unwrap_or(path)
|
||||
);
|
||||
} else {
|
||||
// TODO: Refactor
|
||||
// Create a file to indicate that installation was successfull
|
||||
let valid_path = path_2.clone() + ".valid.windmill";
|
||||
// This is atomic operation, meaning, that it either completes and dependency is valid,
|
||||
// or it does not and dependency is invalid and will be reinstalled next run
|
||||
if let Err(e) = File::create(&valid_path).await {
|
||||
tracing::error!(
|
||||
workspace_id = %w_id_2,
|
||||
job_id = %job_id_2,
|
||||
"Failed to create {}!\n{e}\n
|
||||
This file needed for jobs to function",
|
||||
valid_path
|
||||
);
|
||||
};
|
||||
print_success(
|
||||
true,
|
||||
false,
|
||||
&job_id_2,
|
||||
&w_id_2,
|
||||
&display_name_2,
|
||||
name_tl,
|
||||
counter_arc,
|
||||
total_to_install,
|
||||
start,
|
||||
&conn_2,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(child) = child else {
|
||||
let mut lock = not_pulled.write().await;
|
||||
lock.push(RequiredDependency {
|
||||
path: path_2.clone(),
|
||||
custom_name: custom_name.clone(),
|
||||
short_name: short_name.clone(),
|
||||
});
|
||||
return;
|
||||
};
|
||||
if let Err(e) = crate::handle_child::handle_child(
|
||||
&job_id_2,
|
||||
&conn_2,
|
||||
// TODO: Return mem_peak
|
||||
&mut 0,
|
||||
// TODO: Return canceld_by_ref
|
||||
&mut None,
|
||||
child,
|
||||
!*DISABLE_NSJAIL,
|
||||
&worker_name_2,
|
||||
&w_id_2,
|
||||
&installer_executable_name,
|
||||
None,
|
||||
false,
|
||||
&mut None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
windmill_queue::append_logs(
|
||||
&job_id_2,
|
||||
&w_id_2,
|
||||
format!("error while installing {}: {e:?}", &display_name_2),
|
||||
&conn_2,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
// if let Some(cb) = postinstall_cb {
|
||||
// if let Err(e) = cb(vec![RequiredDependency {
|
||||
// path: path_2.clone(),
|
||||
// custom_name: custom_name.clone(),
|
||||
// short_name: short_name.clone(),
|
||||
// }])
|
||||
// .await
|
||||
// {
|
||||
// tracing::error!(
|
||||
// workspace_id = %w_id_2,
|
||||
// job_id = %job_id_2,
|
||||
// "Postinstall callback failed!\n{e}\n
|
||||
// This might affect execution",
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
print_success(
|
||||
false,
|
||||
true,
|
||||
&job_id_2,
|
||||
&w_id_2,
|
||||
&display_name_2,
|
||||
name_tl,
|
||||
counter_arc,
|
||||
total_to_install,
|
||||
start,
|
||||
&conn_2,
|
||||
)
|
||||
.await;
|
||||
// TODO: Refactor
|
||||
// Create a file to indicate that installation was successfull
|
||||
let valid_path = path_2.clone() + ".valid.windmill";
|
||||
// This is atomic operation, meaning, that it either completes and dependency is valid,
|
||||
// or it does not and dependency is invalid and will be reinstalled next run
|
||||
if let Err(e) = File::create(&valid_path).await {
|
||||
tracing::error!(
|
||||
workspace_id = %w_id_2,
|
||||
job_id = %job_id_2,
|
||||
"Failed to create {}!\n{e}\n
|
||||
This file needed for jobs to function",
|
||||
valid_path
|
||||
);
|
||||
};
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
{
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = crate::global_cache::build_tar_and_push(
|
||||
os,
|
||||
path_2,
|
||||
language_name,
|
||||
custom_name,
|
||||
platform_agnostic,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to build tar and push: {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
if let Err(e) = handle.await {
|
||||
tracing::error!("Error joining handles: {e:?}");
|
||||
}
|
||||
}
|
||||
if !not_pulled.read().await.is_empty() {
|
||||
if let InstallStrategy::AllAtOnce(ref callback, ..) = install_fn {
|
||||
let not_pulled_copy = not_pulled.read().await.clone();
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!("\n\nFetching {} packages...\n", not_pulled_copy.len()),
|
||||
&conn,
|
||||
)
|
||||
.await;
|
||||
let cmd = callback(not_pulled_copy.clone())?;
|
||||
tracing::debug!("{:?}", &cmd);
|
||||
let child = start_child_process(cmd, &installer_executable_name, false).await?;
|
||||
let mut buf = "".to_owned();
|
||||
let pipe_stdout = if stdout_on_err { Some(&mut buf) } else { None };
|
||||
if let Err(e) = crate::handle_child::handle_child(
|
||||
// &job_id,
|
||||
&Uuid::nil(),
|
||||
&conn,
|
||||
// TODO: Return mem_peak
|
||||
&mut 0,
|
||||
// TODO: Return canceld_by_ref
|
||||
&mut None,
|
||||
child,
|
||||
!*DISABLE_NSJAIL,
|
||||
&worker_name,
|
||||
&w_id,
|
||||
&installer_executable_name,
|
||||
None,
|
||||
false,
|
||||
&mut None,
|
||||
pipe_stdout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
bail!(format!(
|
||||
"error while installing dependencies: {e:?}\n{}",
|
||||
buf
|
||||
));
|
||||
}
|
||||
{
|
||||
postinstall_cb(not_pulled_copy.clone()).await?;
|
||||
}
|
||||
for RequiredDependency { path, custom_name: _custom_name, .. } in
|
||||
not_pulled_copy.into_iter()
|
||||
{
|
||||
// TODO: Refactor
|
||||
// Create a file to indicate that installation was successfull
|
||||
let valid_path = path.clone() + ".valid.windmill";
|
||||
// This is atomic operation, meaning, that it either completes and dependency is valid,
|
||||
// or it does not and dependency is invalid and will be reinstalled next run
|
||||
if let Err(e) = File::create(&valid_path).await {
|
||||
tracing::error!(
|
||||
workspace_id = %w_id,
|
||||
job_id = %job_id,
|
||||
"Failed to create {}!\n{e}\n
|
||||
This file needed for jobs to function",
|
||||
valid_path
|
||||
);
|
||||
};
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
{
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
let language_name = language_name.to_owned();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = crate::global_cache::build_tar_and_push(
|
||||
os,
|
||||
path,
|
||||
language_name,
|
||||
_custom_name,
|
||||
platform_agnostic,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to build tar and push: {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let total_time = total_time.elapsed().as_millis();
|
||||
windmill_queue::append_logs(
|
||||
&job_id,
|
||||
w_id,
|
||||
format!(
|
||||
"\nDone. Time spent on installation phase: {}ms\n",
|
||||
total_time
|
||||
),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct S3ModeWorkerData {
|
||||
pub client: AuthedClient,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, path::PathBuf, process::Stdio, sync::Arc};
|
||||
use std::{collections::HashMap, path::PathBuf, process::Stdio};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_recursion::async_recursion;
|
||||
@@ -21,11 +21,13 @@ use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
create_args_and_out_file, get_reserved_variables, par_install_language_dependencies,
|
||||
read_result, start_child_process, OccupancyMetrics, RequiredDependency,
|
||||
create_args_and_out_file, get_reserved_variables, read_result, start_child_process,
|
||||
OccupancyMetrics,
|
||||
},
|
||||
handle_child, COURSIER_CACHE_DIR, DISABLE_NSJAIL, DISABLE_NUSER, JAVA_CACHE_DIR,
|
||||
JAVA_REPOSITORY_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
|
||||
handle_child,
|
||||
universal_pkg_installer::{par_install_language_dependencies_all_at_once, RequiredDependency},
|
||||
COURSIER_CACHE_DIR, DISABLE_NSJAIL, DISABLE_NUSER, JAVA_CACHE_DIR, JAVA_REPOSITORY_DIR,
|
||||
MAVEN_REPOS, NO_DEFAULT_MAVEN, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
|
||||
};
|
||||
use windmill_common::client::AuthedClient;
|
||||
|
||||
@@ -275,14 +277,15 @@ async fn install<'a>(
|
||||
);
|
||||
Ok(RequiredDependency {
|
||||
path,
|
||||
custom_name: Some(format!("{group_id}:{artifact_id}:{version}")),
|
||||
short_name: Some(format!("{artifact_id}:{version}")),
|
||||
_s3_handle: format!("{group_id}:{artifact_id}:{version}"),
|
||||
display_name: format!("{artifact_id}:{version}"),
|
||||
custom_payload: (),
|
||||
})
|
||||
}
|
||||
_ => anyhow::bail!("{line} is not parsable"),
|
||||
}
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<RequiredDependency>>>()?;
|
||||
.collect::<anyhow::Result<Vec<RequiredDependency<_>>>>()?;
|
||||
|
||||
let classpath = deps
|
||||
.clone()
|
||||
@@ -307,14 +310,14 @@ async fn install<'a>(
|
||||
let job_dir = job_dir.to_owned();
|
||||
let fetch_dir = format!("{JAVA_CACHE_DIR}/tmp-fetch-{}", Uuid::new_v4());
|
||||
let fetch_dir2 = fetch_dir.clone();
|
||||
par_install_language_dependencies(
|
||||
par_install_language_dependencies_all_at_once(
|
||||
deps,
|
||||
"java",
|
||||
"java",
|
||||
true,
|
||||
*JAVA_CONCURRENT_DOWNLOADS,
|
||||
true,
|
||||
crate::common::InstallStrategy::AllAtOnce(Arc::new(move |dependencies| {
|
||||
move |dependencies| {
|
||||
let mut cmd = Command::new(if cfg!(windows) {
|
||||
"java"
|
||||
} else {
|
||||
@@ -322,12 +325,8 @@ async fn install<'a>(
|
||||
});
|
||||
let artifacts = dependencies
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
e.custom_name.ok_or(anyhow::anyhow!(
|
||||
"Internal Error: Artifact name should be Some!"
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<String>>>()?;
|
||||
.map(|e| e._s3_handle)
|
||||
.collect::<Vec<String>>();
|
||||
cmd.env_clear()
|
||||
.current_dir(&job_dir)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
@@ -385,7 +384,7 @@ async fn install<'a>(
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
})),
|
||||
},
|
||||
async move |_| {
|
||||
move_to_repository(&fetch_dir2, 0).await?;
|
||||
remove_dir_all(&fetch_dir2).await?;
|
||||
@@ -419,6 +418,7 @@ async fn install<'a>(
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
worker_name,
|
||||
!*DISABLE_NSJAIL,
|
||||
conn,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -13,6 +13,9 @@ mod bash_executor;
|
||||
#[cfg(feature = "java")]
|
||||
mod java_executor;
|
||||
|
||||
#[cfg(feature = "ruby")]
|
||||
mod ruby_executor;
|
||||
|
||||
mod bun_executor;
|
||||
pub mod common;
|
||||
mod config;
|
||||
@@ -52,6 +55,7 @@ pub mod result_processor;
|
||||
mod rust_executor;
|
||||
mod sanitized_sql_params;
|
||||
mod schema;
|
||||
mod universal_pkg_installer;
|
||||
mod worker;
|
||||
mod worker_flow;
|
||||
mod worker_lockfiles;
|
||||
|
||||
@@ -0,0 +1,899 @@
|
||||
use std::{collections::HashMap, process::Stdio};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use const_format::concatcp;
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
process::Command,
|
||||
};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::{
|
||||
client::AuthedClient,
|
||||
error::Error,
|
||||
utils::calculate_hash,
|
||||
worker::{write_file, Connection, RubyAnnotations},
|
||||
};
|
||||
use windmill_parser::Arg;
|
||||
use windmill_parser_ruby::parse_ruby_signature;
|
||||
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
create_args_and_out_file, get_reserved_variables, read_result, start_child_process,
|
||||
OccupancyMetrics,
|
||||
},
|
||||
handle_child::{self},
|
||||
universal_pkg_installer::{par_install_language_dependencies_seq, RequiredDependency},
|
||||
DISABLE_NSJAIL, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, RUBY_CACHE_DIR, RUBY_REPOS,
|
||||
};
|
||||
lazy_static::lazy_static! {
|
||||
static ref RUBY_CONCURRENT_DOWNLOADS: usize = std::env::var("RUBY_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
|
||||
static ref RUBY_PATH: String = std::env::var("RUBY_PATH").unwrap_or_else(|_| "/usr/bin/ruby".to_string());
|
||||
static ref BUNDLE_PATH: String = std::env::var("RUBY_BUNDLE_PATH").unwrap_or_else(|_| "/usr/bin/bundle".to_string());
|
||||
static ref GEM_PATH: String = std::env::var("RUBY_GEM_PATH").unwrap_or_else(|_| "/usr/bin/gem".to_string());
|
||||
static ref RUBY_PROXY_ENVS: Vec<(String, String)> = {
|
||||
PROXY_ENVS
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_lowercase(), v))
|
||||
.collect()
|
||||
};
|
||||
}
|
||||
|
||||
const NSJAIL_CONFIG_RUN_RUBY_CONTENT: &str = include_str!("../nsjail/run.ruby.config.proto");
|
||||
const NSJAIL_CONFIG_DOWNLOAD_RUBY_CONTENT: &str =
|
||||
include_str!("../nsjail/download.ruby.config.proto");
|
||||
const NSJAIL_CONFIG_LOCK_RUBY_CONTENT: &str = include_str!("../nsjail/lock.ruby.config.proto");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const DEV_CONF_NSJAIL: &'static str = r#"
|
||||
# Mount nix store for nixos to work properly
|
||||
mount {
|
||||
src: "/nix/store"
|
||||
dst: "/nix/store"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
"#;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const DEV_CONF_NSJAIL: &'static str = "";
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct JobHandlerInput<'a> {
|
||||
pub base_internal_url: &'a str,
|
||||
pub canceled_by: &'a mut Option<CanceledBy>,
|
||||
pub client: &'a AuthedClient,
|
||||
pub parent_runnable_path: Option<String>,
|
||||
pub conn: &'a Connection,
|
||||
pub envs: HashMap<String, String>,
|
||||
pub inner_content: &'a str,
|
||||
pub job: &'a MiniPulledJob,
|
||||
pub job_dir: &'a str,
|
||||
pub mem_peak: &'a mut i32,
|
||||
pub occupancy_metrics: &'a mut OccupancyMetrics,
|
||||
pub requirements_o: Option<&'a String>,
|
||||
pub shared_mount: &'a str,
|
||||
pub worker_name: &'a str,
|
||||
}
|
||||
|
||||
pub async fn handle_ruby_job<'a>(
|
||||
mut args: JobHandlerInput<'a>,
|
||||
) -> Result<Box<sqlx::types::JsonRawValue>, Error> {
|
||||
// --- Prepare ---
|
||||
{
|
||||
prepare(&args).await?;
|
||||
}
|
||||
// --- Gen Lockfile ---
|
||||
|
||||
let lockfile = resolve(
|
||||
&args.job.id,
|
||||
args.inner_content,
|
||||
args.mem_peak,
|
||||
args.canceled_by,
|
||||
args.job_dir,
|
||||
args.conn,
|
||||
args.worker_name,
|
||||
&args.job.workspace_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// --- Install ---
|
||||
|
||||
let ir = install(&mut args, lockfile).await?;
|
||||
|
||||
// --- Execute ---
|
||||
{
|
||||
run(&mut args, ir).await?;
|
||||
}
|
||||
// --- Retrieve results ---
|
||||
{
|
||||
read_result(&args.job_dir, None).await
|
||||
}
|
||||
}
|
||||
pub async fn prepare<'a>(
|
||||
JobHandlerInput {
|
||||
job,
|
||||
conn,
|
||||
job_dir,
|
||||
inner_content,
|
||||
client,
|
||||
//
|
||||
..
|
||||
}: &JobHandlerInput<'a>,
|
||||
) -> Result<(), Error> {
|
||||
create_args_and_out_file(&client, job, job_dir, conn).await?;
|
||||
File::create(format!("{}/main.rb", job_dir))
|
||||
.await?
|
||||
.write_all(&wrap(inner_content)?.into_bytes())
|
||||
.await?;
|
||||
|
||||
let mini_wm_path = format!("{RUBY_CACHE_DIR}/gems/windmill-internal/windmill");
|
||||
if !std::fs::metadata(&mini_wm_path).is_ok() {
|
||||
fs::create_dir_all(&mini_wm_path).await?;
|
||||
|
||||
// Create inline.rb for dependency management (bundler/inline compatibility)
|
||||
File::create(format!("{}/inline.rb", &mini_wm_path))
|
||||
.await?
|
||||
.write_all(&wrap(
|
||||
r#"
|
||||
class GemfileProxy
|
||||
def initialize
|
||||
@gem_calls = []
|
||||
end
|
||||
|
||||
def source(arg = nil, &block)
|
||||
if arg.is_a?(String) && block_given?
|
||||
# Handle the case where the argument is a string and a block is given
|
||||
gemfile(&block)
|
||||
elsif arg.is_a?(String)
|
||||
# Handle the case where the argument is a string and no block is given
|
||||
# Do nothing in this case
|
||||
else
|
||||
raise ArgumentError, "Invalid argument or block"
|
||||
end
|
||||
end
|
||||
|
||||
def group(*args, **kwargs)
|
||||
# Handle group calls if needed
|
||||
end
|
||||
|
||||
def ruby(*args, **kwargs)
|
||||
warn "Custom Ruby runtime specifications (engine, engine_version, patchlevel) are not supported in this environment. Using system ruby"
|
||||
end
|
||||
|
||||
def gem(name, version = nil, require: nil, **kwargs)
|
||||
@gem_calls << { name: name, version: version, require: require, kwargs: kwargs }
|
||||
|
||||
case require
|
||||
when false
|
||||
# Skip requiring
|
||||
when nil, true
|
||||
begin
|
||||
Kernel.require(name)
|
||||
rescue LoadError => e
|
||||
warn "WARN: Gem '#{name}' failed to auto-require. Try either:"
|
||||
warn " `:require => false` or specify exact require path"
|
||||
warn "Warn details: #{e.class}: #{e.message}"
|
||||
end
|
||||
else
|
||||
begin
|
||||
Kernel.require(require.to_s)
|
||||
rescue LoadError => e
|
||||
warn "WARN: Gem '#{name}' failed to auto-require. Try either:"
|
||||
warn " `:require => false` or specify exact require path"
|
||||
warn "Warn details: #{e.class}: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def call_block(&block)
|
||||
instance_eval(&block) if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
def gemfile(&block)
|
||||
proxy = GemfileProxy.new
|
||||
proxy.call_block(&block)
|
||||
end
|
||||
|
||||
def main
|
||||
end
|
||||
"#
|
||||
)?.into_bytes())
|
||||
.await?;
|
||||
|
||||
// Create mini.rb for Windmill client methods
|
||||
File::create(format!("{}/mini.rb", &mini_wm_path))
|
||||
.await?
|
||||
.write_all(
|
||||
&wrap(
|
||||
r##"
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
require 'json'
|
||||
|
||||
# Windmill mini client methods
|
||||
def get_variable(path)
|
||||
base_url = ENV['BASE_INTERNAL_URL']
|
||||
workspace = ENV['WM_WORKSPACE']
|
||||
token = ENV['WM_TOKEN']
|
||||
|
||||
uri = URI("#{base_url}/api/w/#{workspace}/variables/get_value/#{path}")
|
||||
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = uri.scheme == 'https'
|
||||
|
||||
request = Net::HTTP::Get.new(uri)
|
||||
request['Authorization'] = "Bearer #{token}"
|
||||
|
||||
response = http.request(request)
|
||||
|
||||
if response.code == '200'
|
||||
JSON.parse(response.body)
|
||||
else
|
||||
raise "Failed to get variable #{path}: #{response.code} #{response.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def get_resource(path)
|
||||
base_url = ENV['BASE_INTERNAL_URL']
|
||||
workspace = ENV['WM_WORKSPACE']
|
||||
token = ENV['WM_TOKEN']
|
||||
|
||||
uri = URI("#{base_url}/api/w/#{workspace}/resources/get_value_interpolated/#{path}")
|
||||
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = uri.scheme == 'https'
|
||||
|
||||
request = Net::HTTP::Get.new(uri)
|
||||
request['Authorization'] = "Bearer #{token}"
|
||||
|
||||
response = http.request(request)
|
||||
|
||||
if response.code == '200'
|
||||
JSON.parse(response.body)
|
||||
else
|
||||
raise "Failed to get resource #{path}: #{response.code} #{response.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def main
|
||||
end
|
||||
"##,
|
||||
)?
|
||||
.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resolve<'a>(
|
||||
job_id: &Uuid,
|
||||
inner_content: &str,
|
||||
mem_peak: &mut i32,
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
job_dir: &str,
|
||||
conn: &Connection,
|
||||
worker_name: &str,
|
||||
w_id: &str,
|
||||
) -> Result<String, Error> {
|
||||
lazy_static::lazy_static! {
|
||||
static ref BUNDLER_RE: Regex = Regex::new(r#"(?m)^\s*require\s*['"]bundler/inline['"]"#).unwrap();
|
||||
static ref UNSUPPORTED_SOURCE_RE: Regex = Regex::new(r#"(?m)(?<src>^\s*source\s*['"]\S+://(?<usr>\S+):(?<passwd>\S+)@.*['"]\s*\n)"#).unwrap();
|
||||
static ref SOURCES_RE: Regex = Regex::new(r#"\S+://(?<creds>\S+:\S+)@(?<url>\S*)"#).unwrap();
|
||||
}
|
||||
|
||||
if BUNDLER_RE.find(&inner_content).is_some() {
|
||||
return Err(anyhow!(
|
||||
"Detected `require 'bundler/inline'` - please replace with `require 'windmill/inline'`.
|
||||
Your Gemfile syntax will continue to work as-is."
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let gemfile: String = windmill_parser_ruby::parse_ruby_requirements(&inner_content)?
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty() && !s.starts_with('#'))
|
||||
.join("\n");
|
||||
|
||||
if let Some(caps) = UNSUPPORTED_SOURCE_RE.captures(&gemfile) {
|
||||
if let (Some(src_m), Some(usr_m), Some(passwd_m)) =
|
||||
(caps.name("src"), caps.name("usr"), caps.name("passwd"))
|
||||
{
|
||||
let cause = src_m
|
||||
.as_str()
|
||||
.replace(usr_m.as_str(), "REDACTED")
|
||||
.replace(passwd_m.as_str(), "REDACTED");
|
||||
|
||||
return Err(anyhow!("!> {cause} - inline source credentials are not supported at the moment. Please set up credentials in instance settings, if you do not have access to it, contact your administrator. If you need this feature please let us know.").into());
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = File::create(job_dir.to_owned() + "/Gemfile").await?;
|
||||
file.write_all(&gemfile.as_bytes()).await?;
|
||||
|
||||
let req_hash = format!("ruby-{}", calculate_hash(&gemfile));
|
||||
if let Some(db) = conn.as_sql() {
|
||||
if let Some(cached) = sqlx::query_scalar!(
|
||||
"SELECT lockfile FROM pip_resolution_cache WHERE hash = $1",
|
||||
req_hash
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
{
|
||||
return Ok(cached);
|
||||
}
|
||||
}
|
||||
let lock = {
|
||||
append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!("\n--- RESOLVING LOCKFILE ---\n"),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut cmd = if !cfg!(windows) && !*DISABLE_NSJAIL {
|
||||
let nsjail_proto = format!("{}.lock.config.proto", Uuid::new_v4());
|
||||
let _ = write_file(
|
||||
&job_dir,
|
||||
&nsjail_proto,
|
||||
&NSJAIL_CONFIG_LOCK_RUBY_CONTENT
|
||||
.replace("{JOB_DIR}", job_dir)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
.replace("{DEV}", DEV_CONF_NSJAIL), // .replace("{BUILD}", &build_dir),
|
||||
)?;
|
||||
let mut cmd = Command::new(NSJAIL_PATH.as_str());
|
||||
cmd.args(vec!["--config", &nsjail_proto, "--", BUNDLE_PATH.as_str()]);
|
||||
cmd
|
||||
} else if cfg!(windows) {
|
||||
Command::new("bundle.bat")
|
||||
} else {
|
||||
Command::new(BUNDLE_PATH.as_str())
|
||||
};
|
||||
|
||||
cmd.env_clear()
|
||||
.current_dir(job_dir.to_owned())
|
||||
.envs(vec![
|
||||
("PATH".to_owned(), PATH_ENV.clone()),
|
||||
// Make sure there is nothing written to actual home
|
||||
// This way we keep everything clean, organized and maintable
|
||||
("HOME".to_owned(), RUBY_CACHE_DIR.to_owned()),
|
||||
])
|
||||
.envs(RUBY_PROXY_ENVS.clone());
|
||||
|
||||
for repo in RUBY_REPOS.read().await.clone().unwrap_or_default() {
|
||||
if let (Some(url), usr, Some(passwd)) =
|
||||
(repo.domain(), repo.username(), repo.password())
|
||||
{
|
||||
cmd.env(
|
||||
format!(
|
||||
"BUNDLE_{}",
|
||||
url
|
||||
// Align with bundlers' format. Example:
|
||||
// BUNDLE_GEM1__EXAMPLE__COM
|
||||
.replace(".", "__")
|
||||
.to_uppercase()
|
||||
// Remove trailing slashes
|
||||
.replace("/", "")
|
||||
),
|
||||
// BUNDLE_GEM1__EXAMPLE__COM=admin:123
|
||||
format!("{usr}:{passwd}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
cmd.args(&["lock", "--retry", "2"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
cmd.env("SystemRoot", crate::SYSTEM_ROOT.as_str())
|
||||
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
);
|
||||
}
|
||||
let child = start_child_process(cmd, "bundle", false).await?;
|
||||
|
||||
handle_child::handle_child(
|
||||
job_id,
|
||||
conn,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
child,
|
||||
!*DISABLE_NSJAIL,
|
||||
worker_name,
|
||||
w_id,
|
||||
"bundle",
|
||||
None,
|
||||
false,
|
||||
&mut None,
|
||||
// Some(&mut stdout),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path_lock = format!("{job_dir}/Gemfile.lock");
|
||||
let mut file = File::open(path_lock).await?;
|
||||
let mut req_content = "".to_string();
|
||||
file.read_to_string(&mut req_content).await?;
|
||||
req_content
|
||||
};
|
||||
|
||||
if let Some(db) = conn.as_sql() {
|
||||
sqlx::query!(
|
||||
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
|
||||
req_hash,
|
||||
lock.clone(),
|
||||
).fetch_optional(db).await?;
|
||||
}
|
||||
|
||||
append_logs(job_id, w_id, format!("\n{}", &lock), conn).await;
|
||||
Ok(lock)
|
||||
}
|
||||
|
||||
struct InstallRes {
|
||||
/// "/tmp/windmill/cache/ruby/gems/ab..xz-name-1.0.0/gems/name-1.0.0/lib:/tmp/windmill/..."
|
||||
rubylib: String,
|
||||
|
||||
/// ["/tmp/windmill/cache/ruby/gems/ab..xz-name-1.0.0", "/tmp/windmill/..."]
|
||||
top_level_paths: Vec<String>,
|
||||
}
|
||||
|
||||
async fn install<'a>(
|
||||
JobHandlerInput {
|
||||
worker_name,
|
||||
job,
|
||||
conn,
|
||||
job_dir,
|
||||
inner_content,
|
||||
envs,
|
||||
client,
|
||||
parent_runnable_path,
|
||||
..
|
||||
}: &mut JobHandlerInput<'a>,
|
||||
lockfile: String,
|
||||
) -> Result<InstallRes, Error> {
|
||||
#[derive(Debug, Clone)]
|
||||
enum CurrentSource {
|
||||
GIT { remote: Option<String>, revision: Option<String> },
|
||||
GEM { remote: Option<String> },
|
||||
INIT,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum FinalSource {
|
||||
#[allow(dead_code)]
|
||||
GIT {
|
||||
remote: String,
|
||||
revision: String,
|
||||
},
|
||||
GEM {
|
||||
remote: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CustomPayload {
|
||||
source: FinalSource,
|
||||
version: String,
|
||||
pkg: String,
|
||||
}
|
||||
|
||||
let mut deps = vec![];
|
||||
let mut last_source = CurrentSource::INIT;
|
||||
'lock_lines: for line in lockfile.lines() {
|
||||
// Trimed line
|
||||
let tl = line.trim_start();
|
||||
if tl.starts_with("GIT") {
|
||||
last_source = CurrentSource::GIT { remote: None, revision: None };
|
||||
continue 'lock_lines;
|
||||
} else if tl.starts_with("GEM") {
|
||||
last_source = CurrentSource::GEM { remote: None };
|
||||
continue 'lock_lines;
|
||||
}
|
||||
|
||||
const REMOTE: &str = "remote: ";
|
||||
if line.contains(REMOTE) {
|
||||
match &mut last_source {
|
||||
CurrentSource::GIT { remote, .. } => remote.replace(tl.replace(REMOTE, "")),
|
||||
CurrentSource::GEM { remote } => remote.replace(tl.replace(REMOTE, "")),
|
||||
CurrentSource::INIT => return Err(Error::InternalErr("BUG on installation stage: detected initial enum variant at the stage it was not supposed to be at. Please report this issue.".to_owned())),
|
||||
};
|
||||
continue 'lock_lines;
|
||||
}
|
||||
|
||||
const REVISION: &str = "revision: ";
|
||||
if line.contains(REVISION) {
|
||||
if let CurrentSource::GIT { revision, .. } = &mut last_source {
|
||||
revision.replace(tl.replace(REVISION, ""));
|
||||
}
|
||||
continue 'lock_lines;
|
||||
}
|
||||
|
||||
// Check on untrimed line
|
||||
if !line.starts_with(" ") || line.starts_with(" ") {
|
||||
// "1234" "123456"
|
||||
// For example:
|
||||
//
|
||||
// GIT
|
||||
// remote: https://github.com/rails/rails.git
|
||||
// revision: 8cb4f8ceffe375f64052579cd7a580bfdaf36d61
|
||||
// specs:
|
||||
// actioncable (8.1.0.alpha)
|
||||
// actionpack (= 8.1.0.alpha)
|
||||
// activesupport (= 8.1.0.alpha)
|
||||
// nio4r (~> 2.0)
|
||||
// websocket-driver (>= 0.6.1)
|
||||
// zeitwerk (~> 2.6)
|
||||
// actionmailbox (8.1.0.alpha)
|
||||
// actionpack (= 8.1.0.alpha)
|
||||
// activejob (= 8.1.0.alpha)
|
||||
// activerecord (= 8.1.0.alpha)
|
||||
// activestorage (= 8.1.0.alpha)
|
||||
// activesupport (= 8.1.0.alpha)
|
||||
// mail (>= 2.8.0)
|
||||
// actionmailer (8.1.0.alpha)
|
||||
// ...
|
||||
//
|
||||
// In this snippet we only want actioncable, actionmailbox and actionmailer
|
||||
//
|
||||
// If it got the hit than that's not it, just skip
|
||||
continue 'lock_lines;
|
||||
}
|
||||
|
||||
let buf = tl.replace(['(', ')'], "");
|
||||
let &[pkg, version, ..] = buf.split_whitespace().collect_vec().as_slice() else {
|
||||
return Err(anyhow!("Cannot determine version and package name for: {}", tl).into());
|
||||
};
|
||||
|
||||
// Strip platform from version. E.g:
|
||||
// 1.18.9-x86_64-darwin -> 1.18.9
|
||||
let version = version.split('-').next().unwrap_or(version);
|
||||
|
||||
let custom_payload = CustomPayload {
|
||||
version: version.into(),
|
||||
pkg: pkg.into(),
|
||||
source: match last_source.clone() {
|
||||
CurrentSource::GIT { remote, revision } => FinalSource::GIT {
|
||||
remote: remote.unwrap_or_default(),
|
||||
revision: revision.unwrap_or_default(),
|
||||
},
|
||||
CurrentSource::GEM { remote } => {
|
||||
FinalSource::GEM { remote: remote.unwrap_or_default() }
|
||||
},
|
||||
CurrentSource::INIT => return Err(Error::InternalErr("BUG on installation stage: detected initial enum variant at the stage it was not supposed to be at. Please report this issue.".to_owned())),
|
||||
},
|
||||
};
|
||||
|
||||
// Calculate hash based on source
|
||||
// Obfuscation is essential because remote can include credentials
|
||||
let mut hash = calculate_hash(&format!("{:?}", custom_payload.clone()));
|
||||
|
||||
// Use 32 characters which is 256 bits
|
||||
hash.truncate(32);
|
||||
|
||||
// Final format:
|
||||
// 123...zx-activesupport-8.0.2
|
||||
// ^^^^^^^^ hash based on source and type (GEM or GIT)
|
||||
let handle = format!("{}-{}-{}", hash, pkg, version);
|
||||
let path = format!("{RUBY_CACHE_DIR}/gems/{}", &handle);
|
||||
|
||||
deps.push(RequiredDependency {
|
||||
path,
|
||||
_s3_handle: handle,
|
||||
display_name: tl.to_owned(),
|
||||
custom_payload,
|
||||
});
|
||||
}
|
||||
|
||||
let job_dir = job_dir.to_owned();
|
||||
let jailed = !cfg!(windows) && !*DISABLE_NSJAIL;
|
||||
let RubyAnnotations { verbose } = RubyAnnotations::parse(&inner_content);
|
||||
let repos = RUBY_REPOS.read().await.clone().unwrap_or_default();
|
||||
let (envs, reserved_variables) = (
|
||||
envs.clone(),
|
||||
get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone()).await?,
|
||||
);
|
||||
par_install_language_dependencies_seq(
|
||||
deps.clone(),
|
||||
"ruby",
|
||||
"gem",
|
||||
false,
|
||||
*RUBY_CONCURRENT_DOWNLOADS,
|
||||
// true,
|
||||
// crate::common::InstallStrategy::Single(Arc::new(move |dependency| {
|
||||
move |dependency| {
|
||||
let mut cmd = if jailed {
|
||||
std::fs::create_dir_all(&dependency.path)?;
|
||||
let nsjail_proto = format!("{}.download.config.proto", Uuid::new_v4());
|
||||
let _ = write_file(
|
||||
&job_dir,
|
||||
&nsjail_proto,
|
||||
&NSJAIL_CONFIG_DOWNLOAD_RUBY_CONTENT
|
||||
.replace("{TARGET}", &dependency.path)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
.replace("{DEV}", DEV_CONF_NSJAIL), // .replace("{BUILD}", &build_dir),
|
||||
)?;
|
||||
let mut cmd = Command::new(NSJAIL_PATH.as_str());
|
||||
cmd.args(vec!["--config", &nsjail_proto, "--", GEM_PATH.as_str()]);
|
||||
cmd
|
||||
} else {
|
||||
Command::new(if cfg!(windows) {
|
||||
"gem.cmd"
|
||||
} else {
|
||||
GEM_PATH.as_str()
|
||||
})
|
||||
};
|
||||
cmd.env_clear()
|
||||
.current_dir(&job_dir)
|
||||
.envs(vec![
|
||||
("PATH".to_owned(), PATH_ENV.clone()),
|
||||
// Make sure there is nothing written to actual home
|
||||
// This way we keep everything clean, organized and maintable
|
||||
("HOME".to_owned(), RUBY_CACHE_DIR.to_owned()),
|
||||
])
|
||||
.envs(RUBY_PROXY_ENVS.clone());
|
||||
|
||||
// Figure out source
|
||||
let source = {
|
||||
let source = match dependency.custom_payload.source {
|
||||
FinalSource::GIT { .. } => return Err(anyhow!("GIT is not supported").into()),
|
||||
FinalSource::GEM { remote } => remote,
|
||||
}
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
let src_url = Url::parse(&source)?;
|
||||
repos
|
||||
.iter()
|
||||
.find(|inst_url| {
|
||||
inst_url.domain().is_some()
|
||||
&& (inst_url.domain(), inst_url.path())
|
||||
== (src_url.domain(), src_url.path())
|
||||
})
|
||||
.map(Url::to_string)
|
||||
.unwrap_or(source)
|
||||
};
|
||||
|
||||
cmd.args(&[
|
||||
"install",
|
||||
&dependency.custom_payload.pkg,
|
||||
"--version",
|
||||
&dependency.custom_payload.version,
|
||||
"--install-dir",
|
||||
&dependency.path,
|
||||
// Disable transitive dependencies!
|
||||
"--ignore-dependencies",
|
||||
"--no-document",
|
||||
// Do not use default sources
|
||||
"--clear-sources",
|
||||
// Specify the one from lockfile
|
||||
"--source",
|
||||
source.as_str(),
|
||||
])
|
||||
.envs(envs.clone())
|
||||
.envs(reserved_variables.clone())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
if !verbose {
|
||||
// Make it output minimal info
|
||||
cmd.args(&["--quiet", "--silent"]);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
cmd.env("SystemRoot", crate::SYSTEM_ROOT.as_str())
|
||||
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
)
|
||||
.env(
|
||||
"SYSTEMDRIVE",
|
||||
std::env::var("SYSTEMDRIVE").unwrap_or_else(|_| String::from("C:")),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
},
|
||||
// async move |_| Ok(()),
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
worker_name,
|
||||
jailed,
|
||||
conn,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut res = InstallRes {
|
||||
rubylib: deps
|
||||
.iter()
|
||||
.map(|rd| {
|
||||
format!(
|
||||
"{}/gems/{}-{}/lib",
|
||||
rd.path, rd.custom_payload.pkg, rd.custom_payload.version
|
||||
)
|
||||
})
|
||||
.join(":"),
|
||||
top_level_paths: deps.iter().map(|rd| rd.path.clone()).collect_vec(),
|
||||
};
|
||||
// Include builtin windmill client
|
||||
{
|
||||
const WM_INTERNAL: &str = concatcp!(RUBY_CACHE_DIR, "/gems/windmill-internal");
|
||||
res.top_level_paths.push(WM_INTERNAL.to_owned());
|
||||
res.rubylib += format!(":{WM_INTERNAL}").as_str();
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn run<'a>(
|
||||
JobHandlerInput {
|
||||
occupancy_metrics,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
worker_name,
|
||||
job,
|
||||
conn,
|
||||
job_dir,
|
||||
shared_mount,
|
||||
client,
|
||||
envs,
|
||||
base_internal_url,
|
||||
parent_runnable_path,
|
||||
..
|
||||
}: &mut JobHandlerInput<'a>,
|
||||
InstallRes { rubylib, top_level_paths }: InstallRes,
|
||||
) -> Result<(), Error> {
|
||||
let reserved_variables =
|
||||
get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone()).await?;
|
||||
|
||||
let child = if !cfg!(windows) && !*DISABLE_NSJAIL {
|
||||
append_logs(
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
format!("\n--- ISOLATED RUBY CODE EXECUTION ---\n"),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
let shared_deps = top_level_paths
|
||||
.into_iter()
|
||||
.map(|pp| {
|
||||
format!(
|
||||
r#"
|
||||
mount {{
|
||||
src: "{pp}"
|
||||
dst: "{pp}"
|
||||
is_bind: true
|
||||
rw: false
|
||||
}}
|
||||
"#
|
||||
)
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
write_file(
|
||||
job_dir,
|
||||
"run.config.proto",
|
||||
&NSJAIL_CONFIG_RUN_RUBY_CONTENT
|
||||
.replace("{JOB_DIR}", job_dir)
|
||||
.replace("{SHARED_MOUNT}", &shared_mount)
|
||||
.replace("{SHARED_DEPENDENCIES}", &shared_deps)
|
||||
.replace("{DEV}", DEV_CONF_NSJAIL)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
|
||||
)?;
|
||||
let mut cmd = Command::new(NSJAIL_PATH.as_str());
|
||||
cmd.env_clear()
|
||||
.current_dir(job_dir)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.env("RUBYLIB", rubylib.as_str())
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(RUBY_PROXY_ENVS.clone())
|
||||
.envs(PROXY_ENVS.clone())
|
||||
.args(vec![
|
||||
"--config",
|
||||
"run.config.proto",
|
||||
"--",
|
||||
RUBY_PATH.as_str(),
|
||||
"main.rb",
|
||||
]);
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
start_child_process(cmd, NSJAIL_PATH.as_str(), false).await?
|
||||
} else {
|
||||
append_logs(
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
format!("\n--- RUBY CODE EXECUTION ---\n"),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut cmd = Command::new(if cfg!(windows) {
|
||||
"ruby.exe"
|
||||
} else {
|
||||
RUBY_PATH.as_str()
|
||||
});
|
||||
|
||||
#[cfg(windows)]
|
||||
let rubylib = rubylib.replace(":", ";");
|
||||
|
||||
cmd.env_clear()
|
||||
.current_dir(job_dir.to_owned())
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("RUBYLIB", rubylib.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.envs(reserved_variables)
|
||||
.envs(RUBY_PROXY_ENVS.clone())
|
||||
.envs(PROXY_ENVS.clone())
|
||||
.envs(envs);
|
||||
|
||||
cmd.args(&["main.rb"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
cmd.env("SystemRoot", crate::SYSTEM_ROOT.as_str())
|
||||
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
);
|
||||
}
|
||||
start_child_process(cmd, "ruby", false).await?
|
||||
};
|
||||
handle_child::handle_child(
|
||||
&job.id,
|
||||
conn,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
child,
|
||||
!*DISABLE_NSJAIL,
|
||||
worker_name,
|
||||
&job.workspace_id,
|
||||
"ruby",
|
||||
job.timeout,
|
||||
false,
|
||||
&mut Some(occupancy_metrics),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
fn wrap(inner_content: &str) -> Result<String, Error> {
|
||||
let sig = parse_ruby_signature(inner_content)?;
|
||||
let spread = sig
|
||||
.args
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|Arg { name, .. }| format!("a[\"{}\"]", name))
|
||||
.collect_vec()
|
||||
.join(",");
|
||||
Ok(
|
||||
r#"INNER_CONTENT
|
||||
|
||||
require 'json'
|
||||
a = JSON.parse(File.read("args.json"))
|
||||
res = main(SPREAD)
|
||||
File.open("result.json", "w") do |file|
|
||||
file.write(JSON.generate(res))
|
||||
end
|
||||
"#
|
||||
.replace("INNER_CONTENT", inner_content)
|
||||
.replace("SPREAD", &spread), // .replace("TRANSFORM", transform)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use itertools::Itertools;
|
||||
use process_wrap::tokio::TokioChildWrapper;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
process::Command,
|
||||
sync::{broadcast::Sender, OwnedSemaphorePermit, RwLock, Semaphore},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use windmill_common::{error, worker::Connection};
|
||||
|
||||
use crate::{common::start_child_process, DISABLE_NSJAIL};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequiredDependency<T: Clone + Send + Sync> {
|
||||
/// Expected directory of dependency in cache
|
||||
/// For example:
|
||||
/// /tmp/windmill/cache/python_311/rich==0.0.0
|
||||
/// IMPORTANT!: path should not end with '/'
|
||||
pub path: String,
|
||||
/// Name to use for S3 tars
|
||||
pub _s3_handle: String,
|
||||
/// Display name
|
||||
/// Name that will be used for console output and logging
|
||||
pub display_name: String,
|
||||
/// Custom payload can be used for passing information easily. If you wish not to use it just leave '()'
|
||||
pub custom_payload: T,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum InstallStrategy<T: Clone + Send + Sync> {
|
||||
/// Will invoke callback to install single dependency
|
||||
Single(Arc<dyn Fn(RequiredDependency<T>) -> Result<Command, error::Error> + Send + Sync>),
|
||||
/// Will try to pull S3 first and will invoke closure to install the rest
|
||||
AllAtOnce(Arc<RwLock<Vec<RequiredDependency<T>>>>),
|
||||
}
|
||||
|
||||
/// # General
|
||||
///
|
||||
/// Languages that compile usually include dependencies in final executable.
|
||||
/// When dynamic languages do not and runtime dependencies are provided separately.
|
||||
///
|
||||
/// This helper assumes that the language is dynamic.
|
||||
/// Python, Ruby, Java are dynamic and they can use this helper.
|
||||
///
|
||||
/// # Features
|
||||
///
|
||||
/// This helper will install all specified dependencies in parallel and if it is EE, cache to S3
|
||||
/// It has atomic success file, allowing to distinguish failed installations from succesfull.
|
||||
///
|
||||
/// Besides that it provides console output and does logging.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// Most important arguments in this helper are `deps` and `install_fn`
|
||||
///
|
||||
/// In `deps` you specify all dependencies that are needed to be on worker in order to execute script.
|
||||
/// You don't know which are actually installed and which are not.
|
||||
///
|
||||
/// `deps` is a vector of RequiredDependency. Check [RequiredDependency] for more context.
|
||||
///
|
||||
/// After `deps` are provided helper will check each dependency and check if it is in cache, if not it will try to pull from S3
|
||||
/// and if it does not work either, it will invoke `install_fn` closure.
|
||||
/// Closure arguments has dependency name as well as its expected path in cache.
|
||||
/// Closure should return Command that will install dependency to asked place.
|
||||
#[allow(dead_code)]
|
||||
pub async fn par_install_language_dependencies_all_at_once<
|
||||
'a,
|
||||
T: Clone + std::marker::Send + Sync + 'a + 'static,
|
||||
>(
|
||||
deps: Vec<RequiredDependency<T>>,
|
||||
_language_name: &'a str,
|
||||
installer_executable_name: &'a str,
|
||||
_platform_agnostic: bool,
|
||||
concurrent_downloads: usize,
|
||||
stdout_on_err: bool,
|
||||
callback: impl Fn(Vec<RequiredDependency<T>>) -> Result<Command, error::Error>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
postinstall_cb: impl AsyncFn(Vec<RequiredDependency<T>>) -> Result<(), error::Error>,
|
||||
job_id: &'a Uuid,
|
||||
w_id: &'a str,
|
||||
worker_name: &'a str,
|
||||
jailed: bool,
|
||||
conn: &'a Connection,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let is_not_pro = !matches!(
|
||||
windmill_common::ee_oss::get_license_plan().await,
|
||||
windmill_common::ee_oss::LicensePlan::Pro
|
||||
);
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if is_not_pro {
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!("\nLooking for packages on S3:\n"),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let total_time = std::time::Instant::now();
|
||||
let (missing, name_max_length) = filter_to_missing(deps, job_id, w_id, jailed, conn).await?;
|
||||
if missing.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let to_batch_install = Arc::new(RwLock::new(vec![]));
|
||||
let handles = spawn_wrapped_installation_threads(
|
||||
missing,
|
||||
name_max_length,
|
||||
InstallStrategy::AllAtOnce(to_batch_install.clone()),
|
||||
installer_executable_name,
|
||||
concurrent_downloads,
|
||||
job_id,
|
||||
w_id,
|
||||
worker_name,
|
||||
conn,
|
||||
_language_name,
|
||||
_platform_agnostic,
|
||||
)
|
||||
.await?;
|
||||
let installation_res = process_handles(handles, w_id).await;
|
||||
if !to_batch_install.read().await.is_empty() {
|
||||
let for_all_at_once_copy = to_batch_install.read().await.clone();
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!("\n\nFetching {} packages...\n", for_all_at_once_copy.len()),
|
||||
&conn,
|
||||
)
|
||||
.await;
|
||||
let cmd = callback(for_all_at_once_copy.clone())?;
|
||||
let child = start_child_process(cmd, &installer_executable_name, false).await?;
|
||||
let mut buf = "".to_owned();
|
||||
let pipe_stdout = if stdout_on_err { Some(&mut buf) } else { None };
|
||||
|
||||
if let Err(e) = crate::handle_child::handle_child(
|
||||
&(if pipe_stdout.is_some() {
|
||||
Uuid::nil()
|
||||
} else {
|
||||
*job_id
|
||||
}),
|
||||
conn,
|
||||
// TODO: Return mem_peak
|
||||
&mut 0,
|
||||
// TODO: Return canceld_by_ref
|
||||
&mut None,
|
||||
child,
|
||||
!*DISABLE_NSJAIL,
|
||||
&worker_name,
|
||||
&w_id,
|
||||
&installer_executable_name,
|
||||
None,
|
||||
false,
|
||||
&mut None,
|
||||
pipe_stdout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
bail!(format!(
|
||||
"\nerror while installing dependencies: {e:?}\n{}",
|
||||
buf
|
||||
));
|
||||
}
|
||||
{
|
||||
postinstall_cb(for_all_at_once_copy.clone()).await?;
|
||||
}
|
||||
for RequiredDependency { path, _s3_handle, .. } in for_all_at_once_copy.into_iter() {
|
||||
mark_success(path.clone(), job_id, w_id).await;
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
{
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
let language_name = _language_name.to_owned();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = crate::global_cache::build_tar_and_push(
|
||||
os,
|
||||
path,
|
||||
language_name,
|
||||
Some(_s3_handle),
|
||||
_platform_agnostic,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to build tar and push: {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finish_installation(total_time, job_id, w_id, conn).await;
|
||||
installation_res
|
||||
}
|
||||
|
||||
/// # General
|
||||
///
|
||||
/// Languages that compile usually include dependencies in final executable.
|
||||
/// When dynamic languages do not and runtime dependencies are provided separately.
|
||||
///
|
||||
/// This helper assumes that the language is dynamic.
|
||||
/// Python, Ruby, Java are dynamic and they can use this helper.
|
||||
///
|
||||
/// # Features
|
||||
///
|
||||
/// This helper will install all specified dependencies in parallel and if it is EE, cache to S3
|
||||
/// It has atomic success file, allowing to distinguish failed installations from succesfull.
|
||||
///
|
||||
/// Besides that it provides console output and does logging.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// Most important arguments in this helper are `deps` and `install_fn`
|
||||
///
|
||||
/// In `deps` you specify all dependencies that are needed to be on worker in order to execute script.
|
||||
/// You don't know which are actually installed and which are not.
|
||||
///
|
||||
/// `deps` is a vector of RequiredDependency. Check [RequiredDependency] for more context.
|
||||
///
|
||||
/// After `deps` are provided helper will check each dependency and check if it is in cache, if not it will try to pull from S3
|
||||
/// and if it does not work either, it will invoke `install_fn` closure.
|
||||
/// Closure arguments has dependency name as well as it`s expected path in cache.
|
||||
/// Closure should return Command that will install dependency to asked place.
|
||||
#[allow(dead_code)]
|
||||
pub async fn par_install_language_dependencies_seq<
|
||||
'a,
|
||||
T: Clone + std::marker::Send + Sync + 'a + 'static,
|
||||
>(
|
||||
deps: Vec<RequiredDependency<T>>,
|
||||
_language_name: &'a str,
|
||||
installer_executable_name: &'a str,
|
||||
_platform_agnostic: bool,
|
||||
concurrent_downloads: usize,
|
||||
callback: impl Fn(RequiredDependency<T>) -> Result<Command, error::Error> + Send + Sync + 'static,
|
||||
job_id: &'a Uuid,
|
||||
w_id: &'a str,
|
||||
worker_name: &'a str,
|
||||
jailed: bool,
|
||||
conn: &'a Connection,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let is_not_pro = !matches!(
|
||||
windmill_common::ee_oss::get_license_plan().await,
|
||||
windmill_common::ee_oss::LicensePlan::Pro
|
||||
);
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if is_not_pro {
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!("\nLooking for packages on S3:\n"),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let total_time = std::time::Instant::now();
|
||||
let (missing, name_max_length) = filter_to_missing(deps, job_id, w_id, jailed, conn).await?;
|
||||
if missing.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let handles = spawn_wrapped_installation_threads(
|
||||
missing,
|
||||
name_max_length,
|
||||
InstallStrategy::Single(Arc::new(callback)),
|
||||
installer_executable_name,
|
||||
concurrent_downloads,
|
||||
job_id,
|
||||
w_id,
|
||||
worker_name,
|
||||
conn,
|
||||
_language_name,
|
||||
_platform_agnostic,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let installation_res = process_handles(handles, w_id).await;
|
||||
finish_installation(total_time, job_id, w_id, conn).await;
|
||||
installation_res
|
||||
}
|
||||
|
||||
type NameMaxLength = usize;
|
||||
async fn filter_to_missing<'a, T: Clone + std::marker::Send + Sync + 'a + 'static>(
|
||||
mut deps: Vec<RequiredDependency<T>>,
|
||||
job_id: &Uuid,
|
||||
w_id: &str,
|
||||
jailed: bool,
|
||||
conn: &Connection,
|
||||
) -> anyhow::Result<(Vec<RequiredDependency<T>>, NameMaxLength)> {
|
||||
// Unique to flatten all same values
|
||||
deps = deps.into_iter().unique_by(|rd| rd.path.clone()).collect();
|
||||
// Total to install
|
||||
let mut missing = vec![];
|
||||
// Name max length
|
||||
let mut name_ml = 0;
|
||||
for rd in deps.into_iter() {
|
||||
let display_name = rd.display_name.clone();
|
||||
if rd.path.ends_with("/") {
|
||||
anyhow::bail!("Internal error: path should not end with '/'")
|
||||
}
|
||||
{
|
||||
// Later will help us align text in log console
|
||||
if display_name.len() > name_ml {
|
||||
name_ml = rd.display_name.len();
|
||||
}
|
||||
}
|
||||
// Will look like: /tmp/windmill/cache/lang/dependency.valid.windmill
|
||||
if tokio::fs::metadata(rd.path.clone() + ".valid.windmill")
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
missing.push(rd);
|
||||
}
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
if jailed {
|
||||
format!("\n--- ISOLATED INSTALLATION ---\n\nTo be installed:\n\n")
|
||||
} else {
|
||||
format!("\n--- INSTALLATION ---\n\nTo be installed:\n\n")
|
||||
},
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
let to_log = missing
|
||||
.iter()
|
||||
.map(|rd| format!("- {}", &rd.display_name))
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
|
||||
windmill_queue::append_logs(job_id, w_id, to_log, conn).await;
|
||||
}
|
||||
Ok((missing, name_ml))
|
||||
}
|
||||
|
||||
enum Action<T: Clone + Send + Sync> {
|
||||
Install(Box<dyn TokioChildWrapper + 'static>),
|
||||
AddToBulk(Arc<RwLock<Vec<RequiredDependency<T>>>>),
|
||||
}
|
||||
struct TaskKiller(tokio::sync::broadcast::Sender<String>);
|
||||
impl Drop for TaskKiller {
|
||||
fn drop(&mut self) {
|
||||
// We don't care if it actually stopped anything or not.
|
||||
// In some cases it will fire and actually stop
|
||||
// In other cases do nothing
|
||||
#[allow(unused_must_use)]
|
||||
self.0.send("Installation failed".to_owned());
|
||||
}
|
||||
}
|
||||
// Callback will be invoked only if could not pull from S3
|
||||
async fn spawn_wrapped_installation_threads<
|
||||
'a,
|
||||
T: Clone + std::marker::Send + Sync + 'a + 'static,
|
||||
>(
|
||||
missing: Vec<RequiredDependency<T>>,
|
||||
name_ml: usize,
|
||||
strategy: InstallStrategy<T>,
|
||||
installer_executable_name: &str,
|
||||
concurrent_downloads: usize,
|
||||
job_id: &Uuid,
|
||||
w_id: &str,
|
||||
worker_name: &str,
|
||||
conn: &Connection,
|
||||
_language_name: &str,
|
||||
_platform_agnostic: bool,
|
||||
) -> anyhow::Result<(
|
||||
Vec<JoinHandle<anyhow::Result<TaskKiller>>>,
|
||||
tokio::sync::broadcast::Sender<()>,
|
||||
)> {
|
||||
// Semaphore will panic if value less then 1
|
||||
let parallel_limit = concurrent_downloads.clamp(1, 30);
|
||||
tracing::info!(
|
||||
workspace_id = %w_id,
|
||||
"Install parallel limit: {}, job: {}",
|
||||
parallel_limit,
|
||||
job_id
|
||||
);
|
||||
|
||||
let (mut handles, semaphore, total_to_install, counter_arc) = (
|
||||
vec![],
|
||||
Arc::new(Semaphore::new(parallel_limit)),
|
||||
missing.len(),
|
||||
Arc::new(tokio::sync::Mutex::new(0)),
|
||||
);
|
||||
|
||||
// Pretty sensitive. Single drop will fail installation
|
||||
let (kill_tx, ..) = tokio::sync::broadcast::channel::<String>(10);
|
||||
// ^^^^^^^ Original will go into listen_to_cancel
|
||||
//
|
||||
// But before that it will be used to populate t and r for every thread
|
||||
let trxs: Vec<(
|
||||
tokio::sync::broadcast::Sender<String>,
|
||||
tokio::sync::broadcast::Receiver<String>,
|
||||
)> = (0..missing.len())
|
||||
.map(|_| (kill_tx.clone(), kill_tx.subscribe()))
|
||||
.collect();
|
||||
|
||||
// Listen to cancel events and stop jobs
|
||||
// NOTE:
|
||||
// once closer is dropped, the listener will be closed
|
||||
let closer = listen_to_cancel(job_id.clone(), w_id.to_owned(), conn.clone(), kill_tx).await;
|
||||
|
||||
for (dep, (kill_tx, mut kill_rx)) in missing.into_iter().zip(trxs) {
|
||||
let permit = semaphore.clone().acquire_owned().await; // Acquire a permit
|
||||
|
||||
if let Err(_) = permit {
|
||||
tracing::error!(
|
||||
workspace_id = %w_id,
|
||||
"Cannot acquire permit on semaphore, that can only mean that semaphore has been closed."
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let permit = permit.unwrap();
|
||||
|
||||
let action = match strategy {
|
||||
InstallStrategy::Single(ref callback) => Action::Install(
|
||||
start_child_process(callback(dep.clone())?, &installer_executable_name, false)
|
||||
.await?,
|
||||
),
|
||||
InstallStrategy::AllAtOnce(ref rw_lock) => Action::AddToBulk(Arc::clone(rw_lock)),
|
||||
};
|
||||
let task_fut = try_install_one_detached(
|
||||
dep,
|
||||
installer_executable_name.to_owned(),
|
||||
job_id.clone(),
|
||||
w_id.to_owned(),
|
||||
worker_name.to_owned(),
|
||||
conn.to_owned(),
|
||||
counter_arc.clone(),
|
||||
total_to_install,
|
||||
name_ml,
|
||||
action,
|
||||
_language_name.to_owned(),
|
||||
_platform_agnostic,
|
||||
permit,
|
||||
TaskKiller(kill_tx),
|
||||
);
|
||||
handles.push(tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
reason = kill_rx.recv() => return Err(anyhow::anyhow!("Installation stopped. Reason: {:?}", reason)),
|
||||
r = task_fut => return r
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
Ok((handles, closer))
|
||||
}
|
||||
/// Do not drop the returned sender! As soon as you drop it the polling thread will stop
|
||||
pub async fn listen_to_cancel(
|
||||
job_id: Uuid,
|
||||
w_id: String,
|
||||
conn: Connection,
|
||||
killpill: tokio::sync::broadcast::Sender<String>,
|
||||
) -> tokio::sync::broadcast::Sender<()> {
|
||||
let (close_tx, mut close_rx) = tokio::sync::broadcast::channel::<()>(1);
|
||||
tokio::spawn(async move {
|
||||
'outer: loop {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => {
|
||||
// TODO: Add memory usage tracking
|
||||
if crate::worker_utils::ping_job_status(&conn, &job_id, None, None).await.map(|r| r.canceled_by.is_some()).unwrap_or(false){
|
||||
tracing::info!(
|
||||
// If there is listener on other side,
|
||||
workspace_id = %w_id,
|
||||
"cancelling installations",
|
||||
);
|
||||
if let Err(ref e) = killpill.send("Job was canceled".to_owned()) {
|
||||
tracing::error!(
|
||||
// If there is listener on other side,
|
||||
workspace_id = %w_id,
|
||||
"failed to send done: Probably receiving end closed too early or have not opened yet\n{}",
|
||||
// If there is no listener, it will be dropped safely
|
||||
e
|
||||
);
|
||||
}
|
||||
// Stop
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
},
|
||||
_ = close_rx.recv() => break 'outer,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We will return the sender that can stop created thread.
|
||||
// It is relatively safe, since if this handle is being dropped from the gecko the thread will stop immediately
|
||||
// but it will also chainreaction all installation threads, so be carefull
|
||||
close_tx
|
||||
}
|
||||
|
||||
async fn try_install_one_detached<'a, T: Clone + std::marker::Send + Sync + 'a + 'static>(
|
||||
dep: RequiredDependency<T>,
|
||||
installer_executable_name: String,
|
||||
job_id: Uuid,
|
||||
w_id: String,
|
||||
worker_name: String,
|
||||
conn: Connection,
|
||||
counter_arc: Arc<tokio::sync::Mutex<usize>>,
|
||||
total_to_install: usize,
|
||||
name_ml: usize,
|
||||
action: Action<T>,
|
||||
_language_name: String,
|
||||
_platform_agnostic: bool,
|
||||
_permit: OwnedSemaphorePermit,
|
||||
// If dropped the entire installation fails and all installation threads are being stopped
|
||||
// That's why we just pass it to return so it is not being dropped
|
||||
kill_all_tasks: TaskKiller,
|
||||
) -> anyhow::Result<TaskKiller> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let is_not_pro = !matches!(
|
||||
windmill_common::ee_oss::get_license_plan().await,
|
||||
windmill_common::ee_oss::LicensePlan::Pro
|
||||
);
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let s3_pull_future = if is_not_pro {
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
Some(crate::global_cache::pull_from_tar(
|
||||
os,
|
||||
dep.path.clone(),
|
||||
_language_name.to_owned(),
|
||||
Some(dep._s3_handle.clone()),
|
||||
_platform_agnostic,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(s3_pull_future) = s3_pull_future {
|
||||
if let Err(e) = s3_pull_future.await {
|
||||
tracing::info!(
|
||||
workspace_id = %w_id,
|
||||
"No tarball was found for {:?} on S3 or different problem occured {job_id}:\n{e}",
|
||||
&dep._s3_handle.clone()
|
||||
);
|
||||
} else {
|
||||
mark_success(dep.path, &job_id, &w_id).await;
|
||||
print_success(
|
||||
true,
|
||||
false,
|
||||
&job_id,
|
||||
&w_id,
|
||||
&dep.display_name,
|
||||
name_ml,
|
||||
counter_arc,
|
||||
total_to_install,
|
||||
start,
|
||||
&conn,
|
||||
)
|
||||
.await;
|
||||
return Ok(kill_all_tasks);
|
||||
}
|
||||
}
|
||||
let child = match action {
|
||||
Action::Install(child) => child,
|
||||
Action::AddToBulk(rw_lock) => {
|
||||
rw_lock.write().await.push(dep.clone());
|
||||
return Ok(kill_all_tasks);
|
||||
}
|
||||
};
|
||||
if let Err(e) = crate::handle_child::handle_child(
|
||||
&job_id,
|
||||
&conn,
|
||||
// TODO: Return mem_peak
|
||||
&mut 0,
|
||||
// TODO: Return canceld_by_ref
|
||||
&mut None,
|
||||
child,
|
||||
!*DISABLE_NSJAIL,
|
||||
&worker_name,
|
||||
&w_id,
|
||||
&installer_executable_name,
|
||||
None,
|
||||
false,
|
||||
&mut None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
windmill_queue::append_logs(
|
||||
&job_id,
|
||||
&w_id,
|
||||
format!(
|
||||
"\n[x] - error while installing {}: {e:?}",
|
||||
&dep.display_name
|
||||
),
|
||||
&conn,
|
||||
)
|
||||
.await;
|
||||
bail!(format!(
|
||||
"\n error while installing dependencies: {e:?}\n{}",
|
||||
&dep.display_name
|
||||
));
|
||||
} else {
|
||||
mark_success(dep.path.clone(), &job_id, &w_id).await;
|
||||
print_success(
|
||||
false,
|
||||
true,
|
||||
&job_id,
|
||||
&w_id,
|
||||
&dep.display_name,
|
||||
name_ml,
|
||||
counter_arc,
|
||||
total_to_install,
|
||||
start,
|
||||
&conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
{
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
let language_name = _language_name.to_string();
|
||||
let platform_agnostic = _platform_agnostic;
|
||||
let path = dep.path.clone();
|
||||
let handle = dep._s3_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = crate::global_cache::build_tar_and_push(
|
||||
os,
|
||||
path,
|
||||
language_name,
|
||||
Some(handle),
|
||||
platform_agnostic,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to build tar and push: {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(kill_all_tasks)
|
||||
}
|
||||
|
||||
// Create a file to indicate that installation was successfull
|
||||
async fn mark_success(path: String, job_id: &Uuid, w_id: &str) {
|
||||
let valid_path = path + ".valid.windmill";
|
||||
// This is atomic operation, meaning, that it either completes and dependency is valid,
|
||||
// or it does not and dependency is invalid and will be reinstalled next run
|
||||
if let Err(e) = File::create(&valid_path).await {
|
||||
tracing::error!(
|
||||
workspace_id = %w_id,
|
||||
job_id = %job_id,
|
||||
"Failed to create {}!\n{e}\n
|
||||
This file needed for jobs to function",
|
||||
valid_path
|
||||
);
|
||||
};
|
||||
}
|
||||
// Append logs with line like this:
|
||||
// [9/21] + requests==2.32.3 << (S3) | in 57ms
|
||||
#[allow(unused_assignments)]
|
||||
async fn print_success(
|
||||
mut s3_pull: bool,
|
||||
mut s3_push: bool,
|
||||
job_id: &Uuid,
|
||||
w_id: &str,
|
||||
req: &str,
|
||||
req_tl: usize,
|
||||
counter_arc: Arc<tokio::sync::Mutex<usize>>,
|
||||
total_to_install: usize,
|
||||
instant: std::time::Instant,
|
||||
conn: &Connection,
|
||||
) {
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
{
|
||||
(s3_pull, s3_push) = (false, false);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if windmill_common::s3_helpers::OBJECT_STORE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
(s3_pull, s3_push) = (false, false);
|
||||
}
|
||||
|
||||
let mut counter = counter_arc.lock().await;
|
||||
*counter += 1;
|
||||
|
||||
windmill_queue::append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!(
|
||||
"\n{}+ {}{}{}| in {}ms",
|
||||
windmill_common::worker::pad_string(&format!("[{}/{total_to_install}]", counter), 9),
|
||||
// Because we want to align to max len [999/999] we take 9
|
||||
// 123456789
|
||||
windmill_common::worker::pad_string(&req, req_tl + 1),
|
||||
// Margin to the right ^
|
||||
if s3_pull { "<< (S3) " } else { "" },
|
||||
if s3_push { " > (S3) " } else { "" },
|
||||
instant.elapsed().as_millis(),
|
||||
),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
// Drop lock, so next print success can fire
|
||||
}
|
||||
|
||||
async fn process_handles(
|
||||
(handles, closer): (Vec<JoinHandle<anyhow::Result<TaskKiller>>>, Sender<()>),
|
||||
w_id: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut killers = vec![];
|
||||
for handle in handles {
|
||||
match handle
|
||||
.await
|
||||
.unwrap_or(Err(anyhow::anyhow!("Problem by joining handle")))
|
||||
{
|
||||
// __________< Cannot drop just yet.
|
||||
Ok(kill_tasks) => killers.push(kill_tasks),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
workspace_id = %w_id,
|
||||
"Env installation failed: {:?}",
|
||||
e
|
||||
);
|
||||
// We can safely return,
|
||||
// it will drop `closer`
|
||||
// which will drop first kill_tx for all installation threads
|
||||
// which will trigger chain reaction and kill every installation
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only now we can be sure there is no installation thread running
|
||||
// we can safely stop poller
|
||||
drop(closer);
|
||||
// Drop ONLY after we processed all handles
|
||||
drop(killers);
|
||||
Ok(())
|
||||
}
|
||||
async fn finish_installation(
|
||||
total_time: std::time::Instant,
|
||||
job_id: &Uuid,
|
||||
w_id: &str,
|
||||
conn: &Connection,
|
||||
) {
|
||||
let total_time = total_time.elapsed().as_millis();
|
||||
windmill_queue::append_logs(
|
||||
&job_id,
|
||||
w_id,
|
||||
format!(
|
||||
"\nDone. Time spent on installation phase: {}ms\n",
|
||||
total_time
|
||||
),
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -2999,6 +2999,7 @@ var $RawScript = {
|
||||
"graphql",
|
||||
"nativets",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
],
|
||||
},
|
||||
|
||||
@@ -134,6 +134,9 @@ use crate::nu_executor::{handle_nu_job, JobHandlerInput as JobHandlerInputNu};
|
||||
#[cfg(feature = "java")]
|
||||
use crate::java_executor::{handle_java_job, JobHandlerInput as JobHandlerInputJava};
|
||||
|
||||
#[cfg(feature = "ruby")]
|
||||
use crate::ruby_executor::{handle_ruby_job, JobHandlerInput as JobHandlerInputRuby};
|
||||
|
||||
#[cfg(feature = "php")]
|
||||
use crate::php_executor::handle_php_job;
|
||||
|
||||
@@ -191,10 +194,14 @@ pub const RUST_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "rust");
|
||||
pub const NU_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "nu");
|
||||
pub const CSHARP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "csharp");
|
||||
|
||||
// JAVA
|
||||
// Java
|
||||
pub const JAVA_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "java");
|
||||
pub const COURSIER_CACHE_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/coursier-cache");
|
||||
pub const JAVA_REPOSITORY_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/repository");
|
||||
|
||||
// Ruby
|
||||
pub const RUBY_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "ruby");
|
||||
|
||||
// for related places search: ADD_NEW_LANG
|
||||
pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "bun");
|
||||
pub const BUN_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
|
||||
@@ -338,6 +345,7 @@ lazy_static::lazy_static! {
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false));
|
||||
pub static ref RUBY_REPOS: Arc<RwLock<Option<Vec<url::Url>>>> = 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));
|
||||
@@ -3439,6 +3447,33 @@ mount {{
|
||||
})
|
||||
.await
|
||||
}
|
||||
Some(ScriptLang::Ruby) => {
|
||||
#[cfg(not(feature = "ruby"))]
|
||||
return Err(anyhow::anyhow!(
|
||||
"Ruby is not available because the feature is not enabled"
|
||||
)
|
||||
.into());
|
||||
|
||||
#[cfg(feature = "ruby")]
|
||||
handle_ruby_job(JobHandlerInputRuby {
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
job,
|
||||
conn,
|
||||
client,
|
||||
parent_runnable_path,
|
||||
inner_content: &code,
|
||||
job_dir,
|
||||
requirements_o: lock.as_ref(),
|
||||
shared_mount: &shared_mount,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
envs,
|
||||
occupancy_metrics,
|
||||
})
|
||||
.await
|
||||
}
|
||||
// for related places search: ADD_NEW_LANG
|
||||
_ => panic!("unreachable, language is not supported: {language:#?}"),
|
||||
};
|
||||
tracing::info!(
|
||||
@@ -3511,6 +3546,10 @@ fn parse_sig_of_lang(
|
||||
ScriptLang::Java => Some(windmill_parser_java::parse_java_signature(code)?),
|
||||
#[cfg(not(feature = "java"))]
|
||||
ScriptLang::Java => None,
|
||||
#[cfg(feature = "ruby")]
|
||||
ScriptLang::Ruby => Some(windmill_parser_ruby::parse_ruby_signature(code)?),
|
||||
#[cfg(not(feature = "ruby"))]
|
||||
ScriptLang::Ruby => None,
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -42,7 +42,10 @@ use crate::common::OccupancyMetrics;
|
||||
use crate::csharp_executor::generate_nuget_lockfile;
|
||||
|
||||
#[cfg(feature = "java")]
|
||||
use crate::java_executor::resolve;
|
||||
use crate::java_executor;
|
||||
|
||||
#[cfg(feature = "ruby")]
|
||||
use crate::ruby_executor;
|
||||
|
||||
#[cfg(feature = "php")]
|
||||
use crate::php_executor::{composer_install, parse_php_imports};
|
||||
@@ -2415,7 +2418,7 @@ async fn capture_dependency_job(
|
||||
));
|
||||
}
|
||||
|
||||
resolve(
|
||||
java_executor::resolve(
|
||||
job_id,
|
||||
job_raw_code,
|
||||
job_dir,
|
||||
@@ -2424,6 +2427,26 @@ async fn capture_dependency_job(
|
||||
)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "ruby")]
|
||||
ScriptLang::Ruby => {
|
||||
if raw_deps {
|
||||
return Err(Error::ExecutionErr(
|
||||
"Raw dependencies not supported for Ruby".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
ruby_executor::resolve(
|
||||
job_id,
|
||||
job_raw_code,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
job_dir,
|
||||
&Connection::Sql(db.clone()),
|
||||
worker_name,
|
||||
w_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
// for related places search: ADD_NEW_LANG
|
||||
_ => Ok("".to_owned()),
|
||||
}
|
||||
|
||||
@@ -126,6 +126,11 @@ public class Main {
|
||||
System.out.println("Hello World");
|
||||
}
|
||||
}
|
||||
`,
|
||||
ruby: `
|
||||
def main a, b, c
|
||||
puts a, b, c
|
||||
end
|
||||
`,
|
||||
// for related places search: ADD_NEW_LANG
|
||||
};
|
||||
|
||||
@@ -71,6 +71,8 @@ await build({
|
||||
"yaml",
|
||||
"csharp",
|
||||
"java",
|
||||
"ruby",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
];
|
||||
|
||||
for (const l of dirs) {
|
||||
|
||||
@@ -578,7 +578,9 @@ export function filePathExtensionFromContentType(
|
||||
return ".nu";
|
||||
} else if (language === "java") {
|
||||
return ".java";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (language === "ruby") {
|
||||
return ".rb";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else {
|
||||
throw new Error("Invalid language: " + language);
|
||||
}
|
||||
@@ -608,7 +610,8 @@ export const exts = [
|
||||
".nu",
|
||||
".playbook.yml",
|
||||
".java",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
".rb"
|
||||
// for related places search: ADD_NEW_LANG
|
||||
];
|
||||
|
||||
export function removeExtensionToPath(path: string): string {
|
||||
|
||||
@@ -694,7 +694,8 @@ export async function elementsToMap(
|
||||
"yml",
|
||||
"nu",
|
||||
"java",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
"rb",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
].includes(path.split(".").pop() ?? "") &&
|
||||
!isFileResource(path)
|
||||
)
|
||||
|
||||
@@ -232,6 +232,7 @@ export function getTypeStrFromPath(
|
||||
parsed.ext == ".cs" ||
|
||||
parsed.ext == ".nu" ||
|
||||
parsed.ext == ".java" ||
|
||||
parsed.ext == ".rb" ||
|
||||
// for related places search: ADD_NEW_LANG
|
||||
(parsed.ext == ".yml" && parsed.name.split(".").pop() == "playbook")
|
||||
) {
|
||||
|
||||
@@ -643,7 +643,10 @@ export async function inferSchema(
|
||||
} else if (language === "java") {
|
||||
const { parse_java } = await import("../../wasm/java/windmill_parser_wasm.js");
|
||||
inferedSchema = JSON.parse(parse_java(content));
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (language === "ruby") {
|
||||
const { parse_ruby } = await import("./wasm/ruby/windmill_parser_wasm.js");
|
||||
inferedSchema = JSON.parse(parse_ruby(content));
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else {
|
||||
throw new Error("Invalid language: " + language);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export type ScriptLanguage =
|
||||
| "csharp"
|
||||
| "nu"
|
||||
| "ansible"
|
||||
| "ruby"
|
||||
| "java";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
|
||||
@@ -87,7 +88,9 @@ export function inferContentTypeFromFilePath(
|
||||
return "nu";
|
||||
} else if (contentPath.endsWith(".java")) {
|
||||
return "java";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (contentPath.endsWith(".rb")) {
|
||||
return "ruby";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid language: " + contentPath.substring(contentPath.lastIndexOf("."))
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function parse_ruby(code: string): string;
|
||||
@@ -0,0 +1,121 @@
|
||||
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||
|
||||
const encodeString = function (arg, view) {
|
||||
return cachedTextEncoder.encodeInto(arg, view);
|
||||
};
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = encodeString(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_ruby(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_ruby(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const imports = {
|
||||
__wbindgen_placeholder__: {
|
||||
__wbindgen_init_externref_table: function() {
|
||||
const table = wasm.__wbindgen_export_0;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
;
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url);
|
||||
let wasmCode = '';
|
||||
switch (wasm_url.protocol) {
|
||||
case 'file:':
|
||||
wasmCode = await Deno.readFile(wasm_url);
|
||||
break
|
||||
case 'https:':
|
||||
case 'http:':
|
||||
wasmCode = await (await fetch(wasm_url)).arrayBuffer();
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported protocol: ${wasm_url.protocol}`);
|
||||
}
|
||||
|
||||
const wasmInstance = (await WebAssembly.instantiate(wasmCode, imports)).instance;
|
||||
const wasm = wasmInstance.exports;
|
||||
export const __wasm = wasm;
|
||||
|
||||
wasm.__wbindgen_start();
|
||||
|
||||
Binary file not shown.
+35
@@ -0,0 +1,35 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const parse_ruby: (a: number, b: number) => [number, number];
|
||||
export const free: (a: number) => void;
|
||||
export const abort: () => void;
|
||||
export const malloc: (a: number) => number;
|
||||
export const calloc: (a: number, b: number) => number;
|
||||
export const realloc: (a: number, b: number) => number;
|
||||
export const strncmp: (a: number, b: number, c: number) => number;
|
||||
export const memchr: (a: number, b: number, c: number) => number;
|
||||
export const strchr: (a: number, b: number) => number;
|
||||
export const iswspace: (a: number) => number;
|
||||
export const iswalnum: (a: number) => number;
|
||||
export const iswdigit: (a: number) => number;
|
||||
export const iswupper: (a: number) => number;
|
||||
export const iswalpha: (a: number) => number;
|
||||
export const iswlower: (a: number) => number;
|
||||
export const clock: () => bigint;
|
||||
export const isprint: (a: number) => number;
|
||||
export const fprintf: (a: number, b: number, c: number) => number;
|
||||
export const fputs: (a: number, b: number) => number;
|
||||
export const fputc: (a: number, b: number) => number;
|
||||
export const fdopen: (a: number, b: number) => number;
|
||||
export const fclose: (a: number) => number;
|
||||
export const fwrite: (a: number, b: number, c: number, d: number) => number;
|
||||
export const vsnprintf: (a: number, b: number, c: number, d: number) => number;
|
||||
export const clock_gettime: (a: number, b: number) => void;
|
||||
export const snprintf: () => void;
|
||||
export const __assert_fail: (a: number, b: number, c: number, d: number) => void;
|
||||
export const __wbindgen_export_0: WebAssembly.Table;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_start: () => void;
|
||||
@@ -22,3 +22,6 @@ RUN curl -fLo coursier https://github.com/coursier/coursier/releases/download/v2
|
||||
&& mv ./coursier /usr/bin/coursier \
|
||||
&& chmod +x /usr/bin/coursier
|
||||
RUN /usr/bin/java -jar /usr/bin/coursier about
|
||||
|
||||
# Ruby
|
||||
RUN apt-get install -y ruby ruby-bundler
|
||||
|
||||
@@ -45,3 +45,6 @@ RUN curl -fLo coursier https://github.com/coursier/coursier/releases/download/v2
|
||||
&& mv ./coursier /usr/bin/coursier \
|
||||
&& chmod +x /usr/bin/coursier
|
||||
RUN /usr/bin/java -jar /usr/bin/coursier about
|
||||
|
||||
# Ruby
|
||||
RUN apt-get install -y ruby ruby-bundler
|
||||
|
||||
@@ -70,6 +70,9 @@ RUN curl -fLo coursier https://github.com/coursier/coursier/releases/download/v2
|
||||
&& chmod +x /usr/bin/coursier
|
||||
RUN /usr/bin/java -jar /usr/bin/coursier about
|
||||
|
||||
# Ruby
|
||||
RUN apt-get install -y ruby ruby-bundler
|
||||
|
||||
# iptables
|
||||
RUN apt-get install -y iptables
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# run: #
|
||||
# docker/dev.nu --help #
|
||||
# ------------------------------------ #
|
||||
# NOTE:
|
||||
# Does not work if built from git worktree branch
|
||||
|
||||
let branch = git branch --show-current;
|
||||
# cp -f Caddyfile .dev-docker/Caddyfile
|
||||
@@ -57,6 +59,7 @@ def patch_custom_dockerfile [
|
||||
] {
|
||||
open $custom_dockerfile
|
||||
| str replace 'ghcr.io/windmill-labs/windmill-ee:dev' (get_ident -f $features)
|
||||
| str replace 'ghcr.io/windmill-labs/windmill:dev' (get_ident -f $features)
|
||||
| return $in
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
dotnet-sdk_9
|
||||
oracle-instantclient
|
||||
ansible
|
||||
ruby_3_4
|
||||
|
||||
# LSP/Local dev
|
||||
svelte-language-server
|
||||
@@ -214,7 +215,10 @@
|
||||
GIT_PATH = "${pkgs.git}/bin/git";
|
||||
NODE_ENV = "development";
|
||||
NODE_OPTIONS = "--max-old-space-size=16384";
|
||||
DATABASE_URL = "postgres://postgres:changeme@127.0.0.1:5432/";
|
||||
# DATABASE_URL = "postgres://postgres:changeme@127.0.0.1:5432/";
|
||||
DATABASE_URL =
|
||||
"postgres://postgres:changeme@127.0.0.1:5432/windmill?sslmode=disable";
|
||||
|
||||
REMOTE = "http://127.0.0.1:8000";
|
||||
REMOTE_LSP = "http://127.0.0.1:3001";
|
||||
RUSTC_WRAPPER = "${pkgs.sccache}/bin/sccache";
|
||||
@@ -228,7 +232,10 @@
|
||||
JAVA_PATH = "${pkgs.jdk21}/bin/java";
|
||||
JAVAC_PATH = "${pkgs.jdk21}/bin/javac";
|
||||
COURSIER_PATH = "${coursier}/coursier";
|
||||
# for related places search: ADD_NEW_LANG
|
||||
RUBY_PATH = "${pkgs.ruby}/bin/ruby";
|
||||
RUBY_BUNDLE_PATH = "${pkgs.ruby}/bin/bundle";
|
||||
RUBY_GEM_PATH = "${pkgs.ruby}/bin/gem";
|
||||
# for related places search: ADD_NEW_LANG
|
||||
FLOCK_PATH = "${pkgs.flock}/bin/flock";
|
||||
CARGO_PATH = "${rust}/bin/cargo";
|
||||
CARGO_SWEEP_PATH = "${pkgs.cargo-sweep}/bin/cargo-sweep";
|
||||
|
||||
Generated
+11
@@ -81,6 +81,7 @@
|
||||
"windmill-parser-wasm-php": "1.510.1",
|
||||
"windmill-parser-wasm-py": "1.510.1",
|
||||
"windmill-parser-wasm-regex": "1.512.0",
|
||||
"windmill-parser-wasm-ruby": "1.526.1",
|
||||
"windmill-parser-wasm-rust": "1.510.1",
|
||||
"windmill-parser-wasm-ts": "1.514.1",
|
||||
"windmill-parser-wasm-yaml": "1.510.1",
|
||||
@@ -156,6 +157,11 @@
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"../backend/parsers/windmill-parser-wasm/pkg-ruby": {
|
||||
"name": "windmill-parser-wasm-ruby",
|
||||
"version": "1.510.0",
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
@@ -13144,6 +13150,11 @@
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.512.0.tgz",
|
||||
"integrity": "sha512-mOkuspfjPPhGZwmerBlFOjRKHjycrlzZUpxO7gHy5D6kwKI2bSz+VI3TsJqmK1y9PwAGgO8JnwRmNAR/YLUkzA=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-ruby": {
|
||||
"version": "1.526.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ruby/-/windmill-parser-wasm-ruby-1.526.1.tgz",
|
||||
"integrity": "sha512-rMBQA8s21wmL2kA5ztRs/ZgVA3ckxe9/NLjxl3iQPL0CX6DlvfaUH0O+AnhpXXDMyBs1Y1SZIhcnbnvsHZ3R8g=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-rust": {
|
||||
"version": "1.510.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-rust/-/windmill-parser-wasm-rust-1.510.1.tgz",
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"windmill-parser-wasm-php": "1.510.1",
|
||||
"windmill-parser-wasm-py": "1.510.1",
|
||||
"windmill-parser-wasm-regex": "1.512.0",
|
||||
"windmill-parser-wasm-ruby": "1.526.1",
|
||||
"windmill-parser-wasm-rust": "1.510.1",
|
||||
"windmill-parser-wasm-ts": "1.514.1",
|
||||
"windmill-parser-wasm-yaml": "1.510.1",
|
||||
@@ -543,4 +544,4 @@
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
|
||||
"fsevents": "^2.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,6 @@
|
||||
let editor: meditor.IStandaloneCodeEditor | null = $state(null)
|
||||
|
||||
interface Props {
|
||||
// for related places search: ADD_NEW_LANG
|
||||
code?: string
|
||||
cmdEnterAction?: (() => void) | undefined
|
||||
formatAction?: (() => void) | undefined
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
'rust',
|
||||
'csharp',
|
||||
'nu',
|
||||
'java'
|
||||
'java',
|
||||
'ruby'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
].includes(lang ?? '')
|
||||
)
|
||||
@@ -151,7 +152,8 @@
|
||||
'rust',
|
||||
'csharp',
|
||||
'nu',
|
||||
'java'
|
||||
'java',
|
||||
'ruby'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
].includes(lang ?? '')
|
||||
)
|
||||
@@ -170,7 +172,8 @@
|
||||
'rust',
|
||||
'csharp',
|
||||
'nu',
|
||||
'java'
|
||||
'java',
|
||||
'ruby',
|
||||
// for related places search: ADD_NEW_LANG
|
||||
].includes(lang ?? '')
|
||||
)
|
||||
@@ -436,6 +439,8 @@
|
||||
} else if (lang == 'java') {
|
||||
editor.insertAtCursor(`System.getenv("${name}");`)
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (lang == 'ruby') {
|
||||
editor.insertAtCursor(`ENV['${name}']`)
|
||||
}
|
||||
sendUserToast(`${name} inserted at cursor`)
|
||||
}}
|
||||
@@ -506,6 +511,11 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
|
||||
} else if (lang == 'java') {
|
||||
editor.insertAtCursor(`(Wmill.getVariable("${path}"))`)
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (lang == 'ruby') {
|
||||
if (!editor.getCode().includes("require 'windmill/mini'")) {
|
||||
editor.insertAtBeginning("require 'windmill/mini'\n")
|
||||
}
|
||||
editor.insertAtCursor(`get_variable("${path}")`)
|
||||
}
|
||||
sendUserToast(`${name} inserted at cursor`)
|
||||
}}
|
||||
@@ -595,6 +605,11 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
} else if (lang == 'java') {
|
||||
editor.insertAtCursor(`(Wmill.getResource("${path}"))`)
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (lang == 'ruby') {
|
||||
if (!editor.getCode().includes("require 'windmill/mini'")) {
|
||||
editor.insertAtBeginning("require 'windmill/mini'\n")
|
||||
}
|
||||
editor.insertAtCursor(`get_resource("${path}")`)
|
||||
} else if (lang == 'duckdb') {
|
||||
let t = { postgresql: 'postgres', mysql: 'mysql', bigquery: 'bigquery' }[resType]
|
||||
if (!t) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import csharp from 'svelte-highlight/languages/csharp'
|
||||
import yaml from 'svelte-highlight/languages/yaml'
|
||||
import java from 'svelte-highlight/languages/java'
|
||||
import ruby from 'svelte-highlight/languages/ruby'
|
||||
import type { Script } from '$lib/gen'
|
||||
import { Button } from './common'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
@@ -70,8 +71,10 @@
|
||||
case 'ansible':
|
||||
return yaml
|
||||
case 'java':
|
||||
return java
|
||||
// for related places search: ADD_NEW_LANG
|
||||
return java;
|
||||
case 'ruby':
|
||||
return ruby;
|
||||
// for related places search: ADD_NEW_LANG
|
||||
default:
|
||||
return typescript
|
||||
}
|
||||
|
||||
@@ -1128,7 +1128,7 @@
|
||||
>
|
||||
<LanguageIcon {lang} />
|
||||
<span class="ml-2 py-2 truncate">{label}</span>
|
||||
{#if lang === 'nu'}
|
||||
{#if lang === 'nu' || lang === 'ruby'}
|
||||
<span class="text-tertiary !text-xs"> BETA </span>
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import NuIcon from '$lib/components/icons/NuIcon.svelte'
|
||||
import JavaIcon from '$lib/components/icons/JavaIcon.svelte'
|
||||
import DuckDbIcon from '$lib/components/icons/DuckDbIcon.svelte'
|
||||
import RubyIcon from '$lib/components/icons/RubyIcon.svelte'
|
||||
|
||||
interface Props {
|
||||
lang:
|
||||
@@ -71,8 +72,9 @@
|
||||
ansible: 'Ansible',
|
||||
csharp: 'C#',
|
||||
nu: 'Nu',
|
||||
java: 'Java'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
java: 'Java',
|
||||
ruby: 'Ruby'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
const langToComponent: Record<
|
||||
@@ -105,6 +107,7 @@
|
||||
csharp: CSharpIcon,
|
||||
nu: NuIcon,
|
||||
java: JavaIcon,
|
||||
ruby: RubyIcon,
|
||||
duckdb: DuckDbIcon
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: number
|
||||
width?: number
|
||||
}
|
||||
|
||||
let { height = 24, width = 24 }: Props = $props()
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 198.13 197.58"
|
||||
overflow="visible"
|
||||
enable-background="new 0 0 198.13 197.58"
|
||||
xml:space="preserve"
|
||||
><metadata id="metadata3320"
|
||||
><rdf:RDF>
|
||||
<!-- svelte-ignore attribute_illegal_colon -->
|
||||
<cc:Work rdf:about=""
|
||||
><dc:format>image/svg+xml</dc:format>
|
||||
<!-- svelte-ignore element_invalid_self_closing_tag -->
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work
|
||||
></rdf:RDF
|
||||
></metadata
|
||||
><defs id="defs3489"
|
||||
><linearGradient
|
||||
id="XMLID_17_-9"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="174.0737"
|
||||
y1="215.5488"
|
||||
x2="132.27631"
|
||||
y2="141.7533"
|
||||
><stop offset="0" style="stop-color:#FB7655" id="stop3272-4" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#FB7655"
|
||||
id="stop3274-5"
|
||||
/><stop offset="0.41" style="stop-color:#E42B1E" id="stop3276-1" /><stop
|
||||
offset="0.99"
|
||||
style="stop-color:#990000"
|
||||
id="stop3278-0"
|
||||
/><stop offset="1" style="stop-color:#990000" id="stop3280-3" /></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_18_-7"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="194.895"
|
||||
y1="153.5576"
|
||||
x2="141.0276"
|
||||
y2="117.4093"
|
||||
><stop offset="0" style="stop-color:#871101" id="stop3285-8" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#871101"
|
||||
id="stop3287-8"
|
||||
/><stop offset="0.99" style="stop-color:#911209" id="stop3289-6" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#911209"
|
||||
id="stop3291-0"
|
||||
/></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_19_-4"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="151.79539"
|
||||
y1="217.7852"
|
||||
x2="97.929703"
|
||||
y2="181.638"
|
||||
><stop offset="0" style="stop-color:#871101" id="stop3296-6" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#871101"
|
||||
id="stop3298-7"
|
||||
/><stop offset="0.99" style="stop-color:#911209" id="stop3300-6" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#911209"
|
||||
id="stop3302-0"
|
||||
/></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_20_-9"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="38.696301"
|
||||
y1="127.3906"
|
||||
x2="47.046902"
|
||||
y2="181.66141"
|
||||
><stop offset="0" style="stop-color:#FFFFFF" id="stop3307-7" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#FFFFFF"
|
||||
id="stop3309-5"
|
||||
/><stop offset="0.23" style="stop-color:#E57252" id="stop3311-9" /><stop
|
||||
offset="0.46"
|
||||
style="stop-color:#DE3B20"
|
||||
id="stop3313-7"
|
||||
/><stop offset="0.99" style="stop-color:#A60003" id="stop3315-8" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#A60003"
|
||||
id="stop3317-5"
|
||||
/></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_21_-3"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="96.132797"
|
||||
y1="76.715302"
|
||||
x2="99.209602"
|
||||
y2="132.1021"
|
||||
><stop offset="0" style="stop-color:#FFFFFF" id="stop3322-3" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#FFFFFF"
|
||||
id="stop3324-8"
|
||||
/><stop offset="0.23" style="stop-color:#E4714E" id="stop3326-3" /><stop
|
||||
offset="0.56"
|
||||
style="stop-color:#BE1A0D"
|
||||
id="stop3328-7"
|
||||
/><stop offset="0.99" style="stop-color:#A80D00" id="stop3330-9" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#A80D00"
|
||||
id="stop3332-3"
|
||||
/></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_22_-7"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="147.103"
|
||||
y1="25.521"
|
||||
x2="156.3141"
|
||||
y2="65.216202"
|
||||
><stop offset="0" style="stop-color:#FFFFFF" id="stop3337-8" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#FFFFFF"
|
||||
id="stop3339-7"
|
||||
/><stop offset="0.18" style="stop-color:#E46342" id="stop3341-4" /><stop
|
||||
offset="0.4"
|
||||
style="stop-color:#C82410"
|
||||
id="stop3343-1"
|
||||
/><stop offset="0.99" style="stop-color:#A80D00" id="stop3345-9" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#A80D00"
|
||||
id="stop3347-0"
|
||||
/></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_23_-9"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="118.9761"
|
||||
y1="11.5415"
|
||||
x2="158.66859"
|
||||
y2="-8.3048"
|
||||
><stop offset="0" style="stop-color:#FFFFFF" id="stop3352-8" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#FFFFFF"
|
||||
id="stop3354-8"
|
||||
/><stop offset="0.54" style="stop-color:#C81F11" id="stop3356-5" /><stop
|
||||
offset="0.99"
|
||||
style="stop-color:#BF0905"
|
||||
id="stop3358-8"
|
||||
/><stop offset="1" style="stop-color:#BF0905" id="stop3360-4" /></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_24_-3"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="3.9033"
|
||||
y1="113.5547"
|
||||
x2="7.1701999"
|
||||
y2="146.2628"
|
||||
><stop offset="0" style="stop-color:#FFFFFF" id="stop3365-7" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#FFFFFF"
|
||||
id="stop3367-1"
|
||||
/><stop offset="0.31" style="stop-color:#DE4024" id="stop3369-3" /><stop
|
||||
offset="0.99"
|
||||
style="stop-color:#BF190B"
|
||||
id="stop3371-8"
|
||||
/><stop offset="1" style="stop-color:#BF190B" id="stop3373-0" /></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_25_-9"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="-18.5557"
|
||||
y1="155.10451"
|
||||
x2="135.0152"
|
||||
y2="-2.8092999"
|
||||
><stop offset="0" style="stop-color:#BD0012" id="stop3380-7" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#BD0012"
|
||||
id="stop3382-9"
|
||||
/><stop offset="0.07" style="stop-color:#FFFFFF" id="stop3384-9" /><stop
|
||||
offset="0.17"
|
||||
style="stop-color:#FFFFFF"
|
||||
id="stop3386-3"
|
||||
/><stop offset="0.27" style="stop-color:#C82F1C" id="stop3388-2" /><stop
|
||||
offset="0.33"
|
||||
style="stop-color:#820C01"
|
||||
id="stop3390-4"
|
||||
/><stop offset="0.46" style="stop-color:#A31601" id="stop3392-3" /><stop
|
||||
offset="0.72"
|
||||
style="stop-color:#B31301"
|
||||
id="stop3394-7"
|
||||
/><stop offset="0.99" style="stop-color:#E82609" id="stop3396-1" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#E82609"
|
||||
id="stop3398-2"
|
||||
/></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_26_-2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="99.074699"
|
||||
y1="171.0332"
|
||||
x2="52.817699"
|
||||
y2="159.61659"
|
||||
><stop offset="0" style="stop-color:#8C0C01" id="stop3403-0" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#8C0C01"
|
||||
id="stop3405-2"
|
||||
/><stop offset="0.54" style="stop-color:#990C00" id="stop3407-1" /><stop
|
||||
offset="0.99"
|
||||
style="stop-color:#A80D0E"
|
||||
id="stop3409-7"
|
||||
/><stop offset="1" style="stop-color:#A80D0E" id="stop3411-5" /></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_27_-1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="178.52589"
|
||||
y1="115.5146"
|
||||
x2="137.43269"
|
||||
y2="78.683998"
|
||||
><stop offset="0" style="stop-color:#7E110B" id="stop3416-7" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#7E110B"
|
||||
id="stop3418-4"
|
||||
/><stop offset="0.99" style="stop-color:#9E0C00" id="stop3420-1" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#9E0C00"
|
||||
id="stop3422-7"
|
||||
/></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_28_-1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="193.6235"
|
||||
y1="47.937"
|
||||
x2="173.15421"
|
||||
y2="26.053801"
|
||||
><stop offset="0" style="stop-color:#79130D" id="stop3427-1" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#79130D"
|
||||
id="stop3429-1"
|
||||
/><stop offset="0.99" style="stop-color:#9E120B" id="stop3431-7" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#9E120B"
|
||||
id="stop3433-0"
|
||||
/></linearGradient
|
||||
><radialGradient
|
||||
id="XMLID_29_-4"
|
||||
cx="143.8315"
|
||||
cy="79.388199"
|
||||
r="50.357601"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
><stop offset="0" style="stop-color:#A80D00" id="stop3440-0" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#A80D00"
|
||||
id="stop3442-8"
|
||||
/><stop offset="0.99" style="stop-color:#7E0E08" id="stop3444-5" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#7E0E08"
|
||||
id="stop3446-1"
|
||||
/></radialGradient
|
||||
><radialGradient
|
||||
id="XMLID_30_-6"
|
||||
cx="74.0923"
|
||||
cy="145.75101"
|
||||
r="66.943703"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
><stop offset="0" style="stop-color:#A30C00" id="stop3451-6" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#A30C00"
|
||||
id="stop3453-2"
|
||||
/><stop offset="0.99" style="stop-color:#800E08" id="stop3455-1" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#800E08"
|
||||
id="stop3457-9"
|
||||
/></radialGradient
|
||||
><linearGradient
|
||||
id="XMLID_31_-6"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="26.669901"
|
||||
y1="197.33591"
|
||||
x2="9.9886999"
|
||||
y2="140.742"
|
||||
><stop offset="0" style="stop-color:#8B2114" id="stop3462-4" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#8B2114"
|
||||
id="stop3464-8"
|
||||
/><stop offset="0.43" style="stop-color:#9E100A" id="stop3466-0" /><stop
|
||||
offset="0.99"
|
||||
style="stop-color:#B3100C"
|
||||
id="stop3468-8"
|
||||
/><stop offset="1" style="stop-color:#B3100C" id="stop3470-1" /></linearGradient
|
||||
><linearGradient
|
||||
id="XMLID_32_-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="154.6411"
|
||||
y1="9.7979002"
|
||||
x2="192.039"
|
||||
y2="26.305901"
|
||||
><stop offset="0" style="stop-color:#B31000" id="stop3475-2" /><stop
|
||||
offset="0"
|
||||
style="stop-color:#B31000"
|
||||
id="stop3477-2"
|
||||
/><stop offset="0.44" style="stop-color:#910F08" id="stop3479-9" /><stop
|
||||
offset="0.99"
|
||||
style="stop-color:#791C12"
|
||||
id="stop3481-7"
|
||||
/><stop offset="1" style="stop-color:#791C12" id="stop3483-5" /></linearGradient
|
||||
><linearGradient
|
||||
y2="141.7533"
|
||||
x2="132.27631"
|
||||
y1="215.5488"
|
||||
x1="174.0737"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient3448"
|
||||
xlink:href="#XMLID_17_-9"
|
||||
/></defs
|
||||
>
|
||||
<!-- svelte-ignore element_invalid_self_closing_tag -->
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
id="namedview3487"
|
||||
showgrid="false"
|
||||
/>
|
||||
<linearGradient
|
||||
id="XMLID_17_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="174.0737"
|
||||
y1="215.5488"
|
||||
x2="132.2763"
|
||||
y2="141.7533"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#FB7655" id="stop3272" />
|
||||
<stop offset="0" style="stop-color:#FB7655" id="stop3274" />
|
||||
<stop offset="0.41" style="stop-color:#E42B1E" id="stop3276" />
|
||||
<stop offset="0.99" style="stop-color:#990000" id="stop3278" />
|
||||
<stop offset="1" style="stop-color:#990000" id="stop3280" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_18_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="194.895"
|
||||
y1="153.5576"
|
||||
x2="141.0276"
|
||||
y2="117.4093"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#871101" id="stop3285" />
|
||||
<stop offset="0" style="stop-color:#871101" id="stop3287" />
|
||||
<stop offset="0.99" style="stop-color:#911209" id="stop3289" />
|
||||
<stop offset="1" style="stop-color:#911209" id="stop3291" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_19_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="151.7954"
|
||||
y1="217.7852"
|
||||
x2="97.9297"
|
||||
y2="181.638"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#871101" id="stop3296" />
|
||||
<stop offset="0" style="stop-color:#871101" id="stop3298" />
|
||||
<stop offset="0.99" style="stop-color:#911209" id="stop3300" />
|
||||
<stop offset="1" style="stop-color:#911209" id="stop3302" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_20_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="38.6963"
|
||||
y1="127.3906"
|
||||
x2="47.0469"
|
||||
y2="181.6614"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3307" />
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3309" />
|
||||
<stop offset="0.23" style="stop-color:#E57252" id="stop3311" />
|
||||
<stop offset="0.46" style="stop-color:#DE3B20" id="stop3313" />
|
||||
<stop offset="0.99" style="stop-color:#A60003" id="stop3315" />
|
||||
<stop offset="1" style="stop-color:#A60003" id="stop3317" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_21_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="96.1328"
|
||||
y1="76.7153"
|
||||
x2="99.2096"
|
||||
y2="132.1021"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3322" />
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3324" />
|
||||
<stop offset="0.23" style="stop-color:#E4714E" id="stop3326" />
|
||||
<stop offset="0.56" style="stop-color:#BE1A0D" id="stop3328" />
|
||||
<stop offset="0.99" style="stop-color:#A80D00" id="stop3330" />
|
||||
<stop offset="1" style="stop-color:#A80D00" id="stop3332" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_22_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="147.103"
|
||||
y1="25.521"
|
||||
x2="156.3141"
|
||||
y2="65.2162"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3337" />
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3339" />
|
||||
<stop offset="0.18" style="stop-color:#E46342" id="stop3341" />
|
||||
<stop offset="0.4" style="stop-color:#C82410" id="stop3343" />
|
||||
<stop offset="0.99" style="stop-color:#A80D00" id="stop3345" />
|
||||
<stop offset="1" style="stop-color:#A80D00" id="stop3347" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_23_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="118.9761"
|
||||
y1="11.5415"
|
||||
x2="158.6686"
|
||||
y2="-8.3048"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3352" />
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3354" />
|
||||
<stop offset="0.54" style="stop-color:#C81F11" id="stop3356" />
|
||||
<stop offset="0.99" style="stop-color:#BF0905" id="stop3358" />
|
||||
<stop offset="1" style="stop-color:#BF0905" id="stop3360" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_24_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="3.9033"
|
||||
y1="113.5547"
|
||||
x2="7.1702"
|
||||
y2="146.2628"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3365" />
|
||||
<stop offset="0" style="stop-color:#FFFFFF" id="stop3367" />
|
||||
<stop offset="0.31" style="stop-color:#DE4024" id="stop3369" />
|
||||
<stop offset="0.99" style="stop-color:#BF190B" id="stop3371" />
|
||||
<stop offset="1" style="stop-color:#BF190B" id="stop3373" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_25_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="-18.5557"
|
||||
y1="155.1045"
|
||||
x2="135.0152"
|
||||
y2="-2.8093"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#BD0012" id="stop3380" />
|
||||
<stop offset="0" style="stop-color:#BD0012" id="stop3382" />
|
||||
<stop offset="0.07" style="stop-color:#FFFFFF" id="stop3384" />
|
||||
<stop offset="0.17" style="stop-color:#FFFFFF" id="stop3386" />
|
||||
<stop offset="0.27" style="stop-color:#C82F1C" id="stop3388" />
|
||||
<stop offset="0.33" style="stop-color:#820C01" id="stop3390" />
|
||||
<stop offset="0.46" style="stop-color:#A31601" id="stop3392" />
|
||||
<stop offset="0.72" style="stop-color:#B31301" id="stop3394" />
|
||||
<stop offset="0.99" style="stop-color:#E82609" id="stop3396" />
|
||||
<stop offset="1" style="stop-color:#E82609" id="stop3398" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_26_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="99.0747"
|
||||
y1="171.0332"
|
||||
x2="52.8177"
|
||||
y2="159.6166"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#8C0C01" id="stop3403" />
|
||||
<stop offset="0" style="stop-color:#8C0C01" id="stop3405" />
|
||||
<stop offset="0.54" style="stop-color:#990C00" id="stop3407" />
|
||||
<stop offset="0.99" style="stop-color:#A80D0E" id="stop3409" />
|
||||
<stop offset="1" style="stop-color:#A80D0E" id="stop3411" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_27_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="178.5259"
|
||||
y1="115.5146"
|
||||
x2="137.4327"
|
||||
y2="78.684"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#7E110B" id="stop3416" />
|
||||
<stop offset="0" style="stop-color:#7E110B" id="stop3418" />
|
||||
<stop offset="0.99" style="stop-color:#9E0C00" id="stop3420" />
|
||||
<stop offset="1" style="stop-color:#9E0C00" id="stop3422" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_28_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="193.6235"
|
||||
y1="47.937"
|
||||
x2="173.1542"
|
||||
y2="26.0538"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#79130D" id="stop3427" />
|
||||
<stop offset="0" style="stop-color:#79130D" id="stop3429" />
|
||||
<stop offset="0.99" style="stop-color:#9E120B" id="stop3431" />
|
||||
<stop offset="1" style="stop-color:#9E120B" id="stop3433" />
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient
|
||||
id="XMLID_29_"
|
||||
cx="143.8315"
|
||||
cy="79.3882"
|
||||
r="50.3576"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#A80D00" id="stop3440" />
|
||||
<stop offset="0" style="stop-color:#A80D00" id="stop3442" />
|
||||
<stop offset="0.99" style="stop-color:#7E0E08" id="stop3444" />
|
||||
<stop offset="1" style="stop-color:#7E0E08" id="stop3446" />
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient
|
||||
id="XMLID_30_"
|
||||
cx="74.0923"
|
||||
cy="145.751"
|
||||
r="66.9437"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#A30C00" id="stop3451" />
|
||||
<stop offset="0" style="stop-color:#A30C00" id="stop3453" />
|
||||
<stop offset="0.99" style="stop-color:#800E08" id="stop3455" />
|
||||
<stop offset="1" style="stop-color:#800E08" id="stop3457" />
|
||||
</radialGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_31_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="26.6699"
|
||||
y1="197.3359"
|
||||
x2="9.9887"
|
||||
y2="140.742"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#8B2114" id="stop3462" />
|
||||
<stop offset="0" style="stop-color:#8B2114" id="stop3464" />
|
||||
<stop offset="0.43" style="stop-color:#9E100A" id="stop3466" />
|
||||
<stop offset="0.99" style="stop-color:#B3100C" id="stop3468" />
|
||||
<stop offset="1" style="stop-color:#B3100C" id="stop3470" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_32_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="154.6411"
|
||||
y1="9.7979"
|
||||
x2="192.039"
|
||||
y2="26.3059"
|
||||
>
|
||||
<stop offset="0" style="stop-color:#B31000" id="stop3475" />
|
||||
<stop offset="0" style="stop-color:#B31000" id="stop3477" />
|
||||
<stop offset="0.44" style="stop-color:#910F08" id="stop3479" />
|
||||
<stop offset="0.99" style="stop-color:#791C12" id="stop3481" />
|
||||
<stop offset="1" style="stop-color:#791C12" id="stop3483" />
|
||||
</linearGradient>
|
||||
|
||||
<polygon
|
||||
style="fill:url(#linearGradient3448);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="153.5,130.41 40.38,197.58 186.849,187.641 198.13,39.95 "
|
||||
id="polygon3282"
|
||||
/><polygon
|
||||
style="fill:url(#XMLID_18_-7);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="187.089,187.54 174.5,100.65 140.209,145.93 "
|
||||
id="polygon3293"
|
||||
/><polygon
|
||||
style="fill:url(#XMLID_19_-4);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="187.259,187.54 95.03,180.3 40.87,197.391 "
|
||||
id="polygon3304"
|
||||
/><polygon
|
||||
style="fill:url(#XMLID_20_-9);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="41,197.41 64.04,121.93 13.34,132.771 "
|
||||
id="polygon3319"
|
||||
/><polygon
|
||||
style="fill:url(#XMLID_21_-3);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="140.2,146.18 119,63.14 58.33,120.01 "
|
||||
id="polygon3334"
|
||||
/><polygon
|
||||
style="fill:url(#XMLID_22_-7);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="193.32,64.31 135.97,17.47 120,69.1 "
|
||||
id="polygon3349"
|
||||
/><polygon
|
||||
style="fill:url(#XMLID_23_-9);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="166.5,0.77 132.77,19.41 111.49,0.52 "
|
||||
id="polygon3362"
|
||||
/><polygon
|
||||
style="fill:url(#XMLID_24_-3);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
points="0,158.09 14.13,132.32 2.7,101.62 "
|
||||
id="polygon3375"
|
||||
/><path
|
||||
style="fill:#ffffff;fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 1.94,100.65 11.5,32.62 49.97,-11.211 57.05,-53.02 L 136.56,17.9 111.209,0 68.109,16.13 C 54.53,28.76 28.18,53.75 27.23,54.22 26.29,54.7 9.83,85.81 1.94,100.65 z"
|
||||
id="path3377"
|
||||
/><path
|
||||
style="fill:url(#XMLID_25_-9);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 42.32,42.05 c 29.43,-29.18 67.37,-46.42 81.93,-31.73 14.551,14.69 -0.88,50.39 -30.31,79.56 -29.43,29.17 -66.9,47.36 -81.45,32.67 -14.56,-14.68 0.4,-51.33 29.83,-80.5 z"
|
||||
id="path3400"
|
||||
/><path
|
||||
style="fill:url(#XMLID_26_-2);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 41,197.38 22.86,-75.72 75.92,24.39 C 112.33,171.79 81.8,193.55 41,197.38 z"
|
||||
id="path3413"
|
||||
/><path
|
||||
style="fill:url(#XMLID_27_-1);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 120.56,68.89 19.49,77.2 C 162.98,121.98 183.56,96.06 193.639,64 l -73.079,4.89 z"
|
||||
id="path3424"
|
||||
/><path
|
||||
style="fill:url(#XMLID_28_-1);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 193.44,64.39 c 7.8,-23.54 9.6,-57.31 -27.181,-63.58 l -30.18,16.67 57.361,46.91 z"
|
||||
id="path3435"
|
||||
/><path
|
||||
style="fill:#9e1209;fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 0,157.75 c 1.08,38.851 29.11,39.43 41.05,39.771 L 13.47,133.11 0,157.75 z"
|
||||
id="path3437"
|
||||
/><path
|
||||
style="fill:url(#XMLID_29_-4);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 120.669,69.01 c 17.62,10.83 53.131,32.58 53.851,32.98 1.119,0.63 15.31,-23.93 18.53,-37.81 l -72.381,4.83 z"
|
||||
id="path3448"
|
||||
/><path
|
||||
style="fill:url(#XMLID_30_-6);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 63.83,121.66 30.56,58.96 c 18.07,-9.8 32.22,-21.74 45.18,-34.53 L 63.83,121.66 z"
|
||||
id="path3459"
|
||||
/><path
|
||||
style="fill:url(#XMLID_31_-6);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 13.35,133.19 -4.33,51.56 c 8.17,11.16 19.41,12.13 31.2,11.26 -8.53,-21.23 -25.57,-63.68 -26.87,-62.82 z"
|
||||
id="path3472"
|
||||
/><path
|
||||
style="fill:url(#XMLID_32_-0);fill-rule:evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 135.9,17.61 60.71,8.52 C 193.37,12.4 183.42,3.54 166.46,0.77 L 135.9,17.61 z"
|
||||
id="path3485"
|
||||
/></svg
|
||||
>
|
||||
@@ -331,6 +331,15 @@ export const settings: Record<string, Setting[]> = {
|
||||
fieldType: 'boolean',
|
||||
storage: 'setting',
|
||||
ee_only: ''
|
||||
},
|
||||
{
|
||||
label: 'Ruby Gems repositories',
|
||||
description: 'Add private Ruby repositories with credentials. Should end with /',
|
||||
key: 'ruby_repos',
|
||||
fieldType: 'text',
|
||||
placeholder: 'https://user:password@gems.foo.com/',
|
||||
storage: 'setting',
|
||||
ee_only: ''
|
||||
}
|
||||
],
|
||||
Alerts: [
|
||||
|
||||
@@ -53,8 +53,10 @@ export const defaultTags = [
|
||||
'ansible',
|
||||
'csharp',
|
||||
'nu',
|
||||
'java'
|
||||
// KJQXZ
|
||||
'java',
|
||||
'ruby'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
|
||||
]
|
||||
export const nativeTags = [
|
||||
'nativets',
|
||||
@@ -65,4 +67,4 @@ export const nativeTags = [
|
||||
'mssql',
|
||||
'bigquery',
|
||||
'oracledb'
|
||||
]
|
||||
]
|
||||
|
||||
@@ -97,6 +97,8 @@ export function extToLang(ext: string) {
|
||||
return 'nu'
|
||||
case 'java':
|
||||
return 'java'
|
||||
case 'rb':
|
||||
return 'ruby'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
default:
|
||||
return 'unknown'
|
||||
|
||||
@@ -41,8 +41,6 @@ export function editorConfig(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const updateOptions = { tabSize: 2, insertSpaces: true }
|
||||
|
||||
export function convertKind(kind: string): any {
|
||||
|
||||
@@ -27,6 +27,7 @@ import initYamlParser, { parse_ansible } from 'windmill-parser-wasm-yaml'
|
||||
import initCSharpParser, { parse_csharp } from 'windmill-parser-wasm-csharp'
|
||||
import initNuParser, { parse_nu } from 'windmill-parser-wasm-nu'
|
||||
import initJavaParser, { parse_java } from 'windmill-parser-wasm-java'
|
||||
import initRubyParser, { parse_ruby } from 'windmill-parser-wasm-ruby'
|
||||
|
||||
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'
|
||||
@@ -38,6 +39,7 @@ import wasmUrlYaml from 'windmill-parser-wasm-yaml/windmill_parser_wasm_bg.wasm?
|
||||
import wasmUrlCSharp from 'windmill-parser-wasm-csharp/windmill_parser_wasm_bg.wasm?url'
|
||||
import wasmUrlNu from 'windmill-parser-wasm-nu/windmill_parser_wasm_bg.wasm?url'
|
||||
import wasmUrlJava from 'windmill-parser-wasm-java/windmill_parser_wasm_bg.wasm?url'
|
||||
import wasmUrlRuby from 'windmill-parser-wasm-ruby/windmill_parser_wasm_bg.wasm?url'
|
||||
import { workspaceStore } from './stores.js'
|
||||
import { argSigToJsonSchemaType } from 'windmill-utils-internal'
|
||||
import { type AssetWithAccessType } from './components/assets/lib.js'
|
||||
@@ -79,6 +81,9 @@ async function initWasmNu() {
|
||||
async function initWasmJava() {
|
||||
await initJavaParser(wasmUrlJava)
|
||||
}
|
||||
async function initWasmRuby() {
|
||||
await initRubyParser(wasmUrlRuby)
|
||||
}
|
||||
|
||||
export async function inferAssets(
|
||||
language: SupportedLanguage | undefined,
|
||||
@@ -241,7 +246,10 @@ export async function inferArgs(
|
||||
} else if (language == 'java') {
|
||||
await initWasmJava()
|
||||
inferedSchema = JSON.parse(parse_java(code))
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (language == 'ruby') {
|
||||
await initWasmRuby()
|
||||
inferedSchema = JSON.parse(parse_ruby(code))
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1107,6 +1107,55 @@ public class Main {
|
||||
}
|
||||
}
|
||||
`
|
||||
const RUBY_INIT_CODE = `require 'windmill/inline'
|
||||
require 'windmill/mini'
|
||||
|
||||
# Dependency management: declare gems in gemfile block for automatic installation
|
||||
# Windmill uses bundler/inline compatible syntax with automatic requiring
|
||||
gemfile do
|
||||
source 'https://rubygems.org'
|
||||
gem 'amazing_print', '~> 1.6'
|
||||
end
|
||||
|
||||
def main(
|
||||
no_default,
|
||||
name = "Nicolas Bourbaki",
|
||||
age = 42,
|
||||
obj = { "even": "hashes" },
|
||||
list = ["or", "arrays!"]
|
||||
)
|
||||
puts "Hello World and a warm welcome especially to #{name}"
|
||||
puts "and its acolytes.. #{age} #{obj} #{list}"
|
||||
|
||||
# Retrieve variables using the Windmill mini client
|
||||
begin
|
||||
secret = get_variable("f/examples/secret")
|
||||
rescue => e
|
||||
secret = "No secret yet at f/examples/secret!"
|
||||
end
|
||||
puts "The variable at 'f/examples/secret': #{secret}"
|
||||
|
||||
# Get typed resources using the mini client
|
||||
# database = get_resource("u/user/my_postgresql")
|
||||
|
||||
# Access environment variables provided by Windmill
|
||||
user = ENV['WM_USERNAME']
|
||||
|
||||
# Pretty print results using amazing_print (automatically required from gemfile)
|
||||
result = {
|
||||
"splitted" => name.split,
|
||||
"user" => user,
|
||||
"age" => age,
|
||||
"obj" => obj,
|
||||
"list" => list
|
||||
}
|
||||
|
||||
ap result
|
||||
|
||||
# Return value is automatically converted to JSON
|
||||
return result
|
||||
end
|
||||
`
|
||||
// for related places search: ADD_NEW_LANG
|
||||
export const INITIAL_CODE = {
|
||||
bun: {
|
||||
@@ -1197,8 +1246,11 @@ export const INITIAL_CODE = {
|
||||
},
|
||||
java: {
|
||||
script: JAVA_INIT_CODE
|
||||
}
|
||||
// for related places search: ADD_NEW_LANG
|
||||
},
|
||||
ruby: {
|
||||
script: RUBY_INIT_CODE
|
||||
},
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
export function isInitialCode(content: string): boolean {
|
||||
@@ -1306,7 +1358,9 @@ export function initialCode(
|
||||
return INITIAL_CODE.nu.script
|
||||
} else if (language == 'java') {
|
||||
return INITIAL_CODE.java.script
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (language == 'ruby') {
|
||||
return INITIAL_CODE.ruby.script
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else if (language == 'bun' || language == 'bunnative') {
|
||||
if (kind == 'trigger') {
|
||||
return INITIAL_CODE.bun.trigger
|
||||
|
||||
@@ -143,7 +143,8 @@ const scriptLanguagesArray: [SupportedLanguage | 'docker' | 'bunnative', string]
|
||||
['docker', 'Docker'],
|
||||
['nu', 'Nu'],
|
||||
['java', 'Java'],
|
||||
['duckdb', 'DuckDB']
|
||||
['duckdb', 'DuckDB'],
|
||||
['ruby', 'Ruby']
|
||||
// for related places search: ADD_NEW_LANG
|
||||
]
|
||||
export function processLangs(selected: string | undefined, langs: string[]): string[] {
|
||||
@@ -153,7 +154,7 @@ export function processLangs(selected: string | undefined, langs: string[]): str
|
||||
let ls = langs.filter((lang) => lang !== 'nativets')
|
||||
|
||||
//those languages are newer and may not be in the saved list
|
||||
let nl = ['bunnative', 'rust', 'ansible', 'csharp', 'nu', 'java', 'duckdb']
|
||||
let nl = ['bunnative', 'rust', 'ansible', 'csharp', 'nu', 'java', 'duckdb', 'ruby']
|
||||
// for related places search: ADD_NEW_LANG
|
||||
nl.forEach((lang) => {
|
||||
if (!ls.includes(lang)) {
|
||||
|
||||
@@ -791,7 +791,7 @@
|
||||
<Badge color="blue">priority: {job.priority}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'oracledb', 'nativets', 'bash', 'powershell', 'php', 'rust', 'other', 'ansible', 'csharp', 'nu', 'java', 'duckdb', 'dependency'].includes(job.tag)}
|
||||
{#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'oracledb', 'nativets', 'bash', 'powershell', 'php', 'rust', 'other', 'ansible', 'csharp', 'nu', 'java', 'duckdb', 'dependency', 'ruby'].includes(job.tag)}
|
||||
<!-- for related places search: ADD_NEW_LANG -->
|
||||
<div>
|
||||
<Badge color="indigo">Tag: {job.tag}</Badge>
|
||||
|
||||
Reference in New Issue
Block a user