| // 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_host_mac.h" |
| |
| #include <utility> |
| |
| #include "base/apple/foundation_util.h" |
| #include "base/check_is_test.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_shared_memory.h" |
| #include "base/metrics/persistent_histogram_allocator.h" |
| #include "chrome/browser/apps/app_shim/app_shim_host_bootstrap_mac.h" |
| #include "chrome/browser/web_applications/os_integration/mac/web_app_shortcut_mac.h" |
| #include "chrome/common/chrome_features.h" |
| #include "components/metrics/content/subprocess_metrics_provider.h" |
| #include "components/metrics/histogram_controller.h" |
| #include "components/metrics/public/mojom/histogram_fetcher.mojom.h" |
| #include "components/remote_cocoa/browser/application_host.h" |
| #include "components/remote_cocoa/common/application.mojom.h" |
| #include "content/public/browser/browser_accessibility_state.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/child_process_host.h" |
| #include "content/public/common/process_type.h" |
| #include "mojo/public/cpp/bindings/pending_associated_receiver.h" |
| |
| AppShimHost::AppShimHost(AppShimHost::Client* client, |
| const std::string& app_id, |
| const base::FilePath& profile_path, |
| bool uses_remote_views) |
| : client_(client), |
| app_shim_receiver_(app_shim_.BindNewPipeAndPassReceiver()), |
| app_id_(app_id), |
| profile_path_(profile_path), |
| uses_remote_views_(uses_remote_views), |
| child_process_host_id_( |
| content::ChildProcessHost::GenerateChildProcessUniqueId()), |
| launch_weak_factory_(this) { |
| // Create the interfaces used to host windows, so that browser windows may be |
| // created before the host process finishes launching. |
| if (uses_remote_views_ && |
| base::FeatureList::IsEnabled(features::kAppShimRemoteCocoa)) { |
| // Create the interface that will be used by views::NativeWidgetMac to |
| // create NSWindows hosted in the app shim process. |
| mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application> |
| views_application_receiver; |
| remote_cocoa_application_host_ = |
| std::make_unique<remote_cocoa::ApplicationHost>( |
| &views_application_receiver, |
| web_app::GetBundleIdentifierForShim(app_id, profile_path)); |
| app_shim_->CreateRemoteCocoaApplication( |
| std::move(views_application_receiver)); |
| } |
| |
| auto shared_memory = base::HistogramSharedMemory::Create( |
| child_process_host_id_, |
| {content::PROCESS_TYPE_UTILITY, "AppShimMetrics", 512 << 10}); |
| if (shared_memory) { |
| histogram_allocator_ = std::move(shared_memory->allocator); |
| } |
| metrics::HistogramController::GetInstance()->SetHistogramMemory( |
| this, |
| shared_memory ? std::move(shared_memory->region) |
| : base::UnsafeSharedMemoryRegion(), |
| metrics::HistogramController::ChildProcessMode::kGetHistogramData); |
| } |
| |
| AppShimHost::~AppShimHost() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| metrics::HistogramController::GetInstance()->NotifyChildDied(this); |
| // If this instance gets destructed while a test is still waiting for it to be |
| // connected, we should unblock the test. The shim would have never connected, |
| // but unblocking the test at least can cause the test to fail gracefully |
| // rather than timeout waiting for something that will never happen. |
| if (on_shim_connected_for_testing_) { |
| std::move(on_shim_connected_for_testing_).Run(); |
| } |
| } |
| |
| void AppShimHost::ChannelError(uint32_t custom_reason, |
| const std::string& description) { |
| LOG(ERROR) << "Channel error custom_reason:" << custom_reason |
| << " description: " << description; |
| |
| if (auto* provider = metrics::SubprocessMetricsProvider::GetInstance()) { |
| provider->DeregisterSubprocessAllocator(child_process_host_id_); |
| } else { |
| // SubprocessMetricsProvider can be null in tests. |
| CHECK_IS_TEST(); |
| } |
| |
| // OnShimProcessDisconnected will delete |this|. |
| client_->OnShimProcessDisconnected(this); |
| } |
| |
| void AppShimHost::LaunchShimInternal( |
| web_app::LaunchShimUpdateBehavior update_behavior, |
| web_app::ShimLaunchMode launch_mode) { |
| DCHECK(launch_shim_has_been_called_); |
| DCHECK(!bootstrap_); |
| launch_weak_factory_.InvalidateWeakPtrs(); |
| client_->OnShimLaunchRequested( |
| this, update_behavior, launch_mode, |
| base::BindOnce(&AppShimHost::OnShimProcessLaunched, |
| launch_weak_factory_.GetWeakPtr(), update_behavior, |
| launch_mode), |
| base::BindOnce(&AppShimHost::OnShimProcessTerminated, |
| launch_weak_factory_.GetWeakPtr(), update_behavior, |
| launch_mode)); |
| } |
| |
| void AppShimHost::OnShimProcessLaunched( |
| web_app::LaunchShimUpdateBehavior update_behavior, |
| web_app::ShimLaunchMode launch_mode, |
| base::Process shim_process) { |
| // If a bootstrap connected, then it should have invalidated all weak |
| // pointers, preventing this from being called. |
| DCHECK(!bootstrap_); |
| |
| // If the shim process was created, then await either an AppShimHostBootstrap |
| // connecting or the process exiting. |
| if (shim_process.IsValid()) { |
| return; |
| } |
| |
| // Shim launch failing is treated the same as the shim launching but |
| // terminating before connecting. |
| OnShimProcessTerminated(update_behavior, launch_mode); |
| } |
| |
| void AppShimHost::OnShimProcessTerminated( |
| web_app::LaunchShimUpdateBehavior update_behavior, |
| web_app::ShimLaunchMode launch_mode) { |
| DCHECK(!bootstrap_); |
| |
| if (auto* provider = metrics::SubprocessMetricsProvider::GetInstance()) { |
| provider->DeregisterSubprocessAllocator(child_process_host_id_); |
| } else { |
| // SubprocessMetricsProvider can be null in tests. |
| CHECK_IS_TEST(); |
| } |
| |
| // If this was a launch without recreating shims, then the launch may have |
| // failed because the shims were not present, or because they were out of |
| // date. Try again, recreating the shims this time. |
| if (!web_app::RecreateShimsRequested(update_behavior)) { |
| DLOG(ERROR) << "Failed to launch shim, attempting to recreate."; |
| LaunchShimInternal( |
| web_app::LaunchShimUpdateBehavior::kRecreateUnconditionally, |
| launch_mode); |
| return; |
| } |
| |
| // If we attempted to recreate the app shims and still failed to launch, then |
| // there is no hope to launch the app. Close its windows (since they will |
| // never be seen). |
| // TODO(crbug.com/40605763): Consider adding some UI to tell the |
| // user that the process launch failed. |
| DLOG(ERROR) << "Failed to launch recreated shim, giving up."; |
| |
| // OnShimProcessDisconnected will delete |this|. |
| client_->OnShimProcessDisconnected(this); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // AppShimHost, chrome::mojom::AppShimHost |
| |
| void AppShimHost::SetOnShimConnectedForTesting(base::OnceClosure closure) { |
| on_shim_connected_for_testing_ = std::move(closure); |
| } |
| |
| base::ProcessId AppShimHost::GetAppShimPid() const { |
| if (bootstrap_) { |
| return bootstrap_->GetAppShimPid(); |
| } |
| return base::kNullProcessId; |
| } |
| |
| bool AppShimHost::HasBootstrapConnected() const { |
| return bootstrap_ != nullptr; |
| } |
| |
| void AppShimHost::OnBootstrapConnected( |
| std::unique_ptr<AppShimHostBootstrap> bootstrap) { |
| // Prevent any callbacks from any pending launches (e.g, if an internal and |
| // external launch happen to race). |
| launch_weak_factory_.InvalidateWeakPtrs(); |
| |
| DCHECK(!bootstrap_); |
| bootstrap_ = std::move(bootstrap); |
| bootstrap_->OnConnectedToHost(std::move(app_shim_receiver_)); |
| |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| host_receiver_.Bind(bootstrap_->GetAppShimHostReceiver()); |
| host_receiver_.set_disconnect_with_reason_handler( |
| base::BindOnce(&AppShimHost::ChannelError, base::Unretained(this))); |
| |
| auto* provider = metrics::SubprocessMetricsProvider::GetInstance(); |
| if (!provider) { |
| CHECK_IS_TEST(); |
| } else if (histogram_allocator_) { |
| provider->RegisterSubprocessAllocator( |
| child_process_host_id_, |
| std::make_unique<base::PersistentHistogramAllocator>( |
| std::move(histogram_allocator_))); |
| } |
| |
| if (on_shim_connected_for_testing_) |
| std::move(on_shim_connected_for_testing_).Run(); |
| } |
| |
| void AppShimHost::LaunchShim(web_app::ShimLaunchMode launch_mode) { |
| if (launch_shim_has_been_called_) { |
| return; |
| } |
| launch_shim_has_been_called_ = true; |
| |
| if (bootstrap_) { |
| // If there is a connected app shim process, and this is not a background |
| // launch, focus the app windows. |
| if (launch_mode != web_app::ShimLaunchMode::kBackground) { |
| client_->OnShimFocus(this); |
| } |
| } else { |
| // Otherwise, attempt to launch whatever app shims we find. |
| LaunchShimInternal(web_app::LaunchShimUpdateBehavior::kDoNotRecreate, |
| launch_mode); |
| } |
| } |
| |
| void AppShimHost::FocusApp() { |
| client_->OnShimFocus(this); |
| } |
| |
| void AppShimHost::ReopenApp() { |
| client_->OnShimReopen(this); |
| } |
| |
| void AppShimHost::FilesOpened(const std::vector<base::FilePath>& files) { |
| client_->OnShimOpenedFiles(this, files); |
| } |
| |
| void AppShimHost::ProfileSelectedFromMenu(const base::FilePath& profile_path) { |
| client_->OnShimSelectedProfile(this, profile_path); |
| } |
| |
| void AppShimHost::OpenAppSettings() { |
| client_->OnShimOpenedAppSettings(this); |
| } |
| |
| void AppShimHost::UrlsOpened(const std::vector<GURL>& urls) { |
| client_->OnShimOpenedUrls(this, urls); |
| } |
| |
| void AppShimHost::OpenAppWithOverrideUrl(const GURL& override_url) { |
| client_->OnShimOpenAppWithOverrideUrl(this, override_url); |
| } |
| |
| void AppShimHost::EnableAccessibilitySupport( |
| chrome::mojom::AppShimScreenReaderSupportMode mode) { |
| content::BrowserAccessibilityState* accessibility_state = |
| content::BrowserAccessibilityState::GetInstance(); |
| switch (mode) { |
| case chrome::mojom::AppShimScreenReaderSupportMode::kComplete: { |
| accessibility_state->OnScreenReaderDetected(); |
| break; |
| } |
| case chrome::mojom::AppShimScreenReaderSupportMode::kPartial: { |
| if (!accessibility_state->GetAccessibilityMode().has_mode( |
| ui::kAXModeBasic.flags())) { |
| accessibility_state->AddAccessibilityModeFlags(ui::kAXModeBasic); |
| } |
| break; |
| } |
| } |
| } |
| |
| void AppShimHost::ApplicationWillTerminate() { |
| client_->OnShimWillTerminate(this); |
| } |
| |
| void AppShimHost::NotificationPermissionStatusChanged( |
| mac_notifications::mojom::PermissionStatus status) { |
| client_->OnNotificationPermissionStatusChanged(this, status); |
| } |
| |
| base::FilePath AppShimHost::GetProfilePath() const { |
| // This should only be used by single-profile-app paths. |
| DCHECK(!profile_path_.empty()); |
| return profile_path_; |
| } |
| |
| std::string AppShimHost::GetAppId() const { |
| return app_id_; |
| } |
| |
| remote_cocoa::ApplicationHost* AppShimHost::GetRemoteCocoaApplicationHost() |
| const { |
| return remote_cocoa_application_host_.get(); |
| } |
| |
| chrome::mojom::AppShim* AppShimHost::GetAppShim() const { |
| return app_shim_.get(); |
| } |
| |
| void AppShimHost::BindChildHistogramFetcherFactory( |
| mojo::PendingReceiver<metrics::mojom::ChildHistogramFetcherFactory> |
| factory) { |
| app_shim_->BindChildHistogramFetcherFactory(std::move(factory)); |
| } |