Bug 1338004 - Add headless browser mode. r=jrmuizel, r=ted
authorBrendan Dahl <bdahl@mozilla.com>
Tue, 04 Apr 2017 10:22:00 -0400
changeset 401742 23674d7089036e3c7cb8333b3eaec3b805996d72
parent 401741 4e4a0ac84712e02130e2667528005789d543c36b
child 401743 7f8f5cada5b7e2ae6f26ef4a2eecee3f6b33767d
push id1490
push usermtabara@mozilla.com
push dateMon, 31 Jul 2017 14:08:16 +0000
treeherdermozilla-release@70e32e6bf15e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjrmuizel, ted
bugs1338004
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1338004 - Add headless browser mode. r=jrmuizel, r=ted Supports creating a windowless browser on Linux without an X server. Most of the changes are just adding branches to avoid calls in to GTK which calls into X. Some of the bigger additions were adding a separate headless widget which implements just enough to render a page. A headless look and feel were also added since there are many calls into GTK in the platform specific one.
dom/base/nsDOMWindowUtils.cpp
gfx/thebes/gfxFcPlatformFontList.cpp
gfx/thebes/gfxPlatform.cpp
gfx/thebes/gfxPlatform.h
gfx/thebes/gfxPlatformGtk.cpp
image/decoders/icon/gtk/nsIconChannel.cpp
testing/xpcshell/runxpcshelltests.py
toolkit/xre/nsAppRunner.cpp
widget/gtk/moz.build
widget/gtk/nsWidgetFactory.cpp
widget/headless/HeadlessLookAndFeel.cpp
widget/headless/HeadlessLookAndFeel.h
widget/headless/HeadlessWidget.cpp
widget/headless/HeadlessWidget.h
widget/headless/moz.build
widget/headless/tests/headless.html
widget/headless/tests/moz.build
widget/headless/tests/test_headless.js
widget/headless/tests/xpcshell.ini
widget/moz.build
widget/nsIWidget.h
widget/nsXPLookAndFeel.cpp
widget/nsXPLookAndFeel.h
xpfe/appshell/nsAppShellService.cpp
xpfe/appshell/nsWebShellWindow.cpp
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -310,17 +310,19 @@ nsDOMWindowUtils::Redraw(uint32_t aCount
 
     if (rootFrame) {
       PRIntervalTime iStart = PR_IntervalNow();
 
       for (uint32_t i = 0; i < aCount; i++)
         rootFrame->InvalidateFrame();
 
 #if defined(MOZ_X11) && defined(MOZ_WIDGET_GTK)
-      XSync(GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), False);
+      if (!gfxPlatform::IsHeadless()) {
+        XSync(GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), False);
+      }
 #endif
 
       *aDurationOut = PR_IntervalToMilliseconds(PR_IntervalNow() - iStart);
 
       return NS_OK;
     }
   }
   return NS_ERROR_FAILURE;
