chromium/chrome/browser/chromeos/app_mode/kiosk_browser_session.cc

// Copyright 2015 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/chromeos/app_mode/kiosk_browser_session.h"

#include <errno.h>
#include <signal.h>

#include <optional>

#include "base/functional/bind.h"
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/app_mode/kiosk_browser_window_handler.h"
#include "chrome/browser/chromeos/app_mode/kiosk_metrics_service.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/pref_names.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_child_process_host_iterator.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/process_type.h"
#include "content/public/common/webplugininfo.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "ppapi/buildflags/buildflags.h"

#if BUILDFLAG(ENABLE_PLUGINS)
#include "chrome/browser/chromeos/app_mode/kiosk_session_plugin_handler.h"
#include "chrome/browser/chromeos/app_mode/kiosk_session_plugin_handler_delegate.h"
#include "content/public/browser/plugin_service.h"
#endif

using extensions::AppWindow;
using extensions::AppWindowRegistry;

namespace chromeos {

namespace {

#if BUILDFLAG(ENABLE_PLUGINS)
bool IsPepperPlugin(const base::FilePath& plugin_path) {
  content::WebPluginInfo plugin_info;
  return content::PluginService::GetInstance()->GetPluginInfoByPath(
             plugin_path, &plugin_info) &&
         plugin_info.is_pepper_plugin();
}
#endif

void RebootDevice() {
  chromeos::PowerManagerClient::Get()->RequestRestart(
      power_manager::REQUEST_RESTART_OTHER, "kiosk app session");
}

// Sends a SIGFPE signal to plugin subprocesses that matches `child_ids`
// to trigger a dump.
void DumpPluginProcess(const std::set<int>& child_ids) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  bool dump_requested = false;

  content::BrowserChildProcessHostIterator iter(
      content::PROCESS_TYPE_PPAPI_PLUGIN);
  while (!iter.Done()) {
    const content::ChildProcessData& data = iter.GetData();
    if (child_ids.count(data.id) == 1) {
      // Send a signal to dump the plugin process.
      if (kill(data.GetProcess().Handle(), SIGFPE) == 0) {
        dump_requested = true;
      } else {
        PLOG(WARNING) << "Failed to send SIGFPE to plugin process"
                      << ", pid=" << data.GetProcess().Pid()
                      << ", type=" << data.process_type
                      << ", name=" << data.name;
      }
    }
    ++iter;
  }

  // Wait a bit to let dump finish (if requested) before rebooting the device.
  const int kDumpWaitSeconds = 10;
  content::GetUIThreadTaskRunner({})->PostDelayedTask(
      FROM_HERE, base::BindOnce(&RebootDevice),
      base::Seconds(dump_requested ? kDumpWaitSeconds : 0));
}

}  // namespace

class KioskBrowserSession::AppWindowHandler
    : public AppWindowRegistry::Observer {
 public:
  explicit AppWindowHandler(KioskBrowserSession* session)
      : kiosk_browser_session_(session) {}
  AppWindowHandler(const AppWindowHandler&) = delete;
  AppWindowHandler& operator=(const AppWindowHandler&) = delete;
  ~AppWindowHandler() override {}

  void Init(Profile* profile, const std::string& app_id) {
    DCHECK(!window_registry_);
    window_registry_ = AppWindowRegistry::Get(profile);
    if (window_registry_) {
      window_registry_->AddObserver(this);
    }
    app_id_ = app_id;
  }

 private:
  // extensions::AppWindowRegistry::Observer overrides:
  void OnAppWindowAdded(AppWindow* app_window) override {
    if (app_window->extension_id() != app_id_) {
      return;
    }

    kiosk_browser_session_->OnAppWindowAdded(app_window);
    app_window_created_ = true;
  }

  void OnAppWindowRemoved(AppWindow* app_window) override {
    if (!app_window_created_ ||
        !window_registry_->GetAppWindowsForApp(app_id_).empty()) {
      return;
    }

    LOG(WARNING) << "Last app window removed, ending kiosk session.";
    kiosk_browser_session_->Shutdown();
    window_registry_->RemoveObserver(this);
  }

  const raw_ptr<KioskBrowserSession> kiosk_browser_session_;
  raw_ptr<AppWindowRegistry> window_registry_ = nullptr;
  std::string app_id_;
  bool app_window_created_ = false;
};

#if BUILDFLAG(ENABLE_PLUGINS)
class KioskBrowserSession::PluginHandlerDelegateImpl
    : public KioskSessionPluginHandlerDelegate {
 public:
  explicit PluginHandlerDelegateImpl(KioskBrowserSession* owner)
      : owner_(owner) {}
  PluginHandlerDelegateImpl(const PluginHandlerDelegateImpl&) = delete;
  PluginHandlerDelegateImpl& operator=(const PluginHandlerDelegateImpl&) =
      delete;
  ~PluginHandlerDelegateImpl() override = default;

  // KioskSessionPluginHandlerDelegate:
  bool ShouldHandlePlugin(const base::FilePath& plugin_path) const override {
    // Note that BrowserChildProcessHostIterator in DumpPluginProcess also needs
    // to be updated when adding more plugin types here.
    return IsPepperPlugin(plugin_path);
  }
  void OnPluginCrashed(const base::FilePath& plugin_path) override {
    if (owner_->is_shutting_down()) {
      return;
    }
    owner_->metrics_service_->RecordKioskSessionPluginCrashed();
    owner_->is_shutting_down_ = true;

    LOG(ERROR) << "Reboot due to plugin crash, path=" << plugin_path.value();
    RebootDevice();
  }

  void OnPluginHung(const std::set<int>& hung_plugins) override {
    if (owner_->is_shutting_down()) {
      return;
    }
    owner_->metrics_service_->RecordKioskSessionPluginHung();
    owner_->is_shutting_down_ = true;

    LOG(ERROR) << "Plugin hung detected. Dump and reboot.";
    DumpPluginProcess(hung_plugins);
  }

 private:
  const raw_ptr<KioskBrowserSession> owner_;
};
#endif

