fix the deadlock on macos

This commit is contained in:
Raoul Snyman 2023-08-13 07:40:04 +00:00 committed by Tomas Groth
parent 32dc18a31f
commit 4351651ad5
7 changed files with 27 additions and 55 deletions

2
.gitignore vendored
View File

@ -49,3 +49,5 @@ tags
test
openlp-test-projectordb.sqlite
*/test-results.xml
.DS_Store
OpenLP.spec

View File

@ -18,13 +18,14 @@
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
from flask import jsonify, Blueprint
from PyQt5 import QtCore
from openlp.core.api.lib import old_auth, old_success_response
from openlp.core.common.registry import Registry
from openlp.core.lib.plugin import PluginStatus, StringContent
from openlp.core.state import State
from flask import jsonify, Blueprint
core_views = Blueprint('old_core', __name__)
@ -54,5 +55,8 @@ def plugin_list():
@core_views.route('/main/image')
def main_image():
img = 'data:image/jpeg;base64,{}'.format(Registry().get('live_controller').grab_maindisplay())
live_controller = Registry().get('live_controller')
img_data = live_controller.staticMetaObject.invokeMethod(
live_controller, 'grab_maindisplay', QtCore.Qt.BlockingQueuedConnection, QtCore.Q_RETURN_ARG(str))
img = 'data:image/jpeg;base64,{}'.format(img_data)
return jsonify({'slide_image': img})

View File

@ -19,13 +19,15 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import logging
from flask import jsonify, request, abort, Blueprint
from PyQt5 import QtCore
from openlp.core.api.lib import login_required
from openlp.core.common.registry import Registry
from openlp.core.lib.plugin import PluginStatus, StringContent
from openlp.core.state import State
from flask import jsonify, request, abort, Blueprint
core = Blueprint('core', __name__)
log = logging.getLogger(__name__)
@ -82,6 +84,8 @@ def login():
@core.route('/live-image')
def main_image():
controller = Registry().get('live_controller')
img = 'data:image/jpeg;base64,{}'.format(controller.grab_maindisplay())
live_controller = Registry().get('live_controller')
img_data = live_controller.staticMetaObject.invokeMethod(
live_controller, 'grab_maindisplay', QtCore.Qt.BlockingQueuedConnection, QtCore.Q_RETURN_ARG(str))
img = 'data:image/jpeg;base64,{}'.format(img_data)
return jsonify({'binary_image': img})

View File

@ -262,7 +262,7 @@ def build_icon(icon):
return button_icon
def image_to_byte(image, base_64=True):
def image_to_byte(image: QtGui.QPixmap, base_64: bool = True) -> QtCore.QByteArray | str:
"""
Resize an image to fit on the current screen for the web and returns it as a byte stream.

View File

@ -33,7 +33,7 @@ from openlp.core.common import SlideLimits
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.platform import is_macosx, is_wayland_compositor
from openlp.core.common.platform import is_wayland_compositor
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.utils import wait_for
from openlp.core.display.screens import ScreenList
@ -1288,17 +1288,6 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
return self._capture_maindisplay_desktop()
def _capture_maindisplay_desktop(self):
# At least on macOS, there's a crash when opening /main Remote API endpoint,
# due to macOS requiring the screenshot code to be called on the main thread.
if is_macosx():
self.log_debug('_capture_maindisplay_desktop macos thread-safe call')
return self._capture_maindisplay_desktop_mainthread_safe()
else:
self.log_debug('_capture_maindisplay_desktop default call')
return self._capture_maindisplay_desktop_signal()
@QtCore.pyqtSlot(result='QPixmap')
def _capture_maindisplay_desktop_signal(self):
current_screen = ScreenList().current
display_rect = current_screen.display_geometry
screen_rect = current_screen.geometry
@ -1312,14 +1301,6 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
win_image = current_screen.try_grab_screen_part(relative_x, relative_y, width, height)
return win_image
def _capture_maindisplay_desktop_mainthread_safe(self):
# Using internal Qt's messaging/event system to invoke the function.
# Usually we would need to use PyQt's signals, but they aren't blocking. So we had to resort to this solution,
# which use a less-documented Qt mechanism to invoke the signal in a blocking way.
return QtCore.QMetaObject.invokeMethod(self, '_capture_maindisplay_desktop_signal',
QtCore.Qt.ConnectionType.BlockingQueuedConnection,
QtCore.Q_RETURN_ARG('QPixmap'))
def _capture_maindisplay_window(self):
win_image = None
for display in self.displays:
@ -1347,7 +1328,8 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
slide_ready_time = self.slide_changed_time + datetime.timedelta(seconds=slide_delay_time)
return datetime.datetime.now() > slide_ready_time
def grab_maindisplay(self):
@QtCore.pyqtSlot(result=str)
def grab_maindisplay(self) -> str:
"""
Gets the last taken screenshot
"""

View File

@ -111,11 +111,12 @@ def test_login_with_valid_credetials_returns_token(flask_client, settings):
def test_retrieving_image(flask_client):
class FakeController:
def grab_maindisplay(self):
class FakeImage:
def save(self, first, second):
pass
return FakeImage()
@property
def staticMetaObject(self):
class FakeMetaObject:
def invokeMethod(self, obj, meth, conn_type, return_type):
return ''
return FakeMetaObject()
Registry.create().register('live_controller', FakeController())
res = flask_client.get('/api/v2/core/live-image').get_json()
assert res['binary_image'] != ''

View File

@ -1447,8 +1447,6 @@ def _init__capture_maindisplay_mocks(geometry, mocked_screenlist, mocked_applica
display_mock = MagicMock(grab_screenshot_safe=MagicMock(return_value=windowed_screenshot_mock), is_display=True)
slide_controller.displays = [display_mock]
slide_controller.service_item = ServiceItem(None)
# Bypassing signal call to avoid test freeze
slide_controller._capture_maindisplay_desktop_mainthread_safe = slide_controller._capture_maindisplay_desktop_signal
mocked_geometry = MagicMock(
x=MagicMock(return_value=geometry[1][0]),
y=MagicMock(return_value=geometry[1][1]),
@ -1614,25 +1612,6 @@ def test__capture_maindisplay_window_fakes_black_screen(mocked_is_wayland_compos
assert image.pixelColor(int(geometry[1][2] / 2), int(geometry[1][3] / 2)) == QtGui.QColorConstants.Black
@patch('openlp.core.ui.slidecontroller.is_macosx')
def test__capture_maindisplay_desktop_calls_safe_on_macos(mocked_is_macosx, registry, settings):
"""
Test the _capture_maindisplay_desktop method fallbacks to calling thread-safe code on macOS
(avoids a hard crash due to Cocoa/macOS internal details)
"""
# GIVEN: A mocked system check (running macOS) and mocked maindisplay call
mocked_is_macosx.return_value = True
slide_controller = SlideController()
# slidecontroller._capture_maindisplay_desktop_signal = MagicMock(return_value=QtGui.QPixmap())
slide_controller._capture_maindisplay_desktop_mainthread_safe = MagicMock()
# WHEN: trying to grab desktop screenshot
slide_controller._capture_maindisplay_desktop()
# THEN: Screenshot should have been taken through thread-safe call
slide_controller._capture_maindisplay_desktop_mainthread_safe.assert_called_once()
@patch(u'openlp.core.ui.slidecontroller.image_to_byte')
def test_grab_maindisplay(mocked_image_to_byte, registry):
"""