blob: 22a8150cd22b134c38687b585fc4684decda8207 [file] [log] [blame]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/linux/desktop_session_factory_linux.h"
#include <algorithm>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/notimplemented.h"
#include "base/path_service.h"
#include "base/process/process.h"
#include "base/rand_util.h"
#include "base/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/uuid.h"
#include "base/values.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "remoting/base/async_file_util.h"
#include "remoting/base/auto_thread_task_runner.h"
#include "remoting/base/branding.h"
#include "remoting/base/logging.h"
#include "remoting/host/base/switches.h"
#include "remoting/host/desktop_session.h"
#include "remoting/host/ipc_constants.h"
#include "remoting/host/linux/linux_process_launcher_delegate.h"
#include "remoting/host/linux/remote_display_session_manager.h"
#include "remoting/host/mojom/desktop_session.mojom.h"
#include "remoting/host/pam_utils.h"
#include "remoting/host/worker_process_ipc_delegate.h"
#include "remoting/host/worker_process_launcher.h"
namespace remoting {
namespace {
// Delay to throttle writes to the remote displays file. This prevents bursts of
// remote display changes during creation and greeter->user switchover.
constexpr base::TimeDelta kWriteRemoteDisplaysDelay = base::Seconds(5);
std::string GenerateRandomDisplayName() {
std::string out;
// GDM does not allow hyphens in the RemoteDisplay remote_id, so we just
// remove it.
base::RemoveChars(base::Uuid::GenerateRandomV4().AsLowercaseString(), "-",
&out);
return out;
}
base::FilePath GetRemoteDisplaysConfigFilePath() {
base::FilePath config_dir = GetConfigDir();
if (config_dir.empty()) {
LOG(ERROR) << "Failed to get config directory.";
return {};
}
return config_dir.Append("remote_displays.json");
}
} // namespace
class DesktopSessionFactoryLinux::DesktopSessionLinux
: public DesktopSession,
public WorkerProcessIpcDelegate,
public mojom::DesktopSessionRequestHandler {
public:
DesktopSessionLinux(
DaemonProcess* daemon_process,
int id,
std::string_view display_name,
std::string_view required_username,
std::string_view client_id,
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
base::OnceClosure remove_from_factory);
~DesktopSessionLinux() override;
void OnRemoteDisplaySessionChanged(
const RemoteDisplaySessionManager::RemoteDisplaySession& info);
// Notifies the daemon process and terminates the desktop session. Note that
// `this` will be deleted during the call.
void TerminateSession();
const std::string& client_id() const { return client_id_; }
// DesktopSession implementation.
void SetScreenResolution(const ScreenResolution& resolution) override;
void ReconnectNetworkChannel(
const mojom::DesktopSessionOptions& options) override;
// WorkerProcessIpcDelegate implementation.
void OnChannelConnected(int32_t peer_pid) override;
void OnPermanentError(int exit_code) override;
void OnWorkerProcessStopped() override;
void OnAssociatedInterfaceRequest(
const std::string& interface_name,
mojo::ScopedInterfaceEndpointHandle handle) override;
// mojom::DesktopSessionRequestHandler implementation.
void ConnectDesktopChannel(
mojo::ScopedMessagePipeHandle desktop_pipe) override;
void InjectSecureAttentionSequence() override;
void CrashNetworkProcess() override;
base::WeakPtr<DesktopSessionLinux> GetWeakPtr();
private:
void CrashDesktopProcess(const base::Location& location);
// Returns whether the current desktop session is allowed based on
// `required_username_`. If the session info is not ready yet, this method
// will still return true, since it will be called again once the session info
// is ready.
bool IsSessionUsernameAllowed(
const RemoteDisplaySessionManager::RemoteDisplaySession& session);
SEQUENCE_CHECKER(sequence_checker_);
std::string display_name_ GUARDED_BY_CONTEXT(sequence_checker_);
std::string required_username_ GUARDED_BY_CONTEXT(sequence_checker_);
std::string client_id_ GUARDED_BY_CONTEXT(sequence_checker_);
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_
GUARDED_BY_CONTEXT(sequence_checker_);
base::OnceClosure remove_from_factory_ GUARDED_BY_CONTEXT(sequence_checker_);
std::unique_ptr<WorkerProcessLauncher> launcher_
GUARDED_BY_CONTEXT(sequence_checker_);
mojo::AssociatedReceiver<mojom::DesktopSessionRequestHandler>
desktop_session_request_handler_ GUARDED_BY_CONTEXT(sequence_checker_){
this};
mojo::AssociatedRemote<mojom::DesktopProcessControl> desktop_process_control_
GUARDED_BY_CONTEXT(sequence_checker_);
base::WeakPtrFactory<DesktopSessionLinux> weak_ptr_factory_{this};
};
DesktopSessionFactoryLinux::DesktopSessionLinux::DesktopSessionLinux(
DaemonProcess* daemon_process,
int id,
std::string_view display_name,
std::string_view required_username,
std::string_view client_id,
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
base::OnceClosure remove_from_factory)
: DesktopSession(daemon_process, id),
display_name_(display_name),
required_username_(required_username),
client_id_(client_id),
io_task_runner_(io_task_runner),
remove_from_factory_(std::move(remove_from_factory)) {}
DesktopSessionFactoryLinux::DesktopSessionLinux::~DesktopSessionLinux() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(remove_from_factory_).Run();
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::
OnRemoteDisplaySessionChanged(
const RemoteDisplaySessionManager::RemoteDisplaySession& info) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!info.session_info.has_value() || !info.user_info.has_value() ||
info.environment_variables.empty()) {
// Session is not ready yet, or is detached. Kill the desktop process if
// it is running.
launcher_.reset();
return;
}
if (!IsSessionUsernameAllowed(info)) {
LOG(ERROR) << "User " << info.user_info->username
<< " is not allowed for local login.";
// TODO: crbug.com/475611769 - Pass the SESSION_REJECTED error code to the
// network process so that the client can see the correct error message.
TerminateSession();
return;
}
if (!IsLocalLoginAllowed(info.user_info->username)) {
LOG(ERROR) << "User " << info.user_info->username
<< " is not allowed for local login.";
// TODO: crbug.com/475611769 - Pass the SESSION_REJECTED error code to the
// network process so that the client can see the correct error message.
TerminateSession();
return;
}
// TODO: crbug.com/475611769 - See if we need a dedicated desktop process
// binary.
base::FilePath this_exe;
if (!base::PathService::Get(base::BasePathKey::FILE_EXE, &this_exe)) {
LOG(ERROR) << "Failed to get the current executable path.";
TerminateSession();
return;
}
base::CommandLine command_line(this_exe);
command_line.AppendSwitchASCII(kProcessTypeSwitchName, kProcessTypeDesktop);
LinuxWorkerProcessLauncherDelegate::LaunchOptions options(command_line);
options.new_session = true;
options.uid = info.user_info->uid;
options.gid = info.user_info->gid;
options.working_dir = info.user_info->home_dir;
options.environment_variables = info.environment_variables;
// Launch the desktop process. If there is a desktop process running for the
// previous desktop session, this will kill it.
launcher_ = std::make_unique<WorkerProcessLauncher>(
std::make_unique<LinuxWorkerProcessLauncherDelegate>(std::move(options),
io_task_runner_),
this);
desktop_process_control_.reset();
launcher_->GetRemoteAssociatedInterface(
desktop_process_control_.BindNewEndpointAndPassReceiver());
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::TerminateSession() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The daemon process will delete `this`.
daemon_process()->CloseDesktopSession(id());
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::SetScreenResolution(
const ScreenResolution& resolution) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO: crbug.com/475611769 - Implement.
NOTIMPLEMENTED_LOG_ONCE();
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::ReconnectNetworkChannel(
const mojom::DesktopSessionOptions& options) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (options.required_username != required_username_) {
LOG(ERROR) << "Required username has changed.";
TerminateSession();
return;
}
if (options.client_id != client_id_) {
LOG(ERROR) << "Client ID has changed.";
TerminateSession();
return;
}
if (desktop_process_control_.is_bound()) {
desktop_process_control_->ReconnectNetworkChannel();
}
// If `desktop_process_control_` is not bound, then it means the desktop
// process isn't launched yet. It will send the desktop pipe after it is
// launched anyway so we don't need to do anything.
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::OnChannelConnected(
int32_t peer_pid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
VLOG(1) << "IPC: daemon <- desktop (" << peer_pid << ")";
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::OnPermanentError(
int exit_code) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TerminateSession();
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::OnWorkerProcessStopped() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
desktop_process_control_.reset();
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::
OnAssociatedInterfaceRequest(const std::string& interface_name,
mojo::ScopedInterfaceEndpointHandle handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (interface_name == mojom::DesktopSessionRequestHandler::Name_) {
if (desktop_session_request_handler_.is_bound()) {
LOG(ERROR) << "Receiver already bound for associated interface: "
<< mojom::DesktopSessionRequestHandler::Name_;
CrashDesktopProcess(FROM_HERE);
}
mojo::PendingAssociatedReceiver<mojom::DesktopSessionRequestHandler>
pending_receiver(std::move(handle));
desktop_session_request_handler_.Bind(std::move(pending_receiver));
// Reset the receiver on disconnect so |desktop_session_request_handler_|
// can be re-bound if |launcher_| spawns a new desktop process.
desktop_session_request_handler_.reset_on_disconnect();
} else {
LOG(ERROR) << "Unknown associated interface requested: " << interface_name
<< ", crashing the desktop process";
CrashDesktopProcess(FROM_HERE);
}
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::ConnectDesktopChannel(
mojo::ScopedMessagePipeHandle desktop_pipe) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!daemon_process()->OnDesktopSessionAgentAttached(
id(), /*session_id=*/0, std::move(desktop_pipe))) {
CrashDesktopProcess(FROM_HERE);
}
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::
InjectSecureAttentionSequence() {
NOTIMPLEMENTED();
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::CrashNetworkProcess() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
daemon_process()->CrashNetworkProcess(FROM_HERE);
}
base::WeakPtr<DesktopSessionFactoryLinux::DesktopSessionLinux>
DesktopSessionFactoryLinux::DesktopSessionLinux::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void DesktopSessionFactoryLinux::DesktopSessionLinux::CrashDesktopProcess(
const base::Location& location) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
launcher_->Crash(location);
}
bool DesktopSessionFactoryLinux::DesktopSessionLinux::IsSessionUsernameAllowed(
const RemoteDisplaySessionManager::RemoteDisplaySession& info) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (required_username_.empty()) {
return true;
}
if (!info.user_info.has_value() || !info.session_info.has_value()) {
// The session info is not ready yet. This method will be called again when
// it is ready, so we just return true here.
return true;
}
if (info.session_info->session_class == "greeter") {
HOST_LOG << "Login username check skipped for greeter session.";
return true;
}
if (base::EqualsCaseInsensitiveASCII(required_username_,
info.user_info->username)) {
return true;
}
LOG(ERROR) << "User " << info.user_info->username
<< " does not match the required username: " << required_username_;
return false;
}
// DesktopSessionFactoryLinux implementation.
DesktopSessionFactoryLinux::DesktopSessionFactoryLinux(
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner)
: io_task_runner_(io_task_runner),
write_remote_displays_timer_(
FROM_HERE,
kWriteRemoteDisplaysDelay,
base::BindRepeating(
&DesktopSessionFactoryLinux::WriteRemoteDisplaysToFile,
base::Unretained(this))) {}
DesktopSessionFactoryLinux::~DesktopSessionFactoryLinux() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void DesktopSessionFactoryLinux::Start(Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
remote_display_session_manager_.Start(
this,
base::BindOnce(&DesktopSessionFactoryLinux::OnStartResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
std::unique_ptr<DesktopSession>
DesktopSessionFactoryLinux::CreateDesktopSession(
int id,
DaemonProcess* daemon_process,
const mojom::DesktopSessionOptions& options) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::string display_name;
auto recovered_it = recovered_displays_.find(options.client_id);
if (recovered_it != recovered_displays_.end()) {
display_name = std::move(recovered_it->second);
recovered_displays_.erase(recovered_it);
HOST_LOG << "Reusing recovered remote display name " << display_name
<< " for " << options.client_id;
} else {
display_name = GenerateRandomDisplayName();
}
auto desktop_session = std::make_unique<DesktopSessionLinux>(
daemon_process, id, display_name, options.required_username,
options.client_id, io_task_runner_,
base::BindOnce(&DesktopSessionFactoryLinux::RemoveDesktopSession,
weak_ptr_factory_.GetWeakPtr(), display_name));
if (auto* info =
remote_display_session_manager_.GetRemoteDisplayInfo(display_name)) {
// The remote display already exists. Find a ready session and notify, which
// will launch the desktop process.
LOG_IF(WARNING, info->sessions.size() > 1)
<< "There are more than one remote display session for the remote "
<< "display " << display_name;
for (const auto& [path, session] : info->sessions) {
if (session.session_info.has_value() && session.user_info.has_value() &&
!session.environment_variables.empty()) {
desktop_session->OnRemoteDisplaySessionChanged(session);
break;
}
}
} else {
// Note that this code path will be reached if the recovered remote display
// has been terminated externally. In this case a new remote display with
// the same display name will be created.
// TODO: crbug.com/475611769 - Add timeout mechanism for waiting for the
// desktop session.
remote_display_session_manager_.CreateRemoteDisplay(
display_name,
base::BindOnce(&DesktopSessionFactoryLinux::OnCreateRemoteDisplayResult,
weak_ptr_factory_.GetWeakPtr(), display_name));
}
desktop_sessions_[display_name] = desktop_session->GetWeakPtr();
return desktop_session;
}
void DesktopSessionFactoryLinux::OnStartResult(
Callback callback,
base::expected<void, Loggable> result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!result.has_value()) {
std::move(callback).Run(std::move(result));
return;
}
base::FilePath file_path = GetRemoteDisplaysConfigFilePath();
if (file_path.empty()) {
// Just don't attempt to recover remote displays in this case.
std::move(callback).Run(base::ok());
return;
}
ReadFileAsync(
file_path,
base::BindOnce(&DesktopSessionFactoryLinux::OnRemoteDisplaysFileLoaded,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void DesktopSessionFactoryLinux::OnRemoteDisplaysFileLoaded(
Callback callback,
base::FileErrorOr<std::string> load_result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (load_result.has_value()) {
std::optional<base::Value> json =
base::JSONReader::Read(*load_result, base::JSON_PARSE_RFC);
if (json && json->is_dict()) {
for (const auto [client_id, display_name_value] : json->GetDict()) {
if (!display_name_value.is_string()) {
continue;
}
const std::string& display_name = display_name_value.GetString();
if (remote_display_session_manager_.GetRemoteDisplayInfo(
display_name)) {
HOST_LOG << "Recovering remote display " << display_name << " for "
<< client_id;
// The entry may still be invalid, e.g. if the file has been tampered
// with such that a user is associated with another user's graphical
// session. This will be validated in
// DesktopSessionLinux::OnRemoteDisplaySessionChanged when the client
// connects.
recovered_displays_[client_id] = display_name;
} else {
LOG(WARNING) << "Ignored remote display " << display_name
<< " which no longer exists.";
}
}
} else {
LOG(ERROR) << "Failed to parse remote displays file.";
}
} else if (load_result.error() != base::File::FILE_ERROR_NOT_FOUND) {
LOG(ERROR) << "Failed to read remote displays file: "
<< base::File::ErrorToString(load_result.error());
}
for (const auto& [display_name, info] :
remote_display_session_manager_.remote_displays()) {
bool is_recovered = std::ranges::any_of(
recovered_displays_, [&display_name](const auto& pair) {
return pair.second == display_name;
});
if (!is_recovered) {
HOST_LOG << "Terminating unknown CRD-managed remote display: "
<< display_name;
remote_display_session_manager_.TerminateRemoteDisplay(
display_name,
base::BindOnce([](base::expected<void, Loggable> result) {
if (!result.has_value()) {
LOG(ERROR) << result.error();
}
}));
}
}
std::move(callback).Run(base::ok());
}
void DesktopSessionFactoryLinux::OnCreateRemoteDisplayResult(
std::string_view display_name,
base::expected<void, Loggable> result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (result.has_value()) {
// No need to do anything. OnRemoteDisplayChanged() will be called
// once the session is ready.
return;
}
LOG(ERROR) << result.error();
auto session = FindSession(display_name);
// session may be nullptr if the DesktopSession has been destroyed.
if (session) {
session->TerminateSession();
}
}
void DesktopSessionFactoryLinux::OnRemoteDisplayChanged(
std::string_view display_name,
const RemoteDisplaySessionManager::RemoteDisplayInfo& info) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (info.sessions.empty()) {
LOG(WARNING) << "Remote display " << display_name << " has no sessions.";
return;
}
if (info.sessions.size() == 1) {
auto session = FindSession(display_name);
if (!session) {
return;
}
session->OnRemoteDisplaySessionChanged(info.sessions.begin()->second);
RequestWriteRemoteDisplaysToFile();
return;
}
// There are multiple sessions with the same display name. This usually
// happens during greeter->user transition. This is mostly for GRD to do the
// RDP handover before they terminate the greeter. CRD doesn't do RDP
// handover, so we just find and terminate the greeter session.
auto greeter_it = std::ranges::find_if(info.sessions, [](const auto& pair) {
return pair.second.session_info->session_class == "greeter";
});
const RemoteDisplaySessionManager::RemoteDisplaySession* session = nullptr;
if (greeter_it != info.sessions.end()) {
session = &greeter_it->second;
HOST_LOG << "Terminating greeter session "
<< session->session_info->session_id
<< "for remote display: " << display_name;
} else {
session = &info.sessions.begin()->second;
LOG(WARNING) << "Cannot find greeter session. Terminating the first "
<< session->session_info->session_class << " session "
<< session->session_info->session_id
<< " for remote display: " << display_name;
}
// We just terminate one session, since terminating multiple sessions at a
// time will result in multiple OnRemoteDisplayChanged() calls.
// OnRemoteDisplayChanged() will be called once the session is removed.
remote_display_session_manager_.TerminateRemoteDisplaySession(
*session, base::BindOnce([](base::expected<void, Loggable> result) {
// TODO: crbug.com/475611769 - See what to do with the callback.
if (!result.has_value()) {
LOG(ERROR) << result.error();
}
}));
}
void DesktopSessionFactoryLinux::OnRemoteDisplayTerminated(
std::string_view display_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto session = FindSession(display_name);
// session may be nullptr if the desktop session has already been removed by
// RemoveDesktopSession().
if (session) {
// TODO: crbug.com/475611769 - Pass the SESSION_REJECTED error code to the
// network process so that the client can see the correct error message.
session->TerminateSession();
}
}
void DesktopSessionFactoryLinux::RemoveDesktopSession(
std::string_view display_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
desktop_sessions_.erase(display_name);
RequestWriteRemoteDisplaysToFile();
if (!remote_display_session_manager_.GetRemoteDisplayInfo(display_name)) {
// The remote display has already been terminated.
return;
}
remote_display_session_manager_.TerminateRemoteDisplay(
display_name, base::BindOnce([](base::expected<void, Loggable> result) {
// TODO: crbug.com/475611769 - See what to do with the callback.
if (!result.has_value()) {
LOG(ERROR) << result.error();
}
}));
}
base::WeakPtr<DesktopSessionFactoryLinux::DesktopSessionLinux>
DesktopSessionFactoryLinux::FindSession(std::string_view display_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = desktop_sessions_.find(display_name);
return it == desktop_sessions_.end() ? nullptr : it->second;
}
void DesktopSessionFactoryLinux::RequestWriteRemoteDisplaysToFile() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This either starts or delays the timer.
write_remote_displays_timer_.Reset();
}
void DesktopSessionFactoryLinux::WriteRemoteDisplaysToFile() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::DictValue dict;
for (const auto& [display_name, session] : desktop_sessions_) {
if (!session) {
continue;
}
const auto* info =
remote_display_session_manager_.GetRemoteDisplayInfo(display_name);
if (!info) {
continue;
}
// Only write a remote display with a user session. There is no value to
// recover a greeter session, and it is not recoverable on GNOME 48 or older
// since we can't launch the session reporter process ourselves.
bool has_user_session =
std::ranges::any_of(info->sessions, [](const auto& pair) {
return pair.second.session_info.has_value() &&
pair.second.session_info->session_class == "user";
});
if (!has_user_session) {
continue;
}
if (dict.contains(session->client_id())) {
LOG(WARNING) << "Multiple remote displays found for "
<< session->client_id() << ". Using "
<< *dict.FindString(session->client_id()) << " and ignoring "
<< display_name;
} else {
dict.Set(session->client_id(), display_name);
}
}
base::FilePath file_path = GetRemoteDisplaysConfigFilePath();
if (file_path.empty()) {
return;
}
std::string json_output;
if (!base::JSONWriter::Write(dict, &json_output)) {
LOG(ERROR) << "Failed to serialize remote displays to JSON.";
return;
}
// Only root is allowed to read or write this file.
WriteFileWithPermissionsAsync(
file_path, std::move(json_output), 0o600,
base::BindOnce(
[](const base::FilePath& path, base::FileErrorOr<void> result) {
if (!result.has_value()) {
LOG(ERROR) << "Failed to write remote displays to " << path
<< ": " << base::File::ErrorToString(result.error());
}
},
file_path));
}
} // namespace remoting