diff mbox series

[v2,1/7] ui/cocoa: Ensure we have the iothread lock when calling into QEMU

Message ID 20190214102816.3393-2-peter.maydell@linaro.org
State Superseded
Headers show
Series ui/cocoa: Use OSX's main loop | expand

Commit Message

Peter Maydell Feb. 14, 2019, 10:28 a.m. UTC
The Cocoa UI should run on the main thread; this is enforced
in OSX Mojave. In order to be able to run on the main thread,
we need to make sure we hold the iothread lock whenever we
call into various QEMU UI midlayer functions.

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>

---
 ui/cocoa.m | 83 ++++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 59 insertions(+), 24 deletions(-)

-- 
2.17.2 (Apple Git-113)

Comments

Roman Bolshakov Feb. 22, 2019, 3:19 p.m. UTC | #1
On Thu, Feb 14, 2019 at 10:28:10AM +0000, Peter Maydell wrote:
> The Cocoa UI should run on the main thread; this is enforced

> in OSX Mojave. In order to be able to run on the main thread,

> we need to make sure we hold the iothread lock whenever we

> call into various QEMU UI midlayer functions.

>


Hi Peter,

I wonder if qmp_stop and qmp_cont calls should be made in locked context
within pauseQEMU and resumeQEMU methods, respectively?

I also think it's better to clarify that the reason of the commit is not
Mojave enforcing usage of event loop in main thread but an improvement
of event processing in Cocoa UI, because Cocoa UI works on Mojave.

As of now qemu main loop and Cocoa events processing is done in the same
thread. And you can see from below that invocation of the qemu_main blocks
Cocoa event loop [NSApp run] in applicationDidFinishLaunching as qemu_main
doesn't quit.

