mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 15:40:19 +00:00
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
This commit is contained in:
parent
42c5df7256
commit
d4bb6a1a1e
Notes:
sideshowbarker
2024-07-18 09:05:12 +09:00
Author: https://github.com/X-yl Commit: https://github.com/SerenityOS/serenity/commit/d4bb6a1a1ee Pull-request: https://github.com/SerenityOS/serenity/pull/8690 Reviewed-by: https://github.com/IdanHo Reviewed-by: https://github.com/ccapitalK Reviewed-by: https://github.com/tomuta
8 changed files with 432 additions and 0 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
13
Userland/Services/SpiceAgent/CMakeLists.txt
Normal file
13
Userland/Services/SpiceAgent/CMakeLists.txt
Normal file
|
@ -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)
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ClipboardServerConnection.h"
|
||||
|
31
Userland/Services/SpiceAgent/ClipboardServerConnection.h
Normal file
31
Userland/Services/SpiceAgent/ClipboardServerConnection.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <Clipboard/ClipboardClientEndpoint.h>
|
||||
#include <Clipboard/ClipboardServerEndpoint.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibIPC/ServerConnection.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
class ClipboardServerConnection final
|
||||
: public IPC::ServerConnection<ClipboardClientEndpoint, ClipboardServerEndpoint>
|
||||
, public ClipboardClientEndpoint {
|
||||
C_OBJECT(ClipboardServerConnection);
|
||||
|
||||
Function<void()> on_data_changed;
|
||||
|
||||
private:
|
||||
ClipboardServerConnection()
|
||||
: IPC::ServerConnection<ClipboardClientEndpoint, ClipboardServerEndpoint>(*this, "/tmp/portal/clipboard")
|
||||
{
|
||||
}
|
||||
virtual void clipboard_data_changed(String const&) override
|
||||
{
|
||||
on_data_changed();
|
||||
}
|
||||
};
|
202
Userland/Services/SpiceAgent/SpiceAgent.cpp
Normal file
202
Userland/Services/SpiceAgent/SpiceAgent.cpp
Normal file
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "SpiceAgent.h"
|
||||
#include "ClipboardServerConnection.h"
|
||||
#include <AK/String.h>
|
||||
#include <LibC/memory.h>
|
||||
#include <LibC/unistd.h>
|
||||
|
||||
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<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 clip_data = m_clipboard_connection.get_clipboard_data().data();
|
||||
ByteBuffer byte_buffer = ByteBuffer::copy(clip_data.data<void>(), 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<Clipboard*>(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<void>(), 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<Capability>& 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<ClipboardType>& 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;
|
||||
}
|
127
Userland/Services/SpiceAgent/SpiceAgent.h
Normal file
127
Userland/Services/SpiceAgent/SpiceAgent.h
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ClipboardServerConnection.h"
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
|
||||
#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<Capability>& capabilities);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
private:
|
||||
int m_fd { -1 };
|
||||
RefPtr<Core::Notifier> 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);
|
||||
};
|
47
Userland/Services/SpiceAgent/main.cpp
Normal file
47
Userland/Services/SpiceAgent/main.cpp
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Kyle Pereira <kyle@xylepereira.me>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "SpiceAgent.h"
|
||||
#include <AK/Format.h>
|
||||
#include <LibC/fcntl.h>
|
||||
#include <LibC/unistd.h>
|
||||
#include <LibIPC/ServerConnection.h>
|
||||
|
||||
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();
|
||||
}
|
Loading…
Reference in a new issue