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

Fix master issue 1306 (system-tray icon) and releated "missing icons" issues #1480

Merged
merged 11 commits into from
Jul 17, 2023
Merged
8 changes: 7 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ Back In Time

Version 1.3.4-dev (development of upcoming release)
* Project: Renamed branch "master" to "main" and started "gitflow" branching model.
* Fix bug: Add support for ChainerBackend class as keyring which iterates over all supported keyring backends (#1410)
* Refactoring: Renamed qt4plugin.py to systrayiconplugin.py (we are using Qt5 for years now ;-)
* Fix bug: Missing icon in SSH private key button (#1364)
* Fix bug: Master issue for missing or empty system-tray icon (#1306)
* Fix bug: System-tray icon missing or empty (GUI and cron) (#1236)
* Fix bug: Improve KDE plasma icon compatibility (#1159)
* Fix bug: Unit test fails on some machines due to warning "Ignoring XDG_SESSION_TYPE=wayland on Gnome..." (#1429)
* Fix bug: Generation of config-manpage caused an error with Debian's Lintian (#1398).
* Fix bug: Return empty list in smartRemove (#1392, Debian Bug Report 973760)
* Breaking change: Minimal Python version 3.8 required (#1358).
* Feature: Exclude /swapfile by default (#1053)
* Documentation: Removed outdated docbook (#1345).
* Build: Introduced .readthedocs.yaml as asked by ReadTheDocs.org (#1443).
* Dependency: The oxygen icons should be installed with the BiT Qt GUI since they are used as fallback in case of missing icons
Copy link
Member

Choose a reason for hiding this comment

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

This info should be added to the README.md. There it is where distro maintainers pick up this information.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I forgot that :-)

I will add this ASAP

I would also like to keep it in the CHANGES since this is what pkg maintainers do read first I guess (not the diff of the README)

* Fix bug: Add support for ChainerBackend class as keyring which iterates over all supported keyring backends (#1410)
* Translation: Strings to translate now easier to understand for translators (#1448, #1457, #1462).
* Translation: Updated and completed "German" (#1454).
* Translation: Remove language Canadian English, British English and Javanese (#1455).
Expand Down
2 changes: 1 addition & 1 deletion common/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def _get_qt_information():

# Themes
theme_info = {}
if tools.checkXServer():
if tools.checkXServer(): # TODO use tools.is_Qt5_working() when stable
qapp = PyQt5.QtWidgets.QApplication([])
theme_info = {
'Theme': PyQt5.QtGui.QIcon.themeName(),
Expand Down
121 changes: 121 additions & 0 deletions common/qt5_probing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import sys
import resource
import logger

# This mini python script is used to determine if a Qt5 GUI application
# can be created without an error.
#
# It is used e.g. for diagnostics output of backintime
# or to check if a system tray icon could be shown...
#
# It is called by "tools.is_Qt5_working()" normally
# but you can also execute it manually via
# python3 qt5_probing.py

# It works by trying to create a QApplication instance
# Any error indicates that Qt5 is not available or not correctly configured.

# WORK AROUND:
#
# The C++ code of Qt5 ends abruptly with a SIGABRT signal (qFatal macro)
# if a QApplication cannot be instantiated.
# This causes a coredump creation by the python default signal handler
# and the signal handler cannot be disabled since it reacts to a
# non-python low-level signal sent via C/C++.
#
# Even though the coredump message cannot be prevent there is
# workaround to prevent the cordump **file** creation which
# would take too much time just to probe Qt5's availability:
#
# Use resource.setrlimit() to set resource.RLIMIT_CORE’s soft limit to 0
#
# Note: This does NOT prevent the console output "Aborted (core dumped)"
# even though no coredump file will be created!
# You can check that no coredump file was created with the command
# sudo coredumpctl list -r
#
# More details:
#
# To suppress the creation of coredump file on Linux
# use resource.setrlimit() to set resource.RLIMIT_CORE’s soft limit to 0
# to prevent coredump file creation.
# https://docs.python.org/3.10/library/resource.html#resource.RLIMIT_CORE
# https://docs.python.org/3.10/library/resource.html#resource.setrlimit
# See also the source code of the test.support.SuppressCrashReport() context manager:
# if self.resource is not None:
# try:
# self.old_value = self.resource.getrlimit(self.resource.RLIMIT_CORE)
# self.resource.setrlimit(self.resource.RLIMIT_CORE,
# (0, self.old_value[1]))
# except (ValueError, OSError):
# pass
# https://github.com/python/cpython/blob/32718f908cc92c474fd968912368b8a4500bd055/Lib/test/support/__init__.py#L1712-L1718
# and cpython "faulthandler_suppress_crash_report()"
# https://github.com/python/cpython/blob/32718f908cc92c474fd968912368b8a4500bd055/Modules/faulthandler.c#L954
# See "man 2 getrlimit" for more details:
# > RLIMIT_CORE
# > This is the maximum size of a core file (see core(5)) in bytes
# > that the process may dump. When 0 no core dump files are created
# > When nonzero, larger dumps are truncated to this size.
# > ...
# > The soft limit is the value that the kernel enforces for the corresponding resource.
# > The hard limit acts as a ceiling for the soft limit:
# > an unprivileged process may set only its soft limit to a value
# > in the range from 0 up to the hard limit, and (irreversibly) lower its
# > hard limit. A privileged process (under Linux: one with the
# > CAP_SYS_RESOURCE capability in the initial user namespace) may make
# > arbitrary changes to either limit value.
#
# Note: The context manager test.support.SuppressCrashReport() is NOT used
# here since the "test.support" module not public and its API is subject
# to change without backwards compatibility concerns between releases.

# Work-around to prevent the time-consuming creation of a core dump
old_limits = resource.getrlimit(resource.RLIMIT_CORE)
resource.setrlimit(resource.RLIMIT_CORE, (0, old_limits[1]))

exit_code = 0

try:

logger.debug(f"{__file__} started... Call args: {str(sys.argv)}")

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication

app = QApplication([''])

exit_code = 1

# https://doc.qt.io/qt-5/qsystemtrayicon.html#details:
# > To check whether a system tray is present on the user's desktop,
# > call the QSystemTrayIcon::isSystemTrayAvailable() static function.
#
# This requires a QApplication instance (otherwise Qt5 causes a segfault)
# which we don't have here so we create it to check if a window manager
# ("GUI") is active at all (e.g. in headless installations it isn't).
# See: https://forum.qt.io/topic/3852/issystemtrayavailable-always-crashes-segfault-on-ubuntu-10-10-desktop/6

from PyQt5.QtWidgets import QSystemTrayIcon
is_sys_tray_available = QSystemTrayIcon.isSystemTrayAvailable()

if is_sys_tray_available:
exit_code = 2

logger.debug(f"isSystemTrayAvailable for Qt5: {is_sys_tray_available}")

except Exception as e:
logger.debug(f"Error: {repr(e)}")

logger.debug(f"{__file__} is terminating normally (exit code: {exit_code})")

# Exit codes:
# 0 = no Qt5 GUI available
# 1 = only Qt5 GUI available (no sys tray support)
# 2 = Qt5 GUI and sys tray available
# 134 (-6 as signed byte exit code type!) = SIGABRT caught by python
# ("interrupted by signal 6: SIGABRT").
# This is most probably caused by a misconfigured Qt5...
# So the interpretation is the same as exit code 0.
sys.exit(exit_code)

2 changes: 1 addition & 1 deletion common/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ def backup(self, force = False):
Wrapper for :py:func:`takeSnapshot` which will prepare and clean up
things for the main :py:func:`takeSnapshot` method. This will check
that no other snapshots are running at the same time, there is nothing
prohibing a new snapshot (e.g. on battery) and the profile is configured
prohibiting a new snapshot (e.g. on battery) and the profile is configured
correctly. This will also mount and unmount remote destinations.

Args:
Expand Down
58 changes: 58 additions & 0 deletions common/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,17 @@ def checkXServer():
"""
Check if there is a X11 server running on this system.

Use ``is_Qt5_working`` instead if you want to be sure that Qt5 is working.

Returns:
bool: ``True`` if X11 server is running
"""
# Note: Return values of xdpyinfo <> 0 are not clearly documented.
# xdpyinfo does indeed return 1 if it prints
# xdypinfo: unable to open display "..."
# This seems to be undocumented (at least not in the man pages)
# and the source is not obvious here:
# https://cgit.freedesktop.org/xorg/app/xdpyinfo/tree/xdpyinfo.c
if checkCommand('xdpyinfo'):
proc = subprocess.Popen(['xdpyinfo'],
stdout = subprocess.DEVNULL,
Expand All @@ -520,6 +528,56 @@ def checkXServer():
else:
return False


def is_Qt5_working(systray_required=False):
"""
Check if the Qt5 GUI library is working (installed and configured)

This function is contained in BiT CLI (not BiT Qt) to allow Qt5
diagnostics output even if the BiT Qt GUI is not installed.
This function does NOT add a hard Qt5 dependency (just "probing")
so it is OK to be in BiT CLI.

Args:
systray_required: Set to ``True`` if the systray of the desktop
environment must be available too to consider Qt5 as "working"

Returns:
bool: ``True`` Qt5 can create a GUI
``False`` Qt5 fails (or the systray is not available
if ``systray_required`` is ``True``)
"""

# Spawns a new process since it may crash with a SIGABRT and we
# don't want to crash BiT if this happens...

try:
path = os.path.join(backintimePath("common"), "qt5_probing.py")
cmd = [sys.executable, path]
with subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True) as proc:

std_output, error_output = proc.communicate() # to get the exit code

logger.debug(f"Qt5 probing result: exit code {proc.returncode}")

if proc.returncode != 23: # if some Qt5 parts are missing: Show details
Copy link
Contributor Author

@aryoda aryoda Jul 17, 2023

Choose a reason for hiding this comment

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

Damn, I forgot to remove my testing code. Will follow up on this with a new commit...

Copy link
Member

Choose a reason for hiding this comment

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

I thought it has to be this way. Some more verbose logging in such a complex situation isn't bad.

logger.debug(f"Qt5 probing stdout: {std_output}")
logger.debug(f"Qt5 probing errout: {error_output}")

return proc.returncode == 2 or (proc.returncode == 1 and systray_required is False)

except FileNotFoundError:
logger.error(f"Qt5 probing script not found: {cmd[0]}")
raise

except Exception as e:
logger.error(f"Error: {repr(e)}")
raise


def preparePath(path):
"""
Removes trailing slash '/' from ``path``.
Expand Down
48 changes: 39 additions & 9 deletions qt/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,46 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from PyQt5.QtGui import QIcon
import logger

# TODO setThemeName() only for available themes -> QStyleFactory.keys()
# since this code may activate a theme that is only partially installed
# (or not at all in case of the last theme in the list: oxygen)
# See issues #1364 and #1306
for theme in ('ubuntu-mono-dark', 'gnome', 'oxygen'):
logger.debug("Checking if the current theme contains the BiT icon...")

# If the current theme does not even contain the "document-save" icon
# try to use another well-known working theme (if it is installed):
for theme in ('ubuntu-mono-dark', 'gnome', 'breeze', 'breeze dark', 'hicolor', 'adwaita', 'adwaita-dark', 'yaru', 'oxygen'):
# Check if the current theme does provide the BiT "logo" icon
# (otherwise the theme is not fully/correctly installed)
# and use this theme then for all icons
# Note: "hicolor" does currently (2022) use different icon names
# (not fully compliant to the freedesktop.org spec)
# and is not recommended as main theme (it is meant as fallback only).
if not QIcon.fromTheme('document-save').isNull():
logger.debug(f"Found an installed theme: {QIcon.themeName()}")
break
# try next theme (activate it)...
QIcon.setThemeName(theme)
logger.debug(f"Probing theme: {theme} (activated as {QIcon.themeName()})")

if QIcon.fromTheme('document-save').isNull():
logger.error("No supported theme installed (missing icons). "
"Please consult the project web site for instructions "
"how to fix this!")

# Dev note: Please prefer choosing icons from the freedesktop.org spec
# to improve the chance that the icon is available and
# each installed theme:
# https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
#
# If there is chance that an icon may not always be available use
# the second argument of QIcon.fromTheme() to provide a fallback
# icon from the freedesktop.org spec.

#BackInTime Logo
# BackInTime Logo
# TODO If we knew for sure that the global var "qapp" exists then
# we could use a built-in "standard" Qt5 icon as fallback if the theme does
# not provide the icon.
# => wait for icon.py refactoring than improve this:
# qapp.style().standardIcon(QStyle.SP_DialogSaveButton)
BIT_LOGO = QIcon.fromTheme('document-save')
BIT_LOGO_INFO = QIcon.fromTheme('document-save-as')

Expand All @@ -42,7 +71,7 @@
REMOVE_SNAPSHOT = QIcon.fromTheme('edit-delete')
VIEW_SNAPSHOT_LOG = QIcon.fromTheme('text-plain',
QIcon.fromTheme('text-x-generic'))
VIEW_LAST_LOG = QIcon.fromTheme('document-new')
VIEW_LAST_LOG = QIcon.fromTheme('document-open-recent') # 'document-open-recent') # ('document-new')
SETTINGS = QIcon.fromTheme('gtk-preferences',
QIcon.fromTheme('configure'))
SHUTDOWN = QIcon.fromTheme('system-shutdown')
Expand All @@ -63,8 +92,9 @@

#Files toolbar
UP = QIcon.fromTheme('go-up')
SHOW_HIDDEN = QIcon.fromTheme('show-hidden',
QIcon.fromTheme('list-add'))
SHOW_HIDDEN = QIcon.fromTheme('view-hidden', # currently only in Breeze (see #1159)
QIcon.fromTheme('show-hidden', # icon installed with BiT!
QIcon.fromTheme('list-add')))
RESTORE = QIcon.fromTheme('edit-undo')
RESTORE_TO = QIcon.fromTheme('document-revert')
SNAPSHOTS = QIcon.fromTheme('file-manager',
Expand Down
65 changes: 0 additions & 65 deletions qt/plugins/qt4plugin.py

This file was deleted.

Loading