Here's a backtrace that shows lockup of the primary Cocoa event loop:
(lldb) b cocoa_refresh
Breakpoint 1: where = qemu-system-x86_64`cocoa_refresh + 12 at cocoa.m:1634, address = 0x0000000109a03a0c
(lldb) c
Process 46179 resuming
Process 46179 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000109a03a0c qemu-system-x86_64`cocoa_refresh(dcl=0x00007f914eccefe0) at cocoa.m:1634
   1631
   1632 static void cocoa_refresh(DisplayChangeListener *dcl)
   1633 {
-> 1634     NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
   1635
   1636     COCOA_DEBUG("qemu_cocoa: cocoa_refresh\n");
   1637     graphic_hw_update(NULL);
Target 0: (qemu-system-x86_64) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000109a03a0c qemu-system-x86_64`cocoa_refresh(dcl=0x00007f914eccefe0) at cocoa.m:1634
    frame #1: 0x00000001099f6888 qemu-system-x86_64`dpy_refresh(s=0x00007f915074cbd0) at console.c:1622
    frame #2: 0x00000001099f66dd qemu-system-x86_64`gui_update(opaque=0x00007f915074cbd0) at console.c:205
    frame #3: 0x0000000109ba6009 qemu-system-x86_64`timerlist_run_timers(timer_list=0x00007f915070c390) at qemu-timer.c:574
    frame #4: 0x0000000109ba60e0 qemu-system-x86_64`qemu_clock_run_timers(type=QEMU_CLOCK_REALTIME) at qemu-timer.c:588
    frame #5: 0x0000000109ba662a qemu-system-x86_64`qemu_clock_run_all_timers at qemu-timer.c:708
    frame #6: 0x0000000109ba6b8f qemu-system-x86_64`main_loop_wait(nonblocking=0) at main-loop.c:521
    frame #7: 0x0000000109760464 qemu-system-x86_64`main_loop at vl.c:1923
    frame #8: 0x000000010975b624 qemu-system-x86_64`qemu_main(argc=5, argv=0x00007ffee66a0688, envp=0x00007ffee66a06b8) at vl.c:4578
    frame #9: 0x00000001099ffef9 qemu-system-x86_64`-[QemuCocoaAppController startEmulationWithArgc:argv:](self=0x00007f914ee05e10, _cmd="startEmulationWithArgc:argv:", argc=5, argv=0x00007ffee66a0688) at cocoa.m:1135
    frame #10: 0x00000001099ffd97 qemu-system-x86_64`-[QemuCocoaAppController applicationDidFinishLaunching:](self=0x00007f914ee05e10, _cmd="applicationDidFinishLaunching:", note=@"NSApplicationDidFinishLaunchingNotification") at cocoa.m:1087
    frame #11: 0x00007fff46bd5632 CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
    frame #12: 0x00007fff46bd55ac CoreFoundation`___CFXRegistrationPost_block_invoke + 63
    frame #13: 0x00007fff46bd54cd CoreFoundation`_CFXRegistrationPost + 398
    frame #14: 0x00007fff46bdd929 CoreFoundation`___CFXNotificationPost_block_invoke + 87
    frame #15: 0x00007fff46b450ca CoreFoundation`-[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1633
    frame #16: 0x00007fff46b4448d CoreFoundation`_CFXNotificationPost + 742
    frame #17: 0x00007fff48ecca7b Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 66
    frame #18: 0x00007fff440c964a AppKit`-[NSApplication _postDidFinishNotification] + 313
    frame #19: 0x00007fff440c8f6e AppKit`-[NSApplication _sendFinishLaunchingNotification] + 209
    frame #20: 0x00007fff440c68c8 AppKit`-[NSApplication(NSAppleEventHandling) _handleAEOpenEvent:] + 552
    frame #21: 0x00007fff440c6517 AppKit`-[NSApplication(NSAppleEventHandling) _handleCoreEvent:withReplyEvent:] + 690
    frame #22: 0x00007fff48f17144 Foundation`-[NSAppleEventManager dispatchRawAppleEvent:withRawReply:handlerRefCon:] + 287
    frame #23: 0x00007fff48f16fc0 Foundation`_NSAppleEventManagerGenericHandler + 102
    frame #24: 0x00007fff47dfcb93 AE`aeDispatchAppleEvent(AEDesc const*, AEDesc*, unsigned int, unsigned char*) + 1855
    frame #25: 0x00007fff47dfc3fd AE`dispatchEventAndSendReply(AEDesc const*, AEDesc*) + 41
    frame #26: 0x00007fff47dfc2d5 AE`aeProcessAppleEvent + 439
    frame #27: 0x00007fff45e1110e HIToolbox`AEProcessAppleEvent + 55
    frame #28: 0x00007fff440c2644 AppKit`_DPSNextEvent + 1734
    frame #29: 0x00007fff440c1102 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1362
    frame #30: 0x00007fff440bb165 AppKit`-[NSApplication run] + 699
    frame #31: 0x0000000109a03041 qemu-system-x86_64`main(argc=5, argv=0x00007ffee66a0688) at cocoa.m:1589
    frame #32: 0x00007fff73dc2ed9 libdyld.dylib`start + 1
    frame #33: 0x00007fff73dc2ed9 libdyld.dylib`start + 1

Each millisecond cocoa_refresh gets called by display handler. The function
extracts all available events from the cocoa event queue and dispatches them to
handleEvent:

        event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:distantPast
                        inMode: NSDefaultRunLoopMode dequeue:YES];
        if (event != nil) {
            [cocoaView handleEvent:event];
        }

handleEvent enqueues keyboard and mouse events into ps2 queue and the
other events are enqueued to Cocoa event queue. I'm curious what happens
to the events that are enqueued with:
	[NSApp sendEvent:event];

Perhaps the events are enqueued in handleEvent over and over and never
get out of the event queue. And there could be a chance of the event
queue overrun.

--
Thanks,
Roman
Peter Maydell Feb. 22, 2019, 3:41 p.m. UTC | #2
On Fri, 22 Feb 2019 at 15:19, Roman Bolshakov <r.bolshakov@yadro.com> wrote:
>

> On Thu, Feb 14, 2019 at 10:28:10AM +0000, Peter Maydell wrote:

> > The Cocoa UI should run on the main thread; this is enforced

> > in OSX Mojave. In order to be able to run on the main thread,

> > we need to make sure we hold the iothread lock whenever we

> > call into various QEMU UI midlayer functions.

> >

>

> Hi Peter,

>

> I wonder if qmp_stop and qmp_cont calls should be made in locked context

