Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@justinmc
Copy link
Contributor

This PR works around an error that happened when HasStrings checked the clipboard (in order to see if the Paste button could be shown) when the app was not in the foreground. That information isn't relevant when the app is in the background, so instead of logging an error and worrying developers, I'm swallowing the error and returning false.

Fixes flutter/flutter#95817

@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@justinmc
Copy link
Contributor Author

@stuartmorgan This PR is following up on your comment flutter/flutter#95817 (comment).

I'm not sure how to test this since existing tests (added in #27749) are mocking PlatformHandlerWin32::GetHasStrings, the method that would need to be tested directly here.

@stuartmorgan-g
Copy link
Contributor

stuartmorgan-g commented Mar 24, 2022

I'm not sure how to test this since existing tests (added in #27749) are mocking PlatformHandlerWin32::GetHasStrings, the method that would need to be tested directly here.

What I've been doing for plugins where we need to mock out Win32 APIs is wrapping them with a very thin class, and mocking that instead. Conveniently, there's already a wrapper class around most of the APIs used here, there's just the GetLastError issue. So I would:

  • Move error retrieval into ScopedClipboard by either:
    • adding a GetLastClipboardError(), which returns a value that's populated internally by the other implementation methods any time something fails, or
    • changing all the signatures to return a composite value that includes error (e.g., std::variant<real return type, error int>)
  • Make a new abstract base class with the same interface as ScopedClipboard, that's not file-internal.
  • Make ScopedClipboard implement that interface.
  • Make the PlatformHandlerWin32 constructor allow taking a clipboard implementation, and internally default to an instance of ScopedClipboard if nothing is provided.

Then you can write tests that inject a fake clipboard that has controlled behavior (like emitting this specific error).

@justinmc
Copy link
Contributor Author

@stuartmorgan Thanks for the idea! I've done it and was able to actually test HasStrings. It might need some cleanup still, let me know what you think.

// successful.
bool Open(HWND window);
// Attempts to open the clipboard for the given window, returning the error
// code in the case of failure and -1 otherwise.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using -1 for success? That's pretty confusing, especially since ERROR_SUCCESS in Win32 is 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that was super confusing, switching to 0.

