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

This commit is contained in:
Micah Lee 2018-09-17 20:55:54 -07:00
parent 06f90b91ce
commit 174de57405
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
6 changed files with 196 additions and 106 deletions

View File

@ -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):]

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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.

View File

@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
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 <http://www.gnu.org/licenses/>.
"""
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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.

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
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 <http://www.gnu.org/licenses/>.
"""
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

74
onionshare_gui/threads.py Normal file
View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
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 <http://www.gnu.org/licenses/>.
"""
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'))