Skip to content

Commit 5e7b75d

Browse files
committed
service timeouts + no signals
1 parent 09b0411 commit 5e7b75d

File tree

7 files changed

+98
-99
lines changed

7 files changed

+98
-99
lines changed

src/Dompurify/DompurifyService.php

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,27 @@ public function start(): static
5858
$this->config->port,
5959
]);
6060

61+
$this->serviceProcess->setIdleTimeout($this->config->startupTimeout / 1000);
62+
6163
$this->serviceProcess->start();
6264

65+
// ? rm check
66+
if (! $this->serviceProcess->isRunning()) {
67+
throw new ProcessFailedException($this->serviceProcess);
68+
}
69+
6370
$this->serviceProcess->waitUntil(function (string $type, string $buffer) {
64-
// ? timeout
65-
// ! ensure service always returns output
66-
return strlen($buffer) > 5;
71+
if ($type === Process::ERR) {
72+
throw new ProcessFailedException($this->serviceProcess);
73+
}
74+
75+
// Must output when service is listening
76+
return strlen($buffer) > 4;
6777
});
6878

79+
$this->serviceProcess->setIdleTimeout(null);
80+
81+
// ? rm check
6982
if (! $this->serviceProcess->isRunning()) {
7083
throw new ProcessFailedException($this->serviceProcess);
7184
}
@@ -75,8 +88,23 @@ public function start(): static
7588

7689
public function stop(): static
7790
{
78-
if ($this->isRunning()) {
79-
$this->serviceProcess->stop();
91+
$this->serviceProcess->stop();
92+
93+
if (is_null($this->config->shutdownTimeout)) {
94+
return $this;
95+
}
96+
97+
$waitUntilMicroSec = \hrtime(true) + $this->config->shutdownTimeout * 1000;
98+
99+
$sleep = 10_000_000; // 10us
100+
101+
// TODO: test
102+
while ($this->isRunning()) {
103+
if (\hrtime(true) + $sleep >= $waitUntilMicroSec) {
104+
throw new XsslessException('Shutdown timed out');
105+
}
106+
107+
\usleep($sleep);
80108
}
81109

82110
return $this;
@@ -96,53 +124,4 @@ public function getIncrementalErrorOutput(): string
96124
{
97125
return $this->serviceProcess->getIncrementalErrorOutput();
98126
}
99-
100-
public function throwIfFailedOnTerm(): void
101-
{
102-
if ($this->serviceProcess->isRunning()) {
103-
// ? stop it
104-
throw new XsslessException('The service is still running');
105-
}
106-
107-
// TODO: fix windows check
108-
// https://github.com/medilies/xssless/actions/runs/10288495452/job/28474063301#step:7:26
109-
if ($this->isSigTerm() || $this->isWindows()) {
110-
return;
111-
}
112-
113-
throw new ProcessFailedException($this->serviceProcess);
114-
}
115-
116-
// ? interface
117-
public function waitForTermination(int $timeout): void
118-
{
119-
$elapsed = 0;
120-
$sleepInterval = 100; // Sleep for 100 milliseconds
121-
122-
while ($this->serviceProcess->isRunning() && $elapsed < $timeout) {
123-
usleep($sleepInterval * 1000);
124-
$elapsed += $sleepInterval;
125-
}
126-
127-
if ($this->serviceProcess->isRunning()) {
128-
throw new XsslessException('Process did not terminate within the given timeout');
129-
}
130-
}
131-
132-
// ========================================================================
133-
134-
private function isWindows(): bool
135-
{
136-
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
137-
}
138-
139-
private function isSigTerm(): bool
140-
{
141-
return $this->serviceProcess->getTermSignal() === 15;
142-
}
143-
144-
// private function isSigHup(): bool
145-
// {
146-
// return $this->serviceProcess->getTermSignal() === 1;
147-
// }
148127
}

src/Dompurify/DompurifyServiceConfig.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public function __construct(
1414
public string $host = '127.0.0.1',
1515
public int $port = 63000,
1616
public ?string $binary = null,
17+
public int $startupTimeout = 6000, // ms
18+
public ?int $shutdownTimeout = null // ms
1719
) {
1820
$this->class = DompurifyService::class;
1921
}

src/ServiceInterface.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,4 @@ public function isRunning(): bool;
1717
public function getIncrementalOutput(): string;
1818

1919
public function getIncrementalErrorOutput(): string;
20-
21-
public function throwIfFailedOnTerm(): void;
2220
}

src/laravel/Commands/StartCommand.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public function handle(): void
2424
exit;
2525
};
2626

27+
// ? Is this necessary
2728
pcntl_signal(SIGTERM, $terminate);
2829
pcntl_signal(SIGINT, $terminate);
2930

@@ -43,7 +44,5 @@ public function handle(): void
4344
// Sleep for a short period to avoid busy-waiting
4445
usleep(100_000);
4546
}
46-
47-
$service->throwIfFailedOnTerm();
4847
}
4948
}

tests/Dompurify/DompurifyCliTest.php

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
1414
expect(fn () => $cleaner->exec('foo'))->toThrow(ProcessFailedException::class);
1515
});
1616

