firefox/binary.js

1// Copyright 2014 Selenium committers
2// Copyright 2014 Software Freedom Conservancy
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 * @fileoverview Manages Firefox binaries. This module is considered internal;
18 * users should use {@link selenium-webdriver/firefox}.
19 */
20
21'use strict';
22
23var child = require('child_process'),
24 fs = require('fs'),
25 path = require('path'),
26 util = require('util');
27
28var Serializable = require('..').Serializable,
29 promise = require('..').promise,
30 _base = require('../_base'),
31 io = require('../io'),
32 exec = require('../io/exec');
33
34
35
36/** @const */
37var NO_FOCUS_LIB_X86 = _base.isDevMode() ?
38 path.join(__dirname, '../../../../cpp/prebuilt/i386/libnoblur.so') :
39 path.join(__dirname, '../lib/firefox/i386/libnoblur.so') ;
40
41/** @const */
42var NO_FOCUS_LIB_AMD64 = _base.isDevMode() ?
43 path.join(__dirname, '../../../../cpp/prebuilt/amd64/libnoblur64.so') :
44 path.join(__dirname, '../lib/firefox/amd64/libnoblur64.so') ;
45
46var X_IGNORE_NO_FOCUS_LIB = 'x_ignore_nofocus.so';
47
48var foundBinary = null;
49
50
51/**
52 * Checks the default Windows Firefox locations in Program Files.
53 * @return {!promise.Promise.<?string>} A promise for the located executable.
54 * The promise will resolve to {@code null} if Fireox was not found.
55 */
56function defaultWindowsLocation() {
57 var files = [
58 process.env['PROGRAMFILES'] || 'C:\\Program Files',
59 process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)'
60 ].map(function(prefix) {
61 return path.join(prefix, 'Mozilla Firefox\\firefox.exe');
62 });
63 return io.exists(files[0]).then(function(exists) {
64 return exists ? files[0] : io.exists(files[1]).then(function(exists) {
65 return exists ? files[1] : null;
66 });
67 });
68}
69
70
71/**
72 * Locates the Firefox binary for the current system.
73 * @return {!promise.Promise.<string>} A promise for the located binary. The
74 * promise will be rejected if Firefox cannot be located.
75 */
76function findFirefox() {
77 if (foundBinary) {
78 return foundBinary;
79 }
80
81 if (process.platform === 'darwin') {
82 var osxExe = '/Applications/Firefox.app/Contents/MacOS/firefox-bin';
83 foundBinary = io.exists(osxExe).then(function(exists) {
84 return exists ? osxExe : null;
85 });
86 } else if (process.platform === 'win32') {
87 foundBinary = defaultWindowsLocation();
88 } else {
89 foundBinary = promise.fulfilled(io.findInPath('firefox'));
90 }
91
92 return foundBinary = foundBinary.then(function(found) {
93 if (found) {
94 return found;
95 }
96 throw Error('Could not locate Firefox on the current system');
97 });
98}
99
100
101/**
102 * Copies the no focus libs into the given profile directory.
103 * @param {string} profileDir Path to the profile directory to install into.
104 * @return {!promise.Promise.<string>} The LD_LIBRARY_PATH prefix string to use
105 * for the installed libs.
106 */
107function installNoFocusLibs(profileDir) {
108 var x86 = path.join(profileDir, 'x86');
109 var amd64 = path.join(profileDir, 'amd64');
110
111 return mkdir(x86)
112 .then(copyLib.bind(null, NO_FOCUS_LIB_X86, x86))
113 .then(mkdir.bind(null, amd64))
114 .then(copyLib.bind(null, NO_FOCUS_LIB_AMD64, amd64))
115 .then(function() {
116 return x86 + ':' + amd64;
117 });
118
119 function mkdir(dir) {
120 return io.exists(dir).then(function(exists) {
121 if (!exists) {
122 return promise.checkedNodeCall(fs.mkdir, dir);
123 }
124 });
125 }
126
127 function copyLib(src, dir) {
128 return io.copy(src, path.join(dir, X_IGNORE_NO_FOCUS_LIB));
129 }
130}
131
132
133/**
134 * Silently runs Firefox to install a profile directory (which is assumed to be
135 * defined in the given environment variables).
136 * @param {string} firefox Path to the Firefox executable.
137 * @param {!Object.<string, string>} env The environment variables to use.
138 * @return {!promise.Promise} A promise for when the profile has been installed.
139 */
140function installProfile(firefox, env) {
141 var installed = promise.defer();
142 child.exec(firefox + ' -silent', {env: env, timeout: 180 * 1000},
143 function(err) {
144 if (err) {
145 installed.reject(new Error(
146 'Failed to install Firefox profile: ' + err));
147 return;
148 }
149 installed.fulfill();
150 });
151 return installed.promise;
152}
153
154
155/**
156 * Manages a Firefox subprocess configured for use with WebDriver.
157 *
158 * @param {string=} opt_exe Path to the Firefox binary to use. If not
159 * specified, will attempt to locate Firefox on the current system.
160 * @constructor
161 * @extends {Serializable.<string>}
162 */
163var Binary = function(opt_exe) {
164 Serializable.call(this);
165
166 /** @private {(string|undefined)} */
167 this.exe_ = opt_exe;
168
169 /** @private {!Array.<string>} */
170 this.args_ = [];
171
172 /** @private {!Object.<string, string>} */
173 this.env_ = {};
174 Object.keys(process.env).forEach(function(key) {
175 this.env_[key] = process.env[key];
176 }.bind(this));
177 this.env_['MOZ_CRASHREPORTER_DISABLE'] = '1';
178 this.env_['MOZ_NO_REMOTE'] = '1';
179 this.env_['NO_EM_RESTART'] = '1';
180
181 /** @private {promise.Promise.<!exec.Command>} */
182 this.command_ = null;
183};
184util.inherits(Binary, Serializable);
185
186
187/**
188 * Add arguments to the command line used to start Firefox.
189 * @param {...(string|!Array.<string>)} var_args Either the arguments to add as
190 * varargs, or the arguments as an array.
191 */
192Binary.prototype.addArguments = function(var_args) {
193 for (var i = 0; i < arguments.length; i++) {
194 if (util.isArray(arguments[i])) {
195 this.args_ = this.args_.concat(arguments[i]);
196 } else {
197 this.args_.push(arguments[i]);
198 }
199 }
200};
201
202
203/**
204 * Launches Firefox and eturns a promise that will be fulfilled when the process
205 * terminates.
206 * @param {string} profile Path to the profile directory to use.
207 * @return {!promise.Promise.<!exec.Result>} A promise for the process result.
208 * @throws {Error} If this instance has already been started.
209 */
210Binary.prototype.launch = function(profile) {
211 if (this.command_) {
212 throw Error('Firefox is already running');
213 }
214
215 var env = {};
216 Object.keys(this.env_).forEach(function(key) {
217 env[key] = this.env_[key];
218 }.bind(this));
219 env['XRE_PROFILE_PATH'] = profile;
220
221 var args = ['-foreground'].concat(this.args_);
222
223 var self = this;
224
225 this.command_ = promise.when(this.exe_ || findFirefox(), function(firefox) {
226 if (process.platform === 'win32' || process.platform === 'darwin') {
227 return firefox;
228 }
229 return installNoFocusLibs(profile).then(function(ldLibraryPath) {
230 env['LD_LIBRARY_PATH'] = ldLibraryPath + ':' + env['LD_LIBRARY_PATH'];
231 env['LD_PRELOAD'] = X_IGNORE_NO_FOCUS_LIB;
232 return firefox;
233 });
234 }).then(function(firefox) {
235 var install = exec(firefox, {args: ['-silent'], env: env});
236 return install.result().then(function(result) {
237 if (result.code !== 0) {
238 throw Error(
239 'Failed to install profile; firefox terminated with ' + result);
240 }
241
242 return exec(firefox, {args: args, env: env});
243 });
244 });
245
246 return this.command_.then(function() {
247 // Don't return the actual command handle, just a promise to signal it has
248 // been started.
249 });
250};
251
252
253/**
254 * Kills the managed Firefox process.
255 * @return {!promise.Promise} A promise for when the process has terminated.
256 */
257Binary.prototype.kill = function() {
258 if (!this.command_) {
259 return promise.defer(); // Not running.
260 }
261 return this.command_.then(function(command) {
262 command.kill();
263 return command.result();
264 });
265};
266
267
268/**
269 * Returns a promise for the wire representation of this binary. Note: the
270 * FirefoxDriver only supports passing the path to the binary executable over
271 * the wire; all command line arguments and environment variables will be
272 * discarded.
273 *
274 * @return {!promise.Promise.<string>} A promise for this binary's wire
275 * representation.
276 * @override
277 */
278Binary.prototype.serialize = function() {
279 return promise.when(this.exe_ || findFirefox());
280};
281
282
283// PUBLIC API
284
285
286exports.Binary = Binary;
287