Skip to content

Commit 30c8783

Browse files
committed
applying a timeout for async command execution
1 parent 283e4d0 commit 30c8783

2 files changed

Lines changed: 88 additions & 27 deletions

File tree

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,39 @@ Usage example:
99

1010
```php
1111
use React\EventLoop\Factory as LoopFactory;
12-
use React\Http\Browser;
1312
use Itnelo\React\WebDriver\Client\W3CClient;
1413

1514
$loop = LoopFactory::create();
16-
$browser = new Browser($loop);
1715

1816
$webdriver = new W3CClient(
19-
$browser,
17+
$loop,
2018
[
2119
'server' => [
2220
'host' => 'selenium-hub',
2321
'port' => 4444,
2422
],
25-
'request' => [
23+
'command' => [
2624
'timeout' => 30,
2725
],
26+
'browser' => [
27+
'tcp' => [
28+
'bindto' => '192.168.56.10:0',
29+
],
30+
'tls' => [
31+
'verify_peer' => false,
32+
'verify_peer_name' => false,
33+
],
34+
],
2835
]
2936
);
3037
```
3138

32-
See a self-documented [ClientInterface.php](src/ClientInterface.php) for the API details. Not all methods are ported
33-
(only the most necessary), so feel free to open an issue / make a pull request if you want more.
39+
See a self-documented [ClientInterface.php](src/ClientInterface.php) for the API details. Not all methods and arguments
40+
are ported (only the most necessary), so feel free to open an issue / make a pull request if you want more.
3441

3542
## See also
3643

