Files
windmill/backend/windmill-worker/src/php_executor.rs
T
+1 82620098d2 feat: add native result streaming (#6242)
* feat: add stream output feature to SSE job updates

Adds stream_output field to JobUpdate struct that extracts log lines
starting with '[wm_stream]:' from job logs. Regular logs now exclude
stream lines, which are captured separately for specialized handling.

- Added stream_output: Option<String> field to JobUpdate struct
- Created extract_stream_output_from_logs() function to filter stream lines
- Modified get_job_update_data() to use stream extraction logic
- SSE clients now receive both new_logs and stream_output in job updates

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

* feat: rename stream_output to stream and handle newlines as \n

- Renamed `stream_output` field to `stream` in JobUpdate struct
- Updated extract_stream_output_from_logs to extract_stream_from_logs
- Changed stream output to join with literal \n instead of actual newlines
- Stream lines are properly excluded from regular new_logs

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

Co-Authored-By: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* decision tree nits

* push ee ref

* push ee ref

* fix: fix id renaming in apps

* remove duplicate caching (#6285)

* feat: migrate audit log ids to bigints (blocking migration for EE)

* fix(mcp): add proper check for mcp routes (#6282)

* add proper check for mcp routes

* cleaner

* apply to flow

* fix add checks scopes

---------

Co-authored-by: dieriba <dieriba.pro@gmail.com>

* chore(main): release 1.514.0 (#6283)

* chore(main): release 1.514.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* fix: pin tokio to 1.46.1 and aws-sdks-ts

* pin rustls to 0.23.29 + pin aws-sdk

* chore(main): release 1.514.1 (#6288)

* chore(main): release 1.514.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* fix: improve docker logs collection in docker mode

* support $res: string in form inputs of arrays

* fix import nit

* fix: fix DynSelect

* nits

* fix: resource-type-ts-parser (#6289)

* fix: resource types as arg in typescript handle imported defined types

* Update nix flake (#6291)

* merge

* Small UI fixes (#6294)

* fix step history not refreshing with staticInputs

* fix array of obj not showing up in json editor in test this step

* datatable scales correctly in DisplayResult and scrolling is much more usable

* avoid next button disapearing and changing layout / hurting ux

* nits

* fix bug when renaming module A to B then module C to A, C takes the schema of A

* fix bug with comments in sql repl

* fix aggrid theme randomly not loading

* bindable script

* better delete button in db manager

* property select doesnt exist

* fix all warnings

* delete $flowStateStore[id] on delete

* feat(cli): generate cursor rules on init (#6270)

* create cursor rules on init

* change gen

* add missing resource-type command

* add resource type command in guidance

* add schema option

* revert

* nit

* nit

* add flow guidance

* nit

* chore(main): release 1.515.0 (#6292)

* chore(main): release 1.515.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* fix: improved logs for script

* nits logs

* chore(main): release 1.515.1 (#6295)

* chore(main): release 1.515.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* merge

* even more indexer tracings

* add more tracing logs

* feat: prevent too large results (>500Mb) from OOMing database

* nit naming

* feat: add CA certificate update at startup via environment variable (#6280)

* feat: add CA certificate update at startup via environment variable

Add support for running 'update-ca-certificates' at binary startup
when RUN_UPDATE_CA_CERTIFICATE_AT_START environment variable is set to "true".

- Check for RUN_UPDATE_CA_CERTIFICATE_AT_START env var on startup
- Execute update-ca-certificates command if env var is set to "true"
- Log success/failure appropriately with tracing
- Continue startup even if CA certificate update fails
- Non-blocking implementation with proper error handling

Fixes #6279

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

* refactor: extract CA certificate update logic into separate function

Extract the CA certificate update logic from windmill_main() into a
dedicated update_ca_certificates_if_requested() function for better
code organization and maintainability.

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

* improvements

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>
Co-authored-by: Alexander Petric <alex@windmill.dev>

* fix: indexer collection of job logs before indexing (#6300)

* Add flume as dependecy for indexer

* Update ee-repo-ref

* Remove flags from cargo.toml

* Update ee-repo-ref

* Update ee-repo-ref

* fix rust sdk build error (#6305)

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

* fix broken audit logs filter (#6304)

* rename to from to

* goto fix

* default to false if field not present operator settings (#6301)

* git sync UI improvements (#6303)

* ui improvements round 1

* modal cleanup

* init

* UI refactor

* UI cleanup + refactor

* legacy cleanup

* success model -> github actions, non-ee warnings

* sqlx

* npm check

* ee warning everywhere

* last comments

* formatting

* no hardcoded theme

* claude review improvemenets

* fix: no process relative imports for scripts with codebase

* fix: sqs oidc authentication disconnect #6307

* handle metadata for new scripts happen after commit

* handle_deployment_metadata in a task

* nits

* chore: add windmill-utils-internal package (#6299)

* add utils package

* naming

* cleaning

* add docs

* remove log

* use autogenerated types

* remove old

* fix

* cleaning

* add docs

* chore(main): release 1.516.0 (#6298)

* chore(main): release 1.516.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* merge

* indexer improvements

* upgrade tantivy to 0.24.2

* use tantivy fork

* nit warnings

* fix oss build

* improve indexer

* chore: use windmill-utils-internal for cli (#6297)

* add utils package

* naming

* cleaning

* simplify assignPath

* rename old files

* same for locks

* create on confirm

* default true

* use replaceinlinescripts from utils

* use extractscriptfromflows

* make it compile

* cleaning

* use argsigtojson

* fix

* fix missing await

* cleaner

* cleaning

* cleaning

* use in frontend

* add docs

* testing

* remove log

* use autogenerated types

* remove old

* fix

* cleaning

* adapt usage

* draft

* better build script

* fix build

* revert to default creation

* add docs

* remove and rename

* make everything work

* add await

* only if not installed

* add vs code setting

* add to publish action

* fix bc

* safer use of sep

* fix

* do not rename on push

* no publish on release

* use published package on frontend

* nit

* Add dependencies to run sqlx prepare to nix flake (#6309)

* feat(cli): wmill-lock.yaml v2 for easier git merge diffs

* merge

* merge

* all

* all

* rm warnings

* fix styling on aichatinput (#6312)

* fix: use with_capacity back presusre for tantivy directory multipart writes (#6313)

* use with capacity for tantivy directory multi part uploads

* Update ee repo ref

* Update ee-repo-ref

* Update ee-repo-ref

* chore(main): release 1.517.0 (#6310)

* chore(main): release 1.517.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* fix typo on cli build (#6314)

* cleanup

* feat(utils): add flow.yaml validation function (#6316)

* add validateflow function

* cleaner code

* preprocess json

* cleaning

* create specific package

* cleaning

* add tests

* fix: cleanup concurrency_counter automatically + remove orphans keys automatically

* fix: add disabled support to resource picker in schema forms

* fix: add wm_labels to tracing spans

* all

* merge

* all

* fix: delete empty git connection (#6318)

* fix checks

* bun handling

* all

* all?

* all

* all

* update

* all

* update

* check

* fix history

* all

* all

* all

* Remove leftover debug tracing statements

- Remove commented debug trace in jobs.rs for stream output
- Remove commented debug trace in result_stream.rs for stream processing

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

* fix test

* all

* handle iter

* fix

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: centdix <40307056+centdix@users.noreply.github.com>
Co-authored-by: dieriba <dieriba.pro@gmail.com>
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
Co-authored-by: wendrul <53628737+wendrul@users.noreply.github.com>
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>
Co-authored-by: Alexander Petric <alex@windmill.dev>
Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
2025-08-06 22:40:42 +00:00

349 lines
10 KiB
Rust

use convert_case::{Case, Casing};
use itertools::Itertools;
use regex::Regex;
use serde_json::value::RawValue;
use std::{collections::HashMap, process::Stdio};
use tokio::{fs::File, io::AsyncReadExt, process::Command};
use uuid::Uuid;
use windmill_common::{
error::{self, to_anyhow, Result},
worker::{write_file, Connection},
};
use windmill_queue::MiniPulledJob;
use windmill_parser::Typ;
use windmill_queue::{append_logs, CanceledBy};
use crate::{
common::{
check_executor_binary_exists, create_args_and_out_file, get_reserved_variables,
read_result, start_child_process, OccupancyMetrics,
},
handle_child::handle_child,
COMPOSER_CACHE_DIR, COMPOSER_PATH, DISABLE_NSJAIL, DISABLE_NUSER, NSJAIL_PATH, PHP_PATH,
};
use windmill_common::client::AuthedClient;
const NSJAIL_CONFIG_RUN_PHP_CONTENT: &str = include_str!("../nsjail/run.php.config.proto");
lazy_static::lazy_static! {
static ref RE: Regex = Regex::new(r"^//\s?(\S+)\s*$").unwrap();
}
const COMPOSER_LOCK_SPLIT: &str = "\nLOCK\n";
pub fn parse_php_imports(code: &str) -> anyhow::Result<Option<String>> {
let find_requirements = code
.lines()
.find_position(|x| x.starts_with("//require:") || x.starts_with("// require:"));
if let Some((pos, _)) = find_requirements {
let requirements = code
.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x).map(|x| {
match x.get(1).unwrap().as_str().split("@").collect_vec()[..] {
[path, version] => (path.to_string(), version.to_string()),
[path] | [path, ..] => (path.to_string(), "*".to_string()),
[] => unreachable!(),
}
})
})
.collect::<HashMap<_, _>>();
let composer_json =
serde_json::to_string_pretty(&HashMap::from([("require", requirements)]))
.map_err(to_anyhow)?;
Ok(Some(composer_json))
} else {
Ok(None)
}
}
pub async fn composer_install(
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
job_id: &Uuid,
w_id: &str,
conn: &Connection,
job_dir: &str,
worker_name: &str,
requirements: String,
lock: Option<String>,
occupancy_metrics: &mut OccupancyMetrics,
) -> Result<String> {
check_executor_binary_exists("php", PHP_PATH.as_str(), "php")?;
write_file(job_dir, "composer.json", &requirements)?;
if let Some(lock) = lock.as_ref() {
write_file(job_dir, "composer.lock", lock)?;
}
let mut child_cmd = Command::new(&*COMPOSER_PATH);
let args = vec!["install", "--no-dev", "--no-progress"];
child_cmd
.current_dir(job_dir)
.env("COMPOSER_HOME", &*COMPOSER_CACHE_DIR)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child_process = start_child_process(child_cmd, &*COMPOSER_PATH).await?;
handle_child(
job_id,
conn,
mem_peak,
canceled_by,
child_process,
false,
worker_name,
w_id,
"composer install",
None,
false,
&mut Some(occupancy_metrics),
None,
)
.await?;
match lock {
Some(lock) => Ok(format!("{requirements}{COMPOSER_LOCK_SPLIT}{lock}")),
None => {
let mut lock_content = "".to_string();
let mut lock_file = File::open(format!("{job_dir}/composer.lock")).await?;
lock_file.read_to_string(&mut lock_content).await?;
Ok(format!("{requirements}{COMPOSER_LOCK_SPLIT}{lock_content}"))
}
}
}
fn generate_resource_class(rt_name: &str, arg_name: &str) -> String {
let rt_name = rt_name.to_case(Case::Pascal);
format!(
"#[AllowDynamicProperties]
class {rt_name} {{
public function __construct($data) {{
foreach ($data AS $key => $value) $this->{{$key}} = $value;
}}
}}
$args->{arg_name} = new {rt_name}($args->{arg_name});"
)
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_php_job(
requirements_o: Option<&String>,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
job: &MiniPulledJob,
conn: &Connection,
client: &AuthedClient,
parent_runnable_path: Option<String>,
job_dir: &str,
inner_content: &String,
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
shared_mount: &str,
occupancy_metrics: &mut OccupancyMetrics,
) -> error::Result<Box<RawValue>> {
check_executor_binary_exists("php", PHP_PATH.as_str(), "php")?;
let (composer_json, composer_lock) = match requirements_o {
Some(reqs_and_lock) if !reqs_and_lock.is_empty() => {
let splitted = reqs_and_lock.split(COMPOSER_LOCK_SPLIT).collect_vec();
if splitted.len() != 2 {
return Err(error::Error::ExecutionErr(
format!("Invalid requirements, expected to find LOCK split pattern in reqs. Found: |{reqs_and_lock}|")
));
}
(Some(splitted[0].to_string()), Some(splitted[1].to_string()))
}
_ => (parse_php_imports(inner_content)?, None),
};
let autoload_line = if let Some(composer_json) = composer_json {
let logs1 = "\n\n--- COMPOSER INSTALL ---\n".to_string();
append_logs(&job.id, &job.workspace_id, logs1, conn).await;
composer_install(
mem_peak,
canceled_by,
&job.id,
&job.workspace_id,
conn,
job_dir,
worker_name,
composer_json,
composer_lock,
occupancy_metrics,
)
.await?;
"require './vendor/autoload.php';"
} else {
""
};
let init_logs = "\n\n--- PHP CODE EXECUTION ---\n".to_string();
append_logs(&job.id, job.workspace_id.to_string(), init_logs, conn).await;
let _ = write_file(job_dir, "main.php", inner_content)?;
let main_override = job.script_entrypoint_override.as_deref();
let write_wrapper_f = async {
let args = windmill_parser_php::parse_php_signature(
inner_content,
main_override.map(ToString::to_string),
)?
.args;
let args_to_include = args
.iter()
.filter(|x| {
!x.has_default || job.args.as_ref().is_some_and(|a| a.contains_key(&x.name))
})
.collect::<Vec<_>>();
let func_args_str = args_to_include
.iter()
.map(|x| format!("$args->{}", x.name))
.collect::<Vec<String>>()
.join(",");
let resource_classes = args_to_include
.iter()
.filter_map(|x| match &x.typ {
Typ::Resource(name) => Some((name, &x.name)),
_ => None,
})
.unique()
.map(|(rt_name, arg_name)| generate_resource_class(rt_name, arg_name))
.collect::<Vec<_>>()
.join("\n\n");
let main_name = main_override.unwrap_or("main");
let wrapper_content: String = format!(
r#"
<?php
{autoload_line}
require './main.php';
$args_str = file_get_contents('./args.json');
$args = json_decode($args_str);
{resource_classes}
try {{
$res = {main_name}({func_args_str});
file_put_contents("result.json", json_encode($res));
}} catch (Exception $e) {{
$err = [
"message" => $e->getMessage(),
"name" => get_class($e),
"stack" => $e->getTraceAsString()
];
$step_id = getenv('WM_FLOW_STEP_ID');
if ($step_id) {{
$err["step_id"] = $step_id;
}}
file_put_contents("result.json", json_encode($err));
exit(1);
}}
"#,
);
write_file(job_dir, "wrapper.php", &wrapper_content)?;
Ok(()) as error::Result<()>
};
let reserved_variables_args_out_f = async {
let args_and_out_f = async {
create_args_and_out_file(&client, job, job_dir, conn).await?;
Ok(()) as Result<()>
};
let reserved_variables_f = async {
let vars =
get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone())
.await?;
Ok(vars) as Result<HashMap<String, String>>
};
let (_, reserved_variables) = tokio::try_join!(args_and_out_f, reserved_variables_f)?;
Ok(reserved_variables) as error::Result<HashMap<String, String>>
};
let (reserved_variables, _) = tokio::try_join!(reserved_variables_args_out_f, write_wrapper_f)?;
let child = if !*DISABLE_NSJAIL {
let _ = write_file(
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_PHP_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
let args = vec![
"--config",
"run.config.proto",
"--",
&PHP_PATH,
"/tmp/wrapper.php",
];
nsjail_cmd
.current_dir(job_dir)
.env_clear()
.envs(envs)
.envs(reserved_variables)
.env("COMPOSER_HOME", &*COMPOSER_CACHE_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await?
} else {
let cmd = {
let script_path = format!("{job_dir}/wrapper.php");
let mut php_cmd = Command::new(&*PHP_PATH);
let args = vec![&script_path];
php_cmd
.current_dir(job_dir)
.env_clear()
.envs(envs)
.envs(reserved_variables)
.env("COMPOSER_HOME", &*COMPOSER_CACHE_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
php_cmd
};
start_child_process(cmd, &*PHP_PATH).await?
};
handle_child(
&job.id,
conn,
mem_peak,
canceled_by,
child,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
"php run",
job.timeout,
false,
&mut Some(occupancy_metrics),
None,
)
.await?;
read_result(job_dir, None).await
}