Skip to content

Commit

Permalink
Add support for PyQt5 to updater, notifier
Browse files Browse the repository at this point in the history
Also adds PyQt5 to shared requirements file and renames it to
dev-requirements.txt for consistency.
  • Loading branch information
eloquence committed Sep 9, 2020
1 parent e31a089 commit 7e4b70a
Show file tree
Hide file tree
Showing 15 changed files with 374 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
set -e
virtualenv .venv
source .venv/bin/activate
pip install --require-hashes -r test-requirements.txt
pip install --require-hashes -r dev-requirements.txt
sudo apt install lsof
make test && make bandit
Expand Down
2 changes: 1 addition & 1 deletion launcher/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: update-pip-requirements
update-pip-requirements: ## Updates all Python requirements files via pip-compile.
pip-compile --allow-unsafe --generate-hashes --output-file=test-requirements.txt test-requirements.in
pip-compile --allow-unsafe --generate-hashes --output-file=dev-requirements.txt dev-requirements.in

.PHONY: bandit
bandit:
Expand Down
24 changes: 24 additions & 0 deletions launcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
The preflight updater GUI currently supports both PyQt4 and PyQt5. To
enforce the use of PyQt5, set the environment variable SDW_UPDATER_QT to 5.

## Why support PyQt4 and PyQt5?

Qubes 4.0.3 uses an end-of-life Fedora template in dom0 (fedora-25). See
rationale here:

https://www.qubes-os.org/doc/supported-versions/#note-on-dom0-and-eol

fedora-25 only includes PyQt4, which is why we have to support it for now.
The next version of Qubes, Qubes 4.1, will include PyQt5 in dom0.

## Installing PyQt4

PyQt4 is no longer maintained, and is best installed through system
packages, e.g., https://packages.debian.org/buster/python3-pyqt4

## Installing PyQt5

The recommended version of PyQt5 is included in the developer requirements
for this project, which you can install via:

pip install --require-hashes -r dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ bandit
black
pip-tools
pip
PyQt5==5.11.3
pytest
pytest-cov
27 changes: 26 additions & 1 deletion launcher/test-requirements.txt → launcher/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --allow-unsafe --generate-hashes --output-file=test-requirements.txt test-requirements.in
# pip-compile --allow-unsafe --generate-hashes --output-file=dev-requirements.txt dev-requirements.in
#
appdirs==1.4.3 \
--hash=sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92 \
Expand Down Expand Up @@ -188,6 +188,31 @@ zipp==0.6.0 \
--hash=sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335 \
# via importlib-metadata

pyqt5-sip==4.19.19 \
--hash=sha256:304acf771b6033cb4bafc415939d227c91265d30664ed643b298d7e95f509f81 \
--hash=sha256:39d2677f4de46ed4d7aa3b612f31c74c881975efe51c6a23fbb1d9382e4cc850 \
--hash=sha256:54b99a3057e8f01b90d49cca9ca566b1ea23d8920038760f44e75b90c62b9d5f \
--hash=sha256:59f5332f86f3ccd3ac94674fe91eae6e8aca26da7c6588917cabd0fe22af106d \
--hash=sha256:72be07a21b0f379987c4ec59bc86834a9719a2f9cfb49606a4d4e34dae9aa549 \
--hash=sha256:7b3b8c015e545fa30e42205fc1115b7c6afcb6acec790ce3f330a06323730523 \
--hash=sha256:7fbb6389c20aff4c3257e89bb1787effffcaf05c32d937c00094ae45846bffd5 \
--hash=sha256:828d9911acc483672a2bae1cc1bf79f591eb3338faad1f2c798aa2f45459a318 \
--hash=sha256:a9460dac973deccc6ff2d90f18fd105cbaada147f84e5917ed79374dcb237758 \
--hash=sha256:aade50f9a1b9d20f6aabe88e8999b10db57218f5c31950160f3f7957dd64e07c \
--hash=sha256:ac9e5b282d1f0771a8310ed974afe1961ec31e9ae787d052c0e504ea46ae323a \
--hash=sha256:ba41bd21b89c6713f7077b5f7d4a1c452989190aad5704e215560a266a1ecbab \
--hash=sha256:c309dbbd6c155e961bfd6893496afa5cd184cce6f7dffd87ea68ee048b6f97e1 \
--hash=sha256:cfc21b1f80d4655ffa776c505a2576b4d148bbc52bb3e33fedbf6cfbdbc09d1b \
--hash=sha256:d7b26e0b6d81bf14c1239e6a891ac1303a7e882512d990ec330369c7269226d7 \
--hash=sha256:f8b7a3e05235ce58a38bf317f71a5cb4ab45d3b34dc57421dd8cea48e0e4023e \
# via pyqt5
pyqt5==5.11.3 \
--hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \
--hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \
--hash=sha256:d2309296a5a79d0a1c0e6c387c30f0398b65523a6dcc8a19cc172e46b949e00d \
--hash=sha256:e85936bae1581bcb908847d2038e5b34237a5e6acc03130099a78930770e7ead \
# via -r qt5-requirements.in

# The following packages are considered to be unsafe in a requirements file:
pip==19.3.1 \
--hash=sha256:21207d76c1031e517668898a6b46a9fb1501c7a4710ef5dfd6a40ad9e6757ea7 \
Expand Down
10 changes: 8 additions & 2 deletions launcher/sdw-launcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
from PyQt4 import QtGui
from sdw_updater_gui.UpdaterApp import UpdaterApp
from sdw_util import Util
from sdw_updater_gui import Updater
Expand All @@ -9,6 +8,13 @@
import sys
import argparse

if Util.get_qt_version() == 5:
print("Using Qt5 (experimental)")
from PyQt5.QtWidgets import QApplication
else:
from PyQt4.QtGui import QApplication


DEFAULT_INTERVAL = 28800 # 8hr default for update interval


Expand All @@ -23,7 +29,7 @@ def launch_updater():
Start the updater GUI
"""

app = QtGui.QApplication(sys.argv)
app = QApplication(sys.argv)
form = UpdaterApp()
form.show()
sys.exit(app.exec_())
Expand Down
10 changes: 7 additions & 3 deletions launcher/sdw-notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
from sdw_notify import Notify
from sdw_updater_gui import Updater
from sdw_util import Util
from PyQt4 import QtGui
from PyQt4.QtGui import QMessageBox

if Util.get_qt_version() == 5:
print("Using Qt5 (experimental)")
from PyQt5.QtWidgets import QApplication, QMessageBox
else:
from PyQt4.QtGui import QApplication, QMessageBox


def main():
Expand Down Expand Up @@ -50,7 +54,7 @@ def show_update_warning():
Show a graphical warning reminding the user to check for security updates
using the preflight updater.
"""
app = QtGui.QApplication([]) # noqa: F841
app = QApplication([]) # noqa: F841

