Bug 1356681 - Expand headless mode support for linux. r=automatedtester,jrmuizel,kanru
authorBrendan Dahl <bdahl@mozilla.com>
Wed, 26 Apr 2017 14:29:32 -0700
changeset 359266 d7121e5ad8065a5ef546b349d90606cbf0e5baab
parent 359265 ea227f8d9ce481676a4f30d1a76bdb1bc7787d2e
child 359267 20fd2a3c8039f37ae7ec284316a08e1d670bbd45
push id43028
push userryanvm@gmail.com
push dateFri, 19 May 2017 16:27:11 +0000
treeherderautoland@a4e84d8efb19 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester, jrmuizel, kanru
bugs1356681
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 1356681 - Expand headless mode support for linux. r=automatedtester,jrmuizel,kanru Full Firefox on Linux can now be run with a --headless flag. This includes seven parts: 1) Running all marionette tests in headless mode. 2) Prevents crashes where Firefox calls into GTK. 3) Adds a headless screen helper which supports changing the headless screen size with the environment variables MOZ_HEADLESS_WIDTH and MOZ_HEADLESS_HEIGHT. 4) Supports simulating moving a headless window. 5) Adds a stubbed out nsSound implementation. 6) Supports simulating size mode changes of headless windows. 7) Adds the --headless flag for Firefox.
dom/ipc/ContentChild.cpp
dom/ipc/TabChild.cpp
gfx/thebes/gfxPlatform.cpp
taskcluster/ci/test/test-platforms.yml
taskcluster/ci/test/test-sets.yml
taskcluster/ci/test/tests.yml
testing/marionette/client/marionette_driver/geckoinstance.py
testing/marionette/harness/marionette_harness/runner/base.py
testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
testing/mozharness/scripts/marionette.py
toolkit/xre/nsAppRunner.cpp
widget/gtk/nsAppShell.cpp
widget/gtk/nsSound.cpp
widget/gtk/nsSound.h
widget/gtk/nsWidgetFactory.cpp
widget/headless/HeadlessScreenHelper.cpp
widget/headless/HeadlessScreenHelper.h
widget/headless/HeadlessSound.cpp
widget/headless/HeadlessSound.h
widget/headless/HeadlessWidget.cpp
widget/headless/HeadlessWidget.h
widget/headless/moz.build
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -546,38 +546,42 @@ ContentChild::Init(MessageLoop* aIOLoop,
                    bool aIsForBrowser)
 {
 #ifdef MOZ_WIDGET_GTK
   // We need to pass a display down to gtk_init because it's not going to
   // use the one from the environment on its own when deciding which backend
   // to use, and when starting under XWayland, it may choose to start with
   // the wayland backend instead of the x11 backend.
   // The DISPLAY environment variable is normally set by the parent process.
-  const char* display_name = DetectDisplay();
-  if (display_name) {
-    int argc = 3;
-    char option_name[] = "--display";
-    char* argv[] = {
-      // argv0 is unused because g_set_prgname() was called in
-      // XRE_InitChildProcess().
-      nullptr,
-      option_name,
-      const_cast<char*>(display_name),
-      nullptr
-    };
-    char** argvp = argv;
-    gtk_init(&argc, &argvp);
-  } else {
-    gtk_init(nullptr, nullptr);
+  if (!gfxPlatform::IsHeadless()) {
+    const char* display_name = DetectDisplay();
+    if (display_name) {
+      int argc = 3;
+      char option_name[] = "--display";
+      char* argv[] = {
+        // argv0 is unused because g_set_prgname() was called in
+        // XRE_InitChildProcess().
+        nullptr,
+        option_name,
+        const_cast<char*>(display_name),
+        nullptr
+      };
+      char** argvp = argv;
+      gtk_init(&argc, &argvp);
+    } else {
+      gtk_init(nullptr, nullptr);
+    }
   }
 #endif
 
 #ifdef MOZ_X11
-  // Do this after initializing GDK, or GDK will install its own handler.
-  XRE_InstallX11ErrorHandler();
+  if (!gfxPlatform::IsHeadless()) {
+    // Do this after initializing GDK, or GDK will install its own handler.
+    XRE_InstallX11ErrorHandler();
+  }
 #endif
 
   NS_ASSERTION(!sSingleton, "only one ContentChild per child");
 
   // Once we start sending IPC messages, we need the thread manager to be
   // initialized so we can deal with the responses. Do that here before we
   // try to construct the crash reporter.
   nsresult rv = nsThreadManager::get().Init();
@@ -598,20 +602,22 @@ ContentChild::Init(MessageLoop* aIOLoop,
 #endif
 
   // This must be sent before any IPDL message, which may hit sentinel
   // errors due to parent and content processes having different
   // versions.
   GetIPCChannel()->SendBuildID();
 
 #ifdef MOZ_X11
-  // Send the parent our X socket to act as a proxy reference for our X
-  // resources.
-  int xSocketFd = ConnectionNumber(DefaultXDisplay());
-  SendBackUpXResources(FileDescriptor(xSocketFd));
+  if (!gfxPlatform::IsHeadless()) {
+    // Send the parent our X socket to act as a proxy reference for our X
+    // resources.
+    int xSocketFd = ConnectionNumber(DefaultXDisplay());
+    SendBackUpXResources(FileDescriptor(xSocketFd));
+  }
 #endif
 
 #ifdef MOZ_CRASHREPORTER
   CrashReporterClient::InitSingleton(this);
 #endif
 
   mID = aChildID;
   mIsForBrowser = aIsForBrowser;
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -2499,17 +2499,18 @@ TabChild::RecvSetDocShellIsActive(const 
   if (mCompositorOptions) {
     // Note that |GetLayerManager()| has side-effects in that it creates a layer
     // manager if one doesn't exist already. Calling it inside a debug-only
     // assertion is generally bad but in this case we call it unconditionally
     // just below so it's ok.
     MOZ_ASSERT(mPuppetWidget);
     MOZ_ASSERT(mPuppetWidget->GetLayerManager());
     MOZ_ASSERT(mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_CLIENT
-            || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR);
+            || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR
+            || (gfxPlatform::IsHeadless() && mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_BASIC));
 
     // We send the current layer observer epoch to the compositor so that
     // TabParent knows whether a layer update notification corresponds to the
     // latest SetDocShellIsActive request that was made.
     mPuppetWidget->GetLayerManager()->SetLayerObserverEpoch(aLayerObserverEpoch);
   }
 
   // docshell is consider prerendered only if not active yet
@@ -3016,17 +3017,18 @@ TabChild::GetFrom(uint64_t aLayersId)
 void
 TabChild::DidComposite(uint64_t aTransactionId,
                        const TimeStamp& aCompositeStart,
                        const TimeStamp& aCompositeEnd)
 {
   MOZ_ASSERT(mPuppetWidget);
   MOZ_ASSERT(mPuppetWidget->GetLayerManager());
   MOZ_ASSERT(mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_CLIENT
-             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR);
+             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR
+             || (gfxPlatform::IsHeadless() && mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_BASIC));
 
   mPuppetWidget->GetLayerManager()->DidComposite(aTransactionId, aCompositeStart, aCompositeEnd);
 }
 
 void
 TabChild::DidRequestComposite(const TimeStamp& aCompositeReqStart,
                               const TimeStamp& aCompositeReqEnd)
 {
@@ -3053,28 +3055,30 @@ TabChild::DidRequestComposite(const Time
 }
 
 void
 TabChild::ClearCachedResources()
 {
   MOZ_ASSERT(mPuppetWidget);
   MOZ_ASSERT(mPuppetWidget->GetLayerManager());
   MOZ_ASSERT(mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_CLIENT
-             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR);
+             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR
+             || (gfxPlatform::IsHeadless() && mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_BASIC));
 
   mPuppetWidget->GetLayerManager()->ClearCachedResources();
 }
 
 void
 TabChild::InvalidateLayers()
 {
   MOZ_ASSERT(mPuppetWidget);
   MOZ_ASSERT(mPuppetWidget->GetLayerManager());
   MOZ_ASSERT(mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_CLIENT
-             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR);
+             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR
+             || (gfxPlatform::IsHeadless() && mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_BASIC));
 
   RefPtr<LayerManager> lm = mPuppetWidget->GetLayerManager();
   FrameLayerBuilder::InvalidateAllLayers(lm);
 }
 
 void
 TabChild::ReinitRendering()
 {
@@ -3153,17 +3157,18 @@ TabChild::ReinitRenderingForDeviceReset(
   ReinitRendering();
 }
 
 void
 TabChild::CompositorUpdated(const TextureFactoryIdentifier& aNewIdentifier,
                             uint64_t aDeviceResetSeqNo)
 {
   MOZ_ASSERT(mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_CLIENT
-             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR);
+             || mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_WR
+             || (gfxPlatform::IsHeadless() && mPuppetWidget->GetLayerManager()->GetBackendType() == LayersBackend::LAYERS_BASIC));
 
   RefPtr<LayerManager> lm = mPuppetWidget->GetLayerManager();
 
   mTextureFactoryIdentifier = aNewIdentifier;
   lm->UpdateTextureFactoryIdentifier(aNewIdentifier, aDeviceResetSeqNo);
   FrameLayerBuilder::InvalidateAllLayers(lm);
 }
 
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -831,17 +831,23 @@ gfxPlatform::InitMoz2DLogging()
     cfg.mMaxAllocSize = gfxPrefs::MaxAllocSize();
 
     gfx::Factory::Init(cfg);
 }
 
 /* static */ bool
 gfxPlatform::IsHeadless()
 {
-    return PR_GetEnv("MOZ_HEADLESS");
+    static bool initialized = false;
+    static bool headless = false;
+    if (!initialized) {
+      initialized = true;
+      headless = PR_GetEnv("MOZ_HEADLESS");
+    }
+    return headless;
 }
 
 static bool sLayersIPCIsUp = false;
 
 /* static */ void
 gfxPlatform::InitNullMetadata()
 {
   ScrollMetadata::sNullMetadata = new ScrollMetadata();
--- a/taskcluster/ci/test/test-platforms.yml
+++ b/taskcluster/ci/test/test-platforms.yml
@@ -30,47 +30,52 @@ linux32-nightly/opt:
         -  linux32-tests
         -  linux32-opt-tests
 
 linux64/debug:
     build-platform: linux64/debug
     test-sets:
         - common-tests
         - web-platform-tests
+        - headless
 linux64/opt:
     build-platform: linux64/opt
     test-sets:
         - common-tests
         - web-platform-tests
         - opt-only-tests
         - desktop-screenshot-capture
         - talos
         - awsy
+        - headless
 linux64-nightly/opt:
     build-platform: linux64-nightly/opt
     test-sets:
         - common-tests
         - web-platform-tests
         - opt-only-tests
         - desktop-screenshot-capture
         - talos
         - awsy
+        - headless
 
 # TODO: use 'pgo' and 'asan' labels here, instead of -pgo/opt
 linux64-pgo/opt:
     build-platform: linux64-pgo/opt
     test-sets:
         - common-tests
         - web-platform-tests
         - talos
+        - headless
 
 linux64-asan/opt:
     build-platform: linux64-asan/opt
     test-sets:
         - common-tests
+        - headless
 
 # Stylo builds only run a subset of tests for the moment. So give them
 # their own test set.
 linux64-stylo/debug:
     build-platform: linux64-stylo/debug
     test-sets:
         - stylo-tests
 linux64-stylo/opt:
--- a/taskcluster/ci/test/test-sets.yml
+++ b/taskcluster/ci/test/test-sets.yml
@@ -64,16 +64,19 @@ talos:
     - talos-other
     - talos-svgr
     - talos-tp5o
     - talos-perf-reftest
 
 awsy:
     - awsy
 
+headless:
+    - marionette-headless
+
 ##
 # Limited test sets for specific platforms
 
 stylo-tests:
     - cppunit
     - crashtest
     - reftest-stylo
     - mochitest-style
--- a/taskcluster/ci/test/tests.yml
+++ b/taskcluster/ci/test/tests.yml
@@ -413,16 +413,54 @@ marionette:
                 config:
                     by-test-platform:
                         windows.*:
                             - marionette/windows_taskcluster_config.py
                         default:
                             - marionette/prod_config.py
                             - remove_executables.py
 
+marionette-headless:
+    description: "Marionette headless unittest run"
+    suite: marionette
+    treeherder-symbol: tc(MnH)
+    max-run-time:
+        by-test-platform:
+            default: 5400
+    instance-size:
+        by-test-platform:
+            default: default
+    docker-image: {"in-tree": "desktop1604-test"}
+    tier:
+        by-test-platform:
+            default: default
+    chunks:
+        by-test-platform:
+            default: 1
+    e10s:
+        by-test-platform:
+            default: both
+    run-on-projects:
+        by-test-platform:
+            default: ['all']
+    mozharness:
+        by-test-platform:
+            default:
+                script: marionette.py
+                no-read-buildbot-config: true
+                config:
+                    by-test-platform:
+                        default:
+                            - marionette/prod_config.py
+                            - remove_executables.py
+                extra-options:
+                    by-test-platform:
+                        default:
+                            - --headless
+
 mochitest:
     description: "Mochitest plain run"
     suite:
         by-test-platform:
             linux64-jsdcov/opt: mochitest/plain-chunked-coverage
             default: mochitest/plain-chunked
     treeherder-symbol: tc-M()
     loopback-video: true
--- a/testing/marionette/client/marionette_driver/geckoinstance.py
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -109,17 +109,17 @@ class GeckoInstance(object):
         "toolkit.telemetry.server": "https://%(server)s/dummy/telemetry/",
 
         # Enabling the support for File object creation in the content process.
         "dom.file.createInChild": True,
     }
 
     def __init__(self, host=None, port=None, bin=None, profile=None, addons=None,
                  app_args=None, symbols_path=None, gecko_log=None, prefs=None,
-                 workspace=None, verbose=0):
+                 workspace=None, verbose=0, headless=False):
         self.runner_class = Runner
         self.app_args = app_args or []
         self.runner = None
         self.symbols_path = symbols_path
         self.binary = bin
 
         self.marionette_host = host
         self.marionette_port = port
