Skip to content
This repository was archived by the owner on Aug 1, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ results
npm-debug.log
node_modules
coverage
.idea
.idea
/.vs/phridge/v14
/typings
/phridge.njsproj
/phridge.sln
1,030 changes: 1,030 additions & 0 deletions .vs/config/applicationhost.config

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,28 @@ will terminate all processes.

**I strongly recommend to call** `phridge.disposeAll()` **when the node process exits as this is the only way to ensure that all child processes terminate as well.** Since `disposeAll()` is async it is not safe to call it on `process.on("exit")`. It is better to call it on `SIGINT`, `SIGTERM` and within your regular exit flow.

### Calling back from phantom to node

If you wan't to call node function from the phantom enviroment, you can call the "nodeCallback" function within page.run context.
The function at "onCallback" property of the page element are called with the data that sent as parameter for the function.

```javascript
var page = phantom.createPage();

page.run(function (resolve, reject) {
nodeCallback({message: "Hello World!"});
});
```

At node, you need to set the onCallback property
```javascript
var page = phantom.createPage();

page.onCallback = function(data) {
console.log(data.message);
};
```

<br />

API
Expand Down
15 changes: 15 additions & 0 deletions lib/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ Page.prototype.phantom = null;
*/
Page.prototype._id = null;

/**
* The function to run at phantom callback.
*
* @public
* @type {function}
*/
Object.defineProperty(Page.prototype, "onCallback", {
get: function () {
return this.phantom._onCallbacks[this._id];
},
set: function (value) {
this.phantom._onCallbacks[this._id] = value;
}
});

