View Javadoc

1   /*
2    * Copyright 2006 ThoughtWorks, Inc.
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   *
16   */
17  package com.atlassian.selenium.browsers.firefox;
18  
19  
20  import org.openqa.selenium.Capabilities;
21  import org.openqa.selenium.Platform;
22  import org.openqa.selenium.browserlaunchers.Sleeper;
23  import org.openqa.selenium.browserlaunchers.LauncherUtils;
24  import org.openqa.selenium.browserlaunchers.Proxies;
25  import org.openqa.selenium.browserlaunchers.locators.BrowserInstallation;
26  import org.openqa.selenium.browserlaunchers.locators.CombinedFirefoxLocator;
27  import org.openqa.selenium.os.CommandLine;
28  import org.openqa.selenium.remote.BrowserType;
29  import org.openqa.selenium.server.ApplicationRegistry;
30  import org.openqa.selenium.server.RemoteControlConfiguration;
31  import org.openqa.selenium.server.browserlaunchers.AbstractBrowserLauncher;
32  import org.openqa.selenium.server.browserlaunchers.BrowserOptions;
33  import org.openqa.selenium.server.browserlaunchers.InvalidBrowserExecutableException;
34  import org.openqa.selenium.server.browserlaunchers.ResourceExtractor;
35  
36  import java.io.File;
37  import java.io.IOException;
38  import java.net.MalformedURLException;
39  import java.net.URL;
40  import java.util.logging.Level;
41  import java.util.logging.Logger;
42  
43  /**
44   * This is an override of the {@link org.openqa.selenium.server.browserlaunchers.FirefoxChromeLauncher}
45   * class from selenium so we can control the display system property
46   * to get xvfb to work.
47   */
48  public class DisplayAwareFirefoxChromeLauncher extends AbstractBrowserLauncher {
49    private static final Logger log = Logger.getLogger(DisplayAwareFirefoxChromeLauncher.class.getName());
50  
51    private File customProfileDir = null;
52    private boolean closed = false;
53    private BrowserInstallation browserInstallation;
54    private CommandLine process = null;
55  
56    private boolean changeMaxConnections = false;
57  
58    public DisplayAwareFirefoxChromeLauncher(Capabilities browserOptions,
59        RemoteControlConfiguration configuration, String sessionId, String browserString)
60        throws InvalidBrowserExecutableException {
61      this(browserOptions, configuration,
62          sessionId, ApplicationRegistry.instance()
63              .browserInstallationCache().locateBrowserInstallation(
64                  BrowserType.CHROME, browserString, new CombinedFirefoxLocator()));
65      if (browserInstallation == null) {
66        throw new InvalidBrowserExecutableException(
67            "The specified path to the browser executable is invalid.");
68      }
69    }
70  
71    public DisplayAwareFirefoxChromeLauncher(Capabilities browserOptions,
72        RemoteControlConfiguration configuration, String sessionId,
73        BrowserInstallation browserInstallation) {
74      super(sessionId, configuration, browserOptions);
75  
76      if (browserInstallation == null) {
77        throw new InvalidBrowserExecutableException(
78            "The specified path to the browser executable is invalid.");
79      }
80      this.browserInstallation = browserInstallation;
81    }
82  
83  
84    /*
85     * (non-Javadoc)
86     *
87     * @see
88     * org.openqa.selenium.server.browserlaunchers.AbstractBrowserLauncher#launch(java.lang.String)
89     */
90  
91    @Override
92    protected void launch(String url) {
93      final String profilePath;
94      final String homePage;
95  
96      try {
97        homePage = new ChromeUrlConvert().convert(url);
98        profilePath = makeCustomProfile(homePage);
99        populateCustomProfileDirectory(profilePath);
100 
101       log.info("Launching Firefox...");
102       process = prepareCommand(
103           browserInstallation.launcherFilePath(),
104           "-profile",
105           profilePath
106           );
107       process.setEnvironmentVariable("NO_EM_RESTART", "1");
108       process.executeAsync();
109     } catch (IOException e) {
110       throw new RuntimeException(e);
111     }
112   }
113 
114   private void populateCustomProfileDirectory(String profilePath) {
115     /*
116      * The first time we launch Firefox with an empty profile directory, Firefox will launch itself,
117      * populate the profile directory, then kill/relaunch itself, so our process handle goes out of
118      * date. So, the first time we launch Firefox, we'll start it up at an URL that will immediately
119      * shut itself down.
120      */
121     CommandLine command = prepareCommand(browserInstallation.launcherFilePath(),
122         "-profile", profilePath,
123         "-silent"
124         );
125     command.setDynamicLibraryPath(browserInstallation.libraryPath());
126     log.info("Preparing Firefox profile...");
127     command.execute();
128     try {
129       waitForFullProfileToBeCreated(20 * 1000);
130     } catch (RuntimeException e) {
131       command.destroy();
132       throw e;
133     }
134   }
135 
136   protected CommandLine prepareCommand(String... commands) {
137     CommandLine command = new CommandLine(commands);
138     command.setEnvironmentVariable("MOZ_NO_REMOTE", "1");
139 
140     // don't set the library path on Snow Leopard
141     Platform platform = Platform.getCurrent();
142     if (!platform.is(Platform.MAC) || ((platform.is(Platform.MAC))
143         && platform.getMajorVersion() <= 10
144         && platform.getMinorVersion() <= 5)) {
145       command.setDynamicLibraryPath(browserInstallation.libraryPath());
146     }
147 
148     if (System.getProperty("DISPLAY") != null) {
149         command.setEnvironmentVariable("DISPLAY", System.getProperty("DISPLAY"));
150     }
151 
152     return command;
153   }
154 
155   protected void createCustomProfileDir() {
156     customProfileDir = LauncherUtils.createCustomProfileDir(sessionId);
157   }
158 
159   protected void copyDirectory(File sourceDir, File destDir) {
160     LauncherUtils.copyDirectory(sourceDir, destDir);
161   }
162 
163   protected File initProfileTemplate() {
164     File firefoxProfileTemplate;
165 
166     String relativeProfile = BrowserOptions
167         .getProfile(browserConfigurationOptions);
168     if (relativeProfile == null) {
169       relativeProfile = "";
170     }
171 
172     File profilesLocation = getConfiguration().getProfilesLocation();
173     if (profilesLocation != null && !"".equals(relativeProfile)) {
174 
175       firefoxProfileTemplate = getFileFromParent(profilesLocation, relativeProfile);
176       if (!firefoxProfileTemplate.exists()) {
177         throw new RuntimeException(
178             "The profile specified '" + firefoxProfileTemplate.getAbsolutePath()
179                 + "' does not exist");
180       }
181     } else {
182       firefoxProfileTemplate =
183           BrowserOptions.getFile(browserConfigurationOptions, "firefoxProfileTemplate");
184     }
185 
186     if (firefoxProfileTemplate != null) {
187       copyDirectory(firefoxProfileTemplate, customProfileDir);
188     }
189 
190     return firefoxProfileTemplate;
191   }
192 
193   protected void extractProfileFromJar() throws IOException {
194     ResourceExtractor.extractResourcePath(getClass(), "/customProfileDirCUSTFFCHROME",
195         customProfileDir);
196   }
197 
198   protected void copySingleFileWithOverwrite(File sourceFile, File destFile) {
199     LauncherUtils.copySingleFileWithOverwrite(sourceFile, destFile, true);
200   }
201 
202   protected File getFileFromParent(final File parent, String child) {
203     return new File(parent, child);
204   }
205 
206   protected void copyCert8db(final File firefoxProfileTemplate) {
207     // Make sure that cert8.db of firefoxProfileTemplate is stored into customProfileDir
208     if (firefoxProfileTemplate != null) {
209       File sourceCertFile = getFileFromParent(firefoxProfileTemplate, "cert8.db");
210       if (sourceCertFile.exists()) {
211         File destCertFile = new File(customProfileDir, "cert8.db");
212         copySingleFileWithOverwrite(sourceCertFile, destCertFile);
213       }
214     }
215   }
216 
217   protected void generatePacAndPrefJs(String homePage) throws IOException {
218     browserConfigurationOptions = Proxies.setProxyRequired(browserConfigurationOptions, false);
219     if (browserConfigurationOptions.is("captureNetworkTraffic") ||
220         browserConfigurationOptions.is("addCustomRequestHeaders") ||
221         browserConfigurationOptions.is("trustAllSSLCertificates")) {
222       browserConfigurationOptions = Proxies.setProxyEverything(browserConfigurationOptions, true);
223       browserConfigurationOptions = Proxies.setProxyRequired(browserConfigurationOptions, true);
224     }
225 
226     LauncherUtils.generatePacAndPrefJs(customProfileDir, getPort(), homePage,
227         changeMaxConnections, getTimeout(), browserConfigurationOptions);
228   }
229 
230   private String makeCustomProfile(String homePage) throws IOException {
231 
232     createCustomProfileDir();
233 
234     File firefoxProfileTemplate = initProfileTemplate();
235 
236     extractProfileFromJar();
237 
238     copyCert8db(firefoxProfileTemplate);
239 
240     copyRunnerHtmlFiles();
241 
242     changeMaxConnections = browserConfigurationOptions.is("changeMaxConnections");
243 
244     generatePacAndPrefJs(homePage);
245 
246     return customProfileDir.getAbsolutePath();
247   }
248 
249 
250   private void copyRunnerHtmlFiles() {
251     String guid = "{503A0CD4-EDC8-489b-853B-19E0BAA8F0A4}";
252     File extensionDir = new File(customProfileDir, "extensions/" + guid);
253     File htmlDir = new File(extensionDir, "chrome");
254     htmlDir.mkdirs();
255 
256     LauncherUtils.extractHTAFile(htmlDir, getPort(), "/core/TestRunner.html", "TestRunner.html");
257     LauncherUtils.extractHTAFile(htmlDir, getPort(), "/core/TestPrompt.html", "TestPrompt.html");
258     LauncherUtils.extractHTAFile(htmlDir, getPort(), "/core/RemoteRunner.html",
259         "RemoteRunner.html");
260 
261   }
262 
263 
264   public void close() {
265     if (closed) {
266       return;
267     }
268     FileLockRemainedException fileLockException = null;
269     if (process != null) {
270       try {
271         killFirefoxProcess();
272       } catch (FileLockRemainedException flre) {
273         fileLockException = flre;
274       }
275     }
276     if (customProfileDir != null) {
277       try {
278         removeCustomProfileDir();
279       } catch (RuntimeException e) {
280         if (fileLockException != null) {
281           log.log(Level.SEVERE, "Couldn't delete custom Firefox profile directory", e);
282           log.severe("Perhaps caused by this exception:");
283           log.log(Level.SEVERE, "Perhaps caused by this exception:", fileLockException);
284           throw new RuntimeException("Couldn't delete custom Firefox " +
285               "profile directory, presumably because task kill failed; " +
286               "see error LOGGER!", e);
287         }
288         throw e;
289       }
290     }
291     closed = true;
292   }
293 
294   /**
295    * Wrapper to allow for stubbed-out testing *
296    */
297   protected void removeCustomProfileDir() throws RuntimeException {
298     LauncherUtils.deleteTryTryAgain(customProfileDir, 6);
299   }
300 
301   /**
302    * Wrapper to allow for stubbed-out testing *
303    */
304   protected void killFirefoxProcess() throws FileLockRemainedException {
305     log.info("Killing Firefox...");
306     int exitValue = process.destroy();
307     if (exitValue == 0) {
308       log.warning("Firefox seems to have ended on its own (did we kill the real browser???)");
309     }
310     waitForFileLockToGoAway(0, 500);
311   }
312 
313   /**
314    * Firefox knows it's running by using a "parent.lock" file in the profile directory. Wait for
315    * this file to go away (and stay gone)
316    *
317    * @param timeout max time to wait for the file to go away
318    * @param timeToWait minimum time to wait to make sure the file is gone
319    * @throws FileLockRemainedException
320    */
321   private void waitForFileLockToGoAway(long timeout, long timeToWait)
322       throws FileLockRemainedException {
323     File lock = new File(customProfileDir, "parent.lock");
324     for (long start = System.currentTimeMillis(); System.currentTimeMillis() < start + timeout;) {
325       Sleeper.sleepTight(500);
326       if (!lock.exists() && makeSureFileLockRemainsGone(lock, timeToWait)) {
327         return;
328       }
329     }
330     if (lock.exists()) {
331       throw new FileLockRemainedException("Lock file still present! " + lock.getAbsolutePath());
332     }
333   }
334 
335   /**
336    * When initializing the profile, Firefox rapidly starts, stops, restarts and stops again; we need
337    * to wait a bit to make sure the file lock is really gone.
338    *
339    * @param lock the parent.lock file in the profile directory
340    * @param timeToWait minimum time to wait to see if the file shows back up again. This is not a
341    *        timeout; we will always wait this amount of time or more.
342    * @return true if the file stayed gone for the entire timeToWait; false if the file exists (or
343    *         came back)
344    */
345   private boolean makeSureFileLockRemainsGone(File lock, long timeToWait) {
346     for (long start = System.currentTimeMillis(); System.currentTimeMillis() < start + timeToWait;) {
347       Sleeper.sleepTight(500);
348       if (lock.exists()) {
349         return false;
350       }
351     }
352     return !lock.exists();
353   }
354 
355   /**
356    * Wait for one of the Firefox-generated files to come into existence, then wait for Firefox to
357    * exit
358    *
359    * @param timeout the maximum amount of time to wait for the profile to be created
360    */
361   private void waitForFullProfileToBeCreated(long timeout) {
362     // This will be a characteristic file in the profile
363     File testFile = new File(customProfileDir, "extensions.ini");
364     long start = System.currentTimeMillis();
365     for (; System.currentTimeMillis() < start + timeout;) {
366 
367       Sleeper.sleepTight(500);
368       if (testFile.exists()) {
369         break;
370       }
371     }
372     if (!testFile.exists()) {
373       throw new RuntimeException("Timed out waiting for profile to be created!");
374     }
375     // wait the rest of the timeout for the file lock to go away
376     long subTimeout = timeout - (System.currentTimeMillis() - start);
377     try {
378       waitForFileLockToGoAway(subTimeout, 500);
379     } catch (FileLockRemainedException e) {
380       throw new RuntimeException("Firefox refused shutdown while preparing a profile", e);
381     }
382   }
383 
384   // visible for testing
385 
386   protected void setCustomProfileDir(File value) {
387     customProfileDir = value;
388   }
389 
390   // visible for testing
391 
392   protected void setCommandLine(CommandLine p) {
393     process = p;
394   }
395 
396   protected class FileLockRemainedException extends Exception {
397     FileLockRemainedException(String message) {
398       super(message);
399     }
400   }
401 
402   public static class ChromeUrlConvert {
403     public String convert(String httpUrl) throws MalformedURLException {
404       String query = LauncherUtils.getQueryString(httpUrl);
405       String file = new File(new URL(httpUrl).getPath()).getName();
406       return "chrome://src/content/" + file + "?" + query;
407     }
408   }
409 
410   @Override
411   // need to specify an absolute resultsUrl
412   public void launchHTMLSuite(String suiteUrl, String browserURL) {
413     // If navigating to TestPrompt, use the baked-in version instead.
414     if (suiteUrl != null && suiteUrl.startsWith("TestPrompt.html?")) {
415       suiteUrl =
416           suiteUrl.replaceFirst("^TestPrompt\\.html\\?", "chrome://src/content/TestPrompt.html?");
417     }
418     launch(LauncherUtils.getDefaultHTMLSuiteUrl(browserURL, suiteUrl,
419         (!BrowserOptions.isSingleWindow(browserConfigurationOptions)), getPort()));
420   }
421 
422   @Override
423   // need to specify an absolute driverUrl
424   public void launchRemoteSession(String browserURL) {
425     launch(LauncherUtils.getDefaultRemoteSessionUrl(browserURL, sessionId,
426         (!BrowserOptions.isSingleWindow(browserConfigurationOptions)), getPort(),
427         browserConfigurationOptions.is("browserSideLog")));
428   }
429 
430 }