@@ -135,16 +135,17 @@ class GeckoInstance(object):
         self.prefs = prefs
         self.required_prefs = deepcopy(self.required_prefs)
         if prefs:
             self.required_prefs.update(prefs)
 
         self._gecko_log_option = gecko_log
         self._gecko_log = None
         self.verbose = verbose
+        self.headless = headless
 
     @property
     def gecko_log(self):
         if self._gecko_log:
             return self._gecko_log
 
         path = self._gecko_log_option
         if path != "-":
@@ -225,16 +226,20 @@ class GeckoInstance(object):
 
         if self.gecko_log == "-":
             process_args["stream"] = sys.stdout
         else:
             process_args["logfile"] = self.gecko_log
 
         env = os.environ.copy()
 
+        if self.headless:
+            env["MOZ_HEADLESS"] = "1"
+            env["DISPLAY"] = "77"  # Set a fake display.
+
         # environment variables needed for crashreporting
         # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
         env.update({"MOZ_CRASHREPORTER": "1",
                     "MOZ_CRASHREPORTER_NO_REPORT": "1",
                     "MOZ_CRASHREPORTER_SHUTDOWN": "1",
                     })
 
         return {
--- a/testing/marionette/harness/marionette_harness/runner/base.py
+++ b/testing/marionette/harness/marionette_harness/runner/base.py
@@ -354,16 +354,21 @@ class BaseMarionetteArguments(ArgumentPa
                           default=self.socket_timeout_default,
                           help='Set the global timeout for marionette socket operations.'
                                ' Default: %(default)ss.')
         self.add_argument('--disable-e10s',
                           action='store_false',
                           dest='e10s',
                           default=True,
                           help='Disable e10s when running marionette tests.')
+        self.add_argument('--headless',
+                          action='store_true',
+                          dest='headless',
+                          default=False,
+                          help='Enable headless mode when running marionette tests.')
         self.add_argument('--tag',
                           action='append', dest='test_tags',
                           default=None,
                           help="Filter out tests that don't have the given tag. Can be "
                                "used multiple times in which case the test must contain "
                                "at least one of the given tags.")
         self.add_argument('--workspace',
                           action='store',
@@ -502,17 +507,17 @@ class BaseMarionetteTestRunner(object):
                  repeat=0, testvars=None,
                  symbols_path=None,
                  shuffle=False, shuffle_seed=random.randint(0, sys.maxint), this_chunk=1,
                  total_chunks=1,
                  server_root=None, gecko_log=None, result_callbacks=None,
                  prefs=None, test_tags=None,
                  socket_timeout=BaseMarionetteArguments.socket_timeout_default,
                  startup_timeout=None, addons=None, workspace=None,
-                 verbose=0, e10s=True, emulator=False, **kwargs):
+                 verbose=0, e10s=True, emulator=False, headless=False, **kwargs):
         self._appinfo = None
         self._appName = None
         self._capabilities = None
         self._filename_pattern = None
         self._version_info = {}
 
         self.fixture_servers = {}
         self.fixtures = Fixtures()
