| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // 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/test_mojo_connection_manager.h" |
| |
| #include <fcntl.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_path_watcher.h" |
| #include "base/files/scoped_file.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/notreached.h" |
| #include "base/process/launch.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/bind.h" |
| #include "base/test/multiprocess_test.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_timeouts.h" |
| #include "chrome/browser/ash/crosapi/browser_service_host_ash.h" |
| #include "chrome/browser/ash/crosapi/browser_service_host_observer.h" |
| #include "chrome/browser/ash/crosapi/crosapi_ash.h" |
| #include "chrome/browser/ash/crosapi/crosapi_manager.h" |
| #include "chrome/browser/ash/crosapi/environment_provider.h" |
| #include "chrome/browser/ash/crosapi/idle_service_ash.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/test/base/scoped_testing_local_state.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "chromeos/crosapi/cpp/crosapi_constants.h" |
| #include "chromeos/crosapi/mojom/crosapi.mojom.h" |
| #include "chromeos/login/login_state/login_state.h" |
| #include "chromeos/startup/startup_switches.h" |
| #include "components/account_id/account_id.h" |
| #include "components/user_manager/fake_user_manager.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "mojo/public/cpp/platform/named_platform_channel.h" |
| #include "mojo/public/cpp/platform/platform_channel.h" |
| #include "mojo/public/cpp/platform/socket_utils_posix.h" |
| #include "mojo/public/cpp/system/invitation.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/multiprocess_func_list.h" |
| |
| namespace crosapi { |
| namespace { |
| |
| class TestBrowserService : public crosapi::mojom::BrowserService { |
| public: |
| TestBrowserService() : receiver_(this) {} |
| ~TestBrowserService() override = default; |
| |
| mojo::PendingRemote<crosapi::mojom::BrowserService> |
| BindNewPipeAndPassRemote() { |
| return receiver_.BindNewPipeAndPassRemote(); |
| } |
| |
| void REMOVED_0(REMOVED_0Callback callback) override { NOTIMPLEMENTED(); } |
| void REMOVED_2(crosapi::mojom::BrowserInitParamsPtr) override { |
| NOTIMPLEMENTED(); |
| } |
| |
| void NewWindow(bool incognito, |
| bool should_trigger_session_restore, |
| NewWindowCallback callback) override {} |
| void NewWindowForDetachingTab( |
| const std::u16string& tab_id, |
| const std::u16string& group_id, |
| NewWindowForDetachingTabCallback closure) override {} |
| void NewFullscreenWindow(const GURL& url, |
| NewFullscreenWindowCallback callback) override {} |
| void NewTab(NewTabCallback callback) override {} |
| void OpenUrl(const GURL& url, OpenUrlCallback callback) override {} |
| void RestoreTab(RestoreTabCallback callback) override {} |
| void HandleTabScrubbing(float x_offset) override {} |
| void GetFeedbackData(GetFeedbackDataCallback callback) override {} |
| void GetHistograms(GetHistogramsCallback callback) override {} |
| void GetActiveTabUrl(GetActiveTabUrlCallback callback) override {} |
| void UpdateDeviceAccountPolicy(const std::vector<uint8_t>& policy) override {} |
| void UpdateKeepAlive(bool enabled) override {} |
| |
| private: |
| mojo::Receiver<mojom::BrowserService> receiver_; |
| }; |
| |
| class TestBrowserServiceHostObserver : public BrowserServiceHostObserver { |
| public: |
| // |callback| is invoked when OnBrowserServiceConnected is called |
| // |num_calls| times. |
| TestBrowserServiceHostObserver(size_t num_calls, base::OnceClosure callback) |
| : remaining_num_calls_(num_calls), callback_(std::move(callback)) { |
| DCHECK_GT(num_calls, 0); |
| } |
| |
| void OnBrowserServiceConnected(CrosapiId id, |
| mojo::RemoteSetElementId mojo_id, |
| mojom::BrowserService* browser_service, |
| uint32_t browser_service_version) override { |
| --remaining_num_calls_; |
| if (remaining_num_calls_ == 0) |
| std::move(callback_).Run(); |
| } |
| |
| private: |
| size_t remaining_num_calls_; |
| base::OnceClosure callback_; |
| }; |
| |
| std::vector<base::ScopedFD> ConnectTestingMojoSocket( |
| const std::string& socket_path) { |
| auto channel = mojo::NamedPlatformChannel::ConnectToServer(socket_path); |
| if (!channel.is_valid()) |
| return {}; |
| base::ScopedFD socket_fd = channel.TakePlatformHandle().TakeFD(); |
| |
| uint8_t buf[32]; |
| std::vector<base::ScopedFD> descriptors; |
| ssize_t size; |
| base::RunLoop run_loop; |
| base::ThreadPool::PostTaskAndReply( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(base::BindLambdaForTesting([&]() { |
| // Mark the channel as blocking. |
| int flags = fcntl(socket_fd.get(), F_GETFL); |
| PCHECK(flags != -1); |
| fcntl(socket_fd.get(), F_SETFL, flags & ~O_NONBLOCK); |
| size = mojo::SocketRecvmsg(socket_fd.get(), buf, sizeof(buf), |
| &descriptors, true /*block*/); |
| })), |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| EXPECT_EQ(1, size); |
| EXPECT_EQ(1u, buf[0]); |
| return descriptors; |
| } |
| |
| base::Process LaunchTestSubprocess(std::vector<base::ScopedFD> descriptors) { |
| base::LaunchOptions options; |
| options.fds_to_remap.emplace_back(descriptors[0].get(), descriptors[0].get()); |
| options.fds_to_remap.emplace_back(descriptors[1].get(), descriptors[1].get()); |
| base::CommandLine cmd(base::GetMultiProcessTestChildBaseCommandLine()); |
| cmd.AppendSwitchASCII(chromeos::switches::kCrosStartupDataFD, |
| base::NumberToString(descriptors[0].get())); |
| cmd.AppendSwitchASCII(kCrosapiMojoPlatformChannelHandle, |
| base::NumberToString(descriptors[1].get())); |
| return base::SpawnMultiProcessTestChild("CrosapiClientMain", cmd, options); |
| } |
| |
| } // namespace |
| |
| using TestMojoConnectionManagerTest = testing::Test; |
| |
| TEST_F(TestMojoConnectionManagerTest, ConnectMultipleClients) { |
| // Create temp dir before task environment, just in case lingering tasks need |
| // to access it. |
| base::ScopedTempDir temp_dir; |
| ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); |
| |
| // IdleServiceAsh uses objects that are unavailable for this test. Disable it |
| // to avoid problems. |
| crosapi::IdleServiceAsh::DisableForTesting(); |
| |
| // Use IO type to support the FileDescriptorWatcher API on POSIX. |
| // TestingProfileManager instantiated below requires a TaskRunner. |
| content::BrowserTaskEnvironment task_environment{ |
| base::test::TaskEnvironment::MainThreadType::IO}; |
| |
| chromeos::LoginState::Initialize(); |
| base::ScopedClosureRunner login_state_teardown( |
| base::BindOnce(&chromeos::LoginState::Shutdown)); |
| |
| // Constructing CrosapiManager requires ProfileManager. |
| // Also, constructing BrowserInitParams requires local state prefs. |
| TestingProfileManager testing_profile_manager( |
| TestingBrowserProcess::GetGlobal()); |
| ASSERT_TRUE(testing_profile_manager.SetUp()); |
| |
| // Set up UserManager to fake the login state. |
| user_manager::FakeUserManager user_manager; |
| user_manager.Initialize(); |
| base::ScopedClosureRunner user_manager_teardown( |
| base::BindOnce(base::BindLambdaForTesting( |
| [&user_manager]() { user_manager.Destroy(); }))); |
| const AccountId account = AccountId::FromUserEmail("test@test"); |
| const user_manager::User* user = user_manager.AddUser(account); |
| user_manager.UserLoggedIn(account, user->username_hash(), false, false); |
| TestingProfile* profile = |
| testing_profile_manager.CreateTestingProfile(account.GetUserEmail()); |
| profile->set_profile_name(account.GetUserEmail()); |
| ash::ProfileHelper::Get()->SetUserToProfileMappingForTesting(user, profile); |
| |
| auto crosapi_manager = std::make_unique<CrosapiManager>(); |
| |
| // Ash-chrome queues an invitation, drop a socket and wait for connection. |
| std::string socket_path = |
| temp_dir.GetPath().MaybeAsASCII() + "/lacros.socket"; |
| |
| // Sets up watcher to Wait for the socket to be created by |
| // |TestMojoConnectionManager| before attempting to connect. There is no |
| // garanteen that |OnTestingSocketCreated| has run after the run loop is done, |
| // so this test should NOT depend on the assumption. |
| base::FilePathWatcher watcher; |
| base::RunLoop run_loop1; |
| watcher.Watch(base::FilePath(socket_path), |
| base::FilePathWatcher::Type::kNonRecursive, |
| base::BindRepeating(base::BindLambdaForTesting( |
| [&run_loop1](const base::FilePath& path, bool error) { |
| EXPECT_FALSE(error); |
| run_loop1.Quit(); |
| }))); |
| std::unique_ptr<EnvironmentProvider> environment_provider = |
| std::make_unique<EnvironmentProvider>(); |
| TestMojoConnectionManager test_mojo_connection_manager{ |
| base::FilePath(socket_path), environment_provider.get()}; |
| run_loop1.Run(); |
| |
| // Test connects with ash-chrome via the socket. |
| std::vector<base::ScopedFD> descriptors1 = |
| ConnectTestingMojoSocket(socket_path); |
| ASSERT_EQ(2u, descriptors1.size()); |
| std::vector<base::ScopedFD> descriptors2 = |
| ConnectTestingMojoSocket(socket_path); |
| ASSERT_EQ(2u, descriptors2.size()); |
| |
| base::RunLoop run_loop2; |
| // Two BrowserService connections should be made (one for each subprocess). |
| TestBrowserServiceHostObserver observer(2, run_loop2.QuitClosure()); |
| crosapi_manager->crosapi_ash()->browser_service_host_ash()->AddObserver( |
| &observer); |
| base::ScopedClosureRunner observer_reset(base::BindOnce( |
| base::BindLambdaForTesting([&observer, &crosapi_manager]() { |
| crosapi_manager->crosapi_ash() |
| ->browser_service_host_ash() |
| ->RemoveObserver(&observer); |
| }))); |
| |
| // Then launch two subprocesses running in parallel. |
| // These will connect BrowserService mojo. |
| base::Process sub1 = LaunchTestSubprocess(std::move(descriptors1)); |
| base::Process sub2 = LaunchTestSubprocess(std::move(descriptors2)); |
| |
| // Wait for two BrowserService instances. |
| run_loop2.Run(); |
| |
| // Connection succeeded. Terminate subprocesses. |
| ASSERT_TRUE(base::TerminateMultiProcessTestChild(sub1, 0, true)); |
| sub1.Close(); |
| ASSERT_TRUE(base::TerminateMultiProcessTestChild(sub2, 0, true)); |
| sub2.Close(); |
| } |
| |
| // Another process that emulates the behavior of lacros-chrome. |
| MULTIPROCESS_TEST_MAIN(CrosapiClientMain) { |
| base::test::SingleThreadTaskEnvironment task_environment; |
| |
| mojo::IncomingInvitation invitation = mojo::IncomingInvitation::Accept( |
| mojo::PlatformChannel::RecoverPassedEndpointFromString( |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| kCrosapiMojoPlatformChannelHandle))); |
| base::RunLoop run_loop; |
| mojo::Remote<crosapi::mojom::Crosapi> crosapi( |
| mojo::PendingRemote<crosapi::mojom::Crosapi>( |
| invitation.ExtractMessagePipe(0), 0u)); |
| mojo::Remote<crosapi::mojom::BrowserServiceHost> browser_service_host; |
| crosapi->BindBrowserServiceHost( |
| browser_service_host.BindNewPipeAndPassReceiver()); |
| TestBrowserService test_browser_service; |
| browser_service_host->AddBrowserService( |
| test_browser_service.BindNewPipeAndPassRemote()); |
| // Do not return from the run loop. |
| run_loop.Run(); |
| return 0; |
| } |
| |
| } // namespace crosapi |