diff --git a/onionshare_gui/onion_thread.py b/onionshare_gui/onion_thread.py new file mode 100644 index 00000000..0a25e891 --- /dev/null +++ b/onionshare_gui/onion_thread.py @@ -0,0 +1,45 @@ +# -*- 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/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 53858eeb..54c19bda 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -17,24 +17,16 @@ 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 os -import threading -import time import queue from PyQt5 import QtCore, QtWidgets, QtGui -from onionshare import strings, common -from onionshare.common import Common, ShutdownTimer -from onionshare.onion import * +from onionshare import strings from .share_mode import ShareMode from .receive_mode import ReceiveMode from .tor_connection_dialog import TorConnectionDialog from .settings_dialog import SettingsDialog -from .file_selection import FileSelection -from .server_status import ServerStatus -from .downloads import Downloads from .alert import Alert from .update_checker import UpdateThread @@ -43,12 +35,6 @@ class OnionShareGui(QtWidgets.QMainWindow): OnionShareGui is the main window for the GUI that contains all of the GUI elements. """ - start_server_finished = QtCore.pyqtSignal() - stop_server_finished = QtCore.pyqtSignal() - starting_server_step2 = QtCore.pyqtSignal() - starting_server_step3 = QtCore.pyqtSignal() - starting_server_error = QtCore.pyqtSignal(str) - MODE_SHARE = 'share' MODE_RECEIVE = 'receive' @@ -67,6 +53,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.local_only = local_only self.mode = self.MODE_SHARE + self.new_download = False # For scrolling to the bottom of the downloads list self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) @@ -119,11 +106,19 @@ class OnionShareGui(QtWidgets.QMainWindow): mode_switcher_layout.addWidget(self.receive_mode_button) mode_switcher_layout.addWidget(self.settings_button) - # Share and receive mode widgets - self.receive_mode = ReceiveMode(self.common) - self.share_mode = ReceiveMode(self.common) - - self.update_mode_switcher() + # Server status indicator on the status bar + self.server_status_image_stopped = QtGui.QImage(self.common.get_resource_path('images/server_stopped.png')) + self.server_status_image_working = QtGui.QImage(self.common.get_resource_path('images/server_working.png')) + self.server_status_image_started = QtGui.QImage(self.common.get_resource_path('images/server_started.png')) + self.server_status_image_label = QtWidgets.QLabel() + self.server_status_image_label.setFixedWidth(20) + self.server_status_label = QtWidgets.QLabel() + self.server_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; }') + server_status_indicator_layout = QtWidgets.QHBoxLayout() + server_status_indicator_layout.addWidget(self.server_status_image_label) + server_status_indicator_layout.addWidget(self.server_status_label) + self.server_status_indicator = QtWidgets.QWidget() + self.server_status_indicator.setLayout(server_status_indicator_layout) # Status bar self.status_bar = QtWidgets.QStatusBar() @@ -140,13 +135,27 @@ class OnionShareGui(QtWidgets.QMainWindow): self.status_bar.addPermanentWidget(self.server_status_indicator) self.setStatusBar(self.status_bar) - # Status bar, zip progress bar - self._zip_progress_bar = None # Status bar, sharing messages self.server_share_status_label = QtWidgets.QLabel('') self.server_share_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; padding: 2px; }') self.status_bar.insertWidget(0, self.server_share_status_label) + # Share and receive mode widgets + self.share_mode = ShareMode(self.common, filenames, qtapp, app, web, self.status_bar, self.server_share_status_label) + self.share_mode.server_status.server_started.connect(self.update_server_status_indicator) + self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator) + self.share_mode.start_server_finished.connect(self.update_server_status_indicator) + self.share_mode.stop_server_finished.connect(self.update_server_status_indicator) + self.share_mode.start_server_finished.connect(self.clear_message) + self.share_mode.server_status.button_clicked.connect(self.clear_message) + self.share_mode.server_status.url_copied.connect(self.copy_url) + self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) + self.share_mode.set_server_active.connect(self.set_server_active) + self.receive_mode = ReceiveMode(self.common) + + self.update_mode_switcher() + self.update_server_status_indicator() + # Layouts contents_layout = QtWidgets.QVBoxLayout() contents_layout.setContentsMargins(10, 10, 10, 10) @@ -168,7 +177,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # Create the timer self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.check_for_requests) + self.timer.timeout.connect(self.timer_callback) # Start the "Connecting to Tor" dialog, which calls onion.connect() tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion) @@ -209,6 +218,25 @@ class OnionShareGui(QtWidgets.QMainWindow): self.mode = self.MODE_RECEIVE self.update_mode_switcher() + def update_server_status_indicator(self): + self.common.log('OnionShareGui', 'update_server_status_indicator') + + # Share mode + if self.mode == self.MODE_SHARE: + # Set the status image + if self.share_mode.server_status.status == self.share_mode.server_status.STATUS_STOPPED: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped)) + self.server_status_label.setText(strings._('gui_status_indicator_stopped', True)) + elif self.share_mode.server_status.status == self.share_mode.server_status.STATUS_WORKING: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working)) + self.server_status_label.setText(strings._('gui_status_indicator_working', True)) + elif self.share_mode.server_status.status == self.share_mode.server_status.STATUS_STARTED: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started)) + self.server_status_label.setText(strings._('gui_status_indicator_started', True)) + else: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped)) + self.server_status_label.setText(strings._('gui_status_indicator_stopped', True)) + def _initSystemTray(self): menu = QtWidgets.QMenu() self.settingsAction = menu.addAction(strings._('gui_settings_window_title', True)) @@ -313,9 +341,10 @@ class OnionShareGui(QtWidgets.QMainWindow): self.update_thread.update_available.connect(update_available) self.update_thread.start() - def check_for_requests(self): + def timer_callback(self): """ - Check for messages communicated from the web app, and update the GUI accordingly. + Check for messages communicated from the web app, and update the GUI accordingly. Also, + call ShareMode and ReceiveMode's timer_callbacks. """ self.update() @@ -334,7 +363,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # scroll to the bottom of the dl progress bar log pane # if a new download has been added if self.new_download: - self.downloads.downloads_container.vbar.setValue(self.downloads.downloads_container.vbar.maximum()) + self.share_mode.downloads.downloads_container.vbar.setValue(self.downloads.downloads_container.vbar.maximum()) self.new_download = False events = [] @@ -401,23 +430,10 @@ class OnionShareGui(QtWidgets.QMainWindow): elif event["path"] != '/favicon.ico': self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(self.web.error404_count, strings._('other_page_loaded', True), event["path"])) - # If the auto-shutdown timer has stopped, stop the server - if self.server_status.status == self.server_status.STATUS_STARTED: - if self.app.shutdown_timer and self.common.settings.get('shutdown_timeout'): - if self.timeout > 0: - now = QtCore.QDateTime.currentDateTime() - seconds_remaining = now.secsTo(self.server_status.timeout) - self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining)) - if not self.app.shutdown_timer.is_alive(): - # If there were no attempts to download the share, or all downloads are done, we can stop - if self.web.download_count == 0 or self.web.done: - self.server_status.stop_server() - self.status_bar.clearMessage() - self.server_share_status_label.setText(strings._('close_on_timeout', True)) - # A download is probably still running - hold off on stopping the share - else: - self.status_bar.clearMessage() - self.server_share_status_label.setText(strings._('timeout_download_still_running', True)) + if self.mode == self.MODE_SHARE: + self.share_mode.timer_callback() + else: + self.receive_mode.timer_callback() def copy_url(self): """ @@ -477,84 +493,3 @@ class OnionShareGui(QtWidgets.QMainWindow): except: e.accept() - - -class ZipProgressBar(QtWidgets.QProgressBar): - update_processed_size_signal = QtCore.pyqtSignal(int) - - def __init__(self, total_files_size): - super(ZipProgressBar, self).__init__() - self.setMaximumHeight(20) - self.setMinimumWidth(200) - self.setValue(0) - self.setFormat(strings._('zip_progress_bar_format')) - cssStyleData =""" - QProgressBar { - border: 1px solid #4e064f; - background-color: #ffffff !important; - text-align: center; - color: #9b9b9b; - } - - QProgressBar::chunk { - border: 0px; - background-color: #4e064f; - width: 10px; - }""" - self.setStyleSheet(cssStyleData) - - self._total_files_size = total_files_size - self._processed_size = 0 - - self.update_processed_size_signal.connect(self.update_processed_size) - - @property - def total_files_size(self): - return self._total_files_size - - @total_files_size.setter - def total_files_size(self, val): - self._total_files_size = val - - @property - def processed_size(self): - return self._processed_size - - @processed_size.setter - def processed_size(self, val): - self.update_processed_size(val) - - def update_processed_size(self, val): - self._processed_size = val - if self.processed_size < self.total_files_size: - self.setValue(int((self.processed_size * 100) / self.total_files_size)) - elif self.total_files_size != 0: - self.setValue(100) - else: - self.setValue(0) - - -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/receive_mode.py b/onionshare_gui/receive_mode.py index dd6f5eeb..e88c4d24 100644 --- a/onionshare_gui/receive_mode.py +++ b/onionshare_gui/receive_mode.py @@ -28,3 +28,9 @@ class ReceiveMode(QtWidgets.QWidget): def __init__(self, common): super(ReceiveMode, self).__init__() self.common = common + + def timer_callback(self): + """ + This method is called regularly on a timer while receive mode is active. + """ + pass diff --git a/onionshare_gui/share_mode.py b/onionshare_gui/share_mode.py index b6aa02c9..cad7dc06 100644 --- a/onionshare_gui/share_mode.py +++ b/onionshare_gui/share_mode.py @@ -17,18 +17,41 @@ 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 os +import threading +import time from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -from .mode import Mode +from onionshare.common import Common, ShutdownTimer +from onionshare.onion import * + +from .file_selection import FileSelection +from .server_status import ServerStatus +from .downloads import Downloads +from .onion_thread import OnionThread + class ShareMode(QtWidgets.QWidget): """ Parts of the main window UI for sharing files. """ - def __init__(self, common): + start_server_finished = QtCore.pyqtSignal() + stop_server_finished = QtCore.pyqtSignal() + starting_server_step2 = QtCore.pyqtSignal() + starting_server_step3 = QtCore.pyqtSignal() + starting_server_error = QtCore.pyqtSignal(str) + set_server_active = QtCore.pyqtSignal(bool) + + def __init__(self, common, filenames, qtapp, app, web, status_bar, server_share_status_label): super(ShareMode, self).__init__() self.common = common + self.qtapp = qtapp + self.app = app + self.web = web + + self.status_bar = status_bar + self.server_share_status_label = server_share_status_label # File selection self.file_selection = FileSelection(self.common) @@ -40,27 +63,19 @@ class ShareMode(QtWidgets.QWidget): self.server_status = ServerStatus(self.common, self.qtapp, self.app, self.web, self.file_selection) self.server_status.server_started.connect(self.file_selection.server_started) self.server_status.server_started.connect(self.start_server) - self.server_status.server_started.connect(self.update_server_status_indicator) self.server_status.server_stopped.connect(self.file_selection.server_stopped) self.server_status.server_stopped.connect(self.stop_server) - self.server_status.server_stopped.connect(self.update_server_status_indicator) self.server_status.server_stopped.connect(self.update_primary_action) self.server_status.server_canceled.connect(self.cancel_server) self.server_status.server_canceled.connect(self.file_selection.server_stopped) self.server_status.server_canceled.connect(self.update_primary_action) - self.start_server_finished.connect(self.clear_message) self.start_server_finished.connect(self.server_status.start_server_finished) - self.start_server_finished.connect(self.update_server_status_indicator) self.stop_server_finished.connect(self.server_status.stop_server_finished) - self.stop_server_finished.connect(self.update_server_status_indicator) self.file_selection.file_list.files_updated.connect(self.server_status.update) self.file_selection.file_list.files_updated.connect(self.update_primary_action) - self.server_status.url_copied.connect(self.copy_url) - self.server_status.hidservauth_copied.connect(self.copy_hidservauth) self.starting_server_step2.connect(self.start_server_step2) self.starting_server_step3.connect(self.start_server_step3) self.starting_server_error.connect(self.start_server_error) - self.server_status.button_clicked.connect(self.clear_message) # Filesize warning self.filesize_warning = QtWidgets.QLabel() @@ -70,7 +85,6 @@ class ShareMode(QtWidgets.QWidget): # Downloads self.downloads = Downloads(self.common) - self.new_download = False self.downloads_in_progress = 0 self.downloads_completed = 0 @@ -104,21 +118,6 @@ class ShareMode(QtWidgets.QWidget): self.info_widget.setLayout(self.info_layout) self.info_widget.hide() - # Server status indicator on the status bar - self.server_status_image_stopped = QtGui.QImage(self.common.get_resource_path('images/server_stopped.png')) - self.server_status_image_working = QtGui.QImage(self.common.get_resource_path('images/server_working.png')) - self.server_status_image_started = QtGui.QImage(self.common.get_resource_path('images/server_started.png')) - self.server_status_image_label = QtWidgets.QLabel() - self.server_status_image_label.setFixedWidth(20) - self.server_status_label = QtWidgets.QLabel() - self.server_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; }') - server_status_indicator_layout = QtWidgets.QHBoxLayout() - server_status_indicator_layout.addWidget(self.server_status_image_label) - server_status_indicator_layout.addWidget(self.server_status_label) - self.server_status_indicator = QtWidgets.QWidget() - self.server_status_indicator.setLayout(server_status_indicator_layout) - self.update_server_status_indicator() - # Primary action layout primary_action_layout = QtWidgets.QVBoxLayout() primary_action_layout.addWidget(self.server_status) @@ -128,6 +127,9 @@ class ShareMode(QtWidgets.QWidget): self.primary_action.hide() self.update_primary_action() + # Status bar, zip progress bar + self._zip_progress_bar = None + # Layout layout = QtWidgets.QVBoxLayout() layout.addWidget(self.info_widget) @@ -138,6 +140,28 @@ class ShareMode(QtWidgets.QWidget): # Always start with focus on file selection self.file_selection.setFocus() + def timer_callback(self): + """ + This method is called regularly on a timer while share mode is active. + """ + # If the auto-shutdown timer has stopped, stop the server + if self.server_status.status == self.server_status.STATUS_STARTED: + if self.app.shutdown_timer and self.common.settings.get('shutdown_timeout'): + if self.timeout > 0: + now = QtCore.QDateTime.currentDateTime() + seconds_remaining = now.secsTo(self.server_status.timeout) + self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining)) + if not self.app.shutdown_timer.is_alive(): + # If there were no attempts to download the share, or all downloads are done, we can stop + if self.web.download_count == 0 or self.web.done: + self.server_status.stop_server() + self.status_bar.clearMessage() + self.server_share_status_label.setText(strings._('close_on_timeout', True)) + # A download is probably still running - hold off on stopping the share + else: + self.status_bar.clearMessage() + self.server_share_status_label.setText(strings._('timeout_download_still_running', True)) + def update_primary_action(self): # Show or hide primary action layout file_count = self.file_selection.file_list.count() @@ -164,20 +188,6 @@ class ShareMode(QtWidgets.QWidget): # Resize window self.adjustSize() - def update_server_status_indicator(self): - self.common.log('OnionShareGui', 'update_server_status_indicator') - - # Set the status image - if self.server_status.status == self.server_status.STATUS_STOPPED: - self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped)) - self.server_status_label.setText(strings._('gui_status_indicator_stopped', True)) - elif self.server_status.status == self.server_status.STATUS_WORKING: - self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working)) - self.server_status_label.setText(strings._('gui_status_indicator_working', True)) - elif self.server_status.status == self.server_status.STATUS_STARTED: - self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started)) - self.server_status_label.setText(strings._('gui_status_indicator_started', True)) - def start_server(self): """ Start the onionshare server. This uses multiple threads to start the Tor onion @@ -185,7 +195,7 @@ class ShareMode(QtWidgets.QWidget): """ self.common.log('OnionShareGui', 'start_server') - self.set_server_active(True) + self.set_server_active.emit(True) self.app.set_stealth(self.common.settings.get('use_stealth')) @@ -296,7 +306,7 @@ class ShareMode(QtWidgets.QWidget): """ self.common.log('OnionShareGui', 'start_server_error') - self.set_server_active(False) + self.set_server_active.emit(False) Alert(self.common, error, QtWidgets.QMessageBox.Warning) self.server_status.stop_server() @@ -326,6 +336,7 @@ class ShareMode(QtWidgets.QWidget): # Probably we had no port to begin with (Onion service didn't start) pass self.app.cleanup() + # Remove ephemeral service, but don't disconnect from Tor self.onion.cleanup(stop_tor=False) self.filesize_warning.hide() @@ -334,7 +345,7 @@ class ShareMode(QtWidgets.QWidget): self.update_downloads_in_progress(0) self.file_selection.file_list.adjustSize() - self.set_server_active(False) + self.set_server_active.emit(False) self.stop_server_finished.emit() def downloads_toggled(self, checked): @@ -389,3 +400,58 @@ class ShareMode(QtWidgets.QWidget): if os.path.isdir(filename): total_size += Common.dir_size(filename) return total_size + + +class ZipProgressBar(QtWidgets.QProgressBar): + update_processed_size_signal = QtCore.pyqtSignal(int) + + def __init__(self, total_files_size): + super(ZipProgressBar, self).__init__() + self.setMaximumHeight(20) + self.setMinimumWidth(200) + self.setValue(0) + self.setFormat(strings._('zip_progress_bar_format')) + cssStyleData =""" + QProgressBar { + border: 1px solid #4e064f; + background-color: #ffffff !important; + text-align: center; + color: #9b9b9b; + } + + QProgressBar::chunk { + border: 0px; + background-color: #4e064f; + width: 10px; + }""" + self.setStyleSheet(cssStyleData) + + self._total_files_size = total_files_size + self._processed_size = 0 + + self.update_processed_size_signal.connect(self.update_processed_size) + + @property + def total_files_size(self): + return self._total_files_size + + @total_files_size.setter + def total_files_size(self, val): + self._total_files_size = val + + @property + def processed_size(self): + return self._processed_size + + @processed_size.setter + def processed_size(self, val): + self.update_processed_size(val) + + def update_processed_size(self, val): + self._processed_size = val + if self.processed_size < self.total_files_size: + self.setValue(int((self.processed_size * 100) / self.total_files_size)) + elif self.total_files_size != 0: + self.setValue(100) + else: + self.setValue(0)