17+
it('throws when cannot find binary file', function () {
18+
$cleaner = (new DompurifyCli)->configure(new DompurifyCliConfig(
19+
binary: __DIR__.'/js-mocks/x.js',
20+
));
21+
22+
expect(fn () => $cleaner->exec('foo'))->toThrow(XsslessException::class);
23+
});
24+
25+
it('throws when cannot locate temp folder', function () {
26+
$cleaner = (new DompurifyCli)->configure(new DompurifyCliConfig(
27+
tempFolder: __DIR__.'/x',
28+
));
29+
30+
expect(fn () => $cleaner->exec('foo'))->toThrow(XsslessException::class);
31+
});
32+
1733
test('setup()', function () {
1834
$cleaner = (new Xssless)->using(new DompurifyCliConfig);
1935

@@ -44,20 +60,4 @@
4460
));
4561

4662
expect(fn () => $cleaner->exec('foo'))->toThrow(XsslessException::class);
47-
})->depends('setup()');
48-
49-
it('throws when cannot find binary file', function () {
50-
$cleaner = (new DompurifyCli)->configure(new DompurifyCliConfig(
51-
binary: __DIR__.'/js-mocks/x.js',
52-
));
53-
54-
expect(fn () => $cleaner->exec('foo'))->toThrow(XsslessException::class);
55-
})->depends('setup()');
56-
57-
it('throws when cannot locate temp folder', function () {
58-
$cleaner = (new DompurifyCli)->configure(new DompurifyCliConfig(
59-
tempFolder: __DIR__.'/x',
60-
));
61-
62-
expect(fn () => $cleaner->exec('foo'))->toThrow(XsslessException::class);
63-
})->depends('setup()');
63+
});

tests/Dompurify/DompurifyServiceTest.php

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,44 @@
55
use Medilies\Xssless\Dompurify\DompurifyServiceConfig;
66
use Medilies\Xssless\Xssless;
77
use Symfony\Component\Process\Exception\ProcessFailedException;
8+
use Symfony\Component\Process\Exception\ProcessTimedOutException;
9+
10+
it('throws on bad node path', function () {
11+
$service = (new DompurifyService)->configure(new DompurifyServiceConfig(
12+
node: 'nodeZz',
13+
));
14+
15+
expect(fn () => $service->start())->toThrow(ProcessFailedException::class);
16+
17+
// expect($service->serviceProcess->getExitCode())->toBe(127);
18+
});
19+
20+
it('throws when cannot find binary file', function () {
21+
$cleaner = (new DompurifyService)->configure(new DompurifyServiceConfig(
22+
binary: __DIR__.'/js-mocks/x.js',
23+
));
24+
25+
expect(fn () => $cleaner->start('foo'))->toThrow(ProcessFailedException::class);
26+
});
27+
28+
it('throws on start() timeout', function () {
29+
$service = (new DompurifyService)->configure(new DompurifyServiceConfig(
30+
binary: __DIR__.'/js-mocks/service-start-timeout.js',
31+
startupTimeout: 50,
32+
));
33+
34+
expect(fn () => $service->start())->toThrow(ProcessTimedOutException::class);
35+
});
36+
37+
it('throws on bad host', function () {
38+
$cleaner = (new DompurifyService)->configure(new DompurifyServiceConfig(
39+
host: 'a.b.c.example.com',
40+
));
41+
42+
$dirty = '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">';
43+
44+
expect(fn () => $cleaner->send($dirty))->toThrow(ConnectException::class);
45+
});
846

947
test('setup()', function () {
1048
$cleaner = (new DompurifyService)->configure(new DompurifyServiceConfig);
@@ -21,7 +59,7 @@
2159

2260
$clean = $cleaner->send($dirty);
2361

24-
$cleaner->stop()->throwIfFailedOnTerm();
62+
$cleaner->stop();
2563

2664
expect($clean)->toBe(str_repeat('*/', 34 * 1000).'<img>"&gt;');
2765
})->depends('setup()');
@@ -39,29 +77,8 @@
3977

4078
$clean = $cleaner->clean($dirty);
4179

42-
$service->stop()->throwIfFailedOnTerm();
80+
$service->stop();
4381

4482
expect($clean)->toBe('<img>"&gt;');
83+
// TODO: expect exit with 143
4584
})->depends('setup()');
46-
47-
it('throws on bad host', function () {
48-
$cleaner = (new DompurifyService)->configure(new DompurifyServiceConfig(
49-
host: 'a.b.c.example.com',
50-
));
51-
52-
$dirty = '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">';
53-
54-
expect(fn () => $cleaner->send($dirty))->toThrow(ConnectException::class);
55-
});
56-
57-
it('throws on bad node path', function () {
58-
$service = (new DompurifyService)->configure(new DompurifyServiceConfig(
59-
node: 'nodeZz',
60-
));
61-
62-
expect(fn () => $service->start())->toThrow(ProcessFailedException::class);
63-
64-
// $service->waitForTermination(2000);
65-
// expect($service->serviceProcess->getExitCode())->toBe(127);
66-
// TODO: fix https://github.com/medilies/xssless/actions/runs/10284119857/job/28459470742#step:7:28
67-
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const start = Date.now();
2+
while (Date.now() - start < 500) {
3+
4+
}

0 commit comments

Comments
 (0)