From fcdcc06958cd8e6b23d1603fe18adf8ca1b2e9e2 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Thu, 31 Oct 2024 13:06:31 +0000 Subject: [PATCH] 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 * 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 * remove exploding string commands * fix - return a fresh instance from the facade each time --------- Co-authored-by: simonhamp Co-authored-by: gwleuverink Co-authored-by: Willem Leuverink --- phpstan.neon.dist | 1 - src/ChildProcess.php | 129 ++++++++++++++++++++ src/Commands/MigrateCommand.php | 2 +- src/Events/ChildProcess/ErrorReceived.php | 22 ++++ src/Events/ChildProcess/MessageReceived.php | 22 ++++ src/Events/ChildProcess/ProcessExited.php | 22 ++++ src/Events/ChildProcess/ProcessSpawned.php | 22 ++++ src/Events/EventWatcher.php | 2 +- src/Facades/ChildProcess.php | 26 ++++ tests/ChildProcess/ChildProcessTest.php | 127 +++++++++++++++++++ 10 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 src/ChildProcess.php create mode 100644 src/Events/ChildProcess/ErrorReceived.php create mode 100644 src/Events/ChildProcess/MessageReceived.php create mode 100644 src/Events/ChildProcess/ProcessExited.php create mode 100644 src/Events/ChildProcess/ProcessSpawned.php create mode 100644 src/Facades/ChildProcess.php create mode 100644 tests/ChildProcess/ChildProcessTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a91953b..260b5e1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,5 +10,4 @@ parameters: tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true - checkMissingIterableValueType: false diff --git a/src/ChildProcess.php b/src/ChildProcess.php new file mode 100644 index 0000000..ff80a07 --- /dev/null +++ b/src/ChildProcess.php @@ -0,0 +1,129 @@ +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; + } +} diff --git a/src/Commands/MigrateCommand.php b/src/Commands/MigrateCommand.php index ebf8522..9093d67 100644 --- a/src/Commands/MigrateCommand.php +++ b/src/Commands/MigrateCommand.php @@ -22,6 +22,6 @@ public function handle() { (new NativeServiceProvider($this->laravel))->rewriteDatabase(); - parent::handle(); + return parent::handle(); } } diff --git a/src/Events/ChildProcess/ErrorReceived.php b/src/Events/ChildProcess/ErrorReceived.php new file mode 100644 index 0000000..65db9c6 --- /dev/null +++ b/src/Events/ChildProcess/ErrorReceived.php @@ -0,0 +1,22 @@ +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); +});