1 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
2  * Copyright 2011-2019 Pierre Ossman for Cendio AB
3  *
4  * This is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This software is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this software; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
17  * USA.
18  */
19 
20 // -=- SDisplay.cxx
21 //
22 // The SDisplay class encapsulates a particular system display.
23 
24 #include <assert.h>
25 
26 #include <rfb_win32/SDisplay.h>
27 #include <rfb_win32/Service.h>
28 #include <rfb_win32/TsSessions.h>
29 #include <rfb_win32/CleanDesktop.h>
30 #include <rfb_win32/CurrentUser.h>
31 #include <rfb_win32/MonitorInfo.h>
32 #include <rfb_win32/SDisplayCorePolling.h>
33 #include <rfb_win32/SDisplayCoreWMHooks.h>
34 #include <rfb/Exception.h>
35 #include <rfb/LogWriter.h>
36 #include <rfb/ledStates.h>
37 
38 
39 using namespace rdr;
40 using namespace rfb;
41 using namespace rfb::win32;
42 
43 static LogWriter vlog("SDisplay");
44 
45 // - SDisplay-specific configuration options
46 
47 IntParameter rfb::win32::SDisplay::updateMethod("UpdateMethod",
48   "How to discover desktop updates; 0 - Polling, 1 - Application hooking, 2 - Driver hooking.", 0);
49 BoolParameter rfb::win32::SDisplay::disableLocalInputs("DisableLocalInputs",
50   "Disable local keyboard and pointer input while the server is in use", false);
51 StringParameter rfb::win32::SDisplay::disconnectAction("DisconnectAction",
52   "Action to perform when all clients have disconnected.  (None, Lock, Logoff)", "None");
53 StringParameter displayDevice("DisplayDevice",
54   "Display device name of the monitor to be remoted, or empty to export the whole desktop.", "");
55 BoolParameter rfb::win32::SDisplay::removeWallpaper("RemoveWallpaper",
56   "Remove the desktop wallpaper when the server is in use.", false);
57 BoolParameter rfb::win32::SDisplay::disableEffects("DisableEffects",
58   "Disable desktop user interface effects when the server is in use.", false);
59 
60 
61 //////////////////////////////////////////////////////////////////////////////
62 //
63 // SDisplay
64 //
65 
66 // -=- Constructor/Destructor
67 
SDisplay()68 SDisplay::SDisplay()
69   : server(0), pb(0), device(0),
70     core(0), ptr(0), kbd(0), clipboard(0),
71     inputs(0), monitor(0), cleanDesktop(0), cursor(0),
72     statusLocation(0), queryConnectionHandler(0), ledState(0)
73 {
74   updateEvent.h = CreateEvent(0, TRUE, FALSE, 0);
75   terminateEvent.h = CreateEvent(0, TRUE, FALSE, 0);
76 }
77 
~SDisplay()78 SDisplay::~SDisplay()
79 {
80   // XXX when the VNCServer has been deleted with clients active, stop()
81   // doesn't get called - this ought to be fixed in VNCServerST.  In any event,
82   // we should never call any methods on VNCServer once we're being deleted.
83   // This is because it is supposed to be guaranteed that the SDesktop exists
84   // throughout the lifetime of the VNCServer.  So if we're being deleted, then
85   // the VNCServer ought not to exist and therefore we shouldn't invoke any
86   // methods on it.  Setting server to zero here ensures that stop() doesn't
87   // call setPixelBuffer(0) on the server.
88   server = 0;
89   if (core) stop();
90 }
91 
92 
93 // -=- SDesktop interface
94 
start(VNCServer * vs)95 void SDisplay::start(VNCServer* vs)
96 {
97   vlog.debug("starting");
98 
99   // Try to make session zero the console session
100   if (!inConsoleSession())
101     setConsoleSession();
102 
103   // Start the SDisplay core
104   server = vs;
105   startCore();
106 
107   vlog.debug("started");
108 
109   if (statusLocation) *statusLocation = true;
110 }
111 
stop()112 void SDisplay::stop()
113 {
114   vlog.debug("stopping");
115 
116   // If we successfully start()ed then perform the DisconnectAction
117   if (core) {
118     CurrentUserToken cut;
119     CharArray action(disconnectAction.getData());
120     if (stricmp(action.buf, "Logoff") == 0) {
121       if (!cut.h)
122         vlog.info("ignoring DisconnectAction=Logoff - no current user");
123       else
124         ExitWindowsEx(EWX_LOGOFF, 0);
125     } else if (stricmp(action.buf, "Lock") == 0) {
126       if (!cut.h) {
127         vlog.info("ignoring DisconnectAction=Lock - no current user");
128       } else {
129         LockWorkStation();
130       }
131     }
132   }
133 
134   // Stop the SDisplayCore
135   if (server)
136     server->setPixelBuffer(0);
137   stopCore();
138   server = 0;
139 
140   vlog.debug("stopped");
141 
142   if (statusLocation) *statusLocation = false;
143 }
144 
terminate()145 void SDisplay::terminate()
146 {
147   SetEvent(terminateEvent);
148 }
149 
150 
queryConnection(network::Socket * sock,const char * userName)151 void SDisplay::queryConnection(network::Socket* sock,
152                                const char* userName)
153 {
154   assert(server != NULL);
155 
156   if (queryConnectionHandler) {
157     queryConnectionHandler->queryConnection(sock, userName);
158     return;
159   }
160 
161   server->approveConnection(sock, true);
162 }
163 
164 
startCore()165 void SDisplay::startCore() {
166 
167   // Currently, we just check whether we're in the console session, and
168   //   fail if not
169   if (!inConsoleSession())
170     throw rdr::Exception("Console is not session zero - oreconnect to restore Console sessin");
171 
172   // Switch to the current input desktop
173   if (rfb::win32::desktopChangeRequired()) {
174     if (!rfb::win32::changeDesktop())
175       throw rdr::Exception("unable to switch into input desktop");
176   }
177 
178   // Initialise the change tracker and clipper
179   updates.clear();
180   clipper.setUpdateTracker(server);
181 
182   // Create the framebuffer object
183   recreatePixelBuffer(true);
184 
185   // Create the SDisplayCore
186   updateMethod_ = updateMethod;
187   int tryMethod = updateMethod_;
188   while (!core) {
189     try {
190       if (tryMethod == 1)
191         core = new SDisplayCoreWMHooks(this, &updates);
192       else
193         core = new SDisplayCorePolling(this, &updates);
194       core->setScreenRect(screenRect);
195     } catch (rdr::Exception& e) {
196       delete core; core = 0;
197       if (tryMethod == 0)
198         throw rdr::Exception("unable to access desktop");
199       tryMethod--;
200       vlog.error("%s", e.str());
201     }
202   }
203   vlog.info("Started %s", core->methodName());
204 
205   // Start display monitor, clipboard handler and input handlers
206   monitor = new WMMonitor;
207   monitor->setNotifier(this);
208   clipboard = new Clipboard;
209   clipboard->setNotifier(this);
210   ptr = new SPointer;
211   kbd = new SKeyboard;
212   inputs = new WMBlockInput;
213   cursor = new WMCursor;
214 
215   // Apply desktop optimisations
216   cleanDesktop = new CleanDesktop;
217   if (removeWallpaper)
218     cleanDesktop->disableWallpaper();
219   if (disableEffects)
220     cleanDesktop->disableEffects();
221   isWallpaperRemoved = removeWallpaper;
222   areEffectsDisabled = disableEffects;
223 
224   checkLedState();
225   if (server)
226     server->setLEDState(ledState);
227 }
228 
stopCore()229 void SDisplay::stopCore() {
230   if (core)
231     vlog.info("Stopping %s", core->methodName());
232   delete core; core = 0;
233   delete pb; pb = 0;
234   delete device; device = 0;
235   delete monitor; monitor = 0;
236   delete clipboard; clipboard = 0;
237   delete inputs; inputs = 0;
238   delete ptr; ptr = 0;
239   delete kbd; kbd = 0;
240   delete cleanDesktop; cleanDesktop = 0;
241   delete cursor; cursor = 0;
242   ResetEvent(updateEvent);
243 }
244 
245 
isRestartRequired()246 bool SDisplay::isRestartRequired() {
247   // - We must restart the SDesktop if:
248   // 1. We are no longer in the input desktop.
249   // 2. The any setting has changed.
250 
251   // - Check that our session is the Console
252   if (!inConsoleSession())
253     return true;
254 
255   // - Check that we are in the input desktop
256   if (rfb::win32::desktopChangeRequired())
257     return true;
258 
259   // - Check that the update method setting hasn't changed
260   //   NB: updateMethod reflects the *selected* update method, not
261   //   necessarily the one in use, since we fall back to simpler
262   //   methods if more advanced ones fail!
263   if (updateMethod_ != updateMethod)
264     return true;
265 
266   // - Check that the desktop optimisation settings haven't changed
267   //   This isn't very efficient, but it shouldn't change very often!
268   if ((isWallpaperRemoved != removeWallpaper) ||
269       (areEffectsDisabled != disableEffects))
270     return true;
271 
272   return false;
273 }
274 
275 
restartCore()276 void SDisplay::restartCore() {
277   vlog.info("restarting");
278 
279   // Stop the existing Core  related resources
280   stopCore();
281   try {
282     // Start a new Core if possible
283     startCore();
284     vlog.info("restarted");
285   } catch (rdr::Exception& e) {
286     // If startCore() fails then we MUST disconnect all clients,
287     // to cause the server to stop() the desktop.
288     // Otherwise, the SDesktop is in an inconsistent state
289     // and the server will crash.
290     server->closeClients(e.str());
291   }
292 }
293 
294 
handleClipboardRequest()295 void SDisplay::handleClipboardRequest() {
296   CharArray data(clipboard->getClipText());
297   server->sendClipboardData(data.buf);
298 }
299 
handleClipboardAnnounce(bool available)300 void SDisplay::handleClipboardAnnounce(bool available) {
301   // FIXME: Wait for an application to actually request it
302   if (available)
303     server->requestClipboard();
304 }
305 
handleClipboardData(const char * data)306 void SDisplay::handleClipboardData(const char* data) {
307   clipboard->setClipText(data);
308 }
309 
310 
pointerEvent(const Point & pos,int buttonmask)311 void SDisplay::pointerEvent(const Point& pos, int buttonmask) {
312   if (pb->getRect().contains(pos)) {
313     Point screenPos = pos.translate(screenRect.tl);
314     // - Check that the SDesktop doesn't need restarting
315     if (isRestartRequired())
316       restartCore();
317     if (ptr)
318       ptr->pointerEvent(screenPos, buttonmask);
319   }
320 }
321 
keyEvent(rdr::U32 keysym,rdr::U32 keycode,bool down)322 void SDisplay::keyEvent(rdr::U32 keysym, rdr::U32 keycode, bool down) {
323   // - Check that the SDesktop doesn't need restarting
324   if (isRestartRequired())
325     restartCore();
326   if (kbd)
327     kbd->keyEvent(keysym, keycode, down);
328 }
329 
checkLedState()330 bool SDisplay::checkLedState() {
331   unsigned state = 0;
332 
333   if (GetKeyState(VK_SCROLL) & 0x0001)
334     state |= ledScrollLock;
335   if (GetKeyState(VK_NUMLOCK) & 0x0001)
336     state |= ledNumLock;
337   if (GetKeyState(VK_CAPITAL) & 0x0001)
338     state |= ledCapsLock;
339 
340   if (ledState != state) {
341     ledState = state;
342     return true;
343   }
344 
345   return false;
346 }
347 
348 
349 void
notifyClipboardChanged(bool available)350 SDisplay::notifyClipboardChanged(bool available) {
351   vlog.debug("clipboard text changed");
352   if (server)
353     server->announceClipboard(available);
354 }
355 
356 
357 void
notifyDisplayEvent(WMMonitor::Notifier::DisplayEventType evt)358 SDisplay::notifyDisplayEvent(WMMonitor::Notifier::DisplayEventType evt) {
359   switch (evt) {
360   case WMMonitor::Notifier::DisplaySizeChanged:
361     vlog.debug("desktop size changed");
362     recreatePixelBuffer();
363     break;
364   case WMMonitor::Notifier::DisplayPixelFormatChanged:
365     vlog.debug("desktop format changed");
366     recreatePixelBuffer();
367     break;
368   default:
369     vlog.error("unknown display event received");
370   }
371 }
372 
373 void
processEvent(HANDLE event)374 SDisplay::processEvent(HANDLE event) {
375   if (event == updateEvent) {
376     vlog.write(120, "processEvent");
377     ResetEvent(updateEvent);
378 
379     // - If the SDisplay isn't even started then quit now
380     if (!core) {
381       vlog.error("not start()ed");
382       return;
383     }
384 
385     // - Ensure that the disableLocalInputs flag is respected
386     inputs->blockInputs(disableLocalInputs);
387 
388     // - Only process updates if the server is ready
389     if (server) {
390       // - Check that the SDesktop doesn't need restarting
391       if (isRestartRequired()) {
392         restartCore();
393         return;
394       }
395 
396       // - Flush any updates from the core
397       try {
398         core->flushUpdates();
399       } catch (rdr::Exception& e) {
400         vlog.error("%s", e.str());
401         restartCore();
402         return;
403       }
404 
405       // Ensure the cursor is up to date
406       WMCursor::Info info = cursor->getCursorInfo();
407       if (old_cursor != info) {
408         // Update the cursor shape if the visibility has changed
409         bool set_cursor = info.visible != old_cursor.visible;
410         // OR if the cursor is visible and the shape has changed.
411         set_cursor |= info.visible && (old_cursor.cursor != info.cursor);
412 
413         // Update the cursor shape
414         if (set_cursor)
415           pb->setCursor(info.visible ? info.cursor : 0, server);
416 
417         // Update the cursor position
418         // NB: First translate from Screen coordinates to Desktop
419         Point desktopPos = info.position.translate(screenRect.tl.negate());
420         server->setCursorPos(desktopPos, false);
421 
422         old_cursor = info;
423       }
424 
425       // Flush any changes to the server
426       flushChangeTracker();
427 
428       // Forward current LED state to the server
429       if (checkLedState())
430         server->setLEDState(ledState);
431     }
432     return;
433   }
434   throw rdr::Exception("No such event");
435 }
436 
437 
438 // -=- Protected methods
439 
440 void
recreatePixelBuffer(bool force)441 SDisplay::recreatePixelBuffer(bool force) {
442   // Open the specified display device
443   //   If no device is specified, open entire screen using GetDC().
444   //   Opening the whole display with CreateDC doesn't work on multi-monitor
445   //   systems for some reason.
446   DeviceContext* new_device = 0;
447   TCharArray deviceName(displayDevice.getData());
448   if (deviceName.buf[0]) {
449     vlog.info("Attaching to device %s", (const char*)CStr(deviceName.buf));
450     new_device = new DeviceDC(deviceName.buf);
451   }
452   if (!new_device) {
453     vlog.info("Attaching to virtual desktop");
454     new_device = new WindowDC(0);
455   }
456 
457   // Get the coordinates of the specified dispay device
458   Rect newScreenRect;
459   if (deviceName.buf[0]) {
460     MonitorInfo info(CStr(deviceName.buf));
461     newScreenRect = Rect(info.rcMonitor.left, info.rcMonitor.top,
462                          info.rcMonitor.right, info.rcMonitor.bottom);
463   } else {
464     newScreenRect = new_device->getClipBox();
465   }
466 
467   // If nothing has changed & a recreate has not been forced, delete
468   // the new device context and return
469   if (pb && !force &&
470     newScreenRect.equals(screenRect) &&
471     new_device->getPF().equal(pb->getPF())) {
472     delete new_device;
473     return;
474   }
475 
476   // Flush any existing changes to the server
477   flushChangeTracker();
478 
479   // Delete the old pixelbuffer and device context
480   vlog.debug("deleting old pixel buffer & device");
481   if (pb)
482     delete pb;
483   if (device)
484     delete device;
485 
486   // Create a DeviceFrameBuffer attached to the new device
487   vlog.debug("creating pixel buffer");
488   DeviceFrameBuffer* new_buffer = new DeviceFrameBuffer(*new_device);
489 
490   // Replace the old PixelBuffer
491   screenRect = newScreenRect;
492   pb = new_buffer;
493   device = new_device;
494 
495   // Initialise the pixels
496   pb->grabRegion(pb->getRect());
497 
498   // Prevent future grabRect operations from throwing exceptions
499   pb->setIgnoreGrabErrors(true);
500 
501   // Update the clipping update tracker
502   clipper.setClipRect(pb->getRect());
503 
504   // Inform the core of the changes
505   if (core)
506     core->setScreenRect(screenRect);
507 
508   // Inform the server of the changes
509   if (server)
510     server->setPixelBuffer(pb);
511 }
512 
flushChangeTracker()513 bool SDisplay::flushChangeTracker() {
514   if (updates.is_empty())
515     return false;
516 
517   vlog.write(120, "flushChangeTracker");
518 
519   // Translate the update coordinates from Screen coords to Desktop
520   updates.translate(screenRect.tl.negate());
521 
522   // Clip the updates & flush them to the server
523   updates.copyTo(&clipper);
524   updates.clear();
525   return true;
526 }
527