diff --git a/README.md b/README.md index a847502..ee9b614 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ This is a **personal project**, but the community can also contribute to this pr --- +## ⚠️ THE `CLI` IS NOT READY YET, DON'T USE IN PRODUCTION ⚠️ + ## The why? It's simple, because I need a tool to manage my Chrome Driver and Chrome Browser, but... the reason behind that @@ -23,8 +25,9 @@ with the correct Browser Version, is painful (I **HATE** manual tasks), I decide ## Documentation * [Commands](#commands) - * [Install Google Chrome Browser](#install-google-chrome-browser) - * [Install Google Chrome Driver](#install-google-chrome-driver) + * [Install Google Chrome Browser](#install-google-chrome-browser): `install:browser` + * [Install Google Chrome Driver](#install-google-chrome-driver): `install:driver` + * [Manage Google Chrome Driver](#manage-google-chrome-driver): `manage:driver` ### Commands @@ -82,6 +85,121 @@ any other version to download (if is available). > **Note**: You can check the available versions on this [API endpoint](https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json) 👈, but keep in mind > that for `chromedriver` the versions starts at `115.0.5763.0`, so any version below that we will not have access to the binary download link (for now). +
+ +#### Manage Google Chrome Driver + +If you want to manage one instance or more of Google Chrome Browser, this could be a time-consuming task, and to simplify +this task, we have several action in one command. + + +The command `./google-for-testing manage:driver [--] []` have 6 actions to manage a Google Chorme Driver. + +
+start action + +The first action is `start`, and this is as simple as running the next command: + +```bash +./google-for-testing manage:driver start +``` + +This will start a new instance of Chrome Driver in port `9515` (by default). +
+ +
+stop action + +The second action is `stop`, and this is as simple as running the next command: + +```bash +./google-for-testing manage:driver stop +``` + +This will stop the instance of Chrome Driver in port `9515` (by default). +
+ + +
+restart action + +The third action is `restart`, and this is as simple as running the next command: + +```bash +./google-for-testing manage:driver restart +``` + +This will restart the instance of Chrome Driver in port `9515` (by default). +
+ +
+status action + +The fourth action is `status`, and this is as simple as running the next command: + +```bash +./google-for-testing manage:driver status +``` + +This will check the health of the Chrome Driver instance in port `9515` (by default). +
+ + +
+list action + +The fifth action is `list`, and this is as simple as running the next command: + +```bash +./google-for-testing manage:driver list +``` + +This will list all the Chrome Driver instances in a table. This table will have the +`PID` and `PORT`. + +> **Note**: +> This command will list only the instances spin-up by this CLI. +
+ +
+kill action + +The sixth action is `kill`, and this is as simple as running the next command: + +```bash +./google-for-testing manage:driver kill +``` + +This will search for all the instances of Chrome Driver in different ports, and then kill all the process. + +> **Note**: +> This action will ask you for your permission to do it. +
+ +These are the six actions available in the command `manage:driver`, and we have two options we can use in conjunction +with these actions. For example, if you need to spin-up 2 or more instances of Chrome Driver, you +need to specify the ports where you need to spin up the servers with the option `--port` or `-p`. + +```bash +./google-for-testing manage:driver start --port=9515 --port=9516 +``` + +This will spin-up two servers, one in port `9515`, and the second one in port `9516`. As I said, this option can +be used with the other three other actions: `stop`, `restart`, and `status`. + +> **Note**: +> This comand can only be use with the first four actions: `start`, `stop`, `restart`, and `status`. + +The second one is just to specify where to search for the Chrome Driver binary, you just need to specify the option +`--path`. + +```bash +./google-for-testing manage:driver start --path=/some/directory/path +``` + +This option will only search for the binary `chromedriver` in the path specify, so be sure to have this binary available +and named correctly. + --- ## License diff --git a/app/Commands/DriverManagerCommand.php b/app/Commands/DriverManagerCommand.php new file mode 100644 index 0000000..6a2d9da --- /dev/null +++ b/app/Commands/DriverManagerCommand.php @@ -0,0 +1,251 @@ + 'linux64', + 'mac-arm' => 'mac-arm64', + 'mac-intel' => 'mac-x64', + 'win' => 'win64', + ]; + + protected array $commands = [ + 'start' => './chromedriver --log-level=ALL --port={port} &', + 'pid' => "ps aux | grep '[c]hromedriver --log-level=ALL {options}' | awk '{print $2,$13}'", + 'stop' => 'kill -9 {pid}', + ]; + + public function handle(): int + { + $action = $this->argument('action') ?? select('Select an action to perform', [ + 'start' => 'Start a new server', + 'stop' => 'Stop a server', + 'restart' => 'Restart a server', + 'status' => 'Status of a server', + 'list' => 'List all the server', + 'kill' => 'Kill all the servers', + ]); + + $callable = match ($action) { + 'start' => $this->start(...), + 'stop' => $this->stop(...), + 'restart' => $this->restart(...), + 'status' => $this->status(...), + 'list' => $this->list(...), + 'kill' => $this->kill(...), + }; + + if ($action === 'kill' || $action === 'list') { + return $callable(); + } + + return $this->getPorts()->map(fn (string $port) => $callable(port: $port)) + // Reduce the result of every callable to a single SUCCESS or FAILURE value + ->reduce(fn (int $results, int $result) => $results && $result, self::FAILURE); + } + + protected function start(string $port): int + { + if ($pid = $this->getProcessID($port)) { + warning("[PID: $pid]: There's a server running already on port [$port]"); + + return self::FAILURE; + } + + intro("Stating Google Chrome Driver on port [$port]"); + + $this->command('start', ['{port}' => $port]); + + info('Google Chrome Driver server is up and running'); + + return self::SUCCESS; + } + + public function stop(string $port): int + { + intro("Stopping Google Chrome Driver on port [$port]"); + + $pid = $this->getProcessID($port); + + if (empty($pid)) { + warning("There's no server to stop on port [$port]"); + + return self::FAILURE; + } + + $this->command('stop', ['{pid}' => $pid]); + + info("Google Chrome Driver server stopped on port [$port]"); + + return self::SUCCESS; + } + + protected function restart(string $port): int + { + intro("Restarting Google Chrome Driver on port [$port]"); + + $pid = $this->getProcessID($port); + + if (empty($pid)) { + warning("There's no server to restart on port [$port]"); + + return self::FAILURE; + } + + $this->command('stop', ['{pid}' => $pid]); + + $this->command('start', ['{port}' => $port]); + + info("Google Chrome Driver server restarted on port [$port]"); + + return self::SUCCESS; + } + + protected function status(string $port): int + { + intro("Getting Google Chrome Driver status on port [$port]"); + + $pid = $this->getProcessID($port); + + if (empty($pid)) { + warning("There's no server available on port [$port]"); + + return self::FAILURE; + } + + $response = Http::get('http://localhost:9515/status'); + + $data = $response->json('value'); + + if (array_key_exists('error', $data) || ! $data['ready']) { + error('There was a problem, we cannot establish connection with the server'); + + return self::FAILURE; + } + + info('Google Chrome server status: [OK]'); + + return self::SUCCESS; + } + + protected function list(): int + { + info('Listing all the servers available'); + + $result = $this->getProcessIDs(); + + if (empty($result)) { + warning("There' no servers available to list"); + + return self::FAILURE; + } + + $this->table(['PID', 'PORT'], $result); + + return self::SUCCESS; + } + + protected function kill(): int + { + $pids = $this->getProcessIDs(); + + if (empty($pids)) { + warning("There' no servers to kill"); + + return self::FAILURE; + } + + $this->table(['PID', 'PORT'], $pids); + + if (! $this->confirm('Are you sure you want to do this?')) { + return self::SUCCESS; + } + + info('Stopping all the Google Chrome Driver servers that are available in the system'); + + $pids + ->each(function (array $data) { + info("Stopping Google Chrome Driver [PID: {$data['pid']}]"); + + $this->command('stop', ['{pid}' => $data['pid']]); + }); + + return self::SUCCESS; + } + + protected function command(string $cmd, array $with) + { + return Process::command( + Str::replace( + collect($with)->keys(), + collect($with)->values(), + $this->commands[$cmd] + ) + )->path($this->getChromeDriverDirectory())->run(); + } + + protected function getProcessID(string $port): ?int + { + $process = $this->command('pid', ['{options}' => '--port='.$port]); + + $output = explode(' ', trim($process->output())); + + return (int) $output[0] ?: null; + } + + protected function getProcessIDs(): ?Collection + { + $process = $this->command('pid', ['{options}' => '']); + + if (empty($process->output())) { + return null; + } + + $raw = explode("\n", trim($process->output())); + + return collect($raw)->map(function (string $data) { + $data = explode(' ', $data); + + return ['pid' => $data[0], 'port' => Str::remove('--port=', $data[1])]; + }); + } + + protected function getPorts(): Collection + { + return collect($this->option('port') ? [...$this->option('port')] : $this->port)->unique()->filter(); + } + + protected function getChromeDriverDirectory(): string + { + return $this->option('path') + ?? join_paths( + getenv('HOME'), + '.google-for-testing', + 'chromedriver-'.$this->platforms[OperatingSystem::id()], + ); + } +} diff --git a/tests/Feature/ManageDriverCommandTest.php b/tests/Feature/ManageDriverCommandTest.php new file mode 100644 index 0000000..3e1db3b --- /dev/null +++ b/tests/Feature/ManageDriverCommandTest.php @@ -0,0 +1,169 @@ + 'start']) + ->expectsOutputToContain('Stating Google Chrome Driver on port [9515]') + ->expectsOutputToContain('Google Chrome Driver server is up and running') + ->assertSuccessful(); + + Process::assertRan('./chromedriver --log-level=ALL --port=9515 &'); +}); + +it('stop a Chrome Driver server', function () { + Process::fake([ + 'ps aux *' => '10101', + '*' => Process::result(), + ]); + + artisan('manage:driver', ['action' => 'stop']) + ->expectsOutputToContain('Stopping Google Chrome Driver on port [9515]') + ->expectsOutputToContain('Google Chrome Driver server stopped') + ->doesntExpectOutputToContain("There's no server to stop on port [9515]") + ->assertSuccessful(); + + Process::assertRan("ps aux | grep '[c]hromedriver --log-level=ALL --port=9515' | awk '{print $2,$13}'"); + + Process::assertRan('kill -9 10101'); +}); + +it('restart a Chrome Driver server', function () { + Process::fake([ + 'ps aux *' => Process::result('10101'), + '*' => Process::result(), + ]); + + artisan('manage:driver', ['action' => 'restart']) + ->expectsOutputToContain('Restarting Google Chrome Driver on port [9515]') + ->expectsOutputToContain('Google Chrome Driver server restarted') + ->doesntExpectOutputToContain("There's no server to restart on port [9515]") + ->assertSuccessful(); + + Process::assertRan("ps aux | grep '[c]hromedriver --log-level=ALL --port=9515' | awk '{print $2,$13}'"); + + Process::assertRan('kill -9 10101'); + + Process::assertRan('./chromedriver --log-level=ALL --port=9515 &'); +}); + +test('status of Chrome Driver server', function () { + Process::fake([ + '*' => Process::result('10101'), + ]); + + Http::fake([ + '*' => Http::response(['value' => [ + 'ready' => true, + ]], headers: ['Content-Type' => 'application/json']), + ]); + + artisan('manage:driver', ['action' => 'status']) + ->expectsOutputToContain('Getting Google Chrome Driver status on port [9515]') + ->expectsOutputToContain('Google Chrome server status: [OK]') + ->doesntExpectOutputToContain("There's no server available on port [9515]") + ->assertSuccessful(); +}); + +it('can\'t start a new Chrome Driver server if there\'s one already started', function () { + Process::fake([ + '*' => Process::result('10101'), + ]); + + artisan('manage:driver', ['action' => 'start']) + ->expectsOutputToContain("[PID: 10101]: There's a server running already on port [9515]") + ->doesntExpectOutput('Stating Google Chrome Driver on port [9515]') + ->assertFailed(); +}); + +it('can\'t stop a Chrome Driver server if there\'s no server already started', function () { + Process::fake(); + + artisan('manage:driver', ['action' => 'stop']) + ->expectsOutputToContain("There's no server to stop") + ->assertFailed(); +}); + +it('can\'t restart a Chrome Driver server if there\'s no server already started', function () { + Process::fake(); + + artisan('manage:driver', ['action' => 'stop']) + ->expectsOutputToContain("There's no server to stop on port [9515]") + ->assertFailed(); +}); + +it('can\'t get the status of Chrome Driver server if there\'s no server already started', function () { + Process::fake(); + + artisan('manage:driver', ['action' => 'restart']) + ->expectsOutputToContain("There's no server to restart on port [9515]") + ->assertFailed(); +}); + +it('start 4 Chrome Driver servers', function () { + Process::fake(); + + artisan('manage:driver', ['action' => 'start', '-p' => [9515, 9516, 9517, 9518]]) + ->assertSuccessful(); + + Process::assertRanTimes(fn (PendingProcess $process) => Str::match('/^\.\/chromedriver --log-level=ALL --port=\d+ &$/', $process->command), 4); +}); + +it('stop all the available Chrome Driver servers', function () { + $data = ['9991 1111', '9992 1112', '9993 1113', '9994 1114']; + + Process::fake([ + 'ps aux *' => Process::result(Arr::join($data, "\n")), + '*' => Process::result(), + ]); + + artisan('manage:driver', ['action' => 'kill']) + ->expectsTable(['PID', 'PORT'], collect($data)->map(function (string $value) { + $values = explode(' ', $value); + + return ['pid' => $values[0], 'port' => $values[1]]; + })) + ->expectsConfirmation('Are you sure you want to do this?', 'yes') + ->expectsOutputToContain('Stopping all the Google Chrome Driver servers that are available in the system') + ->expectsOutputToContain('Stopping Google Chrome Driver [PID: 9991]') + ->expectsOutputToContain('Stopping Google Chrome Driver [PID: 9992]') + ->expectsOutputToContain('Stopping Google Chrome Driver [PID: 9993]') + ->expectsOutputToContain('Stopping Google Chrome Driver [PID: 9994]') + ->assertSuccessful(); + + Process::assertRan(fn (PendingProcess $process) => Str::match('/^ps aux .*/', $process->command)); + + Process::assertRanTimes(fn (PendingProcess $process) => Str::match('/^kill -9 \d+/', $process->command), 4); +}); + +it('list all the available Chrome Driver servers', function () { + $data = collect([ + // PID => PORT + '1111' => 9515, + '1112' => 9516, + '1113' => 9517, + '1114' => 9518, + '1115' => 9519, + ]); + + Process::fake([ + 'ps aux | grep *' => Process::result($data->map(fn ($port, $pid) => "$pid $port")->join("\n")), + ]); + + Prompt::fallbackWhen($this->app->runningUnitTests()); + + artisan('manage:driver', ['action' => 'list']) + ->expectsOutputToContain('Listing all the servers available') + ->doesntExpectOutputToContain("There' no servers available to list") + ->expectsTable(['PID', 'PORT'], $data->map(fn ($port, $pid) => [$pid, $port])->values()) + ->assertSuccessful(); +});