@@ -543,16 +548,17 @@ class BaseMarionetteTestRunner(object):
         self.prefs = prefs or {}
         self.test_tags = test_tags
         self.startup_timeout = startup_timeout
         self.workspace = workspace
         # If no workspace is set, default location for gecko.log is .
         # and default location for profile is TMP
         self.workspace_path = workspace or os.getcwd()
         self.verbose = verbose
+        self.headless = headless
         self.e10s = e10s
         if self.e10s:
             self.prefs.update({
                 'browser.tabs.remote.autostart': True,
                 'browser.tabs.remote.force-enable': True,
                 'extensions.e10sBlocksEnabling': False
             })
 
@@ -765,16 +771,19 @@ class BaseMarionetteTestRunner(object):
                     connection.connect((host, int(port)))
                     connection.close()
                 except Exception as e:
                     exc, val, tb = sys.exc_info()
                     msg = "Connection attempt to {0}:{1} failed with error: {2}"
                     raise exc, msg.format(host, port, e), tb
         if self.workspace:
             kwargs['workspace'] = self.workspace_path
+        if self.headless:
+            kwargs['headless'] = True
+
         return kwargs
 
     def record_crash(self):
         crash = True
         try:
             crash = self.marionette.check_for_crash()
             self.crashed += int(crash)
         except Exception:
