11<?php
2+
23namespace CatPaw \Core ;
34
45use function Amp \async ;
56use function Amp \ByteStream \getStdin ;
7+
8+ use Amp \DeferredFuture ;
9+ use function Amp \delay ;
10+ use function Amp \File \isDirectory ;
611use CatPaw \Core \Implementations \Environment \SimpleEnvironment ;
712use CatPaw \Core \Interfaces \EnvironmentInterface ;
813use Error ;
14+ use function preg_split ;
915use Psr \Log \LoggerInterface ;
16+ use function realpath ;
1017use ReflectionFunction ;
1118use Revolt \EventLoop ;
1219use Throwable ;
1320
1421class 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}
0 commit comments