Skip to content

Commit 8187d0d

Browse files
authored
Merge pull request #2 from bangnokia/feat/hot-reloading
LGTM
2 parents e493214 + 44b48d9 commit 8187d0d

11 files changed

Lines changed: 337 additions & 76 deletions

File tree

app/Commands/HttpServeCommand.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace BangNokia\Lina\Commands;
4+
5+
use LaravelZero\Framework\Commands\Command;
6+
use Symfony\Component\Process\PhpExecutableFinder;
7+
use Symfony\Component\Process\Process;
8+
9+
class HttpServeCommand extends Command
10+
{
11+
protected $signature = 'serve:http';
12+
13+
protected $description = 'Start simple web server for development';
14+
15+
protected $hidden = true;
16+
17+
protected int $portOffset = 0;
18+
19+
public function handle()
20+
{
21+
$this->call('clean');
22+
23+
$this->line("<info>Starting development server:</info> http://{$this->host()}:{$this->port()}");
24+
25+
$process = $this->startProcess();
26+
27+
while ($process->isRunning()) {
28+
usleep(0.5 * 1000000);
29+
}
30+
31+
$status = $process->getExitCode();
32+
33+
if ($status && $this->portOffset++ < 10) {
34+
$this->handle();
35+
}
36+
37+
return $status;
38+
}
39+
40+
protected function startProcess()
41+
{
42+
$process = new Process($this->serverCommand(), timeout: 0);
43+
44+
$process->start(function ($type, $data) {
45+
// $this->output->write($data);
46+
});
47+
48+
// Stop the server when the user hits Ctrl+C
49+
// to void the port in used error
50+
$this->trap(fn () => [SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2, SIGQUIT], function ($signal) use ($process) {
51+
if ($process->isRunning()) {
52+
$process->stop(10, $signal);
53+
}
54+
55+
exit;
56+
});
57+
58+
return $process;
59+
}
60+
61+
protected function serverCommand()
62+
{
63+
return [
64+
(new PhpExecutableFinder)->find(false),
65+
'-S',
66+
$this->host().':'.$this->port(),
67+
'-t',
68+
'public',
69+
base_path('server.php')
70+
];
71+
}
72+
73+
protected function host()
74+
{
75+
return '127.0.0.1';
76+
}
77+
78+
protected function port()
79+
{
80+
return 6969 + $this->portOffset;
81+
}
82+
}

app/Commands/ServeCommand.php

Lines changed: 14 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,79 +2,32 @@
22

33
namespace BangNokia\Lina\Commands;
44

5+
use Illuminate\Process\Pool;
6+
use Illuminate\Support\Facades\Process;
57
use LaravelZero\Framework\Commands\Command;
68
use Symfony\Component\Process\PhpExecutableFinder;
7-
use Symfony\Component\Process\Process;
89