@@ -954,16 +963,17 @@ class BaseMarionetteTestRunner(object):
             filters = []
             if self.test_tags:
                 filters.append(tags(self.test_tags))
 
             values = {
                 "appname": self.appName,
                 "e10s": self.e10s,
                 "manage_instance": self.marionette.instance is not None,
+                "headless": self.headless
             }
             values.update(mozinfo.info)
 
             manifest_tests = manifest.active_tests(exists=False,
                                                    disabled=True,
                                                    filters=filters,
                                                    **values)
             if len(manifest_tests) == 0:
--- a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
@@ -75,16 +75,17 @@ skip-if = appname == 'fennec'
 [test_window_rect.py]
 skip-if = appname == 'fennec'
 [test_window_maximize.py]
 skip-if = appname == 'fennec'
 [test_window_status_content.py]
 [test_window_status_chrome.py]
 
 [test_screenshot.py]
+skip-if = headless # Relies on native styling which headless doesn't support.
 [test_cookies.py]
 [test_title.py]
 [test_title_chrome.py]
 skip-if = appname == 'fennec'
 [test_window_type_chrome.py]
 skip-if = appname == 'fennec'
 [test_implicit_waits.py]
 [test_wait.py]
--- a/testing/mozharness/scripts/marionette.py
+++ b/testing/mozharness/scripts/marionette.py
@@ -86,16 +86,23 @@ class MarionetteTest(TestingMixin, Mercu
      ], [
         ["--e10s"],
         {"action": "store_true",
          "dest": "e10s",
          "default": False,
          "help": "Run tests with multiple processes. (Desktop builds only)",
          }
     ], [
+        ["--headless"],
+        {"action": "store_true",
+         "dest": "headless",
+         "default": False,
+         "help": "Run tests in headless mode.",
+        }
+    ], [
        ["--allow-software-gl-layers"],
        {"action": "store_true",
         "dest": "allow_software_gl_layers",
         "default": False,
         "help": "Permits a software GL implementation (such as LLVMPipe) to use the GL compositor."
         }
     ], [
        ["--enable-webrender"],
@@ -280,16 +287,19 @@ class MarionetteTest(TestingMixin, Mercu
                                 self.config['test_manifest'])
 
         if self.config.get('app_arg'):
             config_fmt_args['app_arg'] = self.config['app_arg']
 
         if not self.config['e10s']:
             cmd.append('--disable-e10s')
 
+        if self.config['headless']:
+            cmd.append('--headless')
+
         cmd.append('--gecko-log=%s' % os.path.join(dirs["abs_blob_upload_dir"],
                                                    'gecko.log'))
 
         if self.config.get("structured_output"):
             cmd.append("--log-raw=-")
 
         options_group = self._get_options_group(self.config.get('emulator'))
 
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -1655,16 +1655,20 @@ DumpHelp()
          "  --new-instance     Open new instance, not a new window in running instance.\n"
          "  --UILocale <locale> Start with <locale> resources as UI Locale.\n"
          "  --safe-mode        Disables extensions and themes for this session.\n", (const char*) gAppData->name);
 
 #if defined(XP_WIN)
   printf("  --console          Start %s with a debugging console.\n", (const char*) gAppData->name);
 #endif
 