> within pauseQEMU and resumeQEMU methods, respectively?


Yes, you're right, I missed these, but they should also be
called within a with_iothread_lock wrapper.

> I also think it's better to clarify that the reason of the commit is not

> Mojave enforcing usage of event loop in main thread but an improvement

> of event processing in Cocoa UI, because Cocoa UI works on Mojave.


Hmm? The point of this patchset is exactly that Mojave enforces
that things go on the main thread, where previous OSX versions
did not, and so in some situations QEMU will crash on Mojave
where it did not on older versions. So I'm not sure what you're
suggesting should be clarified here.

> As of now qemu main loop and Cocoa events processing is done in the same

> thread. And you can see from below that invocation of the qemu_main blocks

> Cocoa event loop [NSApp run] in applicationDidFinishLaunching as qemu_main

> doesn't quit.


Yes, indeed. This is how we've traditionally done the Cocoa
event handling. It's worked OK for everything up to Mojave...

> Each millisecond cocoa_refresh gets called by display handler. The function

> extracts all available events from the cocoa event queue and dispatches them to

> handleEvent:

>

>         event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:distantPast

>                         inMode: NSDefaultRunLoopMode dequeue:YES];

>         if (event != nil) {

>             [cocoaView handleEvent:event];

>         }

>

> handleEvent enqueues keyboard and mouse events into ps2 queue and the

> other events are enqueued to Cocoa event queue. I'm curious what happens

> to the events that are enqueued with:

>         [NSApp sendEvent:event];


As I understand it, this sendEvent method doesn't queue anything.
It just hands the event to the usual OSX event handling chain,
which will either use it (for example, if it's a menu-item
key accelerator event) or drop it on the floor. The logic is
basically "QEMU-specific code gets first chance to decide whether
to consume the event; the default (and a few other odd cases)
if it does not is that we let the OSX framework code do whatever
it wants to do with it".

thanks
-- PMM
Roman Bolshakov Feb. 22, 2019, 9:48 p.m. UTC | #3
On Fri, Feb 22, 2019 at 03:41:05PM +0000, Peter Maydell wrote:
> On Fri, 22 Feb 2019 at 15:19, Roman Bolshakov <r.bolshakov@yadro.com> wrote:

> >

> > On Thu, Feb 14, 2019 at 10:28:10AM +0000, Peter Maydell wrote:

> > > The Cocoa UI should run on the main thread; this is enforced

> > > in OSX Mojave. In order to be able to run on the main thread,

> > > we need to make sure we hold the iothread lock whenever we

> > > call into various QEMU UI midlayer functions.

> > >

> >

> > I also think it's better to clarify that the reason of the commit is not

> > Mojave enforcing usage of event loop in main thread but an improvement

> > of event processing in Cocoa UI, because Cocoa UI works on Mojave.

> 

> Hmm? The point of this patchset is exactly that Mojave enforces

> that things go on the main thread, where previous OSX versions

> did not, and so in some situations QEMU will crash on Mojave

> where it did not on older versions. So I'm not sure what you're

> suggesting should be clarified here.

> 


I'm not exactly sure there's an issue with QEMU on Mojave. But I lean
towards the opinion because I haven't seen it :)

> > As of now qemu main loop and Cocoa events processing is done in the same

> > thread. And you can see from below that invocation of the qemu_main blocks

> > Cocoa event loop [NSApp run] in applicationDidFinishLaunching as qemu_main

> > doesn't quit.

> 

> Yes, indeed. This is how we've traditionally done the Cocoa

> event handling. It's worked OK for everything up to Mojave...

> 


Oh, you also mention that in the cover letter. I somehow skipped it.

> > Each millisecond cocoa_refresh gets called by display handler. The function

> > extracts all available events from the cocoa event queue and dispatches them to

> > handleEvent:

> >

> >         event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:distantPast

> >                         inMode: NSDefaultRunLoopMode dequeue:YES];

> >         if (event != nil) {

> >             [cocoaView handleEvent:event];

> >         }

> >

> > handleEvent enqueues keyboard and mouse events into ps2 queue and the

> > other events are enqueued to Cocoa event queue. I'm curious what happens

> > to the events that are enqueued with:

> >         [NSApp sendEvent:event];

