Skip to content

Commit e530649

Browse files
committed
chore(app): adding spawner back, removing deprecated flags and listening to stdin instead of filesystem.
1 parent 20cfb71 commit e530649

File tree

3 files changed

+245
-28
lines changed

3 files changed

+245
-28
lines changed

src/lib/Core/Bootstrap.php

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
<?php
2+
23
namespace CatPaw\Core;
34

45
use function Amp\async;
56
use function Amp\ByteStream\getStdin;
7+
8+
use Amp\DeferredFuture;
9+
use function Amp\delay;
10+
use function Amp\File\isDirectory;
611
use CatPaw\Core\Implementations\Environment\SimpleEnvironment;
712
use CatPaw\Core\Interfaces\EnvironmentInterface;
813
use Error;
14+
use function preg_split;
915
use Psr\Log\LoggerInterface;
16+
use function realpath;
1017
use ReflectionFunction;
1118
use Revolt\EventLoop;
1219
use Throwable;
1320

1421
class Bootstrap {
1522
private function __construct() {
1623
}
24+
1725

1826
/**
1927
* Initialize an application from a source file (that usually defines a global "main" function).
@@ -84,7 +92,7 @@ public static function start(
8492
$env->set('MAIN', $main);
8593
$env->set('LIBRARIES', $libraries);
8694
$env->set('RESOURCES', $resources);
87-
$env->set('DIE_ON_CHANGE', $dieOnStdin);
95+
$env->set('DIE_ON_STDIN', $dieOnStdin);
8896

8997
if ($environment) {
9098
$env->withFileName($environment);
@@ -111,6 +119,9 @@ public static function start(
111119
}
112120

113121
if ($dieOnStdin) {
122+
if (isPhar()) {
123+
self::kill("Watch mode is intended for development only, compiled phar applications cannot watch files for changes.");
124+
}
114125
async(function() {
115126
getStdin()->read();
116127
self::kill("Killing application...", 0);
@@ -165,4 +176,190 @@ public static function kill(false|string|Error $error = false, false|int $code =
165176
die($code);
166177
}
167178
}
179+
180+
/**
181+
* @param string $spawner
182+
* @param string $fileName
183+
* @param array<string> $arguments
184+
* @return void
185+
*/
186+
public static function spawn(
187+
string $spawner,
188+
string $fileName,
189+
array $arguments,
190+
):void {
191+
try {
192+
EventLoop::onSignal(SIGHUP, static fn () => self::kill("Killing application..."));
193+
EventLoop::onSignal(SIGINT, static fn () => self::kill("Killing application..."));
194+
EventLoop::onSignal(SIGQUIT, static fn () => self::kill("Killing application..."));
195+
EventLoop::onSignal(SIGTERM, static fn () => self::kill("Killing application..."));
196+
197+
async(static function() use (
198+
$spawner,
199+
$fileName,
200+
$arguments,
201+
) {
202+
if (!Container::isProvided(LoggerInterface::class)) {
203+
$logger = LoggerFactory::create()->unwrap($error);
204+
if ($error) {
205+
return error($error);
206+
}
207+
Container::provide(LoggerInterface::class, $logger);
208+
} else {
209+
$logger = Container::get(LoggerInterface::class)->unwrap($error);
210+
if ($error) {
211+
return error($error);
212+
}
213+
}
214+
215+
foreach ($arguments as &$argument) {
216+
$parts = preg_split('/=|\s/', $argument, 2);
217+
if (count($parts) < 2) {
218+
continue;
219+
}
220+
221+
$left = $parts[0];
222+
$right = $parts[1];
223+
$slashed = addslashes($right);
224+
$argument = "$left=\"$slashed\"";
225+
}
226+
227+
$argumentsStringified = join(' ', $arguments);
228+
$instruction = "$spawner $fileName $argumentsStringified";
229+
230+
echo "Spawning $instruction".PHP_EOL;
231+
232+
if (DIRECTORY_SEPARATOR === '/') {
233+
EventLoop::onSignal(SIGINT, static function() {
234+
self::kill();
235+
});
236+
}
237+
238+
$ready = new DeferredFuture;
239+
$kill = new Signal;
240+
241+
async(function() use (&$ready, &$kill) {
242+
$stdin = getStdin();
243+
$ready->complete();
244+
while (true) {
245+
$content = $stdin->read();
246+
if (!$content) {
247+
delay(1);
248+
continue;
249+
}
250+
$kill->send();
251+
if (!$ready->isComplete()) {
252+
$ready->complete();
253+
}
254+
}
255+
});
256+
257+
while (true) {
258+
$ready->getFuture()->await();
259+
$code = Process::execute($instruction, out(), kill: $kill)->unwrap($error);
260+
if ($error || $code > 0 && 137 !== $code) {
261+
echo $error.PHP_EOL;
262+
$ready = new DeferredFuture;
263+
}
264+
}
265+
});
266+
267+
EventLoop::run();
268+
} catch (Throwable $error) {
269+
self::kill($error);
270+
}
271+
}
272+
273+
/**
274+
* Start a watcher which will detect file changes.
275+
* Useful for development mode.
276+
* @param string $main
277+
* @param array<string> $libraries
278+
* @param array<string> $resources
279+
* @param callable $function
280+
* @return void
281+
*/
282+
private static function onFileChange(
283+
string $main,
284+
array $libraries,
285+
array $resources,
286+
callable $function,
287+
):void {
288+
async(function() use (
289+
$main,
290+
$libraries,
291+
$resources,
292+
$function,
293+
) {
294+
$changes = [];
295+
$firstPass = true;
296+
297+
while (true) {
298+
clearstatcache();
299+
$countLastPass = count($changes);
300+
301+
$fileNames = match ($main) {
302+
'' => [],
303+
default => [$main => false]
304+
};
305+
/** @var array<string> $files */
306+
$files = [...$libraries, ...$resources];
307+
308+
foreach ($files as $file) {
309+
if (!File::exists($file)) {
310+
continue;
311+
}
312+
313+
if (!isDirectory($file)) {
314+
$fileNames[$file] = false;
315+
continue;
316+
}
317+
318+
$directory = $file;
319+
320+
$flatList = Directory::flat(realpath($directory))->unwrap($error);
321+
322+
if ($error) {
323+
return error($error);
324+
}
325+
326+
foreach ($flatList as $fileName) {
327+
$fileNames[$fileName] = false;
328+
}
329+
}
330+
331+
332+
$countThisPass = count($fileNames);
333+
if (!$firstPass && $countLastPass !== $countThisPass) {
334+
$function();
335+
}
336+
337+
foreach (array_keys($fileNames) as $fileName) {
338+
if (!File::exists($fileName)) {
339+
unset($changes[$fileName]);
340+
continue;
341+
}
342+
343+
$mtime = filemtime($fileName);
344+
345+
if (false === $mtime) {
346+
return error("Could not read file $fileName modification time.");
347+
}
348+
349+
if (!isset($changes[$fileName])) {
350+
$changes[$fileName] = $mtime;
351+
continue;
352+
}
353+
354+
if ($changes[$fileName] !== $mtime) {
355+
$changes[$fileName] = $mtime;
356+
$function();
357+
}
358+
}
359+
360+
$firstPass = false;
361+
delay(2);
362+
}
363+
});
364+
}
168365
}

