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