From 3500a5cac1ec3b5e15033ce22b055d3cbce4dd34 Mon Sep 17 00:00:00 2001
From: Benjamin Franzke <bfr@qbus.de>
Date: Tue, 22 Dec 2020 14:04:31 +0100
Subject: [PATCH] [TASK] Refactor low level console commands to avoid full boot
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This will allow for low level commands like cache:warmup (or flush)
to be implemented.
Commands that're registered in (internal!) service providers are now
defined to be lowlevel. That allows to change behaviour for internal
commands only, without being breaking.

Low level commands…
 * will only be defined by TYPO3 core, not by third party extensions
   or libraries (despite by using internal interfaces)
 * will not use any $GLOBALS in constructor
 * will only inject dependencies defined by service providers
 * must not require a database connection during construction
 * will only be defined in internal service providers
 * may use the internal BootService to bootstrap to a certain
   boot level

Regular commands…
 * are allowed to access TYPO3_CONF_VARS in constructor
 * are allowed to inject any service via constructor
 * are allowed to access TCA in constructor
 * may perform database calls in constructors (although this is not
   recommended)
 * may fail during construction, e.g. because of a stale DI container
   (an upcoming low level cache:flush/warmup command will be
   provided for such scenarios)

The command list `bin/typo3 list`…
 * will show all low level commands
 * will degrade to low level only commands if the DI
   container is not loadable

The upgrade command therefore now runs fully uncached and does not
require a full boot upfront.

The symfony command `help` is treated as non low level command,
as command constructors need to be executed in order for arguments
to be configured and available for help output. Therefore a full
boot is required. This causes the symfony DI container,
ext_localconf and ext_tables to be loaded.
That means: An invalid container cache will cause help to fail,
`list` will degrade do lowlevel commands, but an
(upcoming) low level console command cache:flush/warmup will
still be invokable and is intended for scenarios where 3rd party
code is changed and therefore DI/localconf/TCA cache became stale.

`typo3/cms-cli`, which provides vendor/bin/typo3 in composer mode,
is adapted in: https://github.com/TYPO3/cms-cli/pull/5

Commands used for updating typo3/cms-cli:

  composer require typo3/cms-cli:^3.0
  composer require typo3/cms-cli:^3.0 \
    --no-update --working-dir=typo3/sysext/core

Resolves: #86248
Related: #93174
Releases: master
Change-Id: Ie7cfb73983d96ed67532570be4099a25d106db28
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67241
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Helmut Hummel <typo3@helhum.io>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Helmut Hummel <typo3@helhum.io>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 composer.json                                 |   2 +-
 composer.lock                                 |  22 +--
 .../Command/Descriptor/TextDescriptor.php     |  12 +-
 .../core/Classes/Command/ListCommand.php      |  22 ++-
 .../Classes/Console/CommandApplication.php    |  89 ++++++++--
 .../sysext/core/Classes/Core/BootService.php  | 155 ++++++++++++++++++
 .../DependencyInjection/ContainerBuilder.php  |   3 +
 typo3/sysext/core/Classes/ServiceProvider.php |  20 ++-
 .../sysext/core/Resources/Private/Php/cli.php |   2 +-
 typo3/sysext/core/composer.json               |   2 +-
 .../Command/UpgradeWizardListCommand.php      |   2 +-
 .../Command/UpgradeWizardRunCommand.php       |   2 +-
 .../Classes/Service/LateBootService.php       | 130 +--------------
 13 files changed, 298 insertions(+), 165 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/Core/BootService.php

diff --git a/composer.json b/composer.json
index 3f791a9740c7..2e8cec9d3205 100644
--- a/composer.json
+++ b/composer.json
@@ -75,7 +75,7 @@
 		"symfony/var-dumper": "^5.2",
 		"symfony/yaml": "^5.2",
 		"typo3/class-alias-loader": "^1.0",
-		"typo3/cms-cli": "^2.0",
+		"typo3/cms-cli": "^3.0",
 		"typo3/cms-composer-installers": "^2.0 || ^3.0",
 		"typo3/phar-stream-wrapper": "^3.1.6",
 		"typo3/symfony-psr-event-dispatcher-adapter": "^1.0 || ^2.0",
