// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/crosapi/browser_launcher.h"
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/posix/unix_domain_socket.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/idle_service_ash.h"
#include "chrome/browser/ash/crosapi/test_crosapi_dependency_registry.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/standalone_browser/lacros_selection.h"
#include "chromeos/ash/components/system/fake_statistics_provider.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "chromeos/crosapi/cpp/crosapi_constants.h"
#include "chromeos/startup/startup_switches.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/fake_device_ownership_waiter.h"
#include "components/user_manager/scoped_user_manager.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ui_base_features.h"
namespace crosapi {
namespace {
const char kPrimaryProfileEmail[] = "user@test";
} // namespace
class BrowserLauncherTest : public testing::Test {
public:
BrowserLauncherTest() = default;
void SetUp() override {
fake_user_manager_.Reset(std::make_unique<ash::FakeChromeUserManager>());
CHECK(profile_manager_.SetUp());
// Settings required to create startup data.
crosapi::IdleServiceAsh::DisableForTesting();
ash::LoginState::Initialize();
crosapi_manager_ = crosapi::CreateCrosapiManagerWithTestRegistry();
ash::system::StatisticsProvider::SetTestProvider(
&fake_statistics_provider_);
browser_launcher_.set_device_ownership_waiter_for_testing(
std::make_unique<user_manager::FakeDeviceOwnershipWaiter>());
}
void TearDown() override {
crosapi_manager_.reset();
ash::LoginState::Shutdown();
}
protected:
BrowserLauncher* browser_launcher() { return &browser_launcher_; }
void CreatePrimaryProfile() {
const AccountId account_id(AccountId::FromUserEmail(kPrimaryProfileEmail));
fake_user_manager_->AddUser(account_id);
fake_user_manager_->LoginUser(account_id,
/*set_profile_created_flag=*/false);
profile_manager_.CreateTestingProfile(account_id.GetUserEmail());
fake_user_manager_->SimulateUserProfileLoad(account_id);
}
void PrepareFilesToPreload(const base::FilePath& lacros_dir) {
base::CreateDirectory(lacros_dir.Append("locales"));
for (const auto& file_path : BrowserLauncher::GetPreloadFiles(lacros_dir)) {
base::WriteFile(file_path, "dummy file");
}
}
private:
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<crosapi::CrosapiManager> crosapi_manager_;
ash::ScopedCrosSettingsTestHelper cros_settings_test_helper_;
user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
fake_user_manager_;
TestingProfileManager profile_manager_{TestingBrowserProcess::GetGlobal()};
// Required to create startup data.
ash::system::ScopedFakeStatisticsProvider fake_statistics_provider_;
BrowserLauncher browser_launcher_;
};
TEST_F(BrowserLauncherTest, AdditionalParametersForLaunchParams) {
BrowserLauncher::LaunchParamsFromBackground params;
params.lacros_additional_args.emplace_back("--switch1");
params.lacros_additional_args.emplace_back("--switch2=value2");
base::test::ScopedCommandLine scoped_command_line;
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
ash::switches::kLacrosChromeAdditionalArgs, "--foo####--switch3=value3");
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
ash::switches::kLacrosChromeAdditionalEnv, "foo1=bar2####switch4=value4");
BrowserLauncher::LaunchParams parameters(
base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
browser_launcher()->SetUpAdditionalParametersForTesting(params, parameters);
EXPECT_TRUE(parameters.command_line.HasSwitch("switch1"));
EXPECT_TRUE(parameters.command_line.HasSwitch("foo"));
EXPECT_EQ(parameters.command_line.GetSwitchValueASCII("switch2"), "value2");
EXPECT_EQ(parameters.command_line.GetSwitchValueASCII("switch3"), "value3");
EXPECT_EQ(parameters.options.environment["foo1"], "bar2");
EXPECT_EQ(parameters.options.environment["switch4"], "value4");
EXPECT_EQ(parameters.command_line.GetSwitches().size(), 4u);
EXPECT_EQ(parameters.options.environment.size(), 2u);
}
TEST_F(BrowserLauncherTest, WithoutAdditionalParametersForCommandLine) {
BrowserLauncher::LaunchParamsFromBackground params;
base::test::ScopedCommandLine scoped_command_line;
BrowserLauncher::LaunchParams parameters(
base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
parameters.command_line.RemoveSwitch(
ash::switches::kLacrosChromeAdditionalArgs);
browser_launcher()->SetUpAdditionalParametersForTesting(params, parameters);
EXPECT_EQ(parameters.command_line.GetSwitches().size(), 0u);
EXPECT_EQ(parameters.options.environment.size(), 0u);
}
// A --vmodule value provided via --lacros-chrome-additional-args is preserved.
TEST_F(BrowserLauncherTest, LacrosChromeAdditionalArgsVModule) {
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
ash::switches::kLacrosChromeAdditionalArgs, "--vmodule=foo");
base::ScopedFD dummy_fd1, dummy_fd2;
base::CreateSocketPair(&dummy_fd1, &dummy_fd2);
mojo::PlatformChannel dummy_channel;
BrowserLauncher::LaunchParamsFromBackground bg_params;
bg_params.logfd = std::move(dummy_fd1);
BrowserLauncher::LaunchParams params =
browser_launcher()->CreateLaunchParamsForTesting({}, bg_params, {}, {},
{}, dummy_channel, {});
EXPECT_EQ(params.command_line.GetSwitchValueASCII("vmodule"), "foo");
}
TEST_F(BrowserLauncherTest, LaunchAndTriggerTerminate) {
// We'll use a process just does nothing for 30 seconds, which is long
// enough to stably exercise the test cases we have.
BrowserLauncher::LaunchParams parameters(
base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
browser_launcher()->LaunchProcessForTesting(parameters);
EXPECT_TRUE(browser_launcher()->IsProcessValid());
EXPECT_TRUE(browser_launcher()->TriggerTerminate(/*exit_code=*/0));
int exit_code;
EXPECT_TRUE(
browser_launcher()->GetProcessForTesting().WaitForExit(&exit_code));
// -1 is expected as an `exit_code` because it is compulsorily terminated by
// signal.
EXPECT_EQ(exit_code, -1);
// TODO(mayukoaiba): We should reset process in order to check by
// EXPECT_FALSE(browser_launcher()->IsProcessValid()) whether
// "TriggerTerminate" works properly.
}
TEST_F(BrowserLauncherTest, TerminateOnBackground) {
// We'll use a process just does nothing for 30 seconds, which is long
// enough to stably exercise the test cases we have.
BrowserLauncher::LaunchParams parameters(
base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
browser_launcher()->LaunchProcessForTesting(parameters);
ASSERT_TRUE(browser_launcher()->IsProcessValid());
base::test::TestFuture<void> future;
browser_launcher()->EnsureProcessTerminated(future.GetCallback(),
base::Seconds(5));
EXPECT_FALSE(browser_launcher()->IsProcessValid());
}
TEST_F(BrowserLauncherTest, BackgroundWorkPreLaunch) {
base::ScopedTempDir lacros_dir;
ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());
// Add feature and check if it's reflected to `params`.
base::test::ScopedFeatureList scoped_features;
scoped_features.InitAndEnableFeature(features::kLacrosResourcesFileSharing);
BrowserLauncher::LaunchParamsFromBackground params;
base::test::TestFuture<void> future;
browser_launcher()->WaitForBackgroundWorkPreLaunchForTesting(
lacros_dir.GetPath(), /*clear_shared_resource_file=*/true,
/*launching_at_login_screen=*/false, future.GetCallback(), params);
EXPECT_TRUE(future.Wait());
EXPECT_TRUE(params.enable_resource_file_sharing);
}
TEST_F(BrowserLauncherTest, BackgroundWorkPreLaunchOnLaunchingAtLoginScreen) {
base::ScopedTempDir lacros_dir;
ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());
// Create preload files which will be loaded on launching at login screen.
PrepareFilesToPreload(lacros_dir.GetPath());
// Add feature and check if it's reflected to `params`.
base::test::ScopedFeatureList scoped_features;
scoped_features.InitAndEnableFeature(features::kLacrosResourcesFileSharing);
BrowserLauncher::LaunchParamsFromBackground params;
base::test::TestFuture<void> future;
browser_launcher()->WaitForBackgroundWorkPreLaunchForTesting(
lacros_dir.GetPath(), /*clear_shared_resource_file=*/true,
/*launching_at_login_screen=*/true, future.GetCallback(), params);
EXPECT_TRUE(future.Wait());
EXPECT_TRUE(params.enable_resource_file_sharing);
}
// TODO(elkurin): Add kLacrosChromeAdditionalArgsFile unit test.
TEST_F(BrowserLauncherTest, Launch) {
base::ScopedTempDir lacros_dir;
ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());
// Create dummy lacros binary.
const base::FilePath lacros_path = lacros_dir.GetPath().Append("chrome");
base::WriteFile(lacros_path, "I am chrome binary");
base::test::TestFuture<base::expected<BrowserLauncher::LaunchResults,
BrowserLauncher::LaunchFailureReason>>
future;
constexpr bool launching_at_login_screen = false;
browser_launcher()->Launch(lacros_path, launching_at_login_screen,
ash::standalone_browser::LacrosSelection::kRootfs,
/*mojo_disconneciton_cb=*/{},
/*is_keep_alive_enabled=*/false,
future.GetCallback());
// Before adding primary profile, Launch should not proceed.
EXPECT_FALSE(user_manager::UserManager::Get()->GetPrimaryUser());
EXPECT_FALSE(future.IsReady());
// Create primary profile.
CreatePrimaryProfile();
EXPECT_TRUE(user_manager::UserManager::Get()->GetPrimaryUser());
// Make sure that Launch completes with success.
EXPECT_TRUE(future.Get<0>().has_value());
}
TEST_F(BrowserLauncherTest, LaunchAtLoginScreen) {
base::ScopedTempDir lacros_dir;
ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());
// Create preload files which will be loaded on launching at login screen.
// This will create chrome binary as well.
PrepareFilesToPreload(lacros_dir.GetPath());
const base::FilePath lacros_path = lacros_dir.GetPath().Append("chrome");
base::test::TestFuture<base::expected<BrowserLauncher::LaunchResults,
BrowserLauncher::LaunchFailureReason>>
future;
constexpr bool launching_at_login_screen = true;
browser_launcher()->Launch(lacros_path, launching_at_login_screen,
ash::standalone_browser::LacrosSelection::kRootfs,
/*mojo_disconneciton_cb=*/{},
/*is_keep_alive_enabled=*/false,
future.GetCallback());
// Make sure that Launch completes with success. In launching at login screen
// scenario, we completes Launch flow without waiting for the primary profile.
EXPECT_TRUE(future.Get<0>().has_value());
EXPECT_FALSE(user_manager::UserManager::Get()->GetPrimaryUser());
}
TEST_F(BrowserLauncherTest, ResumeLaunch) {
// Creates postlogin data pipe.
base::ScopedFD read_postlogin_fd =
browser_launcher()->CreatePostLoginPipeForTesting();
ASSERT_TRUE(read_postlogin_fd.is_valid());
// Creates a dedicated thread to read postlogin data which mocks Lacros
// process.
base::test::TestFuture<void> wait_read;
base::ThreadPool::CreateSingleThreadTaskRunner(
{base::MayBlock()}, base::SingleThreadTaskRunnerThreadMode::DEDICATED)
->PostTaskAndReply(
FROM_HERE,
base::BindOnce(
[](base::ScopedFD fd) {
base::ScopedFILE file(fdopen(fd.get(), "r"));
std::string content;
EXPECT_TRUE(base::ReadStreamToString(file.get(), &content));
fd.reset();
},
std::move(read_postlogin_fd)),
base::BindOnce(wait_read.GetCallback()));
// Resume launch. This must be called after potlogin data pipe is constructed.
base::test::TestFuture<
base::expected<base::TimeTicks, BrowserLauncher::LaunchFailureReason>>
future;
browser_launcher()->ResumeLaunch(future.GetCallback());
// On resume launch, we need to wait for device owner and primary profile to
// be ready, so should not complete ResumeLaunch at this point.
EXPECT_FALSE(future.IsReady());
// Create primary profile.
CreatePrimaryProfile();
EXPECT_TRUE(user_manager::UserManager::Get()->GetPrimaryUser());
// Make sure that ResumeLaunch complets with success.
EXPECT_TRUE(future.Get<0>().has_value());
// Wait for fd to close for clean up.
EXPECT_TRUE(wait_read.Wait());
}
TEST_F(BrowserLauncherTest, ShutdownRequestedDuringLaunch) {
base::test::TestFuture<base::expected<BrowserLauncher::LaunchResults,
BrowserLauncher::LaunchFailureReason>>
future;
// To test asynchronous behavior, we assume it's not launching at login
// screen.
constexpr bool launching_at_login_screen = false;
browser_launcher()->Launch(base::FilePath(), launching_at_login_screen,
ash::standalone_browser::LacrosSelection::kRootfs,
/*mojo_disconneciton_cb=*/{},
/*is_keep_alive_enabled=*/false,
future.GetCallback());
// Shutdown is synchronous while Launch preparation is asynchronously waiting,
// for primary profiel to be ready so Shutdown request runs earlier.
browser_launcher()->Shutdown();
// Create primary profile and proceed Launch.
CreatePrimaryProfile();
EXPECT_TRUE(user_manager::UserManager::Get()->GetPrimaryUser());
// Launch should fail due to shutdown requested.
EXPECT_FALSE(future.Get<0>().has_value());
EXPECT_EQ(BrowserLauncher::LaunchFailureReason::kShutdownRequested,
future.Get<0>().error());
}
} // namespace crosapi