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

added fn to prevent multiple GUI updater instances #4309

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions journalist_gui/Pipfile
Expand Up @@ -11,3 +11,4 @@ python_version = "3.5"
[packages]
"pyqt5" = "*"
pexpect = "*"
pytest = "*"
5 changes: 2 additions & 3 deletions journalist_gui/SecureDropUpdater
Expand Up @@ -2,15 +2,14 @@
from PyQt5 import QtWidgets
import sys

from journalist_gui.SecureDropUpdater import UpdaterApp

from journalist_gui.SecureDropUpdater import UpdaterApp, prevent_second_instance

def main():
app = QtWidgets.QApplication(sys.argv)
prevent_second_instance(app, "Securedrop Workstation Updater")
form = UpdaterApp()
form.show()
app.exec_()


if __name__ == '__main__':
main()
21 changes: 21 additions & 0 deletions journalist_gui/journalist_gui/SecureDropUpdater.py
Expand Up @@ -5,6 +5,8 @@
import os
import re
import pexpect
import socket
import sys

from journalist_gui import updaterUI, strings, resources_rc # noqa

Expand All @@ -13,6 +15,25 @@
ESCAPE_POD = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')


def prevent_second_instance(app: QtWidgets.QApplication, name: str) -> None: # noqa
Copy link
Contributor

Choose a reason for hiding this comment

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

Unlike the client, we don't use type hints in the updater GUI, even though it is just supported by the version of Python here. But I see no harm in having these here


# Null byte triggers abstract namespace
IDENTIFIER = '\0' + name
ALREADY_BOUND_ERRNO = 98

app.instance_binding = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
try:
app.instance_binding.bind(IDENTIFIER)
except OSError as e:
if e.errno == ALREADY_BOUND_ERRNO:
err_dialog = QtWidgets.QMessageBox()
err_dialog.setText(name + ' is already running.')
err_dialog.exec()
sys.exit()
else:
raise


class SetupThread(QThread):
signal = pyqtSignal('PyQt_PyObject')

Expand Down
53 changes: 51 additions & 2 deletions journalist_gui/test_gui.py
@@ -1,13 +1,62 @@
import unittest
import subprocess
import pexpect
import pytest
from unittest import mock
from unittest.mock import MagicMock
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QSizePolicy, QInputDialog)
from PyQt5.QtTest import QTest

from journalist_gui.SecureDropUpdater import UpdaterApp, strings, FLAG_LOCATION
from journalist_gui.SecureDropUpdater import prevent_second_instance


@mock.patch('journalist_gui.SecureDropUpdater.sys.exit')
@mock.patch('journalist_gui.SecureDropUpdater.QtWidgets.QMessageBox')
class TestSecondInstancePrevention(unittest.TestCase):
def setUp(self):
self.mock_app = mock.MagicMock()
self.mock_app.applicationName = mock.MagicMock(return_value='sd')

@staticmethod
def socket_mock_generator(already_bound_errno=98):
namespace = set()

def kernel_bind(addr):
if addr in namespace:
error = OSError()
error.errno = already_bound_errno
raise error
else:
namespace.add(addr)

socket_mock = mock.MagicMock()
socket_mock.socket().bind = mock.MagicMock(side_effect=kernel_bind)
return socket_mock

def test_diff_name(self, mock_msgbox, mock_exit):
mock_socket = self.socket_mock_generator()
with mock.patch('journalist_gui.SecureDropUpdater.socket', new=mock_socket):
prevent_second_instance(self.mock_app, 'name1')
prevent_second_instance(self.mock_app, 'name2')

mock_exit.assert_not_called()

def test_same_name(self, mock_msgbox, mock_exit):
mock_socket = self.socket_mock_generator()
with mock.patch('journalist_gui.SecureDropUpdater.socket', new=mock_socket):
prevent_second_instance(self.mock_app, 'name1')
prevent_second_instance(self.mock_app, 'name1')

mock_exit.assert_any_call()

def test_unknown_kernel_error(self, mock_msgbox, mock_exit):
mock_socket = self.socket_mock_generator(131) # crazy unexpected error
with mock.patch('journalist_gui.SecureDropUpdater.socket', new=mock_socket):
with pytest.raises(OSError):
prevent_second_instance(self.mock_app, 'name1')
prevent_second_instance(self.mock_app, 'name1')


class AppTestCase(unittest.TestCase):
Expand Down Expand Up @@ -184,7 +233,7 @@ def test_tails_status_success(self):
'failure_reason': ''}

with mock.patch('os.remove') as mock_remove:
self.window.tails_status(result)
self.window.tails_status(result)

# We do remove the flag file if the update does finish
mock_remove.assert_called_once_with(FLAG_LOCATION)
Expand All @@ -195,7 +244,7 @@ def test_tails_status_failure(self):
'failure_reason': '42'}

with mock.patch('os.remove') as mock_remove:
self.window.tails_status(result)
self.window.tails_status(result)

# We do not remove the flag file if the update does not finish
mock_remove.assert_not_called()
Expand Down