> 

> As I understand it, this sendEvent method doesn't queue anything.

> It just hands the event to the usual OSX event handling chain,

> which will either use it (for example, if it's a menu-item

> key accelerator event) or drop it on the floor. The logic is

> basically "QEMU-specific code gets first chance to decide whether

> to consume the event; the default (and a few other odd cases)

> if it does not is that we let the OSX framework code do whatever

> it wants to do with it".

> 


Thanks, I see! I should have consulted apple reference before raising
the issue. FWIW there's a method to post an event to the event queue but
sendEvent is used to dispatch an event within an app and the event that
is already taken from the queue.

Besides qmp_stop and qmp_cont not being called under iothread lock,
Reviewed-by: Roman Bolshakov <r.bolshakv@yadro.com>

Tested-by: Roman Bolshakov <r.bolshakov@yadro.com>


Thanks,
Roman
Peter Maydell Feb. 23, 2019, 1:11 p.m. UTC | #4
On Fri, 22 Feb 2019 at 21:48, Roman Bolshakov <r.bolshakov@yadro.com> wrote:
>

> On Fri, Feb 22, 2019 at 03:41:05PM +0000, Peter Maydell wrote:

> > On Fri, 22 Feb 2019 at 15:19, Roman Bolshakov <r.bolshakov@yadro.com> wrote:

> > >

> > > On Thu, Feb 14, 2019 at 10:28:10AM +0000, Peter Maydell wrote:

> > > > The Cocoa UI should run on the main thread; this is enforced

> > > > in OSX Mojave. In order to be able to run on the main thread,

> > > > we need to make sure we hold the iothread lock whenever we

> > > > call into various QEMU UI midlayer functions.

> > > >

> > >

> > > I also think it's better to clarify that the reason of the commit is not

> > > Mojave enforcing usage of event loop in main thread but an improvement

> > > of event processing in Cocoa UI, because Cocoa UI works on Mojave.

> >

> > Hmm? The point of this patchset is exactly that Mojave enforces

> > that things go on the main thread, where previous OSX versions

> > did not, and so in some situations QEMU will crash on Mojave

> > where it did not on older versions. So I'm not sure what you're

> > suggesting should be clarified here.

> >

>

> I'm not exactly sure there's an issue with QEMU on Mojave. But I lean

> towards the opinion because I haven't seen it :)


It only happens for some guest workloads. The "usual" case
is that the cocoa_refresh callback is called from the QEMU
main loop, which happens to be on the OSX main thread, which
means OSX is still happy. But in some cases cocoa_refresh can
be called from a guest vCPU thread -- I think we've seen this
when a guest initiates a screen resolution change: the call
from the guest vCPU thread goes into the model of the graphics
device, which makes a call into the UI midlayer to say
"resolution changed", which immediately triggers a
refresh callback to the UI frontend layer from that thread.
In Mojave this causes OSX to terminate QEMU. I think in
older OSX versions it would probably be a race condition,
so it's technically a bug but not one that usually has
any visible bad effects; it's only surfaced as a problem
now that Mojave actively checks for this condition and
kills the process.

thanks
-- PMM
diff mbox series

Patch

diff --git a/ui/cocoa.m b/ui/cocoa.m
index e2567d6946..2931c751fd 100644
--- a/ui/cocoa.m
+++ b/ui/cocoa.m
@@ -129,6 +129,21 @@ 
 NSTextField *pauseLabel;
 NSArray * supportedImageFileTypes;
 
