Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ProcessMaker/Enums/ScriptExecutorType.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,10 @@ enum ScriptExecutorType:string
case System = 'system';
case Custom = 'custom';
case Duplicate = 'duplicate';
case Realtime = 'realtime';

public function isCustomOrRealtime(): bool
{
return $this === self::Custom || $this === self::Realtime;
}
}
6 changes: 5 additions & 1 deletion ProcessMaker/Events/ScriptResponseEvent.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,18 +25,21 @@ class ScriptResponseEvent implements ShouldBroadcastNow

public $nonce;

public $duration;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null)
public function __construct(User $user, $status, array $response, $watcher = null, $nonce = null, $duration = null)
{
$this->userId = $user->id;
$this->status = $status;
$this->response = $response;
$this->watcher = $watcher;
$this->nonce = $nonce;
$this->duration = $duration;
}

/**
Expand DownExpand Up@@ -75,6 +78,7 @@ public function broadcastWith()
'watcher' => $this->watcher,
'response' => $response,
'nonce' => $this->nonce,
'duration' => $this->duration,
];
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,15 +8,20 @@

class ScriptExecutorController extends Controller
{
public function index(Request $request)
public function index(Request $request, ScriptMicroserviceService $service)
{
if (!config('app.custom_executors')) {
abort(404);
}

$scriptMicroserviceEnabled = config('script-runner-microservice.enabled');

return view('admin.script-executors.index',
[
'script_microservice_enabled' => config('script-runner-microservice.enabled'),
'script_microservice_enabled' => $scriptMicroserviceEnabled,
'script_microservice_tenant_id' => $scriptMicroserviceEnabled
? $service->getInstanceUuid()
: null,
]);
}
}
17 changes: 14 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptCreated;
use ProcessMaker\Events\ScriptDeleted;
use ProcessMaker\Events\ScriptDuplicated;
Expand DownExpand Up@@ -178,7 +179,7 @@ public function index(Request $request)
*
* @OA\Response(
* response=200,
* description="success if the script was queued",
* description="The queued status or realtime execution response",
* ),
* ),
* )
Expand All@@ -190,7 +191,12 @@ public function preview(Request $request, Script $script)
$code = $request->get('code');
$nonce = $request->get('nonce');

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
$job = TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}

return ['status' => 'success'];
}
Expand DownExpand Up@@ -249,7 +255,12 @@ public function execute(Request $request, ...$scriptKey)
if ($request->get('sync') === true) {
return (new ExecuteScript($script, $request->user(), $code, $data, $watcher, $config, true))->handle();
} else {
ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config)->onQueue('bpmn');
$job = ExecuteScript::dispatch($script, $request->user(), $code, $data, $watcher, $config);
if ($script->scriptExecutor->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
} else {
$job->onQueue('bpmn');
}
}

return ['status' => 'success', 'key' => $watcher];
Expand Down
60 changes: 57 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Enums\ScriptExecutorType;
Expand DownExpand Up@@ -124,7 +125,15 @@ public function store(Request $request, ScriptMicroserviceService $service)
ScriptExecutorCreated::dispatch($scriptExecutor->getAttributes());
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
} else {
$service->createCustomExecutor($scriptExecutor);
try {
$service->createCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
// The remote executor was rejected, so keeping the local record would leave
// an executor that can never be built.
$scriptExecutor->delete();

$this->throwMicroserviceError($e);
}
}

return ['status' => 'started', 'uuid' => $scriptExecutor->uuid, 'id' => $scriptExecutor->id];
Expand DownExpand Up@@ -188,8 +197,12 @@ public function update(Request $request, ScriptExecutor $scriptExecutor, ScriptM
$request->only($scriptExecutor->getFillable())
);

if (config('script-runner-microservice.enabled') && $scriptExecutor->type == ScriptExecutorType::Custom) {
$service->updateCustomExecutor($scriptExecutor);
if (config('script-runner-microservice.enabled') && $scriptExecutor->type?->isCustomOrRealtime()) {
try {
$service->updateCustomExecutor($scriptExecutor);
} catch (RequestException $e) {
$this->throwMicroserviceError($e);
}
} else {
if (!empty($scriptExecutor->getChanges())) {
ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original, $scriptExecutor->getChanges());
Expand DownExpand Up@@ -274,6 +287,28 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM
return ['status' => 'done'];
}

/**
* Report a script microservice rejection on the form, since the build itself
* is only reported over the websocket channel.
*
* @param RequestException $e Failed script microservice response
*
* @return never
*
* @throws ValidationException|RequestException
*/
private function throwMicroserviceError(RequestException $e): never
{
if (!$e->response->clientError()) {
throw $e;
}

$detail = $e->response->json('detail');
$message = is_string($detail) && $detail !== '' ? $detail : $e->getMessage();

throw ValidationException::withMessages(['language' => [$message]]);
}

private function checkAuth($request)
{
if (!config('app.custom_executors')) {
Expand DownExpand Up@@ -381,11 +416,30 @@ public function availableLanguages()
$languages[] = [
'value' => $key,
'text' => $config['name'],
'language' => $key,
'realtime' => false,
'initDockerfile' => ScriptExecutor::initDockerfile($key),
'configExample' => '',
];
}
}

foreach (['php', 'python', 'javascript'] as $language) {
$dockerfilePath = resource_path("script-executors/realtime/{$language}.Dockerfile");
if (!file_exists($dockerfilePath)) {
continue;
}
$label = $language === 'javascript' ? 'nodejs (realtime)' : "{$language} (realtime)";
$languages[] = [
'value' => "{$language}-realtime",
'text' => $label,
'language' => $language,
'realtime' => true,
'initDockerfile' => '',
'configExample' => file_get_contents($dockerfilePath),
];
}

