mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-06 11:09:05 +03:00
SpiceAgent: Let's start rewriting the messaging system :^)
The old message system was very dependent on syscalls, and the overall structure made it hard to implement new features. The new message system is pretty expandible, where each message has its own dedicated class. As well as this, we now use Core::File and AK::Stream for reading and writing messages. Using AK::Stream also allows us to change the actual data source (in this case, Core::File) without having to update a whole lot of code in the future.
This commit is contained in:
parent
fd4f00ee91
commit
79c73dd260
Notes:
sideshowbarker
2024-07-16 20:05:14 +09:00
Author: https://github.com/caoimhebyrne Commit: https://github.com/SerenityOS/serenity/commit/79c73dd260 Pull-request: https://github.com/SerenityOS/serenity/pull/18834 Reviewed-by: https://github.com/ADKaster Reviewed-by: https://github.com/drunderscore Reviewed-by: https://github.com/supercomputer7
@ -5,6 +5,7 @@ serenity_component(
|
||||
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
Message.cpp
|
||||
SpiceAgent.cpp
|
||||
ConnectionToClipboardServer.cpp
|
||||
)
|
||||
|
55
Userland/Services/SpiceAgent/Message.cpp
Normal file
55
Userland/Services/SpiceAgent/Message.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "Message.h"
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <AK/Stream.h>
|
||||
#include <AK/String.h>
|
||||
|
||||
namespace SpiceAgent {
|
||||
|
||||
ErrorOr<AnnounceCapabilitiesMessage> AnnounceCapabilitiesMessage::read_from_stream(AK::Stream& stream)
|
||||
{
|
||||
// If this message is a capabilities request, we don't have to parse anything else.
|
||||
auto is_requesting = TRY(stream.read_value<u32>()) == 1;
|
||||
if (is_requesting) {
|
||||
return AnnounceCapabilitiesMessage(is_requesting);
|
||||
}
|
||||
|
||||
return Error::from_string_literal("Unexpected non-requesting announce capabilities message received!");
|
||||
}
|
||||
|
||||
ErrorOr<void> AnnounceCapabilitiesMessage::write_to_stream(AK::Stream& stream)
|
||||
{
|
||||
TRY(stream.write_value<u32>(is_request()));
|
||||
|
||||
// Each bit in this u32 indicates if a certain capability is enabled or not.
|
||||
u32 capabilities_bits = 0;
|
||||
for (auto capability : capabilities()) {
|
||||
// FIXME: At the moment, we only support up to 32 capabilities as the Spice protocol
|
||||
// only contains 17 capabilities.
|
||||
auto capability_value = to_underlying(capability);
|
||||
VERIFY(capability_value < 32);
|
||||
|
||||
capabilities_bits |= 1 << capability_value;
|
||||
}
|
||||
|
||||
TRY(stream.write_value(capabilities_bits));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<String> AnnounceCapabilitiesMessage::debug_description()
|
||||
{
|
||||
StringBuilder builder;
|
||||
TRY(builder.try_append("AnnounceCapabilities { "sv));
|
||||
TRY(builder.try_appendff("is_request = {}, ", is_request()));
|
||||
TRY(builder.try_appendff("capabilities.size() = {}", capabilities().size()));
|
||||
TRY(builder.try_append(" }"sv));
|
||||
return builder.to_string();
|
||||
}
|
||||
|
||||
}
|
101
Userland/Services/SpiceAgent/Message.h
Normal file
101
Userland/Services/SpiceAgent/Message.h
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
namespace SpiceAgent {
|
||||
|
||||
static constexpr u32 AGENT_PROTOCOL = 1;
|
||||
|
||||
// Used to communicate what the client/or server is capable of.
|
||||
// Not a lot of documentation is available for all of these, but the headers contain some information:
|
||||
// https://gitlab.freedesktop.org/spice/spice-protocol/-/blob/master/spice/vd_agent.h
|
||||
enum class Capability : u32 {
|
||||
MouseState = 0,
|
||||
MonitorsConfig,
|
||||
Reply,
|
||||
Clipboard,
|
||||
DisplayConfig,
|
||||
ClipboardByDemand,
|
||||
ClipboardSelection,
|
||||
SparseMonitorsConfig,
|
||||
GuestLineEndLF,
|
||||
GuestLineEndCRLF,
|
||||
MaxClipboard,
|
||||
AudioVolumeSync,
|
||||
MonitorsConfigPosition,
|
||||
FileTransferDisabled,
|
||||
FileTransferDetailedErrors,
|
||||
GraphicsCardInfo,
|
||||
ClipboardNoReleaseOnRegrab,
|
||||
ClipboardGrabSerial
|
||||
};
|
||||
|
||||
class Message {
|
||||
public:
|
||||
// The spice protocol headers contain a bit of documentation about these, but nothing major:
|
||||
// https://gitlab.freedesktop.org/spice/spice-protocol/-/blob/master/spice/vd_agent.h
|
||||
enum class Type : u32 {
|
||||
MouseState = 1,
|
||||
MonitorsConfig,
|
||||
Reply,
|
||||
Clipboard,
|
||||
DisplayConfig,
|
||||
AnnounceCapabilities,
|
||||
ClipboardGrab,
|
||||
ClipboardRequest,
|
||||
ClipboardRelease,
|
||||
FileTransferStart,
|
||||
FileTransferStatus,
|
||||
FileTransferData,
|
||||
Disconnected,
|
||||
MaxClipboard,
|
||||
VolumeSync,
|
||||
GraphicsDeviceInfo
|
||||
};
|
||||
|
||||
Message(Type type)
|
||||
: m_type(type)
|
||||
{
|
||||
}
|
||||
|
||||
Type type() { return m_type; }
|
||||
|
||||
virtual ErrorOr<String> debug_description() = 0;
|
||||
virtual ~Message() = default;
|
||||
|
||||
private:
|
||||
Type m_type;
|
||||
};
|
||||
|
||||
// Sent to the server to tell it what we are capable of.
|
||||
// See the Capabilities enum to see the available capabilities.
|
||||
class AnnounceCapabilitiesMessage : public Message {
|
||||
public:
|
||||
AnnounceCapabilitiesMessage(bool is_request, Vector<Capability> capabilities = {})
|
||||
: Message(Type::AnnounceCapabilities)
|
||||
, m_is_request(is_request)
|
||||
, m_capabilities(move(capabilities))
|
||||
{
|
||||
}
|
||||
|
||||
static ErrorOr<AnnounceCapabilitiesMessage> read_from_stream(AK::Stream& stream);
|
||||
|
||||
ErrorOr<void> write_to_stream(AK::Stream& stream);
|
||||
ErrorOr<String> debug_description() override;
|
||||
|
||||
bool is_request() const& { return m_is_request; }
|
||||
Vector<Capability> const& capabilities() { return m_capabilities; }
|
||||
|
||||
private:
|
||||
bool m_is_request { false };
|
||||
Vector<Capability> m_capabilities;
|
||||
};
|
||||
|
||||
}
|
65
Userland/Services/SpiceAgent/MessageHeader.h
Normal file
65
Userland/Services/SpiceAgent/MessageHeader.h
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Message.h"
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/Format.h>
|
||||
#include <AK/Forward.h>
|
||||
|
||||
namespace SpiceAgent {
|
||||
|
||||
// An incoming or outgoing message header.
|
||||
// This contains information about the message, like how long it is, the type, etc.
|
||||
class [[gnu::packed]] MessageHeader {
|
||||
public:
|
||||
MessageHeader(Message::Type type, u32 data_size, u32 protocol_version = AGENT_PROTOCOL, u64 opaque = 0)
|
||||
: m_protocol_version(protocol_version)
|
||||
, m_type(type)
|
||||
, m_opaque(opaque)
|
||||
, m_data_size(data_size)
|
||||
{
|
||||
}
|
||||
|
||||
Message::Type type() const { return m_type; };
|
||||
u32 data_size() const { return m_data_size; };
|
||||
u32 protocol_version() const { return m_protocol_version; };
|
||||
u64 opaque() const { return m_opaque; };
|
||||
|
||||
private:
|
||||
// The protocol version being used.
|
||||
u32 m_protocol_version { AGENT_PROTOCOL };
|
||||
|
||||
// The message type present in `data`.
|
||||
Message::Type m_type { Message::Type::MouseState };
|
||||
|
||||
// A placeholder for message types which only need to pass a single integer as message data,
|
||||
// for message types which have more data it is always set to 0.
|
||||
u64 m_opaque { 0 };
|
||||
|
||||
// The size of the data in the message following this header.
|
||||
u32 m_data_size { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
template<>
|
||||
struct AK::Traits<SpiceAgent::MessageHeader> : public AK::GenericTraits<SpiceAgent::MessageHeader> {
|
||||
static constexpr bool is_trivially_serializable() { return true; }
|
||||
};
|
||||
|
||||
namespace AK {
|
||||
template<>
|
||||
struct Formatter<SpiceAgent::MessageHeader> : Formatter<FormatString> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, SpiceAgent::MessageHeader const& header)
|
||||
{
|
||||
return Formatter<FormatString>::format(builder,
|
||||
"MessageHeader {{ protocol_version = {}, type = {}, opaque = {}, data_size = {} }}"sv,
|
||||
header.protocol_version(), to_underlying(header.type()), header.opaque(), header.data_size());
|
||||
}
|
||||
};
|
||||
}
|
@ -1,310 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "SpiceAgent.h"
|
||||
#include "ConnectionToClipboardServer.h"
|
||||
#include <AK/DeprecatedString.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/ImageFormats/BMPLoader.h>
|
||||
#include <LibGfx/ImageFormats/JPEGLoader.h>
|
||||
#include <LibGfx/ImageFormats/PNGLoader.h>
|
||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
||||
#include <memory.h>
|
||||
#include <unistd.h>
|
||||
|
||||
SpiceAgent::SpiceAgent(int fd, ConnectionToClipboardServer& connection)
|
||||
: m_fd(fd)
|
||||
, m_clipboard_connection(connection)
|
||||
namespace SpiceAgent {
|
||||
|
||||
ErrorOr<NonnullOwnPtr<SpiceAgent>> SpiceAgent::create(StringView device_path)
|
||||
{
|
||||
m_notifier = Core::Notifier::construct(fd, Core::Notifier::Type::Read);
|
||||
auto device = TRY(Core::File::open(device_path, Core::File::OpenMode::ReadWrite | Core::File::OpenMode::Nonblocking));
|
||||
auto clipboard_connection = TRY(ConnectionToClipboardServer::try_create());
|
||||
|
||||
return try_make<SpiceAgent>(move(device), clipboard_connection, Vector { Capability::ClipboardByDemand });
|
||||
}
|
||||
|
||||
SpiceAgent::SpiceAgent(NonnullOwnPtr<Core::File> spice_device, ConnectionToClipboardServer& clipboard_connection, Vector<Capability> const& capabilities)
|
||||
: m_spice_device(move(spice_device))
|
||||
, m_clipboard_connection(clipboard_connection)
|
||||
, m_capabilities(capabilities)
|
||||
{
|
||||
m_notifier = Core::Notifier::construct(
|
||||
m_spice_device->fd(),
|
||||
Core::Notifier::Type::Read);
|
||||
|
||||
m_notifier->on_activation = [this] {
|
||||
auto result = on_message_received();
|
||||
if (result.is_error()) {
|
||||
warnln("Failed to handle message: {}", result.release_error());
|
||||
dbgln("Failed to handle message: {}", result.release_error());
|
||||
}
|
||||
};
|
||||
|
||||
m_clipboard_connection.on_data_changed = [this] {
|
||||
if (m_just_set_clip) {
|
||||
m_just_set_clip = false;
|
||||
return;
|
||||
}
|
||||
auto mime = m_clipboard_connection.get_clipboard_data().mime_type();
|
||||
Optional<ClipboardType> type = mime_type_to_clipboard_type(mime);
|
||||
if (!type.has_value())
|
||||
return;
|
||||
|
||||
auto grab_buffer = ClipboardGrab::make_buffer({ *type });
|
||||
send_message(grab_buffer);
|
||||
};
|
||||
|
||||
auto buffer = AnnounceCapabilities::make_buffer(true, { Capability::ClipboardByDemand });
|
||||
send_message(buffer);
|
||||
}
|
||||
|
||||
Optional<SpiceAgent::ClipboardType> SpiceAgent::mime_type_to_clipboard_type(DeprecatedString const& mime)
|
||||
ErrorOr<void> SpiceAgent::start()
|
||||
{
|
||||
if (mime == "text/plain")
|
||||
return ClipboardType::Text;
|
||||
|
||||
if (mime == "image/jpeg")
|
||||
return ClipboardType::JPEG;
|
||||
|
||||
if (mime == "image/bmp")
|
||||
return ClipboardType::BMP;
|
||||
|
||||
if (mime == "image/png" || mime == "image/x-serenityos")
|
||||
return ClipboardType::PNG;
|
||||
// The server usually requests this from us anyways, but there's no harm in sending it.
|
||||
auto capabilities_message = AnnounceCapabilitiesMessage(false, m_capabilities);
|
||||
TRY(this->send_message(capabilities_message));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> SpiceAgent::on_message_received()
|
||||
{
|
||||
ChunkHeader header {};
|
||||
read_n(&header, sizeof(header));
|
||||
auto buffer = TRY(this->read_message_buffer());
|
||||
auto stream = FixedMemoryStream(buffer.bytes());
|
||||
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(header.size));
|
||||
read_n(buffer.data(), buffer.size());
|
||||
|
||||
auto* message = reinterpret_cast<Message*>(buffer.data());
|
||||
switch (message->type) {
|
||||
case (u32)MessageType::AnnounceCapabilities: {
|
||||
auto* capabilities_message = reinterpret_cast<AnnounceCapabilities*>(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* request_message = reinterpret_cast<ClipboardRequest*>(message->data);
|
||||
auto clipboard = m_clipboard_connection.get_clipboard_data();
|
||||
auto& mime = clipboard.mime_type();
|
||||
|
||||
ByteBuffer backing_byte_buffer;
|
||||
ReadonlyBytes bytes;
|
||||
|
||||
if (mime == "image/x-serenityos") {
|
||||
auto bitmap = m_clipboard_connection.get_bitmap();
|
||||
backing_byte_buffer = MUST(Gfx::PNGWriter::encode(*bitmap));
|
||||
bytes = backing_byte_buffer;
|
||||
} else {
|
||||
auto data = clipboard.data();
|
||||
bytes = { data.data<void>(), data.size() };
|
||||
}
|
||||
|
||||
auto clipboard_buffer = Clipboard::make_buffer((ClipboardType)request_message->type, bytes);
|
||||
send_message(clipboard_buffer);
|
||||
break;
|
||||
}
|
||||
case (u32)MessageType::ClipboardGrab: {
|
||||
auto* grab_message = reinterpret_cast<ClipboardGrab*>(message->data);
|
||||
auto found_type = ClipboardType::None;
|
||||
|
||||
for (size_t i = 0; i < (message->size / 4); i++) {
|
||||
auto type = (ClipboardType)grab_message->types[i];
|
||||
if (found_type == ClipboardType::None) {
|
||||
found_type = static_cast<ClipboardType>(type);
|
||||
} else if (found_type == ClipboardType::Text) {
|
||||
switch (type) {
|
||||
case ClipboardType::PNG:
|
||||
case ClipboardType::BMP:
|
||||
case ClipboardType::JPEG:
|
||||
found_type = type;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found_type == ClipboardType::None)
|
||||
auto header = TRY(stream.read_value<MessageHeader>());
|
||||
switch (header.type()) {
|
||||
case Message::Type::AnnounceCapabilities: {
|
||||
auto message = TRY(AnnounceCapabilitiesMessage::read_from_stream(stream));
|
||||
if (!message.is_request())
|
||||
return {};
|
||||
|
||||
auto request_buffer = ClipboardRequest::make_buffer(found_type);
|
||||
send_message(request_buffer);
|
||||
dbgln("The spice server has requested our capabilities");
|
||||
|
||||
auto capabilities_message = AnnounceCapabilitiesMessage(false, m_capabilities);
|
||||
TRY(this->send_message(capabilities_message));
|
||||
|
||||
break;
|
||||
}
|
||||
case (u32)MessageType::Clipboard: {
|
||||
auto* clipboard_message = reinterpret_cast<Clipboard*>(message->data);
|
||||
auto type = (ClipboardType)clipboard_message->type;
|
||||
auto data_buffer = TRY(ByteBuffer::create_uninitialized(message->size - sizeof(u32)));
|
||||
|
||||
auto const 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;
|
||||
if (type == ClipboardType::Text) {
|
||||
if (data_buffer.is_empty()) {
|
||||
m_clipboard_connection.async_set_clipboard_data({}, "text/plain", {});
|
||||
} else {
|
||||
auto anon_buffer = TRY(Core::AnonymousBuffer::create_with_size(data_buffer.size()));
|
||||
memcpy(anon_buffer.data<void>(), data_buffer.data(), data_buffer.size());
|
||||
m_clipboard_connection.async_set_clipboard_data(anon_buffer, "text/plain", {});
|
||||
}
|
||||
return {};
|
||||
} else {
|
||||
ErrorOr<Gfx::ImageFrameDescriptor> frame_or_error = Gfx::ImageFrameDescriptor {};
|
||||
if (type == ClipboardType::PNG) {
|
||||
if (Gfx::PNGImageDecoderPlugin::sniff({ data_buffer.data(), data_buffer.size() })) {
|
||||
auto png_decoder = TRY(Gfx::PNGImageDecoderPlugin::create({ data_buffer.data(), data_buffer.size() }));
|
||||
if (!png_decoder->initialize().is_error())
|
||||
frame_or_error = png_decoder->frame(0);
|
||||
}
|
||||
} else if (type == ClipboardType::BMP) {
|
||||
if (Gfx::BMPImageDecoderPlugin::sniff({ data_buffer.data(), data_buffer.size() })) {
|
||||
auto bmp_decoder = TRY(Gfx::BMPImageDecoderPlugin::create({ data_buffer.data(), data_buffer.size() }));
|
||||
if (!bmp_decoder->initialize().is_error())
|
||||
frame_or_error = bmp_decoder->frame(0);
|
||||
}
|
||||
} else if (type == ClipboardType::JPEG) {
|
||||
if (Gfx::JPEGImageDecoderPlugin::sniff({ data_buffer.data(), data_buffer.size() })) {
|
||||
auto jpeg_decoder = TRY(Gfx::JPEGImageDecoderPlugin::create({ data_buffer.data(), data_buffer.size() }));
|
||||
if (!jpeg_decoder->initialize().is_error())
|
||||
frame_or_error = jpeg_decoder->frame(0);
|
||||
}
|
||||
} else {
|
||||
dbgln("Unknown clipboard type: {}", (u32)type);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto const& bitmap = frame_or_error.value().image;
|
||||
m_clipboard_connection.set_bitmap(*bitmap);
|
||||
}
|
||||
// We ignore certain messages to prevent it from clogging up the logs.
|
||||
case Message::Type::MonitorsConfig:
|
||||
dbgln_if(SPICE_AGENT_DEBUG, "Ignored message: {}", header);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
dbgln("Unhandled message type {}", message->type);
|
||||
dbgln("Unknown message received: {}", header);
|
||||
break;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void SpiceAgent::read_n(void* dest, size_t n)
|
||||
ErrorOr<ByteBuffer> SpiceAgent::read_message_buffer()
|
||||
{
|
||||
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;
|
||||
}
|
||||
auto port = TRY(m_spice_device->read_value<Port>());
|
||||
if (port != Port::Client) {
|
||||
return Error::from_string_literal("Attempted to read message bytes from a port that wasn't meant for the client!");
|
||||
}
|
||||
}
|
||||
|
||||
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, Vector<Capability> const& capabilities)
|
||||
{
|
||||
size_t required_size = sizeof(ChunkHeader) + sizeof(Message) + sizeof(AnnounceCapabilities);
|
||||
auto buffer = ByteBuffer::create_uninitialized(required_size).release_value_but_fixme_should_propagate_errors(); // FIXME: Handle possible OOM situation.
|
||||
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(Vector<ClipboardType> const& 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).release_value_but_fixme_should_propagate_errors(); // FIXME: Handle possible OOM situation.
|
||||
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).release_value_but_fixme_should_propagate_errors(); // FIXME: Handle possible OOM situation.
|
||||
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).release_value_but_fixme_should_propagate_errors(); // FIXME: Handle possible OOM situation.
|
||||
u8* data = buffer.data();
|
||||
|
||||
auto* message = initialize_headers(data, data_size, MessageType::ClipboardRequest);
|
||||
new (message->data) ClipboardRequest {
|
||||
.type = (u32)type
|
||||
};
|
||||
auto size = TRY(m_spice_device->read_value<u32>());
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(size));
|
||||
TRY(m_spice_device->read_until_filled(buffer));
|
||||
|
||||
return buffer;
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
@ -7,123 +8,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "ConnectionToClipboardServer.h"
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include "Message.h"
|
||||
#include "MessageHeader.h"
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
|
||||
namespace SpiceAgent {
|
||||
|
||||
class SpiceAgent {
|
||||
public:
|
||||
SpiceAgent(int fd, ConnectionToClipboardServer&);
|
||||
|
||||
static constexpr u32 AGENT_PROTOCOL = 1;
|
||||
enum class Port {
|
||||
// Indicates where the message has come from.
|
||||
enum class Port : u32 {
|
||||
Client = 1,
|
||||
|
||||
// There are currently no messages which are meant for the server, so all messages sent by the agent (us) with this port are discarded.
|
||||
Server
|
||||
};
|
||||
|
||||
struct [[gnu::packed]] ChunkHeader {
|
||||
u32 port {};
|
||||
u32 size {};
|
||||
};
|
||||
static ErrorOr<NonnullOwnPtr<SpiceAgent>> create(StringView device_path);
|
||||
SpiceAgent(NonnullOwnPtr<Core::File> spice_device, ConnectionToClipboardServer& clipboard_connection, Vector<Capability> const& capabilities);
|
||||
|
||||
struct [[gnu::packed]] Message {
|
||||
u32 protocol;
|
||||
u32 type;
|
||||
u64 opaque;
|
||||
u32 size;
|
||||
u8 data[];
|
||||
};
|
||||
ErrorOr<void> start();
|
||||
|
||||
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,
|
||||
};
|
||||
template<typename T>
|
||||
ErrorOr<void> send_message(T message)
|
||||
{
|
||||
// Attempt to write the message's data to a stream.
|
||||
auto message_stream = AK::AllocatingMemoryStream();
|
||||
TRY(message.write_to_stream(message_stream));
|
||||
|
||||
enum class Capability {
|
||||
MouseState = 0,
|
||||
MonitorsConfig,
|
||||
Reply,
|
||||
Clipboard,
|
||||
DisplayConfig,
|
||||
ClipboardByDemand,
|
||||
ClipboardSelection,
|
||||
SparseMonitorsConfig,
|
||||
GuestLineEndLF,
|
||||
GuestLineEndCRLF,
|
||||
MaxClipboard,
|
||||
AudioVolumeSync,
|
||||
MonitorsConfigPosition,
|
||||
FileTransferDisabled,
|
||||
FileTransferDetailedErrors,
|
||||
GraphicsCardInfo,
|
||||
ClipboardNoReleaseOnRegrab,
|
||||
ClipboardGrabSerial,
|
||||
__End,
|
||||
};
|
||||
// Create a header to be sent.
|
||||
auto header_stream = AK::AllocatingMemoryStream();
|
||||
auto header = MessageHeader(message.type(), message_stream.used_buffer_size());
|
||||
TRY(header_stream.write_value(header));
|
||||
|
||||
enum class ClipboardType {
|
||||
None = 0,
|
||||
Text,
|
||||
PNG,
|
||||
BMP,
|
||||
TIFF,
|
||||
JPEG,
|
||||
FileList,
|
||||
__Count
|
||||
};
|
||||
// Currently, there are no messages from the agent which are meant for the server.
|
||||
// So, all messages sent by the agent with a port of Port::Server get dropped silently.
|
||||
TRY(m_spice_device->write_value(Port::Client));
|
||||
|
||||
constexpr static size_t CAPABILITIES_SIZE = ((size_t)Capability::__End + 31) / 32;
|
||||
// The length of the subsequent data.
|
||||
auto length = header_stream.used_buffer_size() + message_stream.used_buffer_size();
|
||||
TRY(m_spice_device->write_value<u32>(length));
|
||||
|
||||
struct [[gnu::packed]] AnnounceCapabilities {
|
||||
u32 request;
|
||||
u32 caps[CAPABILITIES_SIZE];
|
||||
// The message's header.
|
||||
TRY(m_spice_device->write_until_depleted(TRY(header_stream.read_until_eof())));
|
||||
|
||||
static ByteBuffer make_buffer(bool request, Vector<Capability> const& capabilities);
|
||||
};
|
||||
// The message content.
|
||||
TRY(m_spice_device->write_until_depleted(TRY(message_stream.read_until_eof())));
|
||||
|
||||
struct [[gnu::packed]] ClipboardGrab {
|
||||
u32 types[0];
|
||||
|
||||
static ByteBuffer make_buffer(Vector<ClipboardType> 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);
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
int m_fd { -1 };
|
||||
RefPtr<Core::Notifier> m_notifier;
|
||||
NonnullOwnPtr<Core::File> m_spice_device;
|
||||
ConnectionToClipboardServer& m_clipboard_connection;
|
||||
Vector<Capability> m_capabilities;
|
||||
|
||||
RefPtr<Core::Notifier> m_notifier;
|
||||
|
||||
ErrorOr<void> on_message_received();
|
||||
|
||||
void send_message(ByteBuffer const& 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);
|
||||
static Optional<ClipboardType> mime_type_to_clipboard_type(DeprecatedString const& mime);
|
||||
ErrorOr<ByteBuffer> read_message_buffer();
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
* Copyright (c) 2023, Caoimhe Byrne <caoimhebyrne06@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
@ -16,15 +17,16 @@ ErrorOr<int> serenity_main(Main::Arguments)
|
||||
{
|
||||
Core::EventLoop loop;
|
||||
|
||||
TRY(Core::System::pledge("unix rpath wpath stdio sendfd recvfd"));
|
||||
TRY(Core::System::unveil(SPICE_DEVICE, "rw"sv));
|
||||
// FIXME: Make Core::File support reading and writing, but without creating:
|
||||
// By default, Core::File opens the file descriptor with O_CREAT when using OpenMode::Write (and subsequently, OpenMode::ReadWrite).
|
||||
// To minimise confusion for people that have already used Core::File, we can probably just do `OpenMode::ReadWrite | OpenMode::DontCreate`.
|
||||
TRY(Core::System::pledge("unix rpath wpath stdio sendfd recvfd cpath"));
|
||||
TRY(Core::System::unveil(SPICE_DEVICE, "rwc"sv));
|
||||
TRY(Core::System::unveil("/tmp/session/%sid/portal/clipboard", "rw"));
|
||||
TRY(Core::System::unveil(nullptr, nullptr));
|
||||
|
||||
int serial_port_fd = TRY(Core::System::open(SPICE_DEVICE, O_RDWR));
|
||||
|
||||
auto conn = TRY(ConnectionToClipboardServer::try_create());
|
||||
auto agent = SpiceAgent(serial_port_fd, conn);
|
||||
auto agent = TRY(SpiceAgent::SpiceAgent::create(SPICE_DEVICE));
|
||||
TRY(agent->start());
|
||||
|
||||
return loop.exec();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user