mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2024-12-04 22:11:32 +01:00
25b8e64adb
This change adds support for the VMware Mouse Position pseudo-encoding[1], which is used to notify VNC clients when X11 clients call `XWarpPointer()`[2]. This function is called by SDL (and other similar libraries) when they detect that the server does not support native relative motion, like some RFB clients. With this, RFB clients can choose to adjust the local cursor position under certain circumstances to match what the server has set. For instance, if pointer lock has been enabled on the client's machine and the cursor is not being drawn locally, the local position of the cursor is irrelevant, so the RFB client can use what the server sends as the canonical absolute position of the cursor. This ultimately enables the possibility of games (especially FPS games) to behave how users expect (if the clients implement the corresponding change). Part of: #619 1: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#vmware-cursor-position-pseudo-encoding 2: https://tronche.com/gui/x/xlib/input/XWarpPointer.html 3: https://hg.libsdl.org/SDL/file/28e3b60e2131/src/events/SDL_mouse.c#l804
495 lines
14 KiB
C++
495 lines
14 KiB
C++
/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
|
|
*
|
|
* This 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 software 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 software; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
|
|
* USA.
|
|
*/
|
|
|
|
// -=- SDisplay.cxx
|
|
//
|
|
// The SDisplay class encapsulates a particular system display.
|
|
|
|
#include <rfb_win32/SDisplay.h>
|
|
#include <rfb_win32/Service.h>
|
|
#include <rfb_win32/TsSessions.h>
|
|
#include <rfb_win32/CleanDesktop.h>
|
|
#include <rfb_win32/CurrentUser.h>
|
|
#include <rfb_win32/MonitorInfo.h>
|
|
#include <rfb_win32/SDisplayCorePolling.h>
|
|
#include <rfb_win32/SDisplayCoreWMHooks.h>
|
|
#include <rfb/Exception.h>
|
|
#include <rfb/LogWriter.h>
|
|
#include <rfb/ledStates.h>
|
|
|
|
|
|
using namespace rdr;
|
|
using namespace rfb;
|
|
using namespace rfb::win32;
|
|
|
|
static LogWriter vlog("SDisplay");
|
|
|
|
// - SDisplay-specific configuration options
|
|
|
|
IntParameter rfb::win32::SDisplay::updateMethod("UpdateMethod",
|
|
"How to discover desktop updates; 0 - Polling, 1 - Application hooking, 2 - Driver hooking.", 1);
|
|
BoolParameter rfb::win32::SDisplay::disableLocalInputs("DisableLocalInputs",
|
|
"Disable local keyboard and pointer input while the server is in use", false);
|
|
StringParameter rfb::win32::SDisplay::disconnectAction("DisconnectAction",
|
|
"Action to perform when all clients have disconnected. (None, Lock, Logoff)", "None");
|
|
StringParameter displayDevice("DisplayDevice",
|
|
"Display device name of the monitor to be remoted, or empty to export the whole desktop.", "");
|
|
BoolParameter rfb::win32::SDisplay::removeWallpaper("RemoveWallpaper",
|
|
"Remove the desktop wallpaper when the server is in use.", false);
|
|
BoolParameter rfb::win32::SDisplay::disableEffects("DisableEffects",
|
|
"Disable desktop user interface effects when the server is in use.", false);
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// SDisplay
|
|
//
|
|
|
|
// -=- Constructor/Destructor
|
|
|
|
SDisplay::SDisplay()
|
|
: server(0), pb(0), device(0),
|
|
core(0), ptr(0), kbd(0), clipboard(0),
|
|
inputs(0), monitor(0), cleanDesktop(0), cursor(0),
|
|
statusLocation(0), ledState(0)
|
|
{
|
|
updateEvent.h = CreateEvent(0, TRUE, FALSE, 0);
|
|
}
|
|
|
|
SDisplay::~SDisplay()
|
|
{
|
|
// XXX when the VNCServer has been deleted with clients active, stop()
|
|
// doesn't get called - this ought to be fixed in VNCServerST. In any event,
|
|
// we should never call any methods on VNCServer once we're being deleted.
|
|
// This is because it is supposed to be guaranteed that the SDesktop exists
|
|
// throughout the lifetime of the VNCServer. So if we're being deleted, then
|
|
// the VNCServer ought not to exist and therefore we shouldn't invoke any
|
|
// methods on it. Setting server to zero here ensures that stop() doesn't
|
|
// call setPixelBuffer(0) on the server.
|
|
server = 0;
|
|
if (core) stop();
|
|
}
|
|
|
|
|
|
// -=- SDesktop interface
|
|
|
|
void SDisplay::start(VNCServer* vs)
|
|
{
|
|
vlog.debug("starting");
|
|
|
|
// Try to make session zero the console session
|
|
if (!inConsoleSession())
|
|
setConsoleSession();
|
|
|
|
// Start the SDisplay core
|
|
server = vs;
|
|
startCore();
|
|
|
|
vlog.debug("started");
|
|
|
|
if (statusLocation) *statusLocation = true;
|
|
}
|
|
|
|
void SDisplay::stop()
|
|
{
|
|
vlog.debug("stopping");
|
|
|
|
// If we successfully start()ed then perform the DisconnectAction
|
|
if (core) {
|
|
CurrentUserToken cut;
|
|
CharArray action(disconnectAction.getData());
|
|
if (stricmp(action.buf, "Logoff") == 0) {
|
|
if (!cut.h)
|
|
vlog.info("ignoring DisconnectAction=Logoff - no current user");
|
|
else
|
|
ExitWindowsEx(EWX_LOGOFF, 0);
|
|
} else if (stricmp(action.buf, "Lock") == 0) {
|
|
if (!cut.h) {
|
|
vlog.info("ignoring DisconnectAction=Lock - no current user");
|
|
} else {
|
|
LockWorkStation();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop the SDisplayCore
|
|
if (server)
|
|
server->setPixelBuffer(0);
|
|
stopCore();
|
|
server = 0;
|
|
|
|
vlog.debug("stopped");
|
|
|
|
if (statusLocation) *statusLocation = false;
|
|
}
|
|
|
|
|
|
void SDisplay::startCore() {
|
|
|
|
// Currently, we just check whether we're in the console session, and
|
|
// fail if not
|
|
if (!inConsoleSession())
|
|
throw rdr::Exception("Console is not session zero - oreconnect to restore Console sessin");
|
|
|
|
// Switch to the current input desktop
|
|
if (rfb::win32::desktopChangeRequired()) {
|
|
if (!rfb::win32::changeDesktop())
|
|
throw rdr::Exception("unable to switch into input desktop");
|
|
}
|
|
|
|
// Initialise the change tracker and clipper
|
|
updates.clear();
|
|
clipper.setUpdateTracker(server);
|
|
|
|
// Create the framebuffer object
|
|
recreatePixelBuffer(true);
|
|
|
|
// Create the SDisplayCore
|
|
updateMethod_ = updateMethod;
|
|
int tryMethod = updateMethod_;
|
|
while (!core) {
|
|
try {
|
|
if (tryMethod == 1)
|
|
core = new SDisplayCoreWMHooks(this, &updates);
|
|
else
|
|
core = new SDisplayCorePolling(this, &updates);
|
|
core->setScreenRect(screenRect);
|
|
} catch (rdr::Exception& e) {
|
|
delete core; core = 0;
|
|
if (tryMethod == 0)
|
|
throw rdr::Exception("unable to access desktop");
|
|
tryMethod--;
|
|
vlog.error("%s", e.str());
|
|
}
|
|
}
|
|
vlog.info("Started %s", core->methodName());
|
|
|
|
// Start display monitor, clipboard handler and input handlers
|
|
monitor = new WMMonitor;
|
|
monitor->setNotifier(this);
|
|
clipboard = new Clipboard;
|
|
clipboard->setNotifier(this);
|
|
ptr = new SPointer;
|
|
kbd = new SKeyboard;
|
|
inputs = new WMBlockInput;
|
|
cursor = new WMCursor;
|
|
|
|
// Apply desktop optimisations
|
|
cleanDesktop = new CleanDesktop;
|
|
if (removeWallpaper)
|
|
cleanDesktop->disableWallpaper();
|
|
if (disableEffects)
|
|
cleanDesktop->disableEffects();
|
|
isWallpaperRemoved = removeWallpaper;
|
|
areEffectsDisabled = disableEffects;
|
|
|
|
checkLedState();
|
|
if (server)
|
|
server->setLEDState(ledState);
|
|
}
|
|
|
|
void SDisplay::stopCore() {
|
|
if (core)
|
|
vlog.info("Stopping %s", core->methodName());
|
|
delete core; core = 0;
|
|
delete pb; pb = 0;
|
|
delete device; device = 0;
|
|
delete monitor; monitor = 0;
|
|
delete clipboard; clipboard = 0;
|
|
delete inputs; inputs = 0;
|
|
delete ptr; ptr = 0;
|
|
delete kbd; kbd = 0;
|
|
delete cleanDesktop; cleanDesktop = 0;
|
|
delete cursor; cursor = 0;
|
|
ResetEvent(updateEvent);
|
|
}
|
|
|
|
|
|
bool SDisplay::isRestartRequired() {
|
|
// - We must restart the SDesktop if:
|
|
// 1. We are no longer in the input desktop.
|
|
// 2. The any setting has changed.
|
|
|
|
// - Check that our session is the Console
|
|
if (!inConsoleSession())
|
|
return true;
|
|
|
|
// - Check that we are in the input desktop
|
|
if (rfb::win32::desktopChangeRequired())
|
|
return true;
|
|
|
|
// - Check that the update method setting hasn't changed
|
|
// NB: updateMethod reflects the *selected* update method, not
|
|
// necessarily the one in use, since we fall back to simpler
|
|
// methods if more advanced ones fail!
|
|
if (updateMethod_ != updateMethod)
|
|
return true;
|
|
|
|
// - Check that the desktop optimisation settings haven't changed
|
|
// This isn't very efficient, but it shouldn't change very often!
|
|
if ((isWallpaperRemoved != removeWallpaper) ||
|
|
(areEffectsDisabled != disableEffects))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void SDisplay::restartCore() {
|
|
vlog.info("restarting");
|
|
|
|
// Stop the existing Core related resources
|
|
stopCore();
|
|
try {
|
|
// Start a new Core if possible
|
|
startCore();
|
|
vlog.info("restarted");
|
|
} catch (rdr::Exception& e) {
|
|
// If startCore() fails then we MUST disconnect all clients,
|
|
// to cause the server to stop() the desktop.
|
|
// Otherwise, the SDesktop is in an inconsistent state
|
|
// and the server will crash.
|
|
server->closeClients(e.str());
|
|
}
|
|
}
|
|
|
|
|
|
void SDisplay::pointerEvent(const Point& pos, int buttonmask) {
|
|
if (pb->getRect().contains(pos)) {
|
|
Point screenPos = pos.translate(screenRect.tl);
|
|
// - Check that the SDesktop doesn't need restarting
|
|
if (isRestartRequired())
|
|
restartCore();
|
|
if (ptr)
|
|
ptr->pointerEvent(screenPos, buttonmask);
|
|
}
|
|
}
|
|
|
|
void SDisplay::keyEvent(rdr::U32 keysym, rdr::U32 keycode, bool down) {
|
|
// - Check that the SDesktop doesn't need restarting
|
|
if (isRestartRequired())
|
|
restartCore();
|
|
if (kbd)
|
|
kbd->keyEvent(keysym, keycode, down);
|
|
}
|
|
|
|
bool SDisplay::checkLedState() {
|
|
unsigned state = 0;
|
|
|
|
if (GetKeyState(VK_SCROLL) & 0x0001)
|
|
state |= ledScrollLock;
|
|
if (GetKeyState(VK_NUMLOCK) & 0x0001)
|
|
state |= ledNumLock;
|
|
if (GetKeyState(VK_CAPITAL) & 0x0001)
|
|
state |= ledCapsLock;
|
|
|
|
if (ledState != state) {
|
|
ledState = state;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SDisplay::clientCutText(const char* text, int len) {
|
|
CharArray clip_sz(len+1);
|
|
memcpy(clip_sz.buf, text, len);
|
|
clip_sz.buf[len] = 0;
|
|
clipboard->setClipText(clip_sz.buf);
|
|
}
|
|
|
|
|
|
void
|
|
SDisplay::notifyClipboardChanged(const char* text, int len) {
|
|
vlog.debug("clipboard text changed");
|
|
if (server)
|
|
server->serverCutText(text, len);
|
|
}
|
|
|
|
|
|
void
|
|
SDisplay::notifyDisplayEvent(WMMonitor::Notifier::DisplayEventType evt) {
|
|
switch (evt) {
|
|
case WMMonitor::Notifier::DisplaySizeChanged:
|
|
vlog.debug("desktop size changed");
|
|
recreatePixelBuffer();
|
|
break;
|
|
case WMMonitor::Notifier::DisplayPixelFormatChanged:
|
|
vlog.debug("desktop format changed");
|
|
recreatePixelBuffer();
|
|
break;
|
|
default:
|
|
vlog.error("unknown display event received");
|
|
}
|
|
}
|
|
|
|
void
|
|
SDisplay::processEvent(HANDLE event) {
|
|
if (event == updateEvent) {
|
|
vlog.write(120, "processEvent");
|
|
ResetEvent(updateEvent);
|
|
|
|
// - If the SDisplay isn't even started then quit now
|
|
if (!core) {
|
|
vlog.error("not start()ed");
|
|
return;
|
|
}
|
|
|
|
// - Ensure that the disableLocalInputs flag is respected
|
|
inputs->blockInputs(disableLocalInputs);
|
|
|
|
// - Only process updates if the server is ready
|
|
if (server) {
|
|
// - Check that the SDesktop doesn't need restarting
|
|
if (isRestartRequired()) {
|
|
restartCore();
|
|
return;
|
|
}
|
|
|
|
// - Flush any updates from the core
|
|
try {
|
|
core->flushUpdates();
|
|
} catch (rdr::Exception& e) {
|
|
vlog.error("%s", e.str());
|
|
restartCore();
|
|
return;
|
|
}
|
|
|
|
// Ensure the cursor is up to date
|
|
WMCursor::Info info = cursor->getCursorInfo();
|
|
if (old_cursor != info) {
|
|
// Update the cursor shape if the visibility has changed
|
|
bool set_cursor = info.visible != old_cursor.visible;
|
|
// OR if the cursor is visible and the shape has changed.
|
|
set_cursor |= info.visible && (old_cursor.cursor != info.cursor);
|
|
|
|
// Update the cursor shape
|
|
if (set_cursor)
|
|
pb->setCursor(info.visible ? info.cursor : 0, server);
|
|
|
|
// Update the cursor position
|
|
// NB: First translate from Screen coordinates to Desktop
|
|
Point desktopPos = info.position.translate(screenRect.tl.negate());
|
|
server->setCursorPos(desktopPos, false);
|
|
|
|
old_cursor = info;
|
|
}
|
|
|
|
// Flush any changes to the server
|
|
flushChangeTracker();
|
|
|
|
// Forward current LED state to the server
|
|
if (checkLedState())
|
|
server->setLEDState(ledState);
|
|
}
|
|
return;
|
|
}
|
|
throw rdr::Exception("No such event");
|
|
}
|
|
|
|
|
|
// -=- Protected methods
|
|
|
|
void
|
|
SDisplay::recreatePixelBuffer(bool force) {
|
|
// Open the specified display device
|
|
// If no device is specified, open entire screen using GetDC().
|
|
// Opening the whole display with CreateDC doesn't work on multi-monitor
|
|
// systems for some reason.
|
|
DeviceContext* new_device = 0;
|
|
TCharArray deviceName(displayDevice.getData());
|
|
if (deviceName.buf[0]) {
|
|
vlog.info("Attaching to device %s", (const char*)CStr(deviceName.buf));
|
|
new_device = new DeviceDC(deviceName.buf);
|
|
}
|
|
if (!new_device) {
|
|
vlog.info("Attaching to virtual desktop");
|
|
new_device = new WindowDC(0);
|
|
}
|
|
|
|
// Get the coordinates of the specified dispay device
|
|
Rect newScreenRect;
|
|
if (deviceName.buf[0]) {
|
|
MonitorInfo info(CStr(deviceName.buf));
|
|
newScreenRect = Rect(info.rcMonitor.left, info.rcMonitor.top,
|
|
info.rcMonitor.right, info.rcMonitor.bottom);
|
|
} else {
|
|
newScreenRect = new_device->getClipBox();
|
|
}
|
|
|
|
// If nothing has changed & a recreate has not been forced, delete
|
|
// the new device context and return
|
|
if (pb && !force &&
|
|
newScreenRect.equals(screenRect) &&
|
|
new_device->getPF().equal(pb->getPF())) {
|
|
delete new_device;
|
|
return;
|
|
}
|
|
|
|
// Flush any existing changes to the server
|
|
flushChangeTracker();
|
|
|
|
// Delete the old pixelbuffer and device context
|
|
vlog.debug("deleting old pixel buffer & device");
|
|
if (pb)
|
|
delete pb;
|
|
if (device)
|
|
delete device;
|
|
|
|
// Create a DeviceFrameBuffer attached to the new device
|
|
vlog.debug("creating pixel buffer");
|
|
DeviceFrameBuffer* new_buffer = new DeviceFrameBuffer(*new_device);
|
|
|
|
// Replace the old PixelBuffer
|
|
screenRect = newScreenRect;
|
|
pb = new_buffer;
|
|
device = new_device;
|
|
|
|
// Initialise the pixels
|
|
pb->grabRegion(pb->getRect());
|
|
|
|
// Prevent future grabRect operations from throwing exceptions
|
|
pb->setIgnoreGrabErrors(true);
|
|
|
|
// Update the clipping update tracker
|
|
clipper.setClipRect(pb->getRect());
|
|
|
|
// Inform the core of the changes
|
|
if (core)
|
|
core->setScreenRect(screenRect);
|
|
|
|
// Inform the server of the changes
|
|
if (server)
|
|
server->setPixelBuffer(pb);
|
|
}
|
|
|
|
bool SDisplay::flushChangeTracker() {
|
|
if (updates.is_empty())
|
|
return false;
|
|
|
|
vlog.write(120, "flushChangeTracker");
|
|
|
|
// Translate the update coordinates from Screen coords to Desktop
|
|
updates.translate(screenRect.tl.negate());
|
|
|
|
// Clip the updates & flush them to the server
|
|
updates.copyTo(&clipper);
|
|
updates.clear();
|
|
return true;
|
|
}
|