chromium/chrome/browser/apps/app_shim/app_shim_host_mac.cc

// 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));
}