/**
* Initializes the page instance.
*
Expand Down
160 changes: 123 additions & 37 deletions lib/Phantom.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

var EventEmitter = require("events").EventEmitter;
var os = require("os");
var ws = require("ws");
var http = require("http");
var util = require("util");
var instances = require("./instances.js");
var Page = require("./Page.js");
var serializeFn = require("./serializeFn.js");
var phantomMethods = require("./phantom/methods.js");
var governor = require("./governor.js");

var pageId = 0;
var slice = Array.prototype.slice;
Expand All @@ -16,20 +20,26 @@ var nextRequestId = 0;
* Provides methods to run code within a given PhantomJS child-process.
*
* @constructor
* @param {ChildProcess} childProcess
*/
function Phantom(childProcess) {
function Phantom() {
Phantom.prototype.constructor.apply(this, arguments);
}

Phantom.prototype = Object.create(EventEmitter.prototype);

/**
* The ChildProcess-instance returned by node.
* The communication port
*
* @type {child_process.ChildProcess}
* @type {number}
*/
Phantom.prototype.childProcess = null;
Phantom.prototype.port = null;

/**
* An object providing the the onCallback function for the phantom himself
*
* @type {Function}
*/
Phantom.prototype.onCallback = null;

/**
* Boolean flag which indicates that this process is about to exit or has already exited.
Expand Down Expand Up @@ -65,6 +75,14 @@ Phantom.prototype._pending = 0;
*/
Phantom.prototype._pendingDeferreds = null;

/**
* An object providing the the onCallback functions for the pages
*
* @type {Object}
* @private
*/
Phantom.prototype._onCallbacks = null;

/**
* A reference to the unexpected error which caused PhantomJS to exit.
* Will be appended to the error message for pending deferreds.
Expand All @@ -74,33 +92,85 @@ Phantom.prototype._pendingDeferreds = null;
*/
Phantom.prototype._unexpectedError = null;

/**
* A reference to the primus server.
*
* @type {WebSocketServer}
* @private
*/
Phantom.prototype._wss = null;

/**
* A reference to the spark object of the current connection.
*
* @type {WebSocketServer.spark}
* @private
*/
Phantom.prototype._spark = null;

/**
* Initializes a new Phantom instance.
*
* @param {child_process.ChildProcess} childProcess
*/
Phantom.prototype.constructor = function (childProcess) {
Phantom.prototype.constructor = function () {
EventEmitter.call(this);

this._receive = this._receive.bind(this);
this._write = this._write.bind(this);
this._afterExit = this._afterExit.bind(this);
this._onUnexpectedError = this._onUnexpectedError.bind(this);

this.childProcess = childProcess;
this._pendingDeferreds = {};
this._onCallbacks = {};

instances.push(this);
};

// Listen for stdout messages dedicated to phridge
childProcess.phridge.on("data", this._receive);
/**
* Starts the local websocket server
*
* @param {Function} resolve
* @param {Function} reject
*/
Phantom.prototype.startLocalServer = function (resolve, reject) {
var self = this;

// Add handlers for unexpected events
childProcess.on("exit", this._onUnexpectedError);
childProcess.on("error", this._onUnexpectedError);
childProcess.stdin.on("error", this._onUnexpectedError);
childProcess.stdout.on("error", this._onUnexpectedError);
childProcess.stderr.on("error", this._onUnexpectedError);
// Creates a local server for the websockets
var server = http.createServer();

return new Promise(function (fulfill) {
server.listen(0, function () {
var loaded = false;

var connectionTimeout = setTimeout(function () {
if (!loaded) {
governor().send({ action: "kill", port: self.port });

var err = new Error("websocket connection timeout");
reject(err);
}
}, 10 * 1000);

self.port = server.address().port;

// Starts the websocket listener
self._wss = new ws.Server({ server: server });

self._wss.on("close", self._onUnexpectedError);

self._wss.on("connection", function connection(spark) {
loaded = true;
clearTimeout(connectionTimeout);

self._spark = spark;
spark.on("message", self._receive);
spark.on("close", self._onUnexpectedError);
resolve(self);
});

fulfill();
});
});
};

/**
Expand Down Expand Up @@ -174,21 +244,19 @@ Phantom.prototype.dispose = function () {
return;
}

// Remove handler for unexpected exits and add regular exit handlers
self.childProcess.removeListener("exit", self._onUnexpectedError);
self.childProcess.on("exit", self._afterExit);
self.childProcess.on("exit", resolve);

self.removeAllListeners();

self.run(phantomMethods.exitPhantom).catch(reject);

self._beforeExit();
self.run(phantomMethods.exitPhantom).catch(function (message) {
if (!message.phantomClosed)
governor().send({ action: "kill", port: self.port });
}).then(function () {
self._beforeExit();
resolve();
self._afterExit();
});
});
};

/**
* Prepares the given message and writes it to childProcess.stdin.
* Prepares the given message and writes it to the websocket.
*
* @param {Object} message
* @param {boolean} fnIsSync
Expand Down Expand Up @@ -220,13 +288,17 @@ Phantom.prototype._send = function (message, fnIsSync) {

/**
* Helper function that stringifies the given message-object, appends an end of line character
* and writes it to childProcess.stdin.
* and writes it to the websocket.
*
* @param {Object} message
* @private
*/
Phantom.prototype._write = function (message) {
this.childProcess.stdin.write(JSON.stringify(message) + os.EOL, "utf8");
try {
this._spark.send(JSON.stringify(message));
} catch (e) {
this._onUnexpectedError({ message: e });
}
};

/**
Expand All @@ -236,14 +308,10 @@ Phantom.prototype._write = function (message) {
* @private
*/
Phantom.prototype._receive = function (message) {
// That's our initial hi message which should be ignored by this method
if (message === "hi") {
return;
}

// Not wrapping with try-catch here because if this message is invalid
// we have no chance to map it back to a pending promise.
// Luckily this JSON can't be invalid because it has been JSON.stringified by PhantomJS.
message = JSON.parse(message);

// pong messages are special
Expand All @@ -256,6 +324,22 @@ Phantom.prototype._receive = function (message) {
}
return;
}

// callback messages are special
if (message.status === "callback") {
// If the callback is for page
if (Number.isInteger(message.pageId)) {
// And there is an callback handler for this page, then send the data to the handler
if (this._onCallbacks[message.pageId]) {
this._onCallbacks[message.pageId](message.data);
}
// If there is phantom callback handler, then send the data to the handler
} else if (this.onCallback) {
this.onCallback(message.data);
}
return;
}

this._resolveDeferred(message);
};

Expand Down Expand Up @@ -335,6 +419,9 @@ Phantom.prototype._beforeExit = function () {
* @private
*/
Phantom.prototype._afterExit = function () {
// Closing the websocket server
this._wss.close();

var deferreds = this._pendingDeferreds;
var errorMessage = "Cannot communicate with PhantomJS process: ";
var error;
Expand All @@ -346,10 +433,9 @@ Phantom.prototype._afterExit = function () {
} else {
errorMessage += "Unknown reason";
error = new Error(errorMessage);
error.phantomClosed = true;
}

this.childProcess = null;

// When there are still any deferreds, we must reject them now
Object.keys(deferreds).forEach(function forEachPendingDeferred(id) {
deferreds[id].reject(error);
Expand All @@ -376,7 +462,7 @@ Phantom.prototype._onUnexpectedError = function (error) {
}

errorMessage = "PhantomJS exited unexpectedly";
if (error) {
if (error && typeof error == "object") {
error.message = errorMessage + ": " + error.message;
} else {
error = new Error(errorMessage);
Expand All @@ -385,7 +471,7 @@ Phantom.prototype._onUnexpectedError = function (error) {

this._beforeExit();
// Chainsaw against PhantomJS zombies
this.childProcess.kill("SIGKILL");
governor().send({ action: "kill", port: this.port });
this._afterExit();

this.emit("unexpectedExit", error);
Expand Down
29 changes: 27 additions & 2 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
"use strict";

var governor = require("./governor.js");

var stdout = process.stdout;
var stderr = process.stderr;

module.exports = {
/**
* A writable stream where phridge will pipe PhantomJS' stdout messages.
*
* @type {stream.Writable}
* @default process.stdout
*/
stdout: process.stdout,
get stdout() {
return stdout;
},
set stdout(value) {
if (!value)
stdout = process.stdout;
else
stdout = value;

governor().stdout.pipe(stdout, { end: false });
},

/**
* A writable stream where phridge will pipe PhantomJS' stderr messages.
*
* @type {stream.Writable}
* @default process.stderr
*/
stderr: process.stderr
get stderr() {
return stderr;
},
set stderr(value) {
if (!value)
stderr = process.stderr;
else
stderr = value;

governor().stderr.pipe(stderr, { end: false });
}
};
Loading