diff options
Diffstat (limited to 'onionshare_gui')
-rw-r--r-- | onionshare_gui/__init__.py | 31 | ||||
-rw-r--r-- | onionshare_gui/mode/__init__.py | 126 | ||||
-rw-r--r-- | onionshare_gui/mode/receive_mode/__init__.py | 14 | ||||
-rw-r--r-- | onionshare_gui/mode/share_mode/__init__.py | 14 | ||||
-rw-r--r-- | onionshare_gui/onionshare_gui.py | 32 | ||||
-rw-r--r-- | onionshare_gui/server_status.py | 222 | ||||
-rw-r--r-- | onionshare_gui/settings_dialog.py | 69 | ||||
-rw-r--r-- | onionshare_gui/threads.py | 69 | ||||
-rw-r--r-- | onionshare_gui/update_checker.py | 2 |
9 files changed, 401 insertions, 178 deletions
diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 675bb52d..99c52937 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -26,7 +26,6 @@ import signal from .widgets import Alert from PyQt5 import QtCore, QtWidgets -from onionshare import strings from onionshare.common import Common from onionshare.onion import Onion from onionshare.onionshare import OnionShare @@ -59,16 +58,8 @@ def main(): common = Common() common.define_css() - # Load the default settings and strings early, for the sake of being able to parse options. - # These won't be in the user's chosen locale necessarily, but we need to parse them - # early in order to even display the option to pass alternate settings (which might - # contain a preferred locale). - # If an alternate --config is passed, we'll reload strings later. - common.load_settings() - strings.load_strings(common) - # Display OnionShare banner - print(strings._('version_string').format(common.version)) + print("OnionShare {0:s} | https://onionshare.org/".format(common.version)) # Allow Ctrl-C to smoothly quit the program instead of throwing an exception # https://stackoverflow.com/questions/42814093/how-to-handle-ctrlc-in-python-app-with-pyqt @@ -80,10 +71,10 @@ def main(): # Parse arguments parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=48)) - parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only")) - parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) - parser.add_argument('--filenames', metavar='filenames', nargs='+', help=strings._('help_filename')) - parser.add_argument('--config', metavar='config', default=False, help=strings._('help_config')) + parser.add_argument('--local-only', action='store_true', dest='local_only', help="Don't use Tor (only for development)") + parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk") + parser.add_argument('--filenames', metavar='filenames', nargs='+', help="List of files or folders to share") + parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)") args = parser.parse_args() filenames = args.filenames @@ -93,25 +84,23 @@ def main(): config = args.config if config: - # Re-load the strings, in case the provided config has changed locale common.load_settings(config) - strings.load_strings(common) local_only = bool(args.local_only) - debug = bool(args.debug) + verbose = bool(args.verbose) - # Debug mode? - common.debug = debug + # Verbose mode? + common.verbose = verbose # Validation if filenames: valid = True for filename in filenames: if not os.path.isfile(filename) and not os.path.isdir(filename): - Alert(common, strings._("not_a_file").format(filename)) + Alert(common, "{0:s} is not a valid file.".format(filename)) valid = False if not os.access(filename, os.R_OK): - Alert(common, strings._("not_a_readable_file").format(filename)) + Alert(common, "{0:s} is not a readable file.".format(filename)) valid = False if not valid: sys.exit() diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py index 4fe335e7..8f5ff32b 100644 --- a/onionshare_gui/mode/__init__.py +++ b/onionshare_gui/mode/__init__.py @@ -20,10 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -from onionshare.common import ShutdownTimer +from onionshare.common import AutoStopTimer from ..server_status import ServerStatus from ..threads import OnionThread +from ..threads import AutoStartTimer from ..widgets import Alert class Mode(QtWidgets.QWidget): @@ -35,6 +36,7 @@ class Mode(QtWidgets.QWidget): starting_server_step2 = QtCore.pyqtSignal() starting_server_step3 = QtCore.pyqtSignal() starting_server_error = QtCore.pyqtSignal(str) + starting_server_early = QtCore.pyqtSignal() set_server_active = QtCore.pyqtSignal(bool) def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): @@ -58,6 +60,7 @@ class Mode(QtWidgets.QWidget): # Threads start out as None self.onion_thread = None self.web_thread = None + self.startup_thread = None # Server status self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only) @@ -68,6 +71,7 @@ class Mode(QtWidgets.QWidget): self.stop_server_finished.connect(self.server_status.stop_server_finished) self.starting_server_step2.connect(self.start_server_step2) self.starting_server_step3.connect(self.start_server_step3) + self.starting_server_early.connect(self.start_server_early) self.starting_server_error.connect(self.start_server_error) # Primary action @@ -88,24 +92,55 @@ class Mode(QtWidgets.QWidget): """ pass + def human_friendly_time(self, secs): + """ + Returns a human-friendly time delta from given seconds. + """ + days = secs//86400 + hours = (secs - days*86400)//3600 + minutes = (secs - days*86400 - hours*3600)//60 + seconds = secs - days*86400 - hours*3600 - minutes*60 + if not seconds: + seconds = '0' + result = ("{0}{1}, ".format(days, strings._('days_first_letter')) if days else "") + \ + ("{0}{1}, ".format(hours, strings._('hours_first_letter')) if hours else "") + \ + ("{0}{1}, ".format(minutes, strings._('minutes_first_letter')) if minutes else "") + \ + "{0}{1}".format(seconds, strings._('seconds_first_letter')) + + return result + def timer_callback(self): """ This method is called regularly on a timer. """ - # If the auto-shutdown timer has stopped, stop the server + # If this is a scheduled share, display the countdown til the share starts + if self.server_status.status == ServerStatus.STATUS_WORKING: + if self.server_status.autostart_timer_datetime: + now = QtCore.QDateTime.currentDateTime() + if self.server_status.local_only: + seconds_remaining = now.secsTo(self.server_status.autostart_timer_widget.dateTime()) + else: + seconds_remaining = now.secsTo(self.server_status.autostart_timer_datetime.replace(second=0, microsecond=0)) + # Update the server button + if seconds_remaining > 0: + self.server_status.server_button.setText(strings._('gui_waiting_to_start').format(self.human_friendly_time(seconds_remaining))) + else: + self.server_status.server_button.setText(strings._('gui_please_wait')) + + # If the auto-stop timer has stopped, stop the server if self.server_status.status == ServerStatus.STATUS_STARTED: - if self.app.shutdown_timer and self.common.settings.get('shutdown_timeout'): - if self.timeout > 0: + if self.app.autostop_timer_thread and self.common.settings.get('autostop_timer'): + if self.autostop_timer_datetime_delta > 0: now = QtCore.QDateTime.currentDateTime() - seconds_remaining = now.secsTo(self.server_status.timeout) + seconds_remaining = now.secsTo(self.server_status.autostop_timer_datetime) # Update the server button - server_button_text = self.get_stop_server_shutdown_timeout_text() - self.server_status.server_button.setText(server_button_text.format(seconds_remaining)) + server_button_text = self.get_stop_server_autostop_timer_text() + self.server_status.server_button.setText(server_button_text.format(self.human_friendly_time(seconds_remaining))) self.status_bar.clearMessage() - if not self.app.shutdown_timer.is_alive(): - if self.timeout_finished_should_stop_server(): + if not self.app.autostop_timer_thread.is_alive(): + if self.autostop_timer_finished_should_stop_server(): self.server_status.stop_server() def timer_callback_custom(self): @@ -114,15 +149,15 @@ class Mode(QtWidgets.QWidget): """ pass - def get_stop_server_shutdown_timeout_text(self): + def get_stop_server_autostop_timer_text(self): """ - Return the string to put on the stop server button, if there's a shutdown timeout + Return the string to put on the stop server button, if there's an auto-stop timer """ pass - def timeout_finished_should_stop_server(self): + def autostop_timer_finished_should_stop_server(self): """ - The shutdown timer expired, should we stop the server? Returns a bool + The auto-stop timer expired, should we stop the server? Returns a bool """ pass @@ -142,7 +177,41 @@ class Mode(QtWidgets.QWidget): self.status_bar.clearMessage() self.server_status_label.setText('') + # Ensure we always get a new random port each time we might launch an OnionThread + self.app.port = None + + # Start the onion thread. If this share was scheduled for a future date, + # the OnionThread will start and exit 'early' to obtain the port, slug + # and onion address, but it will not start the WebThread yet. + if self.server_status.autostart_timer_datetime: + self.start_onion_thread(obtain_onion_early=True) + else: + self.start_onion_thread() + + # If scheduling a share, delay starting the real share + if self.server_status.autostart_timer_datetime: + self.common.log('Mode', 'start_server', 'Starting auto-start timer') + self.startup_thread = AutoStartTimer(self) + # Once the timer has finished, start the real share, with a WebThread + self.startup_thread.success.connect(self.start_scheduled_service) + self.startup_thread.error.connect(self.start_server_error) + self.startup_thread.canceled = False + self.startup_thread.start() + + def start_onion_thread(self, obtain_onion_early=False): self.common.log('Mode', 'start_server', 'Starting an onion thread') + self.obtain_onion_early = obtain_onion_early + self.onion_thread = OnionThread(self) + self.onion_thread.success.connect(self.starting_server_step2.emit) + self.onion_thread.success_early.connect(self.starting_server_early.emit) + self.onion_thread.error.connect(self.starting_server_error.emit) + self.onion_thread.start() + + def start_scheduled_service(self, obtain_onion_early=False): + # We start a new OnionThread with the saved scheduled key from settings + self.common.settings.load() + self.obtain_onion_early = obtain_onion_early + self.common.log('Mode', 'start_server', 'Starting a scheduled onion thread') 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) @@ -154,6 +223,14 @@ class Mode(QtWidgets.QWidget): """ pass + def start_server_early(self): + """ + An 'early' start of an onion service in order to obtain the onion + address for a scheduled start. Shows the onion address in the UI + in advance of actually starting the share. + """ + self.server_status.show_url() + def start_server_step2(self): """ Step 2 in starting the onionshare server. @@ -182,18 +259,18 @@ class Mode(QtWidgets.QWidget): self.start_server_step3_custom() - if self.common.settings.get('shutdown_timeout'): + if self.common.settings.get('autostop_timer'): # Convert the date value to seconds between now and then now = QtCore.QDateTime.currentDateTime() - self.timeout = now.secsTo(self.server_status.timeout) - # Set the shutdown timeout value - if self.timeout > 0: - self.app.shutdown_timer = ShutdownTimer(self.common, self.timeout) - self.app.shutdown_timer.start() - # The timeout has actually already passed since the user clicked Start. Probably the Onion service took too long to start. + self.autostop_timer_datetime_delta = now.secsTo(self.server_status.autostop_timer_datetime) + # Start the auto-stop timer + if self.autostop_timer_datetime_delta > 0: + self.app.autostop_timer_thread = AutoStopTimer(self.common, self.autostop_timer_datetime_delta) + self.app.autostop_timer_thread.start() + # The auto-stop timer has actually already passed since the user clicked Start. Probably the Onion service took too long to start. else: self.stop_server() - self.start_server_error(strings._('gui_server_started_after_timeout')) + self.start_server_error(strings._('gui_server_started_after_autostop_timer')) def start_server_step3_custom(self): """ @@ -225,7 +302,12 @@ class Mode(QtWidgets.QWidget): Cancel the server while it is preparing to start """ self.cancel_server_custom() - + if self.startup_thread: + self.common.log('Mode', 'cancel_server: quitting startup thread') + self.startup_thread.canceled = True + self.app.onion.scheduled_key = None + self.app.onion.scheduled_auth_cookie = None + self.startup_thread.quit() if self.onion_thread: self.common.log('Mode', 'cancel_server: quitting onion thread') self.onion_thread.quit() diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index 5fb33ab3..4c0b49ba 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -86,24 +86,24 @@ class ReceiveMode(Mode): self.wrapper_layout.addWidget(self.history, stretch=1) self.setLayout(self.wrapper_layout) - def get_stop_server_shutdown_timeout_text(self): + def get_stop_server_autostop_timer_text(self): """ - Return the string to put on the stop server button, if there's a shutdown timeout + Return the string to put on the stop server button, if there's an auto-stop timer """ - return strings._('gui_receive_stop_server_shutdown_timeout') + return strings._('gui_receive_stop_server_autostop_timer') - def timeout_finished_should_stop_server(self): + def autostop_timer_finished_should_stop_server(self): """ - The shutdown timer expired, should we stop the server? Returns a bool + The auto-stop timer expired, should we stop the server? Returns a bool """ # If there were no attempts to upload files, or all uploads are done, we can stop if self.web.receive_mode.upload_count == 0 or not self.web.receive_mode.uploads_in_progress: self.server_status.stop_server() - self.server_status_label.setText(strings._('close_on_timeout')) + self.server_status_label.setText(strings._('close_on_autostop_timer')) return True # An upload is probably still running - hold off on stopping the share, but block new shares. else: - self.server_status_label.setText(strings._('gui_receive_mode_timeout_waiting')) + self.server_status_label.setText(strings._('gui_receive_mode_autostop_timer_waiting')) self.web.receive_mode.can_upload = False return False diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 1f5ad00b..6cb50b2b 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -121,24 +121,24 @@ class ShareMode(Mode): # Always start with focus on file selection self.file_selection.setFocus() - def get_stop_server_shutdown_timeout_text(self): + def get_stop_server_autostop_timer_text(self): """ - Return the string to put on the stop server button, if there's a shutdown timeout + Return the string to put on the stop server button, if there's an auto-stop timer """ - return strings._('gui_share_stop_server_shutdown_timeout') + return strings._('gui_share_stop_server_autostop_timer') - def timeout_finished_should_stop_server(self): + def autostop_timer_finished_should_stop_server(self): """ - The shutdown timer expired, should we stop the server? Returns a bool + The auto-stop timer expired, should we stop the server? Returns a bool """ # If there were no attempts to download the share, or all downloads are done, we can stop if self.web.share_mode.download_count == 0 or self.web.done: self.server_status.stop_server() - self.server_status_label.setText(strings._('close_on_timeout')) + self.server_status_label.setText(strings._('close_on_autostop_timer')) return True # A download is probably still running - hold off on stopping the share else: - self.server_status_label.setText(strings._('gui_share_mode_timeout_waiting')) + self.server_status_label.setText(strings._('gui_share_mode_autostop_timer_waiting')) return False def start_server_custom(self): diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 27abf5e5..17839669 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -62,13 +62,17 @@ class OnionShareGui(QtWidgets.QMainWindow): self.config = config if self.config: self.common.load_settings(self.config) + else: + self.common.load_settings() + + strings.load_strings(self.common) # System tray menu = QtWidgets.QMenu() self.settings_action = menu.addAction(strings._('gui_settings_window_title')) self.settings_action.triggered.connect(self.open_settings) - help_action = menu.addAction(strings._('gui_settings_button_help')) - help_action.triggered.connect(SettingsDialog.help_clicked) + self.help_action = menu.addAction(strings._('gui_settings_button_help')) + self.help_action.triggered.connect(lambda: SettingsDialog.help_clicked(self)) exit_action = menu.addAction(strings._('systray_menu_exit')) exit_action.triggered.connect(self.close) @@ -228,7 +232,10 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_status_label.setText(strings._('gui_status_indicator_share_stopped')) elif self.share_mode.server_status.status == ServerStatus.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_share_working')) + if self.share_mode.server_status.autostart_timer_datetime: + self.server_status_label.setText(strings._('gui_status_indicator_share_scheduled')) + else: + self.server_status_label.setText(strings._('gui_status_indicator_share_working')) elif self.share_mode.server_status.status == ServerStatus.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_share_started')) @@ -239,7 +246,10 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_status_label.setText(strings._('gui_status_indicator_receive_stopped')) elif self.receive_mode.server_status.status == ServerStatus.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_receive_working')) + if self.receive_mode.server_status.autostart_timer_datetime: + self.server_status_label.setText(strings._('gui_status_indicator_receive_scheduled')) + else: + self.server_status_label.setText(strings._('gui_status_indicator_receive_working')) elif self.receive_mode.server_status.status == ServerStatus.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_receive_started')) @@ -309,10 +319,16 @@ class OnionShareGui(QtWidgets.QMainWindow): self.receive_mode.on_reload_settings() self.status_bar.clearMessage() - # If we switched off the shutdown timeout setting, ensure the widget is hidden. - if not self.common.settings.get('shutdown_timeout'): - self.share_mode.server_status.shutdown_timeout_container.hide() - self.receive_mode.server_status.shutdown_timeout_container.hide() + # If we switched off the auto-stop timer setting, ensure the widget is hidden. + if not self.common.settings.get('autostop_timer'): + self.share_mode.server_status.autostop_timer_container.hide() + self.receive_mode.server_status.autostop_timer_container.hide() + # If we switched off the auto-start timer setting, ensure the widget is hidden. + if not self.common.settings.get('autostart_timer'): + self.share_mode.server_status.autostart_timer_datetime = None + self.receive_mode.server_status.autostart_timer_datetime = None + self.share_mode.server_status.autostart_timer_container.hide() + self.receive_mode.server_status.autostart_timer_container.hide() d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only) d.settings_saved.connect(reload_settings) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index e34a3d16..0c51119e 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -56,34 +56,60 @@ class ServerStatus(QtWidgets.QWidget): self.app = app self.web = None + self.autostart_timer_datetime = None self.local_only = local_only self.resizeEvent(None) - # Shutdown timeout layout - self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout')) - self.shutdown_timeout = QtWidgets.QDateTimeEdit() - self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") + # Auto-start timer layout + self.autostart_timer_label = QtWidgets.QLabel(strings._('gui_settings_autostart_timer')) + self.autostart_timer_widget = QtWidgets.QDateTimeEdit() + self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") if self.local_only: # For testing - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + self.autostart_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.autostart_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) else: - # Set proposed timeout to be 5 minutes into the future - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + # Set proposed timer to be 5 minutes into the future + self.autostart_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) - self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) - shutdown_timeout_layout = QtWidgets.QHBoxLayout() - shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) - shutdown_timeout_layout.addWidget(self.shutdown_timeout) - - # Shutdown timeout container, so it can all be hidden and shown as a group - shutdown_timeout_container_layout = QtWidgets.QVBoxLayout() - shutdown_timeout_container_layout.addLayout(shutdown_timeout_layout) - self.shutdown_timeout_container = QtWidgets.QWidget() - self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout) - self.shutdown_timeout_container.hide() + self.autostart_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) + self.autostart_timer_widget.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) + autostart_timer_layout = QtWidgets.QHBoxLayout() + autostart_timer_layout.addWidget(self.autostart_timer_label) + autostart_timer_layout.addWidget(self.autostart_timer_widget) + + # Auto-start timer container, so it can all be hidden and shown as a group + autostart_timer_container_layout = QtWidgets.QVBoxLayout() + autostart_timer_container_layout.addLayout(autostart_timer_layout) + self.autostart_timer_container = QtWidgets.QWidget() + self.autostart_timer_container.setLayout(autostart_timer_container_layout) + self.autostart_timer_container.hide() + + # Auto-stop timer layout + self.autostop_timer_label = QtWidgets.QLabel(strings._('gui_settings_autostop_timer')) + self.autostop_timer_widget = QtWidgets.QDateTimeEdit() + self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") + if self.local_only: + # For testing + self.autostop_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.autostop_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + else: + # Set proposed timer to be 5 minutes into the future + self.autostop_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now + self.autostop_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) + self.autostop_timer_widget.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) + autostop_timer_layout = QtWidgets.QHBoxLayout() + autostop_timer_layout.addWidget(self.autostop_timer_label) + autostop_timer_layout.addWidget(self.autostop_timer_widget) + + # Auto-stop timer container, so it can all be hidden and shown as a group + autostop_timer_container_layout = QtWidgets.QVBoxLayout() + autostop_timer_container_layout.addLayout(autostop_timer_layout) + self.autostop_timer_container = QtWidgets.QWidget() + self.autostop_timer_container.setLayout(autostop_timer_container_layout) + self.autostop_timer_container.hide() # Server layout self.server_button = QtWidgets.QPushButton() @@ -123,7 +149,8 @@ class ServerStatus(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.server_button) layout.addLayout(url_layout) - layout.addWidget(self.shutdown_timeout_container) + layout.addWidget(self.autostart_timer_container) + layout.addWidget(self.autostop_timer_container) self.setLayout(layout) def set_mode(self, share_mode, file_selection=None): @@ -154,59 +181,74 @@ class ServerStatus(QtWidgets.QWidget): except: pass + def autostart_timer_reset(self): + """ + Reset the auto-start timer in the UI after stopping a share + """ + self.autostart_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + if not self.local_only: + self.autostart_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) - def shutdown_timeout_reset(self): + def autostop_timer_reset(self): """ - Reset the timeout in the UI after stopping a share + Reset the auto-stop timer in the UI after stopping a share """ - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + self.autostop_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) if not self.local_only: - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) + self.autostop_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) - def update(self): + def show_url(self): """ - Update the GUI elements based on the current state. + Show the URL in the UI. """ - # Set the URL fields - if self.status == self.STATUS_STARTED: - self.url_description.show() + self.url_description.show() - info_image = self.common.get_resource_path('images/info.png') + info_image = self.common.get_resource_path('images/info.png') - if self.mode == ServerStatus.MODE_SHARE: - self.url_description.setText(strings._('gui_share_url_description').format(info_image)) - else: - self.url_description.setText(strings._('gui_receive_url_description').format(info_image)) + if self.mode == ServerStatus.MODE_SHARE: + self.url_description.setText(strings._('gui_share_url_description').format(info_image)) + else: + self.url_description.setText(strings._('gui_receive_url_description').format(info_image)) - # Show a Tool Tip explaining the lifecycle of this URL - if self.common.settings.get('save_private_key'): - if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): - self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent')) - else: - self.url_description.setToolTip(strings._('gui_url_label_persistent')) + # Show a Tool Tip explaining the lifecycle of this URL + if self.common.settings.get('save_private_key'): + if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): + self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent')) else: - if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): - self.url_description.setToolTip(strings._('gui_url_label_onetime')) - else: - self.url_description.setToolTip(strings._('gui_url_label_stay_open')) + self.url_description.setToolTip(strings._('gui_url_label_persistent')) + else: + if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): + self.url_description.setToolTip(strings._('gui_url_label_onetime')) + else: + self.url_description.setToolTip(strings._('gui_url_label_stay_open')) - self.url.setText(self.get_url()) - self.url.show() + self.url.setText(self.get_url()) + self.url.show() + self.copy_url_button.show() - self.copy_url_button.show() + if self.app.stealth: + self.copy_hidservauth_button.show() + else: + self.copy_hidservauth_button.hide() + + def update(self): + """ + Update the GUI elements based on the current state. + """ + # Set the URL fields + if self.status == self.STATUS_STARTED: + self.show_url() if self.common.settings.get('save_private_key'): if not self.common.settings.get('slug'): self.common.settings.set('slug', self.web.slug) self.common.settings.save() - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() + if self.common.settings.get('autostart_timer'): + self.autostart_timer_container.hide() - if self.app.stealth: - self.copy_hidservauth_button.show() - else: - self.copy_hidservauth_button.hide() + if self.common.settings.get('autostop_timer'): + self.autostop_timer_container.hide() else: self.url_description.hide() self.url.hide() @@ -227,8 +269,10 @@ class ServerStatus(QtWidgets.QWidget): else: self.server_button.setText(strings._('gui_receive_start_server')) self.server_button.setToolTip('') - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.show() + if self.common.settings.get('autostart_timer'): + self.autostart_timer_container.show() + if self.common.settings.get('autostop_timer'): + self.autostop_timer_container.show() elif self.status == self.STATUS_STARTED: self.server_button.setStyleSheet(self.common.css['server_status_button_started']) self.server_button.setEnabled(True) @@ -236,43 +280,61 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setText(strings._('gui_share_stop_server')) else: self.server_button.setText(strings._('gui_receive_stop_server')) - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() - if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) - else: - self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip').format(self.timeout)) - + if self.common.settings.get('autostart_timer'): + self.autostart_timer_container.hide() + if self.common.settings.get('autostop_timer'): + self.autostop_timer_container.hide() + self.server_button.setToolTip(strings._('gui_stop_server_autostop_timer_tooltip').format(self.autostop_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy"))) elif self.status == self.STATUS_WORKING: self.server_button.setStyleSheet(self.common.css['server_status_button_working']) self.server_button.setEnabled(True) - self.server_button.setText(strings._('gui_please_wait')) - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() + if self.autostart_timer_datetime: + self.autostart_timer_container.hide() + self.server_button.setToolTip(strings._('gui_start_server_autostart_timer_tooltip').format(self.autostart_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy"))) + else: + self.server_button.setText(strings._('gui_please_wait')) + if self.common.settings.get('autostop_timer'): + self.autostop_timer_container.hide() else: self.server_button.setStyleSheet(self.common.css['server_status_button_working']) self.server_button.setEnabled(False) self.server_button.setText(strings._('gui_please_wait')) - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() + if self.common.settings.get('autostart_timer'): + self.autostart_timer_container.hide() + self.server_button.setToolTip(strings._('gui_start_server_autostart_timer_tooltip').format(self.autostart_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy"))) + if self.common.settings.get('autostop_timer'): + self.autostop_timer_container.hide() def server_button_clicked(self): """ Toggle starting or stopping the server. """ if self.status == self.STATUS_STOPPED: - if self.common.settings.get('shutdown_timeout'): + can_start = True + if self.common.settings.get('autostart_timer'): if self.local_only: - self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() + self.autostart_timer_datetime = self.autostart_timer_widget.dateTime().toPyDateTime() else: - # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen - self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) - # If the timeout has actually passed already before the user hit Start, refuse to start the server. - if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: - Alert(self.common, strings._('gui_server_timeout_expired'), QtWidgets.QMessageBox.Warning) + self.autostart_timer_datetime = self.autostart_timer_widget.dateTime().toPyDateTime().replace(second=0, microsecond=0) + # If the timer has actually passed already before the user hit Start, refuse to start the server. + if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.autostart_timer_datetime: + can_start = False + Alert(self.common, strings._('gui_server_autostart_timer_expired'), QtWidgets.QMessageBox.Warning) + if self.common.settings.get('autostop_timer'): + if self.local_only: + self.autostop_timer_datetime = self.autostop_timer_widget.dateTime().toPyDateTime() else: - self.start_server() - else: + # Get the timer chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen + self.autostop_timer_datetime = self.autostop_timer_widget.dateTime().toPyDateTime().replace(second=0, microsecond=0) + # If the timer has actually passed already before the user hit Start, refuse to start the server. + if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.autostop_timer_datetime: + can_start = False + Alert(self.common, strings._('gui_server_autostop_timer_expired'), QtWidgets.QMessageBox.Warning) + if self.common.settings.get('autostart_timer'): + if self.autostop_timer_datetime <= self.autostart_timer_datetime: + Alert(self.common, strings._('gui_autostop_timer_cant_be_earlier_than_autostart_timer'), QtWidgets.QMessageBox.Warning) + can_start = False + if can_start: self.start_server() elif self.status == self.STATUS_STARTED: self.stop_server() @@ -302,7 +364,8 @@ class ServerStatus(QtWidgets.QWidget): Stop the server. """ self.status = self.STATUS_WORKING - self.shutdown_timeout_reset() + self.autostart_timer_reset() + self.autostop_timer_reset() self.update() self.server_stopped.emit() @@ -312,7 +375,8 @@ class ServerStatus(QtWidgets.QWidget): """ self.common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup') self.status = self.STATUS_WORKING - self.shutdown_timeout_reset() + self.autostart_timer_reset() + self.autostop_timer_reset() self.update() self.server_canceled.emit() diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 2933784c..3c0b83f4 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -71,27 +71,45 @@ class SettingsDialog(QtWidgets.QDialog): self.public_mode_widget = QtWidgets.QWidget() self.public_mode_widget.setLayout(public_mode_layout) - # Whether or not to use a shutdown ('auto-stop') timer - self.shutdown_timeout_checkbox = QtWidgets.QCheckBox() - self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) - self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox")) - shutdown_timeout_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer")) - shutdown_timeout_label.setStyleSheet(self.common.css['settings_whats_this']) - shutdown_timeout_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - shutdown_timeout_label.setOpenExternalLinks(True) - shutdown_timeout_label.setMinimumSize(public_mode_label.sizeHint()) - shutdown_timeout_layout = QtWidgets.QHBoxLayout() - shutdown_timeout_layout.addWidget(self.shutdown_timeout_checkbox) - shutdown_timeout_layout.addWidget(shutdown_timeout_label) - shutdown_timeout_layout.addStretch() - shutdown_timeout_layout.setContentsMargins(0,0,0,0) - self.shutdown_timeout_widget = QtWidgets.QWidget() - self.shutdown_timeout_widget.setLayout(shutdown_timeout_layout) + # Whether or not to use an auto-start timer + self.autostart_timer_checkbox = QtWidgets.QCheckBox() + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked) + self.autostart_timer_checkbox.setText(strings._("gui_settings_autostart_timer_checkbox")) + autostart_timer_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Start-Timer")) + autostart_timer_label.setStyleSheet(self.common.css['settings_whats_this']) + autostart_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + autostart_timer_label.setOpenExternalLinks(True) + autostart_timer_label.setMinimumSize(public_mode_label.sizeHint()) + autostart_timer_layout = QtWidgets.QHBoxLayout() + autostart_timer_layout.addWidget(self.autostart_timer_checkbox) + autostart_timer_layout.addWidget(autostart_timer_label) + autostart_timer_layout.addStretch() + autostart_timer_layout.setContentsMargins(0,0,0,0) + self.autostart_timer_widget = QtWidgets.QWidget() + self.autostart_timer_widget.setLayout(autostart_timer_layout) + + # Whether or not to use an auto-stop timer + self.autostop_timer_checkbox = QtWidgets.QCheckBox() + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked) + self.autostop_timer_checkbox.setText(strings._("gui_settings_autostop_timer_checkbox")) + autostop_timer_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer")) + autostop_timer_label.setStyleSheet(self.common.css['settings_whats_this']) + autostop_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + autostop_timer_label.setOpenExternalLinks(True) + autostop_timer_label.setMinimumSize(public_mode_label.sizeHint()) + autostop_timer_layout = QtWidgets.QHBoxLayout() + autostop_timer_layout.addWidget(self.autostop_timer_checkbox) + autostop_timer_layout.addWidget(autostop_timer_label) + autostop_timer_layout.addStretch() + autostop_timer_layout.setContentsMargins(0,0,0,0) + self.autostop_timer_widget = QtWidgets.QWidget() + self.autostop_timer_widget.setLayout(autostop_timer_layout) # General settings layout general_group_layout = QtWidgets.QVBoxLayout() general_group_layout.addWidget(self.public_mode_widget) - general_group_layout.addWidget(self.shutdown_timeout_widget) + general_group_layout.addWidget(self.autostart_timer_widget) + general_group_layout.addWidget(self.autostop_timer_widget) general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label")) general_group.setLayout(general_group_layout) @@ -488,11 +506,17 @@ class SettingsDialog(QtWidgets.QDialog): else: self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked) - shutdown_timeout = self.old_settings.get('shutdown_timeout') - if shutdown_timeout: - self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) + autostart_timer = self.old_settings.get('autostart_timer') + if autostart_timer: + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked) else: - self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) + + autostop_timer = self.old_settings.get('autostop_timer') + if autostop_timer: + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) save_private_key = self.old_settings.get('save_private_key') if save_private_key: @@ -932,7 +956,8 @@ class SettingsDialog(QtWidgets.QDialog): settings.load() # To get the last update timestamp settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked()) - settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked()) + settings.set('autostart_timer', self.autostart_timer_checkbox.isChecked()) + settings.set('autostop_timer', self.autostop_timer_checkbox.isChecked()) # Complicated logic here to force v2 onion mode on or off depending on other settings if self.use_legacy_v2_onions_checkbox.isChecked(): diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 3b05bebf..26a9ee6b 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -28,6 +28,7 @@ class OnionThread(QtCore.QThread): Starts the onion service, and waits for it to finish """ success = QtCore.pyqtSignal() + success_early = QtCore.pyqtSignal() error = QtCore.pyqtSignal(str) def __init__(self, mode): @@ -41,18 +42,30 @@ class OnionThread(QtCore.QThread): def run(self): self.mode.common.log('OnionThread', 'run') + # Choose port and slug early, because we need them to exist in advance for scheduled shares 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) + if not self.mode.app.port: + self.mode.app.choose_port() + if not self.mode.common.settings.get('public_mode'): + if not self.mode.web.slug: + self.mode.web.generate_slug(self.mode.common.settings.get('slug')) try: - self.mode.app.start_onion_service() - self.success.emit() + if self.mode.obtain_onion_early: + self.mode.app.start_onion_service(await_publication=False, save_scheduled_key=True) + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + self.success_early.emit() + # Unregister the onion so we can use it in the next OnionThread + self.mode.app.onion.cleanup(False) + else: + self.mode.app.start_onion_service(await_publication=True) + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + # start onionshare http service in new thread + self.mode.web_thread = WebThread(self.mode) + self.mode.web_thread.start() + self.success.emit() except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: self.error.emit(e.args[0]) @@ -73,5 +86,39 @@ class WebThread(QtCore.QThread): def run(self): self.mode.common.log('WebThread', 'run') - self.mode.app.choose_port() - self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug')) + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.web.slug) + self.success.emit() + + +class AutoStartTimer(QtCore.QThread): + """ + Waits for a prescribed time before allowing a share to start + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + def __init__(self, mode, canceled=False): + super(AutoStartTimer, self).__init__() + self.mode = mode + self.canceled = canceled + self.mode.common.log('AutoStartTimer', '__init__') + + # allow this thread to be terminated + self.setTerminationEnabled() + + def run(self): + now = QtCore.QDateTime.currentDateTime() + autostart_timer_datetime_delta = now.secsTo(self.mode.server_status.autostart_timer_datetime) + try: + # Sleep until scheduled time + while autostart_timer_datetime_delta > 0 and self.canceled == False: + time.sleep(0.1) + now = QtCore.QDateTime.currentDateTime() + autostart_timer_datetime_delta = now.secsTo(self.mode.server_status.autostart_timer_datetime) + # Timer has now finished + if self.canceled == False: + self.mode.server_status.server_button.setText(strings._('gui_please_wait')) + self.mode.server_status_label.setText(strings._('gui_status_indicator_share_working')) + self.success.emit() + except ValueError as e: + self.error.emit(e.args[0]) + return diff --git a/onionshare_gui/update_checker.py b/onionshare_gui/update_checker.py index a7e0b99c..1e37b73a 100644 --- a/onionshare_gui/update_checker.py +++ b/onionshare_gui/update_checker.py @@ -25,7 +25,7 @@ from distutils.version import LooseVersion as Version from onionshare.settings import Settings from onionshare.onion import Onion -from . import strings +from onionshare import strings class UpdateCheckerCheckError(Exception): """ |