--- a/gfx/thebes/gfxFcPlatformFontList.cpp
+++ b/gfx/thebes/gfxFcPlatformFontList.cpp
@@ -770,17 +770,17 @@ PreparePattern(FcPattern* aPattern, bool
     // pick up dynamic changes.
     if(aIsPrinterFont) {
        cairo_font_options_t *options = cairo_font_options_create();
        cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_NONE);
        cairo_font_options_set_antialias (options, CAIRO_ANTIALIAS_GRAY);
        cairo_ft_font_options_substitute(options, aPattern);
        cairo_font_options_destroy(options);
        FcPatternAddBool(aPattern, PRINTING_FC_PROPERTY, FcTrue);
-    } else {
+    } else if (!gfxPlatform::IsHeadless()) {
 #ifdef MOZ_WIDGET_GTK
         ApplyGdkScreenFontOptions(aPattern);
 
 #ifdef MOZ_X11
         FcValue value;
         int lcdfilter;
         if (FcPatternGet(aPattern, FC_LCD_FILTER, 0, &value) == FcResultNoMatch) {
             GdkDisplay* dpy = gdk_display_get_default();
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -831,16 +831,22 @@ gfxPlatform::InitMoz2DLogging()
     mozilla::gfx::Config cfg;
     cfg.mLogForwarder = fwd;
     cfg.mMaxTextureSize = gfxPrefs::MaxTextureSize();
     cfg.mMaxAllocSize = gfxPrefs::MaxAllocSize();
 
     gfx::Factory::Init(cfg);
 }
 
+/* static */ bool
+gfxPlatform::IsHeadless()
+{
+    return PR_GetEnv("MOZ_HEADLESS");
+}
+
 static bool sLayersIPCIsUp = false;
 
 /* static */ void
 gfxPlatform::InitNullMetadata()
 {
   ScrollMetadata::sNullMetadata = new ScrollMetadata();
   ClearOnShutdown(&ScrollMetadata::sNullMetadata);
 }
--- a/gfx/thebes/gfxPlatform.h
+++ b/gfx/thebes/gfxPlatform.h
@@ -180,16 +180,18 @@ public:
 
     /**
      * Initialize ScrollMetadata statics. Does not depend on gfxPlatform.
      */
     static void InitNullMetadata();
 
     static void InitMoz2DLogging();
 
+    static bool IsHeadless();
+
     /**
      * Create an offscreen surface of the given dimensions
      * and image format.
      */
     virtual already_AddRefed<gfxASurface>
       CreateOffscreenSurface(const IntSize& aSize,
                              gfxImageFormat aFormat) = 0;
 
--- a/gfx/thebes/gfxPlatformGtk.cpp
+++ b/gfx/thebes/gfxPlatformGtk.cpp
@@ -68,22 +68,24 @@ using namespace mozilla::gfx;
 using namespace mozilla::unicode;
 
 #if (MOZ_WIDGET_GTK == 2)
 static cairo_user_data_key_t cairo_gdk_drawable_key;
 #endif
 
 gfxPlatformGtk::gfxPlatformGtk()
 {
-    gtk_init(nullptr, nullptr);
+    if (!gfxPlatform::IsHeadless()) {
+        gtk_init(nullptr, nullptr);
+    }
 
     mMaxGenericSubstitutions = UNINITIALIZED_VALUE;
 
 #ifdef MOZ_X11
-    if (XRE_IsParentProcess()) {
+    if (!gfxPlatform::IsHeadless() && XRE_IsParentProcess()) {
       if (GDK_IS_X11_DISPLAY(gdk_display_get_default()) &&
           mozilla::Preferences::GetBool("gfx.xrender.enabled"))
       {
           gfxVars::SetUseXRender(true);
       }
     }
 #endif
 
@@ -92,17 +94,17 @@ gfxPlatformGtk::gfxPlatformGtk()
 #ifdef USE_SKIA
     canvasMask |= BackendTypeBit(BackendType::SKIA);
     contentMask |= BackendTypeBit(BackendType::SKIA);
 #endif
     InitBackendPrefs(canvasMask, BackendType::CAIRO,
                      contentMask, BackendType::CAIRO);
 
 #ifdef MOZ_X11
-    if (GDK_IS_X11_DISPLAY(gdk_display_get_default())) {
+    if (gfxPlatform::IsHeadless() && GDK_IS_X11_DISPLAY(gdk_display_get_default())) {
       mCompositorDisplay = XOpenDisplay(nullptr);
       MOZ_ASSERT(mCompositorDisplay, "Failed to create compositor display!");
     } else {
       mCompositorDisplay = nullptr;
     }
 #endif // MOZ_X11
 }
 
--- a/image/decoders/icon/gtk/nsIconChannel.cpp
+++ b/image/decoders/icon/gtk/nsIconChannel.cpp
@@ -25,16 +25,17 @@
 
 #include "nsNetUtil.h"
 #include "nsComponentManagerUtils.h"
 #include "nsIStringStream.h"
 #include "nsServiceManagerUtils.h"
 #include "NullPrincipal.h"
 #include "nsIURL.h"
 #include "prlink.h"
+#include "gfxPlatform.h"
 
 NS_IMPL_ISUPPORTS(nsIconChannel,
                   nsIRequest,
                   nsIChannel)
 
 static nsresult
 moz_gdk_pixbuf_to_channel(GdkPixbuf* aPixbuf, nsIURI* aURI,
                           nsIChannel** aChannel)
@@ -303,16 +304,20 @@ nsIconChannel::InitWithGIO(nsIMozIconURI
 #endif // MOZ_ENABLE_GIO
 
 nsresult
 nsIconChannel::Init(nsIURI* aURI)
 {
   nsCOMPtr<nsIMozIconURI> iconURI = do_QueryInterface(aURI);
   NS_ASSERTION(iconURI, "URI is not an nsIMozIconURI");
 
+  if (gfxPlatform::IsHeadless()) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
   nsAutoCString stockIcon;
   iconURI->GetStockIcon(stockIcon);
   if (stockIcon.IsEmpty()) {
 #ifdef MOZ_ENABLE_GIO
     return InitWithGIO(iconURI);
 #else
     return NS_ERROR_NOT_AVAILABLE;
 #endif
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -655,16 +655,20 @@ class XPCShellTestThread(Thread):
             self.env['PYTHON'] = sys.executable
             self.env['BREAKPAD_SYMBOLS_PATH'] = self.symbolsPath
             self.env['DMD_PRELOAD_VAR'] = preloadEnvVar
             self.env['DMD_PRELOAD_VALUE'] = libdmd
 
         if self.test_object.get('subprocess') == 'true':
             self.env['PYTHON'] = sys.executable
 
+        if self.test_object.get('headless', False):
+            self.env["MOZ_HEADLESS"] = '1'
+            self.env["DISPLAY"] = '77' # Set a fake display.
+
         testTimeoutInterval = self.harness_timeout
         # Allow a test to request a multiple of the timeout if it is expected to take long
         if 'requesttimeoutfactor' in self.test_object:
             testTimeoutInterval *= int(self.test_object['requesttimeoutfactor'])
 
         testTimer = None
         if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo:
             testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc))
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -1939,16 +1939,22 @@ ProfileLockedDialog(nsIFile* aProfileDir
 
     nsXPIDLString killTitle;
     sb->FormatStringFromName(u"restartTitle",
                              params, 1, getter_Copies(killTitle));
 
     if (!killMessage || !killTitle)
       return NS_ERROR_FAILURE;
 
+    if (gfxPlatform::IsHeadless()) {
+      // TODO: make a way to turn off all dialogs when headless.
+      Output(true, "%s\n", NS_LossyConvertUTF16toASCII(killMessage).get());
+      return NS_ERROR_FAILURE;
+    }
+
     nsCOMPtr<nsIPromptService> ps
       (do_GetService(NS_PROMPTSERVICE_CONTRACTID));
     NS_ENSURE_TRUE(ps, NS_ERROR_FAILURE);
 
     if (aUnlocker) {
       int32_t button;
 #ifdef MOZ_WIDGET_ANDROID
       java::GeckoAppShell::KillAnyZombies();
@@ -3115,16 +3121,20 @@ XREMain::XRE_mainInit(bool* aExitFlag)
     }
     ChaosMode::SetChaosFeature(feature);
   }
 
   if (ChaosMode::isActive(ChaosFeature::Any)) {
     printf_stderr("*** You are running in chaos test mode. See ChaosMode.h. ***\n");
   }
 
+  if (gfxPlatform::IsHeadless()) {
+    Output(false, "*** You are running in headless mode.\n");
+  }
+
   nsresult rv;
   ArgResult ar;
 
 #ifdef DEBUG
   if (PR_GetEnv("XRE_MAIN_BREAK"))
     NS_BREAK();
 #endif
 
@@ -3784,41 +3794,48 @@ XREMain::XRE_mainStartup(bool* aExitFlag
       printf("TEST-UNEXPECTED-FAIL | gtest | Not compiled with enable-tests\n");
     }
     *aExitFlag = true;
     return result;
   }
 
 #if defined(MOZ_WIDGET_GTK)
   // display_name is owned by gdk.
-  const char *display_name = gdk_get_display_arg_name();
+  const char *display_name = nullptr;
   bool saveDisplayArg = false;
-  if (display_name) {
-    saveDisplayArg = true;
-  } else {
-    display_name = DetectDisplay();
-    if (!display_name) {
-      return 1;
+  if (!gfxPlatform::IsHeadless()) {
+    display_name = gdk_get_display_arg_name();
+    if (display_name) {
+      saveDisplayArg = true;
+    } else {
+      display_name = DetectDisplay();
+      if (!display_name) {
+        return 1;
+      }
     }
   }
 #endif /* MOZ_WIDGET_GTK */
 #ifdef MOZ_X11
   // Init X11 in thread-safe mode. Must be called prior to the first call to XOpenDisplay
   // (called inside gdk_display_open). This is a requirement for off main tread compositing.
-  XInitThreads();
+  if (!gfxPlatform::IsHeadless()) {
+    XInitThreads();
+  }
 #endif
 #if defined(MOZ_WIDGET_GTK)
-  mGdkDisplay = gdk_display_open(display_name);
-  if (!mGdkDisplay) {
-    PR_fprintf(PR_STDERR, "Error: cannot open display: %s\n", display_name);
-    return 1;
-  }
-  gdk_display_manager_set_default_display (gdk_display_manager_get(),
-                                           mGdkDisplay);
-  if (GDK_IS_X11_DISPLAY(mGdkDisplay)) {
+  if (!gfxPlatform::IsHeadless()) {
+    mGdkDisplay = gdk_display_open(display_name);
+    if (!mGdkDisplay) {
+      PR_fprintf(PR_STDERR, "Error: cannot open display: %s\n", display_name);
+      return 1;
+    }
+    gdk_display_manager_set_default_display (gdk_display_manager_get(),
+                                             mGdkDisplay);
+  }
+  if (!gfxPlatform::IsHeadless() && GDK_IS_X11_DISPLAY(mGdkDisplay)) {
     if (saveDisplayArg) {
       SaveWordToEnv("DISPLAY", nsDependentCString(display_name));
     }
   } else {
     mDisableRemote = true;
   }
 #endif
 #ifdef MOZ_ENABLE_XREMOTE
@@ -4760,17 +4777,19 @@ XREMain::XRE_main(int argc, char* argv[]
       CrashReporter::UnsetExceptionHandler();
 #endif
     return rv == NS_ERROR_LAUNCHED_CHILD_PROCESS ? 0 : 1;
   }
 
 #ifdef MOZ_WIDGET_GTK
   // gdk_display_close also calls gdk_display_manager_set_default_display
   // appropriately when necessary.
-  MOZ_gdk_display_close(mGdkDisplay);
+  if (!gfxPlatform::IsHeadless()) {
+    MOZ_gdk_display_close(mGdkDisplay);
+  }
 #endif
 
 #ifdef MOZ_CRASHREPORTER
   if (mAppData->flags & NS_XRE_ENABLE_CRASH_REPORTER)
       CrashReporter::UnsetExceptionHandler();
 #endif
 
   XRE_DeinitCommandLine();
--- a/widget/gtk/moz.build
+++ b/widget/gtk/moz.build
@@ -101,16 +101,17 @@ include('/ipc/chromium/chromium-config.m
 
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
     '/layout/generic',
     '/layout/xul',
     '/other-licenses/atk-1.0',
     '/widget',
+    '/widget/headless',
 ]
 
 if CONFIG['MOZ_X11']:
     LOCAL_INCLUDES += [
         '/widget/x11',
     ]
 
 DEFINES['CAIRO_GFX'] = True
--- a/widget/gtk/nsWidgetFactory.cpp
+++ b/widget/gtk/nsWidgetFactory.cpp
@@ -83,16 +83,19 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsImageTo
 
 // from nsWindow.cpp
 extern bool gDisableNativeTheme;
 
 static nsresult
 nsNativeThemeGTKConstructor(nsISupports *aOuter, REFNSIID aIID,
                             void **aResult)
 {
+    if (gfxPlatform::IsHeadless()) {
+        return NS_ERROR_NO_INTERFACE;
+    }
     nsresult rv;
     nsNativeThemeGTK * inst;
 
     if (gDisableNativeTheme)
         return NS_ERROR_NO_INTERFACE;
 
     *aResult = nullptr;
     if (nullptr != aOuter) {
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeel.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HeadlessLookAndFeel.h"
+
+using mozilla::LookAndFeel;
+
+namespace mozilla {
+namespace widget {
+
+static const char16_t UNICODE_BULLET = 0x2022;
+
+HeadlessLookAndFeel::HeadlessLookAndFeel()
+{
+}
+
+HeadlessLookAndFeel::~HeadlessLookAndFeel()
+{
+}
+
+nsresult
+HeadlessLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor)
+{
+  // Default all colors to black.
+  aColor = NS_RGB(0x00, 0x00, 0x00);
+  return NS_OK;
+}
+
+nsresult
+HeadlessLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult)
+{
+  nsresult res = nsXPLookAndFeel::GetIntImpl(aID, aResult);
+  if (NS_SUCCEEDED(res)) {
+    return res;
+  }
+  aResult = 0;
+  return NS_ERROR_FAILURE;
+}
+
+nsresult
+HeadlessLookAndFeel::GetFloatImpl(FloatID aID, float &aResult)
+{
+  nsresult res = NS_OK;
+  res = nsXPLookAndFeel::GetFloatImpl(aID, aResult);
+  if (NS_SUCCEEDED(res)) {
+    return res;
+  }
+  aResult = -1.0;
+  return NS_ERROR_FAILURE;
+}
+
+bool
+HeadlessLookAndFeel::GetFontImpl(FontID aID, nsString& aFontName,
+               gfxFontStyle& aFontStyle,
+               float aDevPixPerCSSPixel)
+{
+  return true;
+}
+
+char16_t
+HeadlessLookAndFeel::GetPasswordCharacterImpl()
+{
+  return UNICODE_BULLET;
+}
+
+void
+HeadlessLookAndFeel::RefreshImpl()
+{
+  nsXPLookAndFeel::RefreshImpl();
+}
+
+bool
+HeadlessLookAndFeel::GetEchoPasswordImpl() {
+  return false;
+}
+
+} // namespace widget
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeel.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_widget_HeadlessLookAndFeel_h
+#define mozilla_widget_HeadlessLookAndFeel_h
+
+#include "nsXPLookAndFeel.h"
+#include "nsLookAndFeel.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessLookAndFeel: public nsXPLookAndFeel {
+public:
+  HeadlessLookAndFeel();
+  virtual ~HeadlessLookAndFeel();
+
+  virtual nsresult NativeGetColor(ColorID aID, nscolor &aResult);
+  virtual nsresult GetIntImpl(IntID aID, int32_t &aResult);
+  virtual nsresult GetFloatImpl(FloatID aID, float &aResult);
+  virtual bool GetFontImpl(FontID aID,
+                           nsString& aFontName,
+                           gfxFontStyle& aFontStyle,
+                           float aDevPixPerCSSPixel);
+
+  virtual void RefreshImpl();
+  virtual char16_t GetPasswordCharacterImpl();
+  virtual bool GetEchoPasswordImpl();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessWidget.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "HeadlessWidget.h"
+#include "Layers.h"
+#include "BasicLayers.h"
+#include "BasicEvents.h"
+
+using namespace mozilla::layers;
+
+/*static*/ already_AddRefed<nsIWidget>
+nsIWidget::CreateHeadlessWidget()
+{
+  nsCOMPtr<nsIWidget> widget = new mozilla::widget::HeadlessWidget();
+  return widget.forget();
+}
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS_INHERITED0(HeadlessWidget, nsBaseWidget)
+
+nsresult
+HeadlessWidget::Create(nsIWidget* aParent,
+                       nsNativeWidget aNativeParent,
+                       const LayoutDeviceIntRect& aRect,
+                       nsWidgetInitData* aInitData)
+{
+  MOZ_ASSERT(!aNativeParent, "No native parents for headless widgets.");
+
+  BaseCreate(nullptr, aInitData);
+  mBounds = aRect;
+  mVisible = true;
+  mEnabled = true;
+  return NS_OK;
+}
+
+already_AddRefed<nsIWidget>
+HeadlessWidget::CreateChild(const LayoutDeviceIntRect& aRect,
+                            nsWidgetInitData* aInitData,
+                            bool aForceUseIWidgetParent)
+{
+  nsCOMPtr<nsIWidget> widget = nsIWidget::CreateHeadlessWidget();
+  if (!widget) {
+    return nullptr;
+  }
+  if (NS_FAILED(widget->Create(nullptr, nullptr, aRect, aInitData))) {
+    return nullptr;
+  }
+  return widget.forget();
+}
+
+void
+HeadlessWidget::Show(bool aState)
+{
+  mVisible = aState;
+}
+
+bool
+HeadlessWidget::IsVisible() const
+{
+  return mVisible;
+}
+
+void
+HeadlessWidget::Enable(bool aState)
+{
+  mEnabled = aState;
+}
+
+bool
+HeadlessWidget::IsEnabled() const
+{
+  return mEnabled;
+}
+
+LayerManager*
+HeadlessWidget::GetLayerManager(PLayerTransactionChild* aShadowManager,
+                                LayersBackend aBackendHint,
+                                LayerManagerPersistence aPersistence)
+{
+  if (!mLayerManager) {
+    mLayerManager = new BasicLayerManager(BasicLayerManager::BLM_OFFSCREEN);
+  }
+
+  return mLayerManager;
+}
+
+void
+HeadlessWidget::Resize(double aWidth,
+                       double aHeight,
+                       bool   aRepaint)
+{
+  mBounds.SizeTo(LayoutDeviceIntSize(NSToIntRound(aWidth),
+                                     NSToIntRound(aHeight)));
+  if (mWidgetListener) {
+    mWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
+  }
+  if (mAttachedWidgetListener) {
+    mAttachedWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
+  }
+}
+
+void
+HeadlessWidget::Resize(double aX,
+                       double aY,
+                       double aWidth,
+                       double aHeight,
+                       bool   aRepaint)
+{
+  if (mBounds.x != aX || mBounds.y != aY) {
+    NotifyWindowMoved(aX, aY);
+  }
+  return Resize(aWidth, aHeight, aRepaint);
+}
+
+nsresult
+HeadlessWidget::DispatchEvent(WidgetGUIEvent* aEvent, nsEventStatus& aStatus)
+{
+#ifdef DEBUG
+  debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "HeadlessWidget", 0);
+#endif
+
+  aStatus = nsEventStatus_eIgnore;
+
+  if (mAttachedWidgetListener) {
+    aStatus = mAttachedWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+  }
+
+  return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessWidget.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#ifndef HEADLESSWIDGET_H
+#define HEADLESSWIDGET_H
+
+#include "mozilla/widget/InProcessCompositorWidget.h"
+#include "nsBaseWidget.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessWidget : public nsBaseWidget
+{
+public:
+  HeadlessWidget() {}
+
+  NS_DECL_ISUPPORTS_INHERITED
+
+  void* GetNativeData(uint32_t aDataType) override
+  {
+    // Headless widgets have no native data.
+    return nullptr;
+  }
+
+  virtual nsresult Create(nsIWidget* aParent,
+                          nsNativeWidget aNativeParent,
+                          const LayoutDeviceIntRect& aRect,
+                          nsWidgetInitData* aInitData = nullptr) override;
+  using nsBaseWidget::Create; // for Create signature not overridden here
+  virtual already_AddRefed<nsIWidget> CreateChild(const LayoutDeviceIntRect& aRect,
+                                                  nsWidgetInitData* aInitData = nullptr,
+                                                  bool aForceUseIWidgetParent = false) override;
+
+  virtual void Show(bool aState) override;
+  virtual bool IsVisible() const override;
+  virtual void Move(double aX, double aY) override
+  {
+    MOZ_ASSERT_UNREACHABLE("Headless widgets do not support moving.");
+  }
+  virtual void Resize(double aWidth,
+                      double aHeight,
+                      bool   aRepaint) override;
+  virtual void Resize(double aX,
+                      double aY,
+                      double aWidth,
+                      double aHeight,
+                      bool   aRepaint) override;
+  virtual void Enable(bool aState) override;
+  virtual bool IsEnabled() const override;
+  virtual nsresult SetFocus(bool aRaise) override { return NS_OK; }
+  virtual nsresult ConfigureChildren(const nsTArray<Configuration>& aConfigurations) override
+  {
+    MOZ_ASSERT_UNREACHABLE("Headless widgets do not support configuring children.");
+    return NS_ERROR_FAILURE;
+  }
+  virtual void Invalidate(const LayoutDeviceIntRect& aRect) override
+  {
+    // TODO: see if we need to do anything here.
+  }
+  virtual nsresult SetTitle(const nsAString& title) override {
+    // Headless widgets have no title, so just ignore it.
+    return NS_OK;
+  }
+  virtual LayoutDeviceIntPoint WidgetToScreenOffset() override
+  {
+    // For now headless widgets cannot be moved, so always return 0,0.
+    return LayoutDeviceIntPoint(0, 0);
+  }
+  virtual void SetInputContext(const InputContext& aContext,
+                               const InputContextAction& aAction) override
+  {
+    MOZ_ASSERT_UNREACHABLE("Headless widgets do not support input context.");
+  }
+  virtual InputContext GetInputContext() override
+  {
+    MOZ_ASSERT_UNREACHABLE("Headless widgets do not support input context.");
+    InputContext context;
+    return context;
+  }
+
+  virtual LayerManager*
+  GetLayerManager(PLayerTransactionChild* aShadowManager = nullptr,
+                  LayersBackend aBackendHint = mozilla::layers::LayersBackend::LAYERS_NONE,
+                  LayerManagerPersistence aPersistence = LAYER_MANAGER_CURRENT) override;
+
+  virtual nsresult DispatchEvent(WidgetGUIEvent* aEvent,
+                                 nsEventStatus& aStatus) override;
+
+private:
+  ~HeadlessWidget() {}
+  bool mEnabled;
+  bool mVisible;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/widget/headless/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['tests']
+
+LOCAL_INCLUDES += [
+    '/widget',
+    '/widget/gtk',
+]
+
+UNIFIED_SOURCES += [
+    'HeadlessLookAndFeel.cpp',
+    'HeadlessWidget.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
new file mode 100644
--- /dev/null
+++ b/widget/headless/tests/headless.html
@@ -0,0 +1,6 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 255)">
+Hi
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/widget/headless/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/widget/headless/tests/test_headless.js
@@ -0,0 +1,94 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const server = new HttpServer();
+server.registerDirectory("/", do_get_cwd());
+server.start(-1);
+const ROOT = `http://localhost:${server.identity.primaryPort}`;
+const BASE = `${ROOT}/`;
+const HEADLESS_URL = `${BASE}/headless.html`;
+
+function loadContentWindow(webNavigation, uri) {
+  return new Promise((resolve, reject) => {
+    webNavigation.loadURI(uri, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
+    let docShell = webNavigation.QueryInterface(Ci.nsIInterfaceRequestor)
+                  .getInterface(Ci.nsIDocShell);
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIWebProgress);
+    let progressListener = {
+      onLocationChange: function (progress, request, location, flags) {
+        // Ignore inner-frame events
+        if (progress != webProgress) {
+          return;
+        }
+        // Ignore events that don't change the document
+        if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+          return;
+        }
+        let docShell = webNavigation.QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDocShell);
+        let contentWindow = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIDOMWindow);
+        webProgress.removeProgressListener(progressListener);
+        contentWindow.addEventListener("load", (event) => {
+          resolve(contentWindow);
+        }, { once: true });
+      },
+      QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener",
+                                            "nsISupportsWeakReference"])
+    };
+    webProgress.addProgressListener(progressListener,
+                                    Ci.nsIWebProgress.NOTIFY_LOCATION);
+  });
+}
+
+add_task(function* test_snapshot() {
+  let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
+  let contentWindow = yield loadContentWindow(webNavigation, HEADLESS_URL);
+  const contentWidth = 400;
+  const contentHeight = 300;
+  // Verify dimensions.
+  contentWindow.resizeTo(contentWidth, contentHeight);
+  equal(contentWindow.innerWidth, contentWidth);
+  equal(contentWindow.innerHeight, contentHeight);
+
+  // Snapshot the test page.
+  let canvas = contentWindow.document.createElementNS('http://www.w3.org/1999/xhtml', 'html:canvas');
+  let context = canvas.getContext('2d');
+  let width = contentWindow.innerWidth;
+  let height = contentWindow.innerHeight;
+  canvas.width = width;
+  canvas.height = height;
+  context.drawWindow(
+    contentWindow,
+    0,
+    0,
+    width,
+    height,
+    'rgb(255, 255, 255)'
+  );
+  let imageData = context.getImageData(0, 0, width, height).data;
+  ok(imageData[0] === 0 && imageData[1] === 255 && imageData[2] === 0 && imageData[3] === 255, "Page is green.");
+
+  // Search for a blue pixel (a quick and dirty check to see if the blue text is
+  // on the page)
+  let found = false;
+  for (let i = 0; i < imageData.length; i += 4) {
+    if (imageData[i + 2] === 255) {
+      found = true;
+      break;
+    }
+  }
+  ok(found, "Found blue text on page.");
+
+  webNavigation.close();
+  yield new Promise((resolve) => {
+    server.stop(resolve);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/widget/headless/tests/xpcshell.ini
@@ -0,0 +1,4 @@
+[test_headless.js]
+skip-if = os != "linux"
+headless = true
+support-files = headless.html
--- a/widget/moz.build
+++ b/widget/moz.build
@@ -37,16 +37,18 @@ elif toolkit == 'cocoa':
         'nsITaskbarProgress.idl',
     ]
     EXPORTS += [
         'nsINativeMenuService.h',
     ]
 
 TEST_DIRS += ['tests']
 
+DIRS += ['headless']
+
 # Don't build the DSO under the 'build' directory as windows does.
 #
 # The DSOs get built in the toolkit dir itself.  Do this so that
 # multiple implementations of widget can be built on the same
 # source tree.
 #
 if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
     DIRS += ['gtk']
@@ -253,16 +255,17 @@ LOCAL_INCLUDES += [
     '/dom/ipc',
     '/layout/base',
     '/layout/forms',
     '/layout/generic',
     '/layout/painting',
     '/layout/xul',
     '/view',
     '/widget',
+    '/widget/headless',
 ]
 
 if toolkit == 'windows':
     IPDL_SOURCES = [
         'windows/PCompositorWidget.ipdl',
         'windows/PlatformWidgetTypes.ipdlh',
     ]
 elif 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT'] and CONFIG['MOZ_X11']:
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -1848,16 +1848,19 @@ public:
      * config, on this platform, or for this process type.
      *
      * This function is called "Create" to match CreateInstance().
      * The returned widget must still be nsIWidget::Create()d.
      */
     static already_AddRefed<nsIWidget>
     CreatePuppetWidget(TabChild* aTabChild);
 
+    static already_AddRefed<nsIWidget>
+    CreateHeadlessWidget();
+
     /**
      * Allocate and return a "plugin proxy widget", a subclass of PuppetWidget
      * used in wrapping a PPluginWidget connection for remote widgets. Note
      * this call creates the base object, it does not create the widget. Use
      * nsIWidget's Create to do this.
      */
     static already_AddRefed<nsIWidget>
     CreatePluginProxyWidget(TabChild* aTabChild,
--- a/widget/nsXPLookAndFeel.cpp
+++ b/widget/nsXPLookAndFeel.cpp
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/ArrayUtils.h"
 
 #include "nscore.h"
 
 #include "nsXPLookAndFeel.h"
 #include "nsLookAndFeel.h"
+#include "HeadlessLookAndFeel.h"
 #include "nsCRT.h"
 #include "nsFont.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/widget/WidgetMessageUtils.h"
 
@@ -245,30 +246,34 @@ const char nsXPLookAndFeel::sColorPrefs[
 int32_t nsXPLookAndFeel::sCachedColors[LookAndFeel::eColorID_LAST_COLOR] = {0};
 int32_t nsXPLookAndFeel::sCachedColorBits[COLOR_CACHE_SIZE] = {0};
 
 bool nsXPLookAndFeel::sInitialized = false;
 bool nsXPLookAndFeel::sUseNativeColors = true;
 bool nsXPLookAndFeel::sUseStandinsForNativeColors = false;
 bool nsXPLookAndFeel::sFindbarModalHighlight = false;
 
-nsLookAndFeel* nsXPLookAndFeel::sInstance = nullptr;
+nsXPLookAndFeel* nsXPLookAndFeel::sInstance = nullptr;
 bool nsXPLookAndFeel::sShutdown = false;
 
 // static
-nsLookAndFeel*
+nsXPLookAndFeel*
 nsXPLookAndFeel::GetInstance()
 {
   if (sInstance) {
     return sInstance;
   }
 
   NS_ENSURE_TRUE(!sShutdown, nullptr);
 
-  sInstance = new nsLookAndFeel();
+  if (gfxPlatform::IsHeadless()) {
+    sInstance = new widget::HeadlessLookAndFeel();
+  } else {
+    sInstance = new nsLookAndFeel();
+  }
   return sInstance;
 }
 
 // static
 void
 nsXPLookAndFeel::Shutdown()
 {
   if (sShutdown) {
--- a/widget/nsXPLookAndFeel.h
+++ b/widget/nsXPLookAndFeel.h
@@ -37,17 +37,17 @@ struct nsLookAndFeelFloatPref
 #define CACHE_COLOR(x, y)  nsXPLookAndFeel::sCachedColors[(x)] = y; \
               nsXPLookAndFeel::sCachedColorBits[CACHE_BLOCK(x)] |= CACHE_BIT(x);
 
 class nsXPLookAndFeel: public mozilla::LookAndFeel
 {
 public:
   virtual ~nsXPLookAndFeel();
 
-  static nsLookAndFeel* GetInstance();
+  static nsXPLookAndFeel* GetInstance();
   static void Shutdown();
 
   void Init();
 
   //
   // All these routines will return NS_OK if they have a value,
   // in which case the nsLookAndFeel should use that value;
   // otherwise we'll return NS_ERROR_NOT_AVAILABLE, in which case, the
@@ -108,13 +108,13 @@ protected:
    */
   static const char sColorPrefs[][38];
   static int32_t sCachedColors[LookAndFeel::eColorID_LAST_COLOR];
   static int32_t sCachedColorBits[COLOR_CACHE_SIZE];
   static bool sUseNativeColors;
   static bool sUseStandinsForNativeColors;
   static bool sFindbarModalHighlight;
 
-  static nsLookAndFeel* sInstance;
+  static nsXPLookAndFeel* sInstance;
   static bool sShutdown;
 };
 
 #endif
--- a/xpfe/appshell/nsAppShellService.cpp
+++ b/xpfe/appshell/nsAppShellService.cpp
@@ -220,18 +220,20 @@ nsAppShellService::CreateTopLevelWindow(
  * This class provides a stub implementation of nsIWebBrowserChrome2, as needed
  * by nsAppShellService::CreateWindowlessBrowser
  */
 class WebBrowserChrome2Stub : public nsIWebBrowserChrome2,
                               public nsIEmbeddingSiteWindow,
                               public nsIInterfaceRequestor,
                               public nsSupportsWeakReference {
 protected:
+    nsCOMPtr<nsIWebBrowser> mBrowser;
     virtual ~WebBrowserChrome2Stub() {}
 public:
+    explicit WebBrowserChrome2Stub(nsIWebBrowser *aBrowser) : mBrowser(aBrowser) {}
     NS_DECL_ISUPPORTS
     NS_DECL_NSIWEBBROWSERCHROME
     NS_DECL_NSIWEBBROWSERCHROME2
     NS_DECL_NSIINTERFACEREQUESTOR
     NS_DECL_NSIEMBEDDINGSITEWINDOW
 };
 
 NS_INTERFACE_MAP_BEGIN(WebBrowserChrome2Stub)
@@ -350,17 +352,20 @@ WebBrowserChrome2Stub::GetDimensions(uin
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 WebBrowserChrome2Stub::SetDimensions(uint32_t flags, int32_t x, int32_t y, int32_t cx, int32_t cy)
 {
-  return NS_ERROR_NOT_IMPLEMENTED;
+  nsCOMPtr<nsIBaseWindow> window = do_QueryInterface(mBrowser);
+  NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
+  window->SetSize(cx, cy, true);
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 WebBrowserChrome2Stub::SetFocus()
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
@@ -502,31 +507,31 @@ nsAppShellService::CreateWindowlessBrows
     return NS_ERROR_FAILURE;
   }
 
   /* Next, we set the container window for our instance of nsWebBrowser. Since
    * we don't actually have a window, we instead set the container window to be
    * an instance of WebBrowserChrome2Stub, which provides a stub implementation
    * of nsIWebBrowserChrome2.
    */
-  RefPtr<WebBrowserChrome2Stub> stub = new WebBrowserChrome2Stub();
+  RefPtr<WebBrowserChrome2Stub> stub = new WebBrowserChrome2Stub(browser);
   browser->SetContainerWindow(stub);
 
   nsCOMPtr<nsIWebNavigation> navigation = do_QueryInterface(browser);
 
   nsCOMPtr<nsIDocShellTreeItem> item = do_QueryInterface(navigation);
   item->SetItemType(aIsChrome ? nsIDocShellTreeItem::typeChromeWrapper
                               : nsIDocShellTreeItem::typeContentWrapper);
 
   /* A windowless web browser doesn't have an associated OS level window. To
    * accomplish this, we initialize the window associated with our instance of
    * nsWebBrowser with an instance of PuppetWidget, which provides a stub
    * implementation of nsIWidget.
    */
-  nsCOMPtr<nsIWidget> widget = nsIWidget::CreatePuppetWidget(nullptr);
+  nsCOMPtr<nsIWidget> widget = nsIWidget::CreateHeadlessWidget();
   if (!widget) {
     NS_ERROR("Couldn't create instance of PuppetWidget");
     return NS_ERROR_FAILURE;
   }
   nsresult rv =
     widget->Create(nullptr, 0, LayoutDeviceIntRect(0, 0, 0, 0), nullptr);
   NS_ENSURE_SUCCESS(rv, rv);
   nsCOMPtr<nsIBaseWindow> window = do_QueryInterface(navigation);
--- a/xpfe/appshell/nsWebShellWindow.cpp
+++ b/xpfe/appshell/nsWebShellWindow.cpp
@@ -67,16 +67,18 @@
 #include "nsIDocShellTreeItem.h"
 
 #include "mozilla/Attributes.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/MouseEvents.h"
 
 #include "nsPIWindowRoot.h"
 
+#include "gfxPlatform.h"
+
 #ifdef XP_MACOSX
 #include "nsINativeMenuService.h"
 #define USE_NATIVE_MENUS
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
@@ -143,17 +145,24 @@ nsresult nsWebShellWindow::Initialize(ns
     }
   }
 
   // XXX: need to get the default window size from prefs...
   // Doesn't come from prefs... will come from CSS/XUL/RDF
   DesktopIntRect deskRect(initialX, initialY, aInitialWidth, aInitialHeight);
 
   // Create top level window
-  mWindow = do_CreateInstance(kWindowCID, &rv);
+  if (gfxPlatform::IsHeadless()) {
+    mWindow = nsIWidget::CreateHeadlessWidget();
+    if (mWindow) {
+      rv = NS_OK;
+    }
+  } else {
+    mWindow = do_CreateInstance(kWindowCID, &rv);
+  }
   if (NS_OK != rv) {
     return rv;
   }
 
   /* This next bit is troublesome. We carry two different versions of a pointer
      to our parent window. One is the parent window's widget, which is passed
      to our own widget. The other is a weak reference we keep here to our
      parent WebShellWindow. The former is useful to the widget, and we can't