mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 23:21:29 +00:00
Report guest HID keyboard LED state from LGInput to LGIdd and publish it with the LGMP input endpoint status. Forward SPICE modifier updates through the same provider state so evdev can synchronize physical keyboard LEDs across transport changes and reconnects. Bump the versioned pipe and KVMFR input status contracts for the new feedback state. Accept earlier LGMP input status versions without LED feedback so the client retains input while the guest driver is being upgraded.
1039 lines
27 KiB
C++
1039 lines
27 KiB
C++
/**
|
|
* 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 <ntstatus.h>
|
|
|
|
#include "transport/lgmp/CLGMPInputTransport.h"
|
|
|
|
#include <wudfwdm.h>
|
|
|
|
#include "config/CSettings.h"
|
|
#include "transport/lgmp/CLGMPHost.h"
|
|
#include "Atomic.h"
|
|
#include "CDebug.h"
|
|
#include "CSRWLock.h"
|
|
#include "Seq.h"
|
|
|
|
#include "common/KVMFRInput.h"
|
|
#include "common/LGMPConfig.h"
|
|
|
|
#include <avrt.h>
|
|
#include <string.h>
|
|
|
|
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
|
|
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
|
|
#endif
|
|
|
|
static const LGMPQueueConfig INPUT_QUEUE_CONFIG =
|
|
{
|
|
LGMP_Q_INPUT,
|
|
LGMP_Q_INPUT_LEN,
|
|
1000,
|
|
};
|
|
|
|
static constexpr int32_t MAX_SPLIT_REPORTS = 4;
|
|
static constexpr int32_t MAX_MOUSE_DELTA =
|
|
INT16_MAX * MAX_SPLIT_REPORTS;
|
|
static constexpr int32_t MIN_MOUSE_DELTA =
|
|
INT16_MIN * MAX_SPLIT_REPORTS;
|
|
static constexpr int32_t MAX_MOUSE_WHEEL =
|
|
INT8_MAX * MAX_SPLIT_REPORTS;
|
|
static constexpr int32_t MIN_MOUSE_WHEEL =
|
|
-INT8_MAX * MAX_SPLIT_REPORTS;
|
|
static bool IsZero(const void * data, size_t size)
|
|
{
|
|
const uint8_t * byte = static_cast<const uint8_t *>(data);
|
|
for (size_t i = 0; i < size; ++i)
|
|
if (byte[i])
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static bool ArmPollTimer(HANDLE timer, uint32_t waitUs)
|
|
{
|
|
LARGE_INTEGER due = {};
|
|
due.QuadPart = -static_cast<LONGLONG>(waitUs) * 10;
|
|
return SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE) != FALSE;
|
|
}
|
|
|
|
static KVMFRStreamDescriptor ExportStreamDescriptor(
|
|
const LGMPStreamDescriptor& source)
|
|
{
|
|
KVMFRStreamDescriptor result = {};
|
|
result.magic = source.magic;
|
|
result.version = source.version;
|
|
result.size = source.size;
|
|
result.offset = source.offset;
|
|
result.regionSize = source.regionSize;
|
|
result.direction = source.direction;
|
|
result.policy = source.policy;
|
|
result.slotCount = source.slotCount;
|
|
result.slotSize = source.slotSize;
|
|
return result;
|
|
}
|
|
|
|
CLGMPInputTransport::~CLGMPInputTransport()
|
|
{
|
|
DeInit();
|
|
}
|
|
|
|
bool CLGMPInputTransport::Initialize()
|
|
{
|
|
if (m_queue)
|
|
return true;
|
|
|
|
LGMP_STATUS status = m_host.CreateQueue(
|
|
INPUT_QUEUE_CONFIG, &m_queue);
|
|
if (status != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostQueueCreate Failed (Input): %s",
|
|
lgmpStatusString(status));
|
|
return false;
|
|
}
|
|
|
|
const LGMPStreamConfig streamConfig =
|
|
{
|
|
LGMP_STREAM_CLIENT_TO_HOST,
|
|
LGMP_STREAM_RELIABLE_FIFO,
|
|
KVMFR_INPUT_STREAM_SLOT_COUNT,
|
|
KVMFR_INPUT_STREAM_SLOT_SIZE,
|
|
};
|
|
for (StreamEndpoint& endpoint : m_streamEndpoint)
|
|
{
|
|
status = m_host.CreateStream(streamConfig, &endpoint.stream);
|
|
if (status != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostStreamNew Failed (Input): %s",
|
|
lgmpStatusString(status));
|
|
DeInit();
|
|
return false;
|
|
}
|
|
lgmpHostStreamGetDescriptor(endpoint.stream, &endpoint.descriptor);
|
|
}
|
|
Seq::Inc(m_streamGeneration);
|
|
|
|
for (PLGMPMemory& memory : m_statusMemory)
|
|
{
|
|
status = m_host.Allocate(sizeof(KVMFRInputStatus), &memory);
|
|
if (status != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostMemAlloc Failed (Input Status): %s",
|
|
lgmpStatusString(status));
|
|
DeInit();
|
|
return false;
|
|
}
|
|
memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRInputStatus));
|
|
}
|
|
|
|
{
|
|
CSRWExclusiveLock lock(m_statusLock);
|
|
m_statusDirty = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CLGMPInputTransport::DeInit()
|
|
{
|
|
Stop();
|
|
for (PLGMPMemory& memory : m_statusMemory)
|
|
lgmpHostMemFree(&memory);
|
|
for (StreamEndpoint& endpoint : m_streamEndpoint)
|
|
{
|
|
lgmpHostStreamFree(&endpoint.stream);
|
|
endpoint = {};
|
|
}
|
|
m_streamGeneration = 0;
|
|
m_streamDrainCursor = 0;
|
|
m_queue = nullptr;
|
|
}
|
|
|
|
bool CLGMPInputTransport::ReconcileStreams(bool& changed)
|
|
{
|
|
changed = false;
|
|
uint32_t activeClientIDs[32] = {};
|
|
unsigned activeClientCount = 0;
|
|
const LGMP_STATUS clientsStatus = lgmpHostGetClientIDs(
|
|
m_queue, activeClientIDs, &activeClientCount);
|
|
if (clientsStatus != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostGetClientIDs Failed (Input): %s",
|
|
lgmpStatusString(clientsStatus));
|
|
return false;
|
|
}
|
|
|
|
CSRWExclusiveLock lock(m_streamLock);
|
|
for (StreamEndpoint& endpoint : m_streamEndpoint)
|
|
{
|
|
if (!endpoint.clientID)
|
|
continue;
|
|
|
|
bool active = false;
|
|
for (unsigned i = 0; i < activeClientCount; ++i)
|
|
if (activeClientIDs[i] == endpoint.clientID)
|
|
{
|
|
active = true;
|
|
break;
|
|
}
|
|
if (!active)
|
|
{
|
|
if (!endpoint.draining)
|
|
{
|
|
endpoint.draining = true;
|
|
changed = true;
|
|
}
|
|
|
|
const LGMP_STATUS status = lgmpHostStreamUnbind(endpoint.stream);
|
|
if (status == LGMP_ERR_STREAM_BUSY)
|
|
{
|
|
// A vanished peer may have left an active reservation. Keep this
|
|
// endpoint draining; force-unbind is reserved for transport teardown.
|
|
continue;
|
|
}
|
|
if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
|
|
{
|
|
DEBUG_ERROR("lgmpHostStreamUnbind Failed (Input): %s",
|
|
lgmpStatusString(status));
|
|
return false;
|
|
}
|
|
endpoint.clientID = 0;
|
|
endpoint.epoch = 0;
|
|
endpoint.draining = false;
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
if (!endpoint.draining)
|
|
continue;
|
|
|
|
const LGMP_STATUS status = lgmpHostStreamUnbind(endpoint.stream);
|
|
if (status == LGMP_ERR_STREAM_BUSY)
|
|
continue;
|
|
if (status != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostStreamUnbind Failed (Input): %s",
|
|
lgmpStatusString(status));
|
|
return false;
|
|
}
|
|
endpoint.clientID = 0;
|
|
endpoint.epoch = 0;
|
|
endpoint.draining = false;
|
|
changed = true;
|
|
}
|
|
|
|
for (unsigned i = 0; i < activeClientCount; ++i)
|
|
{
|
|
const uint32_t clientID = activeClientIDs[i];
|
|
bool bound = false;
|
|
for (const StreamEndpoint& endpoint : m_streamEndpoint)
|
|
if (endpoint.clientID == clientID)
|
|
{
|
|
bound = true;
|
|
break;
|
|
}
|
|
if (bound)
|
|
continue;
|
|
|
|
StreamEndpoint * available = nullptr;
|
|
for (StreamEndpoint& endpoint : m_streamEndpoint)
|
|
if (!endpoint.clientID)
|
|
{
|
|
available = &endpoint;
|
|
break;
|
|
}
|
|
if (!available)
|
|
continue;
|
|
|
|
uint32_t epoch = 0;
|
|
const LGMP_STATUS status = lgmpHostStreamBind(
|
|
available->stream, clientID, &epoch);
|
|
if (status != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostStreamBind Failed (Input client %u): %s",
|
|
clientID, lgmpStatusString(status));
|
|
return false;
|
|
}
|
|
available->clientID = clientID;
|
|
available->epoch = epoch;
|
|
available->draining = false;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed)
|
|
Seq::Inc(m_streamGeneration);
|
|
return true;
|
|
}
|
|
|
|
void CLGMPInputTransport::ResetStreams()
|
|
{
|
|
bool changed = false;
|
|
{
|
|
CSRWExclusiveLock lock(m_streamLock);
|
|
for (StreamEndpoint& endpoint : m_streamEndpoint)
|
|
{
|
|
if (!endpoint.clientID)
|
|
continue;
|
|
const LGMP_STATUS status = lgmpHostStreamUnbind(endpoint.stream);
|
|
if (status != LGMP_OK && status != LGMP_ERR_STREAM_BUSY)
|
|
DEBUG_WARN("lgmpHostStreamUnbind Failed (Input): %s",
|
|
lgmpStatusString(status));
|
|
if (status == LGMP_OK)
|
|
{
|
|
endpoint.clientID = 0;
|
|
endpoint.epoch = 0;
|
|
endpoint.draining = false;
|
|
}
|
|
else if (status == LGMP_ERR_STREAM_BUSY)
|
|
{
|
|
// Stop is restartable, so preserve outstanding records and finish
|
|
// the graceful unbind after the transport starts again.
|
|
endpoint.draining = true;
|
|
}
|
|
changed = true;
|
|
}
|
|
if (changed)
|
|
Seq::Inc(m_streamGeneration);
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
CSRWExclusiveLock lock(m_statusLock);
|
|
m_statusDirty = true;
|
|
}
|
|
}
|
|
|
|
InputSourceId CLGMPInputTransport::Owner() const
|
|
{
|
|
InputSourceId source;
|
|
source.client = m_ownerClientID;
|
|
source.generation = m_ownerGeneration;
|
|
return source;
|
|
}
|
|
|
|
void CLGMPInputTransport::UpdateTargetState(
|
|
const InputTargetState& state)
|
|
{
|
|
CSRWExclusiveLock lock(m_statusLock);
|
|
if (state.state == m_targetState.state &&
|
|
state.available == m_targetState.available &&
|
|
state.owned == m_targetState.owned &&
|
|
state.keyboardLEDsValid == m_targetState.keyboardLEDsValid &&
|
|
state.keyboardLEDs == m_targetState.keyboardLEDs &&
|
|
state.owner.client == m_targetState.owner.client &&
|
|
state.owner.generation == m_targetState.owner.generation)
|
|
return;
|
|
|
|
if (state.state != m_targetState.state)
|
|
{
|
|
Seq::Inc(m_endpointGeneration);
|
|
}
|
|
m_targetState = state;
|
|
m_statusDirty = true;
|
|
}
|
|
|
|
bool CLGMPInputTransport::PublishStatus()
|
|
{
|
|
if (!m_queue)
|
|
return true;
|
|
|
|
bool streamChanged = false;
|
|
if (!ReconcileStreams(streamChanged))
|
|
return false;
|
|
|
|
CSRWSharedLock streamLock(m_streamLock);
|
|
CSRWExclusiveLock lock(m_statusLock);
|
|
|
|
if (lgmpHostQueueNewSubs(m_queue))
|
|
m_statusDirty = true;
|
|
if (streamChanged)
|
|
m_statusDirty = true;
|
|
if (!m_statusDirty || !lgmpHostQueueHasSubs(m_queue))
|
|
return true;
|
|
|
|
PLGMPMemory memory = nullptr;
|
|
for (PLGMPMemory candidate : m_statusMemory)
|
|
if (!lgmpHostQueuePayloadPending(m_queue, candidate))
|
|
{
|
|
memory = candidate;
|
|
break;
|
|
}
|
|
if (!memory)
|
|
return true;
|
|
|
|
const bool available = m_targetState.available;
|
|
KVMFRInputStatus status = {};
|
|
status.version = KVMFR_INPUT_VERSION;
|
|
status.capabilities = available ?
|
|
KVMFR_INPUT_CAP_MOUSE_RELATIVE |
|
|
KVMFR_INPUT_CAP_MOUSE_ABSOLUTE |
|
|
KVMFR_INPUT_CAP_KEYBOARD : 0;
|
|
status.flags = available ?
|
|
KVMFR_INPUT_STATUS_AVAILABLE : 0;
|
|
if (m_targetState.owned)
|
|
{
|
|
status.flags |= KVMFR_INPUT_STATUS_HAS_OWNER;
|
|
status.ownerClientID = m_targetState.owner.client;
|
|
status.ownerGeneration = m_targetState.owner.generation;
|
|
}
|
|
if (available && m_targetState.keyboardLEDsValid)
|
|
{
|
|
status.flags |= KVMFR_INPUT_STATUS_KEYBOARD_LEDS_VALID;
|
|
status.keyboardLEDs = m_targetState.keyboardLEDs;
|
|
}
|
|
status.generation = m_endpointGeneration;
|
|
status.lease = static_cast<uint32_t>(OWNER_LEASE_MS);
|
|
status.maxButtons = KVMFR_INPUT_MOUSE_BUTTON_COUNT;
|
|
status.streamVersion = KVMFR_INPUT_STREAM_VERSION;
|
|
status.streamEndpointCount = KVMFR_INPUT_STREAM_ENDPOINT_COUNT;
|
|
status.streamGeneration = m_streamGeneration;
|
|
for (unsigned i = 0; i < KVMFR_INPUT_STREAM_ENDPOINT_COUNT; ++i)
|
|
{
|
|
const StreamEndpoint& endpoint = m_streamEndpoint[i];
|
|
KVMFRInputStreamEndpoint& exported = status.streamEndpoint[i];
|
|
exported.stream =
|
|
ExportStreamDescriptor(endpoint.descriptor);
|
|
exported.flags =
|
|
KVMFR_INPUT_STREAM_ENDPOINT_AVAILABLE;
|
|
if (endpoint.clientID && !endpoint.draining)
|
|
{
|
|
exported.boundClientID = endpoint.clientID;
|
|
exported.bindingGeneration = endpoint.epoch;
|
|
exported.flags |= KVMFR_INPUT_STREAM_ENDPOINT_BOUND;
|
|
}
|
|
}
|
|
memcpy(lgmpHostMemPtr(memory), &status, sizeof(status));
|
|
|
|
const uint32_t serial = Seq::Next(m_statusSerial);
|
|
const LGMP_STATUS result = lgmpHostQueuePost(m_queue, serial, memory);
|
|
if (result == LGMP_OK)
|
|
{
|
|
m_statusSerial = serial;
|
|
m_statusDirty = false;
|
|
}
|
|
else if (result != LGMP_ERR_QUEUE_FULL)
|
|
{
|
|
DEBUG_WARN("lgmpHostQueuePost Failed (Input Status): %s",
|
|
lgmpStatusString(result));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CLGMPInputTransport::FlushStatus()
|
|
{
|
|
if (Atomic::Load(m_statusFailed, std::memory_order_acquire) ||
|
|
PublishStatus())
|
|
return;
|
|
|
|
Atomic::Store(m_statusFailed, true, std::memory_order_release);
|
|
CSRWSharedLock lock(m_lifecycleLock);
|
|
if (m_stopEvent)
|
|
SetEvent(m_stopEvent);
|
|
}
|
|
|
|
bool CLGMPInputTransport::Start(IInputTarget& target)
|
|
{
|
|
CSRWExclusiveLock lock(m_lifecycleLock);
|
|
if (m_thread)
|
|
{
|
|
const DWORD state = WaitForSingleObject(m_thread, 0);
|
|
if (state == WAIT_TIMEOUT)
|
|
return true;
|
|
if (state != WAIT_OBJECT_0)
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(),
|
|
"Failed to inspect LGMP input worker");
|
|
return false;
|
|
}
|
|
|
|
CloseHandle(m_thread);
|
|
CloseHandle(m_pollTimer);
|
|
CloseHandle(m_stopEvent);
|
|
m_thread = nullptr;
|
|
m_pollTimer = nullptr;
|
|
m_stopEvent = nullptr;
|
|
m_target = nullptr;
|
|
ResetStreams();
|
|
}
|
|
|
|
if (!m_queue)
|
|
return false;
|
|
|
|
m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
|
m_pollTimer = CreateWaitableTimerExW(nullptr, nullptr,
|
|
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
|
if (!m_pollTimer)
|
|
m_pollTimer = CreateWaitableTimerExW(
|
|
nullptr, nullptr, 0, TIMER_ALL_ACCESS);
|
|
|
|
if (!m_stopEvent || !m_pollTimer)
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(),
|
|
"Failed to create LGMP input worker resources");
|
|
if (m_pollTimer)
|
|
CloseHandle(m_pollTimer);
|
|
if (m_stopEvent)
|
|
CloseHandle(m_stopEvent);
|
|
m_pollTimer = nullptr;
|
|
m_stopEvent = nullptr;
|
|
return false;
|
|
}
|
|
|
|
m_target = ⌖
|
|
{
|
|
CSRWExclusiveLock statusLock(m_statusLock);
|
|
m_targetState = target.GetState({});
|
|
Seq::Inc(m_endpointGeneration);
|
|
m_statusDirty = true;
|
|
}
|
|
Atomic::Store(m_statusFailed, false, std::memory_order_release);
|
|
m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
|
|
if (!m_thread)
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "Failed to create LGMP input worker");
|
|
m_target = nullptr;
|
|
{
|
|
CSRWExclusiveLock statusLock(m_statusLock);
|
|
m_targetState = {};
|
|
m_statusDirty = true;
|
|
}
|
|
CloseHandle(m_pollTimer);
|
|
CloseHandle(m_stopEvent);
|
|
m_pollTimer = nullptr;
|
|
m_stopEvent = nullptr;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CLGMPInputTransport::Stop()
|
|
{
|
|
CSRWExclusiveLock lock(m_lifecycleLock);
|
|
|
|
if (m_stopEvent)
|
|
SetEvent(m_stopEvent);
|
|
if (m_thread)
|
|
WaitForSingleObject(m_thread, INFINITE);
|
|
|
|
if (m_thread)
|
|
{
|
|
CloseHandle(m_thread);
|
|
m_thread = nullptr;
|
|
}
|
|
if (m_pollTimer)
|
|
{
|
|
CloseHandle(m_pollTimer);
|
|
m_pollTimer = nullptr;
|
|
}
|
|
if (m_stopEvent)
|
|
{
|
|
CloseHandle(m_stopEvent);
|
|
m_stopEvent = nullptr;
|
|
}
|
|
m_target = nullptr;
|
|
m_ownerClientID = 0;
|
|
m_ownerGeneration = 0;
|
|
m_ownerSequence = 0;
|
|
m_ownerDeadline = 0;
|
|
ResetStreams();
|
|
{
|
|
CSRWExclusiveLock statusLock(m_statusLock);
|
|
m_targetState = {};
|
|
m_statusDirty = true;
|
|
}
|
|
}
|
|
|
|
bool CLGMPInputTransport::IsOwner(
|
|
uint32_t sourceClientID, uint32_t generation) const
|
|
{
|
|
return m_ownerClientID == sourceClientID &&
|
|
m_ownerGeneration == generation;
|
|
}
|
|
|
|
bool CLGMPInputTransport::Claim(
|
|
uint32_t sourceClientID, const KVMFRInputMessage& message)
|
|
{
|
|
if (message.sequence != 1)
|
|
{
|
|
++m_statistics.sequenceErrors;
|
|
return false;
|
|
}
|
|
if (!m_target)
|
|
{
|
|
++m_statistics.deliveryFailures;
|
|
return false;
|
|
}
|
|
|
|
InputSourceId source;
|
|
source.client = sourceClientID;
|
|
source.generation = message.generation;
|
|
const InputResult result = m_target->Claim(source);
|
|
if (result != InputResult::ACCEPTED)
|
|
{
|
|
UpdateTargetState(m_target->GetState(source));
|
|
++m_statistics.deliveryFailures;
|
|
return false;
|
|
}
|
|
|
|
m_ownerClientID = sourceClientID;
|
|
m_ownerGeneration = message.generation;
|
|
m_ownerSequence = message.sequence;
|
|
RenewLease();
|
|
UpdateTargetState(m_target->GetState(source));
|
|
++m_statistics.claims;
|
|
return true;
|
|
}
|
|
|
|
void CLGMPInputTransport::RenewLease()
|
|
{
|
|
m_ownerDeadline = GetTickCount64() + OWNER_LEASE_MS;
|
|
}
|
|
|
|
void CLGMPInputTransport::ReleaseOwner(bool reset)
|
|
{
|
|
if (!m_ownerClientID)
|
|
return;
|
|
|
|
if (m_target)
|
|
{
|
|
m_target->Release(Owner(), reset);
|
|
UpdateTargetState(m_target->GetState({}));
|
|
}
|
|
|
|
m_ownerClientID = 0;
|
|
m_ownerGeneration = 0;
|
|
m_ownerSequence = 0;
|
|
m_ownerDeadline = 0;
|
|
++m_statistics.releases;
|
|
}
|
|
|
|
void CLGMPInputTransport::CheckOwner()
|
|
{
|
|
if (!m_target)
|
|
return;
|
|
|
|
if (m_ownerClientID)
|
|
{
|
|
bool found = false;
|
|
bool retiring = false;
|
|
{
|
|
CSRWSharedLock lock(m_streamLock);
|
|
for (const StreamEndpoint& endpoint : m_streamEndpoint)
|
|
if (endpoint.clientID == m_ownerClientID)
|
|
{
|
|
found = true;
|
|
retiring = endpoint.draining;
|
|
break;
|
|
}
|
|
}
|
|
if (!found || retiring)
|
|
{
|
|
ReleaseOwner(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const InputTargetState state = m_target->GetState(Owner());
|
|
UpdateTargetState(state);
|
|
|
|
if (!m_ownerClientID)
|
|
return;
|
|
|
|
if (!state.available || !state.owned)
|
|
{
|
|
ReleaseOwner(false);
|
|
return;
|
|
}
|
|
|
|
if (GetTickCount64() >= m_ownerDeadline)
|
|
ReleaseOwner(true);
|
|
}
|
|
|
|
bool CLGMPInputTransport::ValidatePayload(
|
|
const KVMFRInputMessage& message) const
|
|
{
|
|
switch (message.type)
|
|
{
|
|
case KVMFR_INPUT_MESSAGE_CLAIM:
|
|
case KVMFR_INPUT_MESSAGE_RELEASE:
|
|
case KVMFR_INPUT_MESSAGE_KEEPALIVE:
|
|
case KVMFR_INPUT_MESSAGE_RESET:
|
|
return IsZero(&message.payload, sizeof(message.payload));
|
|
|
|
case KVMFR_INPUT_MESSAGE_MOUSE_RELATIVE:
|
|
return message.payload.mouseRelative.deltaX >= MIN_MOUSE_DELTA &&
|
|
message.payload.mouseRelative.deltaX <= MAX_MOUSE_DELTA &&
|
|
message.payload.mouseRelative.deltaY >= MIN_MOUSE_DELTA &&
|
|
message.payload.mouseRelative.deltaY <= MAX_MOUSE_DELTA &&
|
|
message.payload.mouseRelative.wheel >= MIN_MOUSE_WHEEL &&
|
|
message.payload.mouseRelative.wheel <= MAX_MOUSE_WHEEL;
|
|
|
|
case KVMFR_INPUT_MESSAGE_MOUSE_ABSOLUTE:
|
|
return message.payload.mouseAbsolute.x <=
|
|
KVMFR_INPUT_MOUSE_ABSOLUTE_MAX &&
|
|
message.payload.mouseAbsolute.y <=
|
|
KVMFR_INPUT_MOUSE_ABSOLUTE_MAX &&
|
|
message.payload.mouseAbsolute.wheel >= MIN_MOUSE_WHEEL &&
|
|
message.payload.mouseAbsolute.wheel <= MAX_MOUSE_WHEEL &&
|
|
!message.payload.mouseAbsolute.reserved;
|
|
|
|
case KVMFR_INPUT_MESSAGE_KEYBOARD:
|
|
if (!IsZero(message.payload.keyboard.reserved,
|
|
sizeof(message.payload.keyboard.reserved)))
|
|
return false;
|
|
for (size_t i = 0; i < KVMFR_INPUT_KEYBOARD_KEY_COUNT; ++i)
|
|
if (message.payload.keyboard.keys[i] >
|
|
KVMFR_INPUT_KEYBOARD_USAGE_MAX)
|
|
return false;
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool CLGMPInputTransport::ProcessMessage(
|
|
uint32_t sourceClientID, const KVMFRInputMessage& message)
|
|
{
|
|
const bool owner = IsOwner(sourceClientID, message.generation);
|
|
InputSourceId source;
|
|
source.client = sourceClientID;
|
|
source.generation = message.generation;
|
|
|
|
if (!message.generation || !message.sequence || message.reserved ||
|
|
!ValidatePayload(message))
|
|
{
|
|
++m_statistics.malformedMessage;
|
|
if (owner)
|
|
ReleaseOwner(true);
|
|
return false;
|
|
}
|
|
|
|
if (m_target)
|
|
UpdateTargetState(m_target->GetState(
|
|
m_ownerClientID ? Owner() : source));
|
|
|
|
if (message.type == KVMFR_INPUT_MESSAGE_CLAIM)
|
|
{
|
|
if (m_ownerClientID)
|
|
{
|
|
if (!owner)
|
|
{
|
|
++m_statistics.nonOwner;
|
|
return true;
|
|
}
|
|
if (message.sequence == 1 && m_ownerSequence == 1)
|
|
return true;
|
|
|
|
++m_statistics.sequenceErrors;
|
|
ReleaseOwner(true);
|
|
return false;
|
|
}
|
|
return Claim(sourceClientID, message);
|
|
}
|
|
|
|
if (!owner)
|
|
{
|
|
++m_statistics.nonOwner;
|
|
return true;
|
|
}
|
|
|
|
const uint32_t expectedSequence = Seq::Next(m_ownerSequence);
|
|
if (message.sequence != expectedSequence)
|
|
{
|
|
++m_statistics.sequenceErrors;
|
|
ReleaseOwner(true);
|
|
return false;
|
|
}
|
|
|
|
InputResult result = InputResult::STALE;
|
|
bool inputReport = false;
|
|
switch (message.type)
|
|
{
|
|
case KVMFR_INPUT_MESSAGE_RELEASE:
|
|
ReleaseOwner(true);
|
|
return true;
|
|
|
|
case KVMFR_INPUT_MESSAGE_KEEPALIVE:
|
|
result = m_target ? m_target->Touch(source) :
|
|
InputResult::UNAVAILABLE;
|
|
break;
|
|
|
|
case KVMFR_INPUT_MESSAGE_RESET:
|
|
result = m_target ? m_target->Reset(source) :
|
|
InputResult::UNAVAILABLE;
|
|
break;
|
|
|
|
case KVMFR_INPUT_MESSAGE_MOUSE_RELATIVE:
|
|
inputReport = true;
|
|
result = m_target ? m_target->SendMouseRelative(source,
|
|
message.payload.mouseRelative.deltaX,
|
|
message.payload.mouseRelative.deltaY,
|
|
message.payload.mouseRelative.wheel,
|
|
message.payload.mouseRelative.buttons) : InputResult::UNAVAILABLE;
|
|
break;
|
|
|
|
case KVMFR_INPUT_MESSAGE_MOUSE_ABSOLUTE:
|
|
inputReport = true;
|
|
result = m_target ? m_target->SendMouseAbsolute(source,
|
|
message.payload.mouseAbsolute.x,
|
|
message.payload.mouseAbsolute.y,
|
|
message.payload.mouseAbsolute.wheel,
|
|
message.payload.mouseAbsolute.buttons) : InputResult::UNAVAILABLE;
|
|
break;
|
|
|
|
case KVMFR_INPUT_MESSAGE_KEYBOARD:
|
|
inputReport = true;
|
|
result = m_target ? m_target->SendKeyboard(source,
|
|
message.payload.keyboard.modifiers,
|
|
message.payload.keyboard.keys) : InputResult::UNAVAILABLE;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (result != InputResult::ACCEPTED)
|
|
{
|
|
++m_statistics.deliveryFailures;
|
|
ReleaseOwner(true);
|
|
return false;
|
|
}
|
|
|
|
UpdateTargetState(m_target->GetState(source));
|
|
|
|
m_ownerSequence = message.sequence;
|
|
RenewLease();
|
|
if (inputReport)
|
|
++m_statistics.reports;
|
|
return true;
|
|
}
|
|
|
|
bool CLGMPInputTransport::DrainStreamMessages(bool& received)
|
|
{
|
|
received = false;
|
|
unsigned count = 0;
|
|
CSRWSharedLock lock(m_streamLock);
|
|
for (; count < DRAIN_LIMIT; ++count)
|
|
{
|
|
StreamEndpoint * selected = nullptr;
|
|
LGMPStreamBuffer buffer = {};
|
|
for (unsigned scanned = 0;
|
|
scanned < KVMFR_INPUT_STREAM_ENDPOINT_COUNT; ++scanned)
|
|
{
|
|
const unsigned index = (m_streamDrainCursor + scanned) %
|
|
KVMFR_INPUT_STREAM_ENDPOINT_COUNT;
|
|
StreamEndpoint& endpoint = m_streamEndpoint[index];
|
|
if (!endpoint.clientID)
|
|
continue;
|
|
|
|
const LGMP_STATUS status = lgmpHostStreamReadPeek(
|
|
endpoint.stream, &buffer);
|
|
if (status == LGMP_ERR_STREAM_EMPTY ||
|
|
status == LGMP_ERR_STREAM_UNBOUND)
|
|
continue;
|
|
if (status != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostStreamReadPeek Failed (Input): %s",
|
|
lgmpStatusString(status));
|
|
return false;
|
|
}
|
|
|
|
selected = &endpoint;
|
|
m_streamDrainCursor = (index + 1) %
|
|
KVMFR_INPUT_STREAM_ENDPOINT_COUNT;
|
|
break;
|
|
}
|
|
|
|
if (!selected)
|
|
break;
|
|
|
|
received = true;
|
|
++m_statistics.messages;
|
|
// Retired endpoint generations are consumed only to finish their
|
|
// graceful unbind; they must not reach a newly started input target.
|
|
if (!selected->draining)
|
|
{
|
|
if (buffer.size != sizeof(KVMFRInputMessage))
|
|
{
|
|
DEBUG_WARN("Ignoring invalid KVMFR input stream message size");
|
|
++m_statistics.malformedSize;
|
|
if (selected->clientID == m_ownerClientID)
|
|
ReleaseOwner(true);
|
|
}
|
|
else
|
|
{
|
|
KVMFRInputMessage message = {};
|
|
memcpy(&message, buffer.data, sizeof(message));
|
|
ProcessMessage(selected->clientID, message);
|
|
}
|
|
}
|
|
|
|
const LGMP_STATUS releaseStatus = lgmpHostStreamReadRelease(
|
|
selected->stream, &buffer);
|
|
if (releaseStatus != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("lgmpHostStreamReadRelease Failed (Input): %s",
|
|
lgmpStatusString(releaseStatus));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (count > m_statistics.maxDrain)
|
|
m_statistics.maxDrain = count;
|
|
if (count == DRAIN_LIMIT)
|
|
++m_statistics.drainLimit;
|
|
return true;
|
|
}
|
|
|
|
void CLGMPInputTransport::LogStatistics(ULONGLONG now)
|
|
{
|
|
if (!m_statistics.lastLog)
|
|
{
|
|
m_statistics.lastLog = now;
|
|
return;
|
|
}
|
|
if (now - m_statistics.lastLog < LOG_INTERVAL_MS)
|
|
return;
|
|
|
|
const Statistics statistics = m_statistics;
|
|
m_statistics = {};
|
|
m_statistics.lastLog = now;
|
|
|
|
if (!statistics.messages && !statistics.claims &&
|
|
!statistics.releases && !statistics.deliveryFailures)
|
|
return;
|
|
|
|
if (!g_settings.ShouldLogStatistics())
|
|
return;
|
|
|
|
const double elapsed =
|
|
static_cast<double>(now - statistics.lastLog) / 1000.0;
|
|
DEBUG_TRACE("LGMP input host: %.1f msg/s, %llu reports, "
|
|
"drain max %u, %llu limit; %llu bad size, %llu malformed, "
|
|
"%llu sequence, %llu non-owner, %llu delivery failures; "
|
|
"%llu claims, %llu releases",
|
|
statistics.messages / elapsed,
|
|
static_cast<unsigned long long>(statistics.reports),
|
|
statistics.maxDrain,
|
|
static_cast<unsigned long long>(statistics.drainLimit),
|
|
static_cast<unsigned long long>(statistics.malformedSize),
|
|
static_cast<unsigned long long>(statistics.malformedMessage),
|
|
static_cast<unsigned long long>(statistics.sequenceErrors),
|
|
static_cast<unsigned long long>(statistics.nonOwner),
|
|
static_cast<unsigned long long>(statistics.deliveryFailures),
|
|
static_cast<unsigned long long>(statistics.claims),
|
|
static_cast<unsigned long long>(statistics.releases));
|
|
}
|
|
|
|
DWORD CALLBACK CLGMPInputTransport::ThreadProc(void * context)
|
|
{
|
|
static_cast<CLGMPInputTransport *>(context)->Thread();
|
|
return 0;
|
|
}
|
|
|
|
void CLGMPInputTransport::Thread()
|
|
{
|
|
DWORD avTask = 0;
|
|
HANDLE avTaskHandle =
|
|
AvSetMmThreadCharacteristicsW(L"Distribution", &avTask);
|
|
if (avTaskHandle &&
|
|
!AvSetMmThreadPriority(avTaskHandle, AVRT_PRIORITY_HIGH))
|
|
DEBUG_WARN("Failed to raise input MMCSS priority: %lu",
|
|
GetLastError());
|
|
|
|
LGMPStreamPollState streamPoll = {};
|
|
LGMPStreamPollConfig pollConfig = {};
|
|
pollConfig.spinCount = 64U;
|
|
pollConfig.minWaitUs = 25U;
|
|
pollConfig.maxWaitUs = 1000U;
|
|
const LGMP_STATUS pollStatus = lgmpStreamPollInit(&streamPoll,
|
|
pollConfig);
|
|
if (pollStatus != LGMP_OK)
|
|
{
|
|
DEBUG_ERROR("Failed to initialize LGMP input polling: %s",
|
|
lgmpStatusString(pollStatus));
|
|
if (m_target)
|
|
m_target->Failed();
|
|
if (avTaskHandle)
|
|
AvRevertMmThreadCharacteristics(avTaskHandle);
|
|
return;
|
|
}
|
|
|
|
m_statistics = {};
|
|
m_statistics.lastLog = GetTickCount64();
|
|
const HANDLE waitHandles[] = { m_stopEvent, m_pollTimer };
|
|
bool failed = false;
|
|
for (;;)
|
|
{
|
|
CheckOwner();
|
|
bool streamReceived = false;
|
|
if (!DrainStreamMessages(streamReceived))
|
|
{
|
|
failed = true;
|
|
break;
|
|
}
|
|
if (!PublishStatus())
|
|
{
|
|
Atomic::Store(m_statusFailed, true, std::memory_order_release);
|
|
failed = true;
|
|
break;
|
|
}
|
|
if (streamReceived)
|
|
lgmpStreamPollActivity(&streamPoll);
|
|
const ULONGLONG now = GetTickCount64();
|
|
LogStatistics(now);
|
|
|
|
const uint32_t waitUs = lgmpStreamPollIdle(&streamPoll);
|
|
if (!waitUs)
|
|
continue;
|
|
if (!ArmPollTimer(m_pollTimer, waitUs))
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "Failed to arm LGMP input timer");
|
|
if (WaitForSingleObject(m_stopEvent, 1) != WAIT_TIMEOUT)
|
|
break;
|
|
failed = true;
|
|
break;
|
|
}
|
|
|
|
const DWORD wait = WaitForMultipleObjects(
|
|
_countof(waitHandles), waitHandles, FALSE, INFINITE);
|
|
if (wait == WAIT_OBJECT_0)
|
|
{
|
|
failed = Atomic::Load(m_statusFailed, std::memory_order_acquire);
|
|
break;
|
|
}
|
|
if (wait != WAIT_OBJECT_0 + 1)
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "LGMP input worker wait failed");
|
|
failed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ReleaseOwner(true);
|
|
UpdateTargetState({});
|
|
PublishStatus();
|
|
if (failed && m_target)
|
|
m_target->Failed();
|
|
if (avTaskHandle)
|
|
AvRevertMmThreadCharacteristics(avTaskHandle);
|
|
}
|