Refactor and simplify the ConGetSet API#12247
Conversation
|
Note to reviewers: it will make things a lot easier if you disable whitespace before reviewing these changes. It may also help to review outputStream.cpp and getset.cpp side by side, since many of those changes are just moving code from the one file to the other. Also, if you prefer, I could actually split this up into two separate PRs, with the |
| void ConhostInternalGetSet::SetAutoWrapMode(const bool wrapAtEOL) | ||
| { | ||
| return NT_SUCCESS(DoSrvPrivateSetAutoWrapMode(wrapAtEOL)); | ||
| auto& outputMode = _io.GetActiveOutputBuffer().OutputMode; |
There was a problem hiding this comment.
This is the site of the #12194 fix, if you're following along from getset.cpp like I am.
|
I was going to get you this today, but my performance review stuff is due this week. So I have it half done and will hopefully finish on Monday. Thanks for your patience @j4james. I'm very excited by this. I feel like you keep taking on all the stuff I wish I had time to do of your own volition and it's amazing. |
miniksa
left a comment
There was a problem hiding this comment.
Overall, looks great. I figure all the things that are like "always returning true" will end up getting worked out into something more sensible as this refactoring continues into its next phases, so I didn't cry about it.
Just a few comments that you could take or leave... or things I noted out for other readers.
As always, @j4james, thank you for your contribution. I love seeing this get cleaner and cleaner!
| // - true if successful (see DoSrvPrivateSetScrollingRegion). false otherwise. | ||
| bool ConhostInternalGetSet::PrivateSetScrollingRegion(const SMALL_RECT& scrollMargins) | ||
| // - <none> | ||
| void ConhostInternalGetSet::SetScrollingRegion(const SMALL_RECT& scrollMargins) |
There was a problem hiding this comment.
It looks like the Top>Bottom check is gone now. Was that intentional?
There was a problem hiding this comment.
Yeah. All the validation is already handled in AdaptDispatch::_DoSetTopBottomScrollingMargins, since that's where the margins are really being managed. This method is just forwarding a copy to SCREEN_INFORMATION so they can be accessed from the AdjustCursorPosition function. I'm hoping one day we'll be able to avoid that though.
| coordDestination.Y = viewport.Top + 1; | ||
|
|
||
| // Note the revealed lines are filled with the standard erase attributes. | ||
| ScrollRegion(srScroll, srScroll, coordDestination, true); |
There was a problem hiding this comment.
Was about to ask where the wrapper went, but you're just letting the exception fly here. OK.
| // - insert - true if inserting lines, false if deleting lines | ||
| void ConhostInternalGetSet::_modifyLines(const size_t count, const bool insert) | ||
| { | ||
| auto& screenInfo = _io.GetActiveOutputBuffer(); |
There was a problem hiding this comment.
And this is the site of the #12193 fix (the absence of .GetActiveBuffer() hanging off of GetActiveOutputBuffer())
There was a problem hiding this comment.
The GetActiveBuffer thing wasn't the bug - those calls were all just redundant, because GetActiveOutputBuffer is already required to return the active buffer (in some cases it's literally an alias for GetActiveBuffer).
The bug was that the original code was getting the buffer from the global CONSOLE_INFORMATION class, which is not necessarily the same buffer as the one obtained from _io (which is tracking the current output handle).
| ScrollRegion(srScroll, srScroll, coordDestination, true); | ||
|
|
||
| // The IL and DL controls are also expected to move the cursor to the left margin. | ||
| // For now this is just column 0, since we don't yet support DECSLRM. | ||
| THROW_IF_NTSTATUS_FAILED(screenInfo.SetCursorPosition({ 0, cursorPosition.Y }, false)); |
There was a problem hiding this comment.
Errors now propagate up here (by exception throwing) instead of a silent eat and log like before.
There was a problem hiding this comment.
Yeah, I don't think it matters much either way, but this seemed more consistent with how we're handling cursor position failures elsewhere. And at some point we're probably going to want to normalize all the different ways we're handling cursor positioning anyway.
| { | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
Lock is now gone here, but that is fine as we had to have been on lock already to hit the internal get/set from the VT callbacks anyway. Eliminates a recursive entry/exit.
There was a problem hiding this comment.
(and for the other places where the lock is now missing)
| case DispatchTypes::WindowManipulationType::RefreshWindow: | ||
| success = DispatchCommon::s_RefreshWindow(*_pConApi); | ||
| break; | ||
| DispatchCommon::s_RefreshWindow(*_pConApi); |
There was a problem hiding this comment.
Reminder, these are now throwing (here and below). So the early return true is fine and the error propagates on exception with the false return being "I didn't do nothin', boss".
| size_t rowFixed = row; | ||
| size_t colFixed = col; | ||
| // The parser should never return 0 (0 maps to 1), so this is a failure condition. | ||
| THROW_HR_IF(E_INVALIDARG, row == 0); |
There was a problem hiding this comment.
I apologize for the point in time where somehow there was an allergy to early returns.
| THROW_IF_FAILED(SizeTToShort(colFixed, &coordCursor.X)); | ||
|
|
||
| // Set the line and column values as offsets from the viewport edge. Use safe math to prevent overflow. | ||
| THROW_IF_FAILED(ShortAdd(coordCursor.Y, csbiex.srWindow.Top, &coordCursor.Y)); |
There was a problem hiding this comment.
Hmmm. I thought we had something in the Numerics library that would just throw on fail here with a typed math exception instead of packing up the HRESULT in a generic wil_exception. @lhecker am I crazy or does this exist?
(@j4james, not strictly necessary for this PR... but moving to the base:: numerics might make things look cleaner around these conversions.)
There was a problem hiding this comment.
Yeah. We've got an issue for this somewhere I think. I definitely do want to clean this stuff up at some point, but I was trying to keep the PR focused on just moving the code out of getset.cpp and eliminating the bool returns.
| template<typename TLambda> | ||
| bool StateMachine::_SafeExecute(TLambda&& lambda) noexcept | ||
| try | ||
| { | ||
| return lambda(); | ||
| } | ||
| catch (...) | ||
| { | ||
| LOG_HR(wil::ResultFromCaughtException()); | ||
| return false; | ||
| } | ||
|
|
||
| template<typename TLambda> | ||
| bool StateMachine::_SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda) noexcept | ||
| { | ||
| const bool success = _SafeExecute(std::forward<TLambda>(lambda)); | ||
| if (!success) | ||
| { | ||
| TermTelemetry::Instance().LogFailed(wch); | ||
| } | ||
| return success; | ||
| } |
There was a problem hiding this comment.
(maybe something like this belongs in til... but that can be for future consideration)
There was a problem hiding this comment.
Yeah, that's a good point.
| // - string - Characters to dispatch. | ||
| // Return Value: | ||
| // - <none> | ||
| void StateMachine::_ActionPrintString(const std::wstring_view string) |
There was a problem hiding this comment.
Yeah I mean in hindsight that feels obvious to put this as a helper. hahaha.
| void ConhostInternalGetSet::SetScrollingRegion(const SMALL_RECT& scrollMargins) | ||
| { | ||
| return NT_SUCCESS(DoSrvPrivateSetScrollingRegion(_io.GetActiveOutputBuffer(), scrollMargins)); | ||
| auto& screenInfo = _io.GetActiveOutputBuffer(); |
There was a problem hiding this comment.
note to self: did we keep the STATUS_INVALID_PARAMETER check when Top > Bottom?
There was a problem hiding this comment.
Yeah, see my comment to @miniksa above - all the validation is already handled in AdaptDispatch::_DoSetTopBottomScrollingMargins.
| const bool standardFillAttrs) | ||
| { | ||
| auto& screenInfo = _io.GetActiveOutputBuffer(); | ||
|
|
There was a problem hiding this comment.
note to self: this had a lock before. Does ::ScrollRegion take the lock itself? Was the lock redundant?
There was a problem hiding this comment.
This code is only triggered by a VT sequence, so we would have already obtained the lock in the ApiRoutines::WriteConsoleWImpl method.
I don't think the original code actually needed it, but functions in getset.cpp were often also used in console API calls (which would have required a lock), so it was probably just following the pattern of other functions there.
I'm not sure how this will ultimately work out, but if we get to the point where we can rely on #1173 exclusively, then we shouldn't ever need to pass-through individual sequences, and all of the bool returns could eventually be dropped. But even if we don't get to that, I'm of the opinion that we'd be better off only passing through selective sequences that we know to be safe, and handling that via the For now, though, if we're going to indicate pass-through via a bool return, I think it's easier to just keep all the dispatch methods following that pattern. |
|
Hello @zadjii-msft! Because this pull request has the p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (
|
This PR updates the `ITerminalApi` and `TerminalDispatch` classes to allow exceptions to be thrown in case of errors instead of using boolean return values. ## References This brings the Terminal code into alignment with the `AdaptDispatch` and `ConGetSet` changes made in PR #12247. And while this isn't exactly a fix for #12378, it does at least stop the app from crashing now. ## Detailed Description of the Pull Request / Additional comments All the `TerminalDispatch` methods have had their `noexcept` specifiers dropped, and any `try`/`catch` wrapping removed, so exceptions will now fall through to the `StateMachine` class where they should be safely caught and logged. The same goes for the `ITerminalApi` interface and its implementation in the `Terminal` class. And many of the methods in this interface have also had their `bool` return values changed to `void`, since there is usually not a need for error return values now. ## Validation Steps Performed I've manually tested the `OSC 9;9` sequence described in #12378 and confirmed that it no longer crashes.
This PR updates the `ITerminalApi` and `TerminalDispatch` classes to allow exceptions to be thrown in case of errors instead of using boolean return values. ## References This brings the Terminal code into alignment with the `AdaptDispatch` and `ConGetSet` changes made in PR #12247. And while this isn't exactly a fix for #12378, it does at least stop the app from crashing now. ## Detailed Description of the Pull Request / Additional comments All the `TerminalDispatch` methods have had their `noexcept` specifiers dropped, and any `try`/`catch` wrapping removed, so exceptions will now fall through to the `StateMachine` class where they should be safely caught and logged. The same goes for the `ITerminalApi` interface and its implementation in the `Terminal` class. And many of the methods in this interface have also had their `bool` return values changed to `void`, since there is usually not a need for error return values now. ## Validation Steps Performed I've manually tested the `OSC 9;9` sequence described in #12378 and confirmed that it no longer crashes. (cherry picked from commit 9c11e02)
This is an attempt to simplify the `ConGetSet` interface down to the smallest set of methods necessary to support the `AdaptDispatch` class. The idea is that it should then be easier to implement that interface in Windows Terminal, so we can replace the `TerminalDispatch` class with `AdaptDispatch`. This is a continuation of the refactoring started in #12247, and a significant step towards #3849. ## Detailed Description of the Pull Request / Additional comments The general idea was to give the `AdaptDispatch` class direct access to the high-level structures on which it needs to operate. Some of these structures are now passed in when the class is constructed (the `Renderer`, `RenderSettings`, and `TerminalInput`), and some are exposed as new methods in `ConGetSet` (`GetStateMachine`, `GetTextBuffer`, and `GetViewport`). Many of the existing `ConhostInternalGetSet` methods could easily then be reimplemented in `AdaptDispatch`, since they were often simply forwarding to methods in one of the above structures. Some were a little more complicated, though, and require further explanation. * `GetConsoleScreenBufferInfoEx`: What we were typically using this for was to obtain the viewport, although what we really wanted was the virtual viewport, which is now accessible via the `GetViewport` method. This was also used to obtain the cursor position and buffer width, which we can now get via the `GetTextBuffer` method. * `SetConsoleScreenBufferInfoEx`: This was only really used for the `AdaptDispatch::SetColumns` implementation (for `DECCOLM` mode), and that could be replaced with `ResizeWindow`. This is a slight change in behaviour (it sizes the window rather than the buffer), but neither is technically correct for `DECCOLM`, so I think it's good enough for now, and at least it's consistent with the other VT sizing operations. * `SetCursorPosition`: This could mostly be replaced with direct manipulation of the `Cursor` object (accessible via the text buffer), although again this is a slight change in behavior. The original code would also have made a call to `ConsoleImeResizeCompStrView` (which I don't think is applicable to VT movement), and would potentially have moved the viewport (not essential for now, but could later be supported by `DECHCCM`). It also called `VtIo::SetCursorPosition` to handle cursor inheritance, but that should only apply to `InteractDispatch`, so I've moved that to the `InteractDispatch::MoveCursor` method. * `ScrollRegion`: This has been replaced by two simple helper methods in `AdaptDispatch` which better meet the VT requirements - `_ScrollRectVertically` and `_ScrollRectHorizontally`. Unlike the original `ScrollRegion` implementation, these don't generate `EVENT_CONSOLE_UPDATE_SCROLL` events (see #12656 for more details). * `FillRegion`: This has been replaced by the `_FillRect` helper method in `AdaptDispatch`. It differs from the original `FillRegion` in that it takes a rect rather than a start position and length, which gives us more flexibility for future operations. * `ReverseLineFeed`: This has been replaced with a somewhat refactored reimplementation in `AdaptDispatch`, mostly using the `_ScrollRectVertically` helper described above. * `EraseAll`: This was previously handled by `SCREEN_INFORMATION::VtEraseAll`, but has now been entirely reimplemented in the `AdaptDispatch::_EraseAll` method. * `DeleteLines`/`InsertLines`/`_modifyLines`: These have been replaced by the `_InsertDeleteLineHelper` method in `AdaptDispatch`, which mostly relies on the `_ScrollRectVertically` helper described above. Finally there were a few methods that weren't actually needed in the `ConGetSet` interface: * `MoveToBottom`: This was really just a hack to get the virtual viewport from `GetConsoleScreenBufferInfoEx`. We may still want something like in the future (e.g. to support `DECVCCM` or #8879), but I don't think it's essential for now. * `SuppressResizeRepaint`: This was only needed in `InteractDispatch` and `PtySignalInputThread`, and they could easily access the `VtIo` object to implement it themselves. * `ClearBuffer`: This was only used in `PtySignalInputThread`, and that could easily access the buffer directly via the global console information. * `WriteControlInput`: This was only used in `InteractDispatch`, and that could easily be replaced with a direct call to `HandleGenericKeyEvent`. As part of these changes, I've also refactored some of the existing `AdaptDispatch` code: * `_InsertDeleteHelper` (renamed `_InsertDeleteCharacterHelper`) is now just a straightforward call to the new `_ScrollRectHorizontally` helper. * `EraseInDisplay` and `EraseInLine` have been implemented as a series of `_FillRect` calls, so `_EraseSingleLineHelper` is no longer required. * `_EraseScrollback` is a essentially a special form of scrolling operation, which mostly depends on the `TextBuffer::ScrollRows` method, and with the filling now provided by the new `_FillRect` helper. * There are quite a few operations now in `AdaptDispatch` that are affected by the scrolling margins, so I've pulled out the common margin setup into a new `_GetVerticalMargins` helper method. This also fixes some edge cases where margins could end up out of range. ## Validation Steps Performed There were a number of unit tests that needed to be updated to work around functions that have now been removed, but these substitutions were fairly straightforward for the most part. The adapter tests were a different story, though. In that case we were explicitly testing how operations were passed through to the `ConGetSet` interface, but with more than half those methods now gone, a significant rewrite was required. I've tried to retain the crux of the original tests, but we now have to validate the state changes on the underlying data structures, where before that state would have been tracked in the `TestGetSet` mock. And in some cases we were testing what happened when a method failed, but since that scenario is no longer possible, I've simply removed those tests. I've also tried to manually test all the affected operations to confirm that they're still working as expected, both in vttest as well as my own test scripts. Closes #12662
|
🎉 Handy links: |
Summary of the Pull Request
This PR refactors the
ConGetSetAPI, eliminating some of the bloat produced by theDoSrvPrivateXXXXfunctions, simplifying the method naming to more closely match theITerminalApiinterface, and making better use of exceptions for error conditions in place of boolean return values.References
This is another small step towards merging the
AdaptDispatchandTerminalDispatchclasses (#3849).PR Checklist
Detailed Description of the Pull Request / Additional comments
There are two main parts to this. The first step was to get rid of all the
DoSrvPrivateXXXfunctions, and move their implementation directly into theConhostInternalGetSetclass. For the most part this was just copying and pasting the code, but I also fixed a couple of bugs where we were using the wrong output buffer (the global buffer rather than the one associated with the output handle), and got rid of some unnecessary calls toGetActiveBuffer.The second part was to make better use of exceptions for error conditions. Instead of catching the exceptions at the
ConGetSetlevel, we now allow them to fall through all the way to theStateMachine. This greatly simplifies theAdaptDispatchimplementation since it no longer needs to check a boolean return value on everyConGetSetcall. This also enables the getter methods to return properties directly instead of having to use a reference parameter.Validation Steps Performed
A number of the unit tests had to be updated to match the new API. Sometimes this just required changes to method names, but in other cases error conditions that were previously detected with boolean returns now needed to be caught as exceptions.
There were also a few direct calls to
DoSrvPrivateXXXfunctions that now needed to be invoked through other means: either by generating an equivalent escape sequence, or calling a lower level API serving the same purpose.And in the adapter tests, the mock
ConGetSetimplementation required significant refactoring to match the new interface, mostly to account for the changes in error handling.