# 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 |
23 | var child = require('child_process'),
24 | fs = require('fs'),
25 | path = require('path'),
26 | util = require('util');
27 |
28 | var 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 */
37 | var 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 */
42 | var 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 |
46 | var X_IGNORE_NO_FOCUS_LIB = 'x_ignore_nofocus.so';
47 |
48 | var 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 | */
56 | function 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 | */
76 | function 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 | */
107 | function 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 | */
140 | function 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 | */
163 | var 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 | };
184 | util.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 | */
192 | Binary.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 | */
210 | Binary.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 | */
257 | Binary.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 | */
278 | Binary.prototype.serialize = function() {
279 | return promise.when(this.exe_ || findFirefox());
280 | };
281 |
282 |
283 | // PUBLIC API
284 |
285 |
286 | exports.Binary = Binary;
287 |