blob: 544a7ade02b959ab36fafd3fc6e36ea2a8ef2a72 [file] [log] [blame]
// Copyright (c) 2012 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/extensions/api/terminal/terminal_private_api.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "ash/public/cpp/ash_pref_names.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "base/values.h"
#include "chrome/browser/chromeos/crostini/crostini_features.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_pref_names.h"
#include "chrome/browser/chromeos/crostini/crostini_terminal.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/extensions/api/terminal/crostini_startup_status.h"
#include "chrome/browser/extensions/api/terminal/terminal_extension_helper.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/api/terminal_private.h"
#include "chromeos/process_proxy/process_proxy_registry.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/api/storage/settings_namespace.h"
#include "extensions/browser/api/storage/storage_frontend.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/common/extension.h"
namespace terminal_private = extensions::api::terminal_private;
namespace OnTerminalResize =
extensions::api::terminal_private::OnTerminalResize;
namespace OpenTerminalProcess =
extensions::api::terminal_private::OpenTerminalProcess;
namespace OpenVmshellProcess =
extensions::api::terminal_private::OpenVmshellProcess;
namespace CloseTerminalProcess =
extensions::api::terminal_private::CloseTerminalProcess;
namespace SendInput = extensions::api::terminal_private::SendInput;
namespace AckOutput = extensions::api::terminal_private::AckOutput;
namespace SetSettings = extensions::api::terminal_private::SetSettings;
using crostini::mojom::InstallerState;
namespace {
const char kCroshName[] = "crosh";
const char kCroshCommand[] = "/usr/bin/crosh";
// We make stubbed crosh just echo back input.
const char kStubbedCroshCommand[] = "cat";
const char kVmShellName[] = "vmshell";
const char kVmShellCommand[] = "/usr/bin/vsh";
const char kSwitchOwnerId[] = "owner_id";
const char kSwitchVmName[] = "vm_name";
const char kSwitchTargetContainer[] = "target_container";
const char kSwitchStartupId[] = "startup_id";
const char kSwitchCurrentWorkingDir[] = "cwd";
// Copies the value of |switch_name| if present from |src| to |dst|. If not
// present, uses |default_value|. Returns the value set into |dst|.
std::string GetSwitch(const base::CommandLine* src,
base::CommandLine* dst,
const std::string& switch_name,
const std::string& default_value) {
std::string result = src->HasSwitch(switch_name)
? src->GetSwitchValueASCII(switch_name)
: default_value;
dst->AppendSwitchASCII(switch_name, result);
return result;
}
void NotifyProcessOutput(content::BrowserContext* browser_context,
int tab_id,
const std::string& terminal_id,
const std::string& output_type,
const std::string& output) {
if (!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&NotifyProcessOutput, browser_context, tab_id,
terminal_id, output_type, output));
return;
}
std::unique_ptr<base::ListValue> args(new base::ListValue());
args->AppendInteger(tab_id);
args->AppendString(terminal_id);
args->AppendString(output_type);
args->AppendString(output);
extensions::EventRouter* event_router =
extensions::EventRouter::Get(browser_context);
if (event_router) {
std::unique_ptr<extensions::Event> event(new extensions::Event(
extensions::events::TERMINAL_PRIVATE_ON_PROCESS_OUTPUT,
terminal_private::OnProcessOutput::kEventName, std::move(args)));
event_router->BroadcastEvent(std::move(event));
}
}
// Returns tab ID, or window session ID (for platform apps) for |web_contents|.
int GetTabOrWindowSessionId(content::BrowserContext* browser_context,
content::WebContents* web_contents) {
int tab_id = extensions::ExtensionTabUtil::GetTabId(web_contents);
if (tab_id >= 0)
return tab_id;
extensions::AppWindow* window =
extensions::AppWindowRegistry::Get(browser_context)
->GetAppWindowForWebContents(web_contents);
return window ? window->session_id().id() : -1;
}
void PreferenceChanged(Profile* profile,
const std::string& pref_name,
extensions::events::HistogramValue histogram,
const char* eventName) {
auto args = std::make_unique<base::ListValue>();
args->Append(profile->GetPrefs()->Get(pref_name)->CreateDeepCopy());
extensions::EventRouter* event_router = extensions::EventRouter::Get(profile);
if (event_router) {
auto event = std::make_unique<extensions::Event>(histogram, eventName,
std::move(args));
event_router->BroadcastEvent(std::move(event));
}
}
} // namespace
namespace extensions {
TerminalPrivateAPI::TerminalPrivateAPI(content::BrowserContext* context)
: context_(context),
pref_change_registrar_(std::make_unique<PrefChangeRegistrar>()) {
Profile* profile = Profile::FromBrowserContext(context);
pref_change_registrar_->Init(profile->GetPrefs());
pref_change_registrar_->Add(
crostini::prefs::kCrostiniTerminalSettings,
base::BindRepeating(
&PreferenceChanged, profile,
crostini::prefs::kCrostiniTerminalSettings,
extensions::events::TERMINAL_PRIVATE_ON_SETTINGS_CHANGED,
terminal_private::OnSettingsChanged::kEventName));
pref_change_registrar_->Add(
ash::prefs::kAccessibilitySpokenFeedbackEnabled,
base::BindRepeating(
&PreferenceChanged, profile,
ash::prefs::kAccessibilitySpokenFeedbackEnabled,
extensions::events::TERMINAL_PRIVATE_ON_A11Y_STATUS_CHANGED,
terminal_private::OnA11yStatusChanged::kEventName));
}
TerminalPrivateAPI::~TerminalPrivateAPI() = default;
static base::LazyInstance<BrowserContextKeyedAPIFactory<TerminalPrivateAPI>>::
DestructorAtExit g_factory = LAZY_INSTANCE_INITIALIZER;
// static
BrowserContextKeyedAPIFactory<TerminalPrivateAPI>*
TerminalPrivateAPI::GetFactoryInstance() {
return g_factory.Pointer();
}
TerminalPrivateOpenTerminalProcessFunction::
~TerminalPrivateOpenTerminalProcessFunction() = default;
ExtensionFunction::ResponseAction
TerminalPrivateOpenTerminalProcessFunction::Run() {
std::unique_ptr<OpenTerminalProcess::Params> params(
OpenTerminalProcess::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
return OpenProcess(params->process_name, std::move(params->args));
}
ExtensionFunction::ResponseAction
TerminalPrivateOpenTerminalProcessFunction::OpenProcess(
const std::string& process_name,
std::unique_ptr<std::vector<std::string>> args) {
const std::string& user_id_hash =
extensions::ExtensionsBrowserClient::Get()->GetUserIdHashFromContext(
browser_context());
content::WebContents* caller_contents = GetSenderWebContents();
if (!caller_contents)
return RespondNow(Error("No web contents."));
// Passed to terminalPrivate.ackOutput, which is called from the API's custom
// bindings after terminalPrivate.onProcessOutput is dispatched. It is used to
// determine whether ackOutput call should be handled or not. ackOutput will
// be called from every web contents in which a onProcessOutput listener
// exists (because the API custom bindings hooks are run in every web contents
// with a listener). Only ackOutput called from the web contents that has the
// target terminal instance should be handled.
// TODO(tbarzic): Instead of passing tab/app window session id around, keep
// mapping from web_contents to terminal ID running in it. This will be
// needed to fix crbug.com/210295.
int tab_id = GetTabOrWindowSessionId(browser_context(), caller_contents);
if (tab_id < 0)
return RespondNow(Error("Not called from a tab or app window"));
// Passing --crosh-command overrides any JS process name.
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kCroshCommand)) {
OpenProcess(user_id_hash, tab_id,
{command_line->GetSwitchValueASCII(switches::kCroshCommand)});
} else if (process_name == kCroshName) {
// command=crosh: use '/usr/bin/crosh' on a device, 'cat' otherwise.
if (base::SysInfo::IsRunningOnChromeOS()) {
OpenProcess(user_id_hash, tab_id, {kCroshCommand});
} else {
OpenProcess(user_id_hash, tab_id, {kStubbedCroshCommand});
}
} else if (process_name == kVmShellName) {
// Ensure crostini is allowed before starting terminal.
Profile* profile = Profile::FromBrowserContext(browser_context());
if (!crostini::CrostiniFeatures::Get()->IsAllowed(profile))
return RespondNow(Error("vmshell not allowed"));
// command=vmshell: ensure --owner_id, --vm_name, --target_container, --cwd
// are set, and the specified vm/container is running.
base::CommandLine vmshell_cmd({kVmShellCommand});
if (!args)
args = std::make_unique<std::vector<std::string>>();
args->insert(args->begin(), kVmShellCommand);
base::CommandLine params_args(*args);
std::string owner_id =
GetSwitch(&params_args, &vmshell_cmd, kSwitchOwnerId, user_id_hash);
std::string vm_name = GetSwitch(&params_args, &vmshell_cmd, kSwitchVmName,
crostini::kCrostiniDefaultVmName);
std::string container_name =
GetSwitch(&params_args, &vmshell_cmd, kSwitchTargetContainer,
crostini::kCrostiniDefaultContainerName);
GetSwitch(&params_args, &vmshell_cmd, kSwitchCurrentWorkingDir, "");
std::string startup_id = params_args.GetSwitchValueASCII(kSwitchStartupId);
crostini::ContainerId container_id(vm_name, container_name);
auto* mgr = crostini::CrostiniManager::GetForProfile(profile);
bool verbose = !mgr->GetContainerInfo(container_id).has_value();
auto observer = std::make_unique<CrostiniStartupStatus>(
base::BindRepeating(&NotifyProcessOutput, browser_context(), tab_id,
startup_id,
api::terminal_private::ToString(
api::terminal_private::OUTPUT_TYPE_STDOUT)),
verbose);
// Save copy of pointer for RestartObserver before moving object.
CrostiniStartupStatus* observer_ptr = observer.get();
observer->ShowProgressAtInterval();
mgr->RestartCrostini(
container_id,
base::BindOnce(
&TerminalPrivateOpenTerminalProcessFunction::OnCrostiniRestarted,
this, std::move(observer), user_id_hash, tab_id,
vmshell_cmd.argv()),
observer_ptr);
} else {
// command=[unrecognized].
return RespondNow(Error("Invalid process name: " + process_name));
}
return RespondLater();
}
void TerminalPrivateOpenTerminalProcessFunction::OnCrostiniRestarted(
std::unique_ptr<CrostiniStartupStatus> startup_status,
const std::string& user_id_hash,
int tab_id,
const std::vector<std::string>& arguments,
crostini::CrostiniResult result) {
if (crostini::MaybeShowCrostiniDialogBeforeLaunch(
Profile::FromBrowserContext(browser_context()), result)) {
const std::string msg = "Waiting for component update dialog response";
LOG(ERROR) << msg;
Respond(Error(msg));
return;
}
startup_status->OnCrostiniRestarted(result);
if (result == crostini::CrostiniResult::SUCCESS) {
OpenProcess(user_id_hash, tab_id, arguments);
} else {
const std::string msg =
base::StringPrintf("Error starting crostini for terminal: %d", result);
LOG(ERROR) << msg;
Respond(Error(msg));
}
}
void TerminalPrivateOpenTerminalProcessFunction::OpenProcess(
const std::string& user_id_hash,
int tab_id,
const std::vector<std::string>& arguments) {
DCHECK(!arguments.empty());
// Registry lives on its own task runner.
chromeos::ProcessProxyRegistry::GetTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
&TerminalPrivateOpenTerminalProcessFunction::OpenOnRegistryTaskRunner,
this, base::Bind(&NotifyProcessOutput, browser_context(), tab_id),
base::Bind(
&TerminalPrivateOpenTerminalProcessFunction::RespondOnUIThread,
this),
arguments, user_id_hash));
}
void TerminalPrivateOpenTerminalProcessFunction::OpenOnRegistryTaskRunner(
const ProcessOutputCallback& output_callback,
const OpenProcessCallback& callback,
const std::vector<std::string>& arguments,
const std::string& user_id_hash) {
chromeos::ProcessProxyRegistry* registry =
chromeos::ProcessProxyRegistry::Get();
const base::CommandLine cmdline{arguments};
std::string terminal_id;
bool success = registry->OpenProcess(cmdline, user_id_hash, output_callback,
&terminal_id);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(callback, success, terminal_id));
}
TerminalPrivateOpenVmshellProcessFunction::
~TerminalPrivateOpenVmshellProcessFunction() = default;
ExtensionFunction::ResponseAction
TerminalPrivateOpenVmshellProcessFunction::Run() {
std::unique_ptr<OpenVmshellProcess::Params> params(
OpenVmshellProcess::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
// Only opens 'vmshell'.
return OpenProcess(kVmShellName, std::move(params->args));
}
TerminalPrivateSendInputFunction::~TerminalPrivateSendInputFunction() = default;
void TerminalPrivateOpenTerminalProcessFunction::RespondOnUIThread(
bool success,
const std::string& terminal_id) {
if (!success) {
Respond(Error("Failed to open process."));
return;
}
Respond(OneArgument(std::make_unique<base::Value>(terminal_id)));
}
ExtensionFunction::ResponseAction TerminalPrivateSendInputFunction::Run() {
std::unique_ptr<SendInput::Params> params(SendInput::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
// Registry lives on its own task runner.
chromeos::ProcessProxyRegistry::GetTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
&TerminalPrivateSendInputFunction::SendInputOnRegistryTaskRunner,
this, params->id, params->input));
return RespondLater();
}
void TerminalPrivateSendInputFunction::SendInputOnRegistryTaskRunner(
const std::string& terminal_id,
const std::string& text) {
bool success =
chromeos::ProcessProxyRegistry::Get()->SendInput(terminal_id, text);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&TerminalPrivateSendInputFunction::RespondOnUIThread, this,
success));
}
void TerminalPrivateSendInputFunction::RespondOnUIThread(bool success) {
Respond(OneArgument(std::make_unique<base::Value>(success)));
}
TerminalPrivateCloseTerminalProcessFunction::
~TerminalPrivateCloseTerminalProcessFunction() = default;
ExtensionFunction::ResponseAction
TerminalPrivateCloseTerminalProcessFunction::Run() {
std::unique_ptr<CloseTerminalProcess::Params> params(
CloseTerminalProcess::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
// Registry lives on its own task runner.
chromeos::ProcessProxyRegistry::GetTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(&TerminalPrivateCloseTerminalProcessFunction::
CloseOnRegistryTaskRunner,
this, params->id));
return RespondLater();
}
void TerminalPrivateCloseTerminalProcessFunction::CloseOnRegistryTaskRunner(
const std::string& terminal_id) {
bool success =
chromeos::ProcessProxyRegistry::Get()->CloseProcess(terminal_id);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&TerminalPrivateCloseTerminalProcessFunction::RespondOnUIThread, this,
success));
}
void TerminalPrivateCloseTerminalProcessFunction::RespondOnUIThread(
bool success) {
Respond(OneArgument(std::make_unique<base::Value>(success)));
}
TerminalPrivateOnTerminalResizeFunction::
~TerminalPrivateOnTerminalResizeFunction() = default;
ExtensionFunction::ResponseAction
TerminalPrivateOnTerminalResizeFunction::Run() {
std::unique_ptr<OnTerminalResize::Params> params(
OnTerminalResize::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
// Registry lives on its own task runner.
chromeos::ProcessProxyRegistry::GetTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(&TerminalPrivateOnTerminalResizeFunction::
OnResizeOnRegistryTaskRunner,
this, params->id, params->width, params->height));
return RespondLater();
}
void TerminalPrivateOnTerminalResizeFunction::OnResizeOnRegistryTaskRunner(
const std::string& terminal_id,
int width,
int height) {
bool success = chromeos::ProcessProxyRegistry::Get()->OnTerminalResize(
terminal_id, width, height);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&TerminalPrivateOnTerminalResizeFunction::RespondOnUIThread, this,
success));
}
void TerminalPrivateOnTerminalResizeFunction::RespondOnUIThread(bool success) {
Respond(OneArgument(std::make_unique<base::Value>(success)));
}
TerminalPrivateAckOutputFunction::~TerminalPrivateAckOutputFunction() = default;
ExtensionFunction::ResponseAction TerminalPrivateAckOutputFunction::Run() {
std::unique_ptr<AckOutput::Params> params(AckOutput::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
content::WebContents* caller_contents = GetSenderWebContents();
if (!caller_contents)
return RespondNow(Error("No web contents."));
int tab_id = GetTabOrWindowSessionId(browser_context(), caller_contents);
if (tab_id < 0)
return RespondNow(Error("Not called from a tab or app window"));
if (tab_id != params->tab_id)
return RespondNow(NoArguments());
// Registry lives on its own task runner.
chromeos::ProcessProxyRegistry::GetTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
&TerminalPrivateAckOutputFunction::AckOutputOnRegistryTaskRunner,
this, params->id));
return RespondNow(NoArguments());
}
void TerminalPrivateAckOutputFunction::AckOutputOnRegistryTaskRunner(
const std::string& terminal_id) {
chromeos::ProcessProxyRegistry::Get()->AckOutput(terminal_id);
}
TerminalPrivateOpenWindowFunction::~TerminalPrivateOpenWindowFunction() =
default;
ExtensionFunction::ResponseAction TerminalPrivateOpenWindowFunction::Run() {
crostini::LaunchTerminal(Profile::FromBrowserContext(browser_context()));
return RespondNow(NoArguments());
}
TerminalPrivateOpenOptionsPageFunction::
~TerminalPrivateOpenOptionsPageFunction() = default;
ExtensionFunction::ResponseAction
TerminalPrivateOpenOptionsPageFunction::Run() {
crostini::LaunchTerminalSettings(
Profile::FromBrowserContext(browser_context()));
return RespondNow(NoArguments());
}
TerminalPrivateGetCroshSettingsFunction::
~TerminalPrivateGetCroshSettingsFunction() = default;
ExtensionFunction::ResponseAction
TerminalPrivateGetCroshSettingsFunction::Run() {
const Extension* crosh_extension =
TerminalExtensionHelper::GetTerminalExtension(
Profile::FromBrowserContext(browser_context()));
if (!crosh_extension) {
return RespondNow(OneArgument(std::make_unique<base::DictionaryValue>()));
}
StorageFrontend* frontend = StorageFrontend::Get(browser_context());
frontend->RunWithStorage(
crosh_extension, settings_namespace::SYNC,
base::Bind(&TerminalPrivateGetCroshSettingsFunction::AsyncRunWithStorage,
this));
return RespondLater();
}
void TerminalPrivateGetCroshSettingsFunction::AsyncRunWithStorage(
ValueStore* storage) {
ValueStore::ReadResult result = storage->Get();
ExtensionFunction::ResponseValue response =
result.status().ok() ? OneArgument(result.PassSettings())
: Error(result.status().message);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&TerminalPrivateGetCroshSettingsFunction::Respond, this,
std::move(response)));
}
TerminalPrivateGetSettingsFunction::~TerminalPrivateGetSettingsFunction() =
default;
ExtensionFunction::ResponseAction TerminalPrivateGetSettingsFunction::Run() {
crostini::RecordTerminalSettingsChangesUMAs(
Profile::FromBrowserContext(browser_context()));
PrefService* service =
Profile::FromBrowserContext(browser_context())->GetPrefs();
const base::DictionaryValue* value =
service->GetDictionary(crostini::prefs::kCrostiniTerminalSettings);
return RespondNow(OneArgument(value->CreateDeepCopy()));
}
TerminalPrivateSetSettingsFunction::~TerminalPrivateSetSettingsFunction() =
default;
ExtensionFunction::ResponseAction TerminalPrivateSetSettingsFunction::Run() {
std::unique_ptr<SetSettings::Params> params(
SetSettings::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
PrefService* service =
Profile::FromBrowserContext(browser_context())->GetPrefs();
service->Set(crostini::prefs::kCrostiniTerminalSettings,
params->settings.additional_properties);
return RespondNow(NoArguments());
}
TerminalPrivateGetA11yStatusFunction::~TerminalPrivateGetA11yStatusFunction() =
default;
ExtensionFunction::ResponseAction TerminalPrivateGetA11yStatusFunction::Run() {
return RespondNow(
OneArgument(Profile::FromBrowserContext(browser_context())
->GetPrefs()
->Get(ash::prefs::kAccessibilitySpokenFeedbackEnabled)
->CreateDeepCopy()));
}
} // namespace extensions