return ['languages' => $languages];
}
}
3 changes: 2 additions & 1 deletion ProcessMaker/Jobs/ErrorHandling.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,7 +83,8 @@ private function requeue($job)
);
}
$newJob->delay($this->retryWaitTime());
$newJob->onQueue('bpmn');
$newJob->onConnection($job->connection);
$newJob->onQueue($job->queue ?? 'bpmn');
dispatch($newJob);
}

Expand Down
25 changes: 17 additions & 8 deletions ProcessMaker/Jobs/TestScript.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,10 +7,12 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\Services\ScriptMicroserviceService;
use Throwable;

class TestScript implements ShouldQueue
Expand All@@ -35,8 +37,8 @@ class TestScript implements ShouldQueue
/**
* Create a new job instance to execute a script.
*
* @param ProcessMaker\Models\Script $script
* @param ProcessMaker\Models\User $current_user
* @param Script $script
* @param User $current_user
* @param string $code
* @param array $data
* @param array $configuration
Expand All@@ -58,24 +60,31 @@ public function __construct(Script $script, User $current_user, $code, array $da
*/
public function handle()
{
$startTime = microtime(true);
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;
$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
'start_time' => microtime(true),
];
$response = $this->script->runScript($this->data, $this->configuration, '', null, 0, $metadata);
\Log::debug('Response from runScript: ' . print_r($response, true));
Log::debug('Response from runScript: ' . print_r($response, true));

if (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response);
if ($this->script->scriptExecutor->type === ScriptExecutorType::Realtime) {
// Realtime executors return the microservice payload synchronously rather than
// posting back to the callback endpoint, so format it the same way here.
$formatted = (new ScriptMicroserviceService())->formatPreviewResponse($response);
$this->sendResponse($formatted['status'], $formatted['output'], (microtime(true) - $startTime));
} elseif (!config('script-runner-microservice.enabled')) {
$this->sendResponse(200, $response, (microtime(true) - $startTime));
}
} catch (Throwable $exception) {
$this->sendResponse(500, [
'exception' => get_class($exception),
'message' => $exception->getMessage(),
]);
], (microtime(true) - $startTime));
}
}

Expand All@@ -85,8 +94,8 @@ public function handle()
* @param int $status
* @param array $response
*/
private function sendResponse($status, array $response)
private function sendResponse($status, array $response, float $duration)
{
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce));
event(new ScriptResponseEvent($this->current_user, $status, $response, null, $this->nonce, $duration));
}
}
1 change: 1 addition & 0 deletions ProcessMaker/Models/Script.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout =
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}
$metadata['start_time'] = microtime(true);

return $runner->run($this->code, $data, $config, $timeout, $user, $sync, $metadata);
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/ScriptExecutor.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="config", type="string"),
* @OA\Property(property="is_system", type="boolean"),
* @OA\Property(property="type", type="string"),
* ),
* @OA\Schema(
* schema="scriptExecutors",
Expand DownExpand Up@@ -177,6 +178,12 @@ public static function rules($existing = null)
});
}

// Realtime executors may use php/python/javascript even if a package is not installed
$allowedLanguages = array_values(array_unique(array_merge(
$allowedLanguages,
['php', 'python', 'javascript']
)));

return [
'title' => 'required',
'language' => [
Expand Down
11 changes: 10 additions & 1 deletion ProcessMaker/Nayra/Managers/WorkflowManagerDefault.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
use ProcessMaker\BpmnEngine;
use ProcessMaker\Contracts\ServiceTaskImplementationInterface;
use ProcessMaker\Contracts\WorkflowManagerInterface;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Jobs\BoundaryEvent;
use ProcessMaker\Jobs\CallProcess;
use ProcessMaker\Jobs\CatchEvent;
Expand All@@ -23,6 +24,7 @@
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken as Token;
use ProcessMaker\Models\Script;
use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface;
Expand DownExpand Up@@ -231,7 +233,14 @@ public function runScripTask(ScriptTaskInterface $scriptTask, Token $token)

$instance = $token->processRequest;
$process = $instance->process;
RunScriptTask::dispatch($process, $instance, $token, [])->onQueue('bpmn');
$job = new RunScriptTask($process, $instance, $token, []);
$script = Script::find($scriptTask->getProperty('scriptRef'));

if ($script?->scriptExecutor?->type === ScriptExecutorType::Realtime) {
$job->onConnection('redis-realtime')->onQueue('realtime');
}

dispatch($job);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
$scriptRunner = $this->service->getScriptRunner(
$this->language,
$this->script->scriptExecutor->uuid,
$this->script->scriptExecutor->type === ScriptExecutorType::Custom
$this->script->scriptExecutor->type?->isCustomOrRealtime()
);

if (!$scriptRunner) {
Expand All@@ -62,7 +62,8 @@ public function run($code, array $data, array $config, $timeout, $user, $sync, $
'callback_token' => $environmentVariables['API_TOKEN'],
'debug' => true,
'timeout' => $timeout,
'sync' => $sync,
// Realtime executors always run synchronously (service ignores sync too)
'sync' => $this->script->scriptExecutor->type === ScriptExecutorType::Realtime ? true : $sync,
];

Log::debug('Payload: ' . print_r($payload, true));
Expand Down
Loading
Loading