-
Notifications
You must be signed in to change notification settings - Fork 345
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
feat: Added reload from the terminal by pressing a key #746
Conversation
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.
Hi @saintsebastian,
I added an initial set of review comments about this PR,
most of it looks nice, but there are some changes needed (e.g. related to a flow failure workaround that is currently used in this version of the PR, the testing strategy).
Also, as a "note to self" (and to @kumar303), it seems that process.stdin.isTTY
has some issues on windows (e.g. like described in this nodejs issue nodejs/node#10238) and winpty looks like the most common suggested workaround to this issue (and so we will need to look into this a bit more deeply before landing this feature).
src/cmd/run.js
Outdated
|}; | ||
|
||
|
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.
please leave this empty line (the two empty lines are currently used as a separator between "sections" of the source file)
src/cmd/run.js
Outdated
export type WatcherCreatorFn = (params: WatcherCreatorParams) => Watchpack; | ||
|
||
export function defaultWatcherCreator( | ||
{ | ||
addonId, client, sourceDir, artifactsDir, | ||
onSourceChange = defaultSourceWatcher, | ||
desktopNotifications = defaultDesktopNotifications, | ||
reloader = reloadFn, |
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 would probably prefer a slightly more descriptive name (e.g. how about addonReload
?)
src/cmd/run.js
Outdated
@@ -256,6 +273,19 @@ export default async function run( | |||
); | |||
} | |||
|
|||
readline.emitKeypressEvents(process.stdin); | |||
if (process.stdin.isTTY) { |
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 think that it would make sense to do not register the process.stdin.on('keypress')
event listener if the process.stdin
is not a tty.
src/cmd/run.js
Outdated
@@ -256,6 +273,19 @@ export default async function run( | |||
); | |||
} | |||
|
|||
readline.emitKeypressEvents(process.stdin); | |||
if (process.stdin.isTTY) { | |||
log.info('Press R to reload'); |
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 should also let the user know that we also support a keyboard shortcut for exiting the tool. (e.g. Press R to reload (and Ctrl-C to quit)
);
tests/unit/test-cmd/test.run.js
Outdated
|
||
it('exits when user presses CTRL+C in shell console', () => { | ||
const cmd = prepareRun(); | ||
const exits = sinon.stub(process, 'exit', sinon.spy(() => {})); |
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.
stubbing process.exit
concerns me a bit, I know that it is supposed to be restored by the after hook but I would prefer if we could avoid this.
e.g. There is nothing in our code that currently exit in a arbitrary part of the sources using process.exit
, it can be probably reasonable to solve this by:
- creating a new utility method (e.g.
exitProgram
orexitWebExt
) that incapsulate the strategy that we are going to use to exit the application (and it should print a log message to let the user know that web-ext is exiting because the user has requested it to) - inject the new utility method in the command run entrypoint function, by setting its default value to the utility method defined above
- override the new utility method in the tests using sinon.spy
I'm also wondering if process.exit()
is the best strategy to exit the application.
src/cmd/run.js
Outdated
readline.emitKeypressEvents(process.stdin); | ||
if (process.stdin.isTTY) { | ||
log.info('Press R to reload'); | ||
(process.stdin: any).setRawMode(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 (process.stdin: any).setRawMode(...)
looks like something that is hiding a flow validation error, am I right?
we should try to fix the flow validation error properly (and not by using the any
type workaround which is too broad), e.g. it seems that the issue could be that flow is not able to know if process.stdin is going to be a tty.ReadStream (https://nodejs.org/api/tty.html#tty_class_tty_readstream), and so my guess is that something like this should make flow happy:
if (process.stdin instanceof tty.ReadStream) {
process.stdin.setRawMode(true);
}
src/cmd/run.js
Outdated
@@ -256,6 +273,19 @@ export default async function run( | |||
); | |||
} | |||
|
|||
readline.emitKeypressEvents(process.stdin); |
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 would be probably better moved inside the if (process.stdin.isTTY) { ... }
below.
@rpl the issue you've mentioned may cause troubles, is there some way (automated testing, virtual machine) i can use to make sure this feature actually works outside of my system? |
@saintsebastian I just tried on a Windows7 system and it worked correctly (with the interactive reload and exit on Ctrl-C as on OSX and Linux) with both cmd and ConEmu. @saintsebastian currently there are conflicts with master (related to PR #753 landed recently), do you mind to merge the updated master into your branch to resolve the conflicts? |
12b8c4f
to
0b1c2f8
Compare
@kumar303 this PR is now ready for a final look: @saintsebastian Thank you so much for all the passion, hard work (and patience :-)) that you have put in it during the past reviews on this PR! 👍 |
@rpl this was lots of fun! thanks for sticking with me all along! |
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.
Hi @saintsebastian, thanks for your patience :) I got caught up in deploying the new addons.mozilla.org
mobile site but I have a bit more time now.
I requested some architectural changes but didn't get a chance to fully review the tests. When you're ready for a re-review, just select my name in the box so that I see the notification.
src/cmd/run.js
Outdated
}: ReloadParams | ||
): Promise<void> { | ||
log.debug(`Reloading add-on ID ${addonId}`); | ||
return client.reloadAddon(addonId) |
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.
Is there any reason why this is using promise code instead of async/await code? I think it would read better as an async function with a try/catch.
src/cmd/run.js
Outdated
|}; | ||
|
||
export type CmdRunOptions = {| | ||
firefoxApp: typeof defaultFirefoxApp, | ||
firefoxClient: typeof defaultFirefoxClient, | ||
reloadStrategy: typeof defaultReloadStrategy, | ||
addonReload: typeof defaultAddonReload, | ||
AddonRunner: typeof ExtensionRunner |
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: we typically use trailing commas to make refactoring easier but eslint doesn't enforce it in flow annotations.
src/cmd/run.js
Outdated
|}; | ||
|
||
export default async function run( | ||
{ | ||
sourceDir, artifactsDir, firefox, firefoxProfile, | ||
keepProfileChanges = false, preInstall = false, noReload = false, | ||
browserConsole = false, customPrefs, startUrl, ignoreFiles, | ||
stdin = process.stdin, |
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 command handler functions, all of these parameters should correlate to command line options. Thus, stdin
should be moved a couple lines down to the CmdRunOptions
object.
src/cmd/run.js
Outdated
|}; | ||
|
||
export type CmdRunOptions = {| | ||
firefoxApp: typeof defaultFirefoxApp, | ||
firefoxClient: typeof defaultFirefoxClient, | ||
reloadStrategy: typeof defaultReloadStrategy, | ||
addonReload: typeof defaultAddonReload, | ||
AddonRunner: typeof ExtensionRunner |
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 about calling this ExtensionRunnerClass
? I know the word "addon" has crept into the code quite a bit but I've been trying to call them extensions for our theoretical and optimistic future compatibility with Chrome :-)
src/cmd/run.js
Outdated
}: CmdRunParams, | ||
{ | ||
firefoxApp = defaultFirefoxApp, | ||
firefoxClient = defaultFirefoxClient, | ||
reloadStrategy = defaultReloadStrategy, | ||
addonReload = defaultAddonReload, |
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 think it will be easier to move the new stdin loop logic to defaultReloadStrategy
since that already is doing something very similar which is starting a loop to watch for source file changes.
Here is a quick patch to illustrate what I mean:
diff --git a/src/cmd/run.js b/src/cmd/run.js
index cdeab34..a524a25 100644
--- a/src/cmd/run.js
+++ b/src/cmd/run.js
@@ -46,17 +46,16 @@ export type WatcherCreatorParams = {|
onSourceChange?: OnSourceChangeFn,
ignoreFiles?: Array<string>,
createFileFilter?: FileFilterCreatorFn,
- addonReload?: typeof defaultAddonReload,
+ addonReload: typeof defaultAddonReload,
|};
export type WatcherCreatorFn = (params: WatcherCreatorParams) => Watchpack;
export function defaultWatcherCreator(
{
- addonId, client, sourceDir, artifactsDir, ignoreFiles,
+ addonId, addonReload, client, sourceDir, artifactsDir, ignoreFiles,
onSourceChange = defaultSourceWatcher,
createFileFilter = defaultFileFilterCreator,
- addonReload = defaultAddonReload,
}: WatcherCreatorParams
): Watchpack {
const fileFilter = createFileFilter(
@@ -109,6 +108,7 @@ export type ReloadStrategyParams = {|
|};
export type ReloadStrategyOptions = {|
+ addonReload?: typeof defaultAddonReload,
createWatcher?: WatcherCreatorFn,
createFileFilter?: FileFilterCreatorFn,
|};
@@ -119,18 +119,20 @@ export function defaultReloadStrategy(
sourceDir, artifactsDir, ignoreFiles,
}: ReloadStrategyParams,
{
+ addonReload = defaultAddonReload,
createWatcher = defaultWatcherCreator,
}: ReloadStrategyOptions = {}
): void {
- const watcher: Watchpack = (
- createWatcher({addonId, client, sourceDir, artifactsDir, ignoreFiles})
- );
+ const watcher: Watchpack = createWatcher({
+ addonId, addonReload, client, sourceDir, artifactsDir, ignoreFiles,
+ });
firefoxProcess.on('close', () => {
client.disconnect();
watcher.close();
});
+ // Do the while (!userExit) loop right here...
}
src/cmd/run.js
Outdated
Promise.resolve().then(async function() { | ||
log.info('Press R to reload (and Ctrl-C to quit)'); | ||
|
||
let userExit = false; |
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.
To expand on my comment above about moving this logic to defaultReloadStrategy
, you're going to need to handle the case where Firefox quits. In other words, try these steps:
- Execute
web-ext run
- Let Firefox open up
- Go to File > Quit in the Firefox application to close it
After this, web-ext
should stop running the while loop to check stdin
for keyboard input but currently it does not. If you move your logic to defaultReloadStrategy
then you can hook something into the already existing close event:
firefoxProcess.on('close', () => {
client.disconnect();
watcher.close();
// stop polling stdin...
});
@kumar303 hey Kumar! i think this is ready for further review |
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 for the changes. The new architecture looks good, here are a few requests.
tests/unit/test-cmd/test.run.js
Outdated
} | ||
|
||
function prepare() { | ||
const client = new RemoteFirefox(fakeFirefoxClient()); | ||
const watcher = { | ||
close: sinon.spy(() => {}), | ||
}; | ||
const fakeStdin = new tty.ReadStream(); |
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'd like to see the default fakeStdin
set to something that has stdin.isTTY == false
because that will prevent the while loop from getting triggered on every test of this function. For the tests that need to trigger the while loop, you can just pass in the tty.ReadStream()
object as an explicit option.
|
||
let userExit = false; | ||
|
||
while (!userExit) { |
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.
Some quick console logging tells me that this while loop never exits in about four tests. Since the test suite itself runs in a loop (when files change), this will produce a memory leak but perhaps it's insignificant. I mentioned below that you can fix some defaults to prevent most tests from entering this while loop. Maybe that change will be enough. If it's not too difficult, could you make sure that every test causes this while loop to exit?
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.
@Kumar thanks for catching this, indeed defaults prevent the loop from starting, so it only happens in two tests, in one it doesn't exit since it is waiting for possible input after reload in another it exits on ctrl+c. Hope I understood your correctly and implemented this the way you imagined
|
||
if (keyPressed.ctrl && keyPressed.name === 'c') { | ||
userExit = true; | ||
} else if (keyPressed.name === 'r' && addonId) { |
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 think it would be nice to log a debug message here so that we know the reload was triggered by a key press
tests/unit/test.program.js
Outdated
@@ -280,7 +285,7 @@ describe('program.main', () => { | |||
getVersion: () => 'not-a-real-version', | |||
checkForUpdates: spy(), | |||
shouldExitProgram: false, | |||
systemProcess: fake(process), | |||
systemProcess: createFakeProcess, |
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 think was supposed to be createFakeProcess()
, right?
@kumar303 thanks for catching all these things! |
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, this looks great. I only have some minor cleanup requests.
src/cmd/run.js
Outdated
if (keyPressed.ctrl && keyPressed.name === 'c') { | ||
userExit = true; | ||
} else if (keyPressed.name === 'r' && addonId) { | ||
log.info('\nReloading extension on user request'); |
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 be log.debug()
because we only want it to be printed in --verbose
mode. Since we don't reset the cursor in verbose mode, you won't need a \n
(new line) in the message.
stdin.setRawMode(true); | ||
|
||
// NOTE: this `Promise.resolve().then(...)` is basically used to spawn a "co-routine" that is executed | ||
// before the callback attached to the Promise returned by this function (and it allows the `run` 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.
I think it's no longer accurate to say this is "executed before the callback attached to the Promise returned by this function..." (since the function no longer returns a promise, right?) -- could you update the comment? It's important to get this comment right because I had to read it several times to understand what this code is doing :)
tests/unit/test-cmd/test.run.js
Outdated
createWatcher: sinon.spy(() => watcher), | ||
stdin: fakeStdin, |
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 could just be: stdin: new stream.Readable(),
@kumar303 thanks for such a fast review! Hopefully most of the work is done and I can replicate what I've learnt here in other PRs. |
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 do it!
Fixes #705
Would love to here ideas about testing, my idea is mocking stdin, but I am open to suggestions.