37-
- [php-webdriver/webdriver](https://github.com/php-webdriver/php-webdriver) — the original, "blocking" implementation;
44+
- [php-webdriver/webdriver](https://github.com/php-webdriver/php-webdriver) — the original, "blocking" implementation;
3845
to get information how to use some advanced methods. For example, [WebDriverKeys](https://github.com/php-webdriver/php-webdriver/blob/main/lib/WebDriverKeys.php#L10)
3946
helper describes Unicode strings for sending special inputs to page elements (e.g. `Ctrl`, `Alt` and other keys).
4047

src/Client/W3CClient.php

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717

1818
use Itnelo\React\WebDriver\ClientInterface;
1919
use Psr\Http\Message\ResponseInterface;
20+
use React\EventLoop\LoopInterface;
2021
use React\Http\Browser;
2122
use React\Promise\Deferred;
2223
use React\Promise\PromiseInterface;
24+
use React\Socket\Connector as SocketConnector;
2325
use RuntimeException;
24-
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface as OptionsResolverExceptionInterface;
2526
use Symfony\Component\OptionsResolver\OptionsResolver;
2627
use Throwable;
28+
use function React\Promise\Timer\timeout;
2729

2830
/**
2931
* W3C compliant WebDriver client for Selenium Grid server (hub) that performs asynchronously.
@@ -38,14 +40,21 @@
3840
*/
3941
class W3CClient implements ClientInterface
4042
{
43+
/**
44+
* An event loop instance to manage an underlying browser and command timeouts
45+
*
46+
* @var LoopInterface
47+
*/
48+
private LoopInterface $loop;
49+
4150
/**
4251
* Sends commands to the Selenium Grid endpoint using W3C protocol over HTTP
4352
*
4453
* @var Browser
4554
*
4655
* @see https://www.w3.org/TR/webdriver
4756
*/
48-
private Browser $httpClient;
57+
private Browser $_httpClient;
4958

5059
/**
5160
* Array of options for the client
@@ -61,40 +70,48 @@ class W3CClient implements ClientInterface
6170
*
6271
* ```
6372
* $loop = \React\EventLoop\Factory::create();
64-
* $browser = new \React\Http\Browser($loop);
6573
*
6674
* $webdriver = new \Itnelo\React\WebDriver\Client\W3CClient(
67-
* $browser,
75+
* $loop,
6876
* [
6977
* 'server' => [
7078
* 'host' => 'selenium-hub',
7179
* 'port' => 4444,
7280
* ],
73-
* 'request' => [
81+
* 'command' => [
7482
* 'timeout' => 30,
7583
* ],
84+
* 'browser' => [
85+
* 'tcp' => [
86+
* 'bindto' => '192.168.56.10:0',
87+
* ],
88+
* 'tls' => [
89+
* 'verify_peer' => false,
90+
* 'verify_peer_name' => false,
91+
* ],
92+
* ],
7693
* ]
7794
* );
7895
* ```
7996
*
80-
* The "request.timeout" option here doesn't correlate with ReactPHP Browser's timeouts and will just cancel a
81-
* pending promise after the specified time (in seconds); an HTTP request itself, which is handled by the ReactPHP
82-
* Browser, may (or may not) be completed. Furthermore, the client can reject promise with a runtime exception if
83-
* underlying browser has decided to stop waiting for the response by its own timeout settings.
97+
* For all available "browser" options see \React\Socket\Connector class (will be instantiated for the underlying
98+
* browser) and socket context options: https://www.php.net/manual/en/context.socket.php.
8499
*
85-
* @param Browser $httpClient Sends commands to the Selenium Grid endpoint using W3C protocol over HTTP
86-
* @param array $options Array of options for the client
100+
* The "command.timeout" option here doesn't correlate with ReactPHP Browser's timeouts and will just cancel a
101+
* pending promise after the specified time (in seconds); an HTTP request itself, which is handled by the internal
102+
* ReactPHP Browser, may (or may not) be completed. Furthermore, the client can reject promise with a runtime
103+
* exception if an underlying browser has decided to stop waiting for the response by its own settings.
87104
*
88-
* @throws OptionsResolverExceptionInterface Whenever an error has been occurred during client configuration
105+
* @param LoopInterface $loop An event loop instance to manage an underlying browser and command timeouts
106+
* @param array $options Array of options for the client
89107
*/
90-
public function __construct(Browser $httpClient, array $options = [])
108+
public function __construct(LoopInterface $loop, array $options = [])
91109
{
92-
$this->httpClient = $httpClient;
93-
94110
$optionsResolver = new OptionsResolver();
95111

96112
$optionsResolver
97113
->define('server')
114+
->info('Options for establishing a socket connection to the remote server')
98115
->default(
99116
function (OptionsResolver $serverOptionsResolver) {
100117
$serverOptionsResolver
@@ -113,19 +130,39 @@ function (OptionsResolver $serverOptionsResolver) {
113130
;
114131

115132
$optionsResolver
116-
->define('request')
133+
->define('command')
134+
->info('Options to control behavior of the commands, which will be executed on the remote server')
117135
->default(
118136
function (OptionsResolver $requestOptionsResolver) {
119137
$requestOptionsResolver
120138
->define('timeout')
139+
->info(
140+
'Maximum time to wait (in seconds) for command execution '
141+
. '(do not correlate with browser timeouts)'
142+
)
121143
->allowedTypes('int')
122144
->default(30)
123145
;
124146
}
125147
)
126148
;
127149

150+
$optionsResolver
151+
->define('browser')
152+
->info('Options to customize a socket connector, which will be used by the internal http client')
153+
->default(
154+
function (OptionsResolver $browserOptionsResolver) {
155+
$browserOptionsResolver->setDefined(['tcp', 'tls', 'unix', 'dns', 'timeout', 'happy_eyeballs']);
156+
}
157+
)
158+
;
159+
128160
$this->_options = $optionsResolver->resolve($options);
161+
162+
$socketConnector = new SocketConnector($loop, $this->_options['browser']);
163+
$this->_httpClient = new Browser($loop, $socketConnector);
164+
165+
$this->loop = $loop;
129166
}
130167

131168
/**
@@ -154,9 +191,10 @@ public function createSession(): PromiseInterface
154191
];
155192

156193
// todo: implement custom executable args / prefs support (omitted in the interface)
157-
$requestContents = '{"capabilities":{"firstMatch":[{"browserName":"chrome","goog:chromeOptions":{"prefs":{"intl.accept_languages":"RU-ru,ru,en-US,en"},"args":["--user-data-dir=\/opt\/google\/chrome\/profiles"]}}]},"desiredCapabilities":{"browserName":"chrome","platform":"ANY","goog:chromeOptions":{"prefs":{"intl.accept_languages":"RU-ru,ru,en-US,en"},"args":["--user-data-dir=\/opt\/google\/chrome\/profiles"]}}}';
194+
$requestContents =
195+
'{"capabilities":{"firstMatch":[{"browserName":"chrome","goog:chromeOptions":{"prefs":{"intl.accept_languages":"RU-ru,ru,en-US,en"},"args":["--user-data-dir=\/opt\/google\/chrome\/profiles"]}}]},"desiredCapabilities":{"browserName":"chrome","platform":"ANY","goog:chromeOptions":{"prefs":{"intl.accept_languages":"RU-ru,ru,en-US,en"},"args":["--user-data-dir=\/opt\/google\/chrome\/profiles"]}}}';
158196

159-
$responsePromise = $this->httpClient->post($requestUri, $requestHeaders, $requestContents);
197+
$responsePromise = $this->_httpClient->post($requestUri, $requestHeaders, $requestContents);
160198

161199
$responsePromise->then(
162200
function (ResponseInterface $response) use ($sessionOpeningDeferred) {
@@ -190,9 +228,25 @@ function (Throwable $rejectionReason) use ($sessionOpeningDeferred) {
190228

191229
$sessionIdentifierPromise = $sessionOpeningDeferred->promise();
192230

193-
// todo: apply timeout
231+
// applying command timeout.
232+
$commandTimeoutInSeconds = $this->_options['command']['timeout'];
233+
234+
// global rejection handler for all internal side effects (timeout inclusive).
235+
// todo: move to WebDriver wrapper
236+
$sessionIdentifierTimedPromise = timeout($sessionIdentifierPromise, $commandTimeoutInSeconds, $this->loop);
237+
238+
$sessionIdentifierTimedPromise = $sessionIdentifierTimedPromise->otherwise(
239+
function (Throwable $rejectionReason) use (&$responsePromise) {
240+
if (method_exists($responsePromise, 'cancel')) {
241+
$responsePromise->cancel();
242+
}
243+
$responsePromise = null;
244+
245+
throw new RuntimeException('Unable to finish a session create command.', 0, $rejectionReason);
246+
}
247+
);
194248

195-
return $sessionIdentifierPromise;
249+
return $sessionIdentifierTimedPromise;
196250
}
197251

198252
/**

0 commit comments

Comments
 (0)