Initial revision


git-svn-id: svn://svn.code.sf.net/p/tigervnc/code/trunk@2 3789f03b-4d11-0410-bbf8-ca57d06f2519
diff --git a/rfb_win32/SDisplay.cxx b/rfb_win32/SDisplay.cxx
new file mode 100644
index 0000000..6fa3ff0
--- /dev/null
+++ b/rfb_win32/SDisplay.cxx
@@ -0,0 +1,612 @@
+/* Copyright (C) 2002-2004 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 <assert.h>
+
+#include <rfb_win32/SDisplay.h>
+#include <rfb_win32/Service.h>
+#include <rfb_win32/WMShatter.h>
+#include <rfb_win32/osVersion.h>
+#include <rfb_win32/Win32Util.h>
+#include <rfb_win32/IntervalTimer.h>
+#include <rfb_win32/CleanDesktop.h>
+
+#include <rfb/util.h>
+#include <rfb/LogWriter.h>
+#include <rfb/Exception.h>
+
+#include <rfb/Configuration.h>
+
+using namespace rdr;
+using namespace rfb;
+using namespace rfb::win32;
+
+static LogWriter vlog("SDisplay");
+
+// - SDisplay-specific configuration options
+
+BoolParameter rfb::win32::SDisplay::use_hooks("UseHooks",
+  "Set hooks in the operating system to capture display updates more efficiently", true);
+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");
+
+BoolParameter rfb::win32::SDisplay::removeWallpaper("RemoveWallpaper",
+  "Remove the desktop wallpaper when the server in in use.", false);
+BoolParameter rfb::win32::SDisplay::removePattern("RemovePattern",
+  "Remove the desktop background pattern when the server in in use.", false);
+BoolParameter rfb::win32::SDisplay::disableEffects("DisableEffects",
+  "Disable desktop user interface effects when the server is in use.", false);
+
+
+// - WM_TIMER ID values
+
+#define TIMER_CURSOR 1
+#define TIMER_UPDATE 2
+#define TIMER_UPDATE_AND_POLL 3
+
+
+// -=- Polling settings
+
+const int POLLING_SEGMENTS = 16;
+
+const int FG_POLLING_FPS = 20;
+const int FG_POLLING_FS_INTERVAL = 1000 / FG_POLLING_FPS;
+const int FG_POLLING_INTERVAL = FG_POLLING_FS_INTERVAL / POLLING_SEGMENTS;
+
+const int BG_POLLING_FS_INTERVAL = 5000;
+const int BG_POLLING_INTERVAL = BG_POLLING_FS_INTERVAL / POLLING_SEGMENTS;
+
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// SDisplayCore
+//
+
+// The SDisplay Core object is created by SDisplay's start() method
+// and deleted by its stop() method.
+// The Core must be created in the current input desktop in order
+// to operate - SDisplay is responsible for ensuring that.
+// The structures contained in the Core are manipulated directly
+// by the SDisplay, which is also responsible for detecting when
+// a desktop-switch is required.
+
+class rfb::win32::SDisplayCore : public MsgWindow {
+public:
+  SDisplayCore(SDisplay* display);
+  ~SDisplayCore();
+
+  void setPixelBuffer(DeviceFrameBuffer* pb_);
+
+  bool isRestartRequired();
+
+  virtual LRESULT processMessage(UINT msg, WPARAM wParam, LPARAM lParam);
+
+  // -=- Timers
+  IntervalTimer pollTimer;
+  IntervalTimer cursorTimer;
+
+  // -=- Input handling
+  rfb::win32::SPointer ptr;
+  rfb::win32::SKeyboard kbd;
+  rfb::win32::Clipboard clipboard;
+
+  // -=- Hook handling objects used outside thread run() method
+  WMCopyRect wm_copyrect;
+  WMPoller wm_poller;
+  WMCursor cursor;
+  WMMonitor wm_monitor;
+  WMHooks wm_hooks;
+  WMBlockInput wm_input;
+
+  // -=- Tidying the desktop
+  CleanDesktop cleanDesktop;
+  bool isWallpaperRemoved;
+  bool isPatternRemoved;
+  bool areEffectsDisabled;
+
+  // -=- Full screen polling
+  int poll_next_y;
+  int poll_y_increment;
+
+  // Are we using hooks?
+  bool use_hooks;
+  bool using_hooks;
+
+  // State of the display object
+  SDisplay* display;
+};
+
+SDisplayCore::SDisplayCore(SDisplay* display_)
+: MsgWindow(_T("SDisplayCore")), display(display_),
+  using_hooks(0), use_hooks(rfb::win32::SDisplay::use_hooks),
+  isWallpaperRemoved(rfb::win32::SDisplay::removeWallpaper),
+  isPatternRemoved(rfb::win32::SDisplay::removePattern),
+  areEffectsDisabled(rfb::win32::SDisplay::disableEffects),
+  pollTimer(getHandle(), TIMER_UPDATE_AND_POLL),
+  cursorTimer(getHandle(), TIMER_CURSOR) {
+  setPixelBuffer(display->pb);
+}
+
+SDisplayCore::~SDisplayCore() {
+}
+
+void SDisplayCore::setPixelBuffer(DeviceFrameBuffer* pb) {
+  poll_y_increment = (display->pb->height()+POLLING_SEGMENTS-1)/POLLING_SEGMENTS;
+  poll_next_y = display->screenRect.tl.y;
+  wm_hooks.setClipRect(display->screenRect);
+  wm_copyrect.setClipRect(display->screenRect);
+  wm_poller.setClipRect(display->screenRect);
+}
+
+
+bool SDisplayCore::isRestartRequired() {
+  // - We must restart the SDesktop if:
+  // 1. We are no longer in the input desktop.
+  // 2. The use_hooks setting has changed.
+
+  // - Check that we are in the input desktop
+  if (rfb::win32::desktopChangeRequired())
+    return true;
+
+  // - Check that the hooks setting hasn't changed
+  // NB: We can't just check using_hooks because that can be false
+  // because they failed, even though use_hooks is true!
+  if (use_hooks != rfb::win32::SDisplay::use_hooks)
+    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 != rfb::win32::SDisplay::removeWallpaper) ||
+      (isPatternRemoved != rfb::win32::SDisplay::removePattern) ||
+      (areEffectsDisabled != rfb::win32::SDisplay::disableEffects))
+    return true;
+
+  return false;
+}
+
+LRESULT SDisplayCore::processMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
+  switch (msg) {
+
+  case WM_TIMER:
+
+    if (display->server && display->server->clientsReadyForUpdate()) {
+
+      // - Check that the SDesktop doesn't need restarting
+      if (isRestartRequired()) {
+        display->restart();
+        return 0;
+      }
+    
+      // - Action depends on the timer message type
+      switch (wParam) {
+
+        // POLL THE SCREEN
+      case TIMER_UPDATE_AND_POLL:
+        // Handle window dragging, polling of consoles, etc.
+        while (wm_poller.processEvent()) {}
+
+        // Poll the next strip of the screen (in Screen coordinates)
+        {
+          Rect pollrect = display->screenRect;
+          if (poll_next_y >= pollrect.br.y) {
+            // Yes.  Reset the counter and return
+            poll_next_y = pollrect.tl.y;
+          } else {
+            // No.  Poll the next section
+            pollrect.tl.y = poll_next_y;
+            poll_next_y += poll_y_increment;
+            pollrect.br.y = min(poll_next_y, pollrect.br.y);
+            display->add_changed(pollrect);
+          }
+        }
+        break;
+
+      case TIMER_CURSOR:
+        display->triggerUpdate();
+        break;
+
+      };
+
+    }
+    return 0;
+
+  };
+
+  return MsgWindow::processMessage(msg, wParam, lParam);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// SDisplay
+//
+
+// -=- Constructor/Destructor
+
+SDisplay::SDisplay(const TCHAR* devName)
+  : server(0), change_tracker(true), pb(0),
+    deviceName(tstrDup(devName)), device(0), releaseDevice(false),
+    core(0), statusLocation(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");
+  server = vs;
+
+  // Switch to the current input desktop
+  // ***
+  if (rfb::win32::desktopChangeRequired()) {
+    if (!rfb::win32::changeDesktop())
+      throw rdr::Exception("unable to switch into input desktop");
+  }
+
+  // Clear the change tracker
+  change_tracker.clear();
+
+  // Create the framebuffer object
+  recreatePixelBuffer();
+
+  // Create the SDisplayCore
+  core = new SDisplayCore(this);
+  assert(core);
+
+  // Start display monitor and clipboard handler
+  core->wm_monitor.setNotifier(this);
+  core->clipboard.setNotifier(this);
+
+  // Apply desktop optimisations
+  if (removePattern)
+    core->cleanDesktop.disablePattern();
+  if (removeWallpaper)
+    core->cleanDesktop.disableWallpaper();
+  if (disableEffects)
+    core->cleanDesktop.disableEffects();
+
+  // Start hooks
+  core->wm_hooks.setClipRect(screenRect);
+  if (core->use_hooks) {
+    // core->wm_hooks.setDiagnosticRange(0, 0x400-1);
+    core->using_hooks = core->wm_hooks.setUpdateTracker(this);
+    if (!core->using_hooks)
+      vlog.debug("hook subsystem failed to initialise");
+  }
+
+  // Set up timers
+  core->pollTimer.start(core->using_hooks ? BG_POLLING_INTERVAL : FG_POLLING_INTERVAL);
+  core->cursorTimer.start(10);
+
+  // Register an interest in faked copyrect events
+  core->wm_copyrect.setUpdateTracker(&change_tracker);
+  core->wm_copyrect.setClipRect(screenRect);
+
+  // Polling of particular windows on the desktop
+  core->wm_poller.setUpdateTracker(&change_tracker);
+  core->wm_poller.setClipRect(screenRect);
+
+  vlog.debug("started");
+
+  if (statusLocation) *statusLocation = true;
+}
+
+void SDisplay::stop()
+{
+  vlog.debug("stopping");
+  if (core) {
+    // If SDisplay was actually active then perform the disconnect action
+    CharArray action = disconnectAction.getData();
+    if (stricmp(action.buf, "Logoff") == 0) {
+      ExitWindowsEx(EWX_LOGOFF, 0);
+    } else if (stricmp(action.buf, "Lock") == 0) {
+      typedef BOOL (WINAPI *_LockWorkStation_proto)();
+      DynamicFn<_LockWorkStation_proto> _LockWorkStation(_T("user32.dll"), "LockWorkStation");
+      if (_LockWorkStation.isValid())
+        (*_LockWorkStation)();
+      else
+        ExitWindowsEx(EWX_LOGOFF, 0);
+    }
+  }
+  delete core;
+  core = 0;
+  delete pb;
+  pb = 0;
+  if (device) {
+    if (releaseDevice)
+      ReleaseDC(0, device);
+    else
+      DeleteDC(device);
+  }
+  device = 0;
+  if (server)
+    server->setPixelBuffer(0);
+
+  server = 0;
+  vlog.debug("stopped");
+
+  if (statusLocation) *statusLocation = false;
+}
+
+void SDisplay::restart() {
+  vlog.debug("restarting");
+  // Close down the hooks
+  delete core;
+  core = 0;
+  try {
+    // Re-start the hooks if possible
+    start(server);
+    vlog.debug("restarted");
+  } catch (rdr::Exception& e) {
+    // If start() fails then we MUST disconnect all clients,
+    // to cause the server to stop using 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, rdr::U8 buttonmask) {
+  if (pb->getRect().contains(pos)) {
+    Point screenPos = pos.translate(screenRect.tl);
+    core->ptr.pointerEvent(screenPos, buttonmask);
+  }
+}
+
+void SDisplay::keyEvent(rdr::U32 key, bool down) {
+  core->kbd.keyEvent(key, down);
+}
+
+void SDisplay::clientCutText(const char* text, int len) {
+  CharArray clip_sz(len+1);
+  memcpy(clip_sz.buf, text, len);
+  clip_sz.buf[len] = 0;
+  core->clipboard.setClipText(clip_sz.buf);
+}
+
+
+void SDisplay::framebufferUpdateRequest()
+{
+  triggerUpdate();
+}
+
+Point SDisplay::getFbSize() {
+  bool startAndStop = !core;
+  // If not started, do minimal initialisation to get desktop size.
+  if (startAndStop) recreatePixelBuffer();
+  Point result = Point(pb->width(), pb->height());
+  // Destroy the initialised structures.
+  if (startAndStop) stop();
+  return result;
+}
+
+
+void
+SDisplay::add_changed(const Region& rgn) {
+  change_tracker.add_changed(rgn);
+  triggerUpdate();
+}
+
+void
+SDisplay::add_copied(const Region& dest, const Point& delta) {
+  change_tracker.add_copied(dest, delta);
+  triggerUpdate();
+}
+
+
+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;
+  case WMMonitor::Notifier::DisplayColourMapChanged:
+    vlog.debug("desktop colourmap changed");
+    pb->updateColourMap();
+    if (server)
+      server->setColourMapEntries();
+    break;
+  default:
+    vlog.error("unknown display event received");
+  }
+}
+
+bool
+SDisplay::processEvent(HANDLE event) {
+  if (event == updateEvent) {
+    vlog.info("processEvent");
+    ResetEvent(updateEvent);
+
+    // - If the SDisplay isn't even started then quit now
+    if (!core) {
+      vlog.error("not start()ed");
+      return true;
+    }
+
+    // - Ensure that the disableLocalInputs flag is respected
+    core->wm_input.blockInputs(SDisplay::disableLocalInputs);
+
+    // - Only process updates if the server is ready
+    if (server && server->clientsReadyForUpdate()) {
+      bool try_update = false;
+
+      // - Check that the SDesktop doesn't need restarting
+      if (core->isRestartRequired()) {
+        restart();
+        return true;
+      }
+    
+      // *** window dragging can be improved - more frequent, more cunning about updates
+      while (core->wm_copyrect.processEvent()) {}
+        
+      // Ensure the cursor is up to date
+      WMCursor::Info info = core->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.x, desktopPos.y);
+        try_update = true;
+
+        old_cursor = info;
+      }
+
+      // Flush any changes to the server
+      try_update = flushChangeTracker() || try_update;
+      if (try_update)
+        server->tryUpdate();
+    }
+  } else {
+    CloseHandle(event);
+    return false;
+  }
+  return true;
+}
+
+
+// -=- Protected methods
+
+void
+SDisplay::recreatePixelBuffer() {
+  vlog.debug("attaching to device %s", deviceName);
+
+  // Open the specified display device
+  HDC new_device;
+  if (deviceName.buf) {
+    new_device = ::CreateDC(_T("DISPLAY"), deviceName.buf, NULL, NULL);
+    releaseDevice = false;
+  } else {
+    // If no device is specified, open entire screen.
+    // Doing this with CreateDC creates problems on multi-monitor systems.
+    new_device = ::GetDC(0);
+    releaseDevice = true;
+  }
+  if (!new_device)
+    throw SystemException("cannot open the display", GetLastError());
+
+  // Get the coordinates of the entire virtual display
+  Rect newScreenRect;
+  {
+    WindowDC rootDC(0);
+    RECT r;
+    if (!GetClipBox(rootDC, &r))
+      throw rdr::SystemException("GetClipBox", GetLastError());
+    newScreenRect = Rect(r.left, r.top, r.right, r.bottom);
+  }
+
+  // Create a DeviceFrameBuffer attached to it
+  DeviceFrameBuffer* new_buffer = new DeviceFrameBuffer(new_device);
+
+  // Has anything actually changed about the screen or the buffer?
+  if (!pb ||
+      (!newScreenRect.equals(screenRect)) ||
+      (!new_buffer->getPF().equal(pb->getPF())))
+  {
+    // Yes.  Update the buffer state.
+    screenRect = newScreenRect;
+    vlog.debug("creating pixel buffer for device");
+
+    // Flush any existing changes to the server
+    flushChangeTracker();
+
+    // Replace the old PixelBuffer
+    if (pb) delete pb;
+    if (device) DeleteDC(device);
+    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 SDisplayCore if required
+    if (core)
+      core->setPixelBuffer(pb);
+
+    // Inform the server of the changes
+    if (server)
+      server->setPixelBuffer(pb);
+
+  } else {
+    delete new_buffer;
+    DeleteDC(new_device);
+  }
+}
+
+bool SDisplay::flushChangeTracker() {
+  if (change_tracker.is_empty())
+    return false;
+  // Translate the update coordinates from Screen coords to Desktop
+  change_tracker.translate(screenRect.tl.negate());
+  // Flush the updates through
+  change_tracker.get_update(*server);
+  change_tracker.clear();
+  return true;
+}
+
+void SDisplay::triggerUpdate() {
+  if (core)
+    SetEvent(updateEvent);
+}