Compare commits

...
Author SHA1 Message Date
Ruben Fiszel fb2a7c3ba1 all 2025-08-22 07:44:09 +00:00
Ruben Fiszel 06d078ebfa fix: make relevant sidebar menu items a instead of button 2025-08-21 18:37:32 +00:00
Ruben Fiszel 4373dfbd80 make tag select removable in custom ui 2025-08-21 18:17:52 +00:00
Ruben Fiszel d0c7ac9f95 nit 2 2025-08-21 17:55:29 +00:00
Ruben Fiszel 6e132e8ee9 ctrl drop on apps improvements 2025-08-21 17:51:06 +00:00
Ruben Fiszel 51ea9473ef fix(app): fix ctrl drag for insertion into subgrids 2025-08-21 17:32:51 +00:00
Fred Reimerandpyranota c92bfe6601 feat: bump Go version from 1.22.0 to 1.25.0 #6415
Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
2025-08-21 15:15:09 +00:00
pyranota d152e8e58f add go1_22_compat annotation (#6432)
* bump go to 1.25

Signed-off-by: pyranota <pyra@duck.com>

* Update Dockerfile

* add comms

---------

Signed-off-by: pyranota <pyra@duck.com>
2025-08-21 15:14:29 +00:00
hugocasa 28f1d61164 fix(frontend): graph cache of ai agent step tools (#6431) 2025-08-21 13:59:16 +02:00
Ruben Fiszel 54f36fcce7 remove default db port on docker-compose 2025-08-21 01:35:59 +00:00
Ruben Fiszel 14b0edd8a4 nit fix 2025-08-20 22:55:44 +00:00
hugocasa 958e8af782 feat: ai agent steps (#6393)
* feat: ai agent steps base

* better backend and graph

* feat: anthropic, log viewer

* nit

* fix(frontend): hide tool nodes from timeline

* move ai agent actions from flow status to flow status module

* nits and workspace/hub scripts support

* tmp ref

* fix merge

* feat: display agent tools status in the graph

* fix reactivity

* fix flow status

* nit
2025-08-20 22:40:57 +00:00
Guilhem 4b79e53f0d fix unsafe mutation in input picker getter (#6428)
* fix bad mutation

* remove unnecessary data structure for step args
2025-08-20 22:40:14 +00:00
Ruben Fiszelandrubenfiszel 58bc913dfc chore(main): release 1.530.0 (#6426)
* chore(main): release 1.530.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-20 18:20:55 +00:00
Alexander Petric 511ff5e9f7 fix: aggrid newchange to point to correct idx (#6425)
* fix: aggrid newchange to point to correct idx

* fix: aggrid newchange to point to correct idx
2025-08-20 18:16:54 +00:00
Ruben Fiszel aa5a0300f4 better preprocessing fix 2025-08-20 18:06:57 +00:00
Ruben Fiszel 47e49b243d fix: fix preprocessor preview 2025-08-20 17:54:18 +00:00
Ruben Fiszel 05648c7c81 multiple flow editor fixes 2025-08-20 17:28:41 +00:00
Ruben Fiszel bcd05753f4 flow nits 2025-08-20 17:06:11 +00:00
Ruben Fiszel ac066abb98 fix: improve flow editor log streaming for individual tests 2025-08-20 16:33:08 +00:00
ae49737676 feat(mcp): add script preview testing tool (#6417)
* add endpoint to test script

* add same for flow

* better tool spec + remove flow preview from tools

* fix

* fix wrong required fields

* feat(mcp): add warning for missing required fields in schema properties

- Add stderr warning when x-mcp-required-fields contains fields not found in body schema properties
- Prevents silent misconfigurations in MCP tool generation
- Helps debug schema validation issues

Co-authored-by: centdix <centdix@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
2025-08-20 14:50:46 +02:00
Ruben Fiszelandrubenfiszel 26a47ee699 chore(main): release 1.529.0 (#6423)
* chore(main): release 1.529.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-20 09:32:32 +00:00
Ruben Fiszel 1734862718 fix In 2025-08-20 09:28:00 +00:00
Ruben Fiszel c1770cd769 improve logic 2025-08-20 09:06:25 +00:00
Ruben Fiszel 18cc474cc7 fix: fix Out output of flow node in frontend be reactive to relevant flowStateStore change 2025-08-20 09:03:05 +00:00
centdix b698ced881 nits(aichat): better ui for tool execution (#6418)
* better conf messages

* nits on tool exec
2025-08-20 10:51:06 +02:00
49ed757424 feat: add prometheus metric queue_running_count (#6413)
* feat: add prometheus metric queue_running_count

Adds a new Prometheus metric queue_running_count that tracks the number
of currently running jobs per tag, similar to the existing queue_count
metric but filtered for running=true instead of running=false.

Changes:
- Added get_queue_running_counts() function in windmill-common/src/queue.rs
- Added QUEUE_RUNNING_COUNT Prometheus metric in monitor.rs
- Added /workers/queue_running_counts API endpoint
- Updated OpenAPI specification
- Added SQLx query cache entry

Requested by @rubenfiszel

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* Remove database insertion for queue_running_count metrics

Keep Prometheus metrics and API endpoint functionality while removing
the database INSERT statements as requested.

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* sqlx

* improve logic

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-08-19 22:12:36 +00:00
pyranota 4349a2024d feat(frontend): add relative line numbers toggle (#6416)
* feat(frontend): add relative line numbers toggle

Signed-off-by: pyranota <pyra@duck.com>

* nits

Signed-off-by: pyranota <pyra@duck.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
2025-08-19 22:09:01 +00:00
Diego Imbert 51568eee02 fix: improve computeAssetNodes rendering caching and performance (#6414)
* better caching for computeAssetNodes

* only pass required fields

* unecessary returns

* type not necessary
2025-08-19 21:12:34 +00:00
claude[bot]andwindmill-internal-app[bot] 7f11eb98b5 fix(cli): pass HEADERS environment variable to fetch calls in generate-locks (#6422)
- Import getHeaders function in metadata.ts
- Update updateScriptLock() to include extra headers from HEADERS env var
- Update updateFlow() to include extra headers in both fetch branches
- Fixes issue where wmill flow generate-locks ignored custom headers

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-08-19 18:05:33 +00:00
centdix dfb32d2949 feat(aichat): add tool to test specific module in flow mode (#6381)
* draft

* cleaning

* cleaning

* fixes

* fix placeholders

* better fallback

* cleaning
2025-08-19 15:18:30 +02:00
Ruben Fiszel 205618af0a nit module tracker 2025-08-19 07:56:17 +00:00
Ruben Fiszelandrubenfiszel 31892ca11e chore(main): release 1.528.0 (#6407)
* chore(main): release 1.528.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-19 03:45:30 +01:00
eaf4054bd3 feat: native k8s autoscaling integration (EE) (#6405)
* stage progress

* stage progress (dirty)

* feat: native k8s autoscaling integration (EE)

Signed-off-by: pyranota <pyra@duck.com>

* fix frontend

Signed-off-by: pyranota <pyra@duck.com>

* update ee ref

Signed-off-by: pyranota <pyra@duck.com>

* update ee ref

Signed-off-by: pyranota <pyra@duck.com>

* Update backend/windmill-api/src/configs.rs

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update ee-repo-ref.txt

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-08-19 02:20:14 +00:00
Ruben Fiszel 5e73c49ab6 fix: flow status reactivity improvement (#6402) 2025-08-19 02:12:57 +00:00
pyranota 896238a1a7 remove ruby from default features (#6406)
* fix: remove `ruby` from default features

* fix build error
2025-08-19 02:05:46 +00:00
David P. Kleinschmidt ccf755b351 Add windir to Windows environment (#6403) 2025-08-18 13:41:15 +01:00
Ruben Fiszelandrubenfiszel 68bf2955f7 chore(main): release 1.527.1 (#6400)
* chore(main): release 1.527.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-16 10:40:55 +00:00
Ruben Fiszel 03c82af00a fix: fix inlinecompletion errors 2025-08-16 10:20:21 +00:00
pyranota f4851e7747 fix(cli): module not found ../ruby/../wasm.js (#6399)
Signed-off-by: pyranota <pyra@duck.com>
2025-08-15 17:05:26 +00:00
Ruben Fiszelandrubenfiszel 396b6e5c7f chore(main): release 1.527.0 (#6395)
* chore(main): release 1.527.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-15 13:27:11 +00:00
pyranotaandellipsis-dev[bot] 11dd4118ce 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>
2025-08-15 13:03:32 +00:00
Alexander Petric 9b07d6d546 improve git scripts for ecs container permissions (#6398) 2025-08-15 12:38:07 +00:00
Ruben Fiszel eae0c09979 fix: improve flow layout for more complex flow 2025-08-14 21:43:39 +00:00
Ruben Fiszelandrubenfiszel af226162f5 chore(main): release 1.526.1 (#6391)
* chore(main): release 1.526.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-08-14 18:18:46 +00:00
dieribaandGitHub Action c2de0a8e61 fix token query (#6392)
* fix

* Update SQLx metadata

---------

Co-authored-by: GitHub Action <action@github.com>
2025-08-14 18:18:23 +00:00
Ruben Fiszel afc4044bbd fix websocket trigger info query 2025-08-14 17:38:29 +00:00
Ruben Fiszel 65bcc00cd9 fix: add timeouts to more queries to prevent some rare deadlocks scnarios 2025-08-14 17:31:38 +00:00
265 changed files with 11886 additions and 2819 deletions
+68
View File
@@ -1,5 +1,73 @@
# Changelog
## [1.530.0](https://github.com/windmill-labs/windmill/compare/v1.529.0...v1.530.0) (2025-08-20)
### Features
* **mcp:** add script preview testing tool ([#6417](https://github.com/windmill-labs/windmill/issues/6417)) ([ae49737](https://github.com/windmill-labs/windmill/commit/ae497376769f5cd49a41c22cf558a9f052d5b56e))
### Bug Fixes
* aggrid newchange to point to correct idx ([#6425](https://github.com/windmill-labs/windmill/issues/6425)) ([511ff5e](https://github.com/windmill-labs/windmill/commit/511ff5e9f794c29c3c8a5fc0480675a1258fb056))
* fix preprocessor preview ([47e49b2](https://github.com/windmill-labs/windmill/commit/47e49b243d8cf6d29fa5a59918a2168b87111352))
* improve flow editor log streaming for individual tests ([ac066ab](https://github.com/windmill-labs/windmill/commit/ac066abb980501577cc96330a4a5f1309aa35661))
## [1.529.0](https://github.com/windmill-labs/windmill/compare/v1.528.0...v1.529.0) (2025-08-19)
### Features
* add prometheus metric queue_running_count ([#6413](https://github.com/windmill-labs/windmill/issues/6413)) ([49ed757](https://github.com/windmill-labs/windmill/commit/49ed7574245784681f800199b6c7a47df5788e45))
* **aichat:** add tool to test specific module in flow mode ([#6381](https://github.com/windmill-labs/windmill/issues/6381)) ([dfb32d2](https://github.com/windmill-labs/windmill/commit/dfb32d2949541ed149722cff88918d7c6e3dc307))
* **frontend:** add relative line numbers toggle ([#6416](https://github.com/windmill-labs/windmill/issues/6416)) ([4349a20](https://github.com/windmill-labs/windmill/commit/4349a2024da2aa7406b16254fbfc427526718903))
### Bug Fixes
* **cli:** pass HEADERS environment variable to fetch calls in generate-locks ([#6422](https://github.com/windmill-labs/windmill/issues/6422)) ([7f11eb9](https://github.com/windmill-labs/windmill/commit/7f11eb98b5682511e05c270d7ba860e3e0db61e9))
* improve computeAssetNodes rendering caching and performance ([#6414](https://github.com/windmill-labs/windmill/issues/6414)) ([51568ee](https://github.com/windmill-labs/windmill/commit/51568eee025eab6e0069a8e719a562b721fc8c43))
## [1.528.0](https://github.com/windmill-labs/windmill/compare/v1.527.1...v1.528.0) (2025-08-19)
### Features
* native k8s autoscaling integration (EE) ([#6405](https://github.com/windmill-labs/windmill/issues/6405)) ([eaf4054](https://github.com/windmill-labs/windmill/commit/eaf4054bd380101856c02a0d07430ff3a0180880))
### Bug Fixes
* flow status reactivity improvement ([#6402](https://github.com/windmill-labs/windmill/issues/6402)) ([5e73c49](https://github.com/windmill-labs/windmill/commit/5e73c49ab670be0f55794f5d0cb182de9efd500a))
## [1.527.1](https://github.com/windmill-labs/windmill/compare/v1.527.0...v1.527.1) (2025-08-16)
### Bug Fixes
* **cli:** module not found ../ruby/../wasm.js ([#6399](https://github.com/windmill-labs/windmill/issues/6399)) ([f4851e7](https://github.com/windmill-labs/windmill/commit/f4851e7747c057ea093d55b122a976a1eeec8d98))
* fix inlinecompletion errors ([03c82af](https://github.com/windmill-labs/windmill/commit/03c82af00a113378bb7e00d8018c054856eb2314))
## [1.527.0](https://github.com/windmill-labs/windmill/compare/v1.526.1...v1.527.0) (2025-08-15)
### Features
* add ruby support ([#5939](https://github.com/windmill-labs/windmill/issues/5939)) ([11dd411](https://github.com/windmill-labs/windmill/commit/11dd4118ce43136da900601401ee0d04e60c246b))
### Bug Fixes
* improve flow layout for more complex flow ([eae0c09](https://github.com/windmill-labs/windmill/commit/eae0c099790aedb3fd5ac69b551cafc5efc1bd62))
## [1.526.1](https://github.com/windmill-labs/windmill/compare/v1.526.0...v1.526.1) (2025-08-14)
### Bug Fixes
* add timeouts to more queries to prevent some rare deadlocks scnarios ([65bcc00](https://github.com/windmill-labs/windmill/commit/65bcc00cd9b289193e27a6f74b053e71f90be698))
## [1.526.0](https://github.com/windmill-labs/windmill/compare/v1.525.0...v1.526.0) (2025-08-14)
+1 -1
View File
@@ -90,7 +90,7 @@ ARG POWERSHELL_VERSION=7.5.0
ARG POWERSHELL_DEB_VERSION=7.5.0-1
ARG KUBECTL_VERSION=1.28.7
ARG HELM_VERSION=3.14.3
ARG GO_VERSION=1.22.5
ARG GO_VERSION=1.25.0
ARG APP=/usr/src/app
ARG WITH_POWERSHELL=true
ARG WITH_KUBECTL=true
@@ -62,7 +62,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
"describe": {
"columns": [],
"parameters": {
@@ -39,7 +39,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -71,7 +72,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -120,10 +122,11 @@
]
}
}
}
},
"Bool"
]
},
"nullable": []
},
"hash": "acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597"
"hash": "193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b"
}
@@ -43,7 +43,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag AS \"tag!\", count(*) AS \"count!\" FROM v2_job_queue WHERE\n running = true\n GROUP BY tag",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag!",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
null
]
},
"hash": "1b56a720d99a689e80d12ee1efbfeb71d7cd7bb17e936746749c958062cdff9e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:scripts:' || $2]::text[]\n OR scopes @> ARRAY['run:scripts/' || $2]::text[]\n )\n ",
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:scripts:' || $2]::text[]\n OR scopes @> ARRAY['run:script/' || $2]::text[]\n )\n ",
"describe": {
"columns": [
{
@@ -55,5 +55,5 @@
true
]
},
"hash": "e8b74683f994d198a051c2f211a2844241bf2a7d7702c569017ae53fca706c59"
"hash": "29673d489fbf45fc249da04c1a2fd60e2364ba87263f962ed7d4329c916620a1"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $2::TEXT, 'agent_actions_success'],\n COALESCE(\n flow_status->'modules'->$2->'agent_actions_success',\n to_jsonb(ARRAY[]::bool[])\n ) || to_jsonb(ARRAY[$3::bool])\n )\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "3e3afba04a10f16606e17cea6b31d9578cac41b76092722fd2698afb4cf08834"
}
@@ -28,7 +28,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -32,7 +32,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -40,7 +40,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -71,7 +72,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -68,7 +68,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -138,7 +139,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -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"
}
@@ -62,7 +62,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:flows:' || $2]::text[]\n OR scopes @> ARRAY['run:flows/' || $2]::text[]\n )\n ",
"query": "\n SELECT label,\n concat(substring(token for 10)) AS token_prefix,\n expiration,\n created_at,\n last_used_at,\n scopes,\n email\n FROM token\n WHERE workspace_id = $1\n AND (\n scopes @> ARRAY['jobs:run:flows:' || $2]::text[]\n OR scopes @> ARRAY['run:flow/' || $2]::text[]\n )\n ",
"describe": {
"columns": [
{
@@ -55,5 +55,5 @@
true
]
},
"hash": "334c9a6d770471ac53099eb5227778899a5c680b899bc6a6d1a3f89fdbf8c730"
"hash": "6a254de9005594dc75a59a545546417c8a5aa7635be1dc0b37dc29d0f9e7c163"
}
@@ -98,7 +98,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -148,7 +149,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -38,7 +38,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -35,7 +35,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -0,0 +1,61 @@
{
"db_name": "PostgreSQL",
"query": "SELECT runnable_id as \"runnable_id: ScriptHash\", raw_flow as \"raw_flow: _\", kind as \"kind: _\" FROM v2_job WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "runnable_id: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "raw_flow: _",
"type_info": "Jsonb"
},
{
"ordinal": 2,
"name": "kind: _",
"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",
"aiagent"
]
}
}
}
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true,
true,
false
]
},
"hash": "805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5"
}
@@ -28,7 +28,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $3::TEXT, 'agent_actions'],\n $2\n )\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Jsonb",
"Text"
]
},
"nullable": []
},
"hash": "9a923c85a015e4149328f650e77872a1c39d5992efd6abd2d3b8a558d7b884a1"
}
@@ -98,7 +98,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -148,7 +149,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -37,7 +37,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -42,7 +42,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -35,7 +35,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -66,7 +67,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
@@ -33,7 +33,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -41,11 +41,11 @@
]
},
"nullable": [
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
true
]
},
@@ -35,7 +35,8 @@
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
"appscript",
"aiagent"
]
}
}
@@ -66,7 +67,8 @@
"oracledb",
"nu",
"java",
"duckdb"
"duckdb",
"ruby"
]
}
}
+718 -313
View File
File diff suppressed because it is too large Load Diff
+11 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.526.0"
version = "1.530.0"
authors.workspace = true
edition.workspace = true
@@ -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",
@@ -32,7 +33,7 @@ members = [
]
[workspace.package]
version = "1.526.0"
version = "1.530.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -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"]
@@ -144,7 +146,8 @@ pep440_rs.workspace = true
systemstat.workspace = true
size.workspace = true
strum.workspace = true
kube.workspace = true
k8s-openapi.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = { optional = true, workspace = true }
@@ -180,6 +183,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" }
@@ -393,6 +397,8 @@ systemstat = "0.2.4"
size = "0.5.0"
flume = { version = "0.11.1", features = ["async"] }
kube = { version = "1.1.0", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.25.0", features = ["latest"] }
# Macro-related
proc-macro2 = "1.0"
@@ -406,8 +412,8 @@ 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"] }
strum_macros = "^0"
+1 -1
View File
@@ -1 +1 @@
d04959121c6869d764e11e005676003fd2919629
15a7592ca66b93b9760d49e58b23c090ead06fe2
@@ -41,7 +41,7 @@ def load_openapi_spec(file_path: str) -> Dict[str, Any]:
print(f"Error loading OpenAPI spec: {e}", file=sys.stderr)
sys.exit(1)
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any]) -> tuple:
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None) -> tuple:
"""Extract separate schemas for path parameters, query parameters, and request body."""
path_params_schema = {
"type": "object",
@@ -92,6 +92,20 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
# Process request body if present
if request_body:
body_schema = extract_request_body_schema(request_body, spec)
# If we have required fields specified and a body schema, update the required array
if body_schema and required_fields:
if 'required' not in body_schema:
body_schema['required'] = []
# Add each required field if it exists in the schema properties
for field in required_fields:
if 'properties' in body_schema and field in body_schema['properties']:
if field not in body_schema['required']:
body_schema['required'].append(field)
else:
# Log warning when a required field is missing from schema properties
print(f"Warning: Required field '{field}' not found in body schema properties", file=sys.stderr)
# Return None for empty schemas
path_params_schema = path_params_schema if path_params_schema['properties'] else None
@@ -199,6 +213,7 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
'method': method.upper(),
'parameters': operation.get('parameters', []),
'requestBody': operation.get('requestBody'),
'required_fields': operation.get('x-mcp-required-fields', []),
}
tools.append(tool)
@@ -227,7 +242,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
# Generate separate schemas
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec
tool['parameters'], tool['requestBody'], spec, tool['required_fields']
)
path_params_rust = schema_to_rust_value(path_params_schema)
@@ -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;
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TYPE JOB_KIND ADD VALUE IF NOT EXISTS 'aiagent';
@@ -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);
+25 -5
View File
@@ -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
View File
@@ -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()
+154 -10
View File
@@ -18,6 +18,7 @@ use sqlx::{Pool, Postgres};
use tokio::{
join,
sync::{mpsc, RwLock},
time::timeout,
};
use uuid::Uuid;
@@ -113,6 +114,12 @@ lazy_static::lazy_static! {
&["tag"]
).unwrap();
static ref QUEUE_RUNNING_COUNT: prometheus::IntGaugeVec = prometheus::register_int_gauge_vec!(
"queue_running_count",
"Number of running jobs in the queue",
&["tag"]
).unwrap();
}
lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
@@ -139,6 +146,7 @@ lazy_static::lazy_static! {
pub static ref WORKERS_NAMES: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
static ref QUEUE_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
static ref QUEUE_RUNNING_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
static ref DISABLE_CONCURRENCY_LIMIT: bool = std::env::var("DISABLE_CONCURRENCY_LIMIT").is_ok_and(|s| s == "true");
static ref STALE_JOB_TRESHOLD_MINUTES: Option<u64> = std::env::var("STALE_JOB_TRESHOLD_MINUTES")
@@ -290,6 +298,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;
}
}
@@ -684,20 +693,25 @@ async fn send_log_file_to_object_store(
let (ok_lines, err_lines) = read_log_counters(ts_str);
if let Some(db) = conn.as_sql() {
if let Err(e) = sqlx::query!("INSERT INTO log_file (hostname, mode, worker_group, log_ts, file_path, ok_lines, err_lines, json_fmt)
match timeout(Duration::from_secs(10), sqlx::query!("INSERT INTO log_file (hostname, mode, worker_group, log_ts, file_path, ok_lines, err_lines, json_fmt)
VALUES ($1, $2::text::LOG_MODE, $3, $4, $5, $6, $7, $8)
ON CONFLICT (hostname, log_ts) DO UPDATE SET ok_lines = log_file.ok_lines + $6, err_lines = log_file.err_lines + $7",
hostname, mode.to_string(), worker_group.clone(), ts, highest_file, ok_lines as i64, err_lines as i64, *JSON_FMT)
.execute(db)
.await {
tracing::error!("Error inserting log file: {:?}", e);
} else {
if let Err(e) = LAST_LOG_FILE_SENT.lock().map(|mut last_log_file_sent| {
last_log_file_sent.replace(ts);
}) {
tracing::error!("Error updating last log file sent: {:?}", e);
.execute(db)).await {
Ok(Ok(_)) => {
if let Err(e) = LAST_LOG_FILE_SENT.lock().map(|mut last_log_file_sent| {
last_log_file_sent.replace(ts);
}) {
tracing::error!("Error updating last log file sent: {:?}", e);
}
tracing::info!("Log file sent: {}", highest_file);
}
Ok(Err(e)) => {
tracing::error!("Error inserting log file: {:?}", e);
}
Err(e) => {
tracing::error!("Error inserting log file, timeout elapsed: {:?}", e);
}
tracing::info!("Log file sent: {}", highest_file);
}
} else {
// tracing::warn!("Not sending log file to object store in agent mode");
@@ -1087,6 +1101,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,
@@ -1096,6 +1111,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,
@@ -1112,6 +1128,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,
@@ -1281,6 +1307,100 @@ 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,
@@ -1569,6 +1689,30 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
let mut w = QUEUE_COUNT_TAGS.write().await;
*w = tags_to_watch;
}
#[cfg(feature = "prometheus")]
if metrics_enabled {
// Handle queue running count metrics
let queue_running_counts = windmill_common::queue::get_queue_running_counts(db).await;
for q in QUEUE_RUNNING_COUNT_TAGS.read().await.iter() {
if queue_running_counts.get(q).is_none() {
(*QUEUE_RUNNING_COUNT).with_label_values(&[q]).set(0);
}
}
let mut running_tags_to_watch = vec![];
for q in queue_running_counts {
let count = q.1;
let tag = q.0;
let metric = (*QUEUE_RUNNING_COUNT).with_label_values(&[&tag]);
metric.set(count as i64);
running_tags_to_watch.push(tag.to_string());
}
let mut w = QUEUE_RUNNING_COUNT_TAGS.write().await;
*w = running_tags_to_watch;
}
}
// clean queue metrics older than 14 days
+7
View File
@@ -959,6 +959,7 @@ impl RunJob {
None,
None,
None,
false,
)
.await
.expect("push has to succeed");
@@ -3468,6 +3469,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 +3540,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
tag: None,
paused_until: None,
cron_version: None,
description: None,
},
)
.await
@@ -3621,6 +3624,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 +3696,7 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
tag: None,
paused_until: None,
cron_version: None,
description: None,
},
)
.await
@@ -3777,6 +3782,7 @@ async fn run_deployed_relative_imports(
codebase: None,
has_preprocessor: None,
on_behalf_of_email: None,
assets: vec![],
},
)
.await
@@ -4028,6 +4034,7 @@ async fn assert_lockfile(
codebase: None,
has_preprocessor: None,
on_behalf_of_email: None,
assets: vec![],
},
)
.await
+7
View File
@@ -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
+1
View File
@@ -50,6 +50,7 @@ windmill-parser-py.workspace = true
windmill-parser-py-imports.workspace = true
windmill-git-sync.workspace = true
windmill-indexer = { workspace = true, optional = true }
windmill-autoscaling.workspace = true
windmill-worker.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
+96 -2
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.526.0
version: 1.530.0
title: Windmill API
contact:
@@ -7193,6 +7193,36 @@ paths:
type: string
format: uuid
/w/{workspace}/jobs/run_wait_result/preview:
post:
summary: run script preview and wait for result
operationId: runScriptPreviewAndWaitResult
x-mcp-tool: true
x-mcp-instructions: Allows testing a script before deploying it. For typescript code, the language to send is either bun or deno. By default, send bun if no deno specific code is detected.
x-mcp-required-fields:
- content
- language
- args
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: preview
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Preview"
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/workflow_as_code/{job_id}/{entrypoint}:
post:
summary: run code-workflow task
@@ -7302,6 +7332,30 @@ paths:
type: string
format: uuid
/w/{workspace}/jobs/run_wait_result/preview_flow:
post:
summary: run flow preview and wait for result
operationId: runFlowPreviewAndWaitResult
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: preview
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FlowPreview"
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/queue/list:
get:
summary: list all queued jobs
@@ -11502,6 +11556,22 @@ paths:
additionalProperties:
type: integer
/workers/queue_running_counts:
get:
summary: get counts of currently running jobs per tag
operationId: getCountsOfRunningJobsPerTag
tags:
- worker
responses:
"200":
description: queue running counts
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
/configs/list_worker_groups:
get:
summary: list worker groups
@@ -11615,6 +11685,22 @@ paths:
items:
$ref: "#/components/schemas/AutoscalingEvent"
/configs/native_kubernetes_autoscaling_healthcheck:
get:
summary: Check Kubernetes autoscaling health for a worker group
operationId: nativeKubernetesAutoscalingHealthcheck
tags:
- config
responses:
"200":
description: Kubernetes autoscaling is healthy
"400":
description: Error
content:
text/plain:
schema:
type: string
/configs/list_available_python_versions:
get:
summary: Get currently available python versions provided by UV.
@@ -13929,6 +14015,8 @@ components:
$ref: "../../openflow.openapi.yaml#/components/schemas/BranchOne"
BranchAll:
$ref: "../../openflow.openapi.yaml#/components/schemas/BranchAll"
AiAgent:
$ref: "../../openflow.openapi.yaml#/components/schemas/AiAgent"
Identity:
$ref: "../../openflow.openapi.yaml#/components/schemas/Identity"
FlowStatus:
@@ -14411,6 +14499,7 @@ components:
"flowscript",
"flownode",
"appscript",
"aiagent",
]
schedule_path:
type: string
@@ -14520,6 +14609,7 @@ components:
"flowscript",
"flownode",
"appscript",
"aiagent",
]
schedule_path:
type: string
@@ -15128,7 +15218,8 @@ components:
csharp,
nu,
java,
duckdb,
ruby,
duckdb
# for related places search: ADD_NEW_LANG
]
@@ -15137,10 +15228,13 @@ components:
properties:
content:
type: string
description: The code to run
path:
type: string
description: The path to the script
script_hash:
type: string
description: The hash of the script
args:
$ref: "#/components/schemas/ScriptArgs"
language:
+3
View File
@@ -1092,6 +1092,7 @@ async fn create_app_internal<'a>(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1469,6 +1470,7 @@ async fn update_app_internal<'a>(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1782,6 +1784,7 @@ async fn execute_component(
None,
None,
None,
false,
)
.await?;
tx.commit().await?;
+24
View File
@@ -34,6 +34,10 @@ pub fn global_service() -> Router {
"/list_autoscaling_events/:worker_group",
get(list_autoscaling_events),
)
.route(
"/native_kubernetes_autoscaling_healthcheck",
get(native_kubernetes_autoscaling_healthcheck),
)
.route(
"/list_available_python_versions",
get(list_available_python_versions),
@@ -246,6 +250,26 @@ async fn list_autoscaling_events(
Ok(Json(events))
}
#[cfg(all(feature = "enterprise", feature = "private"))]
async fn native_kubernetes_autoscaling_healthcheck(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> Result<(), windmill_autoscaling::kubernetes_integration_ee::KubeError> {
require_devops_role(&db, &authed.email).await.map_err(|e| {
windmill_autoscaling::kubernetes_integration_ee::KubeError::Other(e.to_string())
})?;
windmill_autoscaling::kubernetes_integration_ee::kubernetes_healthcheck().await
}
#[cfg(not(all(feature = "enterprise", feature = "private")))]
async fn native_kubernetes_autoscaling_healthcheck(
) -> Result<(), error::Error> {
Err(error::Error::BadRequest(
"Native Kubernetes autoscaling available only in the enterprise version".to_string(),
))
}
async fn list_available_python_versions() -> error::JsonResult<Vec<String>> {
#[cfg(not(feature = "python"))]
return Err(error::Error::BadRequest(
+2
View File
@@ -519,6 +519,7 @@ async fn create_flow(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
@@ -985,6 +986,7 @@ async fn update_flow(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
sqlx::query!(
+52
View File
@@ -177,12 +177,14 @@ pub fn workspaced_service() -> Router {
.layer(ce_headers.clone()),
)
.route("/run/preview", post(run_preview_script))
.route("/run_wait_result/preview", post(run_wait_result_preview_script))
.route(
"/run/preview_bundle",
post(run_bundle_preview_script).layer(axum::extract::DefaultBodyLimit::disable()),
)
.route("/add_batch_jobs/:n", post(add_batch_jobs))
.route("/run/preview_flow", post(run_preview_flow_job))
.route("/run_wait_result/preview_flow", post(run_wait_result_preview_flow))
.route("/list", get(list_jobs))
.route(
"/list_selected_job_groups",
@@ -3883,6 +3885,7 @@ pub async fn run_flow_by_path_inner(
None,
None,
push_authed.as_ref(),
false
)
.await?;
tx.commit().await?;
@@ -3977,6 +3980,7 @@ pub async fn restart_flow(
None,
completed_job.priority,
Some(&authed.clone().into()),
false
)
.await?;
tx.commit().await?;
@@ -4072,6 +4076,7 @@ pub async fn run_script_by_path_inner(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
tx.commit().await?;
@@ -4220,6 +4225,7 @@ pub async fn run_workflow_as_code(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
@@ -4750,6 +4756,7 @@ pub async fn run_wait_result_job_by_path_get(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
tx.commit().await?;
@@ -4900,6 +4907,7 @@ pub async fn run_wait_result_script_by_path_internal(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
tx.commit().await?;
@@ -5014,6 +5022,7 @@ pub async fn run_wait_result_script_by_hash(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
tx.commit().await?;
@@ -5131,6 +5140,7 @@ pub async fn run_wait_result_flow_by_path_internal(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
tx.commit().await?;
@@ -5200,6 +5210,7 @@ async fn run_preview_script(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
tx.commit().await?;
@@ -5207,6 +5218,28 @@ async fn run_preview_script(
Ok((StatusCode::CREATED, uuid.to_string()))
}
async fn run_wait_result_preview_script(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
Json(preview): Json<Preview>,
) -> error::Result<Response> {
let (_status_code, uuid) = run_preview_script(
authed.clone(),
Extension(db.clone()),
Extension(user_db.clone()),
Path(w_id.clone()),
Query(run_query.clone()),
Json(preview)
).await?;
let uuid = uuid.parse::<Uuid>().map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?;
let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
return result;
}
async fn run_bundle_preview_script(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -5288,6 +5321,7 @@ async fn run_bundle_preview_script(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
job_id = Some(uuid);
@@ -5453,6 +5487,7 @@ async fn run_dependencies_job(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
tx.commit().await?;
@@ -5518,6 +5553,7 @@ async fn run_flow_dependencies_job(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
tx.commit().await?;
@@ -5858,6 +5894,7 @@ async fn run_preview_flow_job(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
tx.commit().await?;
@@ -5865,6 +5902,20 @@ async fn run_preview_flow_job(
Ok((StatusCode::CREATED, uuid.to_string()))
}
async fn run_wait_result_preview_flow(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
Json(raw_flow): Json<PreviewFlow>,
) -> error::Result<Response> {
let (_status_code, uuid) = run_preview_flow_job(authed.clone(), Extension(db.clone()), Extension(user_db.clone()), Path(w_id.clone()), Query(run_query.clone()), Json(raw_flow)).await?;
let uuid = uuid.parse::<Uuid>().map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?;
let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
return result;
}
pub async fn run_job_by_hash(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -5983,6 +6034,7 @@ pub async fn run_job_by_hash_inner(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
tx.commit().await?;
@@ -564,6 +564,86 @@ pub fn all_tools() -> Vec<EndpointTool> {
})),
body_schema: None,
},
EndpointTool {
name: Cow::Borrowed("runScriptPreviewAndWaitResult"),
description: Cow::Borrowed("run script preview and wait for result"),
instructions: Cow::Borrowed("Allows testing a script before deploying it. For typescript code, the language to send is either bun or deno. By default, send bun if no deno specific code is detected."),
path: Cow::Borrowed("/w/{workspace}/jobs/run_wait_result/preview"),
method: Cow::Borrowed("POST"),
path_params_schema: None,
query_params_schema: None,
body_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The code to run"
},
"path": {
"type": "string",
"description": "The path to the script"
},
"script_hash": {
"type": "string",
"description": "The hash of the script"
},
"args": {
"type": "object",
"description": "The arguments to pass to the script or flow",
"additionalProperties": {}
},
"language": {
"type": "string",
"enum": [
"python3",
"deno",
"go",
"bash",
"powershell",
"postgresql",
"mysql",
"bigquery",
"snowflake",
"mssql",
"oracledb",
"graphql",
"nativets",
"bun",
"php",
"rust",
"ansible",
"csharp",
"nu",
"java",
"ruby",
"duckdb"
]
},
"tag": {
"type": "string"
},
"kind": {
"type": "string",
"enum": [
"code",
"identity",
"http"
]
},
"dedicated_worker": {
"type": "boolean"
},
"lock": {
"type": "string"
}
},
"required": [
"args",
"content",
"language"
]
})),
},
EndpointTool {
name: Cow::Borrowed("listQueue"),
description: Cow::Borrowed("list all queued jobs"),
+12 -2
View File
@@ -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())
@@ -1022,6 +1023,7 @@ async fn create_script_internal<'c>(
None,
None,
Some(&authed.clone().into()),
false,
)
.await?;
Ok((hash, new_tx, None))
@@ -1387,13 +1389,19 @@ async fn get_empty_ts_script_by_path() -> String {
return String::new();
}
#[derive(Deserialize)]
struct RawScriptByPathQuery {
cache_key: Option<String>,
}
async fn raw_script_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(query): Query<RawScriptByPathQuery>,
) -> Result<String> {
raw_script_by_path_internal(path, user_db, db, authed, w_id, false).await
raw_script_by_path_internal(path, user_db, db, authed, w_id, false, query.cache_key).await
}
async fn raw_script_by_path_unpinned(
@@ -1401,8 +1409,9 @@ async fn raw_script_by_path_unpinned(
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(query): Query<RawScriptByPathQuery>,
) -> Result<String> {
raw_script_by_path_internal(path, user_db, db, authed, w_id, true).await
raw_script_by_path_internal(path, user_db, db, authed, w_id, true, query.cache_key).await
}
lazy_static::lazy_static! {
@@ -1417,6 +1426,7 @@ async fn raw_script_by_path_internal(
authed: ApiAuthed,
w_id: String,
unpin: bool,
cache_key: Option<String>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:read:{}", path))?;
@@ -798,6 +798,7 @@ async fn trigger_script_with_retry_and_error_handler(
None,
None,
push_authed.as_ref(),
false,
)
.await?;
tx.commit().await?;
+2 -2
View File
@@ -212,7 +212,7 @@ pub async fn list_tokens_internal(
WHERE workspace_id = $1
AND (
scopes @> ARRAY['jobs:run:flows:' || $2]::text[]
OR scopes @> ARRAY['run:flows/' || $2]::text[]
OR scopes @> ARRAY['run:flow/' || $2]::text[]
)
"#,
w_id,
@@ -235,7 +235,7 @@ pub async fn list_tokens_internal(
WHERE workspace_id = $1
AND (
scopes @> ARRAY['jobs:run:scripts:' || $2]::text[]
OR scopes @> ARRAY['run:scripts/' || $2]::text[]
OR scopes @> ARRAY['run:script/' || $2]::text[]
)
"#,
w_id,
@@ -233,7 +233,7 @@ async fn get_websocket_trigger(
can_return_message,
error_handler_path,
error_handler_args,
retry,
retry
FROM
websocket_trigger
WHERE
+10
View File
@@ -37,6 +37,7 @@ pub fn global_service() -> Router {
.route("/get_default_tags", get(get_default_tags))
.route("/queue_metrics", get(get_queue_metrics))
.route("/queue_counts", get(get_queue_counts))
.route("/queue_running_counts", get(get_queue_running_counts))
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -219,3 +220,12 @@ async fn get_queue_counts(
let queue_counts = windmill_common::queue::get_queue_counts(&db).await;
Ok(Json(queue_counts))
}
async fn get_queue_running_counts(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> JsonResult<std::collections::HashMap<String, u32>> {
require_super_admin(&db, &authed.email).await?;
let queue_running_counts = windmill_common::queue::get_queue_running_counts(&db).await;
Ok(Json(queue_running_counts))
}
@@ -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
+6 -1
View File
@@ -21,4 +21,9 @@ serde_json.workspace = true
tracing.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-queue.workspace = true
anyhow.workspace = true
anyhow.workspace = true
kube.workspace = true
k8s-openapi.workspace = true
tokio.workspace = true
thiserror.workspace = true
axum.workspace = true
+4
View File
@@ -2,3 +2,7 @@
pub mod autoscaling_ee;
mod autoscaling_oss;
pub use autoscaling_oss::*;
#[cfg(feature = "private")]
pub mod kubernetes_integration_ee;
#[cfg(feature = "private")]
pub use kubernetes_integration_ee::{apply_kubernetes_autoscaling, KubernetesIntegration};
+30 -12
View File
@@ -8,6 +8,7 @@ use crate::{
error::{Error, Result},
jwt,
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL},
utils::WarnAfterExt,
DB,
};
@@ -259,6 +260,22 @@ pub async fn get_groups_for_user(
Ok(groups)
}
pub async fn get_job_perms<'a, E: sqlx::PgExecutor<'a>>(
db: E,
job_id: &Uuid,
w_id: &str,
) -> sqlx::Result<Option<JobPerms>> {
sqlx::query_as!(
JobPerms,
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
job_id,
w_id
)
.fetch_optional(db)
.warn_after_seconds(3)
.await
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn create_token_for_owner(
db: &DB,
@@ -274,14 +291,7 @@ pub async fn create_token_for_owner(
let job_perms = if perms.is_some() {
Ok(perms)
} else {
sqlx::query_as!(
JobPerms,
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
job_id,
w_id
)
.fetch_optional(db)
.await
get_job_perms(db, job_id, w_id).await
};
let job_authed = match job_perms {
Ok(Some(jp)) => jp.into(),
@@ -297,8 +307,16 @@ pub async fn create_token_for_owner(
}
};
create_jwt_token(job_authed, w_id, expires_in, Some(*job_id), Some(label.to_string()), audit_span, None)
.await
create_jwt_token(
job_authed,
w_id,
expires_in,
Some(*job_id),
Some(label.to_string()),
audit_span,
None,
)
.await
}
pub async fn create_jwt_token(
@@ -319,8 +337,8 @@ pub async fn create_jwt_token(
folders: authed.folders.clone(),
label,
workspace_id: workspace_id.to_string(),
exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64))
.timestamp() as usize,
exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64)).timestamp()
as usize,
job_id: job_id.map(|id| id.to_string()),
scopes,
audit_span,
+42
View File
@@ -537,6 +537,48 @@ pub mod flow {
}
}
pub mod python_import_by_path {
use super::*;
use crate::DB;
#[derive(Eq, PartialEq, Debug, Hash, Clone)]
pub struct ScriptPathWithCacheKey {
pub path: String,
pub cache_key: String,
}
impl Item for ScriptPathWithCacheKey {
fn path(&self, root: impl AsRef<Path>) -> PathBuf {
root.as_ref()
.join(self.path.clone())
.join(self.cache_key.clone())
}
}
#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize, Deserialize)]
pub struct ScriptContentOrPrefix {
pub content: String,
pub prefix: bool,
}
make_static! {
static ref CACHE: { ScriptPathWithCacheKey => ScriptContentOrPrefix } in "python_import_by_path" <= 1000;
}
pub async fn fetch(
db: &DB,
path: &str,
w_id: &str,
cache_key: &str,
) -> error::Result<ScriptContentOrPrefix> {
let r =sqlx::query_scalar!(
"SELECT content FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false ORDER BY created_at DESC LIMIT 1",
path,
w_id
).fetch_optional(db).await?;
todo!()
}
}
pub mod script {
use crate::{worker::Connection, DB};
+7
View File
@@ -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 {
@@ -129,6 +129,15 @@ struct UntaggedFlowStatusModule {
approvers: Option<Vec<Approval>>,
failed_retries: Option<Vec<Uuid>>,
skipped: Option<bool>,
agent_actions: Option<Vec<AgentAction>>,
agent_actions_success: Option<Vec<bool>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentAction {
ToolCall { job_id: uuid::Uuid, function_name: String, module_id: String },
Message {},
}
#[derive(Serialize, Debug, Clone)]
@@ -165,6 +174,10 @@ pub enum FlowStatusModule {
parallel: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
while_loop: bool,
#[serde(skip_serializing_if = "Option::is_none")]
agent_actions: Option<Vec<AgentAction>>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_actions_success: Option<Vec<bool>>,
},
Success {
id: String,
@@ -181,6 +194,10 @@ pub enum FlowStatusModule {
#[serde(skip_serializing_if = "Vec::is_empty")]
failed_retries: Vec<Uuid>,
skipped: bool,
#[serde(skip_serializing_if = "Option::is_none")]
agent_actions: Option<Vec<AgentAction>>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_actions_success: Option<Vec<bool>>,
},
Failure {
id: String,
@@ -193,6 +210,10 @@ pub enum FlowStatusModule {
branch_chosen: Option<BranchChosen>,
#[serde(skip_serializing_if = "Vec::is_empty")]
failed_retries: Vec<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_actions: Option<Vec<AgentAction>>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_actions_success: Option<Vec<bool>>,
},
}
@@ -244,6 +265,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
parallel: untagged.parallel.unwrap_or(false),
while_loop: untagged.while_loop.unwrap_or(false),
progress: untagged.progress,
agent_actions: untagged.agent_actions,
agent_actions_success: untagged.agent_actions_success,
}),
"Success" => Ok(FlowStatusModule::Success {
id: untagged
@@ -258,6 +281,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
approvers: untagged.approvers.unwrap_or_default(),
failed_retries: untagged.failed_retries.unwrap_or_default(),
skipped: untagged.skipped.unwrap_or(false),
agent_actions: untagged.agent_actions,
agent_actions_success: untagged.agent_actions_success,
}),
"Failure" => Ok(FlowStatusModule::Failure {
id: untagged
@@ -270,6 +295,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
flow_jobs_success: untagged.flow_jobs_success,
branch_chosen: untagged.branch_chosen,
failed_retries: untagged.failed_retries.unwrap_or_default(),
agent_actions: untagged.agent_actions,
agent_actions_success: untagged.agent_actions_success,
}),
other => Err(serde::de::Error::unknown_variant(
other,
@@ -354,6 +381,30 @@ impl FlowStatusModule {
_ => false,
}
}
pub fn agent_actions(&self) -> Option<Vec<AgentAction>> {
match self {
FlowStatusModule::InProgress { agent_actions, .. } => agent_actions.clone(),
FlowStatusModule::Success { agent_actions, .. } => agent_actions.clone(),
FlowStatusModule::Failure { agent_actions, .. } => agent_actions.clone(),
_ => None,
}
}
pub fn agent_actions_success(&self) -> Option<Vec<bool>> {
match self {
FlowStatusModule::InProgress { agent_actions_success, .. } => {
agent_actions_success.clone()
}
FlowStatusModule::Success { agent_actions_success, .. } => {
agent_actions_success.clone()
}
FlowStatusModule::Failure { agent_actions_success, .. } => {
agent_actions_success.clone()
}
_ => None,
}
}
}
impl FlowStatus {
+12
View File
@@ -529,6 +529,10 @@ pub enum FlowModuleValue {
#[serde(skip_serializing_if = "Option::is_none")]
assets: Option<Vec<AssetWithAltAccessType>>,
},
AIAgent {
input_transforms: HashMap<String, InputTransform>,
tools: Vec<FlowModule>,
},
}
fn is_none_or_empty(expr: &Option<String>) -> bool {
@@ -563,6 +567,7 @@ struct UntaggedFlowModuleValue {
default_node: Option<FlowNodeId>,
modules_node: Option<FlowNodeId>,
assets: Option<Vec<AssetWithAltAccessType>>,
tools: Option<Vec<FlowModule>>,
}
impl<'de> Deserialize<'de> for FlowModuleValue {
@@ -655,6 +660,12 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
assets: untagged.assets,
}),
"identity" => Ok(FlowModuleValue::Identity),
"aiagent" => Ok(FlowModuleValue::AIAgent {
input_transforms: untagged.input_transforms.unwrap_or_default(),
tools: untagged
.tools
.ok_or_else(|| serde::de::Error::missing_field("tools"))?,
}),
other => Err(serde::de::Error::unknown_variant(
other,
&[
@@ -666,6 +677,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
"branchall",
"rawscript",
"identity",
"aiagent",
],
)),
}
@@ -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",
+4
View File
@@ -48,6 +48,7 @@ pub enum JobKind {
FlowScript,
FlowNode,
AppScript,
AIAgent,
}
impl JobKind {
@@ -375,6 +376,9 @@ pub enum JobPayload {
},
Identity,
Noop,
AIAgent {
path: String,
},
}
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
+3 -3
View File
@@ -627,8 +627,8 @@ pub fn get_latest_flow_version_info_for_path<
}
}
pub async fn get_latest_hash_for_path<'c>(
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
pub async fn get_latest_hash_for_path<'c, E: sqlx::PgExecutor<'c>>(
db: E,
w_id: &str,
script_path: &str,
) -> error::Result<(
@@ -652,7 +652,7 @@ pub async fn get_latest_hash_for_path<'c>(
script_path,
w_id
)
.fetch_optional(&mut **db)
.fetch_optional(db)
.await?;
let script = utils::not_found_if_none(r_o, "script", script_path)?;
+13
View File
@@ -14,3 +14,16 @@ pub async fn get_queue_counts(db: &Pool<Postgres>) -> HashMap<String, u32> {
.map(|v| v.into_iter().map(|x| (x.tag, x.count as u32)).collect())
.unwrap_or_else(|| HashMap::new())
}
pub async fn get_queue_running_counts(db: &Pool<Postgres>) -> HashMap<String, u32> {
sqlx::query!(
"SELECT tag AS \"tag!\", count(*) AS \"count!\" FROM v2_job_queue WHERE
running = true
GROUP BY tag",
)
.fetch_all(db)
.await
.ok()
.map(|v| v.into_iter().map(|x| (x.tag, x.count as u32)).collect())
.unwrap_or_else(|| HashMap::new())
}
+1 -1
View File
@@ -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
+6 -1
View File
@@ -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())
}
+25 -5
View File
@@ -18,15 +18,22 @@ use std::{
path::{Component, Path, PathBuf},
str::FromStr,
sync::{atomic::AtomicBool, Arc},
time::Duration,
};
#[cfg(windows)]
use sysinfo::System;
use tokio::sync::RwLock;
use tokio::{sync::RwLock, time::timeout};
use uuid::Uuid;
use windmill_macros::annotations;
use crate::{
agent_workers::PingJobStatusResponse, cache::{unwrap_or_error, RawNode, RawScript}, error::{self, to_anyhow}, global_settings::CUSTOM_TAGS_SETTING, indexer::TantivyIndexerSettings, server::Smtp, KillpillSender, BASE_INTERNAL_URL, DB
agent_workers::PingJobStatusResponse,
cache::{unwrap_or_error, RawNode, RawScript},
error::{self, to_anyhow},
global_settings::CUSTOM_TAGS_SETTING,
indexer::TantivyIndexerSettings,
server::Smtp,
KillpillSender, BASE_INTERNAL_URL, DB,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
@@ -172,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(),
@@ -615,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 {
@@ -628,6 +642,12 @@ pub struct PythonAnnotations {
pub py313: bool,
}
#[derive(Copy, Clone)]
#[annotations("//")]
pub struct GoAnnotations {
pub go1_22_compat: bool,
}
#[annotations("//")]
pub struct TypeScriptAnnotations {
pub npm: bool,
@@ -1353,7 +1373,7 @@ pub async fn update_worker_ping_main_loop_query(
occupancy_rate_30m: Option<f32>,
db: &DB,
) -> anyhow::Result<()> {
sqlx::query!(
timeout(Duration::from_secs(10), sqlx::query!(
"UPDATE worker_ping SET ping_at = now(), jobs_executed = $1, custom_tags = $2,
occupancy_rate = $3, memory_usage = $4, wm_memory_usage = $5, vcpus = COALESCE($7, vcpus),
memory = COALESCE($8, memory), occupancy_rate_15s = $9, occupancy_rate_5m = $10, occupancy_rate_30m = $11 WHERE worker = $6",
@@ -1369,8 +1389,8 @@ pub async fn update_worker_ping_main_loop_query(
occupancy_rate_5m,
occupancy_rate_30m,
)
.execute(db)
.await?;
.execute(db))
.await??;
Ok(())
}
+1 -1
View File
@@ -117,7 +117,7 @@ pub async fn update_workflow_as_code_status(
// TODO: merge as a CTE
#[tracing::instrument(level = "trace", skip_all)]
async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result<Step> {
pub async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result<Step> {
let r = sqlx::query!(
"SELECT (flow_status->'step')::integer as step, jsonb_array_length(flow_status->'modules') as len
FROM v2_job_status WHERE id = $1",
+35 -4
View File
@@ -448,6 +448,7 @@ pub async fn push_init_job<'c>(
None,
None,
None,
false,
)
.await?;
inner_tx.commit().await?;
@@ -500,6 +501,7 @@ pub async fn push_periodic_bash_job<'c>(
None,
None,
None,
false,
)
.await?;
inner_tx.commit().await?;
@@ -1274,6 +1276,7 @@ async fn restart_job_if_perpetual_inner(
None,
queued_job.priority,
None,
false,
)
.await?;
tx.commit().await?;
@@ -2036,6 +2039,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
None,
priority,
None,
false,
)
.await?;
tx.commit().await?;
@@ -2144,6 +2148,7 @@ async fn handle_recovered_schedule<'a, 'c, T: Serialize + Send + Sync>(
None,
None,
None,
false,
)
.await?;
tracing::info!(
@@ -2233,6 +2238,7 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>(
None,
None,
None,
false,
)
.await?;
tracing::info!(
@@ -3618,7 +3624,7 @@ pub async fn push<'c, 'd>(
root_job: Option<Uuid>,
job_id: Option<Uuid>,
_is_flow_step: bool,
mut same_worker: bool,
mut same_worker: bool, // whether the job will be executed on the same worker: if true, the job will be set to running but started_at will not be set.
pre_run_error: Option<&windmill_common::error::Error>,
visible_to_owner: bool,
mut tag: Option<String>,
@@ -3626,6 +3632,7 @@ pub async fn push<'c, 'd>(
flow_step_id: Option<String>,
_priority_override: Option<i16>,
authed: Option<&Authed>,
running: bool, // whether the job is already running: only set this to true if you don't want the job to be picked up by a worker from the queue. It will also set started_at to now.
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
#[cfg(feature = "cloud")]
if *CLOUD_HOSTED {
@@ -4435,6 +4442,21 @@ pub async fn push<'c, 'd>(
None,
None,
),
JobPayload::AIAgent { path } => (
None,
Some(path),
None,
JobKind::AIAgent,
None,
None,
None,
None,
None,
None,
None,
None,
None,
),
};
let final_priority: Option<i16>;
@@ -4462,7 +4484,7 @@ pub async fn push<'c, 'd>(
final_priority
};
let is_running = same_worker;
let is_running = same_worker || running;
if let Some(flow) = raw_flow.as_ref() {
same_worker = same_worker || flow.same_worker;
@@ -4508,7 +4530,10 @@ pub async fn push<'c, 'd>(
let per_workspace = per_workspace_tag(&workspace_id).await;
let default = || {
let ntag = if job_kind.is_flow() || job_kind == JobKind::Identity {
let ntag = if job_kind.is_flow()
|| job_kind == JobKind::Identity
|| job_kind == JobKind::AIAgent
{
"flow".to_string()
} else if job_kind == JobKind::Dependencies
|| job_kind == JobKind::FlowDependencies
@@ -4667,7 +4692,7 @@ pub async fn push<'c, 'd>(
)
INSERT INTO v2_job_queue
(workspace_id, id, running, scheduled_for, started_at, tag, priority)
VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
job_id,
workspace_id,
raw_code,
@@ -4711,6 +4736,7 @@ pub async fn push<'c, 'd>(
job_authed.groups.as_slice(),
root_job.or(parent_job),
trigger_kind as Option<JobTriggerKind>,
running,
)
.execute(&mut *tx)
.warn_after_seconds(1)
@@ -4782,6 +4808,7 @@ pub async fn push<'c, 'd>(
JobKind::FlowScript => "jobs.run.flow_script",
JobKind::FlowNode => "jobs.run.flow_node",
JobKind::AppScript => "jobs.run.app_script",
JobKind::AIAgent => "jobs.run.ai_agent",
};
let audit_author = if format!("u/{user}") != permissioned_as && user != permissioned_as {
@@ -4971,6 +4998,8 @@ async fn restarted_flows_resolution(
parallel,
while_loop: false,
progress: None,
agent_actions: None,
agent_actions_success: None,
});
}
Ok(FlowModuleValue::ForloopFlow { parallel, .. }) => {
@@ -5009,6 +5038,8 @@ async fn restarted_flows_resolution(
parallel,
while_loop: false,
progress: None,
agent_actions: None,
agent_actions_success: None,
});
}
_ => {
+2 -1
View File
@@ -153,7 +153,7 @@ pub async fn push_scheduled_job<'c>(
on_behalf_of_email,
created_by,
) = windmill_common::get_latest_hash_for_path(
&mut tx,
&mut *tx,
&schedule.workspace_id,
&schedule.script_path,
)
@@ -302,6 +302,7 @@ pub async fn push_scheduled_job<'c>(
None,
None,
push_authed,
false,
)
.await?;
+3
View File
@@ -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 }
@@ -57,6 +59,7 @@ windmill-git-sync.workspace = true
flume.workspace = true
sqlx.workspace = true
uuid.workspace = true
ulid.workspace = true
tracing.workspace = true
tokio.workspace = true
tokio-stream.workspace = 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}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -758,7 +758,7 @@ pub async fn prebundle_bun_script(
pub const BUN_BUNDLE_OBJECT_STORE_PREFIX: &str = "bun_bundle/";
async fn get_script_import_updated_at(db: &DB, w_id: &str, script_path: &str) -> Result<String> {
let script_hash = get_latest_hash_for_path(&mut db.begin().await?, w_id, script_path).await?;
let script_hash = get_latest_hash_for_path(db, w_id, script_path).await?;
let last_updated_at = sqlx::query_scalar!(
"SELECT created_at FROM script WHERE workspace_id = $1 AND hash = $2",
w_id,
+5 -561
View File
@@ -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>(
@@ -551,13 +549,14 @@ pub async fn update_worker_ping_for_failed_init_script(
}
}
pub fn error_to_value(err: Error) -> serde_json::Value {
pub fn error_to_value(err: &Error) -> serde_json::Value {
match err {
Error::JsonErr(err) => err,
Error::JsonErr(err) => err.clone(),
_ => json!({"message": err.to_string(), "name": err.name()}),
}
}
#[derive(Clone)]
pub struct OccupancyMetrics {
pub running_job_started_at: Option<Instant>,
pub total_duration_of_running_jobs: f32,
@@ -1072,561 +1071,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,
@@ -433,6 +433,7 @@ async fn spawn_dedicated_workers_for_flow(
}
FlowModuleValue::Flow { .. } => (),
FlowModuleValue::Identity => (),
FlowModuleValue::AIAgent { .. } => (),
}
} else {
tracing::error!("failed to get value for module: {:?}", module);
+12 -2
View File
@@ -12,7 +12,7 @@ use uuid::Uuid;
use windmill_common::{
error::{self, Error},
utils::calculate_hash,
worker::{save_cache, write_file, Connection},
worker::{save_cache, write_file, Connection, GoAnnotations},
};
use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE};
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
@@ -464,6 +464,7 @@ pub async fn install_go_dependencies(
w_id: &str,
occupation_metrics: &mut OccupancyMetrics,
) -> error::Result<String> {
let anns = GoAnnotations::parse(code);
if raw_deps {
let go_mod =
if let Some(module) = code.lines().find(|l| l.trim_start().starts_with("module ")) {
@@ -529,7 +530,11 @@ pub async fn install_go_dependencies(
} else {
"".to_string()
};
let hash = format!("go-{}", hash);
let hash = format!(
"go{}-{}",
if anns.go1_22_compat { "1.22" } else { "" },
hash
);
let mut skip_tidy = has_sum;
@@ -579,6 +584,11 @@ pub async fn install_go_dependencies(
.args(vec!["mod", mod_command])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
// If annotation used we want to call tidy with special flag to pin go to 1.22
// The reason for this that at some point we had to jump from go 1.22 to 1.25 and this addds backward compatibility.
if anns.go1_22_compat && mod_command == "tidy" {
child_cmd.args(vec!["-go", "1.22"]);
}
#[cfg(windows)]
set_windows_env_vars(&mut child_cmd);
+17 -17
View File
@@ -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?;
+5
View File
@@ -13,6 +13,10 @@ mod bash_executor;
#[cfg(feature = "java")]
mod java_executor;
#[cfg(feature = "ruby")]
mod ruby_executor;
mod ai_executor;
mod bun_executor;
pub mod common;
mod config;
@@ -52,6 +56,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;
@@ -852,6 +852,7 @@ mount {{
{
python_cmd.env("SystemRoot", SYSTEM_ROOT.as_str());
python_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
python_cmd.env("windir", SYSTEM_ROOT.as_str());
python_cmd.env(
"LOCALAPPDATA",
std::env::var("LOCALAPPDATA")
@@ -647,7 +647,7 @@ pub async fn process_completed_job(
return Ok(None);
}
async fn handle_non_flow_job_error(
pub async fn handle_non_flow_job_error(
db: &DB,
job: &MiniPulledJob,
mem_peak: i32,
@@ -692,7 +692,7 @@ pub async fn handle_job_error(
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) {
let err_string = format!("{}: {}", err.name(), err.to_string());
let err_json = error_to_value(err);
let err_json = error_to_value(&err);
let update_job_future = || async {
handle_non_flow_job_error(
@@ -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)
)
}
@@ -20,13 +20,13 @@ use windmill_queue::{CanceledBy, MiniPulledJob, HTTP_CLIENT};
use serde::{Deserialize, Serialize};
use crate::common::build_args_values;
use crate::common::{
build_http_client, resolve_job_timeout, s3_mode_args_to_worker_data, OccupancyMetrics,
S3ModeWorkerData,
};
use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args;
use crate::common::build_args_values;
use windmill_common::client::AuthedClient;
#[derive(Serialize)]
@@ -0,0 +1,768 @@
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};
#[allow(dead_code)]
#[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
],
},
+76 -7
View File
@@ -99,6 +99,7 @@ use tokio::{
use rand::Rng;
use crate::ai_executor::handle_ai_agent_job;
use crate::{
agent_workers::{queue_init_job, queue_periodic_job},
bash_executor::{handle_bash_job, handle_powershell_job},
@@ -134,6 +135,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 +195,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 +346,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));
@@ -745,7 +754,7 @@ pub async fn handle_all_job_kind_error(
preprocessed_args: None,
job: job.clone(),
result: Arc::new(windmill_common::worker::to_raw_value(&error_to_value(
err,
&err,
))),
result_columns: None,
mem_peak: 0,
@@ -891,7 +900,7 @@ pub fn start_interactive_worker_shell(
})
}
async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String {
pub async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String {
let job_dir_path = format!("{}/{}", worker_directory, job_id);
create_directory_async(&job_dir_path).await;
@@ -2182,11 +2191,13 @@ pub struct SendResult {
pub time: Instant,
}
#[derive(Clone)]
pub enum SendResultPayload {
JobCompleted(JobCompleted),
UpdateFlow(UpdateFlow),
}
#[derive(Clone)]
pub struct UpdateFlow {
pub flow: Uuid,
pub w_id: String,
@@ -2333,9 +2344,10 @@ pub async fn handle_queued_job(
| JobKind::FlowDependencies,
x,
) => match x.map(|x| x.0) {
None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => {
Some(cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow).await?)
}
None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => Some(
cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone())
.await?,
),
_ => None,
},
_ => None,
@@ -2540,6 +2552,31 @@ pub async fn handle_queued_job(
.flatten()
.map(|x| x.to_owned())
.unwrap_or_else(|| serde_json::from_str("{}").unwrap())),
JobKind::AIAgent => match conn {
Connection::Sql(db) => {
handle_ai_agent_job(
conn,
db,
job.as_ref(),
&client,
&mut canceled_by,
&mut mem_peak,
&mut *occupancy_metrics,
&job_completed_tx,
worker_dir,
base_internal_url,
worker_name,
hostname,
killpill_rx,
)
.await
}
Connection::Http(_) => {
return Err(Error::internal_err(
"Agent worker does not support ai agent jobs".to_string(),
));
}
},
_ => {
let metric_timer = Instant::now();
let preview_data = preview_data.and_then(|data| match data {
@@ -2724,6 +2761,7 @@ async fn try_validate_schema(
JobKind::AppDependencies => 12,
JobKind::Noop => 13,
JobKind::FlowNode => 14,
JobKind::AIAgent => 15,
};
let sv = match job.runnable_id {
@@ -3439,6 +3477,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!(
@@ -3453,7 +3518,7 @@ mount {{
result
}
fn parse_sig_of_lang(
pub fn parse_sig_of_lang(
code: &str,
language: Option<&ScriptLang>,
main_override: Option<String>,
@@ -3511,6 +3576,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 {

Some files were not shown because too many files have changed in this diff Show More