+// Utility function to run specified code block with iothread lock held
+typedef void (^CodeBlock)(void);
+
+static void with_iothread_lock(CodeBlock block)
+{
+    bool locked = qemu_mutex_iothread_locked();
+    if (!locked) {
+        qemu_mutex_lock_iothread();
+    }
+    block();
+    if (!locked) {
+        qemu_mutex_unlock_iothread();
+    }
+}
+
 // Mac to QKeyCode conversion
 const int mac_to_qkeycode_map[] = {
     [kVK_ANSI_A] = Q_KEY_CODE_A,
@@ -306,6 +321,7 @@  - (void) ungrabMouse;
 - (void) toggleFullScreen:(id)sender;
 - (void) handleMonitorInput:(NSEvent *)event;
 - (void) handleEvent:(NSEvent *)event;
+- (void) handleEventLocked:(NSEvent *)event;
 - (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled;
 /* The state surrounding mouse grabbing is potentially confusing.
  * isAbsoluteEnabled tracks qemu_input_is_absolute() [ie "is the emulated
@@ -649,8 +665,14 @@  - (void) handleMonitorInput:(NSEvent *)event
 
 - (void) handleEvent:(NSEvent *)event
 {
-    COCOA_DEBUG("QemuCocoaView: handleEvent\n");
+    with_iothread_lock(^{
+        [self handleEventLocked:event];
+    });
+}
 
+- (void) handleEventLocked:(NSEvent *)event
+{
+    COCOA_DEBUG("QemuCocoaView: handleEvent\n");
     int buttons = 0;
     int keycode = 0;
     bool mouse_event = false;
@@ -945,15 +967,18 @@  - (QEMUScreen) gscreen {return screen;}
  */
 - (void) raiseAllKeys
 {
-    int index;
     const int max_index = ARRAY_SIZE(modifiers_state);
 
-   for (index = 0; index < max_index; index++) {
-       if (modifiers_state[index]) {
-           modifiers_state[index] = 0;
-           qemu_input_event_send_key_qcode(dcl->con, index, false);
-       }
-   }
+    with_iothread_lock(^{
+        int index;
+
+        for (index = 0; index < max_index; index++) {
+            if (modifiers_state[index]) {
+                modifiers_state[index] = 0;
+                qemu_input_event_send_key_qcode(dcl->con, index, false);
+            }
+        }
+    });
 }
 @end
 
@@ -1215,13 +1240,17 @@  - (void)removePause
 /* Restarts QEMU */
 - (void)restartQEMU:(id)sender
 {
-    qmp_system_reset(NULL);
+    with_iothread_lock(^{
+        qmp_system_reset(NULL);
+    });
 }
 
 /* Powers down QEMU */
 - (void)powerDownQEMU:(id)sender
 {
-    qmp_system_powerdown(NULL);
+    with_iothread_lock(^{
+        qmp_system_powerdown(NULL);
+    });
 }
 
 /* Ejects the media.
@@ -1237,9 +1266,11 @@  - (void)ejectDeviceMedia:(id)sender
         return;
     }
 
-    Error *err = NULL;
-    qmp_eject(true, [drive cStringUsingEncoding: NSASCIIStringEncoding],
-              false, NULL, false, false, &err);
+    __block Error *err = NULL;
+    with_iothread_lock(^{
+        qmp_eject(true, [drive cStringUsingEncoding: NSASCIIStringEncoding],
+                  false, NULL, false, false, &err);
+    });
     handleAnyDeviceErrors(err);
 }
 
@@ -1271,16 +1302,18 @@  - (void)changeDeviceMedia:(id)sender
             return;
         }
 
-        Error *err = NULL;
-        qmp_blockdev_change_medium(true,
-                                   [drive cStringUsingEncoding:
-                                          NSASCIIStringEncoding],
-                                   false, NULL,
-                                   [file cStringUsingEncoding:
-                                         NSASCIIStringEncoding],
-                                   true, "raw",
-                                   false, 0,
-                                   &err);
+        __block Error *err = NULL;
+        with_iothread_lock(^{
+            qmp_blockdev_change_medium(true,
+                                       [drive cStringUsingEncoding:
+                                                  NSASCIIStringEncoding],
+                                       false, NULL,
+                                       [file cStringUsingEncoding:
+                                                 NSASCIIStringEncoding],
+                                       true, "raw",
+                                       false, 0,
+                                       &err);
+        });
         handleAnyDeviceErrors(err);
     }
 }
@@ -1419,7 +1452,9 @@  - (void)adjustSpeed:(id)sender
     // get the throttle percentage
     throttle_pct = [sender tag];
 
-    cpu_throttle_set(throttle_pct);
+    with_iothread_lock(^{
+        cpu_throttle_set(throttle_pct);
+    });
     COCOA_DEBUG("cpu throttling at %d%c\n", cpu_throttle_get_percentage(), '%');
 }