Deploy and Integrate Rich Implementation#755
Conversation
7e1400a to
0e97ba6
Compare
0d04a04 to
1b77f78
Compare
|
hmm.....I had initially considered abstracting my attempts to standardize some of the tool management code flow in something like |
freakboy3742
left a comment
There was a problem hiding this comment.
On the whole, this looks really good. +1 to:
- The refactoring of uninstall/upgrade commands
- The rationalization of the "when do we output a blank line?" question. Using
prefixoutput as a marker for a "new context" is an easy to explain and easy to follow rationale, and it results in a very elegant output 👏 - The "waitbar safety catch" on
subprocess.run() - Consistency in simulator startup messaging.
I've flagged a couple of fairly minor tweaks inline; the only thing that I'm on the fence about is some of the increased usage of waitbar.
I'm broadly in support of using waitbar more liberally, and 80% of the changes you've made to that end make perfect sense. Anywhere that involves network access is a no-brainer for a waitbar; anything involving non-trivial I/O operations (e.g., unpacking a potentially large zip file) is in the same category.
However, I question whether it's worth it for some of the smaller operations. The one that really made this obvious was the linuxdeploy chmod check - that's literally a single OS filesystem call. I can't think of any circumstance (short of a catastrophic disk problem) where that call will be anything more than microseconds.
I'm a little more sympathetic to cases where it's a small collection of low impact OS calls - e.g., "installing source" is a check for an existing directory, a mkdir and a copy; bundling them into a single waitbar makes a little more sense, even though each individual operation is unlikely to be slow.
I guess what I'd really like to get to is a similar sort of place that this PR has gotten to with respect to "when should I call info() to clear the console?". If we can determine a clear policy regarding when/how we use a waitbar, then it will be a lot easier to apply that policy consistently in future. My first draft of this is effectively summarizing what I've said above - i.e., Use a waitbar if:
- There's any network access involved in the operation
- There's a disk I/O operation involved that may take an undetermined amount of time
- There's a collection of smaller operations that are thematically linked, each of which is unlikely to take any noticeable duration, but can be gathered together thematically.
If you've got any thoughts/counterarguments on this, I'd love to hear them.
| self.logger.info("Building MSI...", prefix=app.app_name) | ||
|
|
||
| try: | ||
| self.logger.info() |
There was a problem hiding this comment.
On a theme of consistency - are these logger.info()s still needed?
There was a problem hiding this comment.
I actually struggled with these empty info() calls.....because at least one of them always results in output to the terminal.
Here's the terminal's final state without the info() calls:
(venv-3.9) PS C:\Users\Russell\tmp\beeware-tutorial\helloworld> briefcase package
[helloworld] Building MSI...
Compiling application manifest... done
helloworld.wxs
helloworld-manifest.wxs
Compiling application installer... done
Linking application installer... done
[helloworld] Packaged windows\Hello World-0.0.1.msi
That print out concerning wxs files is related to installer compilation....but it appears related to manifest compilation.
I talk about this more in my comment.
9b3bbc5 to
d4d9e5d
Compare
|
The more I think about it, the more I realize that "when and for what do i use a wait bar?" is really a sub-question to "what is the policy for communicating with the user?". (Through yet another monologue of my mine :)) I propose some shifts in how logging should work. A digression first though... I say this because I think it should have important implications for how the Wait Bar is used. And furthermore, how regular logging occurs. In this PR, I initially just replaced every log that contained ellipsis with a Wait Bar...since that log was apparently indicating that something was about to happen (and it may even take some time). However, with the revelation above about "final state" of a screen for users, I think the better approach is considering when to tell the user about something. Therefore, I propose:
Now, as for a broad logging policy:
Notes:
Please let me know what I didn't explain very well. If we like this plan, I still need to implement it in this PR....I can probably do that tomorrow. |
+1 for the issues you're thinking about here - you've definitely got the right concerns in mind.
A qualified maybe on this one. The concern I have is how the log reads when an error occurs. The advantage to telling someone you're about to do something is that when something breaks, the last "info" line in the logs is the thing that you were trying to do: If log messages are "post event", the error message needs to encompass what was being done. I have a mild preference for the former, mostly because in the latter form, the error on thing 3 comes as a surprise - I didn't even know thing 3 was being done, and now it's failed! We also need to differentiate between "interstitial" logs about specific tasks, and the "significant task with a prefix that define a context" - these definitely need to be "pre" not "post".
+1; and the message should encompass the thing being done (i.e., `message="Doing thing...") so that the in-progress output is: And the final output is:
I'd tweak this slightly:
I'd also keep the smaller size of the wait bar; this accommodates for longer "X" descriptions, and also means there is only one "throb" pulsing at any one time. If you had a command that produces 2 lines of output, this would yield the following as in-progress output (after 1 line had been generated): And final output of:
Agreed that "it depends" is the best guidance; if possible, it would be desirable to avoid: or worse: i.e., Ideally the "thing done" line wouldn't be output at all in the case of an error. Also - "error" in this sense is only "unrecoverable error"; there's a couple of situations where an error is expected, and can be handled in a way that allows processing to continue.
+1
+1
+1 except for the discussion points I've raised above.
+1; although, it's worth noting that in some cases, the fix here may be wrapping N micro-waitbars into a single waitbar. For example, Linuxdeploy post processing involves a bunch of small operations that almost never fail, produce no output except in case of a very rare error, and take almost no time. Wrapping all those operations into a single waitbar with a "post-processing binary..." message seems totally appropriate. In that type of situation, a waitbar effectively becomes a good way to decompose individual, largely reliable single operations into a "subtask" inside a context. Interestingly, if we take this approach of grouping reliable minor tasks into waitbars, the question about what to do with standalone
Definitely clear. Also - please consider the opinions I've voiced here "strong opinions, weakly held"; if you think what I've described is either technically infeasible, or will result in a bad user experience, I'm more than happy to reconsider. |
Yeah...this is a bit my concern as well since by the time we're logging something, we're already in an error state, so we need to explain what we were doing and how/why it failed. However, given how much logging will be encompassed in " to: In this way, the original message saying something is about to happens becomes a success message....or a failure message and then the explanation for why it failed. Nonetheless, I don't really want to force this.....so, I'll play around with it and see what makes sense....and just how many "subtasks" would be affected by this strategy.
Absolutely. This also helps ensures there is already context to what's going on when/if a subtask fails.
If the error manifests as an Exception, then the Wait Bar itself, at least, will not print a "done" message. But agreed...we should limit the "done" messages to subtasks that complete successfully.
Agreed. That linuxdeploy example is a good one since it could have output but is very unlikely to since its unlikely to fail....so just wrapping it all in a simple Wait Bar definitely makes sense. I think we're largely on the same page, though....unless you have particular reservations about my post-event logging for the more trivial subtasks. I will pursue a commit tomorrow so our discussion can get a bit more concrete. |
Agreed that we seem to be on the same page; Any reservations I have about post-event logging will be a lot more productive to discuss as specific examples rather than in the abstract. |
- Support subprocess.run calls within a Wait Bar - Implement liberal use of Wait Bar - Apply consistency of messages to users irrepective of workflow
fb8a1fd to
b1d3aee
Compare
| # Check for a device skin. If it doesn't exist, download it. | ||
| skin_path = self.root_path / "skins" / skin | ||
| if skin_path.exists(): | ||
| self.command.logger.info(f"Device skin '{skin}' already exists") |
There was a problem hiding this comment.
This message never seemed particularly valuable to me...
|
So, after reviewing all the calls to Perspective Pre/Post-Event Logging Wait Bar Use Semantics Finally, all of my intended code changes are in....however, I have to run today and cannot run through more thorough manual testing; I'll do that this weekend. You're welcome to comment on the code, though, before then. |
- Miscellaneous fixes - Sign app in Progress Bar instead of Wait Bar - Standardize logging and Wait Bar usage
b1d3aee to
bde0524
Compare
Codecov Report
|
| # Signal that Rich is dynamically controlling the terminal output. | ||
| # Therefore, all output must be printed to the screen by Rich to | ||
| # prevent corruption of dynamic elements like Wait Bars. | ||
| self.is_output_controlled = False |
There was a problem hiding this comment.
I decided to generalize the indication and handling of an active Wait Bar in to any active Rich content. However, "there are two hard things in computer science: cache invalidation and naming things". I struggled with a name for this flag that communicated what its actually indicating. Open to a better name.
There was a problem hiding this comment.
I believe the full quote is "2 hard problems; cache invalidation, naming things, and off-by-one errors"... 😛
I can't say an obviously better name is jumping out at me, so I'm happy to go with this.
|
This is ready for final review now. As a P.S.....one idea I kept coming back to to handle output chronology in a Wait Bar is changing the output semantics of the Wait Bar. Currently the Wait Bar will pin itself to the bottom of the terminal until the Wait Bar exits. Anything that's printed while it is active will inherently appear above the "message" and its "done" indication. Alternatively, the Wait Bar could print the "message" statically and then pin the pulsing bar to the bottom of the terminal. When the Wait Bar exits, a "done" message would be printed underneath the "message". In this way, any intervening output would be sandwiched between the "message" and "done". This is basically what I did manually for the |
freakboy3742
left a comment
There was a problem hiding this comment.
This is awesome. No notes. 💯
Regarding your final notes about other options for waitbar - I can see what you're saying about alternatives to how waitbar operates. My concern about having a top-pinned message is that you'll lose the context about what the "Done" is actually referring to. I actually found the way you've got it set up here to be helpful, because a long-running noisy compile (e.g., Linux AppImage builds) gives you the visual context of what is currently spewing output. I'm sure we could dream up a different visualisation that top pins the progress bar, but I'm also sure that would end up being a nightmare to implement and maintain, so I'm happy to go with what you've got here.
Once again, thanks for an awesome contribution. (It's just so pretty! 😍)
| # Signal that Rich is dynamically controlling the terminal output. | ||
| # Therefore, all output must be printed to the screen by Rich to | ||
| # prevent corruption of dynamic elements like Wait Bars. | ||
| self.is_output_controlled = False |
There was a problem hiding this comment.
I believe the full quote is "2 hard problems; cache invalidation, naming things, and off-by-one errors"... 😛
I can't say an obviously better name is jumping out at me, so I'm happy to go with this.
Follow up PR to the implementation of Rich in #740.
Wide Use of Wait Bar
These changes implement the Wait Bar in many places....it might be argued too many. However, I defend using Wait Bar this prolifically because it creates a more consistent user experience with active feedback for ongoing tasks. While many of the tasks will not last long enough for the pulsing bar to even be visible, the consistent
donemessage after these tasks complete directly confers success to the user....especially when they are considered together with the much more long running tasks.Allow
subprocess.run()Without Redirected IO within Wait BarsA major limitation of the Wait Bar is wrapping a call to
subprocess.run()when IO hasn't been redirected (since Rich cannot accommodate dynamic updates to the screen while other processes are also writing to it). The current workaround is to pipe output back out and print it once the command finishes....but generally speaking the output is most useful in real time; for instance, a long running process with important information presented when it starts.These changes allow callers to wrap
run()calls with a Wait Bar whether they redirect IO or not. This also allows callers to more arbitrarily wrap functions without regard to whether they callrun()or not. (Although, care does need to be taken to avoid a subsequent wait bar or progress bar being instantiated since only one can be active at a time.)If
run()detects a wait bar is active and IO is not redirected, then it will simulatesubprocess.run()by:stdoutwill always be pipedstderr, then it is redirected tostdoutPopensubprocess.run()doesAlternatively, if the caller properly redirects
stdoutandstderr, thensubprocess.run()is called normally. Although, usingcheck_outputis probably more appropriate at that point.There are a couple limitations:
runargumentstimeoutandinputare not implemented.run()without specifyingstdout/errwithin a Wait Bar and it works.stdout/err, then they should ensure it properly redirects IO.runif the shell command has its own robust output; e.g. building an APK.Printing Consistency
A lot of impetus for this PR was allowing
runin a Wait Bar for starting the iOS simulator....then I started comparing what's shown to a user when the iOS simulator starts versus the Android simulator. Then I got sucked in to a black hole of consistency in general among the different workflows. I ultimately ended up trying to create consistency among all of the workflows and even detailed all of the permutations for instantiating briefcase.Areas of Interest
Logwas done first...LogBriefcase Instantiation Permutations
PR Checklist: