We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
On the Admin Workstation,  N SecureDrop updater processes are started if N network interfaces are enabled in Tails
## Description

If a Tails Admin Workstation has more than one network interface, the SecureDrop network manager hook that checks for updates will run for each active NIC. (For example, if a workstation has both Ethernet and Wifi enabled.)

This is confusing to end users and may result in multiple update processes clobbering each other's changes.

## Steps to Reproduce

On an Admin Workstation:
- revert to an earlier SecureDrop version:
```
cd ~/Persistent/securedrop
git checkout 0.10.0
```
- enable multiple network connections (eg Ethernet and Wifi) and wait for their Tor connections to come up

## Expected Behavior
A single instance of the Securedrop Updater is started.

## Actual Behavior
Multiple instances of the SecureDrop Updater are started.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `journalist_gui/journalist_gui/SecureDropUpdater.py`
Content:
```
1 #!/usr/bin/python
2 from PyQt5 import QtGui, QtWidgets
3 from PyQt5.QtCore import QThread, pyqtSignal
4 import subprocess
5 import os
6 import re
7 import pexpect
8 
9 from journalist_gui import updaterUI, strings, resources_rc  # noqa
10 
11 
12 FLAG_LOCATION = "/home/amnesia/Persistent/.securedrop/securedrop_update.flag"  # noqa
13 ESCAPE_POD = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
14 
15 
16 class SetupThread(QThread):
17     signal = pyqtSignal('PyQt_PyObject')
18 
19     def __init__(self):
20         QThread.__init__(self)
21         self.output = ""
22         self.update_success = False
23         self.failure_reason = ""
24 
25     def run(self):
26         sdadmin_path = '/home/amnesia/Persistent/securedrop/securedrop-admin'
27         update_command = [sdadmin_path, 'setup']
28 
29         # Create flag file to indicate we should resume failed updates on
30         # reboot. Don't create the flag if it already exists.
31         if not os.path.exists(FLAG_LOCATION):
32             open(FLAG_LOCATION, 'a').close()
33 
34         try:
35             self.output = subprocess.check_output(
36                 update_command,
37                 stderr=subprocess.STDOUT).decode('utf-8')
38             if 'Failed to install' in self.output:
39                 self.update_success = False
40                 self.failure_reason = strings.update_failed_generic_reason
41             else:
42                 self.update_success = True
43         except subprocess.CalledProcessError as e:
44             self.output += e.output.decode('utf-8')
45             self.update_success = False
46             self.failure_reason = strings.update_failed_generic_reason
47         result = {'status': self.update_success,
48                   'output': self.output,
49                   'failure_reason': self.failure_reason}
50         self.signal.emit(result)
51 
52 
53 # This thread will handle the ./securedrop-admin update command
54 class UpdateThread(QThread):
55     signal = pyqtSignal('PyQt_PyObject')
56 
57     def __init__(self):
58         QThread.__init__(self)
59         self.output = ""
60         self.update_success = False
61         self.failure_reason = ""
62 
63     def run(self):
64         sdadmin_path = '/home/amnesia/Persistent/securedrop/securedrop-admin'
65         update_command = [sdadmin_path, 'update']
66         try:
67             self.output = subprocess.check_output(
68                 update_command,
69                 stderr=subprocess.STDOUT).decode('utf-8')
70             if "Signature verification successful" in self.output:
71                 self.update_success = True
72             else:
73                 self.failure_reason = strings.update_failed_generic_reason
74         except subprocess.CalledProcessError as e:
75             self.update_success = False
76             self.output += e.output.decode('utf-8')
77             if 'Signature verification failed' in self.output:
78                 self.failure_reason = strings.update_failed_sig_failure
79             else:
80                 self.failure_reason = strings.update_failed_generic_reason
81         result = {'status': self.update_success,
82                   'output': self.output,
83                   'failure_reason': self.failure_reason}
84         self.signal.emit(result)
85 
86 
87 # This thread will handle the ./securedrop-admin tailsconfig command
88 class TailsconfigThread(QThread):
89     signal = pyqtSignal('PyQt_PyObject')
90 
91     def __init__(self):
92         QThread.__init__(self)
93         self.output = ""
94         self.update_success = False
95         self.failure_reason = ""
96         self.sudo_password = ""
97 
98     def run(self):
99         tailsconfig_command = ("/home/amnesia/Persistent/"
100                                "securedrop/securedrop-admin "
101                                "tailsconfig")
102         try:
103             child = pexpect.spawn(tailsconfig_command)
104             child.expect('SUDO password:')
105             self.output += child.before.decode('utf-8')
106             child.sendline(self.sudo_password)
107             child.expect(pexpect.EOF)
108             self.output += child.before.decode('utf-8')
109             child.close()
110 
111             # For Tailsconfig to be considered a success, we expect no
112             # failures in the Ansible output.
113             if child.exitstatus:
114                 self.update_success = False
115                 self.failure_reason = strings.tailsconfig_failed_generic_reason  # noqa
116             else:
117                 self.update_success = True
118         except pexpect.exceptions.TIMEOUT:
119             self.update_success = False
120             self.failure_reason = strings.tailsconfig_failed_sudo_password
121 
122         except subprocess.CalledProcessError:
123             self.update_success = False
124             self.failure_reason = strings.tailsconfig_failed_generic_reason
125         result = {'status': self.update_success,
126                   'output': ESCAPE_POD.sub('', self.output),
127                   'failure_reason': self.failure_reason}
128         self.signal.emit(result)
129 
130 
131 class UpdaterApp(QtWidgets.QMainWindow, updaterUI.Ui_MainWindow):
132 
133     def __init__(self, parent=None):
134         super(UpdaterApp, self).__init__(parent)
135         self.setupUi(self)
136         self.statusbar.setSizeGripEnabled(False)
137         self.output = strings.initial_text_box
138         self.plainTextEdit.setPlainText(self.output)
139         self.update_success = False
140 
141         pixmap = QtGui.QPixmap(":/images/static/banner.png")
142         self.label_2.setPixmap(pixmap)
143         self.label_2.setScaledContents(True)
144 
145         self.progressBar.setProperty("value", 0)
146         self.setWindowTitle(strings.window_title)
147         self.setWindowIcon(QtGui.QIcon(':/images/static/securedrop_icon.png'))
148         self.label.setText(strings.update_in_progress)
149 
150         self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab),
151                                   strings.main_tab)
152         self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2),
153                                   strings.output_tab)
154 
155         # Connect buttons to their functions.
156         self.pushButton.setText(strings.install_later_button)
157         self.pushButton.setStyleSheet("""background-color: lightgrey;
158                                       min-height: 2em;
159                                       border-radius: 10px""")
160         self.pushButton.clicked.connect(self.close)
161         self.pushButton_2.setText(strings.install_update_button)
162         self.pushButton_2.setStyleSheet("""background-color: #E6FFEB;
163                                         min-height: 2em;
164                                         border-radius: 10px;""")
165         self.pushButton_2.clicked.connect(self.update_securedrop)
166         self.update_thread = UpdateThread()
167         self.update_thread.signal.connect(self.update_status)
168         self.tails_thread = TailsconfigThread()
169         self.tails_thread.signal.connect(self.tails_status)
170         self.setup_thread = SetupThread()
171         self.setup_thread.signal.connect(self.setup_status)
172 
173     # At the end of this function, we will try to do tailsconfig.
174     # A new slot will handle tailsconfig output
175     def setup_status(self, result):
176         "This is the slot for setup thread"
177         self.output += result['output']
178         self.update_success = result['status']
179         self.failure_reason = result['failure_reason']
180         self.progressBar.setProperty("value", 60)
181         self.plainTextEdit.setPlainText(self.output)
182         self.plainTextEdit.setReadOnly = True
183         if not self.update_success:  # Failed to do setup
184             self.pushButton.setEnabled(True)
185             self.pushButton_2.setEnabled(True)
186             self.update_status_bar_and_output(self.failure_reason)
187             self.progressBar.setProperty("value", 0)
188             self.alert_failure(self.failure_reason)
189             return
190         self.progressBar.setProperty("value", 70)
191         self.call_tailsconfig()
192 
193     # This will update the output text after the git commands.
194     def update_status(self, result):
195         "This is the slot for update thread"
196         self.output += result['output']
197         self.update_success = result['status']
198         self.failure_reason = result['failure_reason']
199         self.progressBar.setProperty("value", 40)
200         self.plainTextEdit.setPlainText(self.output)
201         self.plainTextEdit.setReadOnly = True
202         if not self.update_success:  # Failed to do update
203             self.pushButton.setEnabled(True)
204             self.pushButton_2.setEnabled(True)
205             self.update_status_bar_and_output(self.failure_reason)
206             self.progressBar.setProperty("value", 0)
207             self.alert_failure(self.failure_reason)
208             return
209         self.progressBar.setProperty("value", 50)
210         self.update_status_bar_and_output(strings.doing_setup)
211         self.setup_thread.start()
212 
213     def update_status_bar_and_output(self, status_message):
214         """This method updates the status bar and the output window with the
215         status_message."""
216         self.statusbar.showMessage(status_message)
217         self.output += status_message + '\n'
218         self.plainTextEdit.setPlainText(self.output)
219 
220     def call_tailsconfig(self):
221         # Now let us work on tailsconfig part
222         if self.update_success:
223             # Get sudo password and add an enter key as tailsconfig command
224             # expects
225             sudo_password = self.get_sudo_password()
226             if not sudo_password:
227                 self.update_success = False
228                 self.failure_reason = strings.missing_sudo_password
229                 self.on_failure()
230                 return
231             self.tails_thread.sudo_password = sudo_password + '\n'
232             self.update_status_bar_and_output(strings.updating_tails_env)
233             self.tails_thread.start()
234         else:
235             self.on_failure()
236 
237     def tails_status(self, result):
238         "This is the slot for Tailsconfig thread"
239         self.output += result['output']
240         self.update_success = result['status']
241         self.failure_reason = result['failure_reason']
242         self.plainTextEdit.setPlainText(self.output)
243         self.progressBar.setProperty("value", 80)
244         if self.update_success:
245             # Remove flag file indicating an update is in progress
246             os.remove(FLAG_LOCATION)
247             self.update_status_bar_and_output(strings.finished)
248             self.progressBar.setProperty("value", 100)
249             self.alert_success()
250         else:
251             self.on_failure()
252 
253     def on_failure(self):
254         self.update_status_bar_and_output(self.failure_reason)
255         self.alert_failure(self.failure_reason)
256         # Now everything is done, enable the button.
257         self.pushButton.setEnabled(True)
258         self.pushButton_2.setEnabled(True)
259         self.progressBar.setProperty("value", 0)
260 
261     def update_securedrop(self):
262         self.pushButton_2.setEnabled(False)
263         self.pushButton.setEnabled(False)
264         self.progressBar.setProperty("value", 10)
265         self.update_status_bar_and_output(strings.fetching_update)
266         self.update_thread.start()
267 
268     def alert_success(self):
269         self.success_dialog = QtWidgets.QMessageBox()
270         self.success_dialog.setIcon(QtWidgets.QMessageBox.Information)
271         self.success_dialog.setText(strings.finished_dialog_message)
272         self.success_dialog.setWindowTitle(strings.finished_dialog_title)
273         self.success_dialog.show()
274 
275     def alert_failure(self, failure_reason):
276         self.error_dialog = QtWidgets.QMessageBox()
277         self.error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
278         self.error_dialog.setText(self.failure_reason)
279         self.error_dialog.setWindowTitle(strings.update_failed_dialog_title)
280         self.error_dialog.show()
281 
282     def get_sudo_password(self):
283         sudo_password, ok_is_pressed = QtWidgets.QInputDialog.getText(
284             self, "Tails Administrator password", strings.sudo_password_text,
285             QtWidgets.QLineEdit.Password, "")
286         if ok_is_pressed and sudo_password:
287             return sudo_password
288         else:
289             return None
290
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/journalist_gui/journalist_gui/SecureDropUpdater.py b/journalist_gui/journalist_gui/SecureDropUpdater.py
--- a/journalist_gui/journalist_gui/SecureDropUpdater.py
+++ b/journalist_gui/journalist_gui/SecureDropUpdater.py
@@ -5,6 +5,8 @@
 import os
 import re
 import pexpect
+import socket
+import sys
 
 from journalist_gui import updaterUI, strings, resources_rc  # noqa
 
@@ -13,6 +15,25 @@
 ESCAPE_POD = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
 
 
+def prevent_second_instance(app: QtWidgets.QApplication, name: str) -> None:  # noqa
+
+    # 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')

{"golden_diff": "diff --git a/journalist_gui/journalist_gui/SecureDropUpdater.py b/journalist_gui/journalist_gui/SecureDropUpdater.py\n--- a/journalist_gui/journalist_gui/SecureDropUpdater.py\n+++ b/journalist_gui/journalist_gui/SecureDropUpdater.py\n@@ -5,6 +5,8 @@\n import os\n import re\n import pexpect\n+import socket\n+import sys\n \n from journalist_gui import updaterUI, strings, resources_rc  # noqa\n \n@@ -13,6 +15,25 @@\n ESCAPE_POD = re.compile(r'\\x1B\\[[0-?]*[ -/]*[@-~]')\n \n \n+def prevent_second_instance(app: QtWidgets.QApplication, name: str) -> None:  # noqa\n+\n+    # Null byte triggers abstract namespace\n+    IDENTIFIER = '\\0' + name\n+    ALREADY_BOUND_ERRNO = 98\n+\n+    app.instance_binding = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)\n+    try:\n+        app.instance_binding.bind(IDENTIFIER)\n+    except OSError as e:\n+        if e.errno == ALREADY_BOUND_ERRNO:\n+            err_dialog = QtWidgets.QMessageBox()\n+            err_dialog.setText(name + ' is already running.')\n+            err_dialog.exec()\n+            sys.exit()\n+        else:\n+            raise\n+\n+\n class SetupThread(QThread):\n     signal = pyqtSignal('PyQt_PyObject')\n", "issue": "On the Admin Workstation,  N SecureDrop updater processes are started if N network interfaces are enabled in Tails\n## Description\r\n\r\nIf a Tails Admin Workstation has more than one network interface, the SecureDrop network manager hook that checks for updates will run for each active NIC. (For example, if a workstation has both Ethernet and Wifi enabled.)\r\n\r\nThis is confusing to end users and may result in multiple update processes clobbering each other's changes.\r\n\r\n## Steps to Reproduce\r\n\r\nOn an Admin Workstation:\r\n- revert to an earlier SecureDrop version:\r\n```\r\ncd ~/Persistent/securedrop\r\ngit checkout 0.10.0\r\n```\r\n- enable multiple network connections (eg Ethernet and Wifi) and wait for their Tor connections to come up\r\n\r\n## Expected Behavior\r\nA single instance of the Securedrop Updater is started.\r\n\r\n## Actual Behavior\r\nMultiple instances of the SecureDrop Updater are started.\r\n\n", "before_files": [{"content": "#!/usr/bin/python\nfrom PyQt5 import QtGui, QtWidgets\nfrom PyQt5.QtCore import QThread, pyqtSignal\nimport subprocess\nimport os\nimport re\nimport pexpect\n\nfrom journalist_gui import updaterUI, strings, resources_rc  # noqa\n\n\nFLAG_LOCATION = \"/home/amnesia/Persistent/.securedrop/securedrop_update.flag\"  # noqa\nESCAPE_POD = re.compile(r'\\x1B\\[[0-?]*[ -/]*[@-~]')\n\n\nclass SetupThread(QThread):\n    signal = pyqtSignal('PyQt_PyObject')\n\n    def __init__(self):\n        QThread.__init__(self)\n        self.output = \"\"\n        self.update_success = False\n        self.failure_reason = \"\"\n\n    def run(self):\n        sdadmin_path = '/home/amnesia/Persistent/securedrop/securedrop-admin'\n        update_command = [sdadmin_path, 'setup']\n\n        # Create flag file to indicate we should resume failed updates on\n        # reboot. Don't create the flag if it already exists.\n        if not os.path.exists(FLAG_LOCATION):\n            open(FLAG_LOCATION, 'a').close()\n\n        try:\n            self.output = subprocess.check_output(\n                update_command,\n                stderr=subprocess.STDOUT).decode('utf-8')\n            if 'Failed to install' in self.output:\n                self.update_success = False\n                self.failure_reason = strings.update_failed_generic_reason\n            else:\n                self.update_success = True\n        except subprocess.CalledProcessError as e:\n            self.output += e.output.decode('utf-8')\n            self.update_success = False\n            self.failure_reason = strings.update_failed_generic_reason\n        result = {'status': self.update_success,\n                  'output': self.output,\n                  'failure_reason': self.failure_reason}\n        self.signal.emit(result)\n\n\n# This thread will handle the ./securedrop-admin update command\nclass UpdateThread(QThread):\n    signal = pyqtSignal('PyQt_PyObject')\n\n    def __init__(self):\n        QThread.__init__(self)\n        self.output = \"\"\n        self.update_success = False\n        self.failure_reason = \"\"\n\n    def run(self):\n        sdadmin_path = '/home/amnesia/Persistent/securedrop/securedrop-admin'\n        update_command = [sdadmin_path, 'update']\n        try:\n            self.output = subprocess.check_output(\n                update_command,\n                stderr=subprocess.STDOUT).decode('utf-8')\n            if \"Signature verification successful\" in self.output:\n                self.update_success = True\n            else:\n                self.failure_reason = strings.update_failed_generic_reason\n        except subprocess.CalledProcessError as e:\n            self.update_success = False\n            self.output += e.output.decode('utf-8')\n            if 'Signature verification failed' in self.output:\n                self.failure_reason = strings.update_failed_sig_failure\n            else:\n                self.failure_reason = strings.update_failed_generic_reason\n        result = {'status': self.update_success,\n                  'output': self.output,\n                  'failure_reason': self.failure_reason}\n        self.signal.emit(result)\n\n\n# This thread will handle the ./securedrop-admin tailsconfig command\nclass TailsconfigThread(QThread):\n    signal = pyqtSignal('PyQt_PyObject')\n\n    def __init__(self):\n        QThread.__init__(self)\n        self.output = \"\"\n        self.update_success = False\n        self.failure_reason = \"\"\n        self.sudo_password = \"\"\n\n    def run(self):\n        tailsconfig_command = (\"/home/amnesia/Persistent/\"\n                               \"securedrop/securedrop-admin \"\n                               \"tailsconfig\")\n        try:\n            child = pexpect.spawn(tailsconfig_command)\n            child.expect('SUDO password:')\n            self.output += child.before.decode('utf-8')\n            child.sendline(self.sudo_password)\n            child.expect(pexpect.EOF)\n            self.output += child.before.decode('utf-8')\n            child.close()\n\n            # For Tailsconfig to be considered a success, we expect no\n            # failures in the Ansible output.\n            if child.exitstatus:\n                self.update_success = False\n                self.failure_reason = strings.tailsconfig_failed_generic_reason  # noqa\n            else:\n                self.update_success = True\n        except pexpect.exceptions.TIMEOUT:\n            self.update_success = False\n            self.failure_reason = strings.tailsconfig_failed_sudo_password\n\n        except subprocess.CalledProcessError:\n            self.update_success = False\n            self.failure_reason = strings.tailsconfig_failed_generic_reason\n        result = {'status': self.update_success,\n                  'output': ESCAPE_POD.sub('', self.output),\n                  'failure_reason': self.failure_reason}\n        self.signal.emit(result)\n\n\nclass UpdaterApp(QtWidgets.QMainWindow, updaterUI.Ui_MainWindow):\n\n    def __init__(self, parent=None):\n        super(UpdaterApp, self).__init__(parent)\n        self.setupUi(self)\n        self.statusbar.setSizeGripEnabled(False)\n        self.output = strings.initial_text_box\n        self.plainTextEdit.setPlainText(self.output)\n        self.update_success = False\n\n        pixmap = QtGui.QPixmap(\":/images/static/banner.png\")\n        self.label_2.setPixmap(pixmap)\n        self.label_2.setScaledContents(True)\n\n        self.progressBar.setProperty(\"value\", 0)\n        self.setWindowTitle(strings.window_title)\n        self.setWindowIcon(QtGui.QIcon(':/images/static/securedrop_icon.png'))\n        self.label.setText(strings.update_in_progress)\n\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab),\n                                  strings.main_tab)\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2),\n                                  strings.output_tab)\n\n        # Connect buttons to their functions.\n        self.pushButton.setText(strings.install_later_button)\n        self.pushButton.setStyleSheet(\"\"\"background-color: lightgrey;\n                                      min-height: 2em;\n                                      border-radius: 10px\"\"\")\n        self.pushButton.clicked.connect(self.close)\n        self.pushButton_2.setText(strings.install_update_button)\n        self.pushButton_2.setStyleSheet(\"\"\"background-color: #E6FFEB;\n                                        min-height: 2em;\n                                        border-radius: 10px;\"\"\")\n        self.pushButton_2.clicked.connect(self.update_securedrop)\n        self.update_thread = UpdateThread()\n        self.update_thread.signal.connect(self.update_status)\n        self.tails_thread = TailsconfigThread()\n        self.tails_thread.signal.connect(self.tails_status)\n        self.setup_thread = SetupThread()\n        self.setup_thread.signal.connect(self.setup_status)\n\n    # At the end of this function, we will try to do tailsconfig.\n    # A new slot will handle tailsconfig output\n    def setup_status(self, result):\n        \"This is the slot for setup thread\"\n        self.output += result['output']\n        self.update_success = result['status']\n        self.failure_reason = result['failure_reason']\n        self.progressBar.setProperty(\"value\", 60)\n        self.plainTextEdit.setPlainText(self.output)\n        self.plainTextEdit.setReadOnly = True\n        if not self.update_success:  # Failed to do setup\n            self.pushButton.setEnabled(True)\n            self.pushButton_2.setEnabled(True)\n            self.update_status_bar_and_output(self.failure_reason)\n            self.progressBar.setProperty(\"value\", 0)\n            self.alert_failure(self.failure_reason)\n            return\n        self.progressBar.setProperty(\"value\", 70)\n        self.call_tailsconfig()\n\n    # This will update the output text after the git commands.\n    def update_status(self, result):\n        \"This is the slot for update thread\"\n        self.output += result['output']\n        self.update_success = result['status']\n        self.failure_reason = result['failure_reason']\n        self.progressBar.setProperty(\"value\", 40)\n        self.plainTextEdit.setPlainText(self.output)\n        self.plainTextEdit.setReadOnly = True\n        if not self.update_success:  # Failed to do update\n            self.pushButton.setEnabled(True)\n            self.pushButton_2.setEnabled(True)\n            self.update_status_bar_and_output(self.failure_reason)\n            self.progressBar.setProperty(\"value\", 0)\n            self.alert_failure(self.failure_reason)\n            return\n        self.progressBar.setProperty(\"value\", 50)\n        self.update_status_bar_and_output(strings.doing_setup)\n        self.setup_thread.start()\n\n    def update_status_bar_and_output(self, status_message):\n        \"\"\"This method updates the status bar and the output window with the\n        status_message.\"\"\"\n        self.statusbar.showMessage(status_message)\n        self.output += status_message + '\\n'\n        self.plainTextEdit.setPlainText(self.output)\n\n    def call_tailsconfig(self):\n        # Now let us work on tailsconfig part\n        if self.update_success:\n            # Get sudo password and add an enter key as tailsconfig command\n            # expects\n            sudo_password = self.get_sudo_password()\n            if not sudo_password:\n                self.update_success = False\n                self.failure_reason = strings.missing_sudo_password\n                self.on_failure()\n                return\n            self.tails_thread.sudo_password = sudo_password + '\\n'\n            self.update_status_bar_and_output(strings.updating_tails_env)\n            self.tails_thread.start()\n        else:\n            self.on_failure()\n\n    def tails_status(self, result):\n        \"This is the slot for Tailsconfig thread\"\n        self.output += result['output']\n        self.update_success = result['status']\n        self.failure_reason = result['failure_reason']\n        self.plainTextEdit.setPlainText(self.output)\n        self.progressBar.setProperty(\"value\", 80)\n        if self.update_success:\n            # Remove flag file indicating an update is in progress\n            os.remove(FLAG_LOCATION)\n            self.update_status_bar_and_output(strings.finished)\n            self.progressBar.setProperty(\"value\", 100)\n            self.alert_success()\n        else:\n            self.on_failure()\n\n    def on_failure(self):\n        self.update_status_bar_and_output(self.failure_reason)\n        self.alert_failure(self.failure_reason)\n        # Now everything is done, enable the button.\n        self.pushButton.setEnabled(True)\n        self.pushButton_2.setEnabled(True)\n        self.progressBar.setProperty(\"value\", 0)\n\n    def update_securedrop(self):\n        self.pushButton_2.setEnabled(False)\n        self.pushButton.setEnabled(False)\n        self.progressBar.setProperty(\"value\", 10)\n        self.update_status_bar_and_output(strings.fetching_update)\n        self.update_thread.start()\n\n    def alert_success(self):\n        self.success_dialog = QtWidgets.QMessageBox()\n        self.success_dialog.setIcon(QtWidgets.QMessageBox.Information)\n        self.success_dialog.setText(strings.finished_dialog_message)\n        self.success_dialog.setWindowTitle(strings.finished_dialog_title)\n        self.success_dialog.show()\n\n    def alert_failure(self, failure_reason):\n        self.error_dialog = QtWidgets.QMessageBox()\n        self.error_dialog.setIcon(QtWidgets.QMessageBox.Critical)\n        self.error_dialog.setText(self.failure_reason)\n        self.error_dialog.setWindowTitle(strings.update_failed_dialog_title)\n        self.error_dialog.show()\n\n    def get_sudo_password(self):\n        sudo_password, ok_is_pressed = QtWidgets.QInputDialog.getText(\n            self, \"Tails Administrator password\", strings.sudo_password_text,\n            QtWidgets.QLineEdit.Password, \"\")\n        if ok_is_pressed and sudo_password:\n            return sudo_password\n        else:\n            return None\n", "path": "journalist_gui/journalist_gui/SecureDropUpdater.py"}], "after_files": [{"content": "#!/usr/bin/python\nfrom PyQt5 import QtGui, QtWidgets\nfrom PyQt5.QtCore import QThread, pyqtSignal\nimport subprocess\nimport os\nimport re\nimport pexpect\nimport socket\nimport sys\n\nfrom journalist_gui import updaterUI, strings, resources_rc  # noqa\n\n\nFLAG_LOCATION = \"/home/amnesia/Persistent/.securedrop/securedrop_update.flag\"  # noqa\nESCAPE_POD = re.compile(r'\\x1B\\[[0-?]*[ -/]*[@-~]')\n\n\ndef prevent_second_instance(app: QtWidgets.QApplication, name: str) -> None:  # noqa\n\n    # Null byte triggers abstract namespace\n    IDENTIFIER = '\\0' + name\n    ALREADY_BOUND_ERRNO = 98\n\n    app.instance_binding = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)\n    try:\n        app.instance_binding.bind(IDENTIFIER)\n    except OSError as e:\n        if e.errno == ALREADY_BOUND_ERRNO:\n            err_dialog = QtWidgets.QMessageBox()\n            err_dialog.setText(name + ' is already running.')\n            err_dialog.exec()\n            sys.exit()\n        else:\n            raise\n\n\nclass SetupThread(QThread):\n    signal = pyqtSignal('PyQt_PyObject')\n\n    def __init__(self):\n        QThread.__init__(self)\n        self.output = \"\"\n        self.update_success = False\n        self.failure_reason = \"\"\n\n    def run(self):\n        sdadmin_path = '/home/amnesia/Persistent/securedrop/securedrop-admin'\n        update_command = [sdadmin_path, 'setup']\n\n        # Create flag file to indicate we should resume failed updates on\n        # reboot. Don't create the flag if it already exists.\n        if not os.path.exists(FLAG_LOCATION):\n            open(FLAG_LOCATION, 'a').close()\n\n        try:\n            self.output = subprocess.check_output(\n                update_command,\n                stderr=subprocess.STDOUT).decode('utf-8')\n            if 'Failed to install' in self.output:\n                self.update_success = False\n                self.failure_reason = strings.update_failed_generic_reason\n            else:\n                self.update_success = True\n        except subprocess.CalledProcessError as e:\n            self.output += e.output.decode('utf-8')\n            self.update_success = False\n            self.failure_reason = strings.update_failed_generic_reason\n        result = {'status': self.update_success,\n                  'output': self.output,\n                  'failure_reason': self.failure_reason}\n        self.signal.emit(result)\n\n\n# This thread will handle the ./securedrop-admin update command\nclass UpdateThread(QThread):\n    signal = pyqtSignal('PyQt_PyObject')\n\n    def __init__(self):\n        QThread.__init__(self)\n        self.output = \"\"\n        self.update_success = False\n        self.failure_reason = \"\"\n\n    def run(self):\n        sdadmin_path = '/home/amnesia/Persistent/securedrop/securedrop-admin'\n        update_command = [sdadmin_path, 'update']\n        try:\n            self.output = subprocess.check_output(\n                update_command,\n                stderr=subprocess.STDOUT).decode('utf-8')\n            if \"Signature verification successful\" in self.output:\n                self.update_success = True\n            else:\n                self.failure_reason = strings.update_failed_generic_reason\n        except subprocess.CalledProcessError as e:\n            self.update_success = False\n            self.output += e.output.decode('utf-8')\n            if 'Signature verification failed' in self.output:\n                self.failure_reason = strings.update_failed_sig_failure\n            else:\n                self.failure_reason = strings.update_failed_generic_reason\n        result = {'status': self.update_success,\n                  'output': self.output,\n                  'failure_reason': self.failure_reason}\n        self.signal.emit(result)\n\n\n# This thread will handle the ./securedrop-admin tailsconfig command\nclass TailsconfigThread(QThread):\n    signal = pyqtSignal('PyQt_PyObject')\n\n    def __init__(self):\n        QThread.__init__(self)\n        self.output = \"\"\n        self.update_success = False\n        self.failure_reason = \"\"\n        self.sudo_password = \"\"\n\n    def run(self):\n        tailsconfig_command = (\"/home/amnesia/Persistent/\"\n                               \"securedrop/securedrop-admin \"\n                               \"tailsconfig\")\n        try:\n            child = pexpect.spawn(tailsconfig_command)\n            child.expect('SUDO password:')\n            self.output += child.before.decode('utf-8')\n            child.sendline(self.sudo_password)\n            child.expect(pexpect.EOF)\n            self.output += child.before.decode('utf-8')\n            child.close()\n\n            # For Tailsconfig to be considered a success, we expect no\n            # failures in the Ansible output.\n            if child.exitstatus:\n                self.update_success = False\n                self.failure_reason = strings.tailsconfig_failed_generic_reason  # noqa\n            else:\n                self.update_success = True\n        except pexpect.exceptions.TIMEOUT:\n            self.update_success = False\n            self.failure_reason = strings.tailsconfig_failed_sudo_password\n\n        except subprocess.CalledProcessError:\n            self.update_success = False\n            self.failure_reason = strings.tailsconfig_failed_generic_reason\n        result = {'status': self.update_success,\n                  'output': ESCAPE_POD.sub('', self.output),\n                  'failure_reason': self.failure_reason}\n        self.signal.emit(result)\n\n\nclass UpdaterApp(QtWidgets.QMainWindow, updaterUI.Ui_MainWindow):\n\n    def __init__(self, parent=None):\n        super(UpdaterApp, self).__init__(parent)\n        self.setupUi(self)\n        self.statusbar.setSizeGripEnabled(False)\n        self.output = strings.initial_text_box\n        self.plainTextEdit.setPlainText(self.output)\n        self.update_success = False\n\n        pixmap = QtGui.QPixmap(\":/images/static/banner.png\")\n        self.label_2.setPixmap(pixmap)\n        self.label_2.setScaledContents(True)\n\n        self.progressBar.setProperty(\"value\", 0)\n        self.setWindowTitle(strings.window_title)\n        self.setWindowIcon(QtGui.QIcon(':/images/static/securedrop_icon.png'))\n        self.label.setText(strings.update_in_progress)\n\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab),\n                                  strings.main_tab)\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2),\n                                  strings.output_tab)\n\n        # Connect buttons to their functions.\n        self.pushButton.setText(strings.install_later_button)\n        self.pushButton.setStyleSheet(\"\"\"background-color: lightgrey;\n                                      min-height: 2em;\n                                      border-radius: 10px\"\"\")\n        self.pushButton.clicked.connect(self.close)\n        self.pushButton_2.setText(strings.install_update_button)\n        self.pushButton_2.setStyleSheet(\"\"\"background-color: #E6FFEB;\n                                        min-height: 2em;\n                                        border-radius: 10px;\"\"\")\n        self.pushButton_2.clicked.connect(self.update_securedrop)\n        self.update_thread = UpdateThread()\n        self.update_thread.signal.connect(self.update_status)\n        self.tails_thread = TailsconfigThread()\n        self.tails_thread.signal.connect(self.tails_status)\n        self.setup_thread = SetupThread()\n        self.setup_thread.signal.connect(self.setup_status)\n\n    # At the end of this function, we will try to do tailsconfig.\n    # A new slot will handle tailsconfig output\n    def setup_status(self, result):\n        \"This is the slot for setup thread\"\n        self.output += result['output']\n        self.update_success = result['status']\n        self.failure_reason = result['failure_reason']\n        self.progressBar.setProperty(\"value\", 60)\n        self.plainTextEdit.setPlainText(self.output)\n        self.plainTextEdit.setReadOnly = True\n        if not self.update_success:  # Failed to do setup\n            self.pushButton.setEnabled(True)\n            self.pushButton_2.setEnabled(True)\n            self.update_status_bar_and_output(self.failure_reason)\n            self.progressBar.setProperty(\"value\", 0)\n            self.alert_failure(self.failure_reason)\n            return\n        self.progressBar.setProperty(\"value\", 70)\n        self.call_tailsconfig()\n\n    # This will update the output text after the git commands.\n    def update_status(self, result):\n        \"This is the slot for update thread\"\n        self.output += result['output']\n        self.update_success = result['status']\n        self.failure_reason = result['failure_reason']\n        self.progressBar.setProperty(\"value\", 40)\n        self.plainTextEdit.setPlainText(self.output)\n        self.plainTextEdit.setReadOnly = True\n        if not self.update_success:  # Failed to do update\n            self.pushButton.setEnabled(True)\n            self.pushButton_2.setEnabled(True)\n            self.update_status_bar_and_output(self.failure_reason)\n            self.progressBar.setProperty(\"value\", 0)\n            self.alert_failure(self.failure_reason)\n            return\n        self.progressBar.setProperty(\"value\", 50)\n        self.update_status_bar_and_output(strings.doing_setup)\n        self.setup_thread.start()\n\n    def update_status_bar_and_output(self, status_message):\n        \"\"\"This method updates the status bar and the output window with the\n        status_message.\"\"\"\n        self.statusbar.showMessage(status_message)\n        self.output += status_message + '\\n'\n        self.plainTextEdit.setPlainText(self.output)\n\n    def call_tailsconfig(self):\n        # Now let us work on tailsconfig part\n        if self.update_success:\n            # Get sudo password and add an enter key as tailsconfig command\n            # expects\n            sudo_password = self.get_sudo_password()\n            if not sudo_password:\n                self.update_success = False\n                self.failure_reason = strings.missing_sudo_password\n                self.on_failure()\n                return\n            self.tails_thread.sudo_password = sudo_password + '\\n'\n            self.update_status_bar_and_output(strings.updating_tails_env)\n            self.tails_thread.start()\n        else:\n            self.on_failure()\n\n    def tails_status(self, result):\n        \"This is the slot for Tailsconfig thread\"\n        self.output += result['output']\n        self.update_success = result['status']\n        self.failure_reason = result['failure_reason']\n        self.plainTextEdit.setPlainText(self.output)\n        self.progressBar.setProperty(\"value\", 80)\n        if self.update_success:\n            # Remove flag file indicating an update is in progress\n            os.remove(FLAG_LOCATION)\n            self.update_status_bar_and_output(strings.finished)\n            self.progressBar.setProperty(\"value\", 100)\n            self.alert_success()\n        else:\n            self.on_failure()\n\n    def on_failure(self):\n        self.update_status_bar_and_output(self.failure_reason)\n        self.alert_failure(self.failure_reason)\n        # Now everything is done, enable the button.\n        self.pushButton.setEnabled(True)\n        self.pushButton_2.setEnabled(True)\n        self.progressBar.setProperty(\"value\", 0)\n\n    def update_securedrop(self):\n        self.pushButton_2.setEnabled(False)\n        self.pushButton.setEnabled(False)\n        self.progressBar.setProperty(\"value\", 10)\n        self.update_status_bar_and_output(strings.fetching_update)\n        self.update_thread.start()\n\n    def alert_success(self):\n        self.success_dialog = QtWidgets.QMessageBox()\n        self.success_dialog.setIcon(QtWidgets.QMessageBox.Information)\n        self.success_dialog.setText(strings.finished_dialog_message)\n        self.success_dialog.setWindowTitle(strings.finished_dialog_title)\n        self.success_dialog.show()\n\n    def alert_failure(self, failure_reason):\n        self.error_dialog = QtWidgets.QMessageBox()\n        self.error_dialog.setIcon(QtWidgets.QMessageBox.Critical)\n        self.error_dialog.setText(self.failure_reason)\n        self.error_dialog.setWindowTitle(strings.update_failed_dialog_title)\n        self.error_dialog.show()\n\n    def get_sudo_password(self):\n        sudo_password, ok_is_pressed = QtWidgets.QInputDialog.getText(\n            self, \"Tails Administrator password\", strings.sudo_password_text,\n            QtWidgets.QLineEdit.Password, \"\")\n        if ok_is_pressed and sudo_password:\n            return sudo_password\n        else:\n            return None\n", "path": "journalist_gui/journalist_gui/SecureDropUpdater.py"}]}