From d4bb6a1a1ee30dafb1f03b08390adc2e3716855b Mon Sep 17 00:00:00 2001 From: x-yl Date: Mon, 12 Jul 2021 18:25:37 +0400 Subject: [PATCH] SpiceAgent: Add a new spice agent service :^) A SPICE agent communicates with the host OS to provide nifty features like clipboard sharing :^) This patch implements only plain-text clipboard sharing. See: github.com/freedesktop/spice-protocol/blob/master/spice/vd_agent.h --- Base/etc/SystemServer.ini | 3 + Userland/Services/CMakeLists.txt | 1 + Userland/Services/SpiceAgent/CMakeLists.txt | 13 ++ .../SpiceAgent/ClipboardServerConnection.cpp | 8 + .../SpiceAgent/ClipboardServerConnection.h | 31 +++ Userland/Services/SpiceAgent/SpiceAgent.cpp | 202 ++++++++++++++++++ Userland/Services/SpiceAgent/SpiceAgent.h | 127 +++++++++++ Userland/Services/SpiceAgent/main.cpp | 47 ++++ 8 files changed, 432 insertions(+) create mode 100644 Userland/Services/SpiceAgent/CMakeLists.txt create mode 100644 Userland/Services/SpiceAgent/ClipboardServerConnection.cpp create mode 100644 Userland/Services/SpiceAgent/ClipboardServerConnection.h create mode 100644 Userland/Services/SpiceAgent/SpiceAgent.cpp create mode 100644 Userland/Services/SpiceAgent/SpiceAgent.h create mode 100644 Userland/Services/SpiceAgent/main.cpp diff --git a/Base/etc/SystemServer.ini b/Base/etc/SystemServer.ini index ab1874e2d0..a409caf546 100644 --- a/Base/etc/SystemServer.ini +++ b/Base/etc/SystemServer.ini @@ -190,3 +190,6 @@ Environment=DO_SHUTDOWN_AFTER_TESTS=1 TERM=xterm PATH=/bin:/usr/bin:/usr/local/b User=anon WorkingDirectory=/home/anon BootModes=self-test + +[SpiceAgent] +KeepAlive=0 diff --git a/Userland/Services/CMakeLists.txt b/Userland/Services/CMakeLists.txt index c7bfc5ab3a..0fcf7a417b 100644 --- a/Userland/Services/CMakeLists.txt +++ b/Userland/Services/CMakeLists.txt @@ -14,6 +14,7 @@ add_subdirectory(LookupServer) add_subdirectory(NotificationServer) add_subdirectory(RequestServer) add_subdirectory(SQLServer) +add_subdirectory(SpiceAgent) add_subdirectory(SystemServer) add_subdirectory(Taskbar) add_subdirectory(TelnetServer) diff --git a/Userland/Services/SpiceAgent/CMakeLists.txt b/Userland/Services/SpiceAgent/CMakeLists.txt new file mode 100644 index 0000000000..590e451330 --- /dev/null +++ b/Userland/Services/SpiceAgent/CMakeLists.txt @@ -0,0 +1,13 @@ +serenity_component( + SpiceAgent + TARGETS SpiceAgent +) + +set(SOURCES + main.cpp + SpiceAgent.cpp + ClipboardServerConnection.cpp +) + +serenity_bin(SpiceAgent) +target_link_libraries(SpiceAgent LibGfx LibCore LibIPC) diff --git a/Userland/Services/SpiceAgent/ClipboardServerConnection.cpp b/Userland/Services/SpiceAgent/ClipboardServerConnection.cpp new file mode 100644 index 0000000000..6c7ed5c522 --- /dev/null +++ b/Userland/Services/SpiceAgent/ClipboardServerConnection.cpp @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2021, Kyle Pereira + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ClipboardServerConnection.h" + diff --git a/Userland/Services/SpiceAgent/ClipboardServerConnection.h b/Userland/Services/SpiceAgent/ClipboardServerConnection.h new file mode 100644 index 0000000000..cad8065ffe --- /dev/null +++ b/Userland/Services/SpiceAgent/ClipboardServerConnection.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021, Kyle Pereira + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +#pragma once + +class ClipboardServerConnection final + : public IPC::ServerConnection + , public ClipboardClientEndpoint { + C_OBJECT(ClipboardServerConnection); + + Function on_data_changed; + +private: + ClipboardServerConnection() + : IPC::ServerConnection(*this, "/tmp/portal/clipboard") + { + } + virtual void clipboard_data_changed(String const&) override + { + on_data_changed(); + } +}; diff --git a/Userland/Services/SpiceAgent/SpiceAgent.cpp b/Userland/Services/SpiceAgent/SpiceAgent.cpp new file mode 100644 index 0000000000..082800d0d3 --- /dev/null +++ b/Userland/Services/SpiceAgent/SpiceAgent.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021, Kyle Pereira + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SpiceAgent.h" +#include "ClipboardServerConnection.h" +#include +#include +#include + +SpiceAgent::SpiceAgent(int fd, ClipboardServerConnection& connection) + : m_fd(fd) + , m_clipboard_connection(connection) +{ + m_notifier = Core::Notifier::construct(fd, Core::Notifier::Read); + m_notifier->on_ready_to_read = [this] { + on_message_received(); + }; + m_clipboard_connection.on_data_changed = [this] { + if (m_just_set_clip) { + m_just_set_clip = false; + return; + } + auto grab_buffer = ClipboardGrab::make_buffer({ ClipboardType::Text }); + send_message(grab_buffer); + }; + auto buffer = AnnounceCapabilities::make_buffer(true, { Capability::ClipboardByDemand }); + send_message(buffer); +} + +void SpiceAgent::on_message_received() +{ + ChunkHeader header {}; + read_n(&header, sizeof(header)); + auto buffer = ByteBuffer::create_uninitialized(header.size); + read_n(buffer.data(), buffer.size()); + auto* message = reinterpret_cast(buffer.data()); + switch (message->type) { + case (u32)MessageType::AnnounceCapabilities: { + auto* capabilities_message = reinterpret_cast(message->data); + if (capabilities_message->request) { + auto capabilities_buffer = AnnounceCapabilities::make_buffer(false, { Capability::ClipboardByDemand }); + send_message(capabilities_buffer); + } + break; + } + case (u32)MessageType::ClipboardRequest: { + auto clip_data = m_clipboard_connection.get_clipboard_data().data(); + ByteBuffer byte_buffer = ByteBuffer::copy(clip_data.data(), clip_data.size()); + auto clipboard_buffer = Clipboard::make_buffer(ClipboardType::Text, byte_buffer); + send_message(clipboard_buffer); + break; + } + case (u32)MessageType::ClipboardGrab: { + auto request_buffer = ClipboardRequest::make_buffer(ClipboardType::Text); + send_message(request_buffer); + break; + } + case (u32)MessageType::Clipboard: { + auto* clipboard_message = reinterpret_cast(message->data); + auto data_buffer = ByteBuffer::create_uninitialized(message->size - sizeof(u32)); + + const auto total_bytes = message->size - sizeof(Clipboard); + auto bytes_copied = header.size - sizeof(Message) - sizeof(Clipboard); + memcpy(data_buffer.data(), clipboard_message->data, bytes_copied); + + while (bytes_copied < total_bytes) { + ChunkHeader next_header; + read_n(&next_header, sizeof(ChunkHeader)); + read_n(data_buffer.data() + bytes_copied, next_header.size); + bytes_copied += next_header.size; + } + + m_just_set_clip = true; + auto anon_buffer = Core::AnonymousBuffer::create_with_size(data_buffer.size()); + memcpy(anon_buffer.data(), data_buffer.data(), data_buffer.size()); + m_clipboard_connection.async_set_clipboard_data(anon_buffer, "text/plain", {}); + break; + } + default: + dbgln("Unhandled message type {}", message->type); + } +} + +void SpiceAgent::read_n(void* dest, size_t n) +{ + size_t bytes_read = 0; + while (bytes_read < n) { + int nread = read(m_fd, (u8*)dest + bytes_read, n - bytes_read); + if (nread > 0) { + bytes_read += nread; + } else if (errno == EAGAIN) { + continue; + } else { + dbgln("Failed to read: {}", errno); + return; + } + } +} + +void SpiceAgent::send_message(ByteBuffer const& buffer) +{ + size_t bytes_written = 0; + while (bytes_written < buffer.size()) { + int result = write(m_fd, buffer.data() + bytes_written, buffer.size() - bytes_written); + if (result < 0) { + dbgln("Failed to write: {}", errno); + return; + } + bytes_written += result; + } +} + +SpiceAgent::Message* SpiceAgent::initialize_headers(u8* data, size_t additional_data_size, MessageType type) +{ + new (data) ChunkHeader { + (u32)Port::Client, + (u32)(sizeof(Message) + additional_data_size) + }; + + auto* message = new (data + sizeof(ChunkHeader)) Message { + AGENT_PROTOCOL, + (u32)type, + 0, + (u32)additional_data_size + }; + return message; +} + +ByteBuffer SpiceAgent::AnnounceCapabilities::make_buffer(bool request, const Vector& capabilities) +{ + size_t required_size = sizeof(ChunkHeader) + sizeof(Message) + sizeof(AnnounceCapabilities); + auto buffer = ByteBuffer::create_uninitialized(required_size); + u8* data = buffer.data(); + + auto* message = initialize_headers(data, sizeof(AnnounceCapabilities), MessageType::AnnounceCapabilities); + + auto* announce_message = new (message->data) AnnounceCapabilities { + request, + {} + }; + + for (auto& cap : capabilities) { + announce_message->caps[0] |= (1 << (u32)cap); + } + + return buffer; +} + +ByteBuffer SpiceAgent::ClipboardGrab::make_buffer(const Vector& types) +{ + VERIFY(types.size() > 0); + size_t variable_data_size = sizeof(u32) * types.size(); + size_t required_size = sizeof(ChunkHeader) + sizeof(Message) + variable_data_size; + auto buffer = ByteBuffer::create_uninitialized(required_size); + u8* data = buffer.data(); + + auto* message = initialize_headers(data, variable_data_size, MessageType::ClipboardGrab); + + auto* grab_message = new (message->data) ClipboardGrab {}; + + for (auto i = 0u; i < types.size(); i++) { + grab_message->types[i] = (u32)types[i]; + } + + return buffer; +} + +ByteBuffer SpiceAgent::Clipboard::make_buffer(ClipboardType type, ReadonlyBytes contents) +{ + size_t data_size = sizeof(Clipboard) + contents.size(); + size_t required_size = sizeof(ChunkHeader) + sizeof(Message) + data_size; + auto buffer = ByteBuffer::create_uninitialized(required_size); + u8* data = buffer.data(); + + auto* message = initialize_headers(data, data_size, MessageType::Clipboard); + + auto* clipboard_message = new (message->data) Clipboard { + .type = (u32)type + }; + + memcpy(clipboard_message->data, contents.data(), contents.size()); + + return buffer; +} + +ByteBuffer SpiceAgent::ClipboardRequest::make_buffer(ClipboardType type) +{ + size_t data_size = sizeof(ClipboardRequest); + size_t required_size = sizeof(ChunkHeader) + sizeof(Message) + data_size; + auto buffer = ByteBuffer::create_uninitialized(required_size); + u8* data = buffer.data(); + + auto* message = initialize_headers(data, data_size, MessageType::ClipboardRequest); + new (message->data) ClipboardRequest { + .type = (u32)type + }; + + return buffer; +} diff --git a/Userland/Services/SpiceAgent/SpiceAgent.h b/Userland/Services/SpiceAgent/SpiceAgent.h new file mode 100644 index 0000000000..24bef40b69 --- /dev/null +++ b/Userland/Services/SpiceAgent/SpiceAgent.h @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2021, Kyle Pereira + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ClipboardServerConnection.h" +#include +#include +#include + +#pragma once + +class SpiceAgent { +public: + SpiceAgent(int fd, ClipboardServerConnection&); + + static constexpr u32 AGENT_PROTOCOL = 1; + enum class Port { + Client = 1, + Server + }; + + struct [[gnu::packed]] ChunkHeader { + u32 port {}; + u32 size {}; + }; + + struct [[gnu::packed]] Message { + u32 protocol; + u32 type; + u64 opaque; + u32 size; + u8 data[]; + }; + + enum class MessageType { + MouseState = 1, // server -> client + MonitorsConfig, // client -> agent|server + Reply, // agent -> client + Clipboard, // both directions + DisplayConfig, // client -> agent + AnnounceCapabilities, // both directions + ClipboardGrab, // both directions + ClipboardRequest, // both directions + ClipboardRelease, // both directions + FileTransferStart, + FileTransferStatus, + FileTransferData, + Disconnected, + MaxClipboard, + VolumeSync, + GraphicsDeviceInfo, + }; + + enum class Capability { + MouseState = 0, + MonitorsConfig, + Reply, + Clipboard, + DisplayConfig, + ClipboardByDemand, + ClipboardSelection, + SparseMonitorsConfig, + GuestLineEndLF, + GuestLineEndCRLF, + MaxClipboard, + AudioVolumeSync, + MonitorsConfigPosition, + FileTransferDisabled, + FileTransferDetailedErrors, + GraphicsCardInfo, + ClipboardNoReleaseOnRegrab, + ClipboardGrabSerial, + __End, + }; + + enum class ClipboardType { + None = 0, + Text, + PNG, + BMP, + TIFF, + JPG, + FileList, + __Count + }; + + constexpr static size_t CAPABILITIES_SIZE = ((size_t)Capability::__End + 31) / 32; + + struct [[gnu::packed]] AnnounceCapabilities { + u32 request; + u32 caps[CAPABILITIES_SIZE]; + + static ByteBuffer make_buffer(bool request, const Vector& capabilities); + }; + + struct [[gnu::packed]] ClipboardGrab { + u32 types[0]; + + static ByteBuffer make_buffer(Vector const&); + }; + + struct [[gnu::packed]] Clipboard { + u32 type; + u8 data[]; + + static ByteBuffer make_buffer(ClipboardType, ReadonlyBytes); + }; + + struct [[gnu::packed]] ClipboardRequest { + u32 type; + + static ByteBuffer make_buffer(ClipboardType); + }; + +private: + int m_fd { -1 }; + RefPtr m_notifier; + ClipboardServerConnection& m_clipboard_connection; + + void on_message_received(); + void send_message(const ByteBuffer& buffer); + bool m_just_set_clip { false }; + void read_n(void* dest, size_t n); + static Message* initialize_headers(u8* data, size_t additional_data_size, MessageType type); +}; diff --git a/Userland/Services/SpiceAgent/main.cpp b/Userland/Services/SpiceAgent/main.cpp new file mode 100644 index 0000000000..f2540f7226 --- /dev/null +++ b/Userland/Services/SpiceAgent/main.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021, Kyle Pereira + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SpiceAgent.h" +#include +#include +#include +#include + +static constexpr auto SPICE_DEVICE = "/dev/hvc0p1"; + +int main() +{ + Core::EventLoop loop; + + if (pledge("unix rpath wpath stdio sendfd recvfd", nullptr) < 0) { + perror("pledge"); + return 1; + } + + if (unveil(SPICE_DEVICE, "rw") < 0) { + perror("unveil"); + return 1; + } + if (unveil("/tmp/portal/clipboard", "rw") < 0) { + perror("unveil"); + return 1; + } + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + + int serial_port_fd = open(SPICE_DEVICE, O_RDWR); + if (serial_port_fd < 0) { + dbgln("Couldn't open spice serial port!"); + return 1; + } + + auto conn = ClipboardServerConnection::construct(); + auto agent = SpiceAgent(serial_port_fd, conn); + + return loop.exec(); +}