+#ifdef MOZ_WIDGET_GTK
+  printf("  --headless         Run without a GUI.\n");
+#endif
+
   // this works, but only after the components have registered.  so if you drop in a new command line handler, --help
   // won't not until the second run.
   // out of the bug, because we ship a component.reg file, it works correctly.
   DumpArbitraryHelp();
 }
 
 #if defined(DEBUG) && defined(XP_WIN)
 #ifdef DEBUG_warren
@@ -3134,16 +3138,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 (CheckArg("headless")) {
+    PR_SetEnv("MOZ_HEADLESS=1");
+  }
+
   if (gfxPlatform::IsHeadless()) {
 #ifdef MOZ_WIDGET_GTK
     Output(false, "*** You are running in headless mode.\n");
 #else
     Output(true, "Error: headless mode is not currently supported on this platform.\n");
     return 1;
 #endif
   }
@@ -4773,17 +4781,19 @@ XREMain::XRE_main(int argc, char* argv[]
     if (rv != NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE) {
       // Ensure that these environment variables are set:
       SaveFileToEnvIfUnset("XRE_PROFILE_PATH", mProfD);
       SaveFileToEnvIfUnset("XRE_PROFILE_LOCAL_PATH", mProfLD);
       SaveWordToEnvIfUnset("XRE_PROFILE_NAME", mProfileName);
     }
 
 #ifdef MOZ_WIDGET_GTK
-    MOZ_gdk_display_close(mGdkDisplay);
+    if (!gfxPlatform::IsHeadless()) {
+      MOZ_gdk_display_close(mGdkDisplay);
+    }
 #endif
 
     {
       rv = LaunchChild(mNativeApp, true);
     }
 
 #ifdef MOZ_CRASHREPORTER
     if (mAppData->flags & NS_XRE_ENABLE_CRASH_REPORTER)
--- a/widget/gtk/nsAppShell.cpp
+++ b/widget/gtk/nsAppShell.cpp
@@ -16,21 +16,24 @@
 #include "prenv.h"
 #include "mozilla/HangMonitor.h"
 #include "mozilla/Unused.h"
 #include "GeckoProfiler.h"
 #include "nsIPowerManagerService.h"
 #ifdef MOZ_ENABLE_DBUS
 #include "WakeLockListener.h"
 #endif
+#include "gfxPlatform.h"
 #include "ScreenHelperGTK.h"
+#include "HeadlessScreenHelper.h"
 #include "mozilla/widget/ScreenManager.h"
 
 using mozilla::Unused;
 using mozilla::widget::ScreenHelperGTK;
+using mozilla::widget::HeadlessScreenHelper;
 using mozilla::widget::ScreenManager;
 using mozilla::LazyLogModule;
 
 #define NOTIFY_TOKEN 0xFA
 
 LazyLogModule gWidgetLog("Widget");
 LazyLogModule gWidgetFocusLog("WidgetFocus");
 LazyLogModule gWidgetDragLog("WidgetDrag");
@@ -157,17 +160,21 @@ nsAppShell::Init()
 
     if (!sPollFunc) {
         sPollFunc = g_main_context_get_poll_func(nullptr);
         g_main_context_set_poll_func(nullptr, &PollWrapper);
     }
 
     if (XRE_IsParentProcess()) {
         ScreenManager& screenManager = ScreenManager::GetSingleton();
-        screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperGTK>());
+        if (gfxPlatform::IsHeadless()) {
+            screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>());
+        } else {
+            screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperGTK>());
+        }
     }
 
 #if MOZ_WIDGET_GTK == 3
     if (!sReal_gtk_window_check_resize &&
         gtk_check_version(3,8,0) != nullptr) { // GTK 3.0 to GTK 3.6.
         // GtkWindow is a static class and so will leak anyway but this ref
         // makes sure it isn't recreated.
         gpointer gtk_plug_class = g_type_class_ref(GTK_TYPE_WINDOW);
--- a/widget/gtk/nsSound.cpp
+++ b/widget/gtk/nsSound.cpp
@@ -8,30 +8,33 @@
 #include <string.h>
 
 #include "nscore.h"
 #include "plstr.h"
 #include "prlink.h"
 
 #include "nsSound.h"
 
+#include "HeadlessSound.h"
 #include "nsIURL.h"
 #include "nsIFileURL.h"
 #include "nsNetUtil.h"
 #include "nsIChannel.h"
 #include "nsCOMPtr.h"
 #include "nsString.h"
 #include "nsDirectoryService.h"
 #include "nsDirectoryServiceDefs.h"
 #include "mozilla/FileUtils.h"
 #include "mozilla/Services.h"
 #include "mozilla/Unused.h"
 #include "nsIStringBundle.h"
 #include "nsIXULAppInfo.h"
 #include "nsContentUtils.h"
+#include "gfxPlatform.h"
+#include "mozilla/ClearOnShutdown.h"
 
 #include <stdio.h>
 #include <unistd.h>
 
 #include <gtk/gtk.h>
 static PRLibrary *libcanberra = nullptr;
 
 /* used to play sounds with libcanberra. */
@@ -216,16 +219,39 @@ nsSound::Init()
 nsSound::Shutdown()
 {
     if (libcanberra) {
         PR_UnloadLibrary(libcanberra);
         libcanberra = nullptr;
     }
 }
 
+namespace mozilla {
+namespace sound {
+StaticRefPtr<nsISound> sInstance;
+}
+}
+/* static */ already_AddRefed<nsISound>
+nsSound::GetInstance()
+{
+    using namespace mozilla::sound;
+
+    if (!sInstance) {
+        if (gfxPlatform::IsHeadless()) {
+            sInstance = new widget::HeadlessSound();
+        } else {
+            sInstance = new nsSound();
+        }
+        ClearOnShutdown(&sInstance);
+    }
+
+    RefPtr<nsISound> service = sInstance.get();
+    return service.forget();
+}
+
 NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader *aLoader,
                                         nsISupports *context,
                                         nsresult aStatus,
                                         uint32_t dataLen,
                                         const uint8_t *data)
 {
     // print a load error on bad status, and return
     if (NS_FAILED(aStatus)) {
--- a/widget/gtk/nsSound.h
+++ b/widget/gtk/nsSound.h
@@ -15,16 +15,17 @@
 
 class nsSound : public nsISound, 
                 public nsIStreamLoaderObserver
 { 
 public: 
     nsSound(); 
 
     static void Shutdown();
+    static already_AddRefed<nsISound> GetInstance();
 
     NS_DECL_ISUPPORTS
     NS_DECL_NSISOUND
     NS_DECL_NSISTREAMLOADEROBSERVER
 
 private:
     virtual ~nsSound();
 
--- a/widget/gtk/nsWidgetFactory.cpp
+++ b/widget/gtk/nsWidgetFactory.cpp
@@ -71,17 +71,17 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsTransfe
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsBidiKeyboard)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsHTMLFormatConverter)
 #ifdef MOZ_X11
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsIdleServiceGTK, nsIdleServiceGTK::GetInstance)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsClipboardHelper)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsIClipboard, nsClipboard::GetInstance)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsDragService, nsDragService::GetInstance)
 #endif