QMessageBox.warning(
None,
Expand Down
16 changes: 12 additions & 4 deletions launcher/sdw_updater_gui/UpdaterApp.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
from PyQt4 import QtGui
from PyQt4.QtCore import QThread, pyqtSignal, pyqtSlot
from sdw_updater_gui.UpdaterAppUi import Ui_UpdaterDialog
from sdw_updater_gui import strings
from sdw_updater_gui import Updater
from sdw_updater_gui.Updater import UpdateStatus
from sdw_util import Util
import logging
import subprocess
import sys

if Util.get_qt_version() == 5:
from PyQt5.QtWidgets import QDialog
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot
from sdw_updater_gui.UpdaterAppUiQt5 import Ui_UpdaterDialog
else:
from PyQt4.QtGui import QDialog
from PyQt4.QtCore import QThread, pyqtSignal, pyqtSlot
from sdw_updater_gui.UpdaterAppUi import Ui_UpdaterDialog


logger = logging.getLogger(__name__)


Expand All @@ -24,7 +32,7 @@ def launch_securedrop_client():
sys.exit(0)


class UpdaterApp(QtGui.QDialog, Ui_UpdaterDialog):
class UpdaterApp(QDialog, Ui_UpdaterDialog):
def __init__(self, parent=None):
super(UpdaterApp, self).__init__(parent)

Expand Down
114 changes: 114 additions & 0 deletions launcher/sdw_updater_gui/UpdaterAppUiQt5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'sdw_updater.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_UpdaterDialog(object):
def setupUi(self, UpdaterDialog):
UpdaterDialog.setObjectName("UpdaterDialog")
UpdaterDialog.resize(520, 300)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(UpdaterDialog.sizePolicy().hasHeightForWidth())
UpdaterDialog.setSizePolicy(sizePolicy)
UpdaterDialog.setMaximumSize(QtCore.QSize(600, 420))
self.gridLayout_2 = QtWidgets.QGridLayout(UpdaterDialog)
self.gridLayout_2.setObjectName("gridLayout_2")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setContentsMargins(-1, 15, -1, 15)
self.gridLayout.setHorizontalSpacing(3)
self.gridLayout.setObjectName("gridLayout")
spacerItem = QtWidgets.QSpacerItem(
20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
)
self.gridLayout.addItem(spacerItem, 1, 1, 1, 5)
self.clientOpenButton = QtWidgets.QPushButton(UpdaterDialog)
self.clientOpenButton.setStyleSheet("")
self.clientOpenButton.setAutoDefault(True)
self.clientOpenButton.setObjectName("clientOpenButton")
self.gridLayout.addWidget(self.clientOpenButton, 7, 4, 1, 1)
self.proposedActionDescription = QtWidgets.QLabel(UpdaterDialog)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.proposedActionDescription.sizePolicy().hasHeightForWidth()
)
self.proposedActionDescription.setSizePolicy(sizePolicy)
self.proposedActionDescription.setAlignment(
QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
)
self.proposedActionDescription.setWordWrap(True)
self.proposedActionDescription.setObjectName("proposedActionDescription")
self.gridLayout.addWidget(self.proposedActionDescription, 4, 1, 1, 5)
spacerItem1 = QtWidgets.QSpacerItem(
40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
)
self.gridLayout.addItem(spacerItem1, 7, 1, 1, 1)
self.rebootButton = QtWidgets.QPushButton(UpdaterDialog)
self.rebootButton.setStyleSheet("")
self.rebootButton.setAutoDefault(True)
self.rebootButton.setObjectName("rebootButton")
self.gridLayout.addWidget(self.rebootButton, 7, 3, 1, 1)
self.applyUpdatesButton = QtWidgets.QPushButton(UpdaterDialog)
self.applyUpdatesButton.setStyleSheet("")
self.applyUpdatesButton.setAutoDefault(True)
self.applyUpdatesButton.setDefault(False)
self.applyUpdatesButton.setObjectName("applyUpdatesButton")
self.gridLayout.addWidget(self.applyUpdatesButton, 7, 2, 1, 1)
self.cancelButton = QtWidgets.QPushButton(UpdaterDialog)
self.cancelButton.setStyleSheet("")
self.cancelButton.setAutoDefault(True)
self.cancelButton.setObjectName("cancelButton")
self.gridLayout.addWidget(self.cancelButton, 7, 5, 1, 1)
self.progressBar = QtWidgets.QProgressBar(UpdaterDialog)
self.progressBar.setProperty("value", 0)
self.progressBar.setObjectName("progressBar")
self.gridLayout.addWidget(self.progressBar, 2, 1, 1, 5)
self.headline = QtWidgets.QLabel(UpdaterDialog)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.headline.sizePolicy().hasHeightForWidth())
self.headline.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(18)
font.setBold(True)
font.setItalic(False)
font.setWeight(75)
self.headline.setFont(font)
self.headline.setObjectName("headline")
self.gridLayout.addWidget(self.headline, 0, 1, 1, 5)
spacerItem2 = QtWidgets.QSpacerItem(
20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
)
self.gridLayout.addItem(spacerItem2, 3, 1, 1, 5)
self.gridLayout_2.addLayout(self.gridLayout, 0, 0, 1, 1)

