From 174de57405ba176a873e1cc1cb08732dd7d207f9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 20:55:54 -0700 Subject: [PATCH 1/5] Refactor all of the threading.Threads into QThreads, and quit them all when canceling the server. When canceling the compression thread, specifically mass a cancel message into the Web and ZipWriter objects to make the bail out on compression early --- onionshare/web.py | 25 ++++++--- onionshare_gui/mode.py | 55 +++++++++----------- onionshare_gui/onion_thread.py | 45 ---------------- onionshare_gui/share_mode/__init__.py | 43 ++++++++-------- onionshare_gui/share_mode/threads.py | 60 ++++++++++++++++++++++ onionshare_gui/threads.py | 74 +++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 106 deletions(-) delete mode 100644 onionshare_gui/onion_thread.py create mode 100644 onionshare_gui/share_mode/threads.py create mode 100644 onionshare_gui/threads.py diff --git a/onionshare/web.py b/onionshare/web.py index 10c130cb..e24e4665 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -104,6 +104,8 @@ class Web(object): self.file_info = [] self.zip_filename = None self.zip_filesize = None + self.zip_writer = None + self.cancel_compression = False self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), @@ -534,14 +536,20 @@ class Web(object): self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - # zip up the files and folders - z = ZipWriter(self.common, processed_size_callback=processed_size_callback) + # Zip up the files and folders + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) + self.zip_filename = self.zip_writer.zip_filename for info in self.file_info['files']: - z.add_file(info['filename']) + self.zip_writer.add_file(info['filename']) + # Canceling early? + if self.cancel_compression: + self.zip_writer.close() + return + for info in self.file_info['dirs']: - z.add_dir(info['filename']) - z.close() - self.zip_filename = z.zip_filename + self.zip_writer.add_dir(info['filename']) + + self.zip_writer.close() self.zip_filesize = os.path.getsize(self.zip_filename) def _safe_select_jinja_autoescape(self, filename): @@ -653,6 +661,7 @@ class ZipWriter(object): """ def __init__(self, common, zip_filename=None, processed_size_callback=None): self.common = common + self.cancel_compression = False if zip_filename: self.zip_filename = zip_filename @@ -681,6 +690,10 @@ class ZipWriter(object): dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' for dirpath, dirnames, filenames in os.walk(filename): for f in filenames: + # Canceling early? + if self.cancel_compression: + return + full_filename = os.path.join(dirpath, f) if not os.path.islink(full_filename): arc_filename = full_filename[len(dir_to_strip):] diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 418afffd..feb2f5b6 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -17,15 +17,13 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import time -import threading from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.common import ShutdownTimer from .server_status import ServerStatus -from .onion_thread import OnionThread +from .threads import OnionThread from .widgets import Alert class Mode(QtWidgets.QWidget): @@ -56,6 +54,10 @@ class Mode(QtWidgets.QWidget): # The web object gets created in init() self.web = None + # Threads start out as None + self.onion_thread = None + self.web_thread = None + # Server status self.server_status = ServerStatus(self.common, self.qtapp, self.app) self.server_status.server_started.connect(self.start_server) @@ -138,34 +140,11 @@ class Mode(QtWidgets.QWidget): self.status_bar.clearMessage() self.server_status_label.setText('') - # Start the onion service in a new thread - def start_onion_service(self): - # Choose a port for the web app - self.app.choose_port() - - # Start http service in new thread - t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug'))) - t.daemon = True - t.start() - - # Wait for the web app slug to generate before continuing - if not self.common.settings.get('public_mode'): - while self.web.slug == None: - time.sleep(0.1) - - # Now start the onion service - try: - self.app.start_onion_service() - self.starting_server_step2.emit() - - except Exception as e: - self.starting_server_error.emit(e.args[0]) - return - self.common.log('Mode', 'start_server', 'Starting an onion thread') - self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self}) - self.t.daemon = True - self.t.start() + self.onion_thread = OnionThread(self) + self.onion_thread.success.connect(self.starting_server_step2.emit) + self.onion_thread.error.connect(self.starting_server_error.emit) + self.onion_thread.start() def start_server_custom(self): """ @@ -243,10 +222,22 @@ class Mode(QtWidgets.QWidget): """ Cancel the server while it is preparing to start """ - if self.t: - self.t.quit() + self.cancel_server_custom() + + if self.onion_thread: + self.common.log('Mode', 'cancel_server: quitting onion thread') + self.onion_thread.quit() + if self.web_thread: + self.common.log('Mode', 'cancel_server: quitting web thread') + self.web_thread.quit() self.stop_server() + def cancel_server_custom(self): + """ + Add custom initialization here. + """ + pass + def stop_server(self): """ Stop the onionshare server. diff --git a/onionshare_gui/onion_thread.py b/onionshare_gui/onion_thread.py deleted file mode 100644 index 0a25e891..00000000 --- a/onionshare_gui/onion_thread.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" -from PyQt5 import QtCore - -class OnionThread(QtCore.QThread): - """ - A QThread for starting our Onion Service. - By using QThread rather than threading.Thread, we are able - to call quit() or terminate() on the startup if the user - decided to cancel (in which case do not proceed with obtaining - the Onion address and starting the web server). - """ - def __init__(self, common, function, kwargs=None): - super(OnionThread, self).__init__() - - self.common = common - - self.common.log('OnionThread', '__init__') - self.function = function - if not kwargs: - self.kwargs = {} - else: - self.kwargs = kwargs - - def run(self): - self.common.log('OnionThread', 'run') - - self.function(**self.kwargs) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 37315bbe..d43fe99f 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -17,7 +17,6 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import threading import os from PyQt5 import QtCore, QtWidgets, QtGui @@ -28,6 +27,7 @@ from onionshare.web import Web from .file_selection import FileSelection from .downloads import Downloads +from .threads import CompressThread from ..mode import Mode from ..widgets import Alert @@ -39,6 +39,9 @@ class ShareMode(Mode): """ Custom initialization for ReceiveMode. """ + # Threads start out as None + self.compress_thread = None + # Create the Web object self.web = Web(self.common, True, False) @@ -161,28 +164,13 @@ class ShareMode(Mode): self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames) self.status_bar.insertWidget(0, self._zip_progress_bar) - # Prepare the files for sending in a new thread - def finish_starting_server(self): - # Prepare files to share - def _set_processed_size(x): - if self._zip_progress_bar != None: - self._zip_progress_bar.update_processed_size_signal.emit(x) - - try: - self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) - self.app.cleanup_filenames.append(self.web.zip_filename) - - # Only continue if the server hasn't been canceled - if self.server_status.status != self.server_status.STATUS_STOPPED: - self.starting_server_step3.emit() - self.start_server_finished.emit() - except OSError as e: - self.starting_server_error.emit(e.strerror) - return - - t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) - t.daemon = True - t.start() + # prepare the files for sending in a new thread + self.compress_thread = CompressThread(self) + self.compress_thread.success.connect(self.starting_server_step3.emit) + self.compress_thread.success.connect(self.start_server_finished.emit) + self.compress_thread.error.connect(self.starting_server_error.emit) + self.server_status.server_canceled.connect(self.compress_thread.cancel) + self.compress_thread.start() def start_server_step3_custom(self): """ @@ -222,6 +210,15 @@ class ShareMode(Mode): self.update_downloads_in_progress() self.file_selection.file_list.adjustSize() + def cancel_server_custom(self): + """ + Stop the compression thread on cancel + """ + if self.compress_thread: + self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') + self.compress_thread.cancel() + self.compress_thread.quit() + def handle_tor_broke_custom(self): """ Connection to Tor broke. diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py new file mode 100644 index 00000000..50789049 --- /dev/null +++ b/onionshare_gui/share_mode/threads.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from PyQt5 import QtCore + + +class CompressThread(QtCore.QThread): + """ + Compresses files to be shared + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(CompressThread, self).__init__() + self.mode = mode + self.mode.common.log('CompressThread', '__init__') + + # prepare files to share + def set_processed_size(self, x): + if self.mode._zip_progress_bar != None: + self.mode._zip_progress_bar.update_processed_size_signal.emit(x) + + def run(self): + self.mode.common.log('CompressThread', 'run') + + try: + if self.mode.web.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): + self.success.emit() + else: + # Cancelled + pass + + self.mode.app.cleanup_filenames.append(self.mode.web.zip_filename) + except OSError as e: + self.error.emit(e.strerror) + + def cancel(self): + self.mode.common.log('CompressThread', 'cancel') + + # Let the Web and ZipWriter objects know that we're canceling compression early + self.mode.web.cancel_compression = True + if self.mode.web.zip_writer: + self.mode.web.zip_writer.cancel_compression = True diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py new file mode 100644 index 00000000..3c99d395 --- /dev/null +++ b/onionshare_gui/threads.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import time +from PyQt5 import QtCore + + +class OnionThread(QtCore.QThread): + """ + Starts the onion service, and waits for it to finish + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(OnionThread, self).__init__() + self.mode = mode + self.mode.common.log('OnionThread', '__init__') + + # allow this thread to be terminated + self.setTerminationEnabled() + + def run(self): + self.mode.common.log('OnionThread', 'run') + + try: + self.mode.app.start_onion_service() + self.success.emit() + + except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: + self.error.emit(e.args[0]) + return + + self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') + + # start onionshare http service in new thread + self.mode.web_thread = WebThread(self.mode) + self.mode.web_thread.start() + + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + + +class WebThread(QtCore.QThread): + """ + Starts the web service + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(WebThread, self).__init__() + self.mode = mode + self.mode.common.log('WebThread', '__init__') + + def run(self): + self.mode.common.log('WebThread', 'run') + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('slug')) From 72f76bf659a0aeba1125f2a3d322300308633055 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 13:39:09 -0700 Subject: [PATCH 2/5] We shouldn't call CompressThread.cancel() there because it's already called in a signal --- onionshare_gui/share_mode/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index d43fe99f..65ce1d52 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -216,7 +216,6 @@ class ShareMode(Mode): """ if self.compress_thread: self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') - self.compress_thread.cancel() self.compress_thread.quit() def handle_tor_broke_custom(self): From c52c846227d892e0a68d15c3db7d09390cb3ada6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 13:42:13 -0700 Subject: [PATCH 3/5] Make Web.set_file_info return False on cancel --- onionshare/web.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index e24e4665..dc0effdb 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -544,13 +544,15 @@ class Web(object): # Canceling early? if self.cancel_compression: self.zip_writer.close() - return + return False for info in self.file_info['dirs']: - self.zip_writer.add_dir(info['filename']) + if not self.zip_writer.add_dir(info['filename']): + return False self.zip_writer.close() self.zip_filesize = os.path.getsize(self.zip_filename) + return True def _safe_select_jinja_autoescape(self, filename): if filename is None: @@ -692,8 +694,8 @@ class ZipWriter(object): for f in filenames: # Canceling early? if self.cancel_compression: - return - + return False + full_filename = os.path.join(dirpath, f) if not os.path.islink(full_filename): arc_filename = full_filename[len(dir_to_strip):] @@ -701,6 +703,8 @@ class ZipWriter(object): self._size += os.path.getsize(full_filename) self.processed_size_callback(self._size) + return True + def close(self): """ Close the zip archive. From 0234ff5f37eefb333e50fec102462a2e3d699dbc Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 16:28:54 -0700 Subject: [PATCH 4/5] Set self.cancel_compression to false in the set_file_info() function instead of Web's constructor, so it gets reset every time --- onionshare/web.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/onionshare/web.py b/onionshare/web.py index dc0effdb..38ad398e 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -105,7 +105,6 @@ class Web(object): self.zip_filename = None self.zip_filesize = None self.zip_writer = None - self.cancel_compression = False self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), @@ -518,6 +517,8 @@ class Web(object): page will need to display. This includes zipping up the file in order to get the zip file's name and size. """ + self.cancel_compression = False + # build file info list self.file_info = {'files': [], 'dirs': []} for filename in filenames: From d63808f419df38cad9e9d9bd96f80778945f24c2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 17:44:54 -0700 Subject: [PATCH 5/5] Import onion exceptions that were missing --- onionshare_gui/threads.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 3c99d395..f4acc5e1 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -20,6 +20,8 @@ along with this program. If not, see . import time from PyQt5 import QtCore +from onionshare.onion import * + class OnionThread(QtCore.QThread): """