mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 19:47:46 +00: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
7 changed files with 320 additions and 370 deletions
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue