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();
+});