diff --git a/composer.lock b/composer.lock
index ecdaeff89dac..22157f65aa37 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b4c3074ccc16c2e62a19f8ccba3f0998",
+    "content-hash": "ecf8365aa1212f8dec9609b56adab0f2",
     "packages": [
         {
             "name": "bacon/bacon-qr-code",
@@ -4373,16 +4373,16 @@
         },
         {
             "name": "typo3/cms-cli",
-            "version": "2.0.0",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/TYPO3/cms-cli.git",
-                "reference": "215a0bf5c446b4e0b20f4562bbaf3d6215a5ee82"
+                "reference": "bfb13f4ab6a505104662b79c18108f41c48e9288"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/TYPO3/cms-cli/zipball/215a0bf5c446b4e0b20f4562bbaf3d6215a5ee82",
-                "reference": "215a0bf5c446b4e0b20f4562bbaf3d6215a5ee82",
+                "url": "https://api.github.com/repos/TYPO3/cms-cli/zipball/bfb13f4ab6a505104662b79c18108f41c48e9288",
+                "reference": "bfb13f4ab6a505104662b79c18108f41c48e9288",
                 "shasum": ""
             },
             "require": {
@@ -4400,9 +4400,9 @@
             "homepage": "https://typo3.org",
             "support": {
                 "issues": "https://github.com/TYPO3/cms-cli/issues",
-                "source": "https://github.com/TYPO3/cms-cli/tree/master"
+                "source": "https://github.com/TYPO3/cms-cli/tree/3.0.0"
             },
-            "time": "2018-03-08T20:16:43+00:00"
+            "time": "2021-02-09T12:45:27+00:00"
         },
         {
             "name": "typo3/cms-composer-installers",
@@ -4622,12 +4622,12 @@
             "version": "1.9.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/webmozart/assert.git",
+                "url": "https://github.com/webmozarts/assert.git",
                 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+                "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
                 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
                 "shasum": ""
             },
@@ -4665,8 +4665,8 @@
                 "validate"
             ],
             "support": {
-                "issues": "https://github.com/webmozart/assert/issues",
-                "source": "https://github.com/webmozart/assert/tree/master"
+                "issues": "https://github.com/webmozarts/assert/issues",
+                "source": "https://github.com/webmozarts/assert/tree/1.9.1"
             },
             "time": "2020-07-08T17:02:28+00:00"
         }
