Skip to content

Commit fcdcc06

Browse files
Child processes (#389)
* wip * Fix styling * cleanup * phpstan * Fix event * Fix event watcher * Fix facade * add events * Remove useless stubs * Fix styling * add some sanity tests * wip * add artisan shorthand * allow passing either a string or array * correct return type * flip arguments for consistency * tidy - remove unused class properties * remove unnecessary space escape * add optional arg to make the process persistent * improvements - ChildProcess instances can be used to interact with a process - get, all and restart are piped up * Fix styling * Update src/ChildProcess.php Co-authored-by: Simon Hamp <[email protected]> * feedback - tidy cwd default path * stub out php command tests * fix - tests after upstream merge * add php convenience method * wip - refactor * add phpdoc for facade methods * Update src/Facades/ChildProcess.php Co-authored-by: Simon Hamp <[email protected]> * remove exploding string commands * fix - return a fresh instance from the facade each time --------- Co-authored-by: simonhamp <[email protected]> Co-authored-by: gwleuverink <[email protected]> Co-authored-by: Willem Leuverink <[email protected]>
1 parent 2c89fba commit fcdcc06

10 files changed

+372
-3
lines changed

phpstan.neon.dist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,4 @@ parameters:
1010
tmpDir: build/phpstan
1111
checkOctaneCompatibility: true
1212
checkModelProperties: true
13-
checkMissingIterableValueType: false
1413

src/ChildProcess.php

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace Native\Laravel;
4+
5+
use Native\Laravel\Client\Client;
6+
7+
class ChildProcess
8+
{
9+
public readonly int $pid;
10+
11+
public readonly string $alias;
12+
13+
public readonly array $cmd;
14+
15+
public readonly ?string $cwd;
16+
17+
public readonly ?array $env;
18+
19+
public readonly bool $persistent;
20+
21+
public function __construct(protected Client $client) {}
22+
23+
public function get(?string $alias = null): ?static
24+
{
25+
$alias = $alias ?? $this->alias;
26+
27+
$process = $this->client->get("child-process/get/{$alias}")->json();
28+
29+
if (! $process) {
30+
return null;
31+
}
32+
33+
return $this->fromRuntimeProcess($process);
34+
}
35+
36+
public function all(): array
37+
{
38+
$processes = $this->client->get('child-process/')->json();
39+
40+
if (empty($processes)) {
41+
return [];
42+
}
43+
44+
$hydrated = [];
45+
46+
foreach ($processes as $alias => $process) {
47+
$hydrated[$alias] = (new static($this->client))
48+
->fromRuntimeProcess($process);
49+
}
50+
51+
return $hydrated;
52+
}
53+
54+
public function start(
55+
string|array $cmd,
56+
string $alias,
57+
?string $cwd = null,
58+
?array $env = null,
59+
bool $persistent = false
60+
): static {
61+
62+
$process = $this->client->post('child-process/start', [
63+
'alias' => $alias,
64+
'cmd' => (array) $cmd,
65+
'cwd' => $cwd ?? base_path(),
66+
'env' => $env,
67+
'persistent' => $persistent,
68+
])->json();
69+
70+
return $this->fromRuntimeProcess($process);
71+
}
72+
73+
public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self
74+
{
75+
$cmd = [PHP_BINARY, ...(array) $cmd];
76+
77+
return $this->start($cmd, $alias, env: $env, persistent: $persistent);
78+
}
79+
80+
public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self
81+
{
82+
$cmd = ['artisan', ...(array) $cmd];
83+
84+
return $this->php($cmd, $alias, env: $env, persistent: $persistent);
85+
}
86+
87+
public function stop(?string $alias = null): void
88+
{
89+
$this->client->post('child-process/stop', [
90+
'alias' => $alias ?? $this->alias,
91+
])->json();
92+
}
93+
94+
public function restart(?string $alias = null): ?static
95+
{
96+
$process = $this->client->post('child-process/restart', [
97+
'alias' => $alias ?? $this->alias,
98+
])->json();
99+
100+
if (! $process) {
101+
return null;
102+
}
103+
104+
return $this->fromRuntimeProcess($process);
105+
}
106+
107+
public function message(string $message, ?string $alias = null): static
108+
{
109+
$this->client->post('child-process/message', [
110+
'alias' => $alias ?? $this->alias,
111+
'message' => $message,
112+
])->json();
113+
114+
return $this;
115+
}
116+
117+
protected function fromRuntimeProcess($process): static
118+
{
119+
if (isset($process['pid'])) {
120+
$this->pid = $process['pid'];
121+
}
122+
123+
foreach ($process['settings'] as $key => $value) {
124+
$this->{$key} = $value;
125+
}
126+
127+
return $this;
128+
}
129+
}

src/Commands/MigrateCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ public function handle()
2222
{
2323
(new NativeServiceProvider($this->laravel))->rewriteDatabase();
2424

25-
parent::handle();
25+
return parent::handle();
2626
}
2727
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Native\Laravel\Events\ChildProcess;
4+
5+
use Illuminate\Broadcasting\Channel;
6+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
7+
use Illuminate\Foundation\Events\Dispatchable;
8+
use Illuminate\Queue\SerializesModels;
9+
10+
class ErrorReceived implements ShouldBroadcast
11+
{
12+
use Dispatchable, SerializesModels;
13+
14+
public function __construct(public string $alias, public mixed $data) {}
15+
16+
public function broadcastOn()
17+
{
18+
return [
19+
new Channel('nativephp'),
20+
];
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Native\Laravel\Events\ChildProcess;
4+
5+
use Illuminate\Broadcasting\Channel;
6+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
7+
use Illuminate\Foundation\Events\Dispatchable;
8+
use Illuminate\Queue\SerializesModels;
9+
10+
class MessageReceived implements ShouldBroadcast
11+
{
12+
use Dispatchable, SerializesModels;
13+
14+
public function __construct(public string $alias, public mixed $data) {}
15+
16+
public function broadcastOn()
17+
{
18+
return [
19+
new Channel('nativephp'),
20+
];
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Native\Laravel\Events\ChildProcess;
4+
5+
use Illuminate\Broadcasting\Channel;
6+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
7+
use Illuminate\Foundation\Events\Dispatchable;
8+
use Illuminate\Queue\SerializesModels;
9+
10+
class ProcessExited implements ShouldBroadcast
11+
{
12+
use Dispatchable, SerializesModels;
13+
14+
public function __construct(public string $alias, public int $code) {}
15+
16+
public function broadcastOn()
17+
{
18+
return [
19+
new Channel('nativephp'),
20+
];
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Native\Laravel\Events\ChildProcess;
4+
5+
use Illuminate\Broadcasting\Channel;
6+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
7+
use Illuminate\Foundation\Events\Dispatchable;
8+
use Illuminate\Queue\SerializesModels;
9+
10+
class ProcessSpawned implements ShouldBroadcast
11+
{
12+
use Dispatchable, SerializesModels;
13+
14+
public function __construct(public string $alias) {}
15+
16+
public function broadcastOn()
17+
{
18+
return [
19+
new Channel('nativephp'),
20+
];
21+
}
22+
}

src/Events/EventWatcher.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function register(): void
1313
{
1414
Event::listen('*', function (string $eventName, array $data) {
1515

16-
$event = $data[0] ?? null;
16+
$event = $data[0] ?? (object) null;
1717

1818
if (! method_exists($event, 'broadcastOn')) {
1919
return;

src/Facades/ChildProcess.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Native\Laravel\Facades;
4+
5+
use Illuminate\Support\Facades\Facade;
6+
use Native\Laravel\ChildProcess as Implement;
7+
8+
/**
9+
* @method static \Native\Laravel\ChildProcess[] all()
10+
* @method static \Native\Laravel\ChildProcess get(string $alias = null)
11+
* @method static \Native\Laravel\ChildProcess message(string $message, string $alias = null)
12+
* @method static \Native\Laravel\ChildProcess restart(string $alias = null)
13+
* @method static \Native\Laravel\ChildProcess start(string|array $cmd, string $alias, string $cwd = null, array $env = null, bool $persistent = false)
14+
* @method static \Native\Laravel\ChildProcess php(string|array $cmd, string $alias, array $env = null, bool $persistent = false)
15+
* @method static \Native\Laravel\ChildProcess artisan(string|array $cmd, string $alias, array $env = null, bool $persistent = false)
16+
* @method static void stop(string $alias = null)
17+
*/
18+
class ChildProcess extends Facade
19+
{
20+
protected static function getFacadeAccessor()
21+
{
22+
self::clearResolvedInstance(Implement::class);
23+
24+
return Implement::class;
25+
}
26+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
use Illuminate\Http\Client\Request;
4+
use Illuminate\Support\Facades\Http;
5+
use Mockery;
6+
use Native\Laravel\ChildProcess as ChildProcessImplement;
7+
use Native\Laravel\Client\Client;
8+
use Native\Laravel\Facades\ChildProcess;
9+
10+
beforeEach(function () {
11+
Http::fake();
12+
13+
$mock = Mockery::mock(ChildProcessImplement::class, [resolve(Client::class)])
14+
->makePartial()
15+
->shouldAllowMockingProtectedMethods();
16+
17+
$this->instance(ChildProcessImplement::class, $mock->allows([
18+
'fromRuntimeProcess' => $mock,
19+
]));
20+
});
21+
22+
it('can start a child process', function () {
23+
ChildProcess::start('foo bar', 'some-alias', 'path/to/dir', ['baz' => 'zah']);
24+
25+
Http::assertSent(function (Request $request) {
26+
return $request->url() === 'http://localhost:4000/api/child-process/start' &&
27+
$request['alias'] === 'some-alias' &&
28+
$request['cmd'] === ['foo bar'] &&
29+
$request['cwd'] === 'path/to/dir' &&
30+
$request['env'] === ['baz' => 'zah'];
31+
});
32+
});
33+
34+
it('can start a php command', function () {
35+
ChildProcess::php("-r 'sleep(5);'", 'some-alias', ['baz' => 'zah']);
36+
37+
Http::assertSent(function (Request $request) {
38+
return $request->url() === 'http://localhost:4000/api/child-process/start' &&
39+
$request['alias'] === 'some-alias' &&
40+
$request['cmd'] === [PHP_BINARY, "-r 'sleep(5);'"] &&
41+
$request['cwd'] === base_path() &&
42+
$request['env'] === ['baz' => 'zah'];
43+
});
44+
});
45+
46+
it('can start a artisan command', function () {
47+
ChildProcess::artisan('foo:bar --verbose', 'some-alias', ['baz' => 'zah']);
48+
49+
Http::assertSent(function (Request $request) {
50+
return $request->url() === 'http://localhost:4000/api/child-process/start' &&
51+
$request['alias'] === 'some-alias' &&
52+
$request['cmd'] === [PHP_BINARY, 'artisan', 'foo:bar --verbose'] &&
53+
$request['cwd'] === base_path() &&
54+
$request['env'] === ['baz' => 'zah'];
55+
});
56+
});
57+
58+
it('accepts either a string or a array as start command argument', function () {
59+
ChildProcess::start('foo bar', 'some-alias');
60+
Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo bar']);
61+
62+
ChildProcess::start(['foo', 'baz'], 'some-alias');
63+
Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo', 'baz']);
64+
});
65+
66+
it('accepts either a string or a array as php command argument', function () {
67+
ChildProcess::php("-r 'sleep(5);'", 'some-alias');
68+
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, "-r 'sleep(5);'"]);
69+
70+
ChildProcess::php(['-r', "'sleep(5);'"], 'some-alias');
71+
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, '-r', "'sleep(5);'"]);
72+
});
73+
74+
it('accepts either a string or a array as artisan command argument', function () {
75+
ChildProcess::artisan('foo:bar', 'some-alias');
76+
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, 'artisan', 'foo:bar']);
77+
78+
ChildProcess::artisan(['foo:baz'], 'some-alias');
79+
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, 'artisan', 'foo:baz']);
80+
});
81+
82+
it('sets the cwd to the base path if none was given', function () {
83+
ChildProcess::start(['foo', 'bar'], 'some-alias', cwd: 'path/to/dir');
84+
Http::assertSent(fn (Request $request) => $request['cwd'] === 'path/to/dir');
85+
86+
ChildProcess::start(['foo', 'bar'], 'some-alias');
87+
Http::assertSent(fn (Request $request) => $request['cwd'] === base_path());
88+
});
89+
90+
it('can stop a child process', function () {
91+
ChildProcess::stop('some-alias');
92+
93+
Http::assertSent(function (Request $request) {
94+
return $request->url() === 'http://localhost:4000/api/child-process/stop' &&
95+
$request['alias'] === 'some-alias';
96+
});
97+
});
98+
99+
it('can send messages to a child process', function () {
100+
ChildProcess::message('some-message', 'some-alias');
101+
102+
Http::assertSent(function (Request $request) {
103+
return $request->url() === 'http://localhost:4000/api/child-process/message' &&
104+
$request['alias'] === 'some-alias' &&
105+
$request['message'] === 'some-message';
106+
});
107+
});
108+
109+
it('can mark a process as persistent', function () {
110+
ChildProcess::start('foo bar', 'some-alias', persistent: true);
111+
Http::assertSent(fn (Request $request) => $request['persistent'] === true);
112+
});
113+
114+
it('can mark a php command as persistent', function () {
115+
ChildProcess::php("-r 'sleep(5);'", 'some-alias', persistent: true);
116+
Http::assertSent(fn (Request $request) => $request['persistent'] === true);
117+
});
118+
119+
it('can mark a artisan command as persistent', function () {
120+
ChildProcess::artisan('foo:bar', 'some-alias', persistent: true);
121+
Http::assertSent(fn (Request $request) => $request['persistent'] === true);
122+
});
123+
124+
it('marks the process as non-persistent by default', function () {
125+
ChildProcess::start('foo bar', 'some-alias');
126+
Http::assertSent(fn (Request $request) => $request['persistent'] === false);
127+
});

0 commit comments

Comments
 (0)