blob: 6916b0352d76225a683bb8b2eb57d058edd6e875 [file] [log] [blame]
// Copyright 2025 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/clipboard_gnome.h"
#include <array>
#include <string_view>
#include <utility>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "remoting/base/constants.h"
#include "remoting/base/logging.h"
#include "remoting/host/linux/dbus_interfaces/org_gnome_Mutter_RemoteDesktop.h"
#include "remoting/proto/event.pb.h"
#include "remoting/protocol/clipboard_stub.h"
namespace remoting {
namespace {
constexpr char kRemoteDesktopBusName[] = "org.gnome.Mutter.RemoteDesktop";
// This list was created by looking at the MIME types claimed by some Wayland
// apps that put text onto the clipboard. The first few are claimed by X11 apps
// using XWayland (such as xclip).
constexpr std::array<std::string_view, 5> kTextMimeTypes = {
"UTF8_STRING", "STRING", "TEXT", "text/plain;charset=utf-8", "text/plain"};
} // namespace
ClipboardGnome::ClipboardGnome(GDBusConnectionRef connection,
gvariant::ObjectPath session_path)
: dbus_connection_(std::move(connection)),
session_path_(std::move(session_path)) {}
ClipboardGnome::~ClipboardGnome() = default;
void ClipboardGnome::Start(
std::unique_ptr<protocol::ClipboardStub> client_clipboard) {
client_clipboard_.swap(client_clipboard);
selection_owner_changed_signal_ = dbus_connection_.SignalSubscribe<
org_gnome_Mutter_RemoteDesktop_Session::SelectionOwnerChanged>(
kRemoteDesktopBusName, session_path_,
base::BindRepeating(&ClipboardGnome::OnSelectionOwnerChanged,
weak_factory_.GetWeakPtr()));
selection_transfer_signal_ = dbus_connection_.SignalSubscribe<
org_gnome_Mutter_RemoteDesktop_Session::SelectionTransfer>(
kRemoteDesktopBusName, session_path_,
base::BindRepeating(&ClipboardGnome::OnSelectionTransfer,
weak_factory_.GetWeakPtr()));
dbus_connection_
.Call<org_gnome_Mutter_RemoteDesktop_Session::EnableClipboard>(
kRemoteDesktopBusName, session_path_,
std::tuple(gvariant::EmptyArrayOf<"{sv}">()),
base::BindOnce([](base::expected<std::tuple<>, Loggable> result) {
if (!result.has_value()) {
LOG(ERROR) << "EnableClipboard failed: " << result.error();
}
}));
}
void ClipboardGnome::InjectClipboardEvent(
const protocol::ClipboardEvent& event) {
if (event.mime_type() != kMimeTypeTextUtf8) {
LOG(ERROR) << "Non UTF-8 clipboard type from client: " << event.mime_type();
return;
}
clipboard_data_ = std::move(event.data());
// Claim ownership of the text mime-types. After this, GNOME may request the
// clipboard data via one or more SelectionTransfer signals.
// The variant type here is inconsistent with what GNOME returns in the
// SelectionOwnerChanged signal. Here it must be "as", not "(as)":
// https://gitlab.gnome.org/GNOME/gnome-remote-desktop/-/blob/979631ffeac75f79fad104c3e479dd7513cc2478/src/grd-session.c#L804
auto mime_types_option = GVariantFrom(gvariant::BoxedRef(kTextMimeTypes));
dbus_connection_.Call<org_gnome_Mutter_RemoteDesktop_Session::SetSelection>(
kRemoteDesktopBusName, session_path_,
std::tuple(std::array{std::pair{"mime-types", mime_types_option}}),
base::BindOnce([](base::expected<std::tuple<>, Loggable> result) {
if (!result.has_value()) {
LOG(ERROR) << "SetSelection failed: " << result.error();
}
}));
}
void ClipboardGnome::OnSelectionOwnerChanged(
std::tuple<GVariantRef<"a{sv}">> args) {
HOST_LOG << "Received SelectionOwnerChanged signal.";
auto& [options] = args;
auto maybe_boxed_is_owner = options.LookUp("session-is-owner");
if (maybe_boxed_is_owner.has_value()) {
// "session-is-owner" is a variant holding a boolean.
if (*maybe_boxed_is_owner == GVariantFrom(gvariant::Boxed{true})) {
// The selection is now owned by this D-Bus caller (a recent SetSelection
// call succeeded). SelectionRead should only be used to request the data
// from a different owner.
HOST_LOG << "Ignoring event, session is already owner.";
return;
}
}
auto maybe_boxed_mime_types = options.LookUp("mime-types");
if (maybe_boxed_mime_types.has_value()) {
std::vector<std::string> mime_types;
// std::tie is needed here because mime-types has type "(as)", not "as".
auto destructure_result =
maybe_boxed_mime_types->TryDestructure(std::tie(mime_types));
if (destructure_result.has_value()) {
for (auto mime_type : mime_types) {
if (base::Contains(kTextMimeTypes, mime_type)) {
SelectionRead(mime_type);
return;
}
}
}
}
}
void ClipboardGnome::OnSelectionTransfer(
std::tuple<std::string, std::uint32_t> args) {
const auto& [mime_type, serial] = args;
HOST_LOG << "Got SelectionTransfer signal with mime-type: " << mime_type;
if (!base::Contains(kTextMimeTypes, mime_type)) {
// SelectionTransfer request should be for a mime-type claimed by
// SetSelection.
LOG(ERROR) << "Unexpected mime-type requested: " << mime_type;
return;
}
SelectionWrite(serial);
}
void ClipboardGnome::SelectionRead(std::string_view mime_type) {
dbus_connection_.Call<org_gnome_Mutter_RemoteDesktop_Session::SelectionRead>(
kRemoteDesktopBusName, session_path_, std::tuple(mime_type),
base::BindOnce(&ClipboardGnome::OnSelectionReadReply,
weak_factory_.GetWeakPtr()));
}
void ClipboardGnome::OnSelectionReadReply(
base::expected<std::pair<std::tuple<GDBusFdList::Handle>, GDBusFdList>,
Loggable> result) {
if (!result.has_value()) {
LOG(ERROR) << "SelectionRead failed: " << result.error();
return;
}
auto& [tuple, dbus_fds] = result.value();
auto& [read_handle] = tuple;
SparseFdList fd_list = std::move(dbus_fds).MakeSparse();
base::ScopedFD read_fd = fd_list.Extract(read_handle);
if (!read_fd.is_valid()) {
LOG(ERROR) << "Failed to get FD for SelectionRead.";
return;
}
fd_string_reader_ = FdStringReader::ReadFromPipe(
std::move(read_fd), base::BindOnce(&ClipboardGnome::OnSelectionDataRead,
weak_factory_.GetWeakPtr()));
}
void ClipboardGnome::OnSelectionDataRead(
base::expected<std::string, Loggable> result) {
// Close the FD.
fd_string_reader_.reset();
if (result.has_value()) {
HOST_LOG << "Got clipboard data of size: " << result.value().size();
protocol::ClipboardEvent event;
event.set_mime_type(kMimeTypeTextUtf8);
event.set_data(std::move(result.value()));
client_clipboard_->InjectClipboardEvent(event);
} else {
LOG(ERROR) << "Failed to read clipboard data: " << result.error();
}
}
void ClipboardGnome::SelectionWrite(std::uint32_t serial) {
selection_write_serial_ = serial;
dbus_connection_.Call<org_gnome_Mutter_RemoteDesktop_Session::SelectionWrite>(
kRemoteDesktopBusName, session_path_, std::tuple(serial),
base::BindOnce(&ClipboardGnome::OnSelectionWriteReply,
weak_factory_.GetWeakPtr()));
}
void ClipboardGnome::OnSelectionWriteReply(
base::expected<std::pair<std::tuple<GDBusFdList::Handle>, GDBusFdList>,
Loggable> result) {
if (!result.has_value()) {
LOG(ERROR) << "SelectionWrite failed: " << result.error();
return;
}
auto& [tuple, dbus_fds] = result.value();
auto& [write_handle] = tuple;
SparseFdList fd_list = std::move(dbus_fds).MakeSparse();
base::ScopedFD write_fd = fd_list.Extract(write_handle);
if (!write_fd.is_valid()) {
LOG(ERROR) << "Failed to get FD for SelectionWrite.";
return;
}
fd_string_writer_ = FdStringWriter::Write(
clipboard_data_, std::move(write_fd),
base::BindOnce(&ClipboardGnome::OnSelectionDataWritten,
weak_factory_.GetWeakPtr()));
}
void ClipboardGnome::OnSelectionDataWritten(
base::expected<void, Loggable> result) {
// Close the FD.
fd_string_writer_.reset();
bool success = result.has_value();
if (!success) {
LOG(ERROR) << "Failed to write clipboard data: " << result.error();
}
dbus_connection_
.Call<org_gnome_Mutter_RemoteDesktop_Session::SelectionWriteDone>(
kRemoteDesktopBusName, session_path_,
std::tuple(selection_write_serial_, success),
base::BindOnce([](base::expected<std::tuple<>, Loggable> result) {
if (!result.has_value()) {
LOG(ERROR) << "SelectionWriteDone returned error: "
<< result.error();
}
}));
}
} // namespace remoting