From a67034b14f76775f9459abe7f699e3a3800e104f Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 30 Sep 2026 20:55:56 +1000 Subject: [PATCH] [client/idd] network: add recovery, cursor, and control --- client/transports/Network/CMakeLists.txt | 3 + client/transports/Network/connection.c | 61 +- client/transports/Network/control.c | 369 +++++++ client/transports/Network/control.h | 44 + client/transports/Network/cursor.c | 813 +++++++++++++++ client/transports/Network/cursor.h | 46 + client/transports/Network/network.c | 168 +++- client/transports/Network/recovery.c | 575 +++++++++++ client/transports/Network/recovery.h | 45 + .../transport/network/CNetworkServer.cpp | 937 +++++++++++++++++- .../transport/network/CNetworkSession.cpp | 205 +++- idd/LGIdd/transport/network/CNetworkSession.h | 66 +- .../transport/network/CNetworkTransport.cpp | 5 +- .../transport/network/CNetworkTransport.h | 3 +- repos/LGProtocol | 2 +- 15 files changed, 3241 insertions(+), 101 deletions(-) create mode 100644 client/transports/Network/control.c create mode 100644 client/transports/Network/control.h create mode 100644 client/transports/Network/cursor.c create mode 100644 client/transports/Network/cursor.h create mode 100644 client/transports/Network/recovery.c create mode 100644 client/transports/Network/recovery.h diff --git a/client/transports/Network/CMakeLists.txt b/client/transports/Network/CMakeLists.txt index faff2de0..2d004be5 100644 --- a/client/transports/Network/CMakeLists.txt +++ b/client/transports/Network/CMakeLists.txt @@ -7,9 +7,12 @@ endif() set(TRANSPORT_NETWORK_SOURCES connection.c + control.c + cursor.c input.c known_hosts.c network.c + recovery.c ) if(WIN32) diff --git a/client/transports/Network/connection.c b/client/transports/Network/connection.c index 46dd134a..561a08d2 100644 --- a/client/transports/Network/connection.c +++ b/client/transports/Network/connection.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -49,7 +50,8 @@ #define NETWORK_KEEPALIVE_INTERVAL_MS 1000U #define NETWORK_INITIAL_RTT_MS 1U #define NETWORK_MAX_PEER_STREAMS 16U -#define NETWORK_MAX_CONTROL_PAYLOAD (1024U * 1024U) +#define NETWORK_MAX_CONTROL_PAYLOAD \ + (LG_NET_CURSOR_SHAPE_HEADER_WIRE_SIZE + LG_NET_CURSOR_MAX_SHAPE_LENGTH) #define NETWORK_MAX_CONTROL_BUFFER \ (2U * (LG_NET_ENVELOPE_WIRE_SIZE + NETWORK_MAX_CONTROL_PAYLOAD)) #define NETWORK_MAX_DATAGRAM_PAYLOAD 1136U @@ -98,7 +100,7 @@ struct NetworkConnection NetworkConnectionStateFn state; void * opaque; - LGNetCapability offeredCapabilities[LG_NET_SERVICE_USB + 1U]; + LGNetCapability offeredCapabilities[LG_NET_SERVICE_CONTROL + 1U]; uint32_t offeredCapabilityCount; uint16_t offeredServices; @@ -128,13 +130,13 @@ struct NetworkConnection uint16_t receivedServices; uint16_t reliableServices; uint16_t datagramServices; - uint16_t serviceVersions[LG_NET_SERVICE_USB + 1U]; + uint16_t serviceVersions[LG_NET_SERVICE_CONTROL + 1U]; uint32_t maxPayloadLength; uint32_t maxDatagramPayload; uint16_t maxDatagramSendLength; bool datagramSendEnabled; - uint32_t serviceMaxPayload[LG_NET_SERVICE_USB + 1U]; - uint32_t serviceMaxDatagram[LG_NET_SERVICE_USB + 1U]; + uint32_t serviceMaxPayload[LG_NET_SERVICE_CONTROL + 1U]; + uint32_t serviceMaxDatagram[LG_NET_SERVICE_CONTROL + 1U]; uint8_t clientHelloNonce[LG_NET_HELLO_NONCE_LENGTH]; uint8_t serverHelloNonce[LG_NET_HELLO_NONCE_LENGTH]; @@ -185,7 +187,8 @@ static bool networkCapabilityOfferValid( if (!lgNetCapabilityValid(capability) || !version || capability->service <= LG_NET_SERVICE_CORE || - capability->service >= LG_NET_SERVICE_USB || + capability->service > LG_NET_SERVICE_CONTROL || + capability->service == LG_NET_SERVICE_USB || capability->flags & LG_NET_CAPABILITY_SELECTED || capability->versionMin < version->minimum || capability->versionMax > version->maximum || @@ -196,6 +199,12 @@ static bool networkCapabilityOfferValid( return true; } +static bool networkConnectionServiceValid(LGNetService service) +{ + return service > LG_NET_SERVICE_CORE && + service <= LG_NET_SERVICE_CONTROL && service != LG_NET_SERVICE_USB; +} + static bool networkBytesNonzero(const uint8_t * data, size_t size) { if (!data) @@ -265,6 +274,8 @@ static bool networkConnectionSendPacket(NetworkConnection * this, LG_NET_MESSAGE_VERSION_CURRENT); if (payloadLength > NETWORK_MAX_CONTROL_PAYLOAD || (payloadLength && !payload) || + (service != LG_NET_SERVICE_CORE && + !networkConnectionServiceValid(service)) || (flags & (LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_EARLY_DATA)) || !lgNetEnvelopeRouteValid(&routeEnvelope, LG_NET_ROLE_CLIENT, @@ -364,8 +375,10 @@ static bool networkConnectionSendHello(NetworkConnection * this) goto cleanup; for (LGNetService service = LG_NET_SERVICE_RECOVERY; - service < LG_NET_SERVICE_USB; ++service) + service <= LG_NET_SERVICE_CONTROL; ++service) { + if (service == LG_NET_SERVICE_USB) + continue; if (!(this->offeredServices & (UINT16_C(1) << service))) continue; @@ -426,8 +439,7 @@ static bool networkConnectionHandleCapability(NetworkConnection * this, packet->payload, packet->payloadLength) != LG_NET_PARSE_OK) return false; - if (capability.service <= LG_NET_SERVICE_CORE || - capability.service >= LG_NET_SERVICE_USB) + if (!networkConnectionServiceValid(capability.service)) return false; LG_LOCK(this->lock); @@ -486,8 +498,10 @@ static bool networkConnectionRequiredCapabilitiesSelected( const NetworkConnection * this) { for (LGNetService service = LG_NET_SERVICE_RECOVERY; - service < LG_NET_SERVICE_USB; ++service) + service <= LG_NET_SERVICE_CONTROL; ++service) { + if (service == LG_NET_SERVICE_USB) + continue; const uint16_t serviceMask = UINT16_C(1) << service; if ((this->offeredServices & serviceMask) && (this->offeredCapabilities[service].flags & @@ -518,6 +532,11 @@ static void networkConnectionSetSessionFeatures(NetworkConnection * this) this->session.features |= LG_TRANSPORT_FEATURE_CLIPBOARD; if (services & (UINT16_C(1) << LG_NET_SERVICE_AUDIO)) this->session.features |= LG_TRANSPORT_FEATURE_AUDIO; + if (services & (UINT16_C(1) << LG_NET_SERVICE_CONTROL)) + this->session.features |= + LG_TRANSPORT_FEATURE_SET_CURSOR_POS | + LG_TRANSPORT_FEATURE_WINDOW_SIZE | + LG_TRANSPORT_FEATURE_FRAME_SCHEDULE; } static bool networkConnectionHandleAuthChallenge(NetworkConnection * this, @@ -899,7 +918,8 @@ static bool networkConnectionHandlePacket(NetworkConnection * this, if (packet->envelope.service == LG_NET_SERVICE_CORE) return networkConnectionHandleCore(this, packet); - if (!atomic_load_explicit(&this->sessionValid, memory_order_acquire)) + if (!networkConnectionServiceValid(packet->envelope.service) || + !atomic_load_explicit(&this->sessionValid, memory_order_acquire)) return false; LG_LOCK(this->lock); @@ -930,12 +950,16 @@ static bool networkConnectionValidateControl(NetworkConnection * this, maxPayloadLength = this->maxPayloadLength; LG_UNLOCK(this->lock); + const bool intermediate = + (packet->envelope.service == LG_NET_SERVICE_RECOVERY && + packet->envelope.messageType == LG_NET_RECOVERY_MESSAGE_STATUS) || + (packet->envelope.service == LG_NET_SERVICE_CONTROL && + packet->envelope.messageType == LG_NET_CONTROL_MESSAGE_STATUS); return packet->envelope.headerSize == LG_NET_ENVELOPE_WIRE_SIZE && packet->payloadLength <= maxPayloadLength && packet->envelope.messageVersion == LG_NET_MESSAGE_VERSION_CURRENT && - (packet->envelope.flags & - (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL)) == - (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL) && + (packet->envelope.flags & LG_NET_ENVELOPE_RELIABLE) && + (intermediate || (packet->envelope.flags & LG_NET_ENVELOPE_FINAL)) && !(packet->envelope.flags & (LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_EARLY_DATA)) && lgNetEnvelopeRouteValid(&packet->envelope, LG_NET_ROLE_SERVER, @@ -956,6 +980,8 @@ static bool networkConnectionDecodeDatagram(NetworkConnection * this, if (!atomic_load_explicit( &this->sessionValid, memory_order_acquire) || + (packet->envelope.service != LG_NET_SERVICE_CORE && + !networkConnectionServiceValid(packet->envelope.service)) || packet->envelope.headerSize != LG_NET_ENVELOPE_WIRE_SIZE || !packet->envelope.sequence || packet->envelope.messageVersion != LG_NET_MESSAGE_VERSION_CURRENT || @@ -1645,7 +1671,7 @@ bool networkConnectionServiceAvailable(NetworkConnection * this, LGNetService service, LGNetCapabilityFlags required) { if (!networkConnectionValid(this) || - service <= LG_NET_SERVICE_CORE || service > LG_NET_SERVICE_USB || + !networkConnectionServiceValid(service) || required & ~(LG_NET_CAPABILITY_RELIABLE | LG_NET_CAPABILITY_DATAGRAM)) return false; @@ -1666,7 +1692,7 @@ bool networkConnectionServiceVersion(NetworkConnection * this, LGNetService service, uint16_t * version) { if (!version || !networkConnectionValid(this) || - service <= LG_NET_SERVICE_CORE || service >= LG_NET_SERVICE_USB) + !networkConnectionServiceValid(service)) return false; LG_LOCK(this->lock); @@ -1703,7 +1729,8 @@ LG_TransportStatus networkConnectionSendDatagram(NetworkConnection * this, LGNetEnvelope routeEnvelope; lgNetEnvelopeInit(&routeEnvelope, service, messageType, serviceVersion, LG_NET_MESSAGE_VERSION_CURRENT); - if ((payloadLength && !payload) || service == LG_NET_SERVICE_CORE || + if ((payloadLength && !payload) || + !networkConnectionServiceValid(service) || flags & (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_EARLY_DATA) || !lgNetEnvelopeRouteValid(&routeEnvelope, LG_NET_ROLE_CLIENT, diff --git a/client/transports/Network/control.c b/client/transports/Network/control.c new file mode 100644 index 00000000..c99becfa --- /dev/null +++ b/client/transports/Network/control.c @@ -0,0 +1,369 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "control.h" + +#include "common/locking.h" + +#include + +#include +#include +#include + +#define NETWORK_CONTROL_STATUS_CAPACITY 64U + +struct NetworkControlEntry +{ + uint64_t id; + LGNetControlStatusCode status; + bool used; +}; + +struct NetworkControl +{ + NetworkConnection * connection; + LG_Lock lock; + + bool connected; + bool available; + uint16_t serviceVersion; + uint64_t nextID; + unsigned nextEntry; + + struct NetworkControlEntry entries[NETWORK_CONTROL_STATUS_CAPACITY]; +}; + +static uint64_t networkControlNextID(uint64_t id) +{ + ++id; + return id ? id : 1U; +} + +static bool networkControlTerminal(LGNetControlStatusCode status) +{ + return status != LG_NET_CONTROL_STATUS_PENDING; +} + +static struct NetworkControlEntry * networkControlFindNL( + NetworkControl * control, uint64_t id) +{ + for (size_t index = 0; index < NETWORK_CONTROL_STATUS_CAPACITY; ++index) + if (control->entries[index].used && control->entries[index].id == id) + return &control->entries[index]; + return NULL; +} + +static struct NetworkControlEntry * networkControlReserveNL( + NetworkControl * control, uint64_t id) +{ + for (size_t offset = 0; + offset < NETWORK_CONTROL_STATUS_CAPACITY; ++offset) + { + const unsigned index = + (control->nextEntry + offset) % NETWORK_CONTROL_STATUS_CAPACITY; + struct NetworkControlEntry * entry = &control->entries[index]; + if (entry->used && !networkControlTerminal(entry->status)) + continue; + + *entry = (struct NetworkControlEntry) + { + .id = id, + .status = LG_NET_CONTROL_STATUS_PENDING, + .used = true, + }; + control->nextEntry = + (index + 1U) % NETWORK_CONTROL_STATUS_CAPACITY; + return entry; + } + return NULL; +} + +bool networkControlCreate(NetworkControl ** result, + NetworkConnection * connection) +{ + if (!result || !connection) + return false; + + NetworkControl * control = calloc(1, sizeof(*control)); + if (!control) + return false; + + LG_LOCK_INIT(control->lock); + control->connection = connection; + *result = control; + return true; +} + +void networkControlDestroy(NetworkControl ** result) +{ + if (!result || !*result) + return; + + NetworkControl * control = *result; + LG_LOCK_FREE(control->lock); + free(control); + *result = NULL; +} + +void networkControlConnectionState(NetworkControl * control, bool connected) +{ + uint16_t version = 0; + const bool available = connected && + networkConnectionServiceAvailable(control->connection, + LG_NET_SERVICE_CONTROL, LG_NET_CAPABILITY_RELIABLE) && + networkConnectionServiceVersion(control->connection, + LG_NET_SERVICE_CONTROL, &version); + + LG_LOCK(control->lock); + control->connected = connected; + control->available = available; + control->serviceVersion = available ? version : 0; + control->nextEntry = 0; + memset(control->entries, 0, sizeof(control->entries)); + LG_UNLOCK(control->lock); +} + +bool networkControlPacket(NetworkControl * control, + const LGNetPacketView * packet, bool datagram) +{ + if (!control || !packet || datagram || + packet->envelope.service != LG_NET_SERVICE_CONTROL || + packet->envelope.messageType != LG_NET_CONTROL_MESSAGE_STATUS || + packet->envelope.componentEpoch != 0 || + !packet->envelope.requestID) + return false; + + LGNetControlStatus status; + if (lgNetControlStatusDecode(&status, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + status.controlID != packet->envelope.requestID) + return false; + + const bool terminal = + status.status != LG_NET_CONTROL_STATUS_PENDING; + const LGNetEnvelopeFlags expected = + LG_NET_ENVELOPE_RESPONSE | + LG_NET_ENVELOPE_RELIABLE | + (terminal ? LG_NET_ENVELOPE_FINAL : 0); + if (packet->envelope.flags != expected) + return false; + + LG_LOCK(control->lock); + struct NetworkControlEntry * entry = + networkControlFindNL(control, status.controlID); + const bool valid = + control->connected && control->available && entry; + if (valid) + entry->status = status.status; + LG_UNLOCK(control->lock); + return valid; +} + +static bool networkControlFrameFlags( + LG_TransportFrameScheduleFlags source, + LGNetControlFrameScheduleFlags * destination) +{ + const LG_TransportFrameScheduleFlags known = + LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE | + LG_TRANSPORT_FRAME_SCHEDULE_RELEASE | + LG_TRANSPORT_FRAME_SCHEDULE_RESET | + LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE; + if (!destination || !source || (source & ~known)) + return false; + + *destination = 0; + if (source & LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE) + *destination |= LG_NET_CONTROL_FRAME_SCHEDULE_ACTIVE; + if (source & LG_TRANSPORT_FRAME_SCHEDULE_RELEASE) + *destination |= LG_NET_CONTROL_FRAME_SCHEDULE_RELEASE; + if (source & LG_TRANSPORT_FRAME_SCHEDULE_RESET) + *destination |= LG_NET_CONTROL_FRAME_SCHEDULE_RESET; + if (source & LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE) + *destination |= LG_NET_CONTROL_FRAME_SCHEDULE_IMMEDIATE; + return true; +} + +LG_TransportStatus networkControlSend(NetworkControl * control, + const LG_TransportControl * request, LG_TransportControlToken * token) +{ + if (!control || !request || !token) + return LG_TRANSPORT_ERROR; + + uint64_t id; + uint16_t version; + LG_LOCK(control->lock); + if (!control->connected) + { + LG_UNLOCK(control->lock); + return LG_TRANSPORT_DISCONNECTED; + } + if (!control->available) + { + LG_UNLOCK(control->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + + id = networkControlNextID(control->nextID); + if (!networkControlReserveNL(control, id)) + { + LG_UNLOCK(control->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + control->nextID = id; + version = control->serviceVersion; + LG_UNLOCK(control->lock); + + uint8_t payload[LG_NET_CONTROL_FRAME_SCHEDULE_WIRE_SIZE]; + uint32_t payloadLength; + uint16_t messageType; + bool encoded; + switch (request->type) + { + case LG_TRANSPORT_CONTROL_SET_CURSOR_POS: + { + const LGNetControlCursorPosition position = + { + .controlID = id, + .x = request->cursorPos.x, + .y = request->cursorPos.y, + }; + messageType = LG_NET_CONTROL_MESSAGE_CURSOR_POSITION; + payloadLength = LG_NET_CONTROL_CURSOR_POSITION_WIRE_SIZE; + encoded = lgNetControlCursorPositionEncode( + payload, payloadLength, &position); + break; + } + + case LG_TRANSPORT_CONTROL_WINDOW_SIZE: + { + const LGNetControlDisplaySize display = + { + .controlID = id, + .width = request->windowSize.width, + .height = request->windowSize.height, + }; + messageType = LG_NET_CONTROL_MESSAGE_DISPLAY_SIZE; + payloadLength = LG_NET_CONTROL_DISPLAY_SIZE_WIRE_SIZE; + encoded = lgNetControlDisplaySizeEncode( + payload, payloadLength, &display); + break; + } + + case LG_TRANSPORT_CONTROL_FRAME_SCHEDULE: + { + LGNetControlFrameScheduleFlags flags; + if (!networkControlFrameFlags( + request->frameSchedule.flags, &flags)) + encoded = false; + else + { + const LGNetControlFrameSchedule schedule = + { + .controlID = id, + .generation = request->frameSchedule.generation, + .flags = flags, + .periodNs = request->frameSchedule.period, + .targetSlackNs = request->frameSchedule.targetSlack, + .phaseErrorNs = request->frameSchedule.phaseError, + .feedbackFrameSerial = + request->frameSchedule.feedbackFrameSerial, + .feedbackScheduleEpoch = + request->frameSchedule.feedbackScheduleEpoch, + .feedbackDeadlineSerial = + request->frameSchedule.feedbackDeadlineSerial, + .leaseMs = request->frameSchedule.lease, + }; + encoded = lgNetControlFrameScheduleEncode( + payload, sizeof(payload), &schedule); + } + messageType = LG_NET_CONTROL_MESSAGE_FRAME_SCHEDULE; + payloadLength = LG_NET_CONTROL_FRAME_SCHEDULE_WIRE_SIZE; + break; + } + + default: + encoded = false; + messageType = 0; + payloadLength = 0; + break; + } + + LG_TransportStatus status = LG_TRANSPORT_ERROR; + if (encoded) + status = networkConnectionSend(control->connection, + LG_NET_SERVICE_CONTROL, messageType, version, + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_FINAL, + 0, id, payload, payloadLength); + + if (status != LG_TRANSPORT_OK) + { + LG_LOCK(control->lock); + struct NetworkControlEntry * entry = networkControlFindNL(control, id); + if (entry) + *entry = (struct NetworkControlEntry) { 0 }; + LG_UNLOCK(control->lock); + return status; + } + + *token = id; + return LG_TRANSPORT_OK; +} + +LG_TransportStatus networkControlStatus(NetworkControl * control, + LG_TransportControlToken token) +{ + if (!control || !token) + return LG_TRANSPORT_ERROR; + + LG_LOCK(control->lock); + if (!control->connected) + { + LG_UNLOCK(control->lock); + return LG_TRANSPORT_DISCONNECTED; + } + if (!control->available) + { + LG_UNLOCK(control->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + + const struct NetworkControlEntry * entry = + networkControlFindNL(control, token); + const LGNetControlStatusCode status = entry ? entry->status : 0; + LG_UNLOCK(control->lock); + + switch (status) + { + case LG_NET_CONTROL_STATUS_APPLIED: + return LG_TRANSPORT_OK; + + case LG_NET_CONTROL_STATUS_PENDING: + return LG_TRANSPORT_UNAVAILABLE; + + case LG_NET_CONTROL_STATUS_BUSY: + case LG_NET_CONTROL_STATUS_UNSUPPORTED: + case LG_NET_CONTROL_STATUS_STALE: + case LG_NET_CONTROL_STATUS_INVALID: + case LG_NET_CONTROL_STATUS_ERROR: + default: + return LG_TRANSPORT_ERROR; + } +} diff --git a/client/transports/Network/control.h b/client/transports/Network/control.h new file mode 100644 index 00000000..f2287b8c --- /dev/null +++ b/client/transports/Network/control.h @@ -0,0 +1,44 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "connection.h" + +#include "interface/transport.h" + +#include + +#include + +typedef struct NetworkControl NetworkControl; + +bool networkControlCreate(NetworkControl ** control, + NetworkConnection * connection); +void networkControlDestroy(NetworkControl ** control); + +void networkControlConnectionState(NetworkControl * control, bool connected); +bool networkControlPacket(NetworkControl * control, + const LGNetPacketView * packet, bool datagram); + +LG_TransportStatus networkControlSend(NetworkControl * control, + const LG_TransportControl * request, LG_TransportControlToken * token); +LG_TransportStatus networkControlStatus(NetworkControl * control, + LG_TransportControlToken token); diff --git a/client/transports/Network/cursor.c b/client/transports/Network/cursor.c new file mode 100644 index 00000000..170d715b --- /dev/null +++ b/client/transports/Network/cursor.c @@ -0,0 +1,813 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "cursor.h" + +#include "common/event.h" +#include "common/locking.h" + +#include + +#include +#include +#include +#include + +#define NETWORK_CURSOR_QUEUE_CAPACITY 64U + +struct NetworkCursorItem +{ + struct NetworkCursorItem * next; + LG_TransportPointer pointer; + uint8_t * shape; + KVMFRColorTransform * colorTransform; +}; + +struct NetworkCursorStatusReply +{ + bool send; + uint64_t stateSequence; + uint64_t appliedShapeID; + uint64_t appliedPositionID; + uint64_t appliedTransformID; + LGNetCursorStatusCode status; + uint32_t detail; +}; + +struct NetworkCursor +{ + NetworkConnection * connection; + LGEvent * event; + LG_Lock lock; + + bool connected; + bool available; + bool cancelled; + uint16_t serviceVersion; + + struct NetworkCursorItem * head; + struct NetworkCursorItem * tail; + struct NetworkCursorItem * outstanding; + unsigned count; + + LGNetCursorShape shape; + uint8_t * shapeData; + bool shapeValid; + bool shapeSupported; + + LGNetCursorPosition position; + bool positionValid; + bool positionSupported; + + LGNetCursorTransform transform; + bool transformValid; + bool transformSupported; + + LGNetCursorState pendingState; + bool pendingStateValid; + bool stateApplied; + uint64_t appliedStateSequence; + uint64_t appliedShapeID; + uint64_t appliedPositionID; + uint64_t appliedTransformID; + bool visible; + + uint64_t colorUpdateID; + uint32_t sdrWhiteLevel; +}; + +static void networkCursorItemFree(struct NetworkCursorItem * item) +{ + if (!item) + return; + free(item->colorTransform); + free(item->shape); + free(item); +} + +static void networkCursorQueueClearNL(NetworkCursor * cursor) +{ + while (cursor->head) + { + struct NetworkCursorItem * item = cursor->head; + cursor->head = item->next; + networkCursorItemFree(item); + } + cursor->tail = NULL; + cursor->count = 0; +} + +static void networkCursorCacheClearNL(NetworkCursor * cursor) +{ + free(cursor->shapeData); + cursor->shapeData = NULL; + cursor->shape = (LGNetCursorShape) { 0 }; + cursor->shapeValid = false; + cursor->shapeSupported = false; + cursor->position = (LGNetCursorPosition) { 0 }; + cursor->positionValid = false; + cursor->positionSupported = false; + cursor->transform = (LGNetCursorTransform) { 0 }; + cursor->transformValid = false; + cursor->transformSupported = false; + cursor->pendingState = (LGNetCursorState) { 0 }; + cursor->pendingStateValid = false; + cursor->stateApplied = false; + cursor->appliedStateSequence = 0; + cursor->appliedShapeID = 0; + cursor->appliedPositionID = 0; + cursor->appliedTransformID = 0; + cursor->visible = false; + cursor->colorUpdateID = 0; + cursor->sdrWhiteLevel = LG_SDR_WHITE_LEVEL_DEFAULT; +} + +static bool networkCursorItemCoalescible( + const struct NetworkCursorItem * item) +{ + return item && !(item->pointer.flags & + (LG_TRANSPORT_POINTER_SHAPE | + LG_TRANSPORT_POINTER_COLOR_TRANSFORM)); +} + +static bool networkCursorQueueNL(NetworkCursor * cursor, + struct NetworkCursorItem * item) +{ + if (networkCursorItemCoalescible(item) && + networkCursorItemCoalescible(cursor->tail)) + { + cursor->tail->pointer = item->pointer; + networkCursorItemFree(item); + lgSignalEvent(cursor->event); + return true; + } + + if (cursor->count == NETWORK_CURSOR_QUEUE_CAPACITY) + { + struct NetworkCursorItem * previous = NULL; + struct NetworkCursorItem * current = cursor->head; + while (current && !networkCursorItemCoalescible(current)) + { + previous = current; + current = current->next; + } + if (!current) + return false; + if (previous) + previous->next = current->next; + else + cursor->head = current->next; + if (cursor->tail == current) + cursor->tail = previous; + --cursor->count; + networkCursorItemFree(current); + } + + item->next = NULL; + if (cursor->tail) + cursor->tail->next = item; + else + cursor->head = item; + cursor->tail = item; + ++cursor->count; + lgSignalEvent(cursor->event); + return true; +} + +static bool networkCursorShapeType(LGNetCursorFormat format, + KVMFRCursorType * type) +{ + switch (format) + { + case LG_NET_CURSOR_FORMAT_BGRA8_PREMULTIPLIED: + case LG_NET_CURSOR_FORMAT_RGBA8_PREMULTIPLIED: + *type = CURSOR_TYPE_COLOR; + return true; + case LG_NET_CURSOR_FORMAT_MASKED_COLOR: + *type = CURSOR_TYPE_MASKED_COLOR; + return true; + case LG_NET_CURSOR_FORMAT_MONOCHROME: + *type = CURSOR_TYPE_MONOCHROME; + return true; + default: + return false; + } +} + +static bool networkCursorPositionSupported( + const LGNetCursorPosition * position) +{ + return position->displayID == 0 && + !(position->flags & LG_NET_CURSOR_POSITION_RELATIVE) && + position->x >= INT16_MIN && position->x <= INT16_MAX && + position->y >= INT16_MIN && position->y <= INT16_MAX; +} + +static bool networkCursorTransformSupported( + const LGNetCursorTransform * transform) +{ + return transform->displayID == 0 && + transform->rotation == LG_NET_CURSOR_ROTATION_0 && !transform->flags && + transform->sourceWidth == transform->targetWidth && + transform->sourceHeight == transform->targetHeight && + transform->offsetX == 0 && transform->offsetY == 0 && + transform->scaleNumerator == transform->scaleDenominator; +} + +static bool networkCursorShapeCopy(NetworkCursor * cursor, + const LGNetCursorShape * shape) +{ + KVMFRCursorType type; + const bool supported = networkCursorShapeType(shape->format, &type) && + !(shape->flags & LG_NET_CURSOR_SHAPE_ANIMATED) && + shape->hotspotX <= INT16_MAX && shape->hotspotY <= INT16_MAX; + + uint8_t * data = malloc(shape->dataLength); + if (!data) + return false; + memcpy(data, shape->data, shape->dataLength); + + if (shape->format == LG_NET_CURSOR_FORMAT_RGBA8_PREMULTIPLIED) + for (uint32_t y = 0; y < shape->height; ++y) + for (uint32_t x = 0; x < shape->width; ++x) + { + uint8_t * pixel = data + (size_t)y * shape->pitch + x * 4U; + const uint8_t red = pixel[0]; + pixel[0] = pixel[2]; + pixel[2] = red; + } + + free(cursor->shapeData); + cursor->shape = *shape; + cursor->shapeData = data; + cursor->shape.data = data; + cursor->shapeValid = true; + cursor->shapeSupported = supported; + return true; +} + +static bool networkCursorStateMissingNL(const NetworkCursor * cursor, + const LGNetCursorState * state, bool * impossible) +{ + *impossible = false; + if (state->flags & LG_NET_CURSOR_STATE_SHAPE_VALID) + { + if (!cursor->shapeValid) + return true; + if (cursor->shape.shapeID != state->shapeID) + { + *impossible = cursor->shape.shapeID > state->shapeID; + return true; + } + } + if (state->flags & LG_NET_CURSOR_STATE_POSITION_VALID) + { + if (!cursor->positionValid) + return true; + if (cursor->position.updateID != state->positionID) + { + *impossible = cursor->position.updateID > state->positionID; + return true; + } + } + if (state->flags & LG_NET_CURSOR_STATE_TRANSFORM_VALID) + { + if (!cursor->transformValid) + return true; + if (cursor->transform.transformID != state->transformID) + { + *impossible = cursor->transform.transformID > state->transformID; + return true; + } + } + return false; +} + +static struct NetworkCursorStatusReply networkCursorApplyStateNL( + NetworkCursor * cursor) +{ + struct NetworkCursorStatusReply reply = { 0 }; + if (!cursor->pendingStateValid) + return reply; + + const LGNetCursorState state = cursor->pendingState; + bool impossible; + if (networkCursorStateMissingNL(cursor, &state, &impossible)) + { + if (!impossible) + return reply; + reply.send = true; + reply.stateSequence = state.stateSequence; + reply.status = LG_NET_CURSOR_STATUS_INVALID; + cursor->pendingStateValid = false; + return reply; + } + + const bool shapeValid = + state.flags & LG_NET_CURSOR_STATE_SHAPE_VALID; + const bool positionValid = + state.flags & LG_NET_CURSOR_STATE_POSITION_VALID; + const bool transformValid = + state.flags & LG_NET_CURSOR_STATE_TRANSFORM_VALID; + if (state.displayID != 0 || + (shapeValid && !cursor->shapeSupported) || + (positionValid && (!cursor->positionSupported || + cursor->position.displayID != state.displayID)) || + (transformValid && (!cursor->transformSupported || + cursor->transform.displayID != state.displayID))) + { + reply.send = true; + reply.stateSequence = state.stateSequence; + reply.status = LG_NET_CURSOR_STATUS_UNSUPPORTED; + cursor->pendingStateValid = false; + return reply; + } + + const bool newShape = shapeValid && + cursor->appliedShapeID != state.shapeID; + const bool newPosition = positionValid && + cursor->appliedPositionID != state.positionID; + const bool visible = + !!(state.flags & LG_NET_CURSOR_STATE_VISIBLE) && + !(shapeValid && + (cursor->shape.flags & LG_NET_CURSOR_SHAPE_HIDDEN)); + const bool visibilityChanged = + !cursor->stateApplied || cursor->visible != visible; + + struct NetworkCursorItem * item = NULL; + if (newShape || newPosition || visibilityChanged) + { + item = calloc(1, sizeof(*item)); + if (!item) + goto capacity; + + item->pointer.flags = LG_TRANSPORT_POINTER_VISIBLE_VALID; + item->pointer.sdrWhiteLevel = cursor->sdrWhiteLevel; + if (visible) + item->pointer.flags |= LG_TRANSPORT_POINTER_VISIBLE; + if (newPosition) + { + item->pointer.flags |= LG_TRANSPORT_POINTER_POSITION; + item->pointer.x = (int16_t)cursor->position.x; + item->pointer.y = (int16_t)cursor->position.y; + } + if (newShape) + { + if (!networkCursorShapeType(cursor->shape.format, + &item->pointer.type)) + goto capacity; + item->shape = malloc(cursor->shape.dataLength); + if (!item->shape) + goto capacity; + memcpy(item->shape, cursor->shapeData, cursor->shape.dataLength); + item->pointer.flags |= LG_TRANSPORT_POINTER_SHAPE; + item->pointer.hx = (int16_t)cursor->shape.hotspotX; + item->pointer.hy = (int16_t)cursor->shape.hotspotY; + item->pointer.width = cursor->shape.width; + item->pointer.height = cursor->shape.height; + item->pointer.pitch = cursor->shape.pitch; + item->pointer.shape = item->shape; + } + if (!networkCursorQueueNL(cursor, item)) + goto capacity; + item = NULL; + } + + cursor->stateApplied = true; + cursor->appliedStateSequence = state.stateSequence; + cursor->appliedShapeID = shapeValid ? state.shapeID : 0; + cursor->appliedPositionID = positionValid ? state.positionID : 0; + cursor->appliedTransformID = transformValid ? state.transformID : 0; + cursor->visible = visible; + cursor->pendingStateValid = false; + reply.send = true; + reply.stateSequence = state.stateSequence; + reply.appliedShapeID = cursor->appliedShapeID; + reply.appliedPositionID = cursor->appliedPositionID; + reply.appliedTransformID = cursor->appliedTransformID; + reply.status = LG_NET_CURSOR_STATUS_APPLIED; + return reply; + +capacity: + networkCursorItemFree(item); + cursor->pendingStateValid = false; + reply.send = true; + reply.stateSequence = state.stateSequence; + reply.status = LG_NET_CURSOR_STATUS_ERROR; + return reply; +} + +static bool networkCursorSendStatus(NetworkCursor * cursor, + const struct NetworkCursorStatusReply * reply) +{ + if (!reply->send) + return true; + + uint16_t version; + LG_LOCK(cursor->lock); + const bool available = cursor->connected && cursor->available; + version = cursor->serviceVersion; + LG_UNLOCK(cursor->lock); + if (!available) + return false; + + const LGNetCursorStatus status = + { + .stateSequence = reply->stateSequence, + .appliedShapeID = reply->appliedShapeID, + .appliedPositionID = reply->appliedPositionID, + .appliedTransformID = reply->appliedTransformID, + .status = reply->status, + .detail = reply->detail, + }; + uint8_t payload[LG_NET_CURSOR_STATUS_WIRE_SIZE]; + return lgNetCursorStatusEncode(payload, sizeof(payload), &status) && + networkConnectionSend(cursor->connection, LG_NET_SERVICE_CURSOR, + LG_NET_CURSOR_MESSAGE_STATUS, version, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, + 0, status.stateSequence, payload, sizeof(payload)) == LG_TRANSPORT_OK; +} + +bool networkCursorCreate(NetworkCursor ** result, + NetworkConnection * connection) +{ + if (!result || !connection) + return false; + + NetworkCursor * cursor = calloc(1, sizeof(*cursor)); + if (!cursor) + return false; + + LG_LOCK_INIT(cursor->lock); + cursor->event = lgCreateEvent(true, 0); + if (!cursor->event) + { + LG_LOCK_FREE(cursor->lock); + free(cursor); + return false; + } + + cursor->connection = connection; + cursor->sdrWhiteLevel = LG_SDR_WHITE_LEVEL_DEFAULT; + *result = cursor; + return true; +} + +void networkCursorDestroy(NetworkCursor ** result) +{ + if (!result || !*result) + return; + + NetworkCursor * cursor = *result; + LG_LOCK(cursor->lock); + networkCursorQueueClearNL(cursor); + networkCursorItemFree(cursor->outstanding); + cursor->outstanding = NULL; + networkCursorCacheClearNL(cursor); + LG_UNLOCK(cursor->lock); + lgSignalEvent(cursor->event); + lgFreeEvent(cursor->event); + LG_LOCK_FREE(cursor->lock); + free(cursor); + *result = NULL; +} + +bool networkCursorConnectionState(NetworkCursor * cursor, bool connected) +{ + uint16_t version = 0; + const bool available = connected && + networkConnectionServiceAvailable(cursor->connection, + LG_NET_SERVICE_CURSOR, LG_NET_CAPABILITY_RELIABLE) && + networkConnectionServiceVersion(cursor->connection, + LG_NET_SERVICE_CURSOR, &version); + + LG_LOCK(cursor->lock); + cursor->connected = connected; + cursor->available = available; + cursor->cancelled = false; + cursor->serviceVersion = available ? version : 0; + networkCursorQueueClearNL(cursor); + networkCursorCacheClearNL(cursor); + LG_UNLOCK(cursor->lock); + lgSignalEvent(cursor->event); + return available; +} + +static bool networkCursorEnvelope(const LGNetPacketView * packet, + bool datagram, uint64_t id) +{ + const LGNetEnvelopeFlags expected = LG_NET_ENVELOPE_FINAL | + (datagram ? LG_NET_ENVELOPE_DATAGRAM : LG_NET_ENVELOPE_RELIABLE); + return packet->envelope.flags == expected && + packet->envelope.componentEpoch == 0 && + packet->envelope.requestID == id; +} + +static bool networkCursorPositionPacket(NetworkCursor * cursor, + const LGNetPacketView * packet, bool datagram) +{ + LGNetCursorPosition position; + if (lgNetCursorPositionDecode(&position, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + !networkCursorEnvelope(packet, datagram, position.updateID)) + return false; + + struct NetworkCursorStatusReply reply = { 0 }; + LG_LOCK(cursor->lock); + const bool valid = cursor->connected && cursor->available; + if (valid && (!cursor->positionValid || + position.updateID > cursor->position.updateID)) + { + cursor->position = position; + cursor->positionValid = true; + cursor->positionSupported = networkCursorPositionSupported(&position); + reply = networkCursorApplyStateNL(cursor); + } + LG_UNLOCK(cursor->lock); + return valid && networkCursorSendStatus(cursor, &reply); +} + +static bool networkCursorShapePacket(NetworkCursor * cursor, + const LGNetPacketView * packet, bool datagram) +{ + if (datagram) + return false; + + LGNetCursorShape shape; + if (lgNetCursorShapeDecode(&shape, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + !networkCursorEnvelope(packet, false, shape.shapeID)) + return false; + + struct NetworkCursorStatusReply reply = { 0 }; + LG_LOCK(cursor->lock); + const bool valid = cursor->connected && cursor->available; + bool copied = true; + if (valid && (!cursor->shapeValid || shape.shapeID > cursor->shape.shapeID)) + { + copied = networkCursorShapeCopy(cursor, &shape); + if (copied) + reply = networkCursorApplyStateNL(cursor); + } + LG_UNLOCK(cursor->lock); + return valid && copied && networkCursorSendStatus(cursor, &reply); +} + +static bool networkCursorTransformPacket(NetworkCursor * cursor, + const LGNetPacketView * packet, bool datagram) +{ + if (datagram) + return false; + + LGNetCursorTransform transform; + if (lgNetCursorTransformDecode(&transform, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + !networkCursorEnvelope(packet, false, transform.transformID)) + return false; + + struct NetworkCursorStatusReply reply = { 0 }; + LG_LOCK(cursor->lock); + const bool valid = cursor->connected && cursor->available; + if (valid && (!cursor->transformValid || + transform.transformID > cursor->transform.transformID)) + { + cursor->transform = transform; + cursor->transformValid = true; + cursor->transformSupported = + networkCursorTransformSupported(&transform); + reply = networkCursorApplyStateNL(cursor); + } + LG_UNLOCK(cursor->lock); + return valid && networkCursorSendStatus(cursor, &reply); +} + +static bool networkCursorStatePacket(NetworkCursor * cursor, + const LGNetPacketView * packet, bool datagram) +{ + if (datagram) + return false; + + LGNetCursorState state; + if (lgNetCursorStateDecode(&state, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + !networkCursorEnvelope(packet, false, state.stateSequence)) + return false; + + struct NetworkCursorStatusReply reply = { 0 }; + LG_LOCK(cursor->lock); + const bool valid = cursor->connected && cursor->available; + if (valid && state.stateSequence > cursor->appliedStateSequence && + (!cursor->pendingStateValid || + state.stateSequence > cursor->pendingState.stateSequence)) + { + cursor->pendingState = state; + cursor->pendingStateValid = true; + reply = networkCursorApplyStateNL(cursor); + } + LG_UNLOCK(cursor->lock); + return valid && networkCursorSendStatus(cursor, &reply); +} + +static bool networkCursorColorPacket(NetworkCursor * cursor, + const LGNetPacketView * packet, bool datagram) +{ + if (datagram) + return false; + + LGNetCursorColorTransform transform; + if (lgNetCursorColorTransformDecode(&transform, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + !networkCursorEnvelope(packet, false, transform.updateID)) + return false; + + struct NetworkCursorItem * item = calloc(1, sizeof(*item)); + if (!item) + return false; + item->colorTransform = calloc(1, sizeof(*item->colorTransform)); + if (!item->colorTransform) + { + networkCursorItemFree(item); + return false; + } + + if (transform.flags & LG_NET_CURSOR_COLOR_TRANSFORM_MATRIX) + item->colorTransform->flags |= LG_COLOR_TRANSFORM_MATRIX; + if (transform.flags & LG_NET_CURSOR_COLOR_TRANSFORM_LUT) + item->colorTransform->flags |= LG_COLOR_TRANSFORM_LUT; + memcpy(item->colorTransform->matrix, transform.matrix, + sizeof(transform.matrix)); + item->colorTransform->scalar = transform.scalar; + if ((transform.flags & LG_NET_CURSOR_COLOR_TRANSFORM_LUT) && + !lgNetCursorColorLUTDecode((float *)item->colorTransform->lut, + transform.lut, LG_NET_CURSOR_COLOR_LUT_BYTES)) + { + networkCursorItemFree(item); + return false; + } + item->pointer.flags = LG_TRANSPORT_POINTER_COLOR_TRANSFORM; + item->pointer.sdrWhiteLevel = transform.sdrWhiteLevel; + item->pointer.colorTransform = item->colorTransform; + + LG_LOCK(cursor->lock); + const bool valid = cursor->connected && cursor->available; + bool queued = true; + if (valid && transform.updateID > cursor->colorUpdateID) + { + cursor->colorUpdateID = transform.updateID; + cursor->sdrWhiteLevel = transform.sdrWhiteLevel; + if (cursor->stateApplied) + { + item->pointer.flags |= LG_TRANSPORT_POINTER_VISIBLE_VALID; + if (cursor->visible) + item->pointer.flags |= LG_TRANSPORT_POINTER_VISIBLE; + } + queued = networkCursorQueueNL(cursor, item); + if (queued) + item = NULL; + } + LG_UNLOCK(cursor->lock); + networkCursorItemFree(item); + return valid && queued; +} + +static bool networkCursorStatusPacket(const LGNetPacketView * packet, + bool datagram) +{ + if (datagram) + return false; + + LGNetCursorStatus status; + if (lgNetCursorStatusDecode(&status, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + packet->envelope.componentEpoch != 0 || + packet->envelope.requestID != status.stateSequence) + return false; + + const LGNetEnvelopeFlags base = + LG_NET_ENVELOPE_FINAL | LG_NET_ENVELOPE_RELIABLE; + return packet->envelope.flags == base || + packet->envelope.flags == (base | LG_NET_ENVELOPE_RESPONSE); +} + +bool networkCursorPacket(NetworkCursor * cursor, + const LGNetPacketView * packet, bool datagram) +{ + if (!cursor || !packet || + packet->envelope.service != LG_NET_SERVICE_CURSOR) + return false; + + switch (packet->envelope.messageType) + { + case LG_NET_CURSOR_MESSAGE_STATE: + return networkCursorStatePacket(cursor, packet, datagram); + case LG_NET_CURSOR_MESSAGE_POSITION: + return networkCursorPositionPacket(cursor, packet, datagram); + case LG_NET_CURSOR_MESSAGE_SHAPE: + return networkCursorShapePacket(cursor, packet, datagram); + case LG_NET_CURSOR_MESSAGE_TRANSFORM: + return networkCursorTransformPacket(cursor, packet, datagram); + case LG_NET_CURSOR_MESSAGE_STATUS: + return networkCursorStatusPacket(packet, datagram); + case LG_NET_CURSOR_MESSAGE_COLOR_TRANSFORM: + return networkCursorColorPacket(cursor, packet, datagram); + default: + return false; + } +} + +LG_TransportStatus networkCursorNext(NetworkCursor * cursor, + LG_TransportPointer * pointer) +{ + if (!cursor || !pointer) + return LG_TRANSPORT_ERROR; + + for (;;) + { + LG_LOCK(cursor->lock); + if (!cursor->connected) + { + LG_UNLOCK(cursor->lock); + return LG_TRANSPORT_DISCONNECTED; + } + if (!cursor->available) + { + LG_UNLOCK(cursor->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + if (cursor->cancelled) + { + cursor->cancelled = false; + LG_UNLOCK(cursor->lock); + return LG_TRANSPORT_TIMEOUT; + } + if (cursor->outstanding) + { + LG_UNLOCK(cursor->lock); + return LG_TRANSPORT_ERROR; + } + if (cursor->head) + { + struct NetworkCursorItem * item = cursor->head; + cursor->head = item->next; + if (!cursor->head) + cursor->tail = NULL; + --cursor->count; + item->next = NULL; + cursor->outstanding = item; + *pointer = item->pointer; + if (cursor->head) + lgSignalEvent(cursor->event); + LG_UNLOCK(cursor->lock); + return LG_TRANSPORT_OK; + } + LG_UNLOCK(cursor->lock); + lgWaitEvent(cursor->event, TIMEOUT_INFINITE); + } +} + +void networkCursorRelease(NetworkCursor * cursor, + LG_TransportPointer * pointer) +{ + if (!cursor || !pointer) + return; + + LG_LOCK(cursor->lock); + struct NetworkCursorItem * item = cursor->outstanding; + cursor->outstanding = NULL; + LG_UNLOCK(cursor->lock); + networkCursorItemFree(item); + memset(pointer, 0, sizeof(*pointer)); +} + +void networkCursorCancel(NetworkCursor * cursor) +{ + if (!cursor) + return; + + LG_LOCK(cursor->lock); + cursor->cancelled = true; + LG_UNLOCK(cursor->lock); + lgSignalEvent(cursor->event); +} + +void networkCursorStop(NetworkCursor * cursor) +{ + networkCursorCancel(cursor); +} diff --git a/client/transports/Network/cursor.h b/client/transports/Network/cursor.h new file mode 100644 index 00000000..c1828200 --- /dev/null +++ b/client/transports/Network/cursor.h @@ -0,0 +1,46 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "connection.h" + +#include "interface/transport.h" + +#include + +#include + +typedef struct NetworkCursor NetworkCursor; + +bool networkCursorCreate(NetworkCursor ** cursor, + NetworkConnection * connection); +void networkCursorDestroy(NetworkCursor ** cursor); + +bool networkCursorConnectionState(NetworkCursor * cursor, bool connected); +bool networkCursorPacket(NetworkCursor * cursor, + const LGNetPacketView * packet, bool datagram); + +LG_TransportStatus networkCursorNext(NetworkCursor * cursor, + LG_TransportPointer * pointer); +void networkCursorRelease(NetworkCursor * cursor, + LG_TransportPointer * pointer); +void networkCursorCancel(NetworkCursor * cursor); +void networkCursorStop(NetworkCursor * cursor); diff --git a/client/transports/Network/network.c b/client/transports/Network/network.c index 4f653951..c1521feb 100644 --- a/client/transports/Network/network.c +++ b/client/transports/Network/network.c @@ -20,8 +20,11 @@ #include "network.h" #include "connection.h" +#include "control.h" +#include "cursor.h" #include "input.h" #include "known_hosts.h" +#include "recovery.h" #include "app.h" #include "interface/renderer.h" @@ -30,6 +33,7 @@ #include "common/event.h" #include "common/locking.h" #include +#include #include "common/option.h" #include @@ -40,11 +44,35 @@ #define NETWORK_ADDRESS_MAX 255U #define NETWORK_ENDPOINT_TEXT_SIZE (NETWORK_ADDRESS_MAX + 9U) -#define NETWORK_MAX_PAYLOAD (1024U * 1024U) +#define NETWORK_MAX_PAYLOAD \ + (LG_NET_CURSOR_SHAPE_HEADER_WIRE_SIZE + LG_NET_CURSOR_MAX_SHAPE_LENGTH) #define NETWORK_MAX_DATAGRAM 1136U static const LGNetCapability NETWORK_CAPABILITIES[] = { + { + .service = LG_NET_SERVICE_RECOVERY, + .versionMin = LG_NET_RECOVERY_VERSION_MIN, + .versionMax = LG_NET_RECOVERY_VERSION_CURRENT, + .flags = LG_NET_CAPABILITY_RELIABLE, + .features = 0, + .maxPayloadLength = NETWORK_MAX_PAYLOAD, + .maxDatagramPayload = 0, + .maxBidirectionalStreams = 0, + .maxUnidirectionalStreams = 0, + }, + { + .service = LG_NET_SERVICE_CURSOR, + .versionMin = LG_NET_CURSOR_VERSION_MIN, + .versionMax = LG_NET_CURSOR_VERSION_CURRENT, + .flags = LG_NET_CAPABILITY_RELIABLE | + LG_NET_CAPABILITY_DATAGRAM, + .features = 0, + .maxPayloadLength = NETWORK_MAX_PAYLOAD, + .maxDatagramPayload = NETWORK_MAX_DATAGRAM, + .maxBidirectionalStreams = 0, + .maxUnidirectionalStreams = 0, + }, { .service = LG_NET_SERVICE_INPUT, .versionMin = LG_NET_INPUT_VERSION_MIN, @@ -57,6 +85,17 @@ static const LGNetCapability NETWORK_CAPABILITIES[] = .maxBidirectionalStreams = 0, .maxUnidirectionalStreams = 0, }, + { + .service = LG_NET_SERVICE_CONTROL, + .versionMin = LG_NET_CONTROL_VERSION_MIN, + .versionMax = LG_NET_CONTROL_VERSION_CURRENT, + .flags = LG_NET_CAPABILITY_RELIABLE, + .features = 0, + .maxPayloadLength = NETWORK_MAX_PAYLOAD, + .maxDatagramPayload = 0, + .maxBidirectionalStreams = 0, + .maxUnidirectionalStreams = 0, + }, }; struct NetworkTrustPrompt @@ -69,7 +108,10 @@ struct NetworkTrustPrompt struct LG_Transport { NetworkConnection * connection; + NetworkControl * control; + NetworkCursor * cursor; NetworkInput * input; + NetworkRecovery * recovery; NetworkKnownHosts knownHosts; LGEvent * frameEvent; LGEvent * promptIdle; @@ -265,6 +307,14 @@ static void networkSetUnavailable(LG_VideoComponentStatus * status, status->reason = reason; } +static void networkSetAvailable(LG_VideoComponentStatus * status) +{ + if (!status->available) + status->epoch = networkNextEpoch(status->epoch); + status->available = true; + status->reason = LG_TRANSPORT_OK; +} + static void networkConnectionState(void * opaque, bool connected, LG_TransportStatus reason) { @@ -273,10 +323,17 @@ static void networkConnectionState(void * opaque, bool connected, LG_TRANSPORT_UNAVAILABLE : reason; networkInputConnectionState(this->input, connected); + networkControlConnectionState(this->control, connected); + networkRecoveryConnectionState(this->recovery, connected); + const bool cursorAvailable = + networkCursorConnectionState(this->cursor, connected); LG_LOCK(this->stateLock); networkSetUnavailable(&this->videoStatus.frame, status); - networkSetUnavailable(&this->videoStatus.pointer, status); + if (cursorAvailable) + networkSetAvailable(&this->videoStatus.pointer); + else + networkSetUnavailable(&this->videoStatus.pointer, status); LG_UNLOCK(this->stateLock); lgSignalEvent(this->frameEvent); @@ -287,10 +344,31 @@ static void networkPacket(void * opaque, const LGNetPacketView * packet, bool datagram) { LG_Transport * this = opaque; - if (packet->envelope.service == LG_NET_SERVICE_INPUT && - !networkInputPacket(this->input, packet, datagram)) + bool valid; + switch (packet->envelope.service) { - DEBUG_ERROR("Invalid network input message"); + case LG_NET_SERVICE_RECOVERY: + valid = networkRecoveryPacket(this->recovery, packet, datagram); + break; + case LG_NET_SERVICE_CURSOR: + valid = networkCursorPacket(this->cursor, packet, datagram); + break; + case LG_NET_SERVICE_INPUT: + valid = networkInputPacket(this->input, packet, datagram); + break; + case LG_NET_SERVICE_CONTROL: + valid = networkControlPacket(this->control, packet, datagram); + break; + default: + valid = false; + break; + } + + if (!valid) + { + DEBUG_ERROR("Invalid network service %u message %u", + (unsigned)packet->envelope.service, + (unsigned)packet->envelope.messageType); networkConnectionCancel(this->connection); } } @@ -351,14 +429,20 @@ static bool networkCreate(LG_Transport ** result) }; if (!networkConnectionCreate(&this->connection, ¶ms)) goto fail; - if (!networkInputCreate(&this->input, this->connection)) + if (!networkControlCreate(&this->control, this->connection) || + !networkCursorCreate(&this->cursor, this->connection) || + !networkInputCreate(&this->input, this->connection) || + !networkRecoveryCreate(&this->recovery, this->connection)) goto fail; *result = this; return true; fail: + networkRecoveryDestroy(&this->recovery); networkInputDestroy(&this->input); + networkCursorDestroy(&this->cursor); + networkControlDestroy(&this->control); networkConnectionDestroy(&this->connection); if (this->frameEvent) lgFreeEvent(this->frameEvent); @@ -389,7 +473,10 @@ static void networkDestroy(LG_Transport ** transport) LG_UNLOCK(this->stateLock); LG_UNLOCK(this->callbackLock); + networkRecoveryDestroy(&this->recovery); networkInputDestroy(&this->input); + networkCursorDestroy(&this->cursor); + networkControlDestroy(&this->control); networkConnectionDestroy(&this->connection); lgFreeEvent(this->frameEvent); lgFreeEvent(this->promptIdle); @@ -418,6 +505,9 @@ static LG_TransportStatus networkConnect(LG_Transport * this, static void networkDisconnect(LG_Transport * this) { networkInputConnectionState(this->input, false); + networkControlConnectionState(this->control, false); + networkRecoveryConnectionState(this->recovery, false); + networkCursorConnectionState(this->cursor, false); networkConnectionDisconnect(this->connection); lgSignalEvent(this->frameEvent); } @@ -516,6 +606,36 @@ static void networkStopFrame(LG_Transport * this) lgSignalEvent(this->frameEvent); } +static LG_TransportStatus networkNextPointer(LG_Transport * this, + LG_TransportPointer * pointer) +{ + const LG_TransportStatus status = + networkCursorNext(this->cursor, pointer); + if (status != LG_TRANSPORT_OK) + return status; + + LG_LOCK(this->stateLock); + pointer->epoch = this->videoStatus.pointer.epoch; + LG_UNLOCK(this->stateLock); + return LG_TRANSPORT_OK; +} + +static void networkReleasePointer(LG_Transport * this, + LG_TransportPointer * pointer) +{ + networkCursorRelease(this->cursor, pointer); +} + +static void networkCancelPointerWait(LG_Transport * this) +{ + networkCursorCancel(this->cursor); +} + +static void networkStopPointer(LG_Transport * this) +{ + networkCursorStop(this->cursor); +} + static const LG_FrameOps networkFrameOps = { .setStatusListener = networkSetStatusListener, @@ -527,6 +647,10 @@ static const LG_FrameOps networkFrameOps = .releaseFrame = networkReleaseFrame, .cancelFrameWait = networkCancelFrameWait, .stopFrame = networkStopFrame, + .nextPointer = networkNextPointer, + .releasePointer = networkReleasePointer, + .cancelPointerWait = networkCancelPointerWait, + .stopPointer = networkStopPointer, }; static const LG_VideoOps networkVideoOps = @@ -545,6 +669,10 @@ static const LG_VideoOps * networkGetVideoOps(LG_Transport * this) static const LG_InputOps * networkGetInputOps(LG_Transport * this, void ** opaque) { + *opaque = NULL; + if (!networkConnectionServiceAvailable(this->connection, + LG_NET_SERVICE_INPUT, LG_NET_CAPABILITY_RELIABLE)) + return NULL; *opaque = this->input; return networkInputGetOps(); } @@ -568,18 +696,26 @@ static const LG_ClipboardOps * networkGetClipboardOps(LG_Transport * this, static LG_TransportStatus networkSendControl(LG_Transport * this, const LG_TransportControl * control, LG_TransportControlToken * token) { - (void)control; - (void)token; - return networkSessionValid(this) ? - LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED; + return networkControlSend(this->control, control, token); } -static LG_TransportStatus networkControlStatus(LG_Transport * this, +static LG_TransportStatus networkControlStatusOp(LG_Transport * this, LG_TransportControlToken token) { - (void)token; - return networkSessionValid(this) ? - LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED; + return networkControlStatus(this->control, token); +} + +static LG_TransportStatus networkGetRecoveryInfo(LG_Transport * this, + LG_RecoveryInfo * info) +{ + return networkRecoveryGetInfo(this->recovery, info); +} + +static LG_TransportStatus networkRequestRecovery(LG_Transport * this, + LG_RecoveryRequest request, uint64_t * instance, uint32_t * serial) +{ + return networkRecoveryRequest( + this->recovery, request, instance, serial); } const LG_TransportOps LGT_Network = @@ -596,6 +732,8 @@ const LG_TransportOps LGT_Network = .getInputOps = networkGetInputOps, .getAudioOps = networkGetAudioOps, .getClipboardOps = networkGetClipboardOps, + .getRecoveryInfo = networkGetRecoveryInfo, + .requestRecovery = networkRequestRecovery, .sendControl = networkSendControl, - .controlStatus = networkControlStatus, + .controlStatus = networkControlStatusOp, }; diff --git a/client/transports/Network/recovery.c b/client/transports/Network/recovery.c new file mode 100644 index 00000000..53960cb7 --- /dev/null +++ b/client/transports/Network/recovery.c @@ -0,0 +1,575 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "recovery.h" + +#include "common/event.h" +#include "common/locking.h" + +#include + +#include +#include +#include + +#define NETWORK_RECOVERY_QUERY_TIMEOUT_MS 500U +#define NETWORK_RECOVERY_DEFAULT_TIMEOUT_MS 30000U +#define NETWORK_RECOVERY_REQUEST_CAPACITY 16U + +struct NetworkRecoveryRequestEntry +{ + uint64_t id; + uint64_t session; + uint32_t serial; + LG_RecoveryRequest request; + bool acknowledged; +}; + +struct NetworkRecovery +{ + NetworkConnection * connection; + LGEvent * queryEvent; + LG_Lock lock; + + bool connected; + bool available; + uint16_t serviceVersion; + uint64_t nextID; + uint32_t nextSerial; + + bool queryPending; + uint64_t queryID; + bool infoValid; + LGNetRecoveryInfo info; + char version[LG_NET_RECOVERY_MAX_VERSION_LENGTH + 1U]; + + uint32_t requestSerial; + LG_RecoveryRequest request; + uint32_t ackSerial; + LG_RecoveryRequest ackRequest; + + struct NetworkRecoveryRequestEntry requests[NETWORK_RECOVERY_REQUEST_CAPACITY]; + unsigned nextRequest; +}; + +static uint64_t networkRecoveryNextID(uint64_t id) +{ + ++id; + return id ? id : 1U; +} + +static uint32_t networkRecoveryNextSerial(uint32_t serial) +{ + ++serial; + return serial ? serial : 1U; +} + +static bool networkRecoverySequenceNewer(uint64_t sequence, + uint64_t reference) +{ + return sequence > reference; +} + +static struct NetworkRecoveryRequestEntry * networkRecoveryFindRequestNL( + NetworkRecovery * recovery, uint64_t id) +{ + for (size_t index = 0; + index < NETWORK_RECOVERY_REQUEST_CAPACITY; ++index) + if (recovery->requests[index].id == id) + return &recovery->requests[index]; + return NULL; +} + +static struct NetworkRecoveryRequestEntry * networkRecoveryReserveRequestNL( + NetworkRecovery * recovery) +{ + for (size_t offset = 0; + offset < NETWORK_RECOVERY_REQUEST_CAPACITY; ++offset) + { + const unsigned index = + (recovery->nextRequest + offset) % NETWORK_RECOVERY_REQUEST_CAPACITY; + struct NetworkRecoveryRequestEntry * entry = &recovery->requests[index]; + if (entry->id && !entry->acknowledged) + continue; + + recovery->nextRequest = + (index + 1U) % NETWORK_RECOVERY_REQUEST_CAPACITY; + return entry; + } + return NULL; +} + +static bool networkRecoveryState( + LGNetRecoveryState state, LG_RecoveryState * result) +{ + switch (state) + { + case LG_NET_RECOVERY_STATE_UNKNOWN: + *result = LG_RECOVERY_STATE_UNKNOWN; + return true; + case LG_NET_RECOVERY_STATE_NORMAL: + *result = LG_RECOVERY_STATE_NORMAL; + return true; + case LG_NET_RECOVERY_STATE_SWITCHING: + *result = LG_RECOVERY_STATE_SWITCHING; + return true; + case LG_NET_RECOVERY_STATE_ACTIVE: + *result = LG_RECOVERY_STATE_ACTIVE; + return true; + case LG_NET_RECOVERY_STATE_FAILED: + *result = LG_RECOVERY_STATE_FAILED; + return true; + default: + return false; + } +} + +static bool networkRecoveryError( + LGNetRecoveryError error, LG_RecoveryError * result) +{ + switch (error) + { + case LG_NET_RECOVERY_ERROR_NONE: + *result = LG_RECOVERY_ERR_NONE; + return true; + case LG_NET_RECOVERY_ERROR_UNSUPPORTED: + *result = LG_RECOVERY_ERR_UNSUPPORTED; + return true; + case LG_NET_RECOVERY_ERROR_HELPER_UNAVAILABLE: + *result = LG_RECOVERY_ERR_HELPER_UNAVAILABLE; + return true; + case LG_NET_RECOVERY_ERROR_TOPOLOGY_FAILED: + *result = LG_RECOVERY_ERR_TOPOLOGY_FAILED; + return true; + case LG_NET_RECOVERY_ERROR_NO_FALLBACK_DISPLAY: + *result = LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY; + return true; + case LG_NET_RECOVERY_ERROR_BUSY: + *result = LG_RECOVERY_ERR_BUSY; + return true; + case LG_NET_RECOVERY_ERROR_CAPACITY: + *result = LG_RECOVERY_ERR_CAPACITY; + return true; + case LG_NET_RECOVERY_ERROR_TIMEOUT: + /* The public transport ABI has no timeout-specific recovery error. */ + *result = LG_RECOVERY_ERR_HELPER_UNAVAILABLE; + return true; + default: + return false; + } +} + +static void networkRecoveryClearNL(NetworkRecovery * recovery) +{ + recovery->queryPending = false; + recovery->queryID = 0; + recovery->infoValid = false; + recovery->info = (LGNetRecoveryInfo) { 0 }; + recovery->version[0] = '\0'; + recovery->requestSerial = 0; + recovery->request = LG_RECOVERY_REQ_NONE; + recovery->ackSerial = 0; + recovery->ackRequest = LG_RECOVERY_REQ_NONE; + recovery->nextRequest = 0; + memset(recovery->requests, 0, sizeof(recovery->requests)); +} + +static bool networkRecoveryBeginQuery(NetworkRecovery * recovery, + uint64_t * queryID, uint16_t * version) +{ + LG_LOCK(recovery->lock); + if (!recovery->connected || !recovery->available) + { + LG_UNLOCK(recovery->lock); + return false; + } + + if (recovery->queryPending) + { + *queryID = recovery->queryID; + *version = recovery->serviceVersion; + LG_UNLOCK(recovery->lock); + return true; + } + + recovery->nextID = networkRecoveryNextID(recovery->nextID); + recovery->queryID = recovery->nextID; + recovery->queryPending = true; + *queryID = recovery->queryID; + *version = recovery->serviceVersion; + lgResetEvent(recovery->queryEvent); + LG_UNLOCK(recovery->lock); + + const LG_TransportStatus status = networkConnectionSend( + recovery->connection, LG_NET_SERVICE_RECOVERY, + LG_NET_RECOVERY_MESSAGE_GET_INFO, *version, + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_FINAL, + 0, *queryID, NULL, 0); + if (status == LG_TRANSPORT_OK) + return true; + + LG_LOCK(recovery->lock); + if (recovery->queryID == *queryID) + { + recovery->queryPending = false; + recovery->queryID = 0; + } + LG_UNLOCK(recovery->lock); + lgSignalEvent(recovery->queryEvent); + return false; +} + +bool networkRecoveryCreate(NetworkRecovery ** result, + NetworkConnection * connection) +{ + if (!result || !connection) + return false; + + NetworkRecovery * recovery = calloc(1, sizeof(*recovery)); + if (!recovery) + return false; + + LG_LOCK_INIT(recovery->lock); + recovery->queryEvent = lgCreateEvent(false, 0); + if (!recovery->queryEvent) + { + LG_LOCK_FREE(recovery->lock); + free(recovery); + return false; + } + + recovery->connection = connection; + *result = recovery; + return true; +} + +void networkRecoveryDestroy(NetworkRecovery ** result) +{ + if (!result || !*result) + return; + + NetworkRecovery * recovery = *result; + lgSignalEvent(recovery->queryEvent); + lgFreeEvent(recovery->queryEvent); + LG_LOCK_FREE(recovery->lock); + free(recovery); + *result = NULL; +} + +void networkRecoveryConnectionState(NetworkRecovery * recovery, + bool connected) +{ + uint16_t version = 0; + const bool available = connected && + networkConnectionServiceAvailable(recovery->connection, + LG_NET_SERVICE_RECOVERY, LG_NET_CAPABILITY_RELIABLE) && + networkConnectionServiceVersion(recovery->connection, + LG_NET_SERVICE_RECOVERY, &version); + + LG_LOCK(recovery->lock); + recovery->connected = connected; + recovery->available = available; + recovery->serviceVersion = available ? version : 0; + networkRecoveryClearNL(recovery); + LG_UNLOCK(recovery->lock); + lgSignalEvent(recovery->queryEvent); + + if (available) + { + uint64_t queryID; + networkRecoveryBeginQuery(recovery, &queryID, &version); + } +} + +static bool networkRecoveryInfoPacket(NetworkRecovery * recovery, + const LGNetPacketView * packet) +{ + const LGNetEnvelopeFlags expected = + LG_NET_ENVELOPE_RESPONSE | + LG_NET_ENVELOPE_FINAL | + LG_NET_ENVELOPE_RELIABLE; + if (packet->envelope.flags != expected || + packet->envelope.componentEpoch != 0 || + !packet->envelope.requestID) + return false; + + LGNetRecoveryInfo info; + if (lgNetRecoveryInfoDecode(&info, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK) + return false; + + char version[LG_NET_RECOVERY_MAX_VERSION_LENGTH + 1U]; + if (info.versionLength) + memcpy(version, info.version, info.versionLength); + version[info.versionLength] = '\0'; + info.version = NULL; + + LG_LOCK(recovery->lock); + const bool valid = recovery->connected && recovery->available && + recovery->queryPending && + recovery->queryID == packet->envelope.requestID; + if (valid) + { + if (!recovery->infoValid || + !networkRecoverySequenceNewer( + recovery->info.statusSequence, info.statusSequence)) + { + recovery->info = info; + memcpy(recovery->version, version, info.versionLength + 1U); + } + recovery->infoValid = true; + recovery->queryPending = false; + recovery->queryID = 0; + } + LG_UNLOCK(recovery->lock); + if (valid) + lgSignalEvent(recovery->queryEvent); + return valid; +} + +static bool networkRecoveryStatusPacket(NetworkRecovery * recovery, + const LGNetPacketView * packet) +{ + LGNetRecoveryStatus status; + if (lgNetRecoveryStatusDecode(&status, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + status.requestID != packet->envelope.requestID || + packet->envelope.componentEpoch != 0) + return false; + + const bool terminal = + status.state == LG_NET_RECOVERY_STATE_NORMAL || + status.state == LG_NET_RECOVERY_STATE_ACTIVE || + status.state == LG_NET_RECOVERY_STATE_FAILED; + const LGNetEnvelopeFlags expected = LG_NET_ENVELOPE_RELIABLE | + (status.requestID ? LG_NET_ENVELOPE_RESPONSE : 0) | + (terminal ? LG_NET_ENVELOPE_FINAL : 0); + if (packet->envelope.flags != expected) + return false; + + LG_LOCK(recovery->lock); + struct NetworkRecoveryRequestEntry * request = status.requestID ? + networkRecoveryFindRequestNL(recovery, status.requestID) : NULL; + const bool valid = + recovery->connected && recovery->available && + recovery->infoValid && status.sessionID == recovery->info.sessionID && + (!status.requestID || + (request && request->session == status.sessionID)); + if (valid && (!networkRecoverySequenceNewer( + recovery->info.statusSequence, status.statusSequence))) + { + recovery->info.statusSequence = status.statusSequence; + recovery->info.state = status.state; + recovery->info.error = status.error; + recovery->info.flags = status.flags; + if (request && terminal) + { + request->acknowledged = true; + recovery->ackSerial = request->serial; + recovery->ackRequest = request->request; + } + } + LG_UNLOCK(recovery->lock); + return valid; +} + +bool networkRecoveryPacket(NetworkRecovery * recovery, + const LGNetPacketView * packet, bool datagram) +{ + if (!recovery || !packet || datagram || + packet->envelope.service != LG_NET_SERVICE_RECOVERY) + return false; + + switch (packet->envelope.messageType) + { + case LG_NET_RECOVERY_MESSAGE_INFO: + return networkRecoveryInfoPacket(recovery, packet); + + case LG_NET_RECOVERY_MESSAGE_STATUS: + return networkRecoveryStatusPacket(recovery, packet); + + default: + return false; + } +} + +static bool networkRecoveryCopyInfoNL(const NetworkRecovery * recovery, + LG_RecoveryInfo * result) +{ + LG_RecoveryState state; + LG_RecoveryError error; + if (!recovery->infoValid || + !networkRecoveryState(recovery->info.state, &state) || + !networkRecoveryError(recovery->info.error, &error)) + return false; + + memset(result, 0, sizeof(*result)); + result->abiVersion = recovery->serviceVersion; + if (recovery->info.capabilities & LG_NET_RECOVERY_CAP_DISPLAY) + result->capabilities |= LG_RECOVERY_CAP_DISPLAY; + result->instance = recovery->info.sessionID; + result->heartbeat = (uint32_t)recovery->info.statusSequence; + const size_t versionLength = recovery->info.versionLength < + sizeof(result->producerVersion) - 1U ? + recovery->info.versionLength : sizeof(result->producerVersion) - 1U; + memcpy(result->producerVersion, recovery->version, versionLength); + result->producerVersion[versionLength] = '\0'; + result->versionCount = 1; + memcpy(result->versions[0].component, "Network", sizeof("Network")); + result->versions[0].version = recovery->serviceVersion; + + result->requestSerial = recovery->requestSerial; + result->request = recovery->request; + result->ackSerial = recovery->ackSerial; + result->ackRequest = recovery->ackRequest; + result->state = state; + result->error = error; + return true; +} + +LG_TransportStatus networkRecoveryGetInfo(NetworkRecovery * recovery, + LG_RecoveryInfo * info) +{ + if (!recovery || !info) + return LG_TRANSPORT_ERROR; + + uint64_t queryID; + uint16_t version; + if (!networkRecoveryBeginQuery(recovery, &queryID, &version)) + { + LG_LOCK(recovery->lock); + const bool connected = recovery->connected; + const bool copied = networkRecoveryCopyInfoNL(recovery, info); + LG_UNLOCK(recovery->lock); + if (copied) + return LG_TRANSPORT_OK; + return connected ? LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED; + } + + LG_LOCK(recovery->lock); + bool copied = networkRecoveryCopyInfoNL(recovery, info); + LG_UNLOCK(recovery->lock); + if (copied) + return LG_TRANSPORT_OK; + + lgWaitEvent(recovery->queryEvent, NETWORK_RECOVERY_QUERY_TIMEOUT_MS); + LG_LOCK(recovery->lock); + const bool connected = recovery->connected; + copied = networkRecoveryCopyInfoNL(recovery, info); + LG_UNLOCK(recovery->lock); + if (copied) + return LG_TRANSPORT_OK; + return connected ? LG_TRANSPORT_UNAVAILABLE : LG_TRANSPORT_DISCONNECTED; +} + +LG_TransportStatus networkRecoveryRequest(NetworkRecovery * recovery, + LG_RecoveryRequest request, uint64_t * instance, uint32_t * serial) +{ + if (!recovery || (request != LG_RECOVERY_REQ_NORMAL && + request != LG_RECOVERY_REQ_RECOVERY)) + return LG_TRANSPORT_ERROR; + + LG_RecoveryInfo current; + const LG_TransportStatus infoStatus = + networkRecoveryGetInfo(recovery, ¤t); + if (infoStatus != LG_TRANSPORT_OK) + return infoStatus; + if (!(current.capabilities & LG_RECOVERY_CAP_DISPLAY)) + return LG_TRANSPORT_UNAVAILABLE; + + uint64_t id; + uint32_t requestSerial; + uint16_t version; + LGNetRecoveryRequest wire; + LG_LOCK(recovery->lock); + struct NetworkRecoveryRequestEntry * entry = + networkRecoveryReserveRequestNL(recovery); + if (!entry || !recovery->connected || !recovery->available || + !recovery->infoValid || + recovery->info.sessionID != current.instance) + { + LG_UNLOCK(recovery->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + + id = networkRecoveryNextID(recovery->nextID); + requestSerial = networkRecoveryNextSerial(recovery->nextSerial); + recovery->nextID = id; + recovery->nextSerial = requestSerial; + version = recovery->serviceVersion; + const uint32_t timeout = recovery->info.maxTransitionMs ? + recovery->info.maxTransitionMs : NETWORK_RECOVERY_DEFAULT_TIMEOUT_MS; + wire = (LGNetRecoveryRequest) + { + .requestID = id, + .sessionID = recovery->info.sessionID, + .expectedStatusSequence = recovery->info.statusSequence, + .timeoutMs = timeout > LG_NET_RECOVERY_MAX_TIMEOUT_MS ? + LG_NET_RECOVERY_MAX_TIMEOUT_MS : timeout, + .flags = request == LG_RECOVERY_REQ_RECOVERY ? + LG_NET_RECOVERY_REQUEST_ACTIVE : 0, + }; + *entry = (struct NetworkRecoveryRequestEntry) + { + .id = id, + .session = wire.sessionID, + .serial = requestSerial, + .request = request, + .acknowledged = false, + }; + LG_UNLOCK(recovery->lock); + + uint8_t payload[LG_NET_RECOVERY_REQUEST_WIRE_SIZE]; + if (!lgNetRecoveryRequestEncode(payload, sizeof(payload), &wire)) + goto fail; + + const LG_TransportStatus status = networkConnectionSend( + recovery->connection, LG_NET_SERVICE_RECOVERY, + LG_NET_RECOVERY_MESSAGE_REQUEST, version, + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_FINAL, + 0, id, payload, sizeof(payload)); + if (status != LG_TRANSPORT_OK) + { + LG_LOCK(recovery->lock); + entry = networkRecoveryFindRequestNL(recovery, id); + if (entry) + *entry = (struct NetworkRecoveryRequestEntry) { 0 }; + LG_UNLOCK(recovery->lock); + return status; + } + + LG_LOCK(recovery->lock); + recovery->requestSerial = requestSerial; + recovery->request = request; + LG_UNLOCK(recovery->lock); + if (instance) + *instance = wire.sessionID; + if (serial) + *serial = requestSerial; + return LG_TRANSPORT_OK; + +fail: + LG_LOCK(recovery->lock); + entry = networkRecoveryFindRequestNL(recovery, id); + if (entry) + *entry = (struct NetworkRecoveryRequestEntry) { 0 }; + LG_UNLOCK(recovery->lock); + return LG_TRANSPORT_ERROR; +} diff --git a/client/transports/Network/recovery.h b/client/transports/Network/recovery.h new file mode 100644 index 00000000..c53fb3a0 --- /dev/null +++ b/client/transports/Network/recovery.h @@ -0,0 +1,45 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "connection.h" + +#include "interface/transport.h" + +#include + +#include + +typedef struct NetworkRecovery NetworkRecovery; + +bool networkRecoveryCreate(NetworkRecovery ** recovery, + NetworkConnection * connection); +void networkRecoveryDestroy(NetworkRecovery ** recovery); + +void networkRecoveryConnectionState(NetworkRecovery * recovery, + bool connected); +bool networkRecoveryPacket(NetworkRecovery * recovery, + const LGNetPacketView * packet, bool datagram); + +LG_TransportStatus networkRecoveryGetInfo(NetworkRecovery * recovery, + LG_RecoveryInfo * info); +LG_TransportStatus networkRecoveryRequest(NetworkRecovery * recovery, + LG_RecoveryRequest request, uint64_t * instance, uint32_t * serial); diff --git a/idd/LGIdd/transport/network/CNetworkServer.cpp b/idd/LGIdd/transport/network/CNetworkServer.cpp index b255593e..550cb02c 100644 --- a/idd/LGIdd/transport/network/CNetworkServer.cpp +++ b/idd/LGIdd/transport/network/CNetworkServer.cpp @@ -27,6 +27,7 @@ #include "CDebug.h" #include "CSRWLock.h" +#include "VersionInfo.h" #include #include #include @@ -54,7 +55,9 @@ namespace static constexpr uint32_t NETWORK_INITIAL_RTT_MS = 1; static constexpr uint16_t NETWORK_PEER_BIDI_STREAMS = 16; static constexpr uint16_t NETWORK_PEER_UNIDI_STREAMS = 16; - static constexpr size_t NETWORK_MAX_CONTROL_PAYLOAD = 1024 * 1024; + static constexpr uint32_t NETWORK_MAX_CONTROL_PAYLOAD = + LG_NET_CURSOR_SHAPE_HEADER_WIRE_SIZE + + LG_NET_CURSOR_MAX_SHAPE_LENGTH; static constexpr size_t NETWORK_MAX_CONTROL_BUFFER = LG_NET_ENVELOPE_WIRE_SIZE + NETWORK_MAX_CONTROL_PAYLOAD; static constexpr uint32_t NETWORK_MAX_DATAGRAM_PAYLOAD = 1136; @@ -63,7 +66,8 @@ namespace static constexpr uint16_t NETWORK_KEYBOARD_USAGE_PAGE = 0x07; static constexpr size_t NETWORK_KEYBOARD_KEY_COUNT = 6; static constexpr size_t NETWORK_SERVICE_COUNT = - LG_NET_SERVICE_USB + 1U; + LG_NET_SERVICE_CONTROL + 1U; + static constexpr size_t NETWORK_RECOVERY_REQUESTS = 16; static constexpr uint64_t NETWORK_SEQUENCE_COMPONENT = UINT64_C(1); static constexpr uint8_t NETWORK_DATAGRAM_WINDOW = 64; static constexpr uint32_t NETWORK_PASSWORD_ROUNDS = 100000; @@ -87,6 +91,14 @@ namespace static_assert(NetworkAuthCrypto::SHA256_SIZE == LG_NET_PASSWORD_SHA256_LENGTH, "password proof size mismatch"); + static_assert(NETWORK_MAX_CONTROL_PAYLOAD <= LG_NET_MAX_PAYLOAD_LENGTH, + "control payload limit exceeds the protocol maximum"); + + uint64_t NetworkTimeNs() + { + const uint64_t timestamp = GetTickCount64() * UINT64_C(1000000); + return timestamp ? timestamp : 1; + } bool RandomNonzero(void * data, size_t size) { @@ -224,6 +236,17 @@ struct CNetworkServer::Impl struct Connection { + struct RecoveryRequest + { + uint64_t requestID = 0; + uint64_t sessionID = 0; + uint64_t deadline = 0; + uint64_t sequence = 0; + uint32_t serial = 0; + bool active = false; + bool dispatched = false; + }; + explicit Connection(Impl& owner) : owner(owner) {} ~Connection() @@ -259,6 +282,7 @@ struct CNetworkServer::Impl uint16_t datagramServices = 0; LGNetCapability offeredCapabilities [NETWORK_SERVICE_COUNT] = {}; LGNetCapability selectedCapabilities[NETWORK_SERVICE_COUNT] = {}; + bool datagramSendEnabled = false; bool closing = false; HandshakeState handshake = @@ -275,6 +299,28 @@ struct CNetworkServer::Impl bool inputLEDStateKnown = false; bool keyboardKeys[256] = {}; + RecoveryRequest recoveryRequests[NETWORK_RECOVERY_REQUESTS] = {}; + + uint64_t recoveryStatusSequence = 1; + uint64_t recoveryNextRequestSequence = 1; + uint32_t recoveryNextSerial = 1; + LGNetRecoveryState recoveryState = + LG_NET_RECOVERY_STATE_UNKNOWN; + LGNetRecoveryError recoveryError = + LG_NET_RECOVERY_ERROR_NONE; + LGNetRecoveryFlags recoveryFlags = + LG_NET_RECOVERY_SUPPORTED; + + uint64_t cursorSnapshotRevision = 0; + uint64_t cursorShapeRevision = 0; + uint64_t cursorPositionRevision = 0; + uint64_t cursorColorRevision = 0; + uint64_t cursorStateSequence = 0; + uint64_t cursorAppliedStateSequence = 0; + uint64_t cursorAppliedShapeID = 0; + uint64_t cursorAppliedPositionID = 0; + uint64_t cursorAppliedTransformID = 0; + uint8_t clientNonce[LG_NET_HELLO_NONCE_LENGTH] = {}; uint8_t serverNonce[LG_NET_HELLO_NONCE_LENGTH] = {}; @@ -638,7 +684,8 @@ struct CNetworkServer::Impl if (lgNetCapabilityDecode(&capability, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || capability.service <= LG_NET_SERVICE_CORE || - capability.service >= LG_NET_SERVICE_USB || + capability.service > LG_NET_SERVICE_CONTROL || + capability.service == LG_NET_SERVICE_USB || capability.flags & LG_NET_CAPABILITY_SELECTED) return false; @@ -671,20 +718,47 @@ struct CNetworkServer::Impl { const LGNetServiceVersionInfo * version = lgNetServiceVersionInfo(offered.service); - if (offered.service != LG_NET_SERVICE_INPUT || - !version || + uint32_t minimumPayload; + switch (offered.service) + { + case LG_NET_SERVICE_RECOVERY: + minimumPayload = LG_NET_RECOVERY_REQUEST_WIRE_SIZE; + break; + + case LG_NET_SERVICE_CURSOR: + minimumPayload = LG_NET_CURSOR_POSITION_WIRE_SIZE; + break; + + case LG_NET_SERVICE_INPUT: + minimumPayload = LG_NET_INPUT_ABSOLUTE_WIRE_SIZE; + break; + + case LG_NET_SERVICE_CONTROL: + minimumPayload = LG_NET_CONTROL_FRAME_SCHEDULE_WIRE_SIZE; + break; + + default: + return false; + } + + if (!version || !transport.ServiceEnabled(offered.service) || !(offered.flags & LG_NET_CAPABILITY_RELIABLE) || offered.versionMin > version->current || - offered.versionMax < version->current || - offered.maxPayloadLength < LG_NET_INPUT_ABSOLUTE_WIRE_SIZE || - connection.maxPayload < LG_NET_INPUT_ABSOLUTE_WIRE_SIZE) + offered.versionMax < version->minimum || + offered.maxPayloadLength < minimumPayload || + connection.maxPayload < minimumPayload) + return false; + + const uint16_t selectedVersion = offered.versionMax < version->current ? + offered.versionMax : version->current; + if (selectedVersion < offered.versionMin) return false; selected = {}; selected.service = offered.service; - selected.versionMin = version->current; - selected.versionMax = version->current; + selected.versionMin = selectedVersion; + selected.versionMax = selectedVersion; selected.flags = LG_NET_CAPABILITY_SELECTED | LG_NET_CAPABILITY_RELIABLE; selected.features = 0; @@ -694,9 +768,14 @@ struct CNetworkServer::Impl selected.maxDatagramPayload = 0; selected.maxBidirectionalStreams = 0; selected.maxUnidirectionalStreams = 0; - if ((offered.flags & LG_NET_CAPABILITY_DATAGRAM) && - offered.maxDatagramPayload >= LG_NET_INPUT_ABSOLUTE_WIRE_SIZE && - connection.maxDatagramPayload >= LG_NET_INPUT_ABSOLUTE_WIRE_SIZE) + const uint32_t minimumDatagram = offered.service == + LG_NET_SERVICE_INPUT ? LG_NET_INPUT_ABSOLUTE_WIRE_SIZE : + offered.service == LG_NET_SERVICE_CURSOR ? + LG_NET_CURSOR_POSITION_WIRE_SIZE : 0; + if (minimumDatagram && + (offered.flags & LG_NET_CAPABILITY_DATAGRAM) && + offered.maxDatagramPayload >= minimumDatagram && + connection.maxDatagramPayload >= minimumDatagram) { selected.flags |= LG_NET_CAPABILITY_DATAGRAM; selected.maxDatagramPayload = offered.maxDatagramPayload < @@ -795,8 +874,10 @@ struct CNetworkServer::Impl } for (LGNetService service = LG_NET_SERVICE_RECOVERY; - service < LG_NET_SERVICE_USB; ++service) + service <= LG_NET_SERVICE_CONTROL; ++service) { + if (service == LG_NET_SERVICE_USB) + continue; if (!selected[service].service) continue; @@ -874,8 +955,10 @@ struct CNetworkServer::Impl return false; for (LGNetService service = LG_NET_SERVICE_RECOVERY; - service < LG_NET_SERVICE_USB; ++service) + service <= LG_NET_SERVICE_CONTROL; ++service) { + if (service == LG_NET_SERVICE_USB) + continue; const uint16_t serviceMask = UINT16_C(1) << service; if (!(connection.offeredServices & serviceMask)) continue; @@ -1127,6 +1210,324 @@ struct CNetworkServer::Impl return PublishCapabilities(connection); } + static LGNetRecoveryState RecoveryStateCode(RecoveryState state) + { + switch (state) + { + case RecoveryState::NORMAL: + return LG_NET_RECOVERY_STATE_NORMAL; + + case RecoveryState::ACTIVE: + return LG_NET_RECOVERY_STATE_ACTIVE; + + case RecoveryState::FAILED: + return LG_NET_RECOVERY_STATE_FAILED; + } + + return LG_NET_RECOVERY_STATE_FAILED; + } + + static LGNetRecoveryError RecoveryErrorCode(uint32_t error) + { + switch (error) + { + case ERROR_SUCCESS: + return LG_NET_RECOVERY_ERROR_NONE; + + case ERROR_BUSY: + return LG_NET_RECOVERY_ERROR_BUSY; + + case ERROR_NOT_ENOUGH_QUOTA: + case ERROR_OUTOFMEMORY: + return LG_NET_RECOVERY_ERROR_CAPACITY; + + case ERROR_TIMEOUT: + return LG_NET_RECOVERY_ERROR_TIMEOUT; + + case RPC_S_SERVER_UNAVAILABLE: + return LG_NET_RECOVERY_ERROR_HELPER_UNAVAILABLE; + + default: + return LG_NET_RECOVERY_ERROR_TOPOLOGY_FAILED; + } + } + + static LGNetRecoveryFlags RecoveryFlags( + LGNetRecoveryState state, bool active) + { + LGNetRecoveryFlags flags = LG_NET_RECOVERY_SUPPORTED; + if (state == LG_NET_RECOVERY_STATE_ACTIVE || + (state == LG_NET_RECOVERY_STATE_SWITCHING && active)) + flags |= LG_NET_RECOVERY_ACTIVE; + return flags; + } + + bool SendRecoveryStatus(Connection& connection, uint64_t requestID, + LGNetRecoveryState state, LGNetRecoveryError error, bool active, + uint32_t detail, LGNetEnvelopeFlags envelopeFlags, bool updateState) + { + LGNetRecoveryStatus status = {}; + { + CSRWExclusiveLock guard(connection.lock); + if (connection.closing || + connection.recoveryStatusSequence == UINT64_MAX) + return false; + + status.requestID = requestID; + status.sessionID = connection.sessionEpoch; + status.statusSequence = ++connection.recoveryStatusSequence; + status.state = state; + status.error = error; + status.flags = RecoveryFlags(state, active); + status.detail = detail; + if (updateState) + { + connection.recoveryState = state; + connection.recoveryError = error; + connection.recoveryFlags = status.flags; + } + } + + uint8_t payload[LG_NET_RECOVERY_STATUS_WIRE_SIZE]; + return lgNetRecoveryStatusEncode(payload, sizeof(payload), &status) && + SendPacket(connection, LG_NET_SERVICE_RECOVERY, + LG_NET_RECOVERY_MESSAGE_STATUS, + envelopeFlags, + 0, requestID, payload, sizeof(payload)); + } + + bool SendCurrentRecoveryStatus(Connection& connection, + uint64_t requestID, uint32_t detail) + { + LGNetRecoveryStatus status = {}; + { + CSRWSharedLock guard(connection.lock); + status.requestID = requestID; + status.sessionID = connection.sessionEpoch; + status.statusSequence = connection.recoveryStatusSequence; + status.state = connection.recoveryState; + status.error = connection.recoveryError; + status.flags = connection.recoveryFlags; + status.detail = detail; + } + + uint8_t payload[LG_NET_RECOVERY_STATUS_WIRE_SIZE]; + return lgNetRecoveryStatusEncode(payload, sizeof(payload), &status) && + SendPacket(connection, LG_NET_SERVICE_RECOVERY, + LG_NET_RECOVERY_MESSAGE_STATUS, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, + 0, requestID, payload, sizeof(payload)); + } + + bool SendRecoveryInfo(Connection& connection, + const LGNetPacketView& packet) + { + if (packet.payloadLength || packet.envelope.componentEpoch || + !packet.envelope.requestID) + return false; + + static const char version[] = LG_VERSION_STR; + + LGNetRecoveryInfo info = {}; + { + CSRWSharedLock guard(connection.lock); + info.sessionID = connection.sessionEpoch; + info.statusSequence = connection.recoveryStatusSequence; + info.capabilities = LG_NET_RECOVERY_CAP_DISPLAY; + info.state = connection.recoveryState; + info.error = connection.recoveryError; + info.flags = connection.recoveryFlags; + info.maxTransitionMs = LG_NET_RECOVERY_MAX_TIMEOUT_MS; + info.versionLength = static_cast(sizeof(version) - 1U); + info.version = reinterpret_cast(version); + } + + const size_t payloadSize = lgNetRecoveryInfoSize(&info); + std::vector payload; + try + { + payload.resize(payloadSize); + } + catch (...) + { + return false; + } + + return payloadSize && + lgNetRecoveryInfoEncode(payload.data(), payload.size(), &info) && + SendPacket(connection, LG_NET_SERVICE_RECOVERY, + LG_NET_RECOVERY_MESSAGE_INFO, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, + 0, packet.envelope.requestID, payload.data(), + static_cast(payload.size())); + } + + uint32_t AllocateRecoverySerialLocked(Connection& connection) + { + for (uint64_t attempt = 0; attempt < UINT32_MAX; ++attempt) + { + const uint32_t serial = connection.recoveryNextSerial++; + if (!connection.recoveryNextSerial) + connection.recoveryNextSerial = 1; + if (!serial) + continue; + + bool used = false; + for (const Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (request.requestID && request.serial == serial) + { + used = true; + break; + } + if (!used) + return serial; + } + return 0; + } + + Connection::RecoveryRequest * AllocateRecoveryRequestLocked( + Connection& connection, uint64_t requestID) + { + for (const Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (request.requestID == requestID) + return nullptr; + + for (Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (!request.requestID) + return &request; + return nullptr; + } + + static bool DeadlineReached(uint64_t now, uint64_t deadline) + { + return static_cast(now - deadline) >= 0; + } + + static void RemoveRecoveryRequest(Connection& connection, + uint32_t serial) + { + CSRWExclusiveLock guard(connection.lock); + for (Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (request.requestID && request.serial == serial) + { + request = {}; + return; + } + } + + bool HandleRecoveryRequest(Connection& connection, + const LGNetPacketView& packet) + { + LGNetRecoveryRequest request; + if (lgNetRecoveryRequestDecode(&request, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + packet.envelope.componentEpoch || + packet.envelope.requestID != request.requestID) + return false; + + std::shared_ptr session; + uint64_t statusSequence; + { + CSRWSharedLock guard(connection.lock); + if (request.sessionID != connection.sessionEpoch) + return false; + session = connection.session; + statusSequence = connection.recoveryStatusSequence; + } + if (!session) + return false; + + if (request.expectedStatusSequence != statusSequence && + !(request.flags & LG_NET_RECOVERY_REQUEST_FORCE)) + return SendCurrentRecoveryStatus( + connection, request.requestID, ERROR_REVISION_MISMATCH); + + const bool active = + (request.flags & LG_NET_RECOVERY_REQUEST_ACTIVE) != 0; + uint32_t serial = 0; + bool dispatchNow = true; + { + CSRWExclusiveLock guard(connection.lock); + Connection::RecoveryRequest * correlation = + AllocateRecoveryRequestLocked(connection, request.requestID); + if (correlation) + { + for (const Connection::RecoveryRequest& pending : + connection.recoveryRequests) + if (pending.requestID && pending.active != active) + { + correlation = nullptr; + break; + } + } + if (correlation) + { + serial = AllocateRecoverySerialLocked(connection); + if (serial) + { + for (const Connection::RecoveryRequest& pending : + connection.recoveryRequests) + if (pending.requestID) + { + dispatchNow = false; + break; + } + + correlation->requestID = request.requestID; + correlation->sessionID = request.sessionID; + correlation->deadline = GetTickCount64() + request.timeoutMs; + correlation->sequence = + connection.recoveryNextRequestSequence++; + if (!connection.recoveryNextRequestSequence) + connection.recoveryNextRequestSequence = 1; + correlation->serial = serial; + correlation->active = active; + correlation->dispatched = dispatchNow; + } + } + } + + if (!serial) + return SendRecoveryStatus(connection, request.requestID, + LG_NET_RECOVERY_STATE_FAILED, LG_NET_RECOVERY_ERROR_BUSY, + active, ERROR_BUSY, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, false); + + if (dispatchNow && + !session->RequestRecovery(request.sessionID, serial, active)) + { + RemoveRecoveryRequest(connection, serial); + return SendRecoveryStatus(connection, request.requestID, + LG_NET_RECOVERY_STATE_FAILED, LG_NET_RECOVERY_ERROR_CAPACITY, + active, ERROR_NOT_ENOUGH_QUOTA, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, false); + } + + return SendRecoveryStatus(connection, request.requestID, + LG_NET_RECOVERY_STATE_SWITCHING, LG_NET_RECOVERY_ERROR_NONE, + active, ERROR_SUCCESS, LG_NET_ENVELOPE_RESPONSE, true); + } + + bool HandleRecovery(Connection& connection, + const LGNetPacketView& packet) + { + switch (packet.envelope.messageType) + { + case LG_NET_RECOVERY_MESSAGE_GET_INFO: + return SendRecoveryInfo(connection, packet); + + case LG_NET_RECOVERY_MESSAGE_REQUEST: + return HandleRecoveryRequest(connection, packet); + + default: + return false; + } + } + static LGNetInputStatusCode InputStatusCode(InputResult result) { switch (result) @@ -1542,13 +1943,481 @@ struct CNetworkServer::Impl } } + bool SendControlStatus(Connection& connection, + const NetworkControlStatus& source, LGNetEnvelopeFlags flags) + { + LGNetControlStatus status = {}; + status.controlID = source.controlID; + status.status = source.status; + status.detail = source.detail; + + uint8_t payload[LG_NET_CONTROL_STATUS_WIRE_SIZE]; + return lgNetControlStatusEncode(payload, sizeof(payload), &status) && + SendPacket(connection, LG_NET_SERVICE_CONTROL, + LG_NET_CONTROL_MESSAGE_STATUS, flags, + 0, status.controlID, payload, sizeof(payload)); + } + + bool HandleControl(Connection& connection, + const LGNetPacketView& packet) + { + if (packet.envelope.componentEpoch || !packet.envelope.requestID) + return false; + + std::shared_ptr session; + uint32_t client; + { + CSRWSharedLock guard(connection.lock); + session = connection.session; + client = connection.client; + } + if (!session) + return false; + + uint64_t controlID = 0; + bool queued = false; + switch (packet.envelope.messageType) + { + case LG_NET_CONTROL_MESSAGE_CURSOR_POSITION: + { + LGNetControlCursorPosition position; + if (lgNetControlCursorPositionDecode(&position, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + packet.envelope.requestID != position.controlID) + return false; + controlID = position.controlID; + queued = session->SetCursorPosition( + position.controlID, position.x, position.y); + break; + } + + case LG_NET_CONTROL_MESSAGE_DISPLAY_SIZE: + { + LGNetControlDisplaySize display; + if (lgNetControlDisplaySizeDecode(&display, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + packet.envelope.requestID != display.controlID) + return false; + controlID = display.controlID; + queued = session->SetResolution( + display.controlID, display.width, display.height); + break; + } + + case LG_NET_CONTROL_MESSAGE_FRAME_SCHEDULE: + { + LGNetControlFrameSchedule schedule; + if (lgNetControlFrameScheduleDecode(&schedule, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + packet.envelope.requestID != schedule.controlID) + return false; + + FrameScheduleUpdate update = {}; + update.clientID = client; + update.generation = schedule.generation; + update.period = schedule.periodNs; + update.targetSlack = schedule.targetSlackNs; + update.phaseError = schedule.phaseErrorNs; + update.feedbackFrameSerial = schedule.feedbackFrameSerial; + update.feedbackScheduleEpoch = schedule.feedbackScheduleEpoch; + update.feedbackDeadlineSerial = schedule.feedbackDeadlineSerial; + update.lease = schedule.leaseMs; + if (schedule.flags & LG_NET_CONTROL_FRAME_SCHEDULE_ACTIVE) + update.flags |= FRAME_SCHEDULE_ACTIVE; + if (schedule.flags & LG_NET_CONTROL_FRAME_SCHEDULE_RELEASE) + update.flags |= FRAME_SCHEDULE_RELEASE; + if (schedule.flags & LG_NET_CONTROL_FRAME_SCHEDULE_RESET) + update.flags |= FRAME_SCHEDULE_RESET; + if (schedule.flags & LG_NET_CONTROL_FRAME_SCHEDULE_IMMEDIATE) + update.flags |= FRAME_SCHEDULE_IMMEDIATE; + controlID = schedule.controlID; + queued = session->SetFrameSchedule(schedule.controlID, update); + break; + } + + default: + return false; + } + + NetworkControlStatus status; + status.controlID = controlID; + status.status = queued ? + LG_NET_CONTROL_STATUS_PENDING : LG_NET_CONTROL_STATUS_BUSY; + status.detail = queued ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_QUOTA; + return SendControlStatus(connection, status, + LG_NET_ENVELOPE_RESPONSE | + (queued ? 0 : LG_NET_ENVELOPE_FINAL)); + } + + bool HandleCursorStatus(Connection& connection, + const LGNetPacketView& packet) + { + LGNetCursorStatus status; + if (packet.envelope.messageType != LG_NET_CURSOR_MESSAGE_STATUS || + packet.envelope.componentEpoch || + lgNetCursorStatusDecode(&status, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + packet.envelope.requestID != status.stateSequence) + return false; + + CSRWExclusiveLock guard(connection.lock); + if (!status.stateSequence || + status.stateSequence > connection.cursorStateSequence || + status.appliedShapeID > connection.cursorShapeRevision || + status.appliedPositionID > connection.cursorPositionRevision || + status.appliedTransformID) + return false; + if (status.stateSequence < connection.cursorAppliedStateSequence) + return true; + + connection.cursorAppliedStateSequence = status.stateSequence; + connection.cursorAppliedShapeID = status.appliedShapeID; + connection.cursorAppliedPositionID = status.appliedPositionID; + connection.cursorAppliedTransformID = status.appliedTransformID; + if (status.status != LG_NET_CURSOR_STATUS_APPLIED) + { + connection.cursorSnapshotRevision = 0; + connection.cursorShapeRevision = 0; + connection.cursorPositionRevision = 0; + connection.cursorColorRevision = 0; + } + return true; + } + + bool SendCursorShape(Connection& connection, + const NetworkControlSnapshot& snapshot) + { + LGNetCursorShape shape = {}; + shape.shapeID = snapshot.shapeRevision; + shape.width = snapshot.cursor.CursorShapeInfo.Width; + shape.height = snapshot.cursor.CursorShapeInfo.Height; + shape.pitch = snapshot.cursor.CursorShapeInfo.Pitch; + shape.dataLength = static_cast( + snapshot.cursorData.size()); + shape.format = snapshot.cursor.CursorShapeInfo.CursorType == + IDDCX_CURSOR_SHAPE_TYPE_ALPHA ? + LG_NET_CURSOR_FORMAT_BGRA8_PREMULTIPLIED : + LG_NET_CURSOR_FORMAT_MASKED_COLOR; + shape.hotspotX = snapshot.cursor.CursorShapeInfo.XHot; + shape.hotspotY = snapshot.cursor.CursorShapeInfo.YHot; + shape.data = snapshot.cursorData.data(); + + const size_t payloadSize = lgNetCursorShapeSize(&shape); + std::vector payload; + try + { + payload.resize(payloadSize); + } + catch (...) + { + return false; + } + + return payloadSize && + lgNetCursorShapeEncode(payload.data(), payload.size(), &shape) && + SendPacket(connection, LG_NET_SERVICE_CURSOR, + LG_NET_CURSOR_MESSAGE_SHAPE, LG_NET_ENVELOPE_FINAL, + 0, shape.shapeID, payload.data(), + static_cast(payload.size())); + } + + bool SendCursorPosition(Connection& connection, + const NetworkControlSnapshot& snapshot) + { + LGNetCursorPosition position = {}; + position.updateID = snapshot.positionRevision; + position.timestampNs = NetworkTimeNs(); + position.x = snapshot.cursor.X; + position.y = snapshot.cursor.Y; + position.desktopWidth = LG_NET_CURSOR_MAX_DESKTOP_WIDTH; + position.desktopHeight = LG_NET_CURSOR_MAX_DESKTOP_HEIGHT; + position.flags = snapshot.cursor.IsCursorVisible ? + LG_NET_CURSOR_POSITION_VISIBLE : 0; + position.displayID = 0; + + uint8_t payload[LG_NET_CURSOR_POSITION_WIRE_SIZE]; + return lgNetCursorPositionEncode( + payload, sizeof(payload), &position) && + SendPacket(connection, LG_NET_SERVICE_CURSOR, + LG_NET_CURSOR_MESSAGE_POSITION, LG_NET_ENVELOPE_FINAL, + 0, position.updateID, payload, sizeof(payload)); + } + + bool SendCursorColorTransform(Connection& connection, + const NetworkControlSnapshot& snapshot) + { + std::vector lut; + LGNetCursorColorTransform transform = {}; + transform.updateID = snapshot.colorRevision; + transform.sdrWhiteLevel = snapshot.sdrWhiteLevel ? + snapshot.sdrWhiteLevel : LG_SDR_WHITE_LEVEL_DEFAULT; + transform.scalar = 1.0f; + if (snapshot.transform) + { + if (snapshot.transform->matrixEnabled) + transform.flags |= LG_NET_CURSOR_COLOR_TRANSFORM_MATRIX; + if (snapshot.transform->lutEnabled) + transform.flags |= LG_NET_CURSOR_COLOR_TRANSFORM_LUT; + memcpy(transform.matrix, snapshot.transform->matrix, + sizeof(transform.matrix)); + transform.scalar = snapshot.transform->scalar; + + if (snapshot.transform->lutEnabled) + { + try + { + lut.resize(LG_NET_CURSOR_COLOR_LUT_BYTES); + } + catch (...) + { + return false; + } + if (!lgNetCursorColorLUTEncode(lut.data(), lut.size(), + &snapshot.transform->lut[0][0])) + return false; + transform.lut = lut.data(); + } + } + + const size_t payloadSize = lgNetCursorColorTransformSize(&transform); + std::vector payload; + try + { + payload.resize(payloadSize); + } + catch (...) + { + return false; + } + + return payloadSize && lgNetCursorColorTransformEncode( + payload.data(), payload.size(), &transform) && + SendPacket(connection, LG_NET_SERVICE_CURSOR, + LG_NET_CURSOR_MESSAGE_COLOR_TRANSFORM, LG_NET_ENVELOPE_FINAL, + 0, transform.updateID, payload.data(), + static_cast(payload.size())); + } + + bool SendCursorState(Connection& connection, + const NetworkControlSnapshot& snapshot, bool shapeAvailable) + { + LGNetCursorState state = {}; + state.stateSequence = snapshot.stateRevision; + state.flags = snapshot.cursor.IsCursorVisible ? + LG_NET_CURSOR_STATE_VISIBLE : 0; + if (shapeAvailable) + { + state.shapeID = snapshot.shapeRevision; + state.flags |= LG_NET_CURSOR_STATE_SHAPE_VALID; + } + if (snapshot.cursorValid) + { + state.positionID = snapshot.positionRevision; + state.flags |= LG_NET_CURSOR_STATE_POSITION_VALID; + } + state.displayID = 0; + + uint8_t payload[LG_NET_CURSOR_STATE_WIRE_SIZE]; + return lgNetCursorStateEncode(payload, sizeof(payload), &state) && + SendPacket(connection, LG_NET_SERVICE_CURSOR, + LG_NET_CURSOR_MESSAGE_STATE, LG_NET_ENVELOPE_FINAL, + 0, state.stateSequence, payload, sizeof(payload)); + } + + bool PublishCursor(Connection& connection) + { + uint64_t knownRevision; + uint64_t shapeRevision; + uint64_t positionRevision; + uint64_t colorRevision; + uint32_t maxPayload; + uint16_t serviceVersion; + { + CSRWSharedLock guard(connection.lock); + const uint16_t serviceMask = + UINT16_C(1) << LG_NET_SERVICE_CURSOR; + if (connection.handshake != HandshakeState::READY || + !(connection.selectedServices & serviceMask)) + return true; + knownRevision = connection.cursorSnapshotRevision; + shapeRevision = connection.cursorShapeRevision; + positionRevision = connection.cursorPositionRevision; + colorRevision = connection.cursorColorRevision; + maxPayload = connection.selectedCapabilities + [LG_NET_SERVICE_CURSOR].maxPayloadLength; + serviceVersion = connection.selectedCapabilities + [LG_NET_SERVICE_CURSOR].versionMin; + } + + NetworkControlSnapshot snapshot; + if (!transport.ControlSnapshot(snapshot, knownRevision)) + return true; + + bool shapeAvailable = snapshot.shapeValid; + if (snapshot.shapeValid) + { + const uint64_t shapeSize = + LG_NET_CURSOR_SHAPE_HEADER_WIRE_SIZE + snapshot.cursorData.size(); + shapeAvailable = shapeSize <= maxPayload; + if (snapshot.shapeRevision != shapeRevision && shapeAvailable && + !SendCursorShape(connection, snapshot)) + return false; + } + if (snapshot.cursorValid && + snapshot.positionRevision != positionRevision && + !SendCursorPosition(connection, snapshot)) + return false; + const uint32_t colorPayloadSize = + LG_NET_CURSOR_COLOR_TRANSFORM_HEADER_WIRE_SIZE + + (snapshot.transform && snapshot.transform->lutEnabled ? + LG_NET_CURSOR_COLOR_LUT_BYTES : 0); + if (snapshot.colorRevision != colorRevision && + serviceVersion >= + LG_NET_CURSOR_COLOR_TRANSFORM_INTRODUCED_SERVICE_VERSION && + colorPayloadSize <= maxPayload && + !SendCursorColorTransform(connection, snapshot)) + return false; + if (!SendCursorState(connection, snapshot, shapeAvailable)) + return false; + + { + CSRWExclusiveLock guard(connection.lock); + connection.cursorSnapshotRevision = snapshot.stateRevision; + connection.cursorShapeRevision = snapshot.shapeRevision; + connection.cursorPositionRevision = snapshot.positionRevision; + connection.cursorColorRevision = snapshot.colorRevision; + connection.cursorStateSequence = snapshot.stateRevision; + } + return true; + } + + bool DispatchQueuedRecovery(Connection& connection, + const std::shared_ptr& session) + { + for (;;) + { + Connection::RecoveryRequest selected; + { + CSRWExclusiveLock guard(connection.lock); + for (const Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (request.requestID && request.dispatched) + return true; + + Connection::RecoveryRequest * candidate = nullptr; + for (Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (request.requestID && + (!candidate || request.sequence < candidate->sequence)) + candidate = &request; + if (!candidate) + return true; + + candidate->dispatched = true; + selected = *candidate; + } + + if (session->RequestRecovery( + selected.sessionID, selected.serial, selected.active)) + return true; + + RemoveRecoveryRequest(connection, selected.serial); + if (!SendRecoveryStatus(connection, selected.requestID, + LG_NET_RECOVERY_STATE_FAILED, + LG_NET_RECOVERY_ERROR_CAPACITY, selected.active, + ERROR_NOT_ENOUGH_QUOTA, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, false)) + return false; + } + } + + bool PumpSession(Connection& connection, uint64_t now) + { + std::shared_ptr session; + { + CSRWSharedLock guard(connection.lock); + if (connection.handshake != HandshakeState::READY) + return true; + session = connection.session; + } + if (!session) + return false; + + NetworkRecoveryStatus recovery; + while (session->TakeRecoveryStatus(recovery)) + { + Connection::RecoveryRequest correlation; + { + CSRWExclusiveLock guard(connection.lock); + for (Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (request.requestID && request.sessionID == recovery.session && + request.serial == recovery.serial && + request.active == recovery.active) + { + correlation = request; + request = {}; + break; + } + } + if (!correlation.requestID) + continue; + + LGNetRecoveryState state = RecoveryStateCode(recovery.state); + LGNetRecoveryError error = RecoveryErrorCode(recovery.error); + if (state == LG_NET_RECOVERY_STATE_FAILED && + error == LG_NET_RECOVERY_ERROR_NONE) + error = LG_NET_RECOVERY_ERROR_TOPOLOGY_FAILED; + if (state != LG_NET_RECOVERY_STATE_FAILED) + error = LG_NET_RECOVERY_ERROR_NONE; + if (!SendRecoveryStatus(connection, correlation.requestID, + state, error, recovery.active, recovery.error, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, true)) + return false; + } + + Connection::RecoveryRequest expired[NETWORK_RECOVERY_REQUESTS] = {}; + size_t expiredCount = 0; + { + CSRWExclusiveLock guard(connection.lock); + for (Connection::RecoveryRequest& request : + connection.recoveryRequests) + if (request.requestID && DeadlineReached(now, request.deadline)) + { + expired[expiredCount++] = request; + request = {}; + } + } + for (size_t index = 0; index < expiredCount; ++index) + if (!SendRecoveryStatus(connection, expired[index].requestID, + LG_NET_RECOVERY_STATE_FAILED, LG_NET_RECOVERY_ERROR_TIMEOUT, + expired[index].active, ERROR_TIMEOUT, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL, true)) + return false; + + if (!DispatchQueuedRecovery(connection, session)) + return false; + + NetworkControlStatus control; + while (session->TakeControlStatus(control)) + if (!SendControlStatus(connection, control, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL)) + return false; + + return PublishCursor(connection); + } + bool HandlePacket(Connection& connection, const LGNetPacketView& packet) { + const bool cursorStatus = + packet.envelope.service == LG_NET_SERVICE_CURSOR && + packet.envelope.messageType == LG_NET_CURSOR_MESSAGE_STATUS; if (packet.envelope.headerSize != LG_NET_ENVELOPE_WIRE_SIZE || packet.envelope.payloadLength > NETWORK_MAX_CONTROL_PAYLOAD || - packet.envelope.flags & (LG_NET_ENVELOPE_RESPONSE | - LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_ERROR | - LG_NET_ENVELOPE_EARLY_DATA) || + packet.envelope.flags & (LG_NET_ENVELOPE_DATAGRAM | + LG_NET_ENVELOPE_ERROR | LG_NET_ENVELOPE_EARLY_DATA) || + !!(packet.envelope.flags & LG_NET_ENVELOPE_RESPONSE) != + cursorStatus || (packet.envelope.flags & (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL)) != (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL) || @@ -1638,8 +2507,23 @@ struct CNetworkServer::Impl return false; } - return packet.envelope.service == LG_NET_SERVICE_INPUT && - HandleInput(connection, packet, false); + switch (packet.envelope.service) + { + case LG_NET_SERVICE_RECOVERY: + return HandleRecovery(connection, packet); + + case LG_NET_SERVICE_CURSOR: + return HandleCursorStatus(connection, packet); + + case LG_NET_SERVICE_INPUT: + return HandleInput(connection, packet, false); + + case LG_NET_SERVICE_CONTROL: + return HandleControl(connection, packet); + + default: + return false; + } } bool HandleDatagram(Connection& connection, const QUIC_BUFFER * buffer) @@ -1788,6 +2672,7 @@ struct CNetworkServer::Impl { const uint32_t client = connection.client; const uint32_t generation = connection.generation; + std::shared_ptr session; { CSRWExclusiveLock guard(lock); const auto found = std::find( @@ -1796,9 +2681,13 @@ struct CNetworkServer::Impl connections.erase(found); } - if (connection.session) + { + CSRWExclusiveLock guard(connection.lock); + session = std::move(connection.session); + } + if (session) transport.RemoveSession(client, generation); - connection.session.reset(); + session.reset(); delete &connection; DEBUG_INFO("Network client %u disconnected", client); } @@ -2037,7 +2926,11 @@ struct CNetworkServer::Impl } if (!handle) + { + if (!PumpSession(*connection, now)) + Shutdown(*connection, NETWORK_ERROR_PROTOCOL); continue; + } DEBUG_WARN("Closing network client %u after an incomplete handshake", client); diff --git a/idd/LGIdd/transport/network/CNetworkSession.cpp b/idd/LGIdd/transport/network/CNetworkSession.cpp index 62b72bb2..aaf62b79 100644 --- a/idd/LGIdd/transport/network/CNetworkSession.cpp +++ b/idd/LGIdd/transport/network/CNetworkSession.cpp @@ -20,8 +20,46 @@ #include "transport/network/CNetworkSession.h" +#include #include +namespace +{ + uint64_t NextRevision(uint64_t revision) + { + ++revision; + if (!revision) + ++revision; + return revision; + } + + LGNetControlStatusCode ControlStatusCode(InteractionResult result) + { + switch (result) + { + case InteractionResult::ACCEPTED: + return LG_NET_CONTROL_STATUS_APPLIED; + + case InteractionResult::BUSY: + return LG_NET_CONTROL_STATUS_BUSY; + + case InteractionResult::UNAVAILABLE: + return LG_NET_CONTROL_STATUS_UNSUPPORTED; + + case InteractionResult::STALE: + return LG_NET_CONTROL_STATUS_STALE; + + case InteractionResult::REJECTED: + return LG_NET_CONTROL_STATUS_INVALID; + + case InteractionResult::FAILED: + return LG_NET_CONTROL_STATUS_ERROR; + } + + return LG_NET_CONTROL_STATUS_ERROR; + } +} + bool CNetworkEventQueue::Push(const NetworkEvent& event) { CSRWExclusiveLock lock(m_lock); @@ -147,47 +185,87 @@ ControlResult CNetworkControlSink::SendCursor( { const bool shape = info.CursorShapeInfo.CursorType != IDDCX_CURSOR_SHAPE_TYPE_UNINITIALIZED; + if (shape && info.CursorShapeInfo.CursorType != + IDDCX_CURSOR_SHAPE_TYPE_ALPHA && + info.CursorShapeInfo.CursorType != + IDDCX_CURSOR_SHAPE_TYPE_MASKED_COLOR) + return ControlResult::FAILED; if (shape && info.CursorShapeInfo.Height && info.CursorShapeInfo.Pitch > SIZE_MAX / info.CursorShapeInfo.Height) return ControlResult::FAILED; const size_t shapeSize = shape ? static_cast( info.CursorShapeInfo.Height) * info.CursorShapeInfo.Pitch : 0; - if (size != shapeSize || (size && !data)) + if (size != shapeSize || (size && !data) || + shapeSize > LG_NET_CURSOR_MAX_SHAPE_LENGTH || + (shape && (!info.CursorShapeInfo.Width || + info.CursorShapeInfo.Width > LG_NET_CURSOR_MAX_WIDTH || + !info.CursorShapeInfo.Height || + info.CursorShapeInfo.Height > LG_NET_CURSOR_MAX_HEIGHT || + info.CursorShapeInfo.XHot >= info.CursorShapeInfo.Width || + info.CursorShapeInfo.YHot >= info.CursorShapeInfo.Height || + info.CursorShapeInfo.Pitch < info.CursorShapeInfo.Width * 4U))) return ControlResult::FAILED; std::vector cursorData; - if (shapeSize) - cursorData.assign(data, data + size); + try + { + if (shapeSize) + cursorData.assign(data, data + size); + } + catch (...) + { + return ControlResult::FAILED; + } CSRWExclusiveLock lock(m_lock); if (!m_events) return ControlResult::RETRY; + const UINT whiteLevel = sdrWhiteLevel ? + sdrWhiteLevel : LG_SDR_WHITE_LEVEL_DEFAULT; + if (m_state.sdrWhiteLevel != whiteLevel) + { + m_state.sdrWhiteLevel = whiteLevel; + m_state.colorRevision = NextRevision(m_state.colorRevision); + } m_state.cursor.IsCursorVisible = info.IsCursorVisible; m_state.cursor.X = info.X; m_state.cursor.Y = info.Y; - m_state.sdrWhiteLevel = sdrWhiteLevel; m_state.cursorValid = true; + m_state.positionRevision = NextRevision(m_state.positionRevision); if (shape) { m_state.cursor.IsCursorShapeUpdated = info.IsCursorShapeUpdated; m_state.cursor.CursorShapeInfo = info.CursorShapeInfo; m_state.cursorData = std::move(cursorData); m_state.shapeValid = true; + m_state.shapeRevision = NextRevision(m_state.shapeRevision); } - ++m_state.revision; + m_state.stateRevision = NextRevision(m_state.stateRevision); return ControlResult::APPLIED; } ControlResult CNetworkControlSink::SetColorTransform( std::shared_ptr transform) { + std::shared_ptr copy; + try + { + if (transform) + copy = std::make_shared(*transform); + } + catch (...) + { + return ControlResult::FAILED; + } + CSRWExclusiveLock lock(m_lock); if (!m_events) return ControlResult::RETRY; - m_state.transform = std::move(transform); + m_state.transform = std::move(copy); m_state.transformValid = true; - ++m_state.revision; + m_state.colorRevision = NextRevision(m_state.colorRevision); + m_state.stateRevision = NextRevision(m_state.stateRevision); return ControlResult::APPLIED; } @@ -198,12 +276,21 @@ void CNetworkControlSink::RequestReplay() m_events->OnControlReplay(m_token); } -bool CNetworkControlSink::Snapshot(NetworkControlSnapshot& state) const +bool CNetworkControlSink::Snapshot( + NetworkControlSnapshot& state, uint64_t knownRevision) const { CSRWSharedLock lock(m_lock); - if (!m_events) + if (!m_events || !m_state.stateRevision || + m_state.stateRevision == knownRevision) return false; - state = m_state; + try + { + state = m_state; + } + catch (...) + { + return false; + } return true; } @@ -243,31 +330,50 @@ void CNetworkSession::Close() m_input.Release(m_source, true); } -bool CNetworkSession::SetCursorPosition(int32_t x, int32_t y) +bool CNetworkSession::SetCursorPosition( + uint64_t controlID, int32_t x, int32_t y) { CSRWSharedLock lock(m_lifecycleLock); - if (!m_open) + if (!m_open || !controlID) return false; NetworkEvent event; - event.type = NetworkEventType::SET_CURSOR_POSITION; - event.source = EventSource(); - event.x = x; - event.y = y; + event.type = NetworkEventType::SET_CURSOR_POSITION; + event.source = EventSource(); + event.controlID = controlID; + event.x = x; + event.y = y; return m_events.Push(event); } -bool CNetworkSession::SetResolution(uint32_t width, uint32_t height) +bool CNetworkSession::SetResolution( + uint64_t controlID, uint32_t width, uint32_t height) { CSRWSharedLock lock(m_lifecycleLock); - if (!m_open || !width || !height) + if (!m_open || !controlID || !width || !height) return false; NetworkEvent event; - event.type = NetworkEventType::SET_RESOLUTION; - event.source = EventSource(); - event.width = width; - event.height = height; + event.type = NetworkEventType::SET_RESOLUTION; + event.source = EventSource(); + event.controlID = controlID; + event.width = width; + event.height = height; + return m_events.Push(event); +} + +bool CNetworkSession::SetFrameSchedule( + uint64_t controlID, const FrameScheduleUpdate& schedule) +{ + CSRWSharedLock lock(m_lifecycleLock); + if (!m_open || !controlID) + return false; + + NetworkEvent event; + event.type = NetworkEventType::SET_FRAME_SCHEDULE; + event.source = EventSource(); + event.schedule = schedule; + event.controlID = controlID; return m_events.Push(event); } @@ -298,12 +404,33 @@ void CNetworkSession::ProcessEvent( switch (event.type) { case NetworkEventType::SET_CURSOR_POSITION: - events.OnSetCursorPos(event.source, event.x, event.y); + { + NetworkControlStatus status; + status.controlID = event.controlID; + status.status = ControlStatusCode( + events.OnSetCursorPos(event.source, event.x, event.y)); + SetControlStatus(status); break; + } case NetworkEventType::SET_RESOLUTION: - events.OnSetResolution(event.source, event.width, event.height); + { + NetworkControlStatus status; + status.controlID = event.controlID; + status.status = ControlStatusCode(events.OnSetResolution( + event.source, event.width, event.height)); + SetControlStatus(status); break; + } + + case NetworkEventType::SET_FRAME_SCHEDULE: + { + NetworkControlStatus status; + status.controlID = event.controlID; + status.status = LG_NET_CONTROL_STATUS_UNSUPPORTED; + SetControlStatus(status); + break; + } case NetworkEventType::RECOVERY_REQUEST: { @@ -356,6 +483,36 @@ bool CNetworkSession::TakeRecoveryStatus(NetworkRecoveryStatus& status) return true; } +void CNetworkSession::SetControlStatus( + const NetworkControlStatus& status) +{ + CSRWExclusiveLock lock(m_controlLock); + if (m_controlCount == CONTROL_CAPACITY) + { + m_control[m_controlHead] = {}; + m_controlHead = (m_controlHead + 1) % CONTROL_CAPACITY; + --m_controlCount; + } + + const unsigned tail = + (m_controlHead + m_controlCount) % CONTROL_CAPACITY; + m_control[tail] = status; + ++m_controlCount; +} + +bool CNetworkSession::TakeControlStatus(NetworkControlStatus& status) +{ + CSRWExclusiveLock lock(m_controlLock); + if (!m_controlCount) + return false; + + status = m_control[m_controlHead]; + m_control[m_controlHead] = {}; + m_controlHead = (m_controlHead + 1) % CONTROL_CAPACITY; + --m_controlCount; + return true; +} + InputTargetState CNetworkSession::InputState() const { CSRWSharedLock lock(m_lifecycleLock); diff --git a/idd/LGIdd/transport/network/CNetworkSession.h b/idd/LGIdd/transport/network/CNetworkSession.h index ac72cc30..488a3497 100644 --- a/idd/LGIdd/transport/network/CNetworkSession.h +++ b/idd/LGIdd/transport/network/CNetworkSession.h @@ -21,10 +21,13 @@ #pragma once #include "CSRWLock.h" +#include "capture/CFrameScheduler.h" #include "transport/IControlSink.h" #include "transport/IInputSource.h" #include "transport/ITransport.h" +#include + #include #include @@ -32,20 +35,23 @@ enum class NetworkEventType : uint8_t { SET_CURSOR_POSITION, SET_RESOLUTION, + SET_FRAME_SCHEDULE, RECOVERY_REQUEST, }; struct NetworkEvent { - NetworkEventType type = NetworkEventType::SET_CURSOR_POSITION; - SourceKey source; - int32_t x = 0; - int32_t y = 0; - uint32_t width = 0; - uint32_t height = 0; - uint64_t session = 0; - uint32_t serial = 0; - bool active = false; + NetworkEventType type = NetworkEventType::SET_CURSOR_POSITION; + SourceKey source; + FrameScheduleUpdate schedule = {}; + uint64_t controlID = 0; + int32_t x = 0; + int32_t y = 0; + uint32_t width = 0; + uint32_t height = 0; + uint64_t session = 0; + uint32_t serial = 0; + bool active = false; }; struct NetworkRecoveryStatus @@ -57,13 +63,20 @@ struct NetworkRecoveryStatus uint32_t error = 0; }; +struct NetworkControlStatus +{ + uint64_t controlID = 0; + LGNetControlStatusCode status = LG_NET_CONTROL_STATUS_ERROR; + uint32_t detail = 0; +}; + class CNetworkEventQueue { private: static constexpr unsigned CAPACITY = 128; CSRWLock m_lock; - NetworkEvent m_events[CAPACITY] = {}; + NetworkEvent m_events[CAPACITY] = {}; unsigned m_head = 0; unsigned m_count = 0; @@ -101,11 +114,14 @@ struct NetworkControlSnapshot IDARG_OUT_QUERY_HWCURSOR cursor = {}; std::vector cursorData; std::shared_ptr transform; - UINT sdrWhiteLevel = 0; - uint64_t revision = 0; - bool cursorValid = false; - bool shapeValid = false; - bool transformValid = false; + UINT sdrWhiteLevel = 0; + uint64_t stateRevision = 0; + uint64_t positionRevision = 0; + uint64_t shapeRevision = 0; + uint64_t colorRevision = 0; + bool cursorValid = false; + bool shapeValid = false; + bool transformValid = false; }; class CNetworkControlSink final : public IControlSink @@ -125,7 +141,8 @@ public: std::shared_ptr transform) override; void RequestReplay(); - bool Snapshot(NetworkControlSnapshot& state) const; + bool Snapshot( + NetworkControlSnapshot& state, uint64_t knownRevision) const; }; // A session is transport-independent state owned by the eventual QUIC @@ -135,6 +152,7 @@ class CNetworkSession final { private: static constexpr unsigned RECOVERY_CAPACITY = 16; + static constexpr unsigned CONTROL_CAPACITY = 128; CNetworkEventQueue& m_events; CNetworkInputSource& m_input; @@ -143,10 +161,15 @@ private: bool m_open = true; CSRWLock m_recoveryLock; - NetworkRecoveryStatus m_recovery[RECOVERY_CAPACITY] = {}; + NetworkRecoveryStatus m_recovery[RECOVERY_CAPACITY] = {}; unsigned m_recoveryHead = 0; unsigned m_recoveryCount = 0; + CSRWLock m_controlLock; + NetworkControlStatus m_control[CONTROL_CAPACITY] = {}; + unsigned m_controlHead = 0; + unsigned m_controlCount = 0; + SourceKey EventSource() const; public: @@ -161,12 +184,17 @@ public: bool IsOpen() const; void Close(); - bool SetCursorPosition(int32_t x, int32_t y); - bool SetResolution(uint32_t width, uint32_t height); + bool SetCursorPosition(uint64_t controlID, int32_t x, int32_t y); + bool SetResolution( + uint64_t controlID, uint32_t width, uint32_t height); + bool SetFrameSchedule( + uint64_t controlID, const FrameScheduleUpdate& schedule); bool RequestRecovery(uint64_t session, uint32_t serial, bool active); void ProcessEvent(const NetworkEvent& event, ITransportEvents& events); void SetRecoveryStatus(const NetworkRecoveryStatus& status); bool TakeRecoveryStatus(NetworkRecoveryStatus& status); + void SetControlStatus(const NetworkControlStatus& status); + bool TakeControlStatus(NetworkControlStatus& status); InputTargetState InputState() const; InputResult ClaimInput(); diff --git a/idd/LGIdd/transport/network/CNetworkTransport.cpp b/idd/LGIdd/transport/network/CNetworkTransport.cpp index 57ae43d6..96bc1758 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.cpp +++ b/idd/LGIdd/transport/network/CNetworkTransport.cpp @@ -241,6 +241,7 @@ bool CNetworkTransport::ServiceEnabled(LGNetService service) const { case LG_NET_SERVICE_RECOVERY: case LG_NET_SERVICE_CURSOR: + case LG_NET_SERVICE_CONTROL: return (m_config.services & TRANSPORT_SERVICE_CONTROL) != 0; case LG_NET_SERVICE_INPUT: @@ -265,9 +266,9 @@ bool CNetworkTransport::ServiceEnabled(LGNetService service) const } bool CNetworkTransport::ControlSnapshot( - NetworkControlSnapshot& state) const + NetworkControlSnapshot& state, uint64_t knownRevision) const { - return m_control.Snapshot(state); + return m_control.Snapshot(state, knownRevision); } std::shared_ptr CNetworkTransport::CreateSession( diff --git a/idd/LGIdd/transport/network/CNetworkTransport.h b/idd/LGIdd/transport/network/CNetworkTransport.h index 3608ad5b..357f2c9b 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.h +++ b/idd/LGIdd/transport/network/CNetworkTransport.h @@ -94,7 +94,8 @@ public: const NetworkSettings& Settings() const { return m_settings; } bool ServiceEnabled(LGNetService service) const; - bool ControlSnapshot(NetworkControlSnapshot& state) const; + bool ControlSnapshot( + NetworkControlSnapshot& state, uint64_t knownRevision) const; // The QUIC provider calls these only after authenticating a peer. Keeping // the registry here makes Stop a barrier for session-originated callbacks. diff --git a/repos/LGProtocol b/repos/LGProtocol index e41dca0d..d897fb49 160000 --- a/repos/LGProtocol +++ b/repos/LGProtocol @@ -1 +1 @@ -Subproject commit e41dca0d5f6f9b3c511d5eeda5914a044872b0e6 +Subproject commit d897fb498af03d1ff138f83956a2ab6fec5c0a29