src/lib/Core/Commands/ApplicationCommand.php

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,23 @@
1111
use CatPaw\Core\Result;
1212

1313
readonly class ApplicationCommand implements CommandInterface {
14+
public function __construct(private string $startFileName) {
15+
}
16+
1417
public function build(CommandBuilder $builder):void {
1518
$builder->optional('m', 'main');
1619
$builder->optional('p', 'php');
17-
$builder->optional('i', 'initializer');
1820
$builder->optional('s', 'spawner');
1921
$builder->optional('e', 'environment');
2022
$builder->optional('n', 'name');
21-
$builder->optional('d', "die-on-stdin");
22-
$builder->optional('w', "watch");
2323
$builder->optional('l', 'libraries');
2424
$builder->optional('r', 'resources');
2525
}
2626

2727
public function run(CommandContext $context):Result {
28-
$dieOnStdin = (bool)$context->get('die-on-stdin');
28+
global $argv;
29+
30+
$spawner = $context->get('spawner')?:$context->get('php')?:'';
2931
$name = $context->get('name')?:'App';
3032
$main = $context->get('main')?:'';
3133
$libraries = explode(',', $context->get('libraries')?:'');
@@ -63,15 +65,32 @@ public function run(CommandContext $context):Result {
6365
$environment = realpath($environment);
6466
}
6567

66-
Bootstrap::start(
67-
main: $main,
68-
name: $name,
69-
libraries: $libraries,
70-
resources: $resources,
71-
environment: $environment,
72-
dieOnStdin: $dieOnStdin,
73-
);
74-
68+
if ($spawner) {
69+
$arguments = array_filter(array_slice($argv, 1), function($option) {
70+
if (str_starts_with(trim($option), '--spawner')) {
71+
return false;
72+
}
73+
74+
if (str_starts_with(trim($option), '-s')) {
75+
return false;
76+
}
77+
78+
return true;
79+
});
80+
Bootstrap::spawn(
81+
spawner: $spawner?:'/usr/bin/php',
82+
fileName: $this->startFileName,
83+
arguments: $arguments,
84+
);
85+
} else {
86+
Bootstrap::start(
87+
main: $main,
88+
name: $name,
89+
libraries: $libraries,
90+
resources: $resources,
91+
environment: $environment,
92+
);
93+
}
7594
return ok();
7695
}
7796
}

src/lib/Core/Process.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ public static function execute(
2929
return error($error);
3030
}
3131
$process = AmpProcess::start($command, $workDirectory?:null);
32+
33+
if ($kill) {
34+
$kill->listen(static function() use ($process, $logger) {
35+
if (!$process->isRunning()) {
36+
return;
37+
}
38+
39+
try {
40+
$process->signal(9);
41+
} catch(Throwable $error) {
42+
$logger->error($error);
43+
}
44+
});
45+
}
46+
3247
if ($output) {
3348
pipe($process->getStdout(), $output);
3449
pipe($process->getStderr(), $output);
@@ -38,20 +53,6 @@ public static function execute(
3853
return error($error);
3954
}
4055

41-
if ($kill) {
42-
$kill->listen(static function() use ($process, $logger) {
43-
if (!$process->isRunning()) {
44-
return;
45-
}
46-
47-
try {
48-
$process->signal(9);
49-
} catch(Throwable $error) {
50-
$logger->error($error);
51-
}
52-
});
53-
}
54-
5556
return ok($code);
5657
}
5758

0 commit comments

Comments
 (0)