From d305c80a53cac3adfaf9696c7a7b8ef46259736c Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Fri, 27 Oct 2023 15:41:56 -0500 Subject: [PATCH 01/55] feat: prompts working state --- app/Commands/EnableCommand.php | 31 +++++++++++++++++-------------- composer.json | 3 ++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index 57bb9ef5..240c58cd 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; +use function Laravel\Prompts\search; class EnableCommand extends Command { @@ -47,8 +48,7 @@ public function handle(Environment $environment, Services $services): void } $option = $this->selectService(); - - if (! $option) { + if (!$option) { return; } @@ -87,7 +87,7 @@ public function serverArguments(): array */ public function extractPassthroughOptions(array $arguments): array { - if (! in_array('--', $arguments)) { + if (!in_array('--', $arguments)) { return []; } @@ -130,18 +130,21 @@ private function selectService(): ?string private function defaultMenu(): ?string { - $option = $this->menu(self::MENU_TITLE)->setTitleSeparator('='); - - foreach ($this->enableableServicesByCategory() as $category => $services) { - $separator = str_repeat('-', 1 + Str::length($category)); - - $option->addStaticItem("{$category}:") - ->addStaticItem($separator) - ->addOptions($this->menuItemsForServices($services)) - ->addLineBreak('', 1); - } + $servicesList = collect($this->enableableServicesByCategory())->flatMap(function ($services, $category) { + return collect($this->menuItemsForServices($services))->mapWithKeys(function ($row, $key) use ($category) { + return [$key => "{$category}: {$row}"]; + })->toArray(); + })->toArray(); - return $option->open(); + return search( + label: self::MENU_TITLE, + options: fn (string $value) => strlen($value) > 0 + ? collect($servicesList)->filter(function ($row) use ($value) { + return str($row)->lower()->contains(str($value)->lower()); + })->toArray() + : $servicesList, + scroll: 10 + ); } private function windowsMenu($category = null): ?string diff --git a/composer.json b/composer.json index e0170dfd..67cccd38 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "ext-json": "*", "ext-pcntl": "*", "ext-posix": "*", - "guzzlehttp/psr7": "^1.7" + "guzzlehttp/psr7": "^1.7", + "laravel/prompts": "dev-support-older-php" }, "require-dev": { "guzzlehttp/guzzle": "^7.4", From 81c13eff4951c9eaec3b35840f1cdc105b6ebd49 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Fri, 3 May 2024 12:46:12 -0500 Subject: [PATCH 02/55] update prompts --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 642d2211..53c3735f 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "ext-pcntl": "*", "ext-posix": "*", "guzzlehttp/psr7": "^1.7", - "laravel/prompts": "dev-support-older-php" + "laravel/prompts": "^0.1.21" }, "require-dev": { "guzzlehttp/guzzle": "^7.5", From fa611a41e924264a446f6aeff918dcc97f578ed6 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Thu, 15 Aug 2024 09:33:01 -0500 Subject: [PATCH 03/55] updates tests --- app/Commands/EnableCommand.php | 4 +-- composer.json | 2 +- tests/Feature/EnableCommandTest.php | 49 +++++++++++++++-------------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index 240c58cd..a040b585 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -48,7 +48,7 @@ public function handle(Environment $environment, Services $services): void } $option = $this->selectService(); - if (!$option) { + if (! $option) { return; } @@ -87,7 +87,7 @@ public function serverArguments(): array */ public function extractPassthroughOptions(array $arguments): array { - if (!in_array('--', $arguments)) { + if (! in_array('--', $arguments)) { return []; } diff --git a/composer.json b/composer.json index 53c3735f..ea4ace0d 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "ext-pcntl": "*", "ext-posix": "*", "guzzlehttp/psr7": "^1.7", - "laravel/prompts": "^0.1.21" + "laravel/prompts": "^0.1.24" }, "require-dev": { "guzzlehttp/guzzle": "^7.5", diff --git a/tests/Feature/EnableCommandTest.php b/tests/Feature/EnableCommandTest.php index d32ec890..9ff0ea41 100644 --- a/tests/Feature/EnableCommandTest.php +++ b/tests/Feature/EnableCommandTest.php @@ -9,6 +9,8 @@ use App\Services\PostgreSql; use App\Shell\Docker; use Illuminate\Console\Command; +use Laravel\Prompts\Prompt; +use Laravel\Prompts\SearchPrompt; use NunoMaduro\LaravelConsoleMenu\Menu; use PHPUnit\Framework\Assert; use Tests\TestCase; @@ -33,14 +35,6 @@ function it_can_enable_a_service_from_menu() $postgres = 'postgresql' => $fqcn = 'App\Services\PostgreSql', ]; - $menuItems = [ - ' DATABASE ', - 'PostgreSQL', - ' SEARCH ', - 'MeiliSearch', - 'Exit', - ]; - $this->mock(Docker::class, function ($mock) { $mock->shouldReceive('isInstalled')->andReturn(true); $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); @@ -56,28 +50,37 @@ function it_can_enable_a_service_from_menu() }); if ($this->isWindows()) { + $menuItems = [ + ' DATABASE ', + 'PostgreSQL', + ' SEARCH ', + 'MeiliSearch', + 'Exit', + ]; + $this->artisan('enable') ->expectsChoice('Takeout containers to enable', 'PostgreSQL', $menuItems) ->assertExitCode(0); } else { - $menuMock = $this->mock(Menu::class, function ($mock) use ($postgres) { - $mock->shouldReceive('setTitleSeparator')->andReturnSelf(); - $mock->shouldReceive('addStaticItem')->andReturnSelf()->times(4); - $mock->shouldReceive('addOptions')->andReturnSelf(); - $mock->shouldReceive('addLineBreak')->andReturnSelf(); - $mock->shouldReceive('open')->andReturn($postgres)->once(); - }); + $menuItems = [ + 'Database: PostgreSQL', + 'Search: MeiliSearch', + 'meilisearch', + 'postgresql', + ]; - Command::macro( - 'menu', - function (string $title) use ($menuMock) { - Assert::assertEquals('Takeout containers to enable', $title); + $this->artisan('enable') + ->expectsChoice('Takeout containers to enable', '', $menuItems) + ->assertExitCode(0); - return $menuMock; - } - ); + $menuItems = [ + 'Database: PostgreSQL', + 'postgresql', + ]; - $this->artisan('enable'); + $this->artisan('enable') + ->expectsChoice('Takeout containers to enable', 'PostgreSQL', $menuItems) + ->assertExitCode(0); } } From 6b18b5368ead3151735481cb6fad698da8efb4d7 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Thu, 15 Aug 2024 14:27:40 -0500 Subject: [PATCH 04/55] clean up --- tests/Feature/DisableCommandTest.php | 2 +- tests/Feature/EnableCommandTest.php | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/Feature/DisableCommandTest.php b/tests/Feature/DisableCommandTest.php index e1d719e5..ea915567 100644 --- a/tests/Feature/DisableCommandTest.php +++ b/tests/Feature/DisableCommandTest.php @@ -55,7 +55,7 @@ function it_can_disable_a_service_from_menu() array_push($disableableServices, 'Exit'); $this->artisan('disable') - ->expectsChoice('Takeout containers to disable', $postgressName, $disableableServices) + ->expectsChoice('Takeout containers to disable', $postgressName, array_values($disableableServices)) ->assertExitCode(0); } else { $menuMock = $this->mock(Menu::class, function ($mock) use ($postgressId) { diff --git a/tests/Feature/EnableCommandTest.php b/tests/Feature/EnableCommandTest.php index 9ff0ea41..4f67b289 100644 --- a/tests/Feature/EnableCommandTest.php +++ b/tests/Feature/EnableCommandTest.php @@ -8,11 +8,6 @@ use App\Services\MeiliSearch; use App\Services\PostgreSql; use App\Shell\Docker; -use Illuminate\Console\Command; -use Laravel\Prompts\Prompt; -use Laravel\Prompts\SearchPrompt; -use NunoMaduro\LaravelConsoleMenu\Menu; -use PHPUnit\Framework\Assert; use Tests\TestCase; class EnableCommandTest extends TestCase @@ -66,7 +61,7 @@ function it_can_enable_a_service_from_menu() 'Database: PostgreSQL', 'Search: MeiliSearch', 'meilisearch', - 'postgresql', + $postgres, ]; $this->artisan('enable') @@ -75,11 +70,11 @@ function it_can_enable_a_service_from_menu() $menuItems = [ 'Database: PostgreSQL', - 'postgresql', + $postgres, ]; $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', 'PostgreSQL', $menuItems) + ->expectsChoice('Takeout containers to enable', $postgres, $menuItems) ->assertExitCode(0); } } From 13f2c5ac5a87982b035a2ac6c2a45329deec8b43 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Mon, 16 Dec 2024 15:07:30 -0500 Subject: [PATCH 05/55] cleanup enable cmd and drops windows specifics Co-authored-by: Tony Messias --- app/Commands/EnableCommand.php | 49 --------------- tests/Feature/EnableCommandTest.php | 98 ++++++----------------------- 2 files changed, 18 insertions(+), 129 deletions(-) diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index a040b585..1cc6a3ec 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -121,10 +121,6 @@ public function removeOptions(array $arguments): array private function selectService(): ?string { - if ($this->environment->isWindowsOs()) { - return $this->windowsMenu(); - } - return $this->defaultMenu(); } @@ -147,51 +143,6 @@ private function defaultMenu(): ?string ); } - private function windowsMenu($category = null): ?string - { - $choices = []; - $groupedServices = $this->enableableServicesByCategory(); - - if ($category) { - $groupedServices = Arr::where($groupedServices, function ($value, $key) use ($category) { - return Str::contains($category, strtoupper($key)); - }); - } - - foreach ($groupedServices as $serviceCategory => $services) { - $serviceCategoryMenuItem = ' ' . (Str::upper($serviceCategory)) . ' '; - array_push($choices, $serviceCategoryMenuItem); - - foreach ($this->menuItemsForServices($services) as $menuItemKey => $menuItemName) { - array_push($choices, $menuItemName); - } - } - - if ($category) { - array_push($choices, 'Back'); - } - - array_push($choices, 'Exit'); - - $choice = $this->choice(self::MENU_TITLE, $choices); - - if (Str::contains($choice, 'Back')) { - return $this->windowsMenu(); - } - - if (Str::contains($choice, 'Exit')) { - return null; - } - - foreach ($this->enableableServices() as $shortName => $fqcn) { - if ($choice === $fqcn) { - return $shortName; - } - } - - return $this->windowsMenu($choice); - } - private function menuItemsForServices($services): array { return collect($services)->mapWithKeys(function ($service) { diff --git a/tests/Feature/EnableCommandTest.php b/tests/Feature/EnableCommandTest.php index 4f67b289..ea695478 100644 --- a/tests/Feature/EnableCommandTest.php +++ b/tests/Feature/EnableCommandTest.php @@ -12,16 +12,6 @@ class EnableCommandTest extends TestCase { - function isWindows() - { - return PHP_OS_FAMILY === 'Windows'; - } - - function isLinux() - { - return PHP_OS_FAMILY === 'Linux'; - } - /** @test */ function it_can_enable_a_service_from_menu() { @@ -44,77 +34,25 @@ function it_can_enable_a_service_from_menu() $mock->shouldReceive('enable')->once(); }); - if ($this->isWindows()) { - $menuItems = [ - ' DATABASE ', - 'PostgreSQL', - ' SEARCH ', - 'MeiliSearch', - 'Exit', - ]; - - $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', 'PostgreSQL', $menuItems) - ->assertExitCode(0); - } else { - $menuItems = [ - 'Database: PostgreSQL', - 'Search: MeiliSearch', - 'meilisearch', - $postgres, - ]; - - $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', '', $menuItems) - ->assertExitCode(0); - - $menuItems = [ - 'Database: PostgreSQL', - $postgres, - ]; - - $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', $postgres, $menuItems) - ->assertExitCode(0); - } - } + $menuItems = [ + 'Database: PostgreSQL', + 'Search: MeiliSearch', + 'meilisearch', + $postgres, + ]; - /** @test */ - function it_can_navigate_a_submenu_in_windows() - { - if ($this->isWindows()) { - $services = [ - 'meilisearch' => 'App\Services\MeiliSearch', - 'postgresql' => 'App\Services\PostgreSql', - ]; - - $menuItems = [ - $category = ' DATABASE ', - 'PostgreSQL', - ' SEARCH ', - 'MeiliSearch', - $exit = 'Exit', - ]; - - $submenuItems = [ - ' DATABASE ', - 'PostgreSQL', - $back = 'Back', - 'Exit', - ]; - - $this->mock(Services::class, function ($mock) use ($services) { - $mock->shouldReceive('all')->andReturn($services); - }); - - $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', $category, $menuItems) - ->expectsChoice('Takeout containers to enable', $back, $submenuItems) - ->expectsChoice('Takeout containers to enable', $exit, $menuItems) - ->assertExitCode(0); - } else { - $this->assertTrue(true); - } + $this->artisan('enable') + ->expectsChoice('Takeout containers to enable', '', $menuItems) + ->assertExitCode(0); + + $menuItems = [ + 'Database: PostgreSQL', + $postgres, + ]; + + $this->artisan('enable') + ->expectsChoice('Takeout containers to enable', $postgres, $menuItems) + ->assertExitCode(0); } /** @test */ From a673b99338df60d902d435e8bfb74b292ba71925 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Mon, 16 Dec 2024 18:05:56 -0500 Subject: [PATCH 06/55] refactors disable cmd to use prompts and drops windows tests Co-authored-by: Tony Messias --- app/Commands/DisableCommand.php | 39 +++------- tests/Feature/DisableCommandTest.php | 107 ++++++--------------------- 2 files changed, 32 insertions(+), 114 deletions(-) diff --git a/app/Commands/DisableCommand.php b/app/Commands/DisableCommand.php index 956dc93c..ebbde555 100644 --- a/app/Commands/DisableCommand.php +++ b/app/Commands/DisableCommand.php @@ -8,6 +8,9 @@ use LaravelZero\Framework\Commands\Command; use Throwable; +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\select; + class DisableCommand extends Command { use InitializesCommands; @@ -85,19 +88,17 @@ public function disableByServiceName(string $service): void $this->disableByContainerId($serviceContainerId); } - public function showDisableServiceMenu($disableableServices = null): void + public function showDisableServiceMenu(): void { - if ($serviceContainerId = $this->selectMenu($disableableServices ?? $this->disableableServices)) { - $this->disableByContainerId($serviceContainerId); - } + $serviceContainerId = select( + label: self::MENU_TITLE, + options: $this->disableableServices + ); + $this->disableByContainerId($serviceContainerId); } private function selectMenu($disableableServices): ?string { - if ($this->environment->isWindowsOs()) { - return $this->windowsMenu($disableableServices); - } - return $this->defaultMenu($disableableServices); } @@ -109,15 +110,6 @@ private function defaultMenu($disableableServices): ?string ->open(); } - private function windowsMenu($disableableServices): ?string - { - array_push($disableableServices, 'Exit'); - - $choice = $this->choice(self::MENU_TITLE, array_values($disableableServices)); - - return array_search($choice, $disableableServices); - } - public function disableByContainerId(string $containerId): void { try { @@ -134,18 +126,9 @@ public function disableByContainerId(string $containerId): void if (count($this->docker->allContainers()) === 0) { $question = 'No containers are running. Turn off Docker?'; - if ($this->environment->isWindowsOs()) { - $option = $this->confirm($question); - } else { - $option = $this->menu($question, [ - 'Yes', - 'No', - ])->disableDefaultItems()->open(); - } - - if ($option === 0 || $option === true) { + if (confirm($question)) { $this->task('Stopping Docker service ', $this->docker->stopDockerService()); - } + } } } catch (Throwable $e) { $this->error('Disabling failed! Error: ' . $e->getMessage()); diff --git a/tests/Feature/DisableCommandTest.php b/tests/Feature/DisableCommandTest.php index ea915567..cdca56ee 100644 --- a/tests/Feature/DisableCommandTest.php +++ b/tests/Feature/DisableCommandTest.php @@ -11,17 +11,6 @@ class DisableCommandTest extends TestCase { - - function isWindows() - { - return PHP_OS_FAMILY === 'Windows'; - } - - function isLinux() - { - return PHP_OS_FAMILY === 'Linux'; - } - /** @test */ function it_can_disable_a_service_from_menu() { @@ -47,39 +36,9 @@ function it_can_disable_a_service_from_menu() ->with($postgressId)->once(); }); - if ($this->isWindows()) { - $disableableServices = $services->mapWithKeys(function ($item) { - return [$item['container_id'] => $item['names']]; - })->toArray(); - - array_push($disableableServices, 'Exit'); - - $this->artisan('disable') - ->expectsChoice('Takeout containers to disable', $postgressName, array_values($disableableServices)) - ->assertExitCode(0); - } else { - $menuMock = $this->mock(Menu::class, function ($mock) use ($postgressId) { - $mock->shouldReceive('addLineBreak')->andReturnSelf(); - $mock->shouldReceive('setPadding')->andReturnSelf(); - $mock->shouldReceive('open')->andReturn($postgressId)->once(); - }); - - Command::macro( - 'menu', - function (string $title, array $options) use ($services, $menuMock) { - Assert::assertEquals('Takeout containers to disable', $title); - Assert::assertEquals( - $services->mapWithKeys(function ($container) { - return [$container['container_id'] => $container['names']]; - })->toArray(), - $options - ); - return $menuMock; - } - ); - - $this->artisan('disable'); - } + $this->artisan('disable') + ->expectsQuestion('Takeout containers to disable', $postgressId) + ->assertExitCode(0); } /** @test */ @@ -189,47 +148,23 @@ function it_will_try_to_stop_docker_service_if_no_containers_are_running() ], ]); - if ($this->isLinux() || $this->isWindows()) { - $this->mock(Docker::class, function ($mock) use ($services, $postgressId) { - $mock->shouldReceive('isInstalled')->andReturn(true); - $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); - $mock->shouldReceive('takeoutContainers')->andReturn($services); - - $mock->shouldReceive('attachedVolumeName') - ->with($postgressId)->andReturnNull()->once(); - $mock->shouldReceive('removeContainer') - ->with($postgressId)->once(); - - $mock->shouldReceive('allContainers')->andReturn(new Collection)->once(); - - if ($this->isWindows() || $this->isLinux()) { - $mock->shouldReceive('stopDockerService')->once(); - } - }); - - if ($this->isWindows()) { - $this->artisan('disable postgress') - ->expectsConfirmation('No containers are running. Turn off Docker?', 'Yes') - ->assertExitCode(0); - } else { - $menuMock = $this->mock(Menu::class, function ($mock) { - $mock->shouldReceive('disableDefaultItems')->andReturnSelf(); - $mock->shouldReceive('open')->andReturn(0)->once(); - }); - - Command::macro( - 'menu', - function (string $title, array $options) use ($menuMock) { - Assert::assertEquals('No containers are running. Turn off Docker?', $title); - Assert::assertEquals(['Yes', 'No'], $options); - return $menuMock; - } - ); - - $this->artisan('disable postgress'); - } - } else { - $this->assertTrue(true); - } + $this->mock(Docker::class, function ($mock) use ($services, $postgressId) { + $mock->shouldReceive('isInstalled')->andReturn(true); + $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); + $mock->shouldReceive('takeoutContainers')->andReturn($services); + + $mock->shouldReceive('attachedVolumeName') + ->with($postgressId)->andReturnNull()->once(); + $mock->shouldReceive('removeContainer') + ->with($postgressId)->once(); + + $mock->shouldReceive('allContainers')->andReturn(new Collection)->once(); + + $mock->shouldReceive('stopDockerService')->once(); + }); + + $this->artisan('disable postgress') + ->expectsConfirmation('No containers are running. Turn off Docker?', 'Yes') + ->assertExitCode(0); } } From cba21cffa0eb99b491e0dfe0a297c3d822a42dbb Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Mon, 16 Dec 2024 18:37:21 -0500 Subject: [PATCH 07/55] refactored stop cmd Co-authored-by: Tony Messias --- app/Commands/EnableCommand.php | 2 - app/Commands/StartCommand.php | 18 ++--- app/Commands/StopCommand.php | 115 ++++----------------------- composer.json | 1 - tests/Feature/DisableCommandTest.php | 3 - tests/Feature/StartCommandTest.php | 84 +++---------------- tests/Feature/StopCommandTest.php | 84 +++---------------- 7 files changed, 46 insertions(+), 261 deletions(-) diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index 1cc6a3ec..0ea77b90 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -5,9 +5,7 @@ use App\InitializesCommands; use App\Services; use App\Shell\Environment; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\App; -use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; use function Laravel\Prompts\search; diff --git a/app/Commands/StartCommand.php b/app/Commands/StartCommand.php index 0191835b..1928d086 100644 --- a/app/Commands/StartCommand.php +++ b/app/Commands/StartCommand.php @@ -10,6 +10,8 @@ use LaravelZero\Framework\Commands\Command; use PhpSchool\CliMenu\CliMenu; +use function Laravel\Prompts\select; + class StartCommand extends Command { use InitializesCommands; @@ -56,12 +58,9 @@ public function handle(Docker $docker, Environment $environment): void public function startableContainers(): array { - return $this->docker->startableTakeoutContainers()->map(function ($container) { - $label = sprintf('%s - %s', $container['container_id'], $container['names']); - + return $this->docker->startableTakeoutContainers()->mapWithKeys(function ($container) { return [ - $label, - $this->loadMenuItem($container, $label), + $container['container_id'] => $container['names'], ]; }, collect())->toArray(); } @@ -122,11 +121,12 @@ public function start(string $container): void private function loadMenu($startableContainers) { - if ($this->environment->isWindowsOs()) { - return $this->windowsMenu($startableContainers); - } + $container = select( + label: self::MENU_TITLE, + options: $startableContainers + ); - return $this->defaultMenu($startableContainers); + $this->docker->startContainer($container); } private function defaultMenu($startableContainers) diff --git a/app/Commands/StopCommand.php b/app/Commands/StopCommand.php index 03a77d46..3c72fe1d 100644 --- a/app/Commands/StopCommand.php +++ b/app/Commands/StopCommand.php @@ -5,10 +5,10 @@ use App\InitializesCommands; use App\Shell\Docker; use App\Shell\Environment; -use Illuminate\Support\Arr; use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; -use PhpSchool\CliMenu\CliMenu; + +use function Laravel\Prompts\select; class StopCommand extends Command { @@ -23,6 +23,7 @@ class StopCommand extends Command public function handle(Docker $docker, Environment $environment): void { + $this->docker = $docker; $this->environment = $environment; $this->initializeCommand(); @@ -30,6 +31,7 @@ public function handle(Docker $docker, Environment $environment): void $containers = $this->argument('containerId'); if (filled($containers)) { + foreach ($containers as $container) { $this->stopByServiceNameOrContainerId($container); } @@ -51,17 +53,16 @@ public function handle(Docker $docker, Environment $environment): void return; } - $this->loadMenu($stoppableContainers); + $containerId = $this->loadMenu($stoppableContainers); + + $this->stop($containerId); } public function stoppableContainers(): array { - return $this->docker->stoppableTakeoutContainers()->map(function ($container) { - $label = sprintf('%s - %s', $container['container_id'], $container['names']); - + return $this->docker->stoppableTakeoutContainers()->mapWithKeys(function ($container) { return [ - $label, - $this->loadMenuItem($container, $label), + $container['container_id'] => $container['names'], ]; }, collect())->toArray(); } @@ -80,7 +81,7 @@ public function stopByServiceNameOrContainerId(string $serviceNameOrContainerId) }); if ($containersByServiceName->isEmpty()) { - $this->start($serviceNameOrContainerId); + $this->info('No containers found for ' . $serviceNameOrContainerId); return; } @@ -90,20 +91,13 @@ public function stopByServiceNameOrContainerId(string $serviceNameOrContainerId) return; } - $selectedContainer = $this->loadMenu($containersByServiceName->map(function ($item) { - $label = $item['container']['container_id'] . ' - ' . $item['label']; - + $containerId = $this->loadMenu($containersByServiceName->mapWithKeys(function ($item) { return [ - $label, - $this->loadMenuItem($item['container'], $label), + $item['container']['container_id'] => $item['label'] ]; })->all()); - if (! $selectedContainer) { - return; - } - - $this->stop($selectedContainer); + $this->stop($containerId); } public function stop(string $container): void @@ -117,84 +111,9 @@ public function stop(string $container): void private function loadMenu($stoppableContainers) { - if ($this->environment->isWindowsOs()) { - return $this->windowsMenu($stoppableContainers); - } - - return $this->defaultMenu($stoppableContainers); - } - - private function defaultMenu($stoppableContainers) - { - return $this->menu(self::MENU_TITLE) - ->addItems($stoppableContainers) - ->addLineBreak('', 1) - ->open(); - } - - private function windowsMenu($stoppableContainers) - { - if (! $stoppableContainers) { - return; - } - - $choices = Arr::flatten($stoppableContainers); - $choices = Arr::where($choices, function ($value, $key) { - return is_string($value); - }); - array_push($choices, 'Exit'); - - $choice = $this->choice(self::MENU_TITLE, array_values($choices)); - - if (Str::contains($choice, 'Exit')) { - return; - } - - $chosenStoppableContainer = Arr::where($stoppableContainers, function ($value, $key) use ($choice) { - return $value[0] === $choice; - }); - - return call_user_func(array_values($chosenStoppableContainer)[0][1]); - } - - private function loadMenuItem($container, $label): callable - { - if ($this->environment->isWindowsOs()) { - return $this->windowsMenuItem($container, $label); - } - - return $this->defaultMenuItem($container, $label); - } - - private function windowsMenuItem($container, $label): callable - { - return function () use ($container, $label) { - $this->stop($label); - - $stoppableContainers = $this->stoppableContainers(); - - return $this->windowsMenu($stoppableContainers); - }; - } - - private function defaultMenuItem($container, $label): callable - { - return function (CliMenu $menu) use ($container, $label) { - $this->stop($menu->getSelectedItem()->getText()); - - foreach ($menu->getItems() as $item) { - if ($item->getText() === $label) { - $menu->removeItem($item); - } - } - - if (count($menu->getItems()) === 3) { - $menu->close(); - - return; - } - - $menu->redraw(); - }; + return select( + label: self::MENU_TITLE, + options: $stoppableContainers + ); } } diff --git a/composer.json b/composer.json index c8e89a08..41753800 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,6 @@ "guzzlehttp/guzzle": "^7.5", "laravel-zero/framework": "^11.0", "mockery/mockery": "^1.3.1", - "nunomaduro/laravel-console-menu": "^3.4", "phpunit/phpunit": "^10.5", "squizlabs/php_codesniffer": "^3.5", "tightenco/tlint": "^9.3" diff --git a/tests/Feature/DisableCommandTest.php b/tests/Feature/DisableCommandTest.php index cdca56ee..3ab2d605 100644 --- a/tests/Feature/DisableCommandTest.php +++ b/tests/Feature/DisableCommandTest.php @@ -3,10 +3,7 @@ namespace Tests\Feature; use App\Shell\Docker; -use Illuminate\Console\Command; use Illuminate\Support\Collection; -use NunoMaduro\LaravelConsoleMenu\Menu; -use PHPUnit\Framework\Assert; use Tests\TestCase; class DisableCommandTest extends TestCase diff --git a/tests/Feature/StartCommandTest.php b/tests/Feature/StartCommandTest.php index d5e0a641..3e9176a2 100644 --- a/tests/Feature/StartCommandTest.php +++ b/tests/Feature/StartCommandTest.php @@ -3,31 +3,18 @@ namespace Tests\Feature; use App\Shell\Docker; -use Illuminate\Console\Command; use Illuminate\Support\Collection; -use NunoMaduro\LaravelConsoleMenu\Menu; -use PHPUnit\Framework\Assert; use Tests\TestCase; class StartCommandTest extends TestCase { - function isWindows() - { - return PHP_OS_FAMILY === 'Windows'; - } - - function isLinux() - { - return PHP_OS_FAMILY === 'Linux'; - } - /** @test */ function it_can_start_a_service_from_menu() { $services = Collection::make([ [ 'container_id' => $containerId = '12345', - 'names' => $containerName = 'TO--mysql--8.0.22--3306', + 'names' => 'TO--mysql--8.0.22--3306', 'status' => 'Exited (0) 8 days ago', 'ports' => '', 'base_alias' => 'mysql', @@ -35,11 +22,6 @@ function it_can_start_a_service_from_menu() ], ]); - $menuItems = [ - $mysql = $containerId . ' - ' . $containerName, - 'Exit', - ]; - $this->mock(Docker::class, function ($mock) use ($services, $containerId) { $mock->shouldReceive('isInstalled')->andReturn(true); $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); @@ -47,29 +29,9 @@ function it_can_start_a_service_from_menu() $mock->shouldReceive('startContainer')->with($containerId); }); - if ($this->isWindows()) { - $this->artisan('start') - ->expectsChoice('Takeout containers to start', $mysql, $menuItems) - ->assertExitCode(0); - } else { - $menuMock = $this->mock(Menu::class, function ($mock) use ($mysql) { - $mock->shouldReceive('setTitleSeparator')->andReturnSelf(); - $mock->shouldReceive('addItems')->andReturnSelf(); - $mock->shouldReceive('addLineBreak')->andReturnSelf(); - $mock->shouldReceive('open')->andReturn($mysql)->once(); - }); - - Command::macro( - 'menu', - function (string $title) use ($menuMock) { - Assert::assertEquals('Takeout containers to start', $title); - - return $menuMock; - } - ); - - $this->artisan('start'); - } + $this->artisan('start') + ->expectsQuestion('Takeout containers to start', $containerId) + ->assertExitCode(0); } /** @test */ @@ -102,8 +64,8 @@ function it_can_start_containers_by_name_when_there_are_multiple() { $services = Collection::make([ [ - 'container_id' => $firstContainerId = '12345', - 'names' => $firstContainerName = 'TO--mysql--8.0.22--3306', + 'container_id' => '12345', + 'names' => 'TO--mysql--8.0.22--3306', 'status' => 'Exited (0) 8 days ago', 'ports' => '', 'base_alias' => 'mysql', @@ -111,7 +73,7 @@ function it_can_start_containers_by_name_when_there_are_multiple() ], [ 'container_id' => $secondContainerId = '67890', - 'names' => $secondContainerName = 'TO--mysql--8.0.20-3306', + 'names' => 'TO--mysql--8.0.20-3306', 'status' => 'Exited (0) 8 days ago', 'ports' => '', 'base_alias' => 'mysql', @@ -126,34 +88,8 @@ function it_can_start_containers_by_name_when_there_are_multiple() $mock->shouldReceive('startContainer')->with($secondContainerId)->once(); }); - $menuItems = [ - $firstContainerId . ' - ' . $firstContainerName, - $mysql = $secondContainerId . ' - ' . $secondContainerName, - 'Exit', - ]; - - if ($this->isWindows()) { - $this->artisan('start') - ->expectsChoice('Takeout containers to start', $mysql, $menuItems) - ->assertExitCode(0); - } else { - $menuMock = $this->mock(Menu::class, function ($mock) use ($mysql) { - $mock->shouldReceive('setTitleSeparator')->andReturnSelf(); - $mock->shouldReceive('addItems')->andReturnSelf(); - $mock->shouldReceive('addLineBreak')->andReturnSelf(); - $mock->shouldReceive('open')->andReturn($mysql)->once(); - }); - - Command::macro( - 'menu', - function (string $title) use ($menuMock) { - Assert::assertEquals('Takeout containers to start', $title); - - return $menuMock; - } - ); - - $this->artisan('start', ['containerId' => ['mysql']]); - } + $this->artisan('start') + ->expectsQuestion('Takeout containers to start', $secondContainerId) + ->assertExitCode(0); } } diff --git a/tests/Feature/StopCommandTest.php b/tests/Feature/StopCommandTest.php index ab85955c..e686c075 100644 --- a/tests/Feature/StopCommandTest.php +++ b/tests/Feature/StopCommandTest.php @@ -3,31 +3,18 @@ namespace Tests\Feature; use App\Shell\Docker; -use Illuminate\Console\Command; use Illuminate\Support\Collection; -use NunoMaduro\LaravelConsoleMenu\Menu; -use PHPUnit\Framework\Assert; use Tests\TestCase; class StopCommandTest extends TestCase { - function isWindows() - { - return PHP_OS_FAMILY === 'Windows'; - } - - function isLinux() - { - return PHP_OS_FAMILY === 'Linux'; - } - /** @test */ function it_can_stop_a_service_from_menu() { $services = Collection::make([ [ 'container_id' => $containerId = '12345', - 'names' => $containerName = 'TO--mysql--8.0.22--3306', + 'names' => 'TO--mysql--8.0.22--3306', 'status' => 'Up 27 minutes', 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', 'base_alias' => 'mysql', @@ -35,11 +22,6 @@ function it_can_stop_a_service_from_menu() ], ]); - $menuItems = [ - $mysql = $containerId . ' - ' . $containerName, - 'Exit', - ]; - $this->mock(Docker::class, function ($mock) use ($services, $containerId) { $mock->shouldReceive('isInstalled')->andReturn(true); $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); @@ -47,29 +29,9 @@ function it_can_stop_a_service_from_menu() $mock->shouldReceive('stopContainer')->with($containerId); }); - if ($this->isWindows()) { - $this->artisan('stop') - ->expectsChoice('Takeout containers to stop', $mysql, $menuItems) - ->assertExitCode(0); - } else { - $menuMock = $this->mock(Menu::class, function ($mock) use ($mysql) { - $mock->shouldReceive('setTitleSeparator')->andReturnSelf(); - $mock->shouldReceive('addItems')->andReturnSelf(); - $mock->shouldReceive('addLineBreak')->andReturnSelf(); - $mock->shouldReceive('open')->andReturn($mysql)->once(); - }); - - Command::macro( - 'menu', - function (string $title) use ($menuMock, $services) { - Assert::assertEquals('Takeout containers to stop', $title); - - return $menuMock; - } - ); - - $this->artisan('stop'); - } + $this->artisan('stop') + ->expectsQuestion('Takeout containers to stop', $containerId) + ->assertExitCode(0); } /** @test */ @@ -102,8 +64,8 @@ function it_can_stop_a_service_from_menu_when_there_are_multiple() { $services = Collection::make([ [ - 'container_id' => $firstContainerId = '12345', - 'names' => $firstContainerName = 'TO--mysql--8.0.22--3306', + 'container_id' => '12345', + 'names' => 'TO--mysql--8.0.22--3306', 'status' => 'Up 27 minutes', 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', 'base_alias' => 'mysql', @@ -111,7 +73,7 @@ function it_can_stop_a_service_from_menu_when_there_are_multiple() ], [ 'container_id' => $secondContainerId = '67890', - 'names' => $secondContainerName = 'TO--mysql--8.0.20--3306', + 'names' => 'TO--mysql--8.0.20--3306', 'status' => 'Up 27 minutes', 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', 'base_alias' => 'mysql', @@ -119,12 +81,6 @@ function it_can_stop_a_service_from_menu_when_there_are_multiple() ], ]); - $menuItems = [ - $firstContainerId . ' - ' . $firstContainerName, - $mysql = $secondContainerId . ' - ' . $secondContainerName, - 'Exit', - ]; - $this->mock(Docker::class, function ($mock) use ($services, $secondContainerId) { $mock->shouldReceive('isInstalled')->andReturn(true); $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); @@ -132,28 +88,8 @@ function it_can_stop_a_service_from_menu_when_there_are_multiple() $mock->shouldReceive('stopContainer')->with($secondContainerId)->once(); }); - if ($this->isWindows()) { - $this->artisan('stop') - ->expectsChoice('Takeout containers to stop', $mysql, $menuItems) - ->assertExitCode(0); - } else { - $menuMock = $this->mock(Menu::class, function ($mock) use ($mysql) { - $mock->shouldReceive('setTitleSeparator')->andReturnSelf(); - $mock->shouldReceive('addItems')->andReturnSelf(); - $mock->shouldReceive('addLineBreak')->andReturnSelf(); - $mock->shouldReceive('open')->andReturn($mysql)->once(); - }); - - Command::macro( - 'menu', - function (string $title) use ($menuMock, $services) { - Assert::assertEquals('Takeout containers to stop', $title); - - return $menuMock; - } - ); - - $this->artisan('stop', ['containerId' => ['mysql']]); - } + $this->artisan('stop') + ->expectsQuestion('Takeout containers to stop', $secondContainerId) + ->assertExitCode(0); } } From 9d1a7e4fc70da69a0592447f5374085845f3d3c8 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 16 Dec 2024 20:39:55 -0300 Subject: [PATCH 08/55] Lint --- app/Commands/DisableCommand.php | 2 +- app/Commands/StopCommand.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Commands/DisableCommand.php b/app/Commands/DisableCommand.php index ebbde555..d37d3222 100644 --- a/app/Commands/DisableCommand.php +++ b/app/Commands/DisableCommand.php @@ -128,7 +128,7 @@ public function disableByContainerId(string $containerId): void if (confirm($question)) { $this->task('Stopping Docker service ', $this->docker->stopDockerService()); - } + } } } catch (Throwable $e) { $this->error('Disabling failed! Error: ' . $e->getMessage()); diff --git a/app/Commands/StopCommand.php b/app/Commands/StopCommand.php index 3c72fe1d..d4be38f3 100644 --- a/app/Commands/StopCommand.php +++ b/app/Commands/StopCommand.php @@ -31,7 +31,6 @@ public function handle(Docker $docker, Environment $environment): void $containers = $this->argument('containerId'); if (filled($containers)) { - foreach ($containers as $container) { $this->stopByServiceNameOrContainerId($container); } From 564427d3c00acd0b4db9120b00382adb69f604b7 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Mon, 16 Dec 2024 21:01:24 -0300 Subject: [PATCH 09/55] refactor the disable command to remove the menu dependency Co-authored-by: Guillermo Cava --- app/Commands/DisableCommand.php | 73 ++++++++++++---------------- tests/Feature/DisableCommandTest.php | 4 +- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/app/Commands/DisableCommand.php b/app/Commands/DisableCommand.php index d37d3222..4783469b 100644 --- a/app/Commands/DisableCommand.php +++ b/app/Commands/DisableCommand.php @@ -5,8 +5,10 @@ use App\InitializesCommands; use App\Shell\Docker; use App\Shell\Environment; +use Illuminate\Support\Collection; use LaravelZero\Framework\Commands\Command; use Throwable; +use Illuminate\Support\Str; use function Laravel\Prompts\confirm; use function Laravel\Prompts\select; @@ -28,17 +30,18 @@ public function handle(Docker $docker, Environment $environment) $this->docker = $docker; $this->environment = $environment; $this->initializeCommand(); - $this->disableableServices = $this->disableableServices(); + + $disableableServices = $this->disableableServices(); if ($this->option('all')) { - foreach ($this->disableableServices as $containerId => $name) { + $disableableServices->keys()->each(function ($containerId) { $this->disableByContainerId($containerId); - } + }); return; } - if (empty($this->disableableServices)) { + if ($disableableServices->isEmpty()) { $this->info("There are no containers to disable.\n"); return; @@ -46,71 +49,57 @@ public function handle(Docker $docker, Environment $environment) if (filled($services = $this->argument('serviceNames'))) { foreach ($services as $service) { - $this->disableByServiceName($service); + $this->disableByServiceName($service, $disableableServices); } return; } - $this->showDisableServiceMenu(); + $this->disableByContainerId( + $this->selectOptions($disableableServices), + ); } - public function disableableServices(): array + private function disableableServices(): Collection { return $this->docker->takeoutContainers()->mapWithKeys(function ($container) { return [$container['container_id'] => str_replace('TO--', '', $container['names'])]; - })->toArray(); + }); } - public function disableByServiceName(string $service): void + private function disableByServiceName(string $service, Collection $disableableServices): void { - $serviceMatches = collect($this->disableableServices) + $serviceMatches = collect($disableableServices) ->filter(function ($containerName) use ($service) { - return substr($containerName, 0, strlen($service)) === $service; + return Str::startsWith($service, $containerName); }); - switch ($serviceMatches->count()) { - case 0: - $this->error("\nCannot find a Takeout-managed instance of {$service}."); + if ($serviceMatches->isEmpty()) { + $this->error("\nCannot find a Takeout-managed instance of {$service}."); - return; - case 1: - $serviceContainerId = $serviceMatches->flip()->first(); - break; - default: // > 1 - $serviceContainerId = $this->selectMenu($this->disableableServices); + return; + } - if (! $serviceContainerId) { - return; - } + if ($serviceMatches->count() === 1) { + $this->disableByContainerId($serviceMatches->flip()->first()); + + return; } - $this->disableByContainerId($serviceContainerId); + $this->disableByContainerId( + $this->selectOptions($disableableServices), + ); } - public function showDisableServiceMenu(): void + private function selectOptions(Collection $disableableServices) { - $serviceContainerId = select( + return select( label: self::MENU_TITLE, - options: $this->disableableServices + options: $disableableServices ); - $this->disableByContainerId($serviceContainerId); - } - - private function selectMenu($disableableServices): ?string - { - return $this->defaultMenu($disableableServices); - } - - private function defaultMenu($disableableServices): ?string - { - return $this->menu(self::MENU_TITLE, $disableableServices) - ->addLineBreak('', 1) - ->setPadding(2, 5) - ->open(); } - public function disableByContainerId(string $containerId): void + private function disableByContainerId(string $containerId): void { try { $volumeName = $this->docker->attachedVolumeName($containerId); diff --git a/tests/Feature/DisableCommandTest.php b/tests/Feature/DisableCommandTest.php index 3ab2d605..82bf19da 100644 --- a/tests/Feature/DisableCommandTest.php +++ b/tests/Feature/DisableCommandTest.php @@ -14,10 +14,10 @@ function it_can_disable_a_service_from_menu() $services = Collection::make([ [ 'container_id' => $postgressId = '1234', - 'names' => $postgressName = 'postgress', + 'names' => 'postgress', ], [ - 'container_id' =>'12345', + 'container_id' => '12345', 'names' => 'meilisearch', ], ]); From 64dd734571e0de969a3ccfadc5552cb7d23a08f1 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Mon, 16 Dec 2024 20:38:57 -0500 Subject: [PATCH 10/55] refactors disable, enable, start n stop cmds --- app/Commands/DisableCommand.php | 2 +- app/Commands/EnableCommand.php | 48 ++++----- app/Commands/StartCommand.php | 150 +++++----------------------- app/Commands/StopCommand.php | 65 +++++------- tests/Feature/EnableCommandTest.php | 13 +-- 5 files changed, 75 insertions(+), 203 deletions(-) diff --git a/app/Commands/DisableCommand.php b/app/Commands/DisableCommand.php index 4783469b..a2385e90 100644 --- a/app/Commands/DisableCommand.php +++ b/app/Commands/DisableCommand.php @@ -71,7 +71,7 @@ private function disableByServiceName(string $service, Collection $disableableSe { $serviceMatches = collect($disableableServices) ->filter(function ($containerName) use ($service) { - return Str::startsWith($service, $containerName); + return Str::startsWith($containerName, $service); }); if ($serviceMatches->isEmpty()) { diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index 0ea77b90..1cbfcbb2 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -5,6 +5,7 @@ use App\InitializesCommands; use App\Services; use App\Shell\Environment; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\App; use LaravelZero\Framework\Commands\Command; use function Laravel\Prompts\search; @@ -45,12 +46,11 @@ public function handle(Environment $environment, Services $services): void return; } - $option = $this->selectService(); - if (! $option) { + $service = $this->selectService($this->availableServices()); + if (! $service) { return; } - - $this->enable($option, $useDefaults, $passthroughOptions); + $this->enable($service, $useDefaults, $passthroughOptions); } /** @@ -58,7 +58,7 @@ public function handle(Environment $environment, Services $services): void * $this->argument, we have to do our own manual overriding for testing scenarios, * because pulling $_SERVER['argv'] won't give the right results in testing. */ - public function serverArguments(): array + private function serverArguments(): array { if (App::environment() === 'testing') { $string = array_merge(['takeout', 'enable'], $this->argument('serviceNames')); @@ -117,45 +117,36 @@ public function removeOptions(array $arguments): array return array_slice($arguments, $start); } - private function selectService(): ?string + private function availableServices(): Collection { - return $this->defaultMenu(); - } - - private function defaultMenu(): ?string - { - $servicesList = collect($this->enableableServicesByCategory())->flatMap(function ($services, $category) { - return collect($this->menuItemsForServices($services))->mapWithKeys(function ($row, $key) use ($category) { + return $this->enableableServicesByCategory()->flatMap(function ($services, $category) { + return $this->menuItemsForServices($services)->mapWithKeys(function ($row, $key) use ($category) { return [$key => "{$category}: {$row}"]; })->toArray(); - })->toArray(); + }); + } + private function selectService(Collection $servicesList): ?string + { return search( label: self::MENU_TITLE, options: fn (string $value) => strlen($value) > 0 - ? collect($servicesList)->filter(function ($row) use ($value) { - return str($row)->lower()->contains(str($value)->lower()); + ? $servicesList->filter(function ($row) use ($value) { + return str($row)->lower()->contains(str($value)->lower()); })->toArray() - : $servicesList, + : $servicesList->toArray(), scroll: 10 ); } - private function menuItemsForServices($services): array + private function menuItemsForServices($services): Collection { return collect($services)->mapWithKeys(function ($service) { return [$service['shortName'] => $service['name']]; - })->toArray(); + }); } - public function enableableServices(): array - { - return collect($this->services->all())->mapWithKeys(function ($fqcn, $shortName) { - return [$shortName => $fqcn::name()]; - })->toArray(); - } - - public function enableableServicesByCategory(): array + private function enableableServicesByCategory(): Collection { return collect($this->services->all()) ->mapToGroups(function ($fqcn, $shortName) { @@ -166,8 +157,7 @@ public function enableableServicesByCategory(): array ], ]; }) - ->sortKeys() - ->toArray(); + ->sortKeys(); } public function enable( diff --git a/app/Commands/StartCommand.php b/app/Commands/StartCommand.php index 1928d086..e4f83e87 100644 --- a/app/Commands/StartCommand.php +++ b/app/Commands/StartCommand.php @@ -5,10 +5,9 @@ use App\InitializesCommands; use App\Shell\Docker; use App\Shell\Environment; -use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; -use PhpSchool\CliMenu\CliMenu; use function Laravel\Prompts\select; @@ -28,86 +27,63 @@ public function handle(Docker $docker, Environment $environment): void $this->docker = $docker; $this->environment = $environment; $this->initializeCommand(); + $startableContainers = $this->startableContainers(); - $containers = $this->argument('containerId'); - - if (filled($containers)) { - foreach ($containers as $container) { - $this->startByServiceNameOrContainerId($container); - } + if ($this->option('all')) { + $startableContainers->keys()->each(function ($containerId) { + $this->start($containerId); + }); return; } - if ($this->option('all')) { - foreach ($this->docker->startableTakeoutContainers() as $startableContainer) { - $this->start($startableContainer['container_id']); - } + if ($startableContainers->isEmpty()) { + $this->info("No Takeout containers available to start.\n"); return; } - if (! $startableContainers = $this->startableContainers()) { - $this->info("No Takeout containers available to start.\n"); + if (filled($services = $this->argument('containerId'))) { + foreach ($services as $service) { + $this->startByServiceNameOrContainerId($service, $startableContainers); + } return; } - $this->loadMenu($startableContainers); + $this->start($this->selectOptions($startableContainers)); } - public function startableContainers(): array + public function startableContainers(): Collection { return $this->docker->startableTakeoutContainers()->mapWithKeys(function ($container) { - return [ - $container['container_id'] => $container['names'], - ]; - }, collect())->toArray(); + return [$container['container_id'] => str_replace('TO--', '', $container['names'])]; + }); } - public function startByServiceNameOrContainerId(string $serviceNameOrContainerId): void + public function startByServiceNameOrContainerId(string $service, Collection $startableContainers): void { - $containersByServiceName = $this->docker->startableTakeoutContainers() - ->map(function ($container) { - return [ - 'container' => $container, - 'label' => str_replace('TO--', '', $container['names']), - ]; - }) - ->filter(function ($item) use ($serviceNameOrContainerId) { - return Str::startsWith($item['label'], $serviceNameOrContainerId); + $containersByServiceName = $startableContainers + ->filter(function ($serviceName) use ($service) { + return Str::startsWith($serviceName, $service); }); // If we don't get any container by the service name, that probably means // the user is trying to start a container using its container ID, so // we will just forward that down to the underlying start method. - if ($containersByServiceName->isEmpty()) { - $this->start($serviceNameOrContainerId); + $this->info('No containers found for ' . $service); return; } if ($containersByServiceName->count() === 1) { - $this->start($containersByServiceName->first()['container']['container_id']); + $this->start($containersByServiceName->keys()->first()); return; } - $selectedItem = $this->loadMenu($containersByServiceName->map(function ($item) { - $label = $item['container']['container_id'] . ' - ' . $item['label']; - - return [ - $label, - $this->loadMenuItem($item['container'], $label), - ]; - })->all()); - - if (! $selectedItem) { - return; - } - - $this->start($selectedItem); + $this->start($this->selectOptions($containersByServiceName)); } public function start(string $container): void @@ -119,87 +95,11 @@ public function start(string $container): void $this->docker->startContainer($container); } - private function loadMenu($startableContainers) + private function selectOptions(Collection $startableContainers) { - $container = select( + return select( label: self::MENU_TITLE, options: $startableContainers ); - - $this->docker->startContainer($container); - } - - private function defaultMenu($startableContainers) - { - return $this->menu(self::MENU_TITLE) - ->addItems($startableContainers) - ->addLineBreak('', 1) - ->open(); - } - - private function windowsMenu($startableContainers) - { - if (! $startableContainers) { - return; - } - - $choices = Arr::flatten($startableContainers); - $choices = Arr::where($choices, function ($value, $key) { - return is_string($value); - }); - array_push($choices, 'Exit'); - - $choice = $this->choice(self::MENU_TITLE, array_values($choices)); - - if (Str::contains($choice, 'Exit')) { - return; - } - - $chosenStartableContainer = Arr::where($startableContainers, function ($value, $key) use ($choice) { - return $value[0] === $choice; - }); - - return call_user_func(array_values($chosenStartableContainer)[0][1]); - } - - private function loadMenuItem($container, $label): callable - { - if ($this->environment->isWindowsOs()) { - return $this->windowsMenuItem($container, $label); - } - - return $this->defaultMenuItem($container, $label); - } - - private function windowsMenuItem($container, $label): callable - { - return function () use ($container, $label) { - $this->start($label); - - $startableContainers = $this->startableContainers(); - - return $this->windowsMenu($startableContainers); - }; - } - - private function defaultMenuItem($container, $label): callable - { - return function (CliMenu $menu) use ($container, $label) { - $this->start($menu->getSelectedItem()->getText()); - - foreach ($menu->getItems() as $item) { - if ($item->getText() === $label) { - $menu->removeItem($item); - } - } - - if (count($menu->getItems()) === 3) { - $menu->close(); - - return; - } - - $menu->redraw(); - }; } } diff --git a/app/Commands/StopCommand.php b/app/Commands/StopCommand.php index d4be38f3..ca1219b6 100644 --- a/app/Commands/StopCommand.php +++ b/app/Commands/StopCommand.php @@ -5,6 +5,7 @@ use App\InitializesCommands; use App\Shell\Docker; use App\Shell\Environment; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; @@ -27,76 +28,60 @@ public function handle(Docker $docker, Environment $environment): void $this->docker = $docker; $this->environment = $environment; $this->initializeCommand(); + $stoppableContainers = $this->stoppableContainers(); - $containers = $this->argument('containerId'); - - if (filled($containers)) { - foreach ($containers as $container) { - $this->stopByServiceNameOrContainerId($container); - } + if ($this->option('all')) { + $stoppableContainers->keys()->each(function ($containerId) { + $this->stop($containerId); + }); return; } - if ($this->option('all')) { - foreach ($this->docker->stoppableTakeoutContainers() as $stoppableContainer) { - $this->stop($stoppableContainer['container_id']); - } + if ($stoppableContainers->isEmpty()) { + $this->info("No Takeout containers available to stop.\n"); return; } - if (! $stoppableContainers = $this->stoppableContainers()) { - $this->info("No Takeout containers available to stop.\n"); + if (filled($services = $this->argument('containerId'))) { + foreach ($services as $service) { + $this->stopByServiceNameOrContainerId($service, $stoppableContainers); + } return; } - $containerId = $this->loadMenu($stoppableContainers); - - $this->stop($containerId); + $this->stop($this->selectOptions($stoppableContainers)); } - public function stoppableContainers(): array + private function stoppableContainers(): Collection { return $this->docker->stoppableTakeoutContainers()->mapWithKeys(function ($container) { - return [ - $container['container_id'] => $container['names'], - ]; - }, collect())->toArray(); + return [$container['container_id'] => str_replace('TO--', '', $container['names'])]; + }); } - public function stopByServiceNameOrContainerId(string $serviceNameOrContainerId): void + private function stopByServiceNameOrContainerId(string $service, Collection $stoppableContainers): void { - $containersByServiceName = $this->docker->stoppableTakeoutContainers() - ->map(function ($container) { - return [ - 'container' => $container, - 'label' => str_replace('TO--', '', $container['names']), - ]; - }) - ->filter(function ($item) use ($serviceNameOrContainerId) { - return Str::startsWith($item['label'], $serviceNameOrContainerId); + $containersByServiceName = $stoppableContainers + ->filter(function ($containerName) use ($service) { + return Str::startsWith($containerName, $service); }); if ($containersByServiceName->isEmpty()) { - $this->info('No containers found for ' . $serviceNameOrContainerId); + $this->info('No containers found for ' . $service); return; } if ($containersByServiceName->count() === 1) { - $this->stop($containersByServiceName->first()['container']['container_id']); + $this->stop($containersByServiceName->keys()->first()); + return; } - $containerId = $this->loadMenu($containersByServiceName->mapWithKeys(function ($item) { - return [ - $item['container']['container_id'] => $item['label'] - ]; - })->all()); - - $this->stop($containerId); + $this->stop($this->selectOptions($containersByServiceName)); } public function stop(string $container): void @@ -108,7 +93,7 @@ public function stop(string $container): void $this->docker->stopContainer($container); } - private function loadMenu($stoppableContainers) + private function selectOptions($stoppableContainers) { return select( label: self::MENU_TITLE, diff --git a/tests/Feature/EnableCommandTest.php b/tests/Feature/EnableCommandTest.php index ea695478..7ecf0fca 100644 --- a/tests/Feature/EnableCommandTest.php +++ b/tests/Feature/EnableCommandTest.php @@ -35,23 +35,20 @@ function it_can_enable_a_service_from_menu() }); $menuItems = [ - 'Database: PostgreSQL', - 'Search: MeiliSearch', - 'meilisearch', - $postgres, + $postgres => 'Database: PostgreSQL', + 'meilisearch' => 'Search: MeiliSearch', ]; $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', '', $menuItems) + ->expectsChoice('Takeout containers to enable', '', $menuItems, true) ->assertExitCode(0); $menuItems = [ - 'Database: PostgreSQL', - $postgres, + $postgres => 'Database: PostgreSQL', ]; $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', $postgres, $menuItems) + ->expectsChoice('Takeout containers to enable', $postgres, $menuItems, true) ->assertExitCode(0); } From 80d54866492a479b0865d672c8666ddca5aac7fb Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 15:32:09 -0300 Subject: [PATCH 11/55] Use Prompts for asking the additional questions --- app/Commands/EnableCommand.php | 10 ++++----- app/InitializesCommands.php | 37 ++++++++++++++++++++++++++++++++++ app/Services/BaseService.php | 2 +- app/Shell/Shell.php | 2 +- app/WritesToConsole.php | 17 ++++++++++------ 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index 1cbfcbb2..488f8749 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -47,9 +47,7 @@ public function handle(Environment $environment, Services $services): void } $service = $this->selectService($this->availableServices()); - if (! $service) { - return; - } + $this->enable($service, $useDefaults, $passthroughOptions); } @@ -83,7 +81,7 @@ private function serverArguments(): array * @param array $arguments * @return array */ - public function extractPassthroughOptions(array $arguments): array + private function extractPassthroughOptions(array $arguments): array { if (! in_array('--', $arguments)) { return []; @@ -99,7 +97,7 @@ public function extractPassthroughOptions(array $arguments): array * @param array $arguments * @return array */ - public function removeOptions(array $arguments): array + private function removeOptions(array $arguments): array { $arguments = collect($arguments) ->reject(fn ($argument) => str_starts_with($argument, '--') && strlen($argument) > 2) @@ -160,7 +158,7 @@ private function enableableServicesByCategory(): Collection ->sortKeys(); } - public function enable( + private function enable( string $service, bool $useDefaults = false, array $passthroughOptions = [], diff --git a/app/InitializesCommands.php b/app/InitializesCommands.php index 113c43e2..da62d558 100644 --- a/app/InitializesCommands.php +++ b/app/InitializesCommands.php @@ -6,6 +6,8 @@ use App\Exceptions\DockerNotAvailableException; use App\Shell\Docker; +use function Laravel\Prompts\text; + trait InitializesCommands { public function initializeCommand(): void @@ -22,4 +24,39 @@ public function initializeCommand(): void throw new DockerNotAvailableException; } } + + public function askPromptQuestion(string $question, $default = null) + { + return text(label: $question, default: $default); + } + + public function errorPrompt(string $message): void + { + $this->components->error($message); + } + + public function alertPrompt(string $message): void + { + $this->components->alert($message); + } + + public function warnPrompt(string $message): void + { + $this->components->warn($message); + } + + public function linePrompt(string $message): void + { + $this->components->line($message); + } + + public function infoPrompt(string $message): void + { + $this->components->info($message); + } + + public function taskPrompt(string $message, $callable): void + { + $this->components->task($message, $callable); + } } diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php index 62a68a6e..9aefdd05 100644 --- a/app/Services/BaseService.php +++ b/app/Services/BaseService.php @@ -199,7 +199,7 @@ protected function askQuestion(array $prompt, $useDefaults = false): void $this->promptResponses[$prompt['shortname']] = $prompt['default'] ?? null; if (! $useDefaults) { - $this->promptResponses[$prompt['shortname']] = app('console')->ask(sprintf($prompt['prompt'], $this->imageName), $prompt['default'] ?? null); + $this->promptResponses[$prompt['shortname']] = $this->ask(sprintf($prompt['prompt'], $this->imageName), $prompt['default'] ?? null); } } diff --git a/app/Shell/Shell.php b/app/Shell/Shell.php index f56d16ec..89333a26 100644 --- a/app/Shell/Shell.php +++ b/app/Shell/Shell.php @@ -19,7 +19,7 @@ public function exec(string $command, array $parameters = [], bool $quiet = fals $didAnything = false; $process = $this->buildProcess($command); - $process->run(function ($type, $buffer) use ($quiet, $didAnything) { + $process->run(function ($type, $buffer) use ($quiet, &$didAnything) { if (empty($buffer) || $buffer === PHP_EOL || $quiet) { return; } diff --git a/app/WritesToConsole.php b/app/WritesToConsole.php index 4e7ddc17..5f2bcb52 100644 --- a/app/WritesToConsole.php +++ b/app/WritesToConsole.php @@ -6,31 +6,36 @@ trait WritesToConsole { public function alert(string $message): void { - app('console')->alert($message); + app('console')->alertPrompt($message); } public function warn(string $message): void { - app('console')->warn($message); + app('console')->warnPrompt($message); } public function error(string $message): void { - app('console')->error($message); + app('console')->errorPrompt($message); } public function line(string $message): void { - app('console')->line($message); + app('console')->linePrompt($message); } public function info(string $message): void { - app('console')->info($message); + app('console')->infoPrompt($message); } public function task(string $message, $callable): void { - app('console')->task($message, $callable); + app('console')->taskPrompt($message, $callable); + } + + public function ask(string $message, $default = null) + { + return app('console')->askPromptQuestion($message, $default); } } From 69b4bf541ab1ca163b8535305d08df92857ae660 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 15:52:26 -0300 Subject: [PATCH 12/55] Fix tests --- app/Commands/EnableCommand.php | 10 +++++----- tests/Feature/BaseServiceTest.php | 20 ++++++++++---------- tests/Feature/EnableCommandTest.php | 13 ++----------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index 488f8749..6d457377 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -81,7 +81,7 @@ private function serverArguments(): array * @param array $arguments * @return array */ - private function extractPassthroughOptions(array $arguments): array + public function extractPassthroughOptions(array $arguments): array { if (! in_array('--', $arguments)) { return []; @@ -97,10 +97,10 @@ private function extractPassthroughOptions(array $arguments): array * @param array $arguments * @return array */ - private function removeOptions(array $arguments): array + public function removeOptions(array $arguments): array { $arguments = collect($arguments) - ->reject(fn ($argument) => str_starts_with($argument, '--') && strlen($argument) > 2) + ->reject(fn($argument) => str_starts_with($argument, '--') && strlen($argument) > 2) ->values() ->toArray(); @@ -128,9 +128,9 @@ private function selectService(Collection $servicesList): ?string { return search( label: self::MENU_TITLE, - options: fn (string $value) => strlen($value) > 0 + options: fn(string $value) => strlen($value) > 0 ? $servicesList->filter(function ($row) use ($value) { - return str($row)->lower()->contains(str($value)->lower()); + return str($row)->lower()->contains(str($value)->lower()); })->toArray() : $servicesList->toArray(), scroll: 10 diff --git a/tests/Feature/BaseServiceTest.php b/tests/Feature/BaseServiceTest.php index 44037150..d1118a6c 100644 --- a/tests/Feature/BaseServiceTest.php +++ b/tests/Feature/BaseServiceTest.php @@ -28,13 +28,13 @@ public function it_enables_services() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which host port would you like meilisearch to use?', $defaultPort) ->andReturn(7700); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('What is the Docker volume name?', 'meili_data') ->andReturn('meili_data'); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which tag (version) of meilisearch would you like to use?', 'latest') ->andReturn('v1.1.1'); $mock->shouldIgnoreMissing(); @@ -79,13 +79,13 @@ public function it_can_receive_a_custom_image_in_the_tag() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which host port would you like postgres to use?', $defaultPort) ->andReturn(5432); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which tag (version) of postgres would you like to use?', 'latest') ->andReturn('timescale/timescaledb:latest-pg12'); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('What is the Docker volume name?', 'postgres_data') ->andReturn('postgres_data'); $mock->shouldIgnoreMissing(); @@ -132,10 +132,10 @@ public function it_can_receive_container_arguments_via_passthrough() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which host port would you like _test_image to use?', $defaultPort) ->andReturn(12345); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which tag (version) of _test_image would you like to use?', 'latest') ->andReturn('latest'); $mock->shouldIgnoreMissing(); @@ -185,10 +185,10 @@ public function it_accepts_run_options_and_passthrough_options() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which host port would you like _test_image to use?', $defaultPort) ->andReturn(12345); - $mock->shouldReceive('ask') + $mock->shouldReceive('askPromptQuestion') ->with('Which tag (version) of _test_image would you like to use?', 'latest') ->andReturn('latest'); $mock->shouldIgnoreMissing(); diff --git a/tests/Feature/EnableCommandTest.php b/tests/Feature/EnableCommandTest.php index 7ecf0fca..9727b658 100644 --- a/tests/Feature/EnableCommandTest.php +++ b/tests/Feature/EnableCommandTest.php @@ -13,7 +13,7 @@ class EnableCommandTest extends TestCase { /** @test */ - function it_can_enable_a_service_from_menu() + function it_can_filter_options_based_on_search_term() { $services = [ 'meilisearch' => 'App\Services\MeiliSearch', @@ -34,21 +34,12 @@ function it_can_enable_a_service_from_menu() $mock->shouldReceive('enable')->once(); }); - $menuItems = [ - $postgres => 'Database: PostgreSQL', - 'meilisearch' => 'Search: MeiliSearch', - ]; - - $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', '', $menuItems, true) - ->assertExitCode(0); - $menuItems = [ $postgres => 'Database: PostgreSQL', ]; $this->artisan('enable') - ->expectsChoice('Takeout containers to enable', $postgres, $menuItems, true) + ->expectsSearch('Takeout containers to enable', $postgres, 'postgres', $menuItems) ->assertExitCode(0); } From 8ef351e220155ec1930122453d99a896e77b3522 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:23:59 -0300 Subject: [PATCH 13/55] Simplify the exceptions using prompts --- .../DockerContainerMissingException.php | 9 +-- app/Exceptions/DockerMissingException.php | 14 ++-- .../DockerNotAvailableException.php | 80 +++++++++---------- 3 files changed, 44 insertions(+), 59 deletions(-) diff --git a/app/Exceptions/DockerContainerMissingException.php b/app/Exceptions/DockerContainerMissingException.php index edb8a7a0..df1d7f60 100644 --- a/app/Exceptions/DockerContainerMissingException.php +++ b/app/Exceptions/DockerContainerMissingException.php @@ -2,9 +2,10 @@ namespace App\Exceptions; -use App\Shell\Shell; use Exception; +use function Laravel\Prompts\error; + class DockerContainerMissingException extends Exception { public function __construct(string $containerId) @@ -14,10 +15,6 @@ public function __construct(string $containerId) public function render($request = null): void { - $console = app('console'); - $shell = app(Shell::class); - - $console->line(''); - $console->line($shell->formatErrorMessage($this->getMessage())); + error($this->getMessage()); } } diff --git a/app/Exceptions/DockerMissingException.php b/app/Exceptions/DockerMissingException.php index c11cd2aa..49ee0002 100644 --- a/app/Exceptions/DockerMissingException.php +++ b/app/Exceptions/DockerMissingException.php @@ -2,20 +2,16 @@ namespace App\Exceptions; -use App\Shell\Shell; use Exception; +use function Laravel\Prompts\error; +use function Laravel\Prompts\note; + class DockerMissingException extends Exception { public function render($request = null): void { - $console = app('console'); - $shell = app(Shell::class); - - $console->line(''); - $console->line($shell->formatErrorMessage('Docker is not installed.')); - $console->line(''); - $console->line($shell->formatErrorMessage('Please visit https://docs.docker.com/get-docker/')); - $console->line($shell->formatErrorMessage('for information on how to install Docker for your machine.')); + error('Docker is not installed'); + note('Please visit https://docs.docker.com/get-docker/' . PHP_EOL . 'for information on how to install Docker for your machine.'); } } diff --git a/app/Exceptions/DockerNotAvailableException.php b/app/Exceptions/DockerNotAvailableException.php index fe0a5488..b8708e44 100644 --- a/app/Exceptions/DockerNotAvailableException.php +++ b/app/Exceptions/DockerNotAvailableException.php @@ -3,56 +3,48 @@ namespace App\Exceptions; use App\Shell\Environment; -use App\Shell\Shell; use Exception; +use function Laravel\Prompts\error; +use function Laravel\Prompts\note; + class DockerNotAvailableException extends Exception { public function render($request = null): void { - $console = app('console'); - $shell = app(Shell::class); - - $console->line(''); - $console->line($shell->formatErrorMessage('Docker is not available.')); - $console->line(''); - - if (in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'Windows'])) { - $osSpecificHelp = 'helpFor' . ucfirst(PHP_OS_FAMILY); - $this->$osSpecificHelp($console); + error('Docker is not available'); + + if (PHP_OS_FAMILY === 'Darwin') { + note(implode(PHP_EOL, [ + 'Open Docker for Mac or start it from the terminal running:', + '', + ' open --background -a Docker', + ])); + } elseif (PHP_OS_FAMILY === 'Windows') { + note('Open Docker for Windows to start the Docker service.'); + } else { + note(implode(PHP_EOL, [ + 'Verify that the Docker service is running:', + '', + ' sudo systemctl status docker', + '', + 'Start the Docker service:', + '', + ' sudo systemctl start docker', + ])); + + $environment = app(Environment::class); + + if (! $environment->userIsInDockerGroup()) { + error('You are not in the docker group.'); + note(implode(PHP_EOL, [ + 'You need to be in that group to run Docker as non-root. Add it by running:', + '', + ' sudo usermod -aG docker ${USER}', + '', + 'and restart your session.', + ])); + } } } - - protected function helpForDarwin($console) - { - $console->line('Open Docker for Mac or run:'); - $console->line(' open --background -a Docker'); - $console->line('to start the Docker service.'); - } - - protected function helpForLinux($console) - { - $environment = app(Environment::class); - $shell = app(Shell::class); - - $console->line('Verify that the Docker service is running:'); - $console->line(' sudo systemctl status docker'); - $console->line('Start the Docker service:'); - $console->line(' sudo systemctl start docker'); - - if (! $environment->userIsInDockerGroup()) { - $console->line(''); - $console->line($shell->formatErrorMessage('You are not in the docker group.')); - $console->line(''); - $console->line('This is required to run Docker as a non-root user.'); - $console->line('Add your user to the docker group by running:'); - $console->line(' sudo usermod -aG docker ${USER}'); - $console->line('and restart your session.'); - } - } - - protected function helpForWindows($console) - { - $console->line('Open Docker for Windows to start the Docker service.'); - } } From 55d93284143e3b5dcc695461eb2ec3af3013d299 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:33:21 -0300 Subject: [PATCH 14/55] Merge the default prompts with the extra ones and use Laravel Prompts for that --- app/InitializesCommands.php | 7 ++++--- app/Services/BaseService.php | 39 +++++++++++++----------------------- app/WritesToConsole.php | 4 ++-- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/app/InitializesCommands.php b/app/InitializesCommands.php index da62d558..b0848e42 100644 --- a/app/InitializesCommands.php +++ b/app/InitializesCommands.php @@ -6,6 +6,7 @@ use App\Exceptions\DockerNotAvailableException; use App\Shell\Docker; +use function Laravel\Prompts\error; use function Laravel\Prompts\text; trait InitializesCommands @@ -25,14 +26,14 @@ public function initializeCommand(): void } } - public function askPromptQuestion(string $question, $default = null) + public function askPromptQuestion(string $question, $default = null, $validate = null) { - return text(label: $question, default: $default); + return text(label: $question, default: $default, validate: $validate); } public function errorPrompt(string $message): void { - $this->components->error($message); + error($message); } public function alertPrompt(string $message): void diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php index 9aefdd05..8dcc0c2d 100644 --- a/app/Services/BaseService.php +++ b/app/Services/BaseService.php @@ -160,29 +160,18 @@ protected function prompts(): void { $items = []; - foreach ($this->defaultPrompts as $prompt) { - $this->askQuestion($prompt, $this->useDefaults); - - while ($prompt['shortname'] === 'port' && ! $this->environment->portIsAvailable($this->promptResponses['port'])) { - app('console')->error("Port {$this->promptResponses['port']} is already in use. Please select a different port."); - $this->askQuestion($prompt); - } - } - - foreach ($this->prompts as $prompt) { - $this->askQuestion($prompt, $this->useDefaults); - - while ($prompt['shortname'] === 'volume' && ! $this->docker->volumeIsAvailable($this->promptResponses['volume'])) { - app('console')->error("Volume {$this->promptResponses['volume']} is already in use. Please select a different volume."); - $this->askQuestion($prompt); - } - - while (Str::contains($prompt['shortname'], 'port') && ! $this->environment->portIsAvailable($this->promptResponses[$prompt['shortname']])) { - app('console')->error("Port {$this->promptResponses[$prompt['shortname']]} is already in use. Please select a different port."); - $this->askQuestion($prompt); - } - - $items[] = $prompt; + $questions = array_merge($this->defaultPrompts, $this->prompts); + + foreach ($questions as $prompt) { + $items[] = match (true) { + Str::contains($prompt['shortname'], 'port') => $this->askQuestion($prompt, $this->useDefaults, validate: function (string $port) { + return $this->environment->portIsAvailable($port) ? null : "Port {$port} is already in use. Please select a different port."; + }), + Str::contains($prompt['shortname'], 'volume') => $this->askQuestion($prompt, $this->useDefaults, validate: function (string $volume) { + return $this->docker->volumeIsAvailable($volume) ? null : "Volume {$volume} is already in use. Please select a different volume."; + }), + default => $this->askQuestion($prompt, $this->useDefaults), + }; } // Allow users to pass custom docker images (e.g. "postgis/postgis:latest") when we ask for the tag @@ -194,12 +183,12 @@ protected function prompts(): void $this->tag = $this->resolveTag($this->promptResponses['tag']); } - protected function askQuestion(array $prompt, $useDefaults = false): void + protected function askQuestion(array $prompt, $useDefaults = false, $validate = null): void { $this->promptResponses[$prompt['shortname']] = $prompt['default'] ?? null; if (! $useDefaults) { - $this->promptResponses[$prompt['shortname']] = $this->ask(sprintf($prompt['prompt'], $this->imageName), $prompt['default'] ?? null); + $this->promptResponses[$prompt['shortname']] = $this->ask(sprintf($prompt['prompt'], $this->imageName), $prompt['default'] ?? null, $validate); } } diff --git a/app/WritesToConsole.php b/app/WritesToConsole.php index 5f2bcb52..0bee5b46 100644 --- a/app/WritesToConsole.php +++ b/app/WritesToConsole.php @@ -34,8 +34,8 @@ public function task(string $message, $callable): void app('console')->taskPrompt($message, $callable); } - public function ask(string $message, $default = null) + public function ask(string $message, $default = null, $validate = null) { - return app('console')->askPromptQuestion($message, $default); + return app('console')->askPromptQuestion($message, $default, $validate); } } From 5c2e6e5002a5c5e09effd1e6603eed4e3ba892bd Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:33:39 -0300 Subject: [PATCH 15/55] Use prompts for the list command --- app/Commands/ListCommand.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/Commands/ListCommand.php b/app/Commands/ListCommand.php index 183a7573..613ca26d 100644 --- a/app/Commands/ListCommand.php +++ b/app/Commands/ListCommand.php @@ -7,6 +7,9 @@ use App\Shell\Docker; use LaravelZero\Framework\Commands\Command; +use function Laravel\Prompts\info; +use function Laravel\Prompts\table; + class ListCommand extends Command { use InitializesCommands; @@ -31,7 +34,7 @@ public function handle(Docker $docker): void } if ($containersCollection->isEmpty()) { - $this->info("No Takeout containers are enabled.\n"); + info("No Takeout containers are enabled."); return; } @@ -39,7 +42,6 @@ public function handle(Docker $docker): void $containers = $containersCollection->toArray(); $columns = array_map('App\title_from_slug', array_keys(reset($containers))); - $this->line("\n"); - $this->table($columns, $containers); + table($columns, $containers); } } From 4a6c85d6f869d51d5d39954d7a8cedafae58fa39 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:40:03 -0300 Subject: [PATCH 16/55] Replace netstat with fsockopen to check if a port is available --- app/Shell/Environment.php | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/app/Shell/Environment.php b/app/Shell/Environment.php index d0d071f1..5953304a 100644 --- a/app/Shell/Environment.php +++ b/app/Shell/Environment.php @@ -30,33 +30,19 @@ public function isWindowsOs(): bool public function portIsAvailable($port): bool { - // E.g. Win/Linux: 127.0.0.1:3306 , macOS: 127.0.0.1.3306 - $portText = $this->isLinuxOs() ? "\:{$port}\s" : "\.{$port}\s"; + // To check if the socket is available, we'll attempt to open a socket on the port. + // If we cannot open the socket, it means there's nothing running on it, so the + // port is available. If we are successful, that means it is already in use. - $netstatCmd = $this->netstatCmd(); + $socket = @fsockopen('localhost', $port, $errorCode, $errorMessage, timeout: 5); - // Check to see if the system is running a service with the desired port - $process = $this->shell->execQuietly("{$netstatCmd} -vanp tcp \n - | grep '{$portText}' | grep -v 'TIME_WAIT' | grep -v 'CLOSE_WAIT' | grep -v 'FIN_WAIT'"); - - // A successful netstat command means a port in use was found - return ! $process->isSuccessful(); - } - - public function netstatCmd(): string - { - $netstatCmd = 'netstat'; - - if ($this->isLinuxOs()) { - $linuxVersion = $this->shell->execQuietly('cat /proc/version'); - $isWSL = Str::contains($linuxVersion->getOutput(), 'microsoft'); - - if ($isWSL) { - $netstatCmd = 'netstat.exe'; - } + if (! $socket) { + return true; } - return $netstatCmd; + fclose($socket); + + return false; } public function userIsInDockerGroup(): bool From 698a42ac45b12e414453acb1b87e69968a19afc1 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:40:13 -0300 Subject: [PATCH 17/55] wip --- app/Commands/EnableCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Commands/EnableCommand.php b/app/Commands/EnableCommand.php index 6d457377..4b1a447f 100644 --- a/app/Commands/EnableCommand.php +++ b/app/Commands/EnableCommand.php @@ -8,6 +8,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\App; use LaravelZero\Framework\Commands\Command; + use function Laravel\Prompts\search; class EnableCommand extends Command From 4dab6b22a08c04bd93be5520803d0c33b381ac28 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:43:02 -0300 Subject: [PATCH 18/55] Use an if instead of a ternary --- app/Services/BaseService.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php index 8dcc0c2d..391a6acb 100644 --- a/app/Services/BaseService.php +++ b/app/Services/BaseService.php @@ -165,10 +165,18 @@ protected function prompts(): void foreach ($questions as $prompt) { $items[] = match (true) { Str::contains($prompt['shortname'], 'port') => $this->askQuestion($prompt, $this->useDefaults, validate: function (string $port) { - return $this->environment->portIsAvailable($port) ? null : "Port {$port} is already in use. Please select a different port."; + if (! $this->environment->portIsAvailable($port)) { + return "Port {$port} is already in use. Please select a different port."; + } + + return null; }), Str::contains($prompt['shortname'], 'volume') => $this->askQuestion($prompt, $this->useDefaults, validate: function (string $volume) { - return $this->docker->volumeIsAvailable($volume) ? null : "Volume {$volume} is already in use. Please select a different volume."; + if (! $this->docker->volumeIsAvailable($volume)) { + return "Volume {$volume} is already in use. Please select a different volume."; + } + + return null; }), default => $this->askQuestion($prompt, $this->useDefaults), }; From 9fc28e5292191572aedae33755a1b8a907c0ea80 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:49:44 -0300 Subject: [PATCH 19/55] Use prompts on the shell errors --- app/Shell/Shell.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/Shell/Shell.php b/app/Shell/Shell.php index 89333a26..43e7eaf4 100644 --- a/app/Shell/Shell.php +++ b/app/Shell/Shell.php @@ -5,6 +5,9 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Process\Process; +use function Laravel\Prompts\error; +use function Laravel\Prompts\note; + class Shell { protected $output; @@ -16,22 +19,19 @@ public function __construct(ConsoleOutput $output) public function exec(string $command, array $parameters = [], bool $quiet = false): Process { - $didAnything = false; - $process = $this->buildProcess($command); - $process->run(function ($type, $buffer) use ($quiet, &$didAnything) { + $process->run(function ($type, $buffer) use ($quiet) { if (empty($buffer) || $buffer === PHP_EOL || $quiet) { return; + }; + + if ($type === Process::ERR) { + error('Something went wrong.'); } - $this->output->writeLn($this->formatMessage($buffer, $type === process::ERR)); - $didAnything = true; + note($this->formatMessage($buffer)); }, $parameters); - if ($didAnything) { - $this->output->writeLn("\n"); - } - return $process; } @@ -48,12 +48,10 @@ public function execQuietly(string $command, array $parameters = []): Process return $this->exec($command, $parameters, $quiet = true); } - public function formatMessage(string $buffer, $isError = false): string + public function formatMessage(string $buffer): string { - $pre = $isError ? ' ERR %s' : ' OUT %s'; - - return rtrim(collect(explode("\n", trim($buffer)))->reduce(function ($carry, $line) use ($pre) { - return $carry .= trim(sprintf($pre, $line)) . "\n"; + return rtrim(collect(explode("\n", trim($buffer)))->reduce(function ($carry, $line) { + return $carry .= trim($line) . "\n"; }, '')); } From d374058cf3cd25c2200005847a59b00e7ac74a41 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:49:57 -0300 Subject: [PATCH 20/55] Bump prompts dependency version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 41753800..26ed8e17 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "ext-posix": "*", "composer/semver": "^3.4", "guzzlehttp/psr7": "^2.6", - "laravel/prompts": "^0.1.24" + "laravel/prompts": "^0.3.2" }, "require-dev": { "guzzlehttp/guzzle": "^7.5", From 6b0c80832c48e4aa3d86a24b906d801abbaebd97 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:52:05 -0300 Subject: [PATCH 21/55] Use prompts in the invalid service shortname exception --- app/Exceptions/InvalidServiceShortnameException.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/Exceptions/InvalidServiceShortnameException.php b/app/Exceptions/InvalidServiceShortnameException.php index 7f60ff5f..4fa47dc5 100644 --- a/app/Exceptions/InvalidServiceShortnameException.php +++ b/app/Exceptions/InvalidServiceShortnameException.php @@ -2,17 +2,14 @@ namespace App\Exceptions; -use App\Shell\Shell; use Exception; +use function Laravel\Prompts\error; + class InvalidServiceShortnameException extends Exception { public function render($request = null) { - $console = app('console'); - $shell = app(Shell::class); - - $console->line(''); - $console->line($shell->formatErrorMessage($this->getMessage())); + error($this->getMessage()); } } From 9effc50ffe6127d9813ddeaf77e0863e2d919b87 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:52:59 -0300 Subject: [PATCH 22/55] Remove unused method --- app/Shell/Shell.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/Shell/Shell.php b/app/Shell/Shell.php index 43e7eaf4..3511414b 100644 --- a/app/Shell/Shell.php +++ b/app/Shell/Shell.php @@ -54,9 +54,4 @@ public function formatMessage(string $buffer): string return $carry .= trim($line) . "\n"; }, '')); } - - public function formatErrorMessage(string $buffer) - { - return $this->formatMessage($buffer, true); - } } From a4f52def6180cb4f2ef8436b7d51b51c00e055e4 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:55:26 -0300 Subject: [PATCH 23/55] Turn the formatMessage function private --- app/Shell/Shell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Shell/Shell.php b/app/Shell/Shell.php index 3511414b..2dbb23c3 100644 --- a/app/Shell/Shell.php +++ b/app/Shell/Shell.php @@ -48,7 +48,7 @@ public function execQuietly(string $command, array $parameters = []): Process return $this->exec($command, $parameters, $quiet = true); } - public function formatMessage(string $buffer): string + private function formatMessage(string $buffer): string { return rtrim(collect(explode("\n", trim($buffer)))->reduce(function ($carry, $line) { return $carry .= trim($line) . "\n"; From 98d8f9708a2762a9f889ed16da54e048520a8b3b Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 19:55:45 -0300 Subject: [PATCH 24/55] Lint --- app/Commands/ListCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Commands/ListCommand.php b/app/Commands/ListCommand.php index 613ca26d..f0b548c7 100644 --- a/app/Commands/ListCommand.php +++ b/app/Commands/ListCommand.php @@ -34,7 +34,7 @@ public function handle(Docker $docker): void } if ($containersCollection->isEmpty()) { - info("No Takeout containers are enabled."); + info('No Takeout containers are enabled.'); return; } From 3dafccd38c521397895f832c56ae8ab4b4678820 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:40:00 -0300 Subject: [PATCH 25/55] Fix BaseServiceTest --- tests/Feature/BaseServiceTest.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/Feature/BaseServiceTest.php b/tests/Feature/BaseServiceTest.php index d1118a6c..4eebb7e6 100644 --- a/tests/Feature/BaseServiceTest.php +++ b/tests/Feature/BaseServiceTest.php @@ -29,13 +29,13 @@ public function it_enables_services() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); $mock->shouldReceive('askPromptQuestion') - ->with('Which host port would you like meilisearch to use?', $defaultPort) + ->with('Which host port would you like meilisearch to use?', $defaultPort, M::any()) ->andReturn(7700); $mock->shouldReceive('askPromptQuestion') - ->with('What is the Docker volume name?', 'meili_data') + ->with('What is the Docker volume name?', 'meili_data', M::any()) ->andReturn('meili_data'); $mock->shouldReceive('askPromptQuestion') - ->with('Which tag (version) of meilisearch would you like to use?', 'latest') + ->with('Which tag (version) of meilisearch would you like to use?', 'latest', M::any()) ->andReturn('v1.1.1'); $mock->shouldIgnoreMissing(); })); @@ -80,13 +80,13 @@ public function it_can_receive_a_custom_image_in_the_tag() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); $mock->shouldReceive('askPromptQuestion') - ->with('Which host port would you like postgres to use?', $defaultPort) + ->with('Which host port would you like postgres to use?', $defaultPort, M::any()) ->andReturn(5432); $mock->shouldReceive('askPromptQuestion') - ->with('Which tag (version) of postgres would you like to use?', 'latest') + ->with('Which tag (version) of postgres would you like to use?', 'latest', M::any()) ->andReturn('timescale/timescaledb:latest-pg12'); $mock->shouldReceive('askPromptQuestion') - ->with('What is the Docker volume name?', 'postgres_data') + ->with('What is the Docker volume name?', 'postgres_data', M::any()) ->andReturn('postgres_data'); $mock->shouldIgnoreMissing(); })); @@ -133,10 +133,10 @@ public function it_can_receive_container_arguments_via_passthrough() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); $mock->shouldReceive('askPromptQuestion') - ->with('Which host port would you like _test_image to use?', $defaultPort) + ->with('Which host port would you like _test_image to use?', $defaultPort, M::any()) ->andReturn(12345); $mock->shouldReceive('askPromptQuestion') - ->with('Which tag (version) of _test_image would you like to use?', 'latest') + ->with('Which tag (version) of _test_image would you like to use?', 'latest', M::any()) ->andReturn('latest'); $mock->shouldIgnoreMissing(); })); @@ -186,10 +186,10 @@ public function it_accepts_run_options_and_passthrough_options() app()->instance('console', M::mock(Command::class, function ($mock) use ($service) { $defaultPort = $service->defaultPort(); $mock->shouldReceive('askPromptQuestion') - ->with('Which host port would you like _test_image to use?', $defaultPort) + ->with('Which host port would you like _test_image to use?', $defaultPort, M::any()) ->andReturn(12345); $mock->shouldReceive('askPromptQuestion') - ->with('Which tag (version) of _test_image would you like to use?', 'latest') + ->with('Which tag (version) of _test_image would you like to use?', 'latest', M::any()) ->andReturn('latest'); $mock->shouldIgnoreMissing(); })); From 31a8b642683c233dc15d59b3f321e8cbd6b2127c Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:40:22 -0300 Subject: [PATCH 26/55] Fix the environment port availability detection --- app/Shell/Environment.php | 2 -- tests/Feature/EnvironmentTest.php | 53 ++++++------------------------- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/app/Shell/Environment.php b/app/Shell/Environment.php index 5953304a..026aff0e 100644 --- a/app/Shell/Environment.php +++ b/app/Shell/Environment.php @@ -2,8 +2,6 @@ namespace App\Shell; -use Illuminate\Support\Str; - class Environment { protected $shell; diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 4485b3c5..0ecb66d4 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -3,64 +3,31 @@ namespace Tests\Feature; use App\Shell\Environment; -use App\Shell\Shell; use LaravelZero\Framework\Commands\Command; use Mockery as M; -use Symfony\Component\Process\Process; use Tests\TestCase; class EnvironmentTest extends TestCase { - function isLinux() - { - return PHP_OS_FAMILY === 'Linux'; - } - /** @test **/ - function it_detects_a_port_conflict() + public function it_detects_a_port_conflict() { app()->instance('console', M::mock(Command::class, function ($mock) { $mock->shouldIgnoreMissing(); })); - $this->mock(Shell::class, function ($mock) { - $process = M::mock(Process::class); - $process->shouldReceive('isSuccessful')->once()->andReturn(true); - $process->shouldReceive('getOutput')->andReturn(''); - - $times = 1; - if ($this->isLinux()) { - $times = 2; - } - - $mock->shouldReceive('execQuietly')->times($times)->andReturn($process); - }); + $port = rand(20_000, 50_000); $environment = app(Environment::class); - $this->assertFalse($environment->portIsAvailable(1234)); - } + $this->assertTrue($environment->portIsAvailable($port)); - /** @test **/ - function it_detects_a_port_is_available() - { - app()->instance('console', M::mock(Command::class, function ($mock) { - $mock->shouldIgnoreMissing(); - })); - - $this->mock(Shell::class, function ($mock) { - $process = M::mock(Process::class); - $process->shouldReceive('isSuccessful')->once()->andReturn(false); - $process->shouldReceive('getOutput')->andReturn(''); + $socket = socket_create(domain: AF_INET, type: SOCK_STREAM, protocol: SOL_TCP); + assert($socket !== false, "Was not able to create a socket."); + socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); + assert(socket_bind($socket, 'localhost', $port) !== false, "Was not able to bind socket to port {$port}"); + assert(socket_listen($socket, backlog: 5)); - $times = 1; - if ($this->isLinux()) { - $times = 2; - } - - $mock->shouldReceive('execQuietly')->times($times)->andReturn($process); - }); - - $environment = app(Environment::class); - $this->assertTrue($environment->portIsAvailable(1234)); + $this->assertFalse($environment->portIsAvailable($port)); + socket_close($socket); } } From b314256399666fc157d17ac588693cf5e12f6ecf Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:41:05 -0300 Subject: [PATCH 27/55] Lint --- tests/Feature/EnvironmentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 0ecb66d4..6f2da91f 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -22,7 +22,7 @@ public function it_detects_a_port_conflict() $this->assertTrue($environment->portIsAvailable($port)); $socket = socket_create(domain: AF_INET, type: SOCK_STREAM, protocol: SOL_TCP); - assert($socket !== false, "Was not able to create a socket."); + assert($socket !== false, 'Was not able to create a socket.'); socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); assert(socket_bind($socket, 'localhost', $port) !== false, "Was not able to bind socket to port {$port}"); assert(socket_listen($socket, backlog: 5)); From f82efcc7cf55ef686482cac577fca2f7c251e9e5 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:49:49 -0300 Subject: [PATCH 28/55] Wrap the socket creation in a function --- tests/Feature/EnvironmentTest.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 6f2da91f..362162c1 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -21,13 +21,23 @@ public function it_detects_a_port_conflict() $environment = app(Environment::class); $this->assertTrue($environment->portIsAvailable($port)); + $this->withFakeProcess($port, fn() => ( + $this->assertFalse($environment->portIsAvailable($port)) + )); + } + + private function withFakeProcess(int $port, $callback) + { $socket = socket_create(domain: AF_INET, type: SOCK_STREAM, protocol: SOL_TCP); assert($socket !== false, 'Was not able to create a socket.'); socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); assert(socket_bind($socket, 'localhost', $port) !== false, "Was not able to bind socket to port {$port}"); assert(socket_listen($socket, backlog: 5)); - $this->assertFalse($environment->portIsAvailable($port)); - socket_close($socket); + try { + $callback(); + } finally { + socket_close($socket); + } } } From 0dcc1659e9d57cfb09bea5aed662cb2b7273b2ec Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:52:07 -0300 Subject: [PATCH 29/55] Ensure the sockets extensions is loaded --- tests/Feature/EnvironmentTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 362162c1..a9ea181d 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -12,6 +12,10 @@ class EnvironmentTest extends TestCase /** @test **/ public function it_detects_a_port_conflict() { + if (! extension_loaded('sockets')) { + $this->markTestSkipped('Sockets extension is required (should be included in PHP by default).'); + } + app()->instance('console', M::mock(Command::class, function ($mock) { $mock->shouldIgnoreMissing(); })); From 87cc23c112335be6ed52b6bf6b505fc0a367fef2 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:55:10 -0300 Subject: [PATCH 30/55] Tweaks failed test message --- tests/Feature/EnvironmentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index a9ea181d..68ab6fce 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -26,7 +26,7 @@ public function it_detects_a_port_conflict() $this->assertTrue($environment->portIsAvailable($port)); $this->withFakeProcess($port, fn() => ( - $this->assertFalse($environment->portIsAvailable($port)) + $this->assertFalse($environment->portIsAvailable($port), "Expected port {$port} to be in use, but it was available.") )); } From 30cd08a183d4b2cf325dd83a5c69939b58723b5a Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:56:59 -0300 Subject: [PATCH 31/55] Rename method --- tests/Feature/EnvironmentTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 68ab6fce..7d0422a6 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -25,12 +25,12 @@ public function it_detects_a_port_conflict() $environment = app(Environment::class); $this->assertTrue($environment->portIsAvailable($port)); - $this->withFakeProcess($port, fn() => ( + $this->bindFakeProcessToPort($port, fn() => ( $this->assertFalse($environment->portIsAvailable($port), "Expected port {$port} to be in use, but it was available.") )); } - private function withFakeProcess(int $port, $callback) + private function bindFakeProcessToPort(int $port, $callback) { $socket = socket_create(domain: AF_INET, type: SOCK_STREAM, protocol: SOL_TCP); assert($socket !== false, 'Was not able to create a socket.'); From b3de93c569ac92d58fe28e206a221c75c13fb758 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 22:58:48 -0300 Subject: [PATCH 32/55] Ensure the sockets extension is on the list of extensions on CI It should be included by default in PHP, though. --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4f6ae085..af03fd39 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,7 +24,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: posix, dom, curl, libxml, fileinfo, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: posix, dom, curl, libxml, fileinfo, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, sockets coverage: none - name: Install dependencies From 4efc521d458df6a3fb25f7007ec8609b9a0625f3 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:02:46 -0300 Subject: [PATCH 33/55] Remove unused mock --- tests/Feature/EnvironmentTest.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 7d0422a6..bdc040f1 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -3,8 +3,6 @@ namespace Tests\Feature; use App\Shell\Environment; -use LaravelZero\Framework\Commands\Command; -use Mockery as M; use Tests\TestCase; class EnvironmentTest extends TestCase @@ -16,13 +14,10 @@ public function it_detects_a_port_conflict() $this->markTestSkipped('Sockets extension is required (should be included in PHP by default).'); } - app()->instance('console', M::mock(Command::class, function ($mock) { - $mock->shouldIgnoreMissing(); - })); - $port = rand(20_000, 50_000); $environment = app(Environment::class); + $this->assertTrue($environment->portIsAvailable($port)); $this->bindFakeProcessToPort($port, fn() => ( From 09a8ac858b051d216bde01b6cb7147ab493e8bfd Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:07:57 -0300 Subject: [PATCH 34/55] Tweaks the fake process --- tests/Feature/EnvironmentTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index bdc040f1..0ce0ec94 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -29,8 +29,7 @@ private function bindFakeProcessToPort(int $port, $callback) { $socket = socket_create(domain: AF_INET, type: SOCK_STREAM, protocol: SOL_TCP); assert($socket !== false, 'Was not able to create a socket.'); - socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); - assert(socket_bind($socket, 'localhost', $port) !== false, "Was not able to bind socket to port {$port}"); + assert(socket_bind($socket, '127.0.0.1', $port) !== false, "Was not able to bind socket to port {$port}"); assert(socket_listen($socket, backlog: 5)); try { From 83f2e0df770262a05cb57c074510fcf44c091fb6 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:17:57 -0300 Subject: [PATCH 35/55] Tweaks the fake process creation --- tests/Feature/EnvironmentTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 0ce0ec94..260e3f11 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -28,9 +28,10 @@ public function it_detects_a_port_conflict() private function bindFakeProcessToPort(int $port, $callback) { $socket = socket_create(domain: AF_INET, type: SOCK_STREAM, protocol: SOL_TCP); + assert($socket !== false, 'Was not able to create a socket.'); assert(socket_bind($socket, '127.0.0.1', $port) !== false, "Was not able to bind socket to port {$port}"); - assert(socket_listen($socket, backlog: 5)); + assert(socket_listen($socket)); try { $callback(); From 7c85a0fc1c8c20d944deb44361d44469bcefbb73 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:33:16 -0300 Subject: [PATCH 36/55] Rewrite the test to use socket_create_listen --- tests/Feature/EnvironmentTest.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index 260e3f11..d6415af0 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -14,27 +14,25 @@ public function it_detects_a_port_conflict() $this->markTestSkipped('Sockets extension is required (should be included in PHP by default).'); } - $port = rand(20_000, 50_000); - $environment = app(Environment::class); - $this->assertTrue($environment->portIsAvailable($port)); + $port = $this->withFakeProcessRunning( + fn($port) => $this->assertFalse($environment->portIsAvailable($port), "Expected port {$port} to be taken, but it was available."), + ); - $this->bindFakeProcessToPort($port, fn() => ( - $this->assertFalse($environment->portIsAvailable($port), "Expected port {$port} to be in use, but it was available.") - )); + $this->assertTrue($environment->portIsAvailable($port), "Expected port {$port} to be avaialble, but it was taken."); } - private function bindFakeProcessToPort(int $port, $callback) + private function withFakeProcessRunning($closure) { - $socket = socket_create(domain: AF_INET, type: SOCK_STREAM, protocol: SOL_TCP); + // Passing zero to it will make PHP select a free port... + $socket = socket_create_listen(0); - assert($socket !== false, 'Was not able to create a socket.'); - assert(socket_bind($socket, '127.0.0.1', $port) !== false, "Was not able to bind socket to port {$port}"); - assert(socket_listen($socket)); + // Extract the host and port (we only care about the port) so we can use it... + socket_getsockname($socket, $host, $port); try { - $callback(); + $closure($port); } finally { socket_close($socket); } From 7dbe8193c98e3c8a80e04d28ae74c0ee51d69486 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:36:28 -0300 Subject: [PATCH 37/55] Adds the sockets extension as required --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 26ed8e17..686a60ab 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "ext-json": "*", "ext-pcntl": "*", "ext-posix": "*", + "ext-sockets": "*", "composer/semver": "^3.4", "guzzlehttp/psr7": "^2.6", "laravel/prompts": "^0.3.2" From c012df3069605a43bde9aeb1bcb3d08bf3101936 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:38:03 -0300 Subject: [PATCH 38/55] Remove pcntl and posix dependencies (the posix one was required by the menu) --- composer.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/composer.json b/composer.json index 686a60ab..4efc79eb 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,6 @@ "require": { "php": "^8.2", "ext-json": "*", - "ext-pcntl": "*", - "ext-posix": "*", "ext-sockets": "*", "composer/semver": "^3.4", "guzzlehttp/psr7": "^2.6", From 6670bdca20523bcac98de623098d210700af2c59 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:38:40 -0300 Subject: [PATCH 39/55] Remove pcntl and posix from CI so we can test it --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index af03fd39..28985eda 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,7 +24,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: posix, dom, curl, libxml, fileinfo, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, sockets + extensions: dom, curl, libxml, fileinfo, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, sockets coverage: none - name: Install dependencies From 1328dc30a9e826bab1d5b46f2155a2dfe8d1e7fe Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Tue, 17 Dec 2024 23:58:27 -0300 Subject: [PATCH 40/55] Tweaks the output format --- app/Shell/Shell.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Shell/Shell.php b/app/Shell/Shell.php index 2dbb23c3..e03e9701 100644 --- a/app/Shell/Shell.php +++ b/app/Shell/Shell.php @@ -27,9 +27,10 @@ public function exec(string $command, array $parameters = [], bool $quiet = fals if ($type === Process::ERR) { error('Something went wrong.'); + note(' ERR ' . $this->formatMessage($buffer)); + } else { + note(' OUT ' . $this->formatMessage($buffer)); } - - note($this->formatMessage($buffer)); }, $parameters); return $process; From 74f8d262027d81d70fbbe4b6dc8731d98f277a60 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Wed, 18 Dec 2024 12:09:09 -0300 Subject: [PATCH 41/55] Detect if Takeout is running in a container and use host.docker.localhost instead of localhost This works on Windows and Mac, but will require Linux users to add `--add-host=docker.internal.host:host-gateway` to their aliases. The docs was updated. --- README.md | 10 +++++----- app/Shell/Environment.php | 12 +++++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 13f540f9..f79ba9ef 100644 --- a/README.md +++ b/README.md @@ -32,19 +32,19 @@ The recommended way to install Takeout is the dockerized version via an alias (a On Linux or macOS, use: ```bash -alias takeout="docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -it tighten/takeout:latest" +alias takeout="docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --add-host=host.docker.internal:host-gateway -it tighten/takeout:latest" ``` -On Windows 10, if you're using Bash, use: +On Windows 10|11, if you're using Bash, use: ```bash -alias takeout="docker run --rm -v //var/run/docker.sock:/var/run/docker.sock -it tighten/takeout:latest" +alias takeout="docker run --rm -v //var/run/docker.sock:/var/run/docker.sock --add-host=host.docker.internal:host-gateway -it tighten/takeout:latest" ``` -On Windows 10, if you're using PowerShell, use: +On Windows 10|11, if you're using PowerShell, use: ```bash -function takeout { docker run --rm -v //var/run/docker.sock:/var/run/docker.sock -it tighten/takeout:latest $args } +function takeout { docker run --rm -v //var/run/docker.sock:/var/run/docker.sock --add-host=host.docker.internal:host-gateway -it tighten/takeout:latest $args } ``` That's it. You may now use Takeout on your terminal. The first time you use this alias, it will pull the Takeout image from Docker Hub. diff --git a/app/Shell/Environment.php b/app/Shell/Environment.php index 026aff0e..17e91498 100644 --- a/app/Shell/Environment.php +++ b/app/Shell/Environment.php @@ -32,7 +32,7 @@ public function portIsAvailable($port): bool // If we cannot open the socket, it means there's nothing running on it, so the // port is available. If we are successful, that means it is already in use. - $socket = @fsockopen('localhost', $port, $errorCode, $errorMessage, timeout: 5); + $socket = @fsockopen($this->localhost(), $port, $errorCode, $errorMessage, timeout: 5); if (! $socket) { return true; @@ -43,6 +43,11 @@ public function portIsAvailable($port): bool return false; } + public function isTakeoutRunningOnDocker(): bool + { + return boolval($_SERVER['TAKEOUT_CONTAINER'] ?? false); + } + public function userIsInDockerGroup(): bool { return $this->shell->execQuietly('groups | grep docker')->isSuccessful(); @@ -63,4 +68,9 @@ public function homeDirectory(): string return '~'; } + + private function localhost(): string + { + return $this->isTakeoutRunningOnDocker() ? 'host.docker.internal' : 'localhost'; + } } From 245c6bc45b947a841b2b1e336211720807c1ab02 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Wed, 18 Dec 2024 11:49:49 -0500 Subject: [PATCH 42/55] allows container id to be passed to start and stop --- app/Commands/StartCommand.php | 4 ++-- app/Commands/StopCommand.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Commands/StartCommand.php b/app/Commands/StartCommand.php index e4f83e87..25aeac92 100644 --- a/app/Commands/StartCommand.php +++ b/app/Commands/StartCommand.php @@ -64,8 +64,8 @@ public function startableContainers(): Collection public function startByServiceNameOrContainerId(string $service, Collection $startableContainers): void { $containersByServiceName = $startableContainers - ->filter(function ($serviceName) use ($service) { - return Str::startsWith($serviceName, $service); + ->filter(function ($serviceName, $key) use ($service) { + return Str::startsWith($serviceName, $service) || $key === $service; }); // If we don't get any container by the service name, that probably means diff --git a/app/Commands/StopCommand.php b/app/Commands/StopCommand.php index ca1219b6..4e968ec7 100644 --- a/app/Commands/StopCommand.php +++ b/app/Commands/StopCommand.php @@ -65,8 +65,8 @@ private function stoppableContainers(): Collection private function stopByServiceNameOrContainerId(string $service, Collection $stoppableContainers): void { $containersByServiceName = $stoppableContainers - ->filter(function ($containerName) use ($service) { - return Str::startsWith($containerName, $service); + ->filter(function ($containerName, $key) use ($service) { + return Str::startsWith($containerName, $service) || $key === $service; }); if ($containersByServiceName->isEmpty()) { From adc8b6bc424542af9e49b1fd64c6e30650813810 Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Wed, 18 Dec 2024 11:51:10 -0500 Subject: [PATCH 43/55] refactors to active containers and adds dropdown to logs cmd --- app/Commands/LogCommand.php | 58 ++++++++++++++++++++++++++++--- app/Commands/StopCommand.php | 2 +- app/Shell/Docker.php | 8 ++--- tests/Feature/StopCommandTest.php | 6 ++-- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/app/Commands/LogCommand.php b/app/Commands/LogCommand.php index 798b8466..b20c3401 100644 --- a/app/Commands/LogCommand.php +++ b/app/Commands/LogCommand.php @@ -4,13 +4,18 @@ use App\InitializesCommands; use App\Shell\Docker; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; +use function Laravel\Prompts\select; + class LogCommand extends Command { use InitializesCommands; + const MENU_TITLE = 'Takeout containers logs'; + protected $signature = 'logs {containerId?}'; protected $description = 'Display container logs.'; protected $docker; @@ -20,17 +25,25 @@ public function handle(Docker $docker): void $this->docker = $docker; $this->initializeCommand(); - $container = $this->argument('containerId'); + $loggableContainers = $this->loggableContainers(); + + + if ($loggableContainers->isEmpty()) { + $this->info("No Takeout containers available.\n"); + + return; + } - if (! $container) { - $this->error("Please pass a valid container ID.\n"); + if (filled($service = $this->argument('containerId'))) { + $this->logsByServiceNameOrContainerId($service, $loggableContainers); return; } - $this->logs($container); + $this->logs($this->selectOptions($loggableContainers)); } + public function logs(string $container): void { if (Str::contains($container, ' -')) { @@ -39,4 +52,41 @@ public function logs(string $container): void $this->docker->logContainer($container); } + + private function loggableContainers(): Collection + { + return $this->docker->activeTakeoutContainers()->mapWithKeys(function ($container) { + return [$container['container_id'] => str_replace('TO--', '', $container['names'])]; + }); + } + + private function selectOptions($stoppableContainers) + { + return select( + label: self::MENU_TITLE, + options: $stoppableContainers + ); + } + + private function logsByServiceNameOrContainerId(string $service, Collection $loggableContainers): void + { + $containersByServiceName = $loggableContainers + ->filter(function ($containerName, $key) use ($service) { + return Str::startsWith($containerName, $service) || $key === $service; + }); + + if ($containersByServiceName->isEmpty()) { + $this->info('No containers found for ' . $service); + + return; + } + + if ($containersByServiceName->count() === 1) { + $this->logs($containersByServiceName->keys()->first()); + + return; + } + + $this->logs($this->selectOptions($containersByServiceName)); + } } diff --git a/app/Commands/StopCommand.php b/app/Commands/StopCommand.php index 4e968ec7..bddbce5e 100644 --- a/app/Commands/StopCommand.php +++ b/app/Commands/StopCommand.php @@ -57,7 +57,7 @@ public function handle(Docker $docker, Environment $environment): void private function stoppableContainers(): Collection { - return $this->docker->stoppableTakeoutContainers()->mapWithKeys(function ($container) { + return $this->docker->activeTakeoutContainers()->mapWithKeys(function ($container) { return [$container['container_id'] => str_replace('TO--', '', $container['names'])]; }); } diff --git a/app/Shell/Docker.php b/app/Shell/Docker.php index d183ad3e..12dc77eb 100644 --- a/app/Shell/Docker.php +++ b/app/Shell/Docker.php @@ -28,7 +28,7 @@ public function __construct( public function removeContainer(string $containerId): void { - if ($this->stoppableTakeoutContainers()->contains(function ($container) use ($containerId) { + if ($this->activeTakeoutContainers()->contains(function ($container) use ($containerId) { return $container['container_id'] === $containerId; })) { $this->stopContainer($containerId); @@ -43,7 +43,7 @@ public function removeContainer(string $containerId): void public function stopContainer(string $containerId): void { - if (! $this->stoppableTakeoutContainers()->contains(function ($container) use ($containerId) { + if (! $this->activeTakeoutContainers()->contains(function ($container) use ($containerId) { return $container['container_id'] === $containerId; })) { throw new DockerContainerMissingException($containerId); @@ -58,7 +58,7 @@ public function stopContainer(string $containerId): void public function logContainer(string $containerId): void { - if (! $this->stoppableTakeoutContainers()->contains(function ($container) use ($containerId) { + if (! $this->activeTakeoutContainers()->contains(function ($container) use ($containerId) { return $container['container_id'] === $containerId; })) { throw new DockerContainerMissingException($containerId); @@ -116,7 +116,7 @@ public function startableTakeoutContainers(): Collection }); } - public function stoppableTakeoutContainers(): Collection + public function activeTakeoutContainers(): Collection { return $this->takeoutContainers()->filter(function ($container) { return Str::contains($container['status'], 'Up'); diff --git a/tests/Feature/StopCommandTest.php b/tests/Feature/StopCommandTest.php index e686c075..fd505a11 100644 --- a/tests/Feature/StopCommandTest.php +++ b/tests/Feature/StopCommandTest.php @@ -25,7 +25,7 @@ function it_can_stop_a_service_from_menu() $this->mock(Docker::class, function ($mock) use ($services, $containerId) { $mock->shouldReceive('isInstalled')->andReturn(true); $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); - $mock->shouldReceive('stoppableTakeoutContainers')->andReturn($services, new Collection); + $mock->shouldReceive('activeTakeoutContainers')->andReturn($services, new Collection); $mock->shouldReceive('stopContainer')->with($containerId); }); @@ -51,7 +51,7 @@ function it_can_stop_containers_by_service_name() $this->mock(Docker::class, function ($mock) use ($services, $containerId) { $mock->shouldReceive('isInstalled')->andReturn(true); $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); - $mock->shouldReceive('stoppableTakeoutContainers')->andReturn($services, new Collection); + $mock->shouldReceive('activeTakeoutContainers')->andReturn($services, new Collection); $mock->shouldReceive('stopContainer')->with($containerId)->once(); }); @@ -84,7 +84,7 @@ function it_can_stop_a_service_from_menu_when_there_are_multiple() $this->mock(Docker::class, function ($mock) use ($services, $secondContainerId) { $mock->shouldReceive('isInstalled')->andReturn(true); $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); - $mock->shouldReceive('stoppableTakeoutContainers')->andReturn($services, new Collection); + $mock->shouldReceive('activeTakeoutContainers')->andReturn($services, new Collection); $mock->shouldReceive('stopContainer')->with($secondContainerId)->once(); }); From aaee0e3a3bb422d39949cff309ae156470b01f6e Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Wed, 18 Dec 2024 14:55:35 -0300 Subject: [PATCH 44/55] Allow printing the logs output as plain notes (without the OUT|ERR prefixes) --- app/Commands/LogCommand.php | 1 - app/Shell/Docker.php | 2 +- app/Shell/Shell.php | 11 ++++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/Commands/LogCommand.php b/app/Commands/LogCommand.php index b20c3401..92860efe 100644 --- a/app/Commands/LogCommand.php +++ b/app/Commands/LogCommand.php @@ -27,7 +27,6 @@ public function handle(Docker $docker): void $loggableContainers = $this->loggableContainers(); - if ($loggableContainers->isEmpty()) { $this->info("No Takeout containers available.\n"); diff --git a/app/Shell/Docker.php b/app/Shell/Docker.php index 12dc77eb..21ed5275 100644 --- a/app/Shell/Docker.php +++ b/app/Shell/Docker.php @@ -64,7 +64,7 @@ public function logContainer(string $containerId): void throw new DockerContainerMissingException($containerId); } - $process = $this->shell->exec('docker logs -f ' . $containerId); + $process = $this->shell->exec('docker logs -f ' . $containerId, plain: true); if (! $process->isSuccessful()) { throw new Exception('Failed to log container ' . $containerId); diff --git a/app/Shell/Shell.php b/app/Shell/Shell.php index e03e9701..767d7ff7 100644 --- a/app/Shell/Shell.php +++ b/app/Shell/Shell.php @@ -17,13 +17,18 @@ public function __construct(ConsoleOutput $output) $this->output = $output; } - public function exec(string $command, array $parameters = [], bool $quiet = false): Process + public function exec(string $command, array $parameters = [], bool $quiet = false, bool $plain = false): Process { $process = $this->buildProcess($command); - $process->run(function ($type, $buffer) use ($quiet) { + $process->run(function ($type, $buffer) use ($quiet, $plain) { if (empty($buffer) || $buffer === PHP_EOL || $quiet) { return; - }; + } + + if ($plain) { + note($buffer); + return; + } if ($type === Process::ERR) { error('Something went wrong.'); From f38d2119119f3e2963a52d395d8ab913cf42ef4e Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Wed, 18 Dec 2024 13:27:37 -0500 Subject: [PATCH 45/55] adds tests for id or name on stop n start cmds Co-authored-by: Tony Messias --- app/Commands/StartCommand.php | 2 +- app/Commands/StopCommand.php | 2 +- tests/Feature/StartCommandTest.php | 12 +++++++++--- tests/Feature/StopCommandTest.php | 12 +++++++++--- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/Commands/StartCommand.php b/app/Commands/StartCommand.php index 25aeac92..975ace16 100644 --- a/app/Commands/StartCommand.php +++ b/app/Commands/StartCommand.php @@ -65,7 +65,7 @@ public function startByServiceNameOrContainerId(string $service, Collection $sta { $containersByServiceName = $startableContainers ->filter(function ($serviceName, $key) use ($service) { - return Str::startsWith($serviceName, $service) || $key === $service; + return Str::startsWith($serviceName, $service) || "{$key}" === $service; }); // If we don't get any container by the service name, that probably means diff --git a/app/Commands/StopCommand.php b/app/Commands/StopCommand.php index bddbce5e..362edf27 100644 --- a/app/Commands/StopCommand.php +++ b/app/Commands/StopCommand.php @@ -66,7 +66,7 @@ private function stopByServiceNameOrContainerId(string $service, Collection $sto { $containersByServiceName = $stoppableContainers ->filter(function ($containerName, $key) use ($service) { - return Str::startsWith($containerName, $service) || $key === $service; + return Str::startsWith($containerName, $service) || "{$key}" === $service; }); if ($containersByServiceName->isEmpty()) { diff --git a/tests/Feature/StartCommandTest.php b/tests/Feature/StartCommandTest.php index 3e9176a2..4b854ec7 100644 --- a/tests/Feature/StartCommandTest.php +++ b/tests/Feature/StartCommandTest.php @@ -34,8 +34,14 @@ function it_can_start_a_service_from_menu() ->assertExitCode(0); } - /** @test */ - function it_can_start_containers_by_name() + /** + * @test + * + * @testWith ["12345"] + * ["mysql"] + * + */ + function it_can_start_containers_by_name_or_id($arg) { $services = Collection::make([ [ @@ -55,7 +61,7 @@ function it_can_start_containers_by_name() $mock->shouldReceive('startContainer')->once()->with($containerId); }); - $this->artisan('start', ['containerId' => ['mysql']]) + $this->artisan('start', ['containerId' => [$arg]]) ->assertExitCode(0); } diff --git a/tests/Feature/StopCommandTest.php b/tests/Feature/StopCommandTest.php index fd505a11..0ab30b5e 100644 --- a/tests/Feature/StopCommandTest.php +++ b/tests/Feature/StopCommandTest.php @@ -34,8 +34,14 @@ function it_can_stop_a_service_from_menu() ->assertExitCode(0); } - /** @test */ - function it_can_stop_containers_by_service_name() + /** + * @test + * + * @testWith ["12345"] + * ["mysql"] + * + */ + function it_can_stop_containers_by_service_name($arg) { $services = Collection::make([ [ @@ -55,7 +61,7 @@ function it_can_stop_containers_by_service_name() $mock->shouldReceive('stopContainer')->with($containerId)->once(); }); - $this->artisan('stop', ['containerId' => ['mysql']]) + $this->artisan('stop', ['containerId' => [$arg]]) ->assertExitCode(0); } From ae0a5e06528c3c44c9c62451bc07a1a49c466d6c Mon Sep 17 00:00:00 2001 From: Guillermo Cava Date: Wed, 18 Dec 2024 13:35:27 -0500 Subject: [PATCH 46/55] adds tess for logs cmds --- app/Commands/LogCommand.php | 2 +- tests/Feature/LogCommandTest.php | 68 +++++++++++++++++++++++++++++++ tests/Feature/StopCommandTest.php | 2 +- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/LogCommandTest.php diff --git a/app/Commands/LogCommand.php b/app/Commands/LogCommand.php index 92860efe..a2c1f2ac 100644 --- a/app/Commands/LogCommand.php +++ b/app/Commands/LogCommand.php @@ -71,7 +71,7 @@ private function logsByServiceNameOrContainerId(string $service, Collection $log { $containersByServiceName = $loggableContainers ->filter(function ($containerName, $key) use ($service) { - return Str::startsWith($containerName, $service) || $key === $service; + return Str::startsWith($containerName, $service) || "{$key}" === $service; }); if ($containersByServiceName->isEmpty()) { diff --git a/tests/Feature/LogCommandTest.php b/tests/Feature/LogCommandTest.php new file mode 100644 index 00000000..9f6dc80a --- /dev/null +++ b/tests/Feature/LogCommandTest.php @@ -0,0 +1,68 @@ + $containerId = '12345', + 'names' => 'TO--mysql--8.0.22--3306', + 'status' => 'Up 27 minutes', + 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', + 'base_alias' => 'mysql', + 'full_alias' => 'mysql8.0', + ], + ]); + + $this->mock(Docker::class, function ($mock) use ($services, $containerId) { + $mock->shouldReceive('isInstalled')->andReturn(true); + $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); + $mock->shouldReceive('activeTakeoutContainers')->andReturn($services, new Collection); + $mock->shouldReceive('logContainer')->with($containerId); + }); + + $this->artisan('logs') + ->expectsQuestion('Takeout containers logs', $containerId) + ->assertExitCode(0); + } + + /** + * @test + * + * @testWith ["12345"] + * ["mysql"] + * + */ + public function it_can_access_logs_from_containers_by_service_name_or_id($arg) + { + $services = Collection::make([ + [ + 'container_id' => $containerId = '12345', + 'names' => 'TO--mysql--8.0.22--3306', + 'status' => 'Up 27 minutes', + 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', + 'base_alias' => 'mysql', + 'full_alias' => 'mysql8.0', + ], + ]); + + $this->mock(Docker::class, function ($mock) use ($services, $containerId) { + $mock->shouldReceive('isInstalled')->andReturn(true); + $mock->shouldReceive('isDockerServiceRunning')->andReturn(true); + $mock->shouldReceive('activeTakeoutContainers')->andReturn($services, new Collection); + $mock->shouldReceive('logContainer')->with($containerId); + }); + + + $this->artisan('logs', ['containerId' => $arg]) + ->assertExitCode(0); + } +} diff --git a/tests/Feature/StopCommandTest.php b/tests/Feature/StopCommandTest.php index 0ab30b5e..4c9d31c4 100644 --- a/tests/Feature/StopCommandTest.php +++ b/tests/Feature/StopCommandTest.php @@ -41,7 +41,7 @@ function it_can_stop_a_service_from_menu() * ["mysql"] * */ - function it_can_stop_containers_by_service_name($arg) + function it_can_stop_containers_by_service_name_or_id($arg) { $services = Collection::make([ [ From c79869bb72443f2aa6d461e76d18e6340b79a59c Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Wed, 18 Dec 2024 15:37:29 -0300 Subject: [PATCH 47/55] formatting --- tests/Feature/LogCommandTest.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Feature/LogCommandTest.php b/tests/Feature/LogCommandTest.php index 9f6dc80a..93ac03c2 100644 --- a/tests/Feature/LogCommandTest.php +++ b/tests/Feature/LogCommandTest.php @@ -13,12 +13,12 @@ function it_can_access_logss_from_a_service_from_menu() { $services = Collection::make([ [ - 'container_id' => $containerId = '12345', - 'names' => 'TO--mysql--8.0.22--3306', - 'status' => 'Up 27 minutes', - 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', - 'base_alias' => 'mysql', - 'full_alias' => 'mysql8.0', + 'container_id' => $containerId = '12345', + 'names' => 'TO--mysql--8.0.22--3306', + 'status' => 'Up 27 minutes', + 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', + 'base_alias' => 'mysql', + 'full_alias' => 'mysql8.0', ], ]); @@ -45,12 +45,12 @@ public function it_can_access_logs_from_containers_by_service_name_or_id($arg) { $services = Collection::make([ [ - 'container_id' => $containerId = '12345', - 'names' => 'TO--mysql--8.0.22--3306', - 'status' => 'Up 27 minutes', - 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', - 'base_alias' => 'mysql', - 'full_alias' => 'mysql8.0', + 'container_id' => $containerId = '12345', + 'names' => 'TO--mysql--8.0.22--3306', + 'status' => 'Up 27 minutes', + 'ports' => '0.0.0.0:3306->3306/tcp, 33060/tcp', + 'base_alias' => 'mysql', + 'full_alias' => 'mysql8.0', ], ]); From b10b59e1797782380b8263540c0e3cc36c288405 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Wed, 18 Dec 2024 15:43:42 -0300 Subject: [PATCH 48/55] Port must be a valid number --- app/Services/BaseService.php | 4 ++++ app/Shell/Environment.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php index 391a6acb..df8c2d78 100644 --- a/app/Services/BaseService.php +++ b/app/Services/BaseService.php @@ -165,6 +165,10 @@ protected function prompts(): void foreach ($questions as $prompt) { $items[] = match (true) { Str::contains($prompt['shortname'], 'port') => $this->askQuestion($prompt, $this->useDefaults, validate: function (string $port) { + if (! is_numeric($port)) { + return "Sorry, the port must be a valid number."; + } + if (! $this->environment->portIsAvailable($port)) { return "Port {$port} is already in use. Please select a different port."; } diff --git a/app/Shell/Environment.php b/app/Shell/Environment.php index 17e91498..9dd4119c 100644 --- a/app/Shell/Environment.php +++ b/app/Shell/Environment.php @@ -26,7 +26,7 @@ public function isWindowsOs(): bool return PHP_OS_FAMILY === 'Windows'; } - public function portIsAvailable($port): bool + public function portIsAvailable(int $port): bool { // To check if the socket is available, we'll attempt to open a socket on the port. // If we cannot open the socket, it means there's nothing running on it, so the From 57db66bba9c9f9d680c59da85a55225b0a2dbe9b Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Wed, 18 Dec 2024 15:46:37 -0300 Subject: [PATCH 49/55] Must return the port for the test to work --- tests/Feature/EnvironmentTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php index d6415af0..75f68b2f 100644 --- a/tests/Feature/EnvironmentTest.php +++ b/tests/Feature/EnvironmentTest.php @@ -36,5 +36,7 @@ private function withFakeProcessRunning($closure) } finally { socket_close($socket); } + + return $port; } } From 0582928ebf4d5a11a18f8cfc9ce1d26f1596085d Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Wed, 18 Dec 2024 15:47:05 -0300 Subject: [PATCH 50/55] formatting --- app/Services/BaseService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php index df8c2d78..0165942e 100644 --- a/app/Services/BaseService.php +++ b/app/Services/BaseService.php @@ -166,7 +166,7 @@ protected function prompts(): void $items[] = match (true) { Str::contains($prompt['shortname'], 'port') => $this->askQuestion($prompt, $this->useDefaults, validate: function (string $port) { if (! is_numeric($port)) { - return "Sorry, the port must be a valid number."; + return 'Sorry, the port must be a valid number.'; } if (! $this->environment->portIsAvailable($port)) { From a02d0b7032f081054e8fd8d833194b1c4c105f8b Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Thu, 19 Dec 2024 12:15:01 -0300 Subject: [PATCH 51/55] No need to wrap a collection as a collection --- app/Commands/DisableCommand.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/Commands/DisableCommand.php b/app/Commands/DisableCommand.php index a2385e90..021535ba 100644 --- a/app/Commands/DisableCommand.php +++ b/app/Commands/DisableCommand.php @@ -69,10 +69,9 @@ private function disableableServices(): Collection private function disableByServiceName(string $service, Collection $disableableServices): void { - $serviceMatches = collect($disableableServices) - ->filter(function ($containerName) use ($service) { - return Str::startsWith($containerName, $service); - }); + $serviceMatches = $disableableServices->filter(function ($containerName) use ($service) { + return Str::startsWith($containerName, $service); + }); if ($serviceMatches->isEmpty()) { $this->error("\nCannot find a Takeout-managed instance of {$service}."); From d0157c0ca1623e10224e247356740e72c3bb0e04 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Thu, 19 Dec 2024 12:16:28 -0300 Subject: [PATCH 52/55] formatting --- app/Exceptions/DockerMissingException.php | 2 +- app/Exceptions/DockerNotAvailableException.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Exceptions/DockerMissingException.php b/app/Exceptions/DockerMissingException.php index 49ee0002..1584216a 100644 --- a/app/Exceptions/DockerMissingException.php +++ b/app/Exceptions/DockerMissingException.php @@ -11,7 +11,7 @@ class DockerMissingException extends Exception { public function render($request = null): void { - error('Docker is not installed'); + error('Docker is not installed.'); note('Please visit https://docs.docker.com/get-docker/' . PHP_EOL . 'for information on how to install Docker for your machine.'); } } diff --git a/app/Exceptions/DockerNotAvailableException.php b/app/Exceptions/DockerNotAvailableException.php index b8708e44..ad871ad1 100644 --- a/app/Exceptions/DockerNotAvailableException.php +++ b/app/Exceptions/DockerNotAvailableException.php @@ -12,7 +12,7 @@ class DockerNotAvailableException extends Exception { public function render($request = null): void { - error('Docker is not available'); + error('Docker is not available.'); if (PHP_OS_FAMILY === 'Darwin') { note(implode(PHP_EOL, [ From 05913d01dcc10bcc357eb15e5f31f587f4eed02c Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Thu, 19 Dec 2024 12:33:02 -0300 Subject: [PATCH 53/55] wording --- app/Shell/Environment.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/Shell/Environment.php b/app/Shell/Environment.php index 9dd4119c..563d49ae 100644 --- a/app/Shell/Environment.php +++ b/app/Shell/Environment.php @@ -28,10 +28,9 @@ public function isWindowsOs(): bool public function portIsAvailable(int $port): bool { - // To check if the socket is available, we'll attempt to open a socket on the port. - // If we cannot open the socket, it means there's nothing running on it, so the - // port is available. If we are successful, that means it is already in use. - + // To check if the port is available, we'll attempt to open a socket connection to it. + // Note that the logic here is flipped: successfully openning the socket connection + // means something is using it. If it fails to open, that port is likely unused. $socket = @fsockopen($this->localhost(), $port, $errorCode, $errorMessage, timeout: 5); if (! $socket) { From 3bbe9de5be5d59cf4c2b190c9f996e71526a6412 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Thu, 19 Dec 2024 12:33:42 -0300 Subject: [PATCH 54/55] Rename method --- app/Shell/Environment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Shell/Environment.php b/app/Shell/Environment.php index 563d49ae..a04ab2db 100644 --- a/app/Shell/Environment.php +++ b/app/Shell/Environment.php @@ -42,7 +42,7 @@ public function portIsAvailable(int $port): bool return false; } - public function isTakeoutRunningOnDocker(): bool + public function isTakeoutRunningInsideDocker(): bool { return boolval($_SERVER['TAKEOUT_CONTAINER'] ?? false); } @@ -70,6 +70,6 @@ public function homeDirectory(): string private function localhost(): string { - return $this->isTakeoutRunningOnDocker() ? 'host.docker.internal' : 'localhost'; + return $this->isTakeoutRunningInsideDocker() ? 'host.docker.internal' : 'localhost'; } } From c98d2e4e676045c8de5ce58b72ab650bbf43d72e Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Fri, 3 Jan 2025 10:46:28 -0300 Subject: [PATCH 55/55] Fix typo Co-authored-by: Matt Stauffer --- app/Shell/Environment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Shell/Environment.php b/app/Shell/Environment.php index a04ab2db..54e873b3 100644 --- a/app/Shell/Environment.php +++ b/app/Shell/Environment.php @@ -29,7 +29,7 @@ public function isWindowsOs(): bool public function portIsAvailable(int $port): bool { // To check if the port is available, we'll attempt to open a socket connection to it. - // Note that the logic here is flipped: successfully openning the socket connection + // Note that the logic here is flipped: successfully opening the socket connection // means something is using it. If it fails to open, that port is likely unused. $socket = @fsockopen($this->localhost(), $port, $errorCode, $errorMessage, timeout: 5);