KioskBrowserSession::KioskBrowserSession(Profile* profile)
    : KioskBrowserSession(profile,
                          base::BindOnce(chrome::AttemptUserExit),
                          g_browser_process->local_state()) {}

KioskBrowserSession::KioskBrowserSession(Profile* profile,
                                         base::OnceClosure attempt_user_exit,
                                         PrefService* local_state)
    : KioskBrowserSession(profile,
                          std::move(attempt_user_exit),
                          std::make_unique<KioskMetricsService>(local_state)) {}

KioskBrowserSession::~KioskBrowserSession() {
  if (!is_shutting_down()) {
    metrics_service_->RecordKioskSessionStopped();
  }
}

// static
std::unique_ptr<KioskBrowserSession> KioskBrowserSession::CreateForTesting(
    Profile* profile,
    base::OnceClosure attempt_user_exit,
    PrefService* local_state,
    const std::vector<std::string>& crash_dirs) {
  return base::WrapUnique(new KioskBrowserSession(
      profile, std::move(attempt_user_exit),
      KioskMetricsService::CreateForTesting(local_state, crash_dirs)));
}

void KioskBrowserSession::RegisterLocalStatePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(prefs::kKioskMetrics);
}

void KioskBrowserSession::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterBooleanPref(prefs::kNewWindowsInKioskAllowed, false);
  registry->RegisterBooleanPref(prefs::kKioskTroubleshootingToolsEnabled,
                                false);
  registry->RegisterListPref(prefs::kKioskBrowserPermissionsAllowedForOrigins,
                             PrefRegistrySimple::NO_REGISTRATION_FLAGS);
  registry->RegisterBooleanPref(prefs::kKioskWebAppOfflineEnabled, true);
}

void KioskBrowserSession::InitForChromeAppKiosk(const std::string& app_id) {
  app_window_handler_ = std::make_unique<AppWindowHandler>(this);
  app_window_handler_->Init(profile(), app_id);
  CreateBrowserWindowHandler(std::nullopt);
#if BUILDFLAG(ENABLE_PLUGINS)
  plugin_handler_ = std::make_unique<KioskSessionPluginHandler>(
      plugin_handler_delegate_.get());
#endif
  metrics_service_->RecordKioskSessionStarted();
}

void KioskBrowserSession::InitForWebKiosk(
    const std::optional<std::string>& web_app_name) {
  CreateBrowserWindowHandler(web_app_name);
  metrics_service_->RecordKioskSessionWebStarted();
}

void KioskBrowserSession::SetOnHandleBrowserCallbackForTesting(
    base::RepeatingCallback<void(bool is_closing)> callback) {
  on_handle_browser_callback_ = std::move(callback);
}

KioskSessionPluginHandlerDelegate*
KioskBrowserSession::GetPluginHandlerDelegateForTesting() {
  return plugin_handler_delegate_.get();
}

KioskBrowserSession::KioskBrowserSession(
    Profile* profile,
    base::OnceClosure attempt_user_exit,
    std::unique_ptr<KioskMetricsService> metrics_service)
    : profile_(profile),
#if BUILDFLAG(ENABLE_PLUGINS)
      plugin_handler_delegate_(
          std::make_unique<PluginHandlerDelegateImpl>(this)),
#endif
      attempt_user_exit_(std::move(attempt_user_exit)),
      metrics_service_(std::move(metrics_service)) {
}

void KioskBrowserSession::CreateBrowserWindowHandler(
    const std::optional<std::string>& web_app_name) {
  browser_window_handler_ = std::make_unique<KioskBrowserWindowHandler>(
      profile(), web_app_name,
      base::BindRepeating(&KioskBrowserSession::OnHandledNewBrowserWindow,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&KioskBrowserSession::Shutdown,
                     weak_ptr_factory_.GetWeakPtr()));
}

void KioskBrowserSession::OnHandledNewBrowserWindow(bool is_closing) {
  if (on_handle_browser_callback_) {
    on_handle_browser_callback_.Run(is_closing);
  }
}

void KioskBrowserSession::OnAppWindowAdded(AppWindow* app_window) {
  if (is_shutting_down()) {
    return;
  }

#if BUILDFLAG(ENABLE_PLUGINS)
  plugin_handler_->Observe(app_window->web_contents());
#endif
}

void KioskBrowserSession::OnGuestAdded(
    content::WebContents* guest_web_contents) {
  CHECK(extensions::WebViewGuest::FromWebContents(guest_web_contents));

  // Bail if the session is shutting down.
  if (is_shutting_down()) {
    return;
  }

#if BUILDFLAG(ENABLE_PLUGINS)
  // Plugin handler is initialized only for Chrome app Kiosks.
  if (plugin_handler_) {
    plugin_handler_->Observe(guest_web_contents);
  }
#endif
}

void KioskBrowserSession::Shutdown() {
  if (is_shutting_down()) {
    return;
  }
  is_shutting_down_ = true;
  metrics_service_->RecordKioskSessionStopped();

  std::move(attempt_user_exit_).Run();
}

Browser* KioskBrowserSession::GetSettingsBrowserForTesting() {
  if (browser_window_handler_) {
    return browser_window_handler_->GetSettingsBrowserForTesting();  // IN-TEST
  }
  return nullptr;
}

}  // namespace chromeos