Skip to content

Child processes #389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1176d51
wip
simonhamp Oct 20, 2024
4ac8294
Fix styling
simonhamp Oct 20, 2024
d55b42b
cleanup
simonhamp Oct 20, 2024
624fbc5
phpstan
simonhamp Oct 21, 2024
83d3706
Fix event
simonhamp Oct 22, 2024
927f6e5
Fix event watcher
simonhamp Oct 22, 2024
3d776c1
Fix facade
simonhamp Oct 22, 2024
a99d272
add events
simonhamp Oct 24, 2024
d06f13a
Remove useless stubs
simonhamp Oct 24, 2024
233675a
Fix styling
simonhamp Oct 24, 2024
6cf8488
add some sanity tests
gwleuverink Oct 25, 2024
1e673bb
wip
gwleuverink Oct 25, 2024
7a4f407
add artisan shorthand
gwleuverink Oct 25, 2024
6e4f7db
allow passing either a string or array
gwleuverink Oct 25, 2024
ee53a67
correct return type
gwleuverink Oct 25, 2024
8b342fd
flip arguments for consistency
gwleuverink Oct 29, 2024
bc46e8c
tidy - remove unused class properties
gwleuverink Oct 29, 2024
8af94f6
remove unnecessary space escape
gwleuverink Oct 29, 2024
58a5539
add optional arg to make the process persistent
gwleuverink Oct 29, 2024
ad8ef09
improvements
simonhamp Oct 29, 2024
62ce895
Fix styling
simonhamp Oct 29, 2024
e446a66
Merge branch 'feature/child-processes' into feature/child-processes
simonhamp Oct 29, 2024
160b867
Update src/ChildProcess.php
gwleuverink Oct 29, 2024
62b8faf
feedback - tidy cwd default path
gwleuverink Oct 29, 2024
246a236
stub out php command tests
gwleuverink Oct 29, 2024
31d78ae
fix - tests after upstream merge
gwleuverink Oct 29, 2024
8dbee1d
add php convenience method
gwleuverink Oct 29, 2024
4e9ce39
wip - refactor
gwleuverink Oct 29, 2024
2f76685
add phpdoc for facade methods
gwleuverink Oct 29, 2024
2153f1c
Update src/Facades/ChildProcess.php
gwleuverink Oct 29, 2024
c201b1b
remove exploding string commands
gwleuverink Oct 30, 2024
a8bf775
fix - return a fresh instance from the facade each time
gwleuverink Oct 31, 2024
c1b46f4
Merge pull request #392 from gwleuverink/feature/child-processes
simonhamp Oct 31, 2024
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
1 change: 0 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ parameters:
tmpDir: build/phpstan
checkOctaneCompatibility: true
checkModelProperties: true
checkMissingIterableValueType: false

129 changes: 129 additions & 0 deletions src/ChildProcess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Native\Laravel;

use Native\Laravel\Client\Client;

class ChildProcess
{
public readonly int $pid;

public readonly string $alias;

public readonly array $cmd;

public readonly ?string $cwd;

public readonly ?array $env;

public readonly bool $persistent;

public function __construct(protected Client $client) {}

public function get(?string $alias = null): ?static
{
$alias = $alias ?? $this->alias;

$process = $this->client->get("child-process/get/{$alias}")->json();

if (! $process) {
return null;
}

return $this->fromRuntimeProcess($process);
}

public function all(): array
{
$processes = $this->client->get('child-process/')->json();

if (empty($processes)) {
return [];
}

$hydrated = [];

foreach ($processes as $alias => $process) {
$hydrated[$alias] = (new static($this->client))
->fromRuntimeProcess($process);
}

return $hydrated;
}

public function start(
string|array $cmd,
string $alias,
?string $cwd = null,
?array $env = null,
bool $persistent = false
): static {

$process = $this->client->post('child-process/start', [
'alias' => $alias,
'cmd' => (array) $cmd,
'cwd' => $cwd ?? base_path(),
'env' => $env,
'persistent' => $persistent,
])->json();

return $this->fromRuntimeProcess($process);
}

public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self
{
$cmd = [PHP_BINARY, ...(array) $cmd];

return $this->start($cmd, $alias, env: $env, persistent: $persistent);
}

public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self
{
$cmd = ['artisan', ...(array) $cmd];

return $this->php($cmd, $alias, env: $env, persistent: $persistent);
}

public function stop(?string $alias = null): void
{
$this->client->post('child-process/stop', [
'alias' => $alias ?? $this->alias,
])->json();
}

public function restart(?string $alias = null): ?static
{
$process = $this->client->post('child-process/restart', [
'alias' => $alias ?? $this->alias,
])->json();

if (! $process) {
return null;
}

return $this->fromRuntimeProcess($process);
}

public function message(string $message, ?string $alias = null): static
{
$this->client->post('child-process/message', [
'alias' => $alias ?? $this->alias,
'message' => $message,
])->json();

return $this;
}

protected function fromRuntimeProcess($process): static
{
if (isset($process['pid'])) {
$this->pid = $process['pid'];
}

foreach ($process['settings'] as $key => $value) {
$this->{$key} = $value;
}

return $this;
}
}
2 changes: 1 addition & 1 deletion src/Commands/MigrateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public function handle()
{
(new NativeServiceProvider($this->laravel))->rewriteDatabase();

parent::handle();
return parent::handle();
}
}
22 changes: 22 additions & 0 deletions src/Events/ChildProcess/ErrorReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Native\Laravel\Events\ChildProcess;

