3 var EventEmitter = require("events").EventEmitter;
4 var os = require("os");
5 var instances = require("./instances.js");
6 var Page = require("./Page.js");
7 var serializeFn = require("./serializeFn.js");
8 var phantomMethods = require("./phantom/methods.js");
11 var slice = Array.prototype.slice;
12 var pingInterval = 100;
13 var nextRequestId = 0;
16 * Provides methods to run code within a given PhantomJS child-process.
19 * @param {ChildProcess} childProcess
21 function Phantom(childProcess) {
22 Phantom.prototype.constructor.apply(this, arguments);
25 Phantom.prototype = Object.create(EventEmitter.prototype);
28 * The ChildProcess-instance returned by node.
30 * @type {child_process.ChildProcess}
32 Phantom.prototype.childProcess = null;
35 * Boolean flag which indicates that this process is about to exit or has already exited.
40 Phantom.prototype._isDisposed = false;
43 * The current scheduled ping id as returned by setTimeout()
48 Phantom.prototype._pingTimeoutId = null;
51 * The number of currently pending requests. This is necessary so we can stop the interval
52 * when no requests are pending.
57 Phantom.prototype._pending = 0;
60 * An object providing the resolve- and reject-function of all pending requests. Thus we can
61 * resolve or reject a pending promise in a different scope.
66 Phantom.prototype._pendingDeferreds = null;
69 * A reference to the unexpected error which caused PhantomJS to exit.
70 * Will be appended to the error message for pending deferreds.
75 Phantom.prototype._unexpectedError = null;
78 * Initializes a new Phantom instance.
80 * @param {child_process.ChildProcess} childProcess
82 Phantom.prototype.constructor = function (childProcess) {
83 EventEmitter.call(this);
85 this._receive = this._receive.bind(this);
86 this._write = this._write.bind(this);
87 this._afterExit = this._afterExit.bind(this);
88 this._onUnexpectedError = this._onUnexpectedError.bind(this);
90 this.childProcess = childProcess;
91 this._pendingDeferreds = {};
95 // Listen for stdout messages dedicated to phridge
96 childProcess.phridge.on("data", this._receive);
98 // Add handlers for unexpected events
99 childProcess.on("exit", this._onUnexpectedError);
100 childProcess.on("error", this._onUnexpectedError);
101 childProcess.stdin.on("error", this._onUnexpectedError);
102 childProcess.stdout.on("error", this._onUnexpectedError);
103 childProcess.stderr.on("error", this._onUnexpectedError);
107 * Stringifies the given function fn, sends it to PhantomJS and runs it in the scope of PhantomJS.
108 * You may prepend any number of arguments which will be passed to fn inside of PhantomJS. Please note that all
109 * arguments should be stringifyable with JSON.stringify().
112 * @param {Function} fn
115 Phantom.prototype.run = function (args, fn) {
120 return new Promise(function (resolve, reject) {
121 args = slice.call(args);
128 src: serializeFn(fn, args)
131 args.length === fn.length
132 ).then(resolve, reject);
137 * Returns a new instance of a Page which can be used to run code in the context of a specific page.
141 Phantom.prototype.createPage = function () {
144 return new Page(self, pageId++);
148 * Creates a new instance of Page, opens the given url and resolves when the page has been loaded.
150 * @param {string} url
153 Phantom.prototype.openPage = function (url) {
154 var page = this.createPage();
156 return page.run(url, phantomMethods.openPage)
163 * Exits the PhantomJS process cleanly and cleans up references.
165 * @see http://msdn.microsoft.com/en-us/library/system.idisposable.aspx
168 Phantom.prototype.dispose = function () {
171 return new Promise(function dispose(resolve, reject) {
172 if (self._isDisposed) {
177 // Remove handler for unexpected exits and add regular exit handlers
178 self.childProcess.removeListener("exit", self._onUnexpectedError);
179 self.childProcess.on("exit", self._afterExit);
180 self.childProcess.on("exit", resolve);
182 self.removeAllListeners();
184 self.run(phantomMethods.exitPhantom).catch(reject);
191 * Prepares the given message and writes it to childProcess.stdin.
193 * @param {Object} message
194 * @param {boolean} fnIsSync
198 Phantom.prototype._send = function (message, fnIsSync) {
201 return new Promise(function (resolve, reject) {
202 message.from = new Error().stack
206 message.id = nextRequestId++;
208 self._pendingDeferreds[message.id] = {
213 self._schedulePing();
217 self._write(message);
222 * Helper function that stringifies the given message-object, appends an end of line character
223 * and writes it to childProcess.stdin.
225 * @param {Object} message
228 Phantom.prototype._write = function (message) {
229 this.childProcess.stdin.write(JSON.stringify(message) + os.EOL, "utf8");
233 * Parses the given message via JSON.parse() and resolves or rejects the pending promise.
235 * @param {string} message
238 Phantom.prototype._receive = function (message) {
239 // That's our initial hi message which should be ignored by this method
240 if (message === "hi") {
244 // Not wrapping with try-catch here because if this message is invalid
245 // we have no chance to map it back to a pending promise.
246 // Luckily this JSON can't be invalid because it has been JSON.stringified by PhantomJS.
247 message = JSON.parse(message);
249 // pong messages are special
250 if (message.status === "pong") {
251 this._pingTimeoutId = null;
253 // If we're still waiting for a message, we need to schedule a new ping
254 if (this._pending > 0) {
255 this._schedulePing();
259 this._resolveDeferred(message);
263 * Takes the required actions to respond on the given message.
265 * @param {Object} message
268 Phantom.prototype._resolveDeferred = function (message) {
271 deferred = this._pendingDeferreds[message.id];
273 // istanbul ignore next because this is tested in a separated process and thus isn't recognized by istanbul
275 // This happens when resolve() or reject() have been called twice
276 if (message.status === "success") {
277 throw new Error("Cannot call resolve() after the promise has already been resolved or rejected");
278 } else if (message.status === "fail") {
279 throw new Error("Cannot call reject() after the promise has already been resolved or rejected");
283 delete this._pendingDeferreds[message.id];
286 if (message.status === "success") {
287 deferred.resolve(message.data);
289 deferred.reject(message.data);
294 * Sends a ping to the PhantomJS process after a given delay.
295 * Check out lib/phantom/start.js for an explanation of the ping action.
299 Phantom.prototype._schedulePing = function () {
300 if (this._pingTimeoutId !== null) {
301 // There is already a ping scheduled. It's unnecessary to schedule another one.
304 if (this._isDisposed) {
305 // No need to schedule a ping, this instance is about to be disposed.
306 // Catches rare edge cases where a pong message is received right after the instance has been disposed.
307 // @see https://github.com/peerigon/phridge/issues/41
310 this._pingTimeoutId = setTimeout(this._write, pingInterval, { action: "ping" });
314 * This function is executed before the process is actually killed.
315 * If the process was killed autonomously, however, it gets executed postmortem.
319 Phantom.prototype._beforeExit = function () {
322 this._isDisposed = true;
324 index = instances.indexOf(this);
325 index !== -1 && instances.splice(index, 1);
326 clearTimeout(this._pingTimeoutId);
328 // Seal the run()-method so that future calls will automatically be rejected.
333 * This function is executed after the process actually exited.
337 Phantom.prototype._afterExit = function () {
338 var deferreds = this._pendingDeferreds;
339 var errorMessage = "Cannot communicate with PhantomJS process: ";
342 if (this._unexpectedError) {
343 errorMessage += this._unexpectedError.message;
344 error = new Error(errorMessage);
345 error.originalError = this._unexpectedError;
347 errorMessage += "Unknown reason";
348 error = new Error(errorMessage);
351 this.childProcess = null;
353 // When there are still any deferreds, we must reject them now
354 Object.keys(deferreds).forEach(function forEachPendingDeferred(id) {
355 deferreds[id].reject(error);
356 delete deferreds[id];
361 * Will be called as soon as an unexpected IO error happened on the attached PhantomJS process. Cleans up everything
362 * and emits an unexpectedError event afterwards.
364 * Unexpected IO errors usually happen when the PhantomJS process was killed by another party. This can occur
365 * on some OS when SIGINT is sent to the whole process group. In these cases, node throws EPIPE errors.
366 * (https://github.com/peerigon/phridge/issues/34).
369 * @param {Error} error
371 Phantom.prototype._onUnexpectedError = function (error) {
374 if (this._isDisposed) {
378 errorMessage = "PhantomJS exited unexpectedly";
380 error.message = errorMessage + ": " + error.message;
382 error = new Error(errorMessage);
384 this._unexpectedError = error;
387 // Chainsaw against PhantomJS zombies
388 this.childProcess.kill("SIGKILL");
391 this.emit("unexpectedExit", error);
395 * Will be used as "seal" for the run method to prevent run() calls after dispose.
396 * Appends the original error when there was unexpected error.
401 function runGuard() {
402 var err = new Error("Cannot run function");
403 var cause = this._unexpectedError ? this._unexpectedError.message : "Phantom instance is already disposed";
405 err.message += ": " + cause;
406 err.originalError = this._unexpectedError;
408 return Promise.reject(err);
411 module.exports = Phantom;