//
// On failure, get error information with ::GetLastError().
// Sets the string content of the clipboard, returning the error code on
// failure and -1 otherwise.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

}
std::optional<std::wstring> string = static_cast<wchar_t*>(locked_data.get());
if (!string) {
return ::GetLastError();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this new code? I'm not sure why there's an optional, or why you're checking for failure since I'm not clear on how the string creation would fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the way I wrote this was redundant. I've updated it so it will return an error if locked_data.get fails, otherwise it returns the string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure why there's a std::optional at all. Why not just:

if (!locked_data.get()) {
  return ::GetLastError();
}
return std::string(static_cast<wchar_t*>(locked_data.get()));

private:
// A reference to the Flutter view.
FlutterWindowsView* view_;
// A clipboard instance that can be passed in.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'can be passed in for mocking in tests'

PlatformHandlerWin32::PlatformHandlerWin32(
BinaryMessenger* messenger,
FlutterWindowsView* view,
std::optional<ScopedClipboardInterface*> clipboard_reference = nullptr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default values should be in the declaration, not the definition.

explicit PlatformHandlerWin32(
BinaryMessenger* messenger,
FlutterWindowsView* view,
std::optional<ScopedClipboardInterface*> clipboard_reference);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cbracken Do you have a preference as the primary owner of this code on having constructor-based DI, vs. a private setter with a friend class, for things that will only ever be overridden for test mocks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to go ahead and merge, but @cbracken if you would prefer it a different way when you see this then I'm happy to open a new PR to fix it.

MOCK_METHOD0(NotImplementedInternal, void());
};

TEST(PlatformHandlerWin32, HasStringsAccessDeniedReturnsFalseWithoutError) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add other test cases now that this code is being made testable. E.g., testing that other errors are still passed through, since that would have been easy to accidentally break in this PR. (Also that normal success cases work.)

@skia-gold
Copy link

Gold has detected about 18 new digest(s) on patchset 7.
View them at https://flutter-engine-gold.skia.org/cl/github/32038

@skia-gold
Copy link

Gold has detected about 18 new digest(s) on patchset 11.
View them at https://flutter-engine-gold.skia.org/cl/github/32038

@skia-gold
Copy link

Gold has detected about 18 new digest(s) on patchset 12.
View them at https://flutter-engine-gold.skia.org/cl/github/32038

@skia-gold
Copy link

Gold has detected about 18 new digest(s) on patchset 13.
View them at https://flutter-engine-gold.skia.org/cl/github/32038

@justinmc justinmc force-pushed the windows-clipboard-bg-error branch from eb256b1 to 1ce47a1 Compare April 12, 2022 17:32
@skia-gold
Copy link

Gold has detected about 18 new digest(s) on patchset 13.
View them at https://flutter-engine-gold.skia.org/cl/github/32038

@justinmc
Copy link
Contributor Author

This gold bot isn't indicating any actual failures, and per @Piinks it should be ignored here.

@justinmc justinmc requested a review from stuartmorgan-g April 12, 2022 19:56
@justinmc
Copy link
Contributor Author

@stuartmorgan Ready for re-review. Thanks for all of the help here. Let me know if I should make a change for #32038 (comment).

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nits, otherwise looks good. Adding @cbracken as a reviewer for the question above about test object injection approach.

}
std::optional<std::wstring> string = static_cast<wchar_t*>(locked_data.get());
if (!string) {
return ::GetLastError();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure why there's a std::optional at all. Why not just:

if (!locked_data.get()) {
  return ::GetLastError();
}
return std::string(static_cast<wchar_t*>(locked_data.get()));

@justinmc
Copy link
Contributor Author

@stuartmorgan Thanks for the suggestion, done and ready for re-review.

@justinmc justinmc force-pushed the windows-clipboard-bg-error branch from 9667cd9 to 35ae1b9 Compare April 27, 2022 17:38
Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits.

// A test version of the private ScopedClipboard.
class TestScopedClipboard : public ScopedClipboardInterface {
public:
TestScopedClipboard(int open_error, bool has_strings);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: bool elements are really hard to read at the call site for non-trivial cases (e.g., SetSomeBool(bool)); consider making an enum instead (enum class ClipboardState { kHasString, kNoString }) and using that for the argument type.

TestScopedClipboard(TestScopedClipboard const&) = delete;
TestScopedClipboard& operator=(TestScopedClipboard const&) = delete;

int Open(HWND window);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overrides need override annotations.

std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>());
// HasStrings will fail.
PlatformHandlerWin32 platform_handler(
&messenger, &view, std::make_unique<TestScopedClipboard>(1, true));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this self-documenting, I would make a local const int arbitraryErrorCode = 1; and pass that in.

@justinmc justinmc force-pushed the windows-clipboard-bg-error branch from e46521f to 853c6ee Compare May 3, 2022 00:07
@justinmc justinmc merged commit 130004b into flutter:main May 3, 2022
@justinmc justinmc deleted the windows-clipboard-bg-error branch May 3, 2022 18:12
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request May 3, 2022
@tgucio
Copy link
Contributor

tgucio commented May 6, 2022

Hi @justinmc,

In the commit I see this:

  if (open_result != kErrorSuccess) {
    rapidjson::Document error_code;
    error_code.SetInt(open_result);
    // Swallow errors of type ERROR_ACCESS_DENIED. These happen when the app is
    // not in the foreground and GetHasStrings is irrelevant.
    // See https://github.com/flutter/flutter/issues/95817.
    if (error_code != kAccessDeniedErrorCode) {
      result->Error(kClipboardError, "Unable to open clipboard", error_code);
      return;
    }
    hasStrings = false;
  } else {
    hasStrings = clipboard_->HasString();
  }

Shouldn't the comparison be if (open_result != kAccessDeniedErrorCode) as error_code is a rapidjson::Document?

@stuartmorgan-g
Copy link
Contributor

Shouldn't the comparison be if (open_result != kAccessDeniedErrorCode) as error_code is a rapidjson::Document?

rapidjson::Document has a bunch of implicit conversions and implicit == overrides, so this is probably correct as written.

But it would be clearer (and more efficient in the case of this specific error) to move the creation of error_code completely into the inner if, and switch the check to open_result.

@justinmc
Copy link
Contributor Author

justinmc commented May 6, 2022

Here's a quick PR to clean that up, thanks for the suggestions: #33174

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Windows] PlatformException(Clipboard error, Unable to open clipboard, 5, null);

4 participants