Skip to content

Commit d96e67f

Browse files
Add in-process script preview functionality
- Enhanced ScriptController to support a new `run_in_process` property for script previews, allowing execution via an in-process PHP runner when enabled. - Updated TestScript job to handle the new `runInProcess` parameter, enabling direct script execution without Docker. - Introduced `shouldPreviewInProcess` method to determine if in-process execution is applicable based on script properties and request flags. - Added comprehensive unit tests to validate in-process execution scenarios and ensure proper handling of configuration and execution context. - Implemented new test cases for CoreServiceTask to verify in-process script execution and error handling for various conditions.
1 parent 2b21390 commit d96e67f

8 files changed

Lines changed: 726 additions & 4 deletions

File tree

‎ProcessMaker/Http/Controllers/Api/ScriptController.php‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ public function index(Request $request)
173173
* property="nonce",
174174
* type="string",
175175
* ),
176+
* @OA\Property(
177+
* property="run_in_process",
178+
* type="boolean",
179+
* description="When true (and Core Service Task enabled), preview via in-process PHP runner instead of Docker/microservice. Also auto-enabled when the script has allow_in_process=true.",
180+
* ),
176181
* ),
177182
* ),
178183
*
@@ -189,10 +194,31 @@ public function preview(Request $request, Script $script)
189194
$config = $this->getRequestArray($request->get('config'));
190195
$code = $request->get('code');
191196
$nonce = $request->get('nonce');
197+
$runInProcess = $this->shouldPreviewInProcess($request, $script);
198+
199+
TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce, $runInProcess)->onQueue('bpmn');
200+
201+
return ['status' => 'success', 'run_in_process' => $runInProcess];
202+
}
203+
204+
/**
205+
* Decide if script preview should use Core Service Task in-process runner.
206+
*/
207+
private function shouldPreviewInProcess(Request $request, Script $script): bool
208+
{
209+
if (!config('core-service-task.enabled')) {
210+
return false;
211+
}
192212

193-
TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
213+
if (strtolower((string) $script->language) !== 'php') {
214+
return false;
215+
}
216+
217+
if ($request->boolean('run_in_process')) {
218+
return true;
219+
}
194220

195-
return ['status' => 'success'];
221+
return (bool) $script->allow_in_process;
196222
}
197223

198224
/**

‎ProcessMaker/Jobs/TestScript.php‎

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
use Illuminate\Queue\SerializesModels;
1010
use ProcessMaker\Enums\ScriptExecutorType;
1111
use ProcessMaker\Events\ScriptResponseEvent;
12+
use ProcessMaker\Exception\ConfigurationException;
13+
use ProcessMaker\Facades\ScriptRuntime;
1214
use ProcessMaker\Models\Script;
1315
use ProcessMaker\Models\User;
16+
use ProcessMaker\ScriptRunners\InProcessPhpRunner;
17+
use ProcessMaker\ScriptRuntime\ScriptExecutionContext;
1418
use Throwable;
1519

1620
class TestScript implements ShouldQueue
@@ -32,6 +36,8 @@ class TestScript implements ShouldQueue
3236

3337
protected $nonce;
3438

39+
protected bool $runInProcess;
40+
3541
/**
3642
* Create a new job instance to execute a script.
3743
*
@@ -40,15 +46,25 @@ class TestScript implements ShouldQueue
4046
* @param string $code
4147
* @param array $data
4248
* @param array $configuration
49+
* @param string|null $nonce
50+
* @param bool $runInProcess Use Core Service Task in-process runner (no Docker)
4351
*/
44-
public function __construct(Script $script, User $current_user, $code, array $data, array $configuration, $nonce = null)
45-
{
52+
public function __construct(
53+
Script $script,
54+
User $current_user,
55+
$code,
56+
array $data,
57+
array $configuration,
58+
$nonce = null,
59+
bool $runInProcess = false
60+
) {
4661
$this->script = $script;
4762
$this->current_user = $current_user;
4863
$this->code = $code;
4964
$this->data = $data;
5065
$this->configuration = $configuration;
5166
$this->nonce = $nonce;
67+
$this->runInProcess = $runInProcess;
5268
}
5369

5470
/**
@@ -61,6 +77,15 @@ public function handle()
6177
try {
6278
// Just set the code but do not save the object (preview only)
6379
$this->script->code = $this->code;
80+
81+
if ($this->runInProcess) {
82+
$response = $this->runInProcessPreview();
83+
\Log::debug('Response from in-process preview: ' . print_r($response, true));
84+
$this->sendResponse(200, $response);
85+
86+
return;
87+
}
88+
6489
$metadata = [
6590
'nonce' => $this->nonce,
6691
'current_user' => $this->current_user?->id,
@@ -80,6 +105,60 @@ public function handle()
80105
}
81106
}
82107

108+
/**
109+
* Preview using ScriptRuntime modules (default) or bare InProcessPhpRunner.
110+
*
111+
* @return array{output: mixed}
112+
*/
113+
private function runInProcessPreview(): array
114+
{
115+
if (!config('core-service-task.enabled')) {
116+
throw new ConfigurationException(
117+
'Core Service Task is disabled. Set CORE_SERVICE_TASK_ENABLED=true to enable.'
118+
);
119+
}
120+
121+
if (strtolower((string) $this->script->language) !== 'php') {
122+
throw new ConfigurationException('In-process preview only supports PHP scripts');
123+
}
124+
125+
$user = User::find($this->script->run_as_user_id);
126+
if (!$user) {
127+
throw new ConfigurationException('A user is required to run scripts');
128+
}
129+
130+
$timeout = (int) ($this->script->timeout ?: config('core-service-task.default_timeout', 60));
131+
$max = (int) config('core-service-task.max_timeout', 300);
132+
if ($max > 0 && $timeout > $max) {
133+
$timeout = $max;
134+
}
135+
$timeout = max(1, $timeout);
136+
137+
if (config('core-service-task.execution', 'modules') === 'modules') {
138+
$context = new ScriptExecutionContext(
139+
data: $this->data,
140+
config: $this->configuration,
141+
user: $user,
142+
tokenId: '',
143+
timeout: $timeout,
144+
scriptId: (int) $this->script->id,
145+
source: 'preview',
146+
);
147+
$output = ScriptRuntime::run($this->code, $context);
148+
} else {
149+
$output = app(InProcessPhpRunner::class)->run(
150+
$this->code,
151+
$this->data,
152+
$this->configuration,
153+
$timeout,
154+
$user
155+
);
156+
}
157+
158+
// Match runScript() response shape expected by the Script Editor UI
159+
return ['output' => $output];
160+
}
161+
83162
/**
84163
* Send a response to the user interface
85164
*

‎tests/Feature/Api/ScriptsTest.php‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,69 @@ public function testPreviewScriptFail()
422422
});
423423
}
424424

425+
/**
426+
* Preview via Core Service Task in-process runner when allow_in_process is set.
427+
*/
428+
public function testPreviewScriptInProcess()
429+
{
430+
config(['core-service-task.enabled' => true]);
431+
$this->withPersonalAccessClient();
432+
433+
Event::fake([
434+
ScriptResponseEvent::class,
435+
]);
436+
437+
$script = $this->getScript('php');
438+
$script->allow_in_process = true;
439+
$script->saveOrFail();
440+
441+
$url = route('api.scripts.preview', $script->id);
442+
$response = $this->apiCall('POST', $url, [
443+
'data' => '{"ping":"1"}',
444+
'code' => '<?php return ["pong" => $data["ping"]];',
445+
'nonce' => 'in-process-nonce',
446+
]);
447+
$response->assertStatus(200);
448+
$response->assertJson(['status' => 'success', 'run_in_process' => true]);
449+
450+
Event::assertDispatched(ScriptResponseEvent::class, function ($event) {
451+
return ($event->response['output']['pong'] ?? null) === '1'
452+
&& $event->nonce === 'in-process-nonce';
453+
});
454+
}
455+
456+
/**
457+
* Explicit run_in_process request flag for preview (before saving allow_in_process).
458+
*/
459+
public function testPreviewScriptInProcessWithRequestFlag()
460+
{
461+
config(['core-service-task.enabled' => true]);
462+
$this->withPersonalAccessClient();
463+
464+
Event::fake([
465+
ScriptResponseEvent::class,
466+
]);
467+
468+
$script = $this->getScript('php');
469+
$script->allow_in_process = false;
470+
$script->saveOrFail();
471+
472+
$url = route('api.scripts.preview', $script->id);
473+
$response = $this->apiCall('POST', $url, [
474+
'data' => '{}',
475+
'code' => '<?php return ["via" => "flag"];',
476+
'nonce' => 'flag-nonce',
477+
'run_in_process' => true,
478+
]);
479+
$response->assertStatus(200);
480+
$response->assertJson(['run_in_process' => true]);
481+
482+
Event::assertDispatched(ScriptResponseEvent::class, function ($event) {
483+
return ($event->response['output']['via'] ?? null) === 'flag'
484+
&& $event->nonce === 'flag-nonce';
485+
});
486+
}
487+
425488
/**
426489
* Delete script in process
427490
*/
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace Tests\Unit\ScriptRunners;
4+
5+
use ProcessMaker\Exception\ScriptException;
6+
use ProcessMaker\Exception\ScriptTimeoutException;
7+
use ProcessMaker\Models\User;
8+
use ProcessMaker\ScriptRunners\InProcessPhpRunner;
9+
use Tests\TestCase;
10+
11+
class InProcessPhpRunnerTest extends TestCase
12+
{
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
$this->withPersonalAccessClient();
17+
}
18+
19+
public function testRunsPhpScriptAndReturnsArray(): void
20+
{
21+
$runner = new InProcessPhpRunner();
22+
$user = User::factory()->create();
23+
24+
$output = $runner->run(
25+
'<?php return ["pong" => $data["ping"], "cfg" => $config["mode"] ?? null];',
26+
['ping' => 'hello'],
27+
['mode' => 'test'],
28+
30,
29+
$user
30+
);
31+
32+
$this->assertSame('hello', $output['pong']);
33+
$this->assertSame('test', $output['cfg']);
34+
}
35+
36+
public function testThrowsOnInvalidPhp(): void
37+
{
38+
$this->expectException(ScriptException::class);
39+
40+
$runner = new InProcessPhpRunner();
41+
$runner->run(
42+
'<?php this is not valid php;;;',
43+
[],
44+
[],
45+
30,
46+
User::factory()->create()
47+
);
48+
}
49+
50+
public function testRespectsTimeout(): void
51+
{
52+
$this->expectException(ScriptTimeoutException::class);
53+
54+
$runner = new InProcessPhpRunner();
55+
$runner->run(
56+
'<?php sleep(5); return ["ok" => true];',
57+
[],
58+
[],
59+
1,
60+
User::factory()->create()
61+
);
62+
}
63+
64+
public function testExposesApiHostAndTokenViaGetenv(): void
65+
{
66+
config(['app.docker_host_url' => 'https://pm.example.test']);
67+
68+
$runner = new InProcessPhpRunner();
69+
$output = $runner->run(
70+
'<?php return [
71+
"host" => getenv("API_HOST"),
72+
"has_token" => is_string(getenv("API_TOKEN")) && getenv("API_TOKEN") !== "",
73+
"app_url" => getenv("APP_URL"),
74+
];',
75+
[],
76+
[],
77+
30,
78+
User::factory()->create()
79+
);
80+
81+
$this->assertSame('https://pm.example.test/api/1.0', $output['host']);
82+
$this->assertTrue($output['has_token']);
83+
$this->assertSame('https://pm.example.test', $output['app_url']);
84+
}
85+
}

0 commit comments

Comments
 (0)