UNPKG

8.63 kBJavaScriptView Raw
1/*
2 * Copyright 2013 Amadeus s.a.s.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15var pathUtil = require("path");
16
17var exitProcess = require("exit");
18
19var optimizeParallel = require("../util/optimize-parallel.js");
20var spawn = require("../util/child-processes.js").spawn;
21
22/**
23 * Launcher for PhantomJS, this module listen to attester event to create phantom instances and connect them as slaves
24 */
25
26var attester = require("../attester");
27var config = attester.config;
28var logger = attester.logger;
29
30// Most of the entries in cfg will be set later in "launcher.connect" listener
31var cfg = {
32 maxRetries: 3,
33 // how many times to retry rebooting phantom in case of recoverable errors
34 onAllPhantomsDied: function () {
35 endProcess(1);
36 },
37 phantomPath: null,
38 slaveURL: null,
39 pipeStdOut: true,
40 phantomInstances: 0
41};
42var state = {
43 resetCalled: false,
44 retries: [],
45 // stores how many times each instance was rebooted
46 erroredPhantomInstances: 0
47 // stores how many phantoms died unrecoverably
48};
49
50// Exposing all those methods for the sake of testability.
51// They don't rely on global `cfg` and `state` but on parameters for for the same reason.
52module.exports = {
53 __init__: function () {
54 state.resetCalled = false;
55 attester.event.on("launcher.connect", onLauncherConnect);
56 },
57 __reset__: function () {
58 state.resetCalled = true;
59 attester.event.off("launcher.connect", onLauncherConnect);
60 },
61 /**
62 * Starts PhantomJS child process with instance number `n` using `cfg.path` as PhantomJS path and connects it to
63 * `cfg.slaveURL`
64 * @param {Object} cfg
65 * @param {Object} state
66 * @param {Integer} n
67 */
68 bootPhantom: function (cfg, state, n) {
69 cfg.args = cfg.args || {};
70 var phantomPath = cfg.phantomPath;
71 var controlScript = pathUtil.join(__dirname, '../browsers/phantomjs-control-script.js');
72
73 var args = [];
74 args.push(controlScript);
75 args.push("--auto-exit");
76 if (cfg.args.autoExitPolling) {
77 args.push("--auto-exit-polling=" + cfg.args.autoExitPolling);
78 }
79 if (typeof n == "undefined") {
80 n = Math.round(Math.random() * 1000) % 1000;
81 }
82 args.push("--instance-id=" + n);
83 args.push(cfg.slaveURL);
84
85 var phantomProcess = spawn(phantomPath, args, {
86 stdio: "pipe"
87 });
88 if (cfg.pipeStdOut) {
89 phantomProcess.stdout.pipe(process.stdout);
90 phantomProcess.stderr.pipe(process.stderr);
91 }
92 if (cfg.onData) {
93 phantomProcess.stdout.on("data", cfg.onData);
94 }
95 phantomProcess.on("exit", cfg.onExit || this.createPhantomExitCb(cfg, state, n).bind(this));
96 phantomProcess.on("error", cfg.onError || this.createPhantomErrorCb(cfg, state, n).bind(this));
97 return phantomProcess;
98 },
99
100 /**
101 * Factory of callback functions to be used as 'exit' listener by PhantomJS processes.
102 * @param {Object} cfg
103 * @param {Object} state
104 * @param {Integer} n
105 * @return {Function}
106 */
107 createPhantomExitCb: function (cfg, state, n) {
108 // Node 0.8 and 0.10 differently handle spawning errors ('exit' vs 'error'), but errors that happened after
109 // launching the command are both handled in 'exit' callback
110 return function (code, signal) {
111 // See http://tldp.org/LDP/abs/html/exitcodes.html and http://stackoverflow.com/a/1535733/
112 if (code === 0 || signal == "SIGTERM" || state.resetCalled) {
113 return;
114 }
115
116 var isNotRecoverable = (code == 127 || code == 126);
117 if (isNotRecoverable) {
118 ++state.erroredPhantomInstances;
119 var path = cfg.phantomPath;
120 if (code == 127) {
121 logger.logError("Spawn: exited with code 127. PhantomJS executable not found. Make sure to download PhantomJS and add its folder to your system's PATH, or pass the full path directly to Attester via --phantomjs-path.\nUsed command: '" + path + "'");
122 } else if (code == 126) {
123 logger.logError("Spawn: exited with code 126. Unable to execute PhantomJS. Make sure to have proper read & execute permissions set.\nUsed command: '" + path + "'");
124 }
125 checkIfAllPhantomsDied(cfg, state);
126 return;
127 }
128
129 // Now, try to recover unless retried too many times
130
131 // prepare error message
132 var errMsg;
133 if (code == 75) {
134 errMsg = "Spawn: PhantomJS[" + n + "] exited with code 75: unable to load attester page within specified timeout, or errors happened while loading.";
135 if (cfg.phantomInstances > 1) {
136 errMsg += " You may try decreasing the number of PhantomJS instances in attester config to avoid that problem.";
137 }
138 } else {
139 errMsg = "Spawn: PhantomJS[" + n + "] exited with code " + code + " and signal " + signal;
140 }
141
142 // check how many retries happened for this instance
143 var retries = state.retries;
144 retries[n] = (retries[n] || 0) + 1;
145 if (retries[n] < cfg.maxRetries) {
146 // log just a warning and try rebooting
147 logger.logWarn(errMsg);
148 logger.logWarn("Trying to reboot instance nr " + n + "...");
149 this.bootPhantom(cfg, state, n);
150 } else {
151 logger.logError(errMsg);
152 ++state.erroredPhantomInstances;
153 checkIfAllPhantomsDied(cfg, state);
154 }
155 };
156 },
157
158 /**
159 * Factory of callback functions to be used as 'error' listener by PhantomJS processes.
160 * @param {Object} cfg
161 * @param {Object} state
162 * @param {Integer} n
163 * @return {Function}
164 */
165 createPhantomErrorCb: function (cfg, state, n) {
166 return function (err) {
167 if (err.code == "ENOENT") {
168 logger.logError("Spawn: exited with code ENOENT. PhantomJS executable not found. Make sure to download PhantomJS and add its folder to your system's PATH, or pass the full path directly to Attester via --phantomjs-path.\nUsed command: '" + cfg.phantomPath + "'");
169 } else {
170 logger.logError("Unable to spawn PhantomJS; error code " + err.code);
171 }
172 };
173 }
174};
175
176function onLauncherConnect(slaveURL) {
177 var suggestedInstances = config["phantomjs-instances"]; // config is not available earlier
178 var phantomInstances = optimizeParallel({
179 memoryPerInstance: 60,
180 maxInstances: suggestedInstances
181 }, logger);
182 if (phantomInstances === 0) {
183 return;
184 }
185
186 // Set cfg so that functions depending on these globals work fine
187 cfg.phantomInstances = phantomInstances;
188 cfg.phantomPath = config["phantomjs-path"];
189 cfg.slaveURL = slaveURL;
190
191 logger.logDebug("Spawning " + phantomInstances + " instances of PhantomJS");
192 if (phantomInstances == 1) {
193 // If there's only one phantom, let's assign it a random "id"
194 // This is for analyzing logs of attester suite itself
195 // For normal users requesting N phantoms, assign them ids 1 through N
196 module.exports.bootPhantom(cfg, state);
197 } else {
198 for (var n = 1; n <= phantomInstances; n++) {
199 module.exports.bootPhantom(cfg, state, n);
200 }
201 }
202}
203
204function checkIfAllPhantomsDied(cfg, state) {
205 // If all phantoms died and were unable to recover, something is really wrong
206 if (state.erroredPhantomInstances === cfg.phantomInstances && cfg.phantomInstances > 0) {
207 logger.logError("All the instances of PhantomJS were terminated with errors; disposing attester and exiting");
208 if (cfg.onAllPhantomsDied) {
209 cfg.onAllPhantomsDied();
210 }
211 }
212}
213
214function endProcess(code) {
215 attester.event.emit("closing");
216 process.nextTick(function () {
217 exitProcess(code);
218 });
219}