-NS_GENERIC_FACTORY_CONSTRUCTOR(nsSound)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsISound, nsSound::GetInstance)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(ScreenManager, ScreenManager::GetAddRefedSingleton)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsImageToPixbuf)
 
 
 // from nsWindow.cpp
 extern bool gDisableNativeTheme;
 
 static nsresult
@@ -232,17 +232,17 @@ static const mozilla::Module::CIDEntry k
     { &kNS_WINDOW_CID, false, nullptr, nsWindowConstructor },
     { &kNS_CHILD_CID, false, nullptr, nsChildWindowConstructor },
     { &kNS_APPSHELL_CID, false, nullptr, nsAppShellConstructor, Module::ALLOW_IN_GPU_PROCESS },
     { &kNS_COLORPICKER_CID, false, nullptr, nsColorPickerConstructor, Module::MAIN_PROCESS_ONLY },
     { &kNS_FILEPICKER_CID, false, nullptr, nsFilePickerConstructor, Module::MAIN_PROCESS_ONLY },
 #if (MOZ_WIDGET_GTK == 3)
     { &kNS_APPLICATIONCHOOSER_CID, false, nullptr, nsApplicationChooserConstructor, Module::MAIN_PROCESS_ONLY },
 #endif
-    { &kNS_SOUND_CID, false, nullptr, nsSoundConstructor, Module::MAIN_PROCESS_ONLY },
+    { &kNS_SOUND_CID, false, nullptr, nsISoundConstructor, Module::MAIN_PROCESS_ONLY },
     { &kNS_TRANSFERABLE_CID, false, nullptr, nsTransferableConstructor },
 #ifdef MOZ_X11
     { &kNS_CLIPBOARD_CID, false, nullptr, nsIClipboardConstructor, Module::MAIN_PROCESS_ONLY },
     { &kNS_CLIPBOARDHELPER_CID, false, nullptr, nsClipboardHelperConstructor },
     { &kNS_DRAGSERVICE_CID, false, nullptr, nsDragServiceConstructor, Module::MAIN_PROCESS_ONLY },
 #endif
     { &kNS_HTMLFORMATCONVERTER_CID, false, nullptr, nsHTMLFormatConverterConstructor },
     { &kNS_BIDIKEYBOARD_CID, false, nullptr, nsBidiKeyboardConstructor },
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessScreenHelper.cpp
@@ -0,0 +1,46 @@
+/* -*- 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 "HeadlessScreenHelper.h"
+
+#include "prenv.h"
+#include "mozilla/RefPtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace widget {
+
+/* static */ LayoutDeviceIntRect
+HeadlessScreenHelper::GetScreenRect()
+{
+  char *ev = PR_GetEnv("MOZ_HEADLESS_WIDTH");
+  int width = 1366;
+  if (ev) {
+    width = atoi(ev);
+  }
+  ev = PR_GetEnv("MOZ_HEADLESS_HEIGHT");
+  int height = 768;
+  if (ev) {
+    height = atoi(ev);
+  }
+  return LayoutDeviceIntRect(0, 0, width, height);
+}
+
+HeadlessScreenHelper::HeadlessScreenHelper()
+{
+  AutoTArray<RefPtr<Screen>, 1> screenList;
+  LayoutDeviceIntRect rect = GetScreenRect();
+  RefPtr<Screen> ret = new Screen(rect, rect,
+                                  24, 24,
+                                  DesktopToLayoutDeviceScale(),
+                                  CSSToLayoutDeviceScale());
+  screenList.AppendElement(ret.forget());
+  ScreenManager& screenManager = ScreenManager::GetSingleton();
+  screenManager.Refresh(Move(screenList));
+}
+
+} // namespace widget
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessScreenHelper.h
@@ -0,0 +1,28 @@
+/* -*- 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_HeadlessScreenHelper_h
+#define mozilla_widget_HeadlessScreenHelper_h
+
+#include "mozilla/widget/ScreenManager.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessScreenHelper final : public ScreenManager::Helper
+{
+public:
+  HeadlessScreenHelper();
+  ~HeadlessScreenHelper() override { };
+
+private:
+  static LayoutDeviceIntRect GetScreenRect();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessScreenHelper_h
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessSound.cpp
@@ -0,0 +1,58 @@
+/* -*- 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 "HeadlessSound.h"
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(HeadlessSound, nsISound, nsIStreamLoaderObserver)
+
+HeadlessSound::HeadlessSound()
+{
+}
+
+HeadlessSound::~HeadlessSound()
+{
+}
+
+NS_IMETHODIMP
+HeadlessSound::Init()
+{
+  return NS_OK;
+}
+
+NS_IMETHODIMP HeadlessSound::OnStreamComplete(nsIStreamLoader *aLoader,
+                                              nsISupports *context,
+                                              nsresult aStatus,
+                                              uint32_t dataLen,
+                                              const uint8_t *data)
+{
+  return NS_OK;
+}
+
+NS_IMETHODIMP HeadlessSound::Beep()
+{
+  return NS_OK;
+}
+
+NS_IMETHODIMP HeadlessSound::Play(nsIURL *aURL)
+{
+  return NS_OK;
+}
+
+NS_IMETHODIMP HeadlessSound::PlayEventSound(uint32_t aEventId)
+{
+  return NS_OK;
+}
+
+NS_IMETHODIMP HeadlessSound::PlaySystemSound(const nsAString &aSoundAlias)
+{
+  return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/widget/headless/HeadlessSound.h
@@ -0,0 +1,34 @@
+/* -*- 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_HeadlessSound_h
+#define mozilla_widget_HeadlessSound_h
+
+#include "nsISound.h"
+#include "nsIStreamLoader.h"
+
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessSound : public nsISound,
+                      public nsIStreamLoaderObserver
+{
+public:
+  HeadlessSound();
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISOUND
+  NS_DECL_NSISTREAMLOADEROBSERVER
+
+private:
+  virtual ~HeadlessSound();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessSound_h
--- a/widget/headless/HeadlessWidget.cpp
+++ b/widget/headless/HeadlessWidget.cpp
@@ -26,16 +26,17 @@ HeadlessWidget::Create(nsIWidget* aParen
                        nsNativeWidget aNativeParent,
                        const LayoutDeviceIntRect& aRect,
                        nsWidgetInitData* aInitData)
 {
   MOZ_ASSERT(!aNativeParent, "No native parents for headless widgets.");
 
   BaseCreate(nullptr, aInitData);
   mBounds = aRect;
+  mRestoreBounds = aRect;
   mVisible = true;
   mEnabled = true;
   return NS_OK;
 }
 
 already_AddRefed<nsIWidget>
 HeadlessWidget::CreateChild(const LayoutDeviceIntRect& aRect,
                             nsWidgetInitData* aInitData,
@@ -70,16 +71,47 @@ HeadlessWidget::Enable(bool aState)
 }
 
 bool
 HeadlessWidget::IsEnabled() const
 {
   return mEnabled;
 }
 
+void
+HeadlessWidget::Move(double aX, double aY)
+{
+  double scale = BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+  int32_t x = NSToIntRound(aX * scale);
+  int32_t y = NSToIntRound(aY * scale);
+
+  if (mWindowType == eWindowType_toplevel ||
+      mWindowType == eWindowType_dialog) {
+      SetSizeMode(nsSizeMode_Normal);
+  }
+
+  // Since a popup window's x/y coordinates are in relation to
+  // the parent, the parent might have moved so we always move a
+  // popup window.
+  if (x == mBounds.x && y == mBounds.y &&
+      mWindowType != eWindowType_popup) {
+    return;
+  }
+
+  mBounds.x = x;
+  mBounds.y = y;
+  NotifyRollupGeometryChange();
+}
+
+LayoutDeviceIntPoint
+HeadlessWidget::WidgetToScreenOffset()
+{
+  return LayoutDeviceIntPoint(mBounds.x, mBounds.y);
+}
+
 LayerManager*
 HeadlessWidget::GetLayerManager(PLayerTransactionChild* aShadowManager,
                                 LayersBackend aBackendHint,
                                 LayerManagerPersistence aPersistence)
 {
   if (!mLayerManager) {
     mLayerManager = new BasicLayerManager(BasicLayerManager::BLM_OFFSCREEN);
   }
@@ -110,16 +142,53 @@ HeadlessWidget::Resize(double aX,
                        bool   aRepaint)
 {
   if (mBounds.x != aX || mBounds.y != aY) {
     NotifyWindowMoved(aX, aY);
   }
   return Resize(aWidth, aHeight, aRepaint);
 }
 
+void
+HeadlessWidget::SetSizeMode(nsSizeMode aMode)
+{
+  if (mSizeMode == nsSizeMode_Normal) {
+    // Store the last normal size bounds so it can be restored when entering
+    // normal mode again.
+    mRestoreBounds = mBounds;
+  }
+
+  nsBaseWidget::SetSizeMode(aMode);
+
+  // Normally in real widget backends a window event would be triggered that
+  // would cause the window manager to handle resizing the window. In headless
+  // the window must manually be resized.
+  switch(aMode) {
+  case nsSizeMode_Normal: {
+    Resize(mRestoreBounds.x, mRestoreBounds.y, mRestoreBounds.width, mRestoreBounds.height, false);
+    break;
+  }
+  case nsSizeMode_Minimized:
+    break;
+  case nsSizeMode_Maximized:
+  case nsSizeMode_Fullscreen: {
+    nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+    if (screen) {
+      int32_t left, top, width, height;
+      if (NS_SUCCEEDED(screen->GetRectDisplayPix(&left, &top, &width, &height))) {
+        Resize(0, 0, width, height, true);
+      }
+    }
+    break;
+  }
+  default:
+    break;
+  }
+}
+
 nsresult
 HeadlessWidget::DispatchEvent(WidgetGUIEvent* aEvent, nsEventStatus& aStatus)
 {
 #ifdef DEBUG
   debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "HeadlessWidget", 0);
 #endif
 
   aStatus = nsEventStatus_eIgnore;
--- a/widget/headless/HeadlessWidget.h
+++ b/widget/headless/HeadlessWidget.h
@@ -32,71 +32,67 @@ public:
                           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 Move(double aX, double aY) override;
   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 SetSizeMode(nsSizeMode aMode) 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 LayoutDeviceIntPoint WidgetToScreenOffset() override;
   virtual void SetInputContext(const InputContext& aContext,
                                const InputContextAction& aAction) override
   {
-    MOZ_ASSERT_UNREACHABLE("Headless widgets do not support input context.");
+    mInputContext = aContext;
   }
   virtual InputContext GetInputContext() override
   {
-    MOZ_ASSERT_UNREACHABLE("Headless widgets do not support input context.");
-    InputContext context;
-    return context;
+    return mInputContext;
   }
 
   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;
+  InputContext mInputContext;
+  // In headless there is no window manager to track window bounds
+  // across size mode changes, so we must track it to emulate.
+  LayoutDeviceIntRect mRestoreBounds;
 };
 
 } // namespace widget
 } // namespace mozilla
 
 #endif
--- a/widget/headless/moz.build
+++ b/widget/headless/moz.build
@@ -14,14 +14,16 @@ LOCAL_INCLUDES += [
     '/widget/gtk',
     '/widget/headless',
 ]
 
 UNIFIED_SOURCES += [
     'HeadlessClipboard.cpp',
     'HeadlessClipboardData.cpp',
     'HeadlessLookAndFeel.cpp',
+    'HeadlessScreenHelper.cpp',
+    'HeadlessSound.cpp',
     'HeadlessWidget.cpp',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'