// Copyright 2013 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/apps/app_shim/app_shim_listener.h"
#include <unistd.h>
#include <memory>
#include <optional>
#include "base/apple/foundation_util.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/app_shim/app_shim_controller.h"
#include "chrome/browser/apps/app_shim/app_shim_host_bootstrap_mac.h"
#include "chrome/browser/apps/app_shim/test/app_shim_listener_test_api_mac.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/mac/app_mode_common.h"
#include "chrome/common/mac/app_shim.mojom.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/version_info/version_info.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "mojo/core/embedder/embedder.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "mojo/public/cpp/system/handle.h"
#include "mojo/public/cpp/system/isolated_connection.h"
#include "third_party/ipcz/include/ipcz/ipcz.h"
// A test version of the AppShimController mojo client in chrome_main_app_mode.
class TestShimClient : public chrome::mojom::AppShim {
public:
TestShimClient();
TestShimClient(const TestShimClient&) = delete;
TestShimClient& operator=(const TestShimClient&) = delete;
// Friend accessor.
mojo::PlatformChannelEndpoint ConnectToBrowser(
const mojo::NamedPlatformChannel::ServerName& server_name) {
return AppShimController::ConnectToBrowser(server_name);
}
mojo::PendingReceiver<chrome::mojom::AppShimHost> GetHostReceiver() {
return std::move(host_receiver_);
}
chrome::mojom::AppShimHostBootstrap::OnShimConnectedCallback
GetOnShimConnectedCallback() {
return base::BindOnce(&TestShimClient::OnShimConnectedDone,
base::Unretained(this));
}
mojo::Remote<chrome::mojom::AppShimHostBootstrap>& host_bootstrap() {
return host_bootstrap_;
}
// chrome::mojom::AppShim implementation (not used in testing, but can be).
void CreateRemoteCocoaApplication(
mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
receiver) override {}
void CreateCommandDispatcherForWidget(uint64_t widget_id) override {}
void SetUserAttention(
chrome::mojom::AppShimAttentionType attention_type) override {}
void SetBadgeLabel(const std::string& badge_label) override {}
void UpdateProfileMenu(std::vector<chrome::mojom::ProfileMenuItemPtr>
profile_menu_items) override {}
void UpdateApplicationDockMenu(
std::vector<chrome::mojom::ApplicationDockMenuItemPtr> dock_menu_items)
override {}
void BindNotificationProvider(
mojo::PendingReceiver<mac_notifications::mojom::MacNotificationProvider>
provider) override {}
void RequestNotificationPermission(
RequestNotificationPermissionCallback callback) override {}
void BindChildHistogramFetcherFactory(
mojo::PendingReceiver<metrics::mojom::ChildHistogramFetcherFactory>
receiver) override {}
private:
void OnShimConnectedDone(
chrome::mojom::AppShimLaunchResult result,
variations::VariationsCommandLine feature_state,
mojo::PendingReceiver<chrome::mojom::AppShim> app_shim_receiver) {
shim_receiver_.Bind(std::move(app_shim_receiver));
}
mojo::ScopedMessagePipeHandle ConnectIcpzToShim(
mojo::PlatformChannelEndpoint endpoint) {
// ipcz does not support nodes connecting to themselves, as these tests
// normally do. Instead for ipcz we set up a secondary broker node and use
// that to connect back to Mojo's global ipcz node in this process,
// effectively simulating an external shim process.
//
// Note that ipcz handles and Mojo handles are interchangeable types with
// ipcz enabled, so we can use scoped Mojo handles to manage ipcz object
// lifetime here.
const IpczAPI& ipcz = mojo::core::GetIpczAPIForMojo();
IpczHandle node;
IpczResult result =
ipcz.CreateNode(&mojo::core::GetIpczDriverForMojo(),
IPCZ_CREATE_NODE_AS_BROKER, nullptr, &node);
CHECK_EQ(IPCZ_RESULT_OK, result);
secondary_ipcz_broker_.reset(mojo::Handle{node});
// MojoIpcz reserves the first portal on each invitation connection for
// internal services. We discard it here since it's not needed.
IpczHandle portals[2];
result = ipcz.ConnectNode(
secondary_ipcz_broker_->value(),
mojo::core::CreateIpczTransportFromEndpoint(
std::move(endpoint),
{.local_is_broker = true, .remote_is_broker = true}),
/*num_initial_portals=*/2, IPCZ_CONNECT_NODE_TO_BROKER,
/*options=*/nullptr, portals);
CHECK_EQ(IPCZ_RESULT_OK, result);
ipcz.Close(portals[0], IPCZ_NO_FLAGS, nullptr);
return mojo::ScopedMessagePipeHandle(mojo::MessagePipeHandle(portals[1]));
}
mojo::IsolatedConnection mojo_connection_;
mojo::ScopedHandle secondary_ipcz_broker_;
mojo::Receiver<chrome::mojom::AppShim> shim_receiver_{this};
mojo::Remote<chrome::mojom::AppShimHost> host_;
mojo::PendingReceiver<chrome::mojom::AppShimHost> host_receiver_;
mojo::Remote<chrome::mojom::AppShimHostBootstrap> host_bootstrap_;
};
TestShimClient::TestShimClient() {
base::FilePath user_data_dir;
CHECK(base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir));
std::string name_fragment =
base::StrCat({base::apple::BaseBundleID(), ".",
app_mode::kAppShimBootstrapNameFragment, ".",
base::MD5String(user_data_dir.value())});
mojo::PlatformChannelEndpoint endpoint = ConnectToBrowser(name_fragment);
mojo::ScopedMessagePipeHandle message_pipe;
if (mojo::core::IsMojoIpczEnabled()) {
// With MojoIpcz, we need to set up a secondary node in order to simulate an
// external shim connection.
message_pipe = ConnectIcpzToShim(std::move(endpoint));
// It's important for the AppShimHost interface portals to be created on the
// secondary node too, since the fake shim passes the receiver endpoint back
// to the host in a reply over the primordial AppShim interface connecting
// the two nodes.
const IpczAPI& ipcz = mojo::core::GetIpczAPIForMojo();
IpczHandle remote, receiver;
const IpczResult result =
ipcz.OpenPortals(secondary_ipcz_broker_->value(), IPCZ_NO_FLAGS,
nullptr, &remote, &receiver);
CHECK_EQ(IPCZ_RESULT_OK, result);
host_.Bind(mojo::PendingRemote<chrome::mojom::AppShimHost>(
mojo::ScopedMessagePipeHandle(mojo::MessagePipeHandle(remote)), 0));
host_receiver_ = mojo::PendingReceiver<chrome::mojom::AppShimHost>(
mojo::ScopedMessagePipeHandle(mojo::MessagePipeHandle(receiver)));
} else {
// Non-ipcz Mojo supports processes establishing IsolatedConnections to
// themselves.
message_pipe = mojo_connection_.Connect(std::move(endpoint));
host_receiver_ = host_.BindNewPipeAndPassReceiver();
}
host_bootstrap_ = mojo::Remote<chrome::mojom::AppShimHostBootstrap>(
mojo::PendingRemote<chrome::mojom::AppShimHostBootstrap>(
std::move(message_pipe), 0));
}
// Browser Test for AppShimListener to test IPC interactions.
class AppShimListenerBrowserTest : public InProcessBrowserTest,
public AppShimHostBootstrap::Client,
public chrome::mojom::AppShimHost {
public:
AppShimListenerBrowserTest() = default;
AppShimListenerBrowserTest(const AppShimListenerBrowserTest&) = delete;
AppShimListenerBrowserTest& operator=(const AppShimListenerBrowserTest&) =
delete;
protected:
// Wait for OnShimProcessConnected, then send a quit, and wait for the
// response. Used to test launch behavior.
void RunAndExitGracefully();
// InProcessBrowserTest overrides:
void SetUpOnMainThread() override;
void TearDownOnMainThread() override;
// AppShimHostBootstrap::Client:
void OnShimProcessConnected(
std::unique_ptr<AppShimHostBootstrap> bootstrap) override;
std::unique_ptr<TestShimClient> test_client_;
std::vector<base::FilePath> last_launch_files_;
std::optional<chrome::mojom::AppShimLaunchType> last_launch_type_;
private:
// chrome::mojom::AppShimHost.
void FocusApp() override {}
void ReopenApp() override {}
void FilesOpened(const std::vector<base::FilePath>& files) override {}
void ProfileSelectedFromMenu(const base::FilePath& profile_path) override {}
void OpenAppSettings() override {}
void UrlsOpened(const std::vector<GURL>& urls) override {}
void OpenAppWithOverrideUrl(const GURL& override_url) override {}
void EnableAccessibilitySupport(
chrome::mojom::AppShimScreenReaderSupportMode mode) override {}
void ApplicationWillTerminate() override {}
void NotificationPermissionStatusChanged(
mac_notifications::mojom::PermissionStatus status) override {}
std::unique_ptr<base::RunLoop> runner_;
mojo::Receiver<chrome::mojom::AppShimHost> receiver_{this};
mojo::Remote<chrome::mojom::AppShim> app_shim_;
int launch_count_ = 0;
};
void AppShimListenerBrowserTest::RunAndExitGracefully() {
runner_ = std::make_unique<base::RunLoop>();
EXPECT_EQ(0, launch_count_);
runner_->Run(); // Will stop in OnShimProcessConnected().
EXPECT_EQ(1, launch_count_);
test_client_.reset();
}
void AppShimListenerBrowserTest::SetUpOnMainThread() {
// Can't do this in the constructor, it needs a BrowserProcess.
AppShimHostBootstrap::SetClient(this);
}
void AppShimListenerBrowserTest::TearDownOnMainThread() {
AppShimHostBootstrap::SetClient(nullptr);
}
void AppShimListenerBrowserTest::OnShimProcessConnected(
std::unique_ptr<AppShimHostBootstrap> bootstrap) {
++launch_count_;
receiver_.Bind(bootstrap->GetAppShimHostReceiver());
last_launch_type_ = bootstrap->GetLaunchType();
last_launch_files_ = bootstrap->GetLaunchFiles();
bootstrap->OnConnectedToHost(app_shim_.BindNewPipeAndPassReceiver());
runner_->Quit();
}
// Test regular launch, which would ask Chrome to launch the app.
IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, LaunchNormal) {
test_client_ = std::make_unique<TestShimClient>();
auto app_shim_info = chrome::mojom::AppShimInfo::New();
app_shim_info->profile_path = browser()->profile()->GetPath();
app_shim_info->app_id = "test_app";
app_shim_info->app_url = GURL("https://example.com");
app_shim_info->launch_type = chrome::mojom::AppShimLaunchType::kNormal;
app_shim_info->notification_action_handler =
mojo::PendingRemote<
mac_notifications::mojom::MacNotificationActionHandler>()
.InitWithNewPipeAndPassReceiver();
test_client_->host_bootstrap()->OnShimConnected(
test_client_->GetHostReceiver(), std::move(app_shim_info),
test_client_->GetOnShimConnectedCallback());
RunAndExitGracefully();
EXPECT_EQ(chrome::mojom::AppShimLaunchType::kNormal, last_launch_type_);
EXPECT_TRUE(last_launch_files_.empty());
}
// Test register-only launch, used when Chrome has already launched the app.
IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, LaunchRegisterOnly) {
test_client_ = std::make_unique<TestShimClient>();
auto app_shim_info = chrome::mojom::AppShimInfo::New();
app_shim_info->profile_path = browser()->profile()->GetPath();
app_shim_info->app_id = "test_app";
app_shim_info->app_url = GURL("https://example.com");
app_shim_info->launch_type = chrome::mojom::AppShimLaunchType::kRegisterOnly;
app_shim_info->notification_action_handler =
mojo::PendingRemote<
mac_notifications::mojom::MacNotificationActionHandler>()
.InitWithNewPipeAndPassReceiver();
test_client_->host_bootstrap()->OnShimConnected(
test_client_->GetHostReceiver(), std::move(app_shim_info),
test_client_->GetOnShimConnectedCallback());
RunAndExitGracefully();
EXPECT_EQ(chrome::mojom::AppShimLaunchType::kRegisterOnly,
*last_launch_type_);
EXPECT_TRUE(last_launch_files_.empty());
}
// Ensure bootstrap name registers.
IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, PRE_ReCreate) {
test::AppShimListenerTestApi test_api(
g_browser_process->platform_part()->app_shim_listener());
EXPECT_TRUE(test_api.mach_acceptor());
}
// Ensure the bootstrap name can be re-created after a prior browser process has
// quit.
IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, ReCreate) {
test::AppShimListenerTestApi test_api(
g_browser_process->platform_part()->app_shim_listener());
EXPECT_TRUE(test_api.mach_acceptor());
}
// Tests for the files created by AppShimListener.
class AppShimListenerBrowserTestSymlink : public AppShimListenerBrowserTest {
public:
AppShimListenerBrowserTestSymlink() = default;
AppShimListenerBrowserTestSymlink(const AppShimListenerBrowserTestSymlink&) =
delete;
AppShimListenerBrowserTestSymlink& operator=(
const AppShimListenerBrowserTestSymlink&) = delete;
protected:
base::FilePath version_path_;
private:
bool SetUpUserDataDirectory() override;
void TearDownInProcessBrowserTestFixture() override;
};
bool AppShimListenerBrowserTestSymlink::SetUpUserDataDirectory() {
// Create an existing symlink. It should be replaced by AppShimListener.
base::FilePath user_data_dir;
EXPECT_TRUE(base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir));
// Create an invalid RunningChromeVersion file.
version_path_ =
user_data_dir.Append(app_mode::kRunningChromeVersionSymlinkName);
EXPECT_TRUE(base::CreateSymbolicLink(base::FilePath("invalid_version"),
version_path_));
return AppShimListenerBrowserTest::SetUpUserDataDirectory();
}
void AppShimListenerBrowserTestSymlink::TearDownInProcessBrowserTestFixture() {
// Check that created files have been deleted.
EXPECT_FALSE(base::PathExists(version_path_));
}
IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTestSymlink,
RunningChromeVersionCorrectlyWritten) {
// Check that the RunningChromeVersion file is correctly written.
base::FilePath encoded_config;
EXPECT_TRUE(base::ReadSymbolicLink(version_path_, &encoded_config));
auto config =
app_mode::ChromeConnectionConfig::DecodeFromPath(encoded_config);
EXPECT_EQ(version_info::GetVersionNumber(), config.framework_version);
EXPECT_EQ(mojo::core::IsMojoIpczEnabled(), config.is_mojo_ipcz_enabled);
}