diff --git a/onionshare/web.py b/onionshare/web.py
index 10c130cb..38ad398e 100644
--- a/onionshare/web.py
+++ b/onionshare/web.py
@@ -104,6 +104,7 @@ class Web(object):
self.file_info = []
self.zip_filename = None
self.zip_filesize = None
+ self.zip_writer = None
self.security_headers = [
('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'),
@@ -516,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:
@@ -534,15 +537,23 @@ 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 False
+
for info in self.file_info['dirs']:
- z.add_dir(info['filename'])
- z.close()
- self.zip_filename = z.zip_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:
@@ -653,6 +664,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 +693,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 False
+
full_filename = os.path.join(dirpath, f)
if not os.path.islink(full_filename):
arc_filename = full_filename[len(dir_to_strip):]
@@ -688,6 +704,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.
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..65ce1d52 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,14 @@ 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.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..f4acc5e1
--- /dev/null
+++ b/onionshare_gui/threads.py
@@ -0,0 +1,76 @@
+# -*- 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
+
+from onionshare.onion import *
+
+
+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'))