diff options
author | Miguel Jacq <mig@mig5.net> | 2019-03-05 10:28:27 +1100 |
---|---|---|
committer | Miguel Jacq <mig@mig5.net> | 2019-03-05 10:28:27 +1100 |
commit | 31c360b44d411fc9507eeb5815728a26af6781cf (patch) | |
tree | 5ecc6d80b80989fdbd064611d7b5bfd9742f359a /onionshare_gui | |
parent | 3af05dcc2041026380a71b8aed1af9af849416a5 (diff) | |
download | onionshare-31c360b44d411fc9507eeb5815728a26af6781cf.tar.gz onionshare-31c360b44d411fc9507eeb5815728a26af6781cf.zip |
Add a Startup Timer feature (scheduled start / dead man's switch)
Diffstat (limited to 'onionshare_gui')
-rw-r--r-- | onionshare_gui/mode/__init__.py | 52 | ||||
-rw-r--r-- | onionshare_gui/onionshare_gui.py | 14 | ||||
-rw-r--r-- | onionshare_gui/server_status.py | 70 | ||||
-rw-r--r-- | onionshare_gui/settings_dialog.py | 25 | ||||
-rw-r--r-- | onionshare_gui/threads.py | 69 |
5 files changed, 210 insertions, 20 deletions
diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py index 4fe335e7..d4f0cd09 100644 --- a/onionshare_gui/mode/__init__.py +++ b/onionshare_gui/mode/__init__.py @@ -24,6 +24,7 @@ from onionshare.common import ShutdownTimer from ..server_status import ServerStatus from ..threads import OnionThread +from ..threads import StartupTimer 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 @@ -142,7 +146,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.scheduled_start: + 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.scheduled_start: + self.common.log('Mode', 'start_server', 'Starting startup timer') + self.startup_thread = StartupTimer(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 +192,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. @@ -225,7 +271,11 @@ 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.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/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 27abf5e5..8e4a3338 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -228,7 +228,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.scheduled_start: + 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 +242,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.scheduled_start: + 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')) @@ -313,6 +319,10 @@ class OnionShareGui(QtWidgets.QMainWindow): 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 startup timer setting, ensure the widget is hidden. + if not self.common.settings.get('startup_timer'): + self.share_mode.server_status.startup_timer_container.hide() + self.receive_mode.server_status.startup_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..4bd9241b 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -56,10 +56,36 @@ class ServerStatus(QtWidgets.QWidget): self.app = app self.web = None + self.scheduled_start = None self.local_only = local_only self.resizeEvent(None) + # Startup timer layout + self.startup_timer_label = QtWidgets.QLabel(strings._('gui_settings_startup_timer')) + self.startup_timer = QtWidgets.QDateTimeEdit() + self.startup_timer.setDisplayFormat("hh:mm A MMM d, yy") + if self.local_only: + # For testing + self.startup_timer.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.startup_timer.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + else: + # Set proposed timer to be 5 minutes into the future + self.startup_timer.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.startup_timer.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) + self.startup_timer.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) + startup_timer_layout = QtWidgets.QHBoxLayout() + startup_timer_layout.addWidget(self.startup_timer_label) + startup_timer_layout.addWidget(self.startup_timer) + + # Startup timer container, so it can all be hidden and shown as a group + startup_timer_container_layout = QtWidgets.QVBoxLayout() + startup_timer_container_layout.addLayout(startup_timer_layout) + self.startup_timer_container = QtWidgets.QWidget() + self.startup_timer_container.setLayout(startup_timer_container_layout) + self.startup_timer_container.hide() + # Shutdown timeout layout self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout')) self.shutdown_timeout = QtWidgets.QDateTimeEdit() @@ -123,6 +149,7 @@ class ServerStatus(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.server_button) layout.addLayout(url_layout) + layout.addWidget(self.startup_timer_container) layout.addWidget(self.shutdown_timeout_container) self.setLayout(layout) @@ -154,6 +181,13 @@ class ServerStatus(QtWidgets.QWidget): except: pass + def startup_timer_reset(self): + """ + Reset the timer in the UI after stopping a share + """ + self.startup_timer.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + if not self.local_only: + self.startup_timer.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) def shutdown_timeout_reset(self): """ @@ -163,6 +197,14 @@ class ServerStatus(QtWidgets.QWidget): if not self.local_only: self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) + def show_url(self): + """ + Show the URL in the UI. + """ + self.url.setText(self.get_url()) + self.url.show() + self.copy_url_button.show() + def update(self): """ Update the GUI elements based on the current state. @@ -190,16 +232,16 @@ class ServerStatus(QtWidgets.QWidget): else: self.url_description.setToolTip(strings._('gui_url_label_stay_open')) - self.url.setText(self.get_url()) - self.url.show() - - self.copy_url_button.show() + 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('startup_timer'): + self.startup_timer_container.hide() + if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.hide() @@ -227,6 +269,8 @@ class ServerStatus(QtWidgets.QWidget): else: self.server_button.setText(strings._('gui_receive_start_server')) self.server_button.setToolTip('') + if self.common.settings.get('startup_timer'): + self.startup_timer_container.show() if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.show() elif self.status == self.STATUS_STARTED: @@ -236,23 +280,30 @@ 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('startup_timer'): + self.startup_timer_container.hide() 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)) - 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.scheduled_start: + self.server_button.setText(strings._('gui_waiting_to_start').format(self.scheduled_start)) + self.startup_timer_container.hide() + else: + self.server_button.setText(strings._('gui_please_wait')) if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_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('startup_timer'): + self.startup_timer_container.hide() if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.hide() @@ -261,6 +312,11 @@ class ServerStatus(QtWidgets.QWidget): Toggle starting or stopping the server. """ if self.status == self.STATUS_STOPPED: + if self.common.settings.get('startup_timer'): + if self.local_only: + self.scheduled_start = self.startup_timer.dateTime().toPyDateTime() + else: + self.scheduled_start = self.startup_timer.dateTime().toPyDateTime().replace(second=0, microsecond=0) if self.common.settings.get('shutdown_timeout'): if self.local_only: self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() @@ -302,6 +358,7 @@ class ServerStatus(QtWidgets.QWidget): Stop the server. """ self.status = self.STATUS_WORKING + self.startup_timer_reset() self.shutdown_timeout_reset() self.update() self.server_stopped.emit() @@ -312,6 +369,7 @@ class ServerStatus(QtWidgets.QWidget): """ self.common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup') self.status = self.STATUS_WORKING + self.startup_timer_reset() self.shutdown_timeout_reset() self.update() self.server_canceled.emit() diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 2933784c..f29915a7 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -71,6 +71,23 @@ class SettingsDialog(QtWidgets.QDialog): self.public_mode_widget = QtWidgets.QWidget() self.public_mode_widget.setLayout(public_mode_layout) + # Whether or not to use a startup ('auto-start') timer + self.startup_timer_checkbox = QtWidgets.QCheckBox() + self.startup_timer_checkbox.setCheckState(QtCore.Qt.Checked) + self.startup_timer_checkbox.setText(strings._("gui_settings_startup_timer_checkbox")) + startup_timer_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer")) + startup_timer_label.setStyleSheet(self.common.css['settings_whats_this']) + startup_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + startup_timer_label.setOpenExternalLinks(True) + startup_timer_label.setMinimumSize(public_mode_label.sizeHint()) + startup_timer_layout = QtWidgets.QHBoxLayout() + startup_timer_layout.addWidget(self.startup_timer_checkbox) + startup_timer_layout.addWidget(startup_timer_label) + startup_timer_layout.addStretch() + startup_timer_layout.setContentsMargins(0,0,0,0) + self.startup_timer_widget = QtWidgets.QWidget() + self.startup_timer_widget.setLayout(startup_timer_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) @@ -91,6 +108,7 @@ class SettingsDialog(QtWidgets.QDialog): # General settings layout general_group_layout = QtWidgets.QVBoxLayout() general_group_layout.addWidget(self.public_mode_widget) + general_group_layout.addWidget(self.startup_timer_widget) general_group_layout.addWidget(self.shutdown_timeout_widget) general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label")) general_group.setLayout(general_group_layout) @@ -488,6 +506,12 @@ class SettingsDialog(QtWidgets.QDialog): else: self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked) + startup_timer = self.old_settings.get('startup_timer') + if startup_timer: + self.startup_timer_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.startup_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) + shutdown_timeout = self.old_settings.get('shutdown_timeout') if shutdown_timeout: self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) @@ -932,6 +956,7 @@ 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('startup_timer', self.startup_timer_checkbox.isChecked()) settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked()) # Complicated logic here to force v2 onion mode on or off depending on other settings diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 3b05bebf..fff56bc2 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() + 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 StartupTimer(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(StartupTimer, self).__init__() + self.mode = mode + self.canceled = canceled + self.mode.common.log('StartupTimer', '__init__') + + # allow this thread to be terminated + self.setTerminationEnabled() + + def run(self): + now = QtCore.QDateTime.currentDateTime() + scheduled_start = now.secsTo(self.mode.server_status.scheduled_start) + try: + # Sleep until scheduled time + while scheduled_start > 0 and self.canceled == False: + time.sleep(0.1) + now = QtCore.QDateTime.currentDateTime() + scheduled_start = now.secsTo(self.mode.server_status.scheduled_start) + # Timer has now finished + self.mode.server_status.server_button.setText(strings._('gui_please_wait')) + self.mode.server_status_label.setText(strings._('gui_status_indicator_share_working')) + if self.canceled == False: + self.success.emit() + except ValueError as e: + self.error.emit(e.args[0]) + return |