diff --git a/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php b/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php
index 8d56392e76e3..62b9d8ed2c73 100644
--- a/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php
+++ b/typo3/sysext/core/Classes/Command/Descriptor/TextDescriptor.php
@@ -32,10 +32,12 @@ use TYPO3\CMS\Core\Console\CommandRegistry;
 class TextDescriptor extends SymfonyTextDescriptor
 {
     private CommandRegistry $commandRegistry;
+    private bool $degraded;
 
-    public function __construct(CommandRegistry $commandRegistry)
+    public function __construct(CommandRegistry $commandRegistry, bool $degraded)
     {
         $this->commandRegistry = $commandRegistry;
+        $this->degraded = $degraded;
     }
 
     /**
@@ -57,6 +59,10 @@ class TextDescriptor extends SymfonyTextDescriptor
             return;
         }
 
+        if ($this->degraded) {
+            $this->write("<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n\n", true);
+        }
+
         $namespaces = $this->commandRegistry->getNamespaces();
         $help = $application->getHelp();
         if ($help !== '') {
@@ -89,6 +95,10 @@ class TextDescriptor extends SymfonyTextDescriptor
         }
 
         $this->write("\n");
+
+        if ($this->degraded) {
+            $this->write("\n<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n", true);
+        }
     }
 
     private function describeNamespace(array $namespace, array $commands, int $width): void
diff --git a/typo3/sysext/core/Classes/Command/ListCommand.php b/typo3/sysext/core/Classes/Command/ListCommand.php
index 193afc6ca7bf..887305c5f1f2 100644
--- a/typo3/sysext/core/Classes/Command/ListCommand.php
+++ b/typo3/sysext/core/Classes/Command/ListCommand.php
@@ -17,23 +17,27 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Command;
 
+use Psr\Container\ContainerInterface;
 use Symfony\Component\Console\Command\ListCommand as SymfonyListCommand;
 use Symfony\Component\Console\Helper\DescriptorHelper;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use TYPO3\CMS\Core\Command\Descriptor\TextDescriptor;
 use TYPO3\CMS\Core\Console\CommandRegistry;
+use TYPO3\CMS\Core\Core\BootService;
 
 /**
  * ListCommand displays the list of all available commands for the application.
  */
 class ListCommand extends SymfonyListCommand
 {
-    protected CommandRegistry $commandRegistry;
+    protected ContainerInterface $failsafeContainer;
+    protected BootService $bootService;
 
-    public function __construct(CommandRegistry $commandRegistry)
+    public function __construct(ContainerInterface $failsafeContainer, BootService $bootService)
     {
-        $this->commandRegistry = $commandRegistry;
+        $this->failsafeContainer = $failsafeContainer;
+        $this->bootService = $bootService;
         parent::__construct();
     }
 
@@ -42,8 +46,18 @@ class ListCommand extends SymfonyListCommand
      */
     protected function execute(InputInterface $input, OutputInterface $output)
     {
+        $degraded = false;
+        try {
+            $container = $this->bootService->getContainer();
+        } catch (\Throwable $e) {
+            $container = $this->failsafeContainer;
+            $degraded = true;
+        }
+
+        $commandRegistry = $container->get(CommandRegistry::class);
+
         $helper = new DescriptorHelper();
-        $helper->register('txt', new TextDescriptor($this->commandRegistry));
+        $helper->register('txt', new TextDescriptor($commandRegistry, $degraded));
         $helper->describe($output, $this->getApplication(), [
             'format' => $input->getOption('format'),
             'raw_text' => $input->getOption('raw'),
diff --git a/typo3/sysext/core/Classes/Console/CommandApplication.php b/typo3/sysext/core/Classes/Console/CommandApplication.php
index 3c93507f10b5..0a7ea3f43e10 100644
--- a/typo3/sysext/core/Classes/Console/CommandApplication.php
+++ b/typo3/sysext/core/Classes/Console/CommandApplication.php
@@ -17,15 +17,18 @@ namespace TYPO3\CMS\Core\Console;
 
 use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\ExceptionInterface;
 use Symfony\Component\Console\Input\ArgvInput;
 use Symfony\Component\Console\Output\ConsoleOutput;
 use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
+use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Context\UserAspect;
 use TYPO3\CMS\Core\Context\VisibilityAspect;
 use TYPO3\CMS\Core\Context\WorkspaceAspect;
 use TYPO3\CMS\Core\Core\ApplicationInterface;
+use TYPO3\CMS\Core\Core\BootService;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Information\Typo3Version;
@@ -37,26 +40,27 @@ use TYPO3\CMS\Core\Localization\LanguageService;
  */
 class CommandApplication implements ApplicationInterface
 {
-    /**
-     * @var Context
-     */
-    protected $context;
+    protected Context $context;
 
-    /**
-     * @var CommandRegistry
-     */
-    protected $commandRegistry;
+    protected CommandRegistry $commandRegistry;
 
-    /**
-     * Instance of the symfony application
-     * @var Application
-     */
-    protected $application;
+    protected ConfigurationManager $configurationManager;
 
-    public function __construct(Context $context, CommandRegistry $commandRegistry)
-    {
+    protected BootService $bootService;
+
+    protected Application $application;
+
+    public function __construct(
+        Context $context,
+        CommandRegistry $commandRegistry,
+        ConfigurationManager $configurationMananger,
+        BootService $bootService
+    ) {
         $this->context = $context;
         $this->commandRegistry = $commandRegistry;
+        $this->configurationManager = $configurationMananger;
+        $this->bootService = $bootService;
+
         $this->checkEnvironmentOrDie();
         $this->application = new Application('TYPO3 CMS', sprintf(
             '%s (Application Context: <comment>%s</comment>)',
@@ -76,12 +80,31 @@ class CommandApplication implements ApplicationInterface
      */
     public function run(callable $execute = null)
     {
-        $this->initializeContext();
-
         $input = new ArgvInput();
         $output = new ConsoleOutput();
 
-        Bootstrap::loadExtTables();
+        $commandName = $this->getCommandName($input);
+        if ($this->wantsFullBoot($commandName)) {
+            // Do a full boot if command is not a low-level command
+            $container = $this->bootService->getContainer();
+            $this->application->setCommandLoader($container->get(CommandRegistry::class));
+            $this->context = $container->get(Context::class);
+
+            $isLowLevelCommandShortcut = false;
+            try {
+                $realName = $this->application->find($commandName)->getName();
+                // Do not load ext_localconf if a low level command was found
+                // due to using a shortcut
+                $isLowLevelCommandShortcut = !$this->wantsFullBoot($realName);
+            } catch (ExceptionInterface $e) {
+                // Errors must be ignored, full binding/validation happens later when the console application runs.
+            }
+            if (!$isLowLevelCommandShortcut && $this->essentialConfigurationExists()) {
+                $this->bootService->loadExtLocalconfDatabaseAndExtTables();
+            }
+        }
+
+        $this->initializeContext();
         // create the BE_USER object (not logged in yet)
         Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
         $GLOBALS['LANG'] = LanguageService::createFromUserPreferences($GLOBALS['BE_USER']);
@@ -97,6 +120,36 @@ class CommandApplication implements ApplicationInterface
         exit($exitCode);
     }
 
+    protected function wantsFullBoot(string $commandName): bool
+    {
+        if ($commandName === 'help') {
+            return true;
+        }
+        return !$this->commandRegistry->has($commandName);
+    }
+
+    protected function getCommandName(ArgvInput $input): string
+    {
+        try {
+            $input->bind($this->application->getDefinition());
+        } catch (ExceptionInterface $e) {
+            // Errors must be ignored, full binding/validation happens later when the console application runs.
+        }
+
+        return $input->getFirstArgument() ?? 'list';
+    }
+
+    /**
+     * Check if LocalConfiguration.php and PackageStates.php exist
+     *
+     * @return bool TRUE when the essential configuration is available, otherwise FALSE
+     */
+    protected function essentialConfigurationExists(): bool
+    {
+        return file_exists($this->configurationManager->getLocalConfigurationFileLocation())
+            && file_exists(Environment::getLegacyConfigPath() . '/PackageStates.php');
+    }
+
     /**
      * Check the script is called from a cli environment.
      */
diff --git a/typo3/sysext/core/Classes/Core/BootService.php b/typo3/sysext/core/Classes/Core/BootService.php
new file mode 100644
index 000000000000..2501940781e1
--- /dev/null
+++ b/typo3/sysext/core/Classes/Core/BootService.php
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Core;
+
+use Psr\Container\ContainerInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
+use TYPO3\CMS\Core\Imaging\IconRegistry;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal This is NOT an API class, it is for internal use in TYPO3 core only.
+ */
+class BootService
+{
+    private ContainerBuilder $containerBuilder;
+
+    private ContainerInterface $failsafeContainer;
+
+    private ?ContainerInterface $container = null;
+
+    public function __construct(ContainerBuilder $containerBuilder, ContainerInterface $failsafeContainer)
+    {
+        $this->containerBuilder = $containerBuilder;
+        $this->failsafeContainer = $failsafeContainer;
+    }
+
+    public function getContainer(bool $allowCaching = true): ContainerInterface
+    {
+        return $this->container ?? $this->prepareContainer($allowCaching);
+    }
+
+    private function prepareContainer(bool $allowCaching = true): ContainerInterface
+    {
+        $packageManager = $this->failsafeContainer->get(PackageManager::class);
+        $dependencyInjectionContainerCache = $this->failsafeContainer->get('cache.di');
+
+        $failsafe = false;
+
+        // Build a non-failsafe container which is required for loading ext_localconf
+        $this->container = $this->containerBuilder->createDependencyInjectionContainer($packageManager, $dependencyInjectionContainerCache, $failsafe);
+        $this->container->set('_early.boot-service', $this);
+        if ($allowCaching) {
+            $this->container->get('boot.state')->cacheDisabled = false;
+            $coreCache = Bootstrap::createCache('core');
+            // Core cache is initialized with a NullBackend in failsafe mode.
+            // Replace it with a new cache that uses the real backend.
+            $this->container->set('_early.cache.core', $coreCache);
+            $this->container->set('_early.cache.assets', Bootstrap::createCache('assets'));
+            $this->container->get(PackageManager::class)->injectCoreCache($coreCache);
+        }
+
+        return $this->container;
+    }
+
+    /**
+     * Switch global context to a new context, or revert
+     * to the original booting container if no container
+     * is specified
+     *
+     * @param ContainerInterface $container
+     * @param array $backup
+     * @return array
+     */
+    public function makeCurrent(ContainerInterface $container = null, array $backup = []): array
+    {
+        $container = $container ?? $backup['container'] ?? $this->failsafeContainer;
+
+        $newBackup = [
+            'singletonInstances' => GeneralUtility::getSingletonInstances(),
+            'container' => GeneralUtility::getContainer(),
+        ];
+
+        GeneralUtility::purgeInstances();
+
+        // Set global state to the non-failsafe container and it's instances
+        GeneralUtility::setContainer($container);
+        ExtensionManagementUtility::setPackageManager($container->get(PackageManager::class));
+
+        $backupSingletonInstances = $backup['singletonInstances'] ?? [];
+        foreach ($backupSingletonInstances as $className => $instance) {
+            GeneralUtility::setSingletonInstance($className, $instance);
+        }
+
+        return $newBackup;
+    }
+
+    /**
+     * Bootstrap a non-failsafe container and load ext_localconf
+     *
+     * Use by actions like the database analyzer and the upgrade wizards which
+     * need additional bootstrap actions performed.
+     *
+     * Those actions can potentially fatal if some old extension is loaded that triggers
+     * a fatal in ext_localconf or ext_tables code! Use only if really needed.
+     *
+     * @param bool $resetContainer
+     * @param bool $allowCaching
+     * @return ContainerInterface
+     */
+    public function loadExtLocalconfDatabaseAndExtTables(bool $resetContainer = false, bool $allowCaching = true): ContainerInterface
+    {
+        $container = $this->getContainer($allowCaching);
+
+        $backup = $this->makeCurrent($container);
+        $beUserBackup = $GLOBALS['BE_USER'] ?? null;
+
+        $container->get('boot.state')->done = false;
+        $assetsCache = $container->get('cache.assets');
+        IconRegistry::setCache($assetsCache);
+        PageRenderer::setCache($assetsCache);
+        $eventDispatcher = $container->get(EventDispatcherInterface::class);
+        ExtensionManagementUtility::setEventDispatcher($eventDispatcher);
+        Bootstrap::loadTypo3LoadedExtAndExtLocalconf($allowCaching, $container->get('cache.core'));
+        Bootstrap::unsetReservedGlobalVariables();
+        $GLOBALS['BE_USER'] = $beUserBackup;
+        $container->get('boot.state')->done = true;
+        Bootstrap::loadBaseTca($allowCaching);
+        Bootstrap::loadExtTables($allowCaching);
+
+        if ($resetContainer) {
+            $this->makeCurrent(null, $backup);
+        }
+
+        return $container;
+    }
+
+    public function resetGlobalContainer(): void
+    {
+        $this->makeCurrent(null, []);
+    }
+
+    public function getFailsafeContainer(): ContainerInterface
+    {
+        return $this->failsafeContainer;
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php b/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php
index 8df0e0ae494d..040f679bce10 100644
--- a/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php
+++ b/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php
@@ -127,6 +127,9 @@ class ContainerBuilder
             $containerBuilder->setAlias($id, $syntheticId)->setPublic(true);
         }
 
+        // Optional service, set by BootService as back reference to the original bootService
+        $containerBuilder->register('_early.boot-service')->setSynthetic(true)->setPublic(true);
+
         $containerBuilder->compile();
 
         return $containerBuilder;
diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php
index 08fafac75df1..6c8e53c76ff0 100644
--- a/typo3/sysext/core/Classes/ServiceProvider.php
+++ b/typo3/sysext/core/Classes/ServiceProvider.php
@@ -23,6 +23,7 @@ use Psr\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Console\Command\HelpCommand;
 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
 use TYPO3\CMS\Core\Package\AbstractServiceProvider;
 use TYPO3\SymfonyPsrEventDispatcherAdapter\EventDispatcherAdapter as SymfonyEventDispatcher;
 
@@ -49,6 +50,7 @@ class ServiceProvider extends AbstractServiceProvider
             Console\CommandApplication::class => [ static::class, 'getConsoleCommandApplication' ],
             Console\CommandRegistry::class => [ static::class, 'getConsoleCommandRegistry' ],
             Context\Context::class => [ static::class, 'getContext' ],
+            Core\BootService::class => [ static::class, 'getBootService' ],
             Crypto\PasswordHashing\PasswordHashFactory::class => [ static::class, 'getPasswordHashFactory' ],
             EventDispatcher\EventDispatcher::class => [ static::class, 'getEventDispatcher' ],
             EventDispatcher\ListenerProvider::class => [ static::class, 'getEventListenerProvider' ],
@@ -137,7 +139,8 @@ class ServiceProvider extends AbstractServiceProvider
     public static function getListCommand(ContainerInterface $container): Command\ListCommand
     {
         return new Command\ListCommand(
-            $container->get(Console\CommandRegistry::class)
+            $container,
+            $container->get(Core\BootService::class)
         );
     }
 
@@ -155,7 +158,9 @@ class ServiceProvider extends AbstractServiceProvider
     {
         return new Console\CommandApplication(
             $container->get(Context\Context::class),
-            $container->get(Console\CommandRegistry::class)
+            $container->get(Console\CommandRegistry::class),
+            $container->get(Configuration\ConfigurationManager::class),
+            $container->get(Core\BootService::class)
         );
     }
 
@@ -193,6 +198,17 @@ class ServiceProvider extends AbstractServiceProvider
         return new Context\Context();
     }
 
+    public static function getBootService(ContainerInterface $container): Core\BootService
+    {
+        if ($container->has('_early.boot-service')) {
+            return $container->get('_early.boot-service');
+        }
+        return new Core\BootService(
+            $container->get(ContainerBuilder::class),
+            $container
+        );
+    }
+
     public static function getPasswordHashFactory(ContainerInterface $container): Crypto\PasswordHashing\PasswordHashFactory
     {
         return new Crypto\PasswordHashing\PasswordHashFactory();
diff --git a/typo3/sysext/core/Resources/Private/Php/cli.php b/typo3/sysext/core/Resources/Private/Php/cli.php
index 7b9b55ee2e6c..d96be4e832a4 100644
--- a/typo3/sysext/core/Resources/Private/Php/cli.php
+++ b/typo3/sysext/core/Resources/Private/Php/cli.php
@@ -20,5 +20,5 @@
 call_user_func(function () {
     $classLoader = require __DIR__ . '/../../../../../../vendor/autoload.php';
     \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(4, \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI);
-    \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader)->get(\TYPO3\CMS\Core\Console\CommandApplication::class)->run();
+    \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader, true)->get(\TYPO3\CMS\Core\Console\CommandApplication::class)->run();
 });
diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json
index ae5f12cb3d89..bee5276c5284 100644
--- a/typo3/sysext/core/composer.json
+++ b/typo3/sysext/core/composer.json
@@ -59,7 +59,7 @@
 		"symfony/routing": "^5.2",
 		"symfony/yaml": "^5.2",
 		"typo3/class-alias-loader": "^1.0",
-		"typo3/cms-cli": "^2.0",
+		"typo3/cms-cli": "^3.0",
 		"typo3/cms-composer-installers": "^2.0 || ^3.0",
 		"typo3/phar-stream-wrapper": "^3.1.6",
 		"typo3/symfony-psr-event-dispatcher-adapter": "^1.0 || ^2.0",
diff --git a/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php b/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
index f3e1089accad..6bc57dfe62c2 100644
--- a/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
+++ b/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
@@ -72,7 +72,7 @@ class UpgradeWizardListCommand extends Command
      */
     protected function bootstrap(): void
     {
-        $this->lateBootService->loadExtLocalconfDatabaseAndExtTables();
+        $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false);
         Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
         Bootstrap::initializeBackendAuthentication();
     }
diff --git a/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php b/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
index 93ceba4d9393..9e6280637dc5 100644
--- a/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
+++ b/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
@@ -77,7 +77,7 @@ class UpgradeWizardRunCommand extends Command
      */
     protected function bootstrap(): void
     {
-        $this->lateBootService->loadExtLocalconfDatabaseAndExtTables();
+        $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false);
         Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
         Bootstrap::initializeBackendAuthentication();
         $this->upgradeWizardsService->isDatabaseCharsetUtf8() ?: $this->upgradeWizardsService->setDatabaseCharsetUtf8();
diff --git a/typo3/sysext/install/Classes/Service/LateBootService.php b/typo3/sysext/install/Classes/Service/LateBootService.php
index ac779dfa7ff8..dd44754b6f5c 100644
--- a/typo3/sysext/install/Classes/Service/LateBootService.php
+++ b/typo3/sysext/install/Classes/Service/LateBootService.php
@@ -18,138 +18,20 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Install\Service;
 
 use Psr\Container\ContainerInterface;
-use Psr\EventDispatcher\EventDispatcherInterface;
-use TYPO3\CMS\Core\Core\Bootstrap;
-use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
-use TYPO3\CMS\Core\Imaging\IconRegistry;
-use TYPO3\CMS\Core\Package\PackageManager;
-use TYPO3\CMS\Core\Page\PageRenderer;
-use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Core\BootService;
 
 /**
  * @internal This is NOT an API class, it is for internal use in the install tool only.
  */
-class LateBootService
+class LateBootService extends BootService
 {
-    /**
-     * @var ContainerBuilder
-     */
-    private $containerBuilder;
-
-    /**
-     * @var ContainerInterface
-     */
-    private $failsafeContainer;
-
-    /**
-     * @var ContainerInterface
-     */
-    private $container;
-
-    /**
-     * @param ContainerBuilder $containerBuilder
-     * @param ContainerInterface $failsafeContainer
-     */
-    public function __construct(ContainerBuilder $containerBuilder, ContainerInterface $failsafeContainer)
-    {
-        $this->containerBuilder = $containerBuilder;
-        $this->failsafeContainer = $failsafeContainer;
-    }
-
-    /**
-     * @return ContainerInterface
-     */
-    public function getContainer(): ContainerInterface
-    {
-        return $this->container ?? $this->prepareContainer();
-    }
-
-    /**
-     * @return ContainerInterface
-     */
-    private function prepareContainer(): ContainerInterface
-    {
-        $packageManager = $this->failsafeContainer->get(PackageManager::class);
-        $dependencyInjectionContainerCache = $this->failsafeContainer->get('cache.di');
-
-        $failsafe = false;
-
-        // Build a non-failsafe container which is required for loading ext_localconf
-        return $this->container = $this->containerBuilder->createDependencyInjectionContainer($packageManager, $dependencyInjectionContainerCache, $failsafe);
-    }
-
-    /**
-     * Switch global context to a new context, or revert
-     * to the original booting container if no container
-     * is specified
-     *
-     * @param ContainerInterface $container
-     * @param array $backup
-     * @return array
-     */
-    public function makeCurrent(ContainerInterface $container = null, array $backup = []): array
+    public function getContainer(bool $allowCaching = false): ContainerInterface
     {
-        $container = $container ?? $backup['container'] ?? $this->failsafeContainer;
-
-        $newBackup = [
-            'singletonInstances' => GeneralUtility::getSingletonInstances(),
-            'container' => GeneralUtility::getContainer(),
-        ];
-
-        GeneralUtility::purgeInstances();
-
-        // Set global state to the non-failsafe container and it's instances
-        GeneralUtility::setContainer($container);
-        ExtensionManagementUtility::setPackageManager($container->get(PackageManager::class));
-
-        $backupSingletonInstances = $backup['singletonInstances'] ?? [];
-        foreach ($backupSingletonInstances as $className => $instance) {
-            GeneralUtility::setSingletonInstance($className, $instance);
-        }
-
-        return $newBackup;
-    }
-
-    /**
-     * Bootstrap a non-failsafe container and load ext_localconf
-     *
-     * Use by actions like the database analyzer and the upgrade wizards which
-     * need additional bootstrap actions performed.
-     *
-     * Those actions can potentially fatal if some old extension is loaded that triggers
-     * a fatal in ext_localconf or ext_tables code! Use only if really needed.
-     *
-     * @param bool $resetContainer
-     * @return ContainerInterface
-     */
-    public function loadExtLocalconfDatabaseAndExtTables(bool $resetContainer = true): ContainerInterface
-    {
-        $container = $this->getContainer();
-
-        $backup = $this->makeCurrent($container);
-
-        $container->get('boot.state')->done = false;
-        $assetsCache = $container->get('cache.assets');
-        IconRegistry::setCache($assetsCache);
-        PageRenderer::setCache($assetsCache);
-        $eventDispatcher = $container->get(EventDispatcherInterface::class);
-        ExtensionManagementUtility::setEventDispatcher($eventDispatcher);
-        Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
-        Bootstrap::unsetReservedGlobalVariables();
-        $container->get('boot.state')->done = true;
-        Bootstrap::loadBaseTca(false);
-        Bootstrap::loadExtTables(false);
-
-        if ($resetContainer) {
-            $this->makeCurrent(null, $backup);
-        }
-
-        return $container;
+        return parent::getContainer($allowCaching);
     }
 
-    public function resetGlobalContainer(): void
+    public function loadExtLocalconfDatabaseAndExtTables(bool $resetContainer = true, bool $allowCaching = false): ContainerInterface
     {
-        $this->makeCurrent(null, []);
+        return parent::loadExtLocalconfDatabaseAndExtTables($resetContainer, $allowCaching);
     }
 }
-- 
GitLab