Hook to console.log in electron - javascript

I'm looking to hook to console.log in electron.
I tried following the steps as mentioned here: https://stackoverflow.com/a/9624028/2091948
While the above works well in node.js, this same approach doesn't work in electron (which in-turn is node.js based)
How can I achieve this in electron?
I need to capture all calls to console.log, and console.error, so that I can use the module electron-log(https://www.npmjs.com/package/electron-log) to hook to all console.log, and console.error calls.

I think this could be done just via overriding console.log or console.error like this:
var log = require("electron-log");
console.log = function (message) {
log.info(message);
}
console.error = function (message) {
log.error(message);
}
Alternatively, if you like to preserve the old functions and/or store all messages in an array for later use, you could use the following functions:
var log = require("electron-log"), msgInfo = [], msgErr = [];
// Preserve the old, built-in functions
console.log_old = console.log;
console.error_old = console.error;
console.log = function (message) {
msgInfo.push(message);
log.info(message);
}
console.error = function (message) {
msgErr.push(message);
log.error(message);
}
You can use these functions in a browser window, for the main process, the code you already mentioned should work with some adaptions:
// https://stackoverflow.com/a/9624028, written by "kevin"
var logs = [],
hook_stream = function(_stream, fn) {
// Reference default write method
var old_write = _stream.write;
// _stream now write with our shiny function
_stream.write = fn;
return function() {
// reset to the default write method
_stream.write = old_write;
};
},
// hook up standard output
unhook_stdout = hook_stream(process.stdout, function(string, encoding, fd) {
logs.push(string);
}),
// require electron-log
log = require("electron-log");
// goes to our custom write method
console.log('foo');
console.log('bar');
unhook_stdout();
console.log('Not hooked anymore.');
// Now do what you want with logs stored by the hook
logs.forEach(function(_log) {
// Either log the contents of the message array...
//console.log('logged: ' + _log);
// ...or use electron-log to actually log them.
log.info(_log);
});
This code will work in the main process, because there NodeJS creates the stream to stdout. In the renderer process you will have to use the functions I mentioned above, because Electron doesn't create any stream but rather lets Chromium log all messages to it's Developer Console.

You can hook up to the console-message event of the WebContent object (available from the webContent property on BrowserView and BrowserWindow).
Note that it only provides it as a string so if you are loggign an object it will be received as [object Object]
For more info see the docs

Related

Disabling console access for specific Javascript files

In my current project with lots of dependencies I need a way to disable console access for specific libraries so that those files can't use any of the console functionality.
I could of course disable console functionality by simply finding and replacing it in the library bundle, but as this project has a lot of dependencies that would make updating libraries a huge hassle.
I'm aware that I can disable console functionality by overwriting it with an empty function block:
console.log = function(){};
But that disables the console functionality for the entire project. So im looking for an implementation, or a line of code with which I can disable console functionality for a specific file or code block.
Write a white-listing "middleware" for console.log
// Preserve the old console.log
const log = console.log;
// Used a dictionary because it's faster than lists for lookups
const whiteListedFunctions = {"hello": true};
// Whitelisting "middleware". We used the function's name "funcName"
// as a criteria, but it's adaptable
const isWhitelisted = callerData => callerData.funcName in whiteListedFunctions;
// Replacing the default "console.log"
console.log = arg => {
const stack = new Error().stack.split("at")[2].trim().split(' ');
const fileParts = stack[1].substr(1, stack[1].length - 2).split(':');
const callerData = {
funcName: stack[0],
file: fileParts.slice(0, fileParts.length - 2).join(':'),
lineColNumber: fileParts.slice(fileParts.length - 2).join(':')
};
if (isWhitelisted(callerData)) { // Filtering happens here
log(arg);
}
};
// Define the calling functions
function hello() { console.log("hello"); }
function world() { console.log("world"); }
hello(); // => Prints hello
world(); // => Doesn't print anything
Method explanation
You can do this by creating a whitelist (or blacklist) that will contain your filtering criteria. For example it may contain the name of the functions that call console.log or maybe the file name, or even the line and column numbers.
After that you create your whitelisting "middleware". This will take the caller function data and decide if it can log stuff or not. This will be done based on the previously defined whitelist. You can choose your preferred criteria in this "middleware".
Then you actually replace console.log by overriding with your new logger. This logger will take as an argument the message to log (maybe multiple arguments?). In this function you also need to find the data relating to the caller function (which wanted to call console.log).
Once you have the caller data, you can then use your whitelisting middleware to decide if it can log stuff
Getting information about the caller function
This part is a little "hacky" (but it got the job done in this case). We basically create an Error and check its stack attribute like this new Error().stack. Which will give us this trace
Error
at console.log.arg [as log] (https://stacksnippets.net/js:25:7)
at hello (https://stacksnippets.net/js:41:11)
at https://stacksnippets.net/js:48:1
After processing (split, map, etc...) the trace we get the caller function data. For example here we have
The caller function's name: hello
The file name: https://stacksnippets.net/js
The line and column number: 41:11 (watch out for minifiers)
This bit was inspired by VLAZ's answer in How to disable console.log messages based on criteria from specific javascript source (method, file) or message contents, so make sure to check it out. Really good and thorough post.
Note
To make sense of the trace we can do new Error().stack.split("at")[INDEX].trim().split(' ') where INDEX is the position of the function call you want to target in the stack trace. So if you want to get a different "level" that the one used in this example, try changing INDEX
Just redefine the console to log over a condition, your condition of course will be a check over which library is accessing the function:
// Your condition, could be anything
let condition = true;
/* Redefine the console object changing only the log function with your new version and keeping all the other functionalities intact
*/
let console = (old => ({
...old,
log: text => { if (condition) old.log(text) }
}))(window.console)
// Redefine the old console
window.console = console;
console.log('hello!')
Hope it helped :)
Yes, you can disable console logs from files based on their path! Here's a solution:
// in ./loud-lib.js
module.exports = {
logsSomething: () => console.log('hello from loud-lib')
}
// in ./silent-lib.js
module.exports = {
logsSomething: () => console.log('hello from silent-lib')
}
// in ./index.js
const loud = require('./loud-lib');
const silent = require('./silent-lib');
// save console.log
const log = console.log;
// redefinition of console.log
console.log = (...params) => {
// define regexp for path of libraries that log too much
const loudLibs = [/loud-lib/];
// check if the paths logged in the stacktract match with at least one regexp
const tooLoud = !!loudLibs.find(reg => reg.test(new Error().stack));
// log only if the log is coming from a library that doesn't logs too much
if (!tooLoud) log(...params);
};
loud.logsSomething();
silent.logsSomething();
$ node ./index.js
hello from silent-lib
This is based on the fact that new Error() produces a stack trace that identifies from which file is the error coming from (recursively).
Based on this observation, you can define an array of regular expression that match the name of libraries you don't want to hear logs from. You can get really specific and creative with the re-definition of console.log, but I kept it simple.
However, be aware of this (especially when using Webpack): if you bundle all your JS assets into one single bundle.js, the stacktrace will always point to bundle.js, thus logging everything. You'll have to go further from my code, for example by using stack-source-map, but I don't have sufficient details on your project to deliver a solution. I hope the ideas above are sufficient for you.

Electron - How to share a single promise resolution from Main to multiple Renderers?

I'm new to Electron and trying to figure out the best way to handle a shared object.
Essentially, I want to initialize something once in the Main process, then use it in multiple renderer processes, like so:
// main.js
const node = rosnodejs.initNode(); // returns a promise
// renderer1.js
node.then((nh) => {
nh.subscribe("topic1");
})
// renderer2.js
node.then((nh) => {
nh.subscribe("topic2");
})
I was able to share node using remote, but then nh.subscribe becomes an anonymous function in my renderers and fails. Here's what I'm doing:
// main.js
global.node = rosnodejs.initNode(); // returns a promise
global.node.then((nh) => {
nh.subscribe("topic1"); // WORKS PERFECTLY!
})
// renderer1.js
const remote = require('electron').remote;
const node = remote.getGlobal('node');
node.then((nh) => {
nh.subscribe("topic2"); // FAILS MISERABLY.
})
Failure message is Error: Could not call remote function 'subscribe'. Check that the function signature is correct. Underlying error: type.datatype is not a function.
Is there a decent way to handle this? Should I be using ipcMain/ipcRenderer instead?

Attempting to use MeteorJS w/twit Node Module, Error: [TypeError: Object #<Object> has no method 'request']

So I'm using MeteorJS w/the twit Node module to access the screen name of a tweet. Still just testing the code to see if I can retrieve the JSON from twitter.
Here is my code:
var Tget = Meteor.wrapAsync(T.get);
Meteor.methods({
'screenName' : function() {
try {
var result = Tget('search/tweets', {q:'#UCLA',count:1});
JSON.stringify(result);
console.log(result);
}
catch (e) {
console.log(e);
return false;
}
}
})
The error I'm receiving is :
[TypeError: Object #<Object> has no method 'request']
Here is the twit module git : https://github.com/ttezel/twit/blob/master/README.md
I think I understand. Here's the code of T.get:
Twitter.prototype.get = function (path, params, callback) {
return this.request('GET', path, params, callback)
}
As you can see, it expects this to have the method request. However, because we used wrapAsync without caring about the execution context (accessed with this), it fails.
Consider this example (you can copy/paste that in your browser console):
var obj = {
foo : 'foo',
logThis : function() {
console.log(this);
}
};
If we execute obj.logThis() we have: Object { foo: "foo", logThis: obj.logThis() }
But if we do the following...
var otherLogThis = obj.logThis;
otherLogThis();
It logs the Window object because we got the function out of its context!
How to solve that issue? Binding the function? Tricky call?
Nope, Meteor has the solution. wrapAsync can have two parameters... The second one is the context!
var Tget = Meteor.wrapAsync(T.get, T);
If you want to learn more about JavaScript contexts, I suggest this book :
https://github.com/getify/You-Dont-Know-JS/
It is free and open-source and I am not affiliated in any way other than my deepest affection and tender memories of feeling my brain growing in all kinds of funny ways when I read it.

Javascript object member function referred to globally not recognized in callback

I'm having a problem with the following Javascript code (Phonegap in Eclipse):
function FileStore(onsuccess, onfail){
//chain of Phonegap File API handlers to get certain directories
function onGetSupportDirectorySuccess(dir){
//stuff
onsuccess();
}
function getDirectory(dir){
return "something" + dir;
}
}
var onFileStoreOpened = function(){
if (window.file_store instanceof FileStore){
console.log('window.file_store is a FileStore');
console.log(window.file_store.getDirectory('something'));
}
}
var onDeviceReady = function(){
window.file_store = new FileStore(onFileStoreOpened, onFileStoreFailure);
}
Here, I want to do some things to initialize file services for the app, and then use them in my initialization from the callback. I get the following error messages in LogCat:
07-03 06:26:54.942: D/CordovaLog(223): file:///android_asset/www/index.html: Line 40 : window.file_store is a FileStore
07-03 06:26:55.053: D/CordovaLog(223): file:///android_asset/www/cordova-1.8.1.js: Line 254 : Error in success callback: File7 = TypeError: Result of expression 'window.file_store.getDirectory' [undefined] is not a function.
After moving the code around and stripping out everything in getDirectory() to make sure it was valid, I'm not even sure I understand the error message, which suggested to me that getDirectory() is not seen as a member function of window.file_store, even though window.file_store is recognized as a FileStore object. That makes no sense to me, so I guess that interpretation is incorrect. Any enlightenment will be greatly appreciated.
I've since tried the following:
window.file_store = {
app_data_dir : null,
Init: function(onsuccess, onfail){
//chain of Phonegap File API handlers to get directories
function onGetSupportDirectorySuccess(dir){
window.file_store.app_data_dir = dir;
console.log("opened dir " + dir.name);
onsuccess();
}
},
GetDirectory : function(){
return window.file_store.app_data_dir; //simplified
}
}
var onFileStoreOpened = function(){
var docs = window.file_store.getDirectory();
console.log('APPDATA: ' + docs.fullPath);
}
var onDeviceReady = function() {
window.file_store.Init(onFileStoreOpened, onFileStoreFailure);
}
and I get
D/CordovaLog(224): file:///android_asset/www/base/device.js: Line 81 : opened dir AppData
D/CordovaLog(224): file:///android_asset/www/cordova-1.8.1.js: Line 254 : Error in success callback: File7 = TypeError: Result of expression 'docs' [null] is not an object.
All I want to do here is make sure certain directories exist (I've removed all but one) when I start, save the directory object for future use, and then retrieve and use it after all initialization is done, and I don't want everything in the global namespace. Of course I would like to be able to use specific instances when necessary, and I'm disturbed that I can't make it work that way since it demonstrates there is a problem with my understanding, but I can't even get this to work with a single, global one. Is this a Javascript problem or a Phonegap problem?
As it stands, your getDirectory function is a private function within FileStore. If you wanted to make it a 'member' or 'property' of FileStore, you would need to alter it a little within FileStore to make it like this:
this.getDirectory = function(dir){ };
or leave it how it is and then set a property....
this.getDirectory = getDirectory();
this way when new FileStore is called it will have getDirectory as a property because the 'this' keyword is always returned when calling a function with 'new'
Hope this quick answer helps. There's lots of stuff on the goog about constructor functions.
You understand it correctly. The getDirectory as it stands is a private function and cannot be called using the file_store instance.
Try this in the browser.
function FileStore(onsuccess, onfail){
function onGetSupportDirectorySuccess(dir){
//stuff
onsuccess();
}
this.getDirectory = function (dir){
return "something" + dir;
}
}
window.file_store = new FileStore('', ''); //the empty strings are just placeholders.
if (window.file_store instanceof FileStore){
console.log('window.file_store is a FileStore');
console.log(window.file_store.getDirectory('something'));
}
This will prove that the basic js code is working fine. If there still is a problem while using it in PhoneGap, comment.

Detecting console.log() calls

I'm trying to write a test case for a debugging method that writes messages to the JavaScript console using console.log(). The test has to check that the message has been successfully written to the console. I'm using jQuery.
Is there a way to attach a hook to console.log() or otherwise check that a message has been written to the console, or any other suggestions on how to write the test case?
console.log doesn't keep a record of messages that are logged, or emit any events that you could listen for. It's not possible for your tests to directly verify its output from JavaScript. Instead, your test code will need to replace console.log with a mock implementation that does keep track of log messages for later verification.
Mocking is a common feature supported by most JavaScript test frameworks. For example, the Jest test framework provides a jest.spyOn function which replaces a given method with a mock implementation that records the arguments for each call in a .mock property before passing them on to the original implementation. After each test you may want to call jest.clearAllMocks() to reset the recorded argument lists for the next test, or use the equivalent clearMocks: true config option.
function saySomething() {
console.log("Hello World");
}
jest.spyOn(console, 'log');
test("saySomething says hello", () => {
expect(console.log.mock.calls.length).toBe(0);
saySomething();
expect(console.log.mock.calls.length).toBe(1);
expect(console.log.mock.calls[0][0]).toBe("Hello World");
});
afterEach(() => {
jest.clearAllMocks();
});
If you're not using a test framework (you probably should), you can create a simple mock yourself.
function saySomething() {
console.log("Hello World");
}
function testSomething() {
// Replace console.log with stub implementation.
const originalLog = console.log;
const calls = [];
console.log = (...args) => {
calls.push(args);
originalLog(...args);
};
try {
console.assert(calls.length == 0);
saySomething();
console.assert(calls.length == 1);
console.assert(calls[0][0] == "Hello World");
} catch (error) {
console.error(error);
} finally {
// Restore original implementation after testing.
console.log = originalLog;
}
}
So not bad solutions, but if you're looking for a high powered logger try Paul Irish's log()
If that's too high powered, you can get by with something like this.
var console = window.console,
_log = console ? console.log : function(){};
_log.history = [];
console.log = function( ){
_log.history.push.apply( _log.history, arguments );
_log.apply( console, arguments );
}
Usage
console.log('I','have','an','important','message');
//Use native one instead
_log.call( console, _log.history );
http://jsfiddle.net/BeXdM/
If you're using Jasmine, it's dead simple:
it('is my test', function () {
spyOn(console, 'log');
// do your stuff that should log something
expect(console.log).toHaveBeenCalledWith('something');
});
Head to Jasmine docs for more info.
Just attach your own function to console.log.
On your page, after everything loads,
Before starting tests -
var originalLog = console.log;
console.log = function(msg){
alert('my .log hook received message - '+msg);
//add your logic here
}
After running tests, if necessary -
console.log = originalLog
Probably the easiest way out is to use the NPM package std-mocks.
From their documentation:
var stdMocks = require('std-mocks');
stdMocks.use();
process.stdout.write('ok');
console.log('log test\n');
stdMocks.restore();
var output = stdMocks.flush();
console.log(output.stdout); // ['ok', 'log test\n']
Note: make sure you stdMocks.restore() before your assertions so your test runner is still able to log information about failed assertions.

Categories

Resources