Skip to content
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

Extend multi-window support to Electron (#11642) #49

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

tsmaeder
Copy link
Collaborator

What it does

Fixes eclipse-theia#11642

The main change is to prevent the secondary window from closing until the extracted widget is removed from the window. This includes waiting until any close handling (including dialogs) are finished.

Contributed on behalf of ST Microelectronics.

How to test

Since this just enables the functionality from eclipse-theia#11048 for electron, the "how to test" from that PR applies.

Review checklist

Reminder for reviewers

@tsmaeder tsmaeder changed the title Extends multi-window support to Electron (#11642) Extend multi-window support to Electron (#11642) Sep 14, 2022
Copy link

@lucas-koehler lucas-koehler left a comment

Choose a reason for hiding this comment

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

Hi @tsmaeder ,

I tested the changes and, unfortunately, still noticed a few issues.

First, the updated examples/electron/tsconfig.json should be committed with the reference to the secondary-window extension.

Second, I could still get IllegalAccess errors sometimes. Albeit, I feel their frequency has gone down. To test this I used the cat coding extension as a simple widget and the draw.io integration as a custom editor.

On Linux (Ubuntu 20.04), widgets could be reopened even if an IllegalAccess was reported. It seemed that this was more likely with activated autoSave on the custom editor.

On Windows 11 (VM), widgets could sometimes not be re-opened after an IllegalAccess was reported.

On both systems, closing the draw io custom editor via the Open Editors view did close the widget in the external window but the window stayed open as an empty shell.

Besides these issues, I also discovered positive news about the auto save:
With the draw io extension, auto save was applied in the editor. Meaning, when the setting was activated, the editor automatically persisted changes. Furthermore, the save dialog worked when closing a dirty draw io editor in an external window: The closing of the widget and window were stopped until the dialog was closed. The dialog was opened in the main window. However, I think this is another issue: eclipse-theia#11647

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Sep 19, 2022

Hi @lucas-koehler I don't ever get any "illegal access" with the code in this PR on Window 10. Do you have steps to reproduce and frequencies if not repeatable?

@lucas-koehler
Copy link

lucas-koehler commented Sep 19, 2022

Hi @tsmaeder,
I have two sencarios to reproduce this:

Cat Coding

  1. Repeatedly opening and closing the cat coding widget in a secondary window. Frequency was after about 20 tries. I repeated this 2 times.

Custom editor

  1. Enable autosave
  2. opening a draw io diagram, change something, immediately close window
  3. this seems to work quite reliably. However, the Illegal Access seems to have different consequences as the widget can be re-opened.
illegal-access-auto-save.mp4

@tsmaeder
Copy link
Collaborator Author

Thx @lucas-koehler. Am I correct that both scenarios are with "autosave" on?

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Sep 20, 2022

I'm having trouble reproducing the problem reliably: it just seems to randomly happen. When I start adding console output to track the problem, the problem goes away. That means that I can't just try out stuff and see if if fixes the problem, since I can't verify a fix if I can't reasonably provoke the problem. <sigh>

@paul-marechal
Copy link

When testing on Windows I sometime get in a state where the secondary windows opened is empty and I get Uncaught illegal access when I close the Window. I also noticed that the Theia/PhosphorJS widget seems to be lingering from this point onward, preventing the webview from opening again (I tested using the Hex Editor extension).

I cannot find a surefire way to reproduce it but I'm also able to end up in a state where the webview widget is leaked which prevents me from opening the webview, and this without triggering any error. It seems to somehow remove parts of the editor title bar which is odd:

image

By "widget leaked" I mean that the widget is found in the following piece of logic despite not being on visible in Theia:

custom-editor-opener.tsx#L68

Only when widget is undefined does it seem to show the webview.

@tsmaeder
Copy link
Collaborator Author

Unfortunately, it's quite hard to reproduce this issue for me: sometimes I can try for 15 minutes and not reproduce it, sometimes it happens right away. After lengthy experimentation, I've found the following scenario that reproduces the issue quite reliably. This scenario uses the drawio custom editor

  1. create two files x.drawio and bar.drawio
  2. Open x.drawio, then bar.drawio
  3. Move x.drawio to a secondary window
  4. Use the "Open Editors" view in the navigator to bring the secondary window to the front
  5. Move a figure in the x.drawio editor and immediately close the secondary window
  6. Observe the "illegal access" in the main window browser log

@tsmaeder
Copy link
Collaborator Author

Here's a recording of the scenario:

2022-09-20.15-44-38.mp4

@tsmaeder
Copy link
Collaborator Author

I've added code to remove the 'close' handler from the electron window when it is hit. For me, that seems to fix the problem, but @lucas-koehler can still reproduce the issue. That the electron-remote stuff is involved somehow would make sense, though. A thing to try would be to remove the electron-remote stuff and see if that fixes the problem.

@tsmaeder
Copy link
Collaborator Author

I'll go back to working on eclipse-theia#11643 for the time being.

@tsmaeder
Copy link
Collaborator Author

I had some further insights on this problem:

  • Clean handling of saving dirty editors will not be possible (using the save/don't save/cancel dialog) will not be possible in the browser case: we cannot really prevent browser windows from closing: the best we can do is pop up the native dialog asking whether to "leave the page without saving", same as we do when we close the main window. This prevents the approach of doing a proper remove of the widget and postponing the close after close actions have finished.

  • The approach should work in Electron, though, by adding an on('close') handler.

    • Update: registering a 'close' handler with electronRemote does not seem to prevent closing of the browser window, even if we do a "preventDefault". This needs debugging.

@tsmaeder
Copy link
Collaborator Author

Confirmed that the close handler works fine when attaching it to the newly create window in the electron main process. So either we're doing something wrong in the current logic or we need to figure out how to call the proper pre-close logic from the electron main process.

@tsmaeder tsmaeder force-pushed the 11642_electron_secondary_window branch from 3fc6e2b to 06885a4 Compare October 13, 2022 07:47
@tsmaeder
Copy link
Collaborator Author

@lucas-koehler I've updated with my latest changes and rebased on origin/master. Could you have another look?

Fixes eclipse-theia#11642

The main change is to prevent the secondary window from closing until
the extracted widget is removed from the window. This includes waiting
until any close handling (including dialogs) are finished.
Since we cannot reliably prevent closing windows in the browser case,
we either save or discard unsaved changes according to the autosave
settings

Contributed on behalf of ST Microelectronics.

Signed-off-by: Thomas Mäder <[email protected]>
Copy link

@lucas-koehler lucas-koehler left a comment

Choose a reason for hiding this comment

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

Browser works as expected given the limitations.
Unfortunately, the IllegalAccess is back using the proven reproduction approach with drawio and two editors.
Idk why that happens as it was not reproducible for me back when I tested the https://github.com/tsmaeder/theia/commits/11642_electron_secondary_window_2 branch a week or so ago.

It was reproducible after one or two tries.
Additional info:
Ubuntu 20.04
Default plugins + drawio editor and cat coding
Node 16.15.1, yarn 1.22.15
I even cloned a new repo with only your branch to avoid some left over pollution of the repo.

]);

if (ExtractableWidget.is(current) && current.secondaryWindow && !current.isClosing) {
current.secondaryWindow!.close();

Choose a reason for hiding this comment

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

With the current.secondaryWindow check, the ! shouldn't be necessary.

Suggested change
current.secondaryWindow!.close();
current.secondaryWindow.close();

@@ -22,6 +22,9 @@ import { Widget } from './widget';
export interface ExtractableWidget extends Widget {
/** Set to `true` to mark the widget to be extractable. */
isExtractable: boolean;

/** State variable to keep track of recursive attempty to close the secondary window */

Choose a reason for hiding this comment

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

Suggested change
/** State variable to keep track of recursive attempty to close the secondary window */
/** State variable to keep track of recursive attempts to close the secondary window */

electronRemote.getCurrentWindow().webContents.once('did-create-window', newElectronWindow => {
newElectronWindow.setMenuBarVisibility(false);
// newElectronWindow.setMenuBarVisibility(false);

Choose a reason for hiding this comment

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

Either comment this back in or remove the line completely. I think hiding the menu in secondary windows generally makes sense with the current state (i.e. it's just the default electron menu)

@@ -239,11 +240,17 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract
}

protected forceHide(): void {
console.log('forcing hide');

Choose a reason for hiding this comment

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

The log messages introduced in this file seem like debug logs from development. They should probably be removed again ;)

]);

if (ExtractableWidget.is(current) && current.secondaryWindow && !current.isClosing) {
current.secondaryWindow!.close();

Choose a reason for hiding this comment

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

Suggested change
current.secondaryWindow!.close();
current.secondaryWindow.close();

Comment on lines +158 to +160
await this.applicationShell.closeWidget(widget.id, closeOptions);
widget.isClosing = false;
return widget.isDisposed;

Choose a reason for hiding this comment

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

await this.applicationShell.closeWidget(widget.id, closeOptions); closes the secondary window that the widget resides in with the changes at application-shell.ts https://github.com/eclipsesource/theia/pull/49/files#diff-cb4b87cc3dc274963d24f6fd5682ad9c89cd51d94726100eb2e3c92421e5c171R1565-R1566

Afterwards, the widget is accessed to set its isClosing flag. Maybe this could cause problems with IllegalAccess?
This is especially intereseting because this was not the case on the last known working state at cdamus@2b3d733

The changes stem from the browser save handling as far as I can see but affect the electron implementation in this way, too. It is called here: https://github.com/eclipsesource/theia/pull/49/files#diff-563e180229ce37170c43089f2a51e72ab90eead76b88b8a4300365d8373f93c3R51

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All I can say is that this shouldn't be a problem, since the widget is an object that lives in the context of the main browser window. But it's a diff I should look at.

@tsmaeder
Copy link
Collaborator Author

Some more data:

Next, I'll try to reproduce in the branch from this PR in order to make sure we're not seeing some artifact.

@tsmaeder
Copy link
Collaborator Author

I went through the compiled PhosphorJS code (widget.js) and noticed that widget.id is actually a computed property that accesses the node. Hmh...that is something to look at.

@tsmaeder
Copy link
Collaborator Author

Another note: I can reproduce the "illegal access" as well with terminal windows (though not as reliably), so this does not seem to have anything to do with the particularities of webviews.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Oct 31, 2022

For what it's worth, I got a case of "illegal access" (with a stack trace) from a section of the electron renderer windows init code, from the section that assigned a window error handler inside the "nodeIntegration=true" branch of execution.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Nov 1, 2022

So here's what I tried:

  1. Rebased the original tested change to commit before terminal secondary windows: => could not reproduce
  2. Rebased to commit introducing terminal secondary windows => could reproduce.

As always, this could be conincidence: I'll cross-check.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Nov 2, 2022

So I turned off the keyboard & theme window registrations in a post-terminal commit rebased branch and I can reproduce the problem, but only after numerous attempty.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Nov 2, 2022

I went back to the state rebased on before the external terminals and I can (after around 20 tries) reproduce the issue. Back to square one.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Nov 2, 2022

Bad news: I retested the original version that we thought was error-free and was able to provoke the "illegal access" after around 20 tries. The fact seems to be that if we remove bogus accesses (like the one where we keep references to the closed window's document when we update the theme), we can make the errors less likely. This kind of gives me hope that the error is something we cause. On the other hand, we're not really any closer to finding fixing this problem for good.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Nov 3, 2022

Another candidate for causing the problem is the "open editors" widget: refreshing a tree (by assigning to the root) is always async with no ability to wait for the refresh to end.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Nov 8, 2022

Another thing to try would be to make the the creation of the secondary window loop around to the main electron thread cleanly instead of using electron-remote.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Nov 8, 2022

With the latest commit, I haven't been able to reproduce the "illegal access" in my manual testing with the drawio plugin and terminals.

// to prevent "illegal access" errors. Unfortunately, it is unclear what
// exactly causes the errors.
this._window.focus();
setTimeout(() => {
Copy link
Member

Choose a reason for hiding this comment

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

I suggest to replace lines 101 to 104 with a simple
newWindow.destroy();
When reacting to the newWindow close event, the default propagation of event is stopped in order to check that the second window is safe to be closed. After the check the window was calling close which again triggers the close event, what is not needed. So destroy is the more accurate method to use.
With the change most/all? Illegal access errors are gone.

Copy link
Member

Choose a reason for hiding this comment

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

That was to early, unfortunately the error can still be reproduced after a while.

Comment on lines +101 to +104
setTimeout(() => {
closingState = ClosingState.readyToClose;
newWindow.close();
}, 100);
Copy link
Member

@eneufeld eneufeld Nov 16, 2022

Choose a reason for hiding this comment

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

Suggested change
setTimeout(() => {
closingState = ClosingState.readyToClose;
newWindow.close();
}, 100);
closingState = ClosingState.readyToClose;
newWindow.destroy();

@eneufeld
Copy link
Member

I think there is a memory leak when detaching the windows.
Here is a heaptimeline https://drive.google.com/file/d/1Xp3L0vxjzWvJHY589s-UKBzu7q_4ueAH/view?usp=share_link . Unfortunately I can't directly attach it as the file is to big.

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Dec 6, 2022

Debugging this on the native level reveals that something goes wrong when handling setImmediate() callbacks while closing down the Javascript environment. Not sure exactly what's going on here, since I can't build this old version of electron we're using with full debug symbols (pdb files get > 4Gb, which the old tooling can't handle). Newer versions of electron I've tried don't work with Theia for various reasons or are broken in various ways.
Anyway: here's the stack trace:

electron.exe!v8::internal::`anonymous namespace'::Invoke(v8::internal::Isolate *) Line 320 (c:\Users\thomas\code\testdata\electron\src\v8\src\execution\execution.cc:320)
electron.exe!v8::internal::Execution::Call(v8::internal::Handle<v8::internal::Object>) Line 474 (c:\Users\thomas\code\testdata\electron\src\v8\src\execution\execution.cc:474)
electron.exe!v8::Function::Call(v8::Local<v8::Context>) Line 5090 (c:\Users\thomas\code\testdata\electron\src\v8\src\api\api.cc:5090)
electron.exe!node::InternalMakeCallback(node::Environment *) Line 199 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\src\api\callback.cc:199)
electron.exe!node::MakeCallback(v8::Isolate *) Line 271 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\src\api\callback.cc:271)
electron.exe!node::Environment::CheckImmediate() Line 928 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\src\env.cc:928)
electron.exe!uv_check_invoke(uv_loop_s *) Line 121 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\deps\uv\src\win\loop-watcher.c:121)
electron.exe!uv_run(uv_loop_s *) Line 630 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\deps\uv\src\win\core.c:630)
electron.exe!node::Environment::CleanupHandles() Line 647 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\src\env.cc:647)
electron.exe!node::Environment::RunCleanup() Line 684 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\src\env.cc:684)
electron.exe!node::FreeEnvironment(node::Environment *) Line 375 (c:\Users\thomas\code\testdata\electron\src\third_party\electron_node\src\api\environment.cc:375)
electron.exe!electron::ElectronRendererClient::WillReleaseScriptContext(v8::Local<v8::Context>) Line 161 (c:\Users\thomas\code\testdata\electron\src\electron\shell\renderer\electron_renderer_client.cc:161)
electron.exe!electron::ElectronRenderFrameObserver::WillReleaseScriptContext(v8::Local<v8::Context>) Line 163 (c:\Users\thomas\code\testdata\electron\src\electron\shell\renderer\electron_render_frame_observer.cc:163)
electron.exe!content::RenderFrameImpl::WillReleaseScriptContext(v8::Local<v8::Context>) Line 4460 (c:\Users\thomas\code\testdata\electron\src\content\renderer\render_frame_impl.cc:4460)
electron.exe!blink::LocalFrameClientImpl::WillReleaseScriptContext(v8::Local<v8::Context>) Line 289 (c:\Users\thomas\code\testdata\electron\src\third_party\blink\renderer\core\frame\local_frame_client_impl.cc:289)
electron.exe!blink::LocalWindowProxy::DisposeContext(blink::WindowProxy::Lifecycle) Line 112 (c:\Users\thomas\code\testdata\electron\src\third_party\blink\renderer\bindings\core\v8\local_window_proxy.cc:112)
electron.exe!blink::WindowProxy::ClearForClose() Line 69 (c:\Users\thomas\code\testdata\electron\src\third_party\blink\renderer\bindings\core\v8\window_proxy.cc:69)
electron.exe!blink::WindowProxyManager::ClearForClose() Line 24 (c:\Users\thomas\code\testdata\electron\src\third_party\blink\renderer\bindings\core\v8\window_proxy_manager.cc:24)
electron.exe!blink::Frame::Detach(blink::FrameDetachType) Line 131 (c:\Users\thomas\code\testdata\electron\src\third_party\blink\renderer\core\frame\frame.cc:131)
electron.exe!blink::Page::WillBeDestroyed() Line 972 (c:\Users\thomas\code\testdata\electron\src\third_party\blink\renderer\core\page\page.cc:972)

@tsmaeder
Copy link
Collaborator Author

tsmaeder commented Dec 6, 2022

I am not confident that I'll get anywhere without being able to look at local variables. I'm not fit enough with my native debugging to look at the stack in hex, so it's important to get a build at symbol_level = 2. I'll have to try on Linux, I guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

secondary-window: add electron support
4 participants