Home | History | Annotate | Download | only in host
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "remoting/host/desktop_resizer.h"
      6 #include "remoting/host/linux/x11_util.h"
      7 
      8 #include <string.h>
      9 #include <X11/extensions/Xrandr.h>
     10 #include <X11/Xlib.h>
     11 
     12 #include "base/command_line.h"
     13 #include "remoting/base/logging.h"
     14 
     15 // On Linux, we use the xrandr extension to change the desktop resolution. For
     16 // now, we only support resize-to-client for Xvfb-based servers that can match
     17 // the client resolution exactly. To support best-resolution matching, it would
     18 // be necessary to implement |GetSupportedResolutions|, but it's not considered
     19 // a priority now.
     20 //
     21 // Xrandr has a number of restrictions that make this code more complex:
     22 //
     23 //   1. It's not possible to change the resolution of an existing mode. Instead,
     24 //      the mode must be deleted and recreated.
     25 //   2. It's not possible to delete a mode that's in use.
     26 //   3. Errors are communicated via Xlib's spectacularly unhelpful mechanism
     27 //      of terminating the process unless you install an error handler.
     28 //
     29 // The basic approach is as follows:
     30 //
     31 //   1. Create a new mode with the correct resolution;
     32 //   2. Switch to the new mode;
     33 //   3. Delete the old mode.
     34 //
     35 // Since the new mode must have a different name, and we want the current mode
     36 // name to be consistent, we then additionally:
     37 //
     38 //   4. Recreate the old mode at the new resolution;
     39 //   5. Switch to the old mode;
     40 //   6. Delete the temporary mode.
     41 //
     42 // Name consistency will allow a future CL to disable resize-to-client if the
     43 // user has changed the mode to something other than "Chrome Remote Desktop
     44 // client resolution". It doesn't make the code significantly more complex.
     45 
     46 namespace {
     47 
     48 int PixelsToMillimeters(int pixels, int dpi) {
     49   DCHECK(dpi != 0);
     50 
     51   const double kMillimetersPerInch = 25.4;
     52 
     53   // (pixels / dpi) is the length in inches. Multiplying by
     54   // kMillimetersPerInch converts to mm. Multiplication is done first to
     55   // avoid integer division.
     56   return static_cast<int>(kMillimetersPerInch * pixels / dpi);
     57 }
     58 
     59 // TODO(jamiewalch): Use the correct DPI for the mode: http://crbug.com/172405.
     60 const int kDefaultDPI = 96;
     61 
     62 }  // namespace
     63 
     64 namespace remoting {
     65 
     66 // Wrapper class for the XRRScreenResources struct.
     67 class ScreenResources {
     68  public:
     69   ScreenResources() : resources_(NULL) {
     70   }
     71 
     72   ~ScreenResources() {
     73     Release();
     74   }
     75 
     76   bool Refresh(Display* display, Window window) {
     77     Release();
     78     resources_ = XRRGetScreenResources(display, window);
     79     return resources_ != NULL;
     80   }
     81 
     82   void Release() {
     83     if (resources_) {
     84       XRRFreeScreenResources(resources_);
     85       resources_ = NULL;
     86     }
     87   }
     88 
     89   RRMode GetIdForMode(const char* name) {
     90     CHECK(resources_);
     91     for (int i = 0; i < resources_->nmode; ++i) {
     92       const XRRModeInfo& mode = resources_->modes[i];
     93       if (strcmp(mode.name, name) == 0) {
     94         return mode.id;
     95       }
     96     }
     97     return 0;
     98   }
     99 
    100   // For now, assume we're only ever interested in the first output.
    101   RROutput GetOutput() {
    102     CHECK(resources_);
    103     return resources_->outputs[0];
    104   }
    105 
    106   // For now, assume we're only ever interested in the first crtc.
    107   RRCrtc GetCrtc() {
    108     CHECK(resources_);
    109     return resources_->crtcs[0];
    110   }
    111 
    112   XRROutputInfo* GetOutputInfo(Display* display, RROutput output_id) {
    113     CHECK(resources_);
    114     return XRRGetOutputInfo(display, resources_, output_id);
    115   }
    116 
    117   XRRScreenResources* get() { return resources_; }
    118 
    119  private:
    120   XRRScreenResources* resources_;
    121 };
    122 
    123 
    124 class DesktopResizerLinux : public DesktopResizer {
    125  public:
    126   DesktopResizerLinux();
    127   virtual ~DesktopResizerLinux();
    128 
    129   // DesktopResizer interface
    130   virtual ScreenResolution GetCurrentResolution() OVERRIDE;
    131   virtual std::list<ScreenResolution> GetSupportedResolutions(
    132       const ScreenResolution& preferred) OVERRIDE;
    133   virtual void SetResolution(const ScreenResolution& resolution) OVERRIDE;
    134   virtual void RestoreResolution(const ScreenResolution& original) OVERRIDE;
    135 
    136  private:
    137   // Create a mode, and attach it to the primary output. If the mode already
    138   // exists, it is left unchanged.
    139   void CreateMode(const char* name, int width, int height);
    140 
    141   // Remove the specified mode from the primary output, and delete it. If the
    142   // mode is in use, it is not deleted.
    143   void DeleteMode(const char* name);
    144 
    145   // Switch the primary output to the specified mode. If name is NULL, the
    146   // primary output is disabled instead, which is required before changing
    147   // its resolution.
    148   void SwitchToMode(const char* name);
    149 
    150   Display* display_;
    151   int screen_;
    152   Window root_;
    153   ScreenResources resources_;
    154   bool exact_resize_;
    155 
    156   DISALLOW_COPY_AND_ASSIGN(DesktopResizerLinux);
    157 };
    158 
    159 DesktopResizerLinux::DesktopResizerLinux()
    160     : display_(XOpenDisplay(NULL)),
    161       screen_(DefaultScreen(display_)),
    162       root_(RootWindow(display_, screen_)),
    163       exact_resize_(base::CommandLine::ForCurrentProcess()->
    164                     HasSwitch("server-supports-exact-resize")) {
    165   XRRSelectInput(display_, root_, RRScreenChangeNotifyMask);
    166 }
    167 
    168 DesktopResizerLinux::~DesktopResizerLinux() {
    169   XCloseDisplay(display_);
    170 }
    171 
    172 ScreenResolution DesktopResizerLinux::GetCurrentResolution() {
    173   if (!exact_resize_) {
    174     // TODO(jamiewalch): Remove this early return if we decide to support
    175     // non-Xvfb servers.
    176     return ScreenResolution();
    177   }
    178 
    179   // TODO(lambroslambrou): Xrandr requires that we process RRScreenChangeNotify
    180   // events, otherwise DisplayWidth and DisplayHeight do not return the current
    181   // values. Normally, this would be done via a central X event loop, but we
    182   // don't have one, hence this horrible hack.
    183   //
    184   // Note that the WatchFileDescriptor approach taken in XServerClipboard
    185   // doesn't work here because resize events have already been read from the
    186   // X server socket by the time the resize function returns, hence the
    187   // file descriptor is never seen as readable.
    188   while (XEventsQueued(display_, QueuedAlready)) {
    189     XEvent event;
    190     XNextEvent(display_, &event);
    191     XRRUpdateConfiguration(&event);
    192   }
    193 
    194   ScreenResolution result(
    195       webrtc::DesktopSize(
    196           DisplayWidth(display_, DefaultScreen(display_)),
    197           DisplayHeight(display_, DefaultScreen(display_))),
    198       webrtc::DesktopVector(kDefaultDPI, kDefaultDPI));
    199   return result;
    200 }
    201 
    202 std::list<ScreenResolution> DesktopResizerLinux::GetSupportedResolutions(
    203     const ScreenResolution& preferred) {
    204   std::list<ScreenResolution> result;
    205   if (exact_resize_) {
    206     // Clamp the specified size to something valid for the X server.
    207     int min_width = 0, min_height = 0, max_width = 0, max_height = 0;
    208     XRRGetScreenSizeRange(display_, root_,
    209                           &min_width, &min_height,
    210                           &max_width, &max_height);
    211     int width = std::min(std::max(preferred.dimensions().width(), min_width),
    212                          max_width);
    213     int height = std::min(std::max(preferred.dimensions().height(), min_height),
    214                           max_height);
    215     // Additionally impose a minimum size of 640x480, since anything smaller
    216     // doesn't seem very useful.
    217     ScreenResolution actual(
    218         webrtc::DesktopSize(std::max(640, width), std::max(480, height)),
    219         webrtc::DesktopVector(kDefaultDPI, kDefaultDPI));
    220     result.push_back(actual);
    221   } else {
    222     // TODO(jamiewalch): Return the list of supported resolutions if we can't
    223     // support exact-size matching.
    224   }
    225   return result;
    226 }
    227 
    228 void DesktopResizerLinux::SetResolution(const ScreenResolution& resolution) {
    229   if (!exact_resize_) {
    230     // TODO(jamiewalch): Remove this early return if we decide to support
    231     // non-Xvfb servers.
    232     return;
    233   }
    234 
    235   // Ignore X errors encountered while resizing the display. We might hit an
    236   // error, for example if xrandr has been used to add a mode with the same
    237   // name as our temporary mode, or to remove the "client resolution" mode. We
    238   // don't want to terminate the process if this happens.
    239   ScopedXErrorHandler handler(ScopedXErrorHandler::Ignore());
    240 
    241   // Grab the X server while we're changing the display resolution. This ensures
    242   // that the display configuration doesn't change under our feet.
    243   ScopedXGrabServer grabber(display_);
    244 
    245   // The name of the mode representing the current client view resolution and
    246   // the temporary mode used for the reasons described at the top of this file.
    247   // The former should be localized if it's user-visible; the latter only
    248   // exists briefly and does not need to localized.
    249   const char* kModeName = "Chrome Remote Desktop client resolution";
    250   const char* kTempModeName = "Chrome Remote Desktop temporary mode";
    251 
    252   // Actually do the resize operation, preserving the current mode name. Note
    253   // that we have to detach the output from any mode in order to resize it
    254   // (strictly speaking, this is only required when reducing the size, but it
    255   // seems safe to do it regardless).
    256   HOST_LOG << "Changing desktop size to " << resolution.dimensions().width()
    257             << "x" << resolution.dimensions().height();
    258 
    259   // TODO(lambroslambrou): Use the DPI from client size information.
    260   int width_mm = PixelsToMillimeters(resolution.dimensions().width(),
    261                                      kDefaultDPI);
    262   int height_mm = PixelsToMillimeters(resolution.dimensions().height(),
    263                                       kDefaultDPI);
    264   CreateMode(kTempModeName, resolution.dimensions().width(),
    265              resolution.dimensions().height());
    266   SwitchToMode(NULL);
    267   XRRSetScreenSize(display_, root_, resolution.dimensions().width(),
    268                    resolution.dimensions().height(), width_mm, height_mm);
    269   SwitchToMode(kTempModeName);
    270   DeleteMode(kModeName);
    271   CreateMode(kModeName, resolution.dimensions().width(),
    272              resolution.dimensions().height());
    273   SwitchToMode(kModeName);
    274   DeleteMode(kTempModeName);
    275 }
    276 
    277 void DesktopResizerLinux::RestoreResolution(const ScreenResolution& original) {
    278   // Since the desktop is only visible via a remote connection, the original
    279   // resolution of the desktop will never been seen and there's no point
    280   // restoring it; if we did, we'd just risk messing up the user's window
    281   // layout.
    282 }
    283 
    284 void DesktopResizerLinux::CreateMode(const char* name, int width, int height) {
    285   XRRModeInfo mode;
    286   memset(&mode, 0, sizeof(mode));
    287   mode.width = width;
    288   mode.height = height;
    289   mode.name = const_cast<char*>(name);
    290   mode.nameLength = strlen(name);
    291   XRRCreateMode(display_, root_, &mode);
    292 
    293   if (!resources_.Refresh(display_, root_)) {
    294     return;
    295   }
    296   RRMode mode_id = resources_.GetIdForMode(name);
    297   if (!mode_id) {
    298     return;
    299   }
    300   XRRAddOutputMode(display_, resources_.GetOutput(), mode_id);
    301 }
    302 
    303 void DesktopResizerLinux::DeleteMode(const char* name) {
    304   RRMode mode_id = resources_.GetIdForMode(name);
    305   if (mode_id) {
    306     XRRDeleteOutputMode(display_, resources_.GetOutput(), mode_id);
    307     XRRDestroyMode(display_, mode_id);
    308     resources_.Refresh(display_, root_);
    309   }
    310 }
    311 
    312 void DesktopResizerLinux::SwitchToMode(const char* name) {
    313   RRMode mode_id = None;
    314   RROutput* outputs = NULL;
    315   int number_of_outputs = 0;
    316   if (name) {
    317     mode_id = resources_.GetIdForMode(name);
    318     CHECK(mode_id);
    319     outputs = resources_.get()->outputs;
    320     number_of_outputs = resources_.get()->noutput;
    321   }
    322   XRRSetCrtcConfig(display_, resources_.get(), resources_.GetCrtc(),
    323                    CurrentTime, 0, 0, mode_id, 1, outputs, number_of_outputs);
    324 }
    325 
    326 scoped_ptr<DesktopResizer> DesktopResizer::Create() {
    327   return scoped_ptr<DesktopResizer>(new DesktopResizerLinux);
    328 }
    329 
    330 }  // namespace remoting
    331