New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[fix]: Removed synchronous operation with file system. #879
Conversation
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed, please reply here (e.g.
|
I signed it! |
CLAs look good, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
lib/Page.js
Outdated
@@ -26,6 +26,12 @@ const {Keyboard, Mouse, Touchscreen} = require('./Input'); | |||
const Tracing = require('./Tracing'); | |||
const {helper, debugError} = require('./helper'); | |||
|
|||
function writeFileAsync(...options) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aslushnikov , should this be added in helper? I could use it in #881.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it seems to me, in a certain file/folder "utils" or "helpers" it is necessary to make the function promisify (as here) and use it to create wrappers of the fs methods or some other in local module scope (as here). Or we can add some external dependency, such as Bluebird or pify.
IMHO, I, as a person external to the project, can not make such decisions. And I do not want to rewrite the code, so let's work out solution and I will implement it in this PR. This is almost my first PR to open source project, so I may not know some rules, sorry =)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CheerlessCloud let's add both promisify
and writeFileAsync
into helper.
Hey @CheerlessCloud, thank you for the pull request!
I'd be happy to review PRs. |
Do not you think that the file "helper" for 300+ lines is antipattern? What about separate his to files, single file for one method? Yes, this does not completely solve the problem, but it will make it a little easier. |
13eaf6d
to
fc34f65
Compare
I think its fine. |
lib/Launcher.js
Outdated
@@ -122,7 +127,7 @@ class Launcher { | |||
process.kill(-chromeProcess.pid, 'SIGKILL'); | |||
// Attempt to remove temporary profile directory to avoid littering. | |||
try { | |||
removeSync(temporaryUserDataDir); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should stay sync: handlers for process exit
are expected to complete synchronously.
lib/Tracing.js
Outdated
const {helper} = require('./helper'); | ||
const fs = require('fs'); | ||
|
||
fs.openAsync = helper.promisify(fs.open); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's not spoil the globals
const openAsync = helper.promisify(fs.open);
const writeAsync = helper.promisify(fs.write);
const closeAsync = helper.promisify(fs.close);
lib/Launcher.js
Outdated
@@ -15,16 +15,18 @@ | |||
*/ | |||
const os = require('os'); | |||
const path = require('path'); | |||
const removeSync = require('rimraf').sync; | |||
const {helper} = require('./helper'); | |||
const rimrafAsync = helper.promisify(require('rimraf')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's name this removeAsync
: the rimraf
doesn't say much of what the function is doing.
lib/Launcher.js
Outdated
const ChromiumRevision = require('../package.json').puppeteer.chromium_revision; | ||
|
||
fs.mkdtempAsync = helper.promisify(fs.mkdtemp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's not spoil the global:
const mkdtempAsync = helper.promisify(fs.mkdtemp);
lib/helper.js
Outdated
* const fs = Helper.promisifyAll(require('fs')); | ||
* fs.writeFileAsync(...); // -> Promise | ||
*/ | ||
static promisifyAll(object) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't need this method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I can remove it, but it may be needed in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, we'll add it if/when the need arises.
lib/helper.js
Outdated
@@ -225,6 +226,86 @@ class Helper { | |||
static isNumber(obj) { | |||
return typeof obj === 'number' || obj instanceof Number; | |||
} | |||
|
|||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's not fallback to util
- the helper.promisify
implementation is simple enough for our use case. It's easier to read and maintain a single code path.
/**
* @param {function(?)} nodeFunction
* @return {function(?):!Promise<?>}
*/
static promisify(nodeFunction) {
/**
* @param {!Array<?>} options
* @return {!Promise<?>}
*/
return function(...options) {
return new Promise(function(fulfill, reject) {
options.push(callback);
nodeFunction.call(null, ...options);
function callback(err, result) {
if (err)
reject(err);
else
fulfill(result);
}
});
};
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "not fallback to util" you mean using util.promisify.custom symbol?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, and also the util.promisify
method.
I'd just stick with the simple shim we already have somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use https://npmjs.com/util.promisify if you want a compatible shim.
lib/helper.js
Outdated
@@ -225,6 +226,86 @@ class Helper { | |||
static isNumber(obj) { | |||
return typeof obj === 'number' || obj instanceof Number; | |||
} | |||
|
|||
/** | |||
* @static |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't use @static
, @throws
and @example
declarations. They are harmless, but I'd prefer them to be removed for consistency with the rest of the code.
lib/Launcher.js
Outdated
@@ -90,8 +92,11 @@ class Launcher { | |||
chromeProcess.once('close', () => { | |||
// Cleanup as processes exit. | |||
if (temporaryUserDataDir) | |||
removeSync(temporaryUserDataDir); | |||
fulfill(); | |||
rimrafAsync(temporaryUserDataDir) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: braces are required around multi-line branches:
if (temporaryUserDataDir) {
rimrafAsync(..)
..
....
} else {
fulfill();
}
lib/Launcher.js
Outdated
fulfill(); | ||
rimrafAsync(temporaryUserDataDir) | ||
.then(() => fulfill()) | ||
.catch(err => console.error(err)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's reject the waitForChromeToClose
promise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a few cases we don't catch errors from this promise, so we can get an unhandledRejection.
…m and delete promisifyAll method.
Rename rimrafAsync to removeFolderAsync. Place mkdtempAsync locally. Use removeFolder.sync in killChrome function.
👍 |
@@ -86,12 +89,16 @@ class Launcher { | |||
chromeProcess.stderr.pipe(process.stderr); | |||
} | |||
|
|||
const waitForChromeToClose = new Promise(fulfill => { | |||
const waitForChromeToClose = new Promise((fulfill, reject) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't mix new Promise
, x.then
and async/await
. Convert callback apis into promises ASAP, then switch back to the highest abstraction.
const waitForCloseEvent = () => new Promise(resolve => chromeProcess.once('close', resolve);
const waitForChromeToClose = (async () => {
await waitForCloseEvent();
if (temporaryUserDateDir) {
try {
await removeFolderAsync(temporaryUserDataDir);
} catch(e) {
console.error(e); // are you sure, what's wrong with letting it throw?
}
}
})();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert I don't see much benefits in the approach you suggested, nor it is easier for me to read. I'd say the current code is just good enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aslushnikov I disagree, new Promise should not be mixed with x.then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert I might be misunderstanding something. Lets split this discussion out of this thread: would you mind sending a separate PR that does changes that you find important and explains why are they necessary?
@@ -585,7 +587,7 @@ class Page extends EventEmitter { | |||
|
|||
const buffer = new Buffer(result.data, 'base64'); | |||
if (options.path) | |||
fs.writeFileSync(options.path, buffer); | |||
await writeFileAsync(options.path, buffer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there even an option to write the file here? Either puppeteer should return the actual stream or users should just write the buffer themselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert two reasons:
- it comes handy from user standpoint: the majority of
page.screenshot
usages havepath
option in it. - it's cheap from the project standpoint.
lib/helper.js
Outdated
* @param {function(?)} nodeFunction | ||
* @return {function(?):!Promise<?>} | ||
*/ | ||
static promisify(nodeFunction) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not use @ljharb's wonderful https://www.npmjs.com/package/util.promisify
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert no need to pull in a dependency for 30 lines of code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when it's reliable and faster in modern node, it's always better to have more deps :-)
if (temporaryUserDataDir) { | ||
removeFolderAsync(temporaryUserDataDir) | ||
.then(() => fulfill()) | ||
.catch(err => console.error(err)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just let this throw, like removeSync would have done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert to avoid unhandledPromiseRejection
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
presumably people are going to await launch();
and thus catch any errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How's Page.pdf
related to this code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aslushnikov ah you're right. Edited comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @CheerlessCloud for working on this. I left a few minor comments before we can merge this.
@@ -86,12 +89,16 @@ class Launcher { | |||
chromeProcess.stderr.pipe(process.stderr); | |||
} | |||
|
|||
const waitForChromeToClose = new Promise(fulfill => { | |||
const waitForChromeToClose = new Promise((fulfill, reject) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: reject
is not used
@@ -86,12 +89,16 @@ class Launcher { | |||
chromeProcess.stderr.pipe(process.stderr); | |||
} | |||
|
|||
const waitForChromeToClose = new Promise(fulfill => { | |||
const waitForChromeToClose = new Promise((fulfill, reject) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert I don't see much benefits in the approach you suggested, nor it is easier for me to read. I'd say the current code is just good enough.
@@ -585,7 +587,7 @@ class Page extends EventEmitter { | |||
|
|||
const buffer = new Buffer(result.data, 'base64'); | |||
if (options.path) | |||
fs.writeFileSync(options.path, buffer); | |||
await writeFileAsync(options.path, buffer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert two reasons:
- it comes handy from user standpoint: the majority of
page.screenshot
usages havepath
option in it. - it's cheap from the project standpoint.
lib/helper.js
Outdated
* @param {function(?)} nodeFunction | ||
* @return {function(?):!Promise<?>} | ||
*/ | ||
static promisify(nodeFunction) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert no need to pull in a dependency for 30 lines of code.
lib/helper.js
Outdated
if (typeof nodeFunction !== 'function') | ||
throw new TypeError('A function is expected'); | ||
|
||
// Because native util.promisify differently work with arguments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's drop the comment
lib/helper.js
Outdated
}); | ||
} | ||
|
||
promisified.__isPromisified__ = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is never used - let's drop it.
lib/helper.js
Outdated
if (err) | ||
return reject(err); | ||
|
||
// If callback invoked with single argument, we return in in Promise directly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's drop the comment, the code is self explanatory
lib/helper.js
Outdated
* @return {function(?):!Promise<?>} | ||
*/ | ||
static promisify(nodeFunction) { | ||
if (typeof nodeFunction !== 'function') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's drop the check - the API is for internal use only.
if (temporaryUserDataDir) { | ||
removeFolderAsync(temporaryUserDataDir) | ||
.then(() => fulfill()) | ||
.catch(err => console.error(err)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert to avoid unhandledPromiseRejection
.
removeSync(temporaryUserDataDir); | ||
fulfill(); | ||
if (temporaryUserDataDir) { | ||
removeFolderAsync(temporaryUserDataDir) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CheerlessCloud I've seen your comment that the promise is not not catched in a few places, thanks for the clarification.
let's fulfill regardless of the temprorary data directory cleanup, and use debugError
instead of console.error
:
if (temporaryUserDataDir) {
removeFolderAsync(temporaryUserDataDir)
.catch(debugError)
.then(fulfill);
} else {
fulfill();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to be able to try/catch the error that removing the temporaryUserDataDir throws. You should await it and throw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@graingert what are you going to do with the exception? The temporaryUserDataDir
is internal to launcher's implementation.
removeSync(temporaryUserDataDir); | ||
fulfill(); | ||
if (temporaryUserDataDir) { | ||
removeFolderAsync(temporaryUserDataDir) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to be able to try/catch the error that removing the temporaryUserDataDir throws. You should await it and throw.
@CheerlessCloud any updates on this? I'm very interested in this going in. |
@aslushnikov Sorry, I've been busy the last few days. I'll try to finish it in the near future) |
Hey @CheerlessCloud! I see you're pressed on time. I'd like to add a few commits to this PR to address all the comments and merge this in. Let me know if this would work for you. |
I failed to come up with a proper jsdoc for the method. @JoelEinbinder.
This patch: - introduces `helper.promisify` - a simple polyfill for the `util.promisify`. The `util.promisify` could not be used due to Node6 compatibility issues. - migrates all sync filesystem operations to the async replicas Fixes puppeteer#884.
I'm completely discouraged by this code. @aslushnikov How could you write such a thing?
I would somehow understand this solution if the code were written on callbacks. But you use the async
function! And inside the asynchronous function you are happy to block the EventLoop. What for?
If someone tells me where to put this "promisify", I'll move it without problems. But still I recommended somehow to solve this problem in the near future.
The remaining cases use synchronous writing and reading are not as critical, but it is advisable to fix them too: 1, 2.