910
class ServeCommand extends Command
1011
{
1112
protected $signature = 'serve';
1213

13-
protected $description = 'Start simple web server for development';
14+
protected $description = 'Start development server';
1415

15-
protected int $portOffset = 0;
16-
17-
public function handle()
16+
public function handle(): int
1817
{
19-
$this->call('clean');
20-
21-
$this->line("<info>Starting development server:</info> http://{$this->host()}:{$this->port()}");
22-
23-
$process = $this->startProcess();
18+
$phpBinary = (new PhpExecutableFinder())->find();
19+
// get the current php binary path which is running the command
20+
$pool = Process::pool(function (Pool $pool) use ($phpBinary) {
21+
$pool->path(getcwd())->command([$phpBinary, base_path('lina'), 'serve:http']);
22+
$pool->path(getcwd())->command([$phpBinary, base_path('lina'), 'serve:ws']);
23+
})->start(function (string $type, string $output, string $key) {
24+
$this->output->write($output);
25+
});
2426

25-
while ($process->isRunning()) {
27+
while ($pool->running()->isNotEmpty()) {
2628
usleep(0.5 * 1000000);
2729
}
2830

29-
$status = $process->getExitCode();
30-
31-
if ($status && $this->portOffset++ < 10) {
32-
$this->handle();
33-
}
34-
35-
return $status;
36-
}
37-
38-
protected function startProcess()
39-
{
40-
$process = new Process($this->serverCommand(), timeout: 0);
41-
42-
$process->start(function ($type, $data) {
43-
$this->output->write($data);
44-
});
45-
46-
// Stop the server when the user hits Ctrl+C
47-
// to void the port in used error
48-
$this->trap(fn () => [SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2, SIGQUIT], function ($signal) use ($process) {
49-
if ($process->isRunning()) {
50-
$process->stop(10, $signal);
51-
}
52-
53-
exit;
54-
});
55-
56-
return $process;
57-
}
58-
59-
protected function serverCommand()
60-
{
61-
return [
62-
(new PhpExecutableFinder)->find(false),
63-
'-S',
64-
$this->host().':'.$this->port(),
65-
'-t',
66-
'public',
67-
base_path('server.php')
68-
];
69-
}
70-
71-
protected function host()
72-
{
73-
return '127.0.0.1';
74-
}
75-
76-
protected function port()
77-
{
78-
return 6969 + $this->portOffset;
31+
$pool->wait();
7932
}
8033
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace BangNokia\Lina\Commands;
4+
5+
use BangNokia\Lina\Socket;
6+
use BangNokia\Lina\Watcher;
7+
use Illuminate\Support\Facades\Cache;
8+
use LaravelZero\Framework\Commands\Command;
9+
use Ratchet\Http\HttpServer;
10+
use Ratchet\Server\IoServer;
11+
use Ratchet\WebSocket\WsServer;
12+
use React\EventLoop\Loop;
13+
use React\EventLoop\LoopInterface;
14+
use Symfony\Component\Finder\Finder;
15+
use Ratchet\ConnectionInterface;
16+
use React\Socket\SocketServer as Reactor;
17+
18+
19+
class WebsocketServeCommand extends Command
20+
{
21+
protected $signature = 'serve:ws';
22+
23+
protected $description = 'Start websocket server for development';
24+
25+
protected $hidden = true;
26+
27+
protected LoopInterface $loop;
28+
29+
protected IoServer $server;
30+
31+
public static int $port = 9696;
32+
33+
public static int $portOffset = 0;
34+
35+
public function handle()
36+
{
37+
$this->loop = Loop::get();
38+
39+
$this->loop->futureTick(function () {
40+
$this->line("<info>Starting websocket server:</info> ws://{$this->host()}:{$this->port()}");
41+
});
42+
43+
$this
44+
->startWatcher()
45+
->startServer();
46+
}
47+
48+
protected function startWatcher(): static
49+
{
50+
$dirs = $this->dirs();
51+
52+
if (empty($dirs)) {
53+
$this->warn('No directory to watch, please check you are in the correct directory.');
54+
return $this;
55+
}
56+
57+
$finder = (new Finder())->files()->in($this->dirs());
58+
59+
(new Watcher($this->loop, $finder))
60+
->startWatching(function () {
61+
$this->info('Changes detected, reloading...');
62+
collect(Socket::$clients)
63+
->map(function (ConnectionInterface $client) {
64+
$client->send('reload');
65+
});
66+
});
67+
68+
return $this;
69+
}
70+
71+
protected function startServer(): static
72+
{
73+
try {
74+
$this->server = new IoServer(
75+
new HttpServer(new WsServer(new Socket())),
76+
new Reactor("{$this->host()}:{$this->port()}", [], $this->loop),
77+
$this->loop
78+
);
79+
$this->loop->addPeriodicTimer(1, fn() => Cache::put('ws_is_running', true, 5));
80+
81+
$this->server->run();
82+
} catch (\Exception $exception) {
83+
if (static::$portOffset < 10) {
84+
static::$portOffset++;
85+
$this->startServer();
86+
}
87+
}
88+
89+
return $this;
90+
}
91+
92+
public static function isRunning(): bool
93+
{
94+
return Cache::get('ws_is_running', false);
95+
}
96+
97+
public static function host()
98+
{
99+
return '127.0.0.1';
100+
}
101+
102+
public static function port()
103+
{
104+
return static::$port + static::$portOffset;
105+
}
106+
107+
protected function dirs(): array
108+
{
109+
$currentDir = getcwd();
110+
111+
$proposalDirs = [
112+
$currentDir . '/content',
113+
$currentDir . '/public',
114+
$currentDir . '/resources/views',
115+
];
116+
117+
$realDirs = [];
118+
119+
foreach ($proposalDirs as $dir) {
120+
if (is_dir($dir)) {
121+
$realDirs[] = $dir;
122+
}
123+
}
124+
125+
return $realDirs;
126+
}
127+
}

app/MarkdownParser.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace BangNokia\Lina;
44

55
use BangNokia\Lina\Contracts\MarkdownParser as MarkdownParserContract;
6-
use ParsedownToC;
76

87
class MarkdownParser implements MarkdownParserContract
98
{
@@ -16,8 +15,6 @@ public function __construct()
1615

1716
public function parse(string $text): string
1817
{
19-
$content = trim($this->driver->text($text));
20-
// dd($content);
21-
return $content;
18+
return trim($this->driver->text($text));
2219
}
2320
}

app/MarkdownRenderer.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace BangNokia\Lina;
44

55
use BangNokia\Lina\Contracts\Renderer;
6-
use Illuminate\Support\Facades\Blade;
76

87
class MarkdownRenderer implements Renderer
98
{
@@ -17,9 +16,9 @@ public function __construct(protected string $rootDir)
1716
config(['view.compiled' => $this->rootDir . '/resources/cache']);
1817
}
1918

20-
public function render(string $realPath): string
19+
public function render(string $file): string
2120
{
22-
$content = app(ContentFinder::class)->get($realPath, true);
21+
$content = app(ContentFinder::class)->get($file, true);
2322

2423
return view($content->layout, [
2524
'data' => $content,

app/Router.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace BangNokia\Lina;
44

5+
use BangNokia\Lina\Commands\WebsocketServeCommand;
6+
use Illuminate\Support\Facades\Cache;
57
use Symfony\Component\Finder\Finder;
68
use Symfony\Component\HttpFoundation\Request;
79
use Symfony\Component\HttpFoundation\Response;
@@ -29,13 +31,37 @@ public function parse(Request $request): Response
2931

3032
$contentFileRealPath = $this->contentFinder->tryFind($path);
3133

34+
$html = app(MarkdownRenderer::class)->render($contentFileRealPath);
35+
36+
// we don't have middleware so let inject the websocket script here
37+
$html = $this->injectWebSocketScript($html);
38+
3239
return new Response(
33-
app(MarkdownRenderer::class)->render($contentFileRealPath),
40+
$html,
3441
200,
3542
['Content-Type' => 'text/html']
3643
);
3744
}
3845

46+
protected function injectWebSocketScript(string $html): string
47+
{
48+
$port = WebsocketServeCommand::port();
49+
50+
$script = <<<JS
51+
<script>
52+
(new WebSocket('ws://127.0.0.1:$port')).onmessage = function (message) {
53+
if (message.data === 'reload') {
54+
window.location.reload(true);
55+
}
56+
};
57+
</script>
58+
JS;
59+
60+
$html = $html . $script; // so who care about well-formed html here xD!
61+
62+
return $html;
63+
}
64+
3965
protected function isStaticFile(string $path): bool
4066
{
4167
return in_array(pathinfo($path, PATHINFO_EXTENSION), ['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'svg']);

0 commit comments

Comments
 (0)