self.retranslateUi(UpdaterDialog)
QtCore.QMetaObject.connectSlotsByName(UpdaterDialog)

def retranslateUi(self, UpdaterDialog):
_translate = QtCore.QCoreApplication.translate
UpdaterDialog.setWindowTitle(
_translate("UpdaterDialog", "SecureDrop Workstation preflight updater")
)
self.clientOpenButton.setText(_translate("UpdaterDialog", "Continue"))
self.proposedActionDescription.setText(_translate("UpdaterDialog", "Description goes here"))
self.rebootButton.setText(_translate("UpdaterDialog", "Reboot"))
self.applyUpdatesButton.setText(_translate("UpdaterDialog", "Start Updates"))
self.cancelButton.setText(_translate("UpdaterDialog", "Cancel"))
self.headline.setText(_translate("UpdaterDialog", "Headline goes here"))
55 changes: 55 additions & 0 deletions launcher/sdw_util/Util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
# Folder where logs are stored
LOG_DIRECTORY = os.path.join(BASE_DIRECTORY, "logs")

# File that contains Qubes version information (overridden by tests)
OS_RELEASE_FILE = "/etc/os-release"

# Shared error string
LOCK_ERROR = "Error obtaining lock on '{}'. Process may already be running."

Expand Down Expand Up @@ -108,3 +111,55 @@ def is_conflicting_process_running(list):
sdlog.error("Conflicting process '{}' is currently running.".format(name))
return True
return False


def get_qubes_version():
"""
Helper function for checking the Qubes version. Returns None if not on Qubes.
"""
is_qubes = False
version = None
try:
with open(OS_RELEASE_FILE) as f:
for line in f:
try:
key, value = line.rstrip().split("=")
except ValueError:
continue

if key == "NAME" and "qubes" in value.lower():
is_qubes = True
if key == "VERSION":
version = value
except FileNotFoundError:
return None

if not is_qubes:
return None

return version


def get_qt_version():
"""
Determine the version of Qt appropriate for the environment we're in.
"""
qubes_version = get_qubes_version()

# For now we must support both Qt4 and Qt5. We default to Qt4, because
# that's used in Qubes 4.0, the current stable version.
if qubes_version is not None and "4.1" in qubes_version:
default_version = 5
else:
default_version = 4

version_str = os.getenv("SDW_UPDATER_QT", default_version)
try:
version = int(version_str)
except ValueError:
version = None

if version in [4, 5]:
return version
else:
raise ValueError("Qt version not supported: {}".format(version_str))
4 changes: 4 additions & 0 deletions launcher/tests/fixtures/bad-os-release-file
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# No line
VERSION=
[we're doing toml now]
RELEASES = [ ["gamma", "delta"], [1, 2] ]
7 changes: 7 additions & 0 deletions launcher/tests/fixtures/os-release-qubes-4.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NAME=Qubes
VERSION="4.0 (R4.0)"
ID=qubes
VERSION_ID=4.0
PRETTY_NAME="Qubes 4.0 (R4.0)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:ITL:qubes:4.0"
7 changes: 7 additions & 0 deletions launcher/tests/fixtures/os-release-qubes-4.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NAME=Qubes
VERSION="4.1 (R4.1)"
ID=qubes
VERSION_ID=4.0
PRETTY_NAME="Qubes 4.1 (R4.1)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:ITL:qubes:4.1"
12 changes: 12 additions & 0 deletions launcher/tests/fixtures/os-release-ubuntu
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.5 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
Loading

0 comments on commit 7e4b70a

Please sign in to comment.