use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ErrorReceived implements ShouldBroadcast
{
use Dispatchable, SerializesModels;

public function __construct(public string $alias, public mixed $data) {}

public function broadcastOn()
{
return [
new Channel('nativephp'),
];
}
}
22 changes: 22 additions & 0 deletions src/Events/ChildProcess/MessageReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Native\Laravel\Events\ChildProcess;

use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageReceived implements ShouldBroadcast
{
use Dispatchable, SerializesModels;

public function __construct(public string $alias, public mixed $data) {}

public function broadcastOn()
{
return [
new Channel('nativephp'),
];
}
}
22 changes: 22 additions & 0 deletions src/Events/ChildProcess/ProcessExited.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Native\Laravel\Events\ChildProcess;

use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ProcessExited implements ShouldBroadcast
{
use Dispatchable, SerializesModels;

public function __construct(public string $alias, public int $code) {}

public function broadcastOn()
{
return [
new Channel('nativephp'),
];
}
}
22 changes: 22 additions & 0 deletions src/Events/ChildProcess/ProcessSpawned.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Native\Laravel\Events\ChildProcess;

use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ProcessSpawned implements ShouldBroadcast
{
use Dispatchable, SerializesModels;

public function __construct(public string $alias) {}

public function broadcastOn()
{
return [
new Channel('nativephp'),
];
}
}
2 changes: 1 addition & 1 deletion src/Events/EventWatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function register(): void
{
Event::listen('*', function (string $eventName, array $data) {

$event = $data[0] ?? null;
$event = $data[0] ?? (object) null;

if (! method_exists($event, 'broadcastOn')) {
return;
Expand Down
26 changes: 26 additions & 0 deletions src/Facades/ChildProcess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Native\Laravel\Facades;

use Illuminate\Support\Facades\Facade;
use Native\Laravel\ChildProcess as Implement;

/**
* @method static \Native\Laravel\ChildProcess[] all()
* @method static \Native\Laravel\ChildProcess get(string $alias = null)
* @method static \Native\Laravel\ChildProcess message(string $message, string $alias = null)
* @method static \Native\Laravel\ChildProcess restart(string $alias = null)
* @method static \Native\Laravel\ChildProcess start(string|array $cmd, string $alias, string $cwd = null, array $env = null, bool $persistent = false)
* @method static \Native\Laravel\ChildProcess php(string|array $cmd, string $alias, array $env = null, bool $persistent = false)
* @method static \Native\Laravel\ChildProcess artisan(string|array $cmd, string $alias, array $env = null, bool $persistent = false)
* @method static void stop(string $alias = null)
*/
class ChildProcess extends Facade
{
protected static function getFacadeAccessor()
{
self::clearResolvedInstance(Implement::class);

return Implement::class;
}
}
127 changes: 127 additions & 0 deletions tests/ChildProcess/ChildProcessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Mockery;

Check warning on line 5 in tests/ChildProcess/ChildProcessTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L11.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'Mockery' has no effect

Check warning on line 5 in tests/ChildProcess/ChildProcessTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L10.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'Mockery' has no effect

Check warning on line 5 in tests/ChildProcess/ChildProcessTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L10.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'Mockery' has no effect

Check warning on line 5 in tests/ChildProcess/ChildProcessTest.php

View workflow job for this annotation

GitHub Actions / P8.2 - L11.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'Mockery' has no effect

Check warning on line 5 in tests/ChildProcess/ChildProcessTest.php

View workflow job for this annotation

GitHub Actions / P8.1 - L10.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'Mockery' has no effect

Check warning on line 5 in tests/ChildProcess/ChildProcessTest.php

View workflow job for this annotation

GitHub Actions / P8.1 - L10.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'Mockery' has no effect
use Native\Laravel\ChildProcess as ChildProcessImplement;
use Native\Laravel\Client\Client;
use Native\Laravel\Facades\ChildProcess;

beforeEach(function () {
Http::fake();

$mock = Mockery::mock(ChildProcessImplement::class, [resolve(Client::class)])
->makePartial()
->shouldAllowMockingProtectedMethods();

$this->instance(ChildProcessImplement::class, $mock->allows([
'fromRuntimeProcess' => $mock,
]));
});

it('can start a child process', function () {
ChildProcess::start('foo bar', 'some-alias', 'path/to/dir', ['baz' => 'zah']);

Http::assertSent(function (Request $request) {
return $request->url() === 'http://localhost:4000/api/child-process/start' &&
$request['alias'] === 'some-alias' &&
$request['cmd'] === ['foo bar'] &&
$request['cwd'] === 'path/to/dir' &&
$request['env'] === ['baz' => 'zah'];
});
});

it('can start a php command', function () {
ChildProcess::php("-r 'sleep(5);'", 'some-alias', ['baz' => 'zah']);

Http::assertSent(function (Request $request) {
return $request->url() === 'http://localhost:4000/api/child-process/start' &&
$request['alias'] === 'some-alias' &&
$request['cmd'] === [PHP_BINARY, "-r 'sleep(5);'"] &&
$request['cwd'] === base_path() &&
$request['env'] === ['baz' => 'zah'];
});
});

it('can start a artisan command', function () {
ChildProcess::artisan('foo:bar --verbose', 'some-alias', ['baz' => 'zah']);

Http::assertSent(function (Request $request) {
return $request->url() === 'http://localhost:4000/api/child-process/start' &&
$request['alias'] === 'some-alias' &&
$request['cmd'] === [PHP_BINARY, 'artisan', 'foo:bar --verbose'] &&
$request['cwd'] === base_path() &&
$request['env'] === ['baz' => 'zah'];
});
});

it('accepts either a string or a array as start command argument', function () {
ChildProcess::start('foo bar', 'some-alias');
Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo bar']);

ChildProcess::start(['foo', 'baz'], 'some-alias');
Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo', 'baz']);
});

it('accepts either a string or a array as php command argument', function () {
ChildProcess::php("-r 'sleep(5);'", 'some-alias');
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, "-r 'sleep(5);'"]);

ChildProcess::php(['-r', "'sleep(5);'"], 'some-alias');
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, '-r', "'sleep(5);'"]);
});

it('accepts either a string or a array as artisan command argument', function () {
ChildProcess::artisan('foo:bar', 'some-alias');
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, 'artisan', 'foo:bar']);

ChildProcess::artisan(['foo:baz'], 'some-alias');
Http::assertSent(fn (Request $request) => $request['cmd'] === [PHP_BINARY, 'artisan', 'foo:baz']);
});

it('sets the cwd to the base path if none was given', function () {
ChildProcess::start(['foo', 'bar'], 'some-alias', cwd: 'path/to/dir');
Http::assertSent(fn (Request $request) => $request['cwd'] === 'path/to/dir');

ChildProcess::start(['foo', 'bar'], 'some-alias');
Http::assertSent(fn (Request $request) => $request['cwd'] === base_path());
});

it('can stop a child process', function () {
ChildProcess::stop('some-alias');

Http::assertSent(function (Request $request) {
return $request->url() === 'http://localhost:4000/api/child-process/stop' &&
$request['alias'] === 'some-alias';
});
});

it('can send messages to a child process', function () {
ChildProcess::message('some-message', 'some-alias');

Http::assertSent(function (Request $request) {
return $request->url() === 'http://localhost:4000/api/child-process/message' &&
$request['alias'] === 'some-alias' &&
$request['message'] === 'some-message';
});
});

it('can mark a process as persistent', function () {
ChildProcess::start('foo bar', 'some-alias', persistent: true);
Http::assertSent(fn (Request $request) => $request['persistent'] === true);
});

it('can mark a php command as persistent', function () {
ChildProcess::php("-r 'sleep(5);'", 'some-alias', persistent: true);
Http::assertSent(fn (Request $request) => $request['persistent'] === true);
});

it('can mark a artisan command as persistent', function () {
ChildProcess::artisan('foo:bar', 'some-alias', persistent: true);
Http::assertSent(fn (Request $request) => $request['persistent'] === true);
});

it('marks the process as non-persistent by default', function () {
ChildProcess::start('foo bar', 'some-alias');
Http::assertSent(fn (Request $request) => $request['persistent'] === false);
});
Loading