-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reset spyder and restart from within running application #2423
Changes from 23 commits
17395a3
01c2188
037e59d
58d3772
6c9f61f
e04aca3
519201a
bc95a72
13330fc
f0ec3f5
4003c37
28fc618
eb8b585
d341b5c
4e9b789
a6f8646
a0d7a5f
3f4b398
a738d31
4931b49
363853c
63ca7be
73eab81
37e0535
abdb8f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,8 @@ | |
""" | ||
Restart Spyder | ||
|
||
A helper script that allows Spyder to restart from within the application. | ||
A helper script that allows to restart (and also reset) Spyder from within the | ||
running application. | ||
""" | ||
|
||
import ast | ||
|
@@ -19,7 +20,18 @@ | |
import time | ||
|
||
|
||
from spyderlib.baseconfig import _, get_image_path | ||
from spyderlib.qt.QtCore import Qt, QTimer | ||
from spyderlib.qt.QtGui import (QColor, QMessageBox, QPixmap, QSplashScreen, | ||
QWidget, QApplication) | ||
from spyderlib.utils import icon_manager as ima | ||
from spyderlib.utils.qthelpers import qapplication | ||
|
||
|
||
PY2 = sys.version[0] == '2' | ||
IS_WINDOWS = os.name == 'nt' | ||
SLEEP_TIME = 0.2 # Seconds for throttling control | ||
CLOSE_ERROR, RESET_ERROR, RESTART_ERROR = [1, 2, 3] # Spyder error codes | ||
|
||
|
||
def _is_pid_running_on_windows(pid): | ||
|
@@ -84,34 +96,128 @@ def to_text_string(obj, encoding=None): | |
return str(obj, encoding) | ||
|
||
|
||
class Restarter(QWidget): | ||
"""Widget in charge of displaying the splash information screen and the | ||
error messages. | ||
""" | ||
def __init__(self): | ||
super(Restarter, self).__init__() | ||
self.ellipsis = ['', '.', '..', '...', '..', '.'] | ||
|
||
# Widgets | ||
self.timer_ellipsis = QTimer(self) | ||
self.splash = QSplashScreen(QPixmap(get_image_path('splash.png'), | ||
'png')) | ||
|
||
# Widget setup | ||
self.setVisible(False) | ||
|
||
font = self.splash.font() | ||
font.setPixelSize(10) | ||
self.splash.setFont(font) | ||
self.splash.show() | ||
|
||
self.timer_ellipsis.timeout.connect(self.animate_ellipsis) | ||
|
||
def _show_message(self, text): | ||
"""Show message on splash screen.""" | ||
self.splash.showMessage(text, Qt.AlignBottom | Qt.AlignCenter | | ||
Qt.AlignAbsolute, QColor(Qt.white)) | ||
|
||
def animate_ellipsis(self): | ||
"""Animate dots at the end of the splash screen message.""" | ||
ellipsis = self.ellipsis.pop(0) | ||
text = ' '*len(ellipsis) + self.splash_text + ellipsis | ||
self.ellipsis.append(ellipsis) | ||
self._show_message(text) | ||
|
||
def set_splash_message(self, text): | ||
"""Sets the text in the bottom of the Splash screen.""" | ||
self.splash_text = text | ||
self._show_message(text) | ||
self.timer_ellipsis.start(500) | ||
|
||
def launch_error_message(self, error_type, error=None): | ||
"""Launch a message box with a predefined error message. | ||
|
||
Parameters | ||
---------- | ||
error_type : int [CLOSE_ERROR, RESET_ERROR, RESTART_ERROR] | ||
Possible error codes when restarting/reseting spyder. | ||
error : Exception | ||
Actual Python exception error caught. | ||
""" | ||
messages = {CLOSE_ERROR: _("It was not possible to close the previous " | ||
"Spyder instance.\nRestart aborted."), | ||
RESET_ERROR: _("Spyder could not reset to factory " | ||
"defaults.\nRestart aborted."), | ||
RESTART_ERROR: _("It was not possible to restart Spyder.\n" | ||
"Operation aborted.")} | ||
titles = {CLOSE_ERROR: _("Spyder exit error"), | ||
RESET_ERROR: _("Spyder reset error"), | ||
RESTART_ERROR: _("Spyder restart error")} | ||
|
||
if error: | ||
e = error.__repr__() | ||
message = messages[error_type] + _("\n\n{0}".format(e)) | ||
else: | ||
message = messages[error_type] | ||
|
||
title = titles[error_type] | ||
self.splash.hide() | ||
QMessageBox.warning(self, title, message, QMessageBox.Ok) | ||
raise RuntimeError(message) | ||
|
||
|
||
def main(): | ||
# Splash screen | ||
# ------------------------------------------------------------------------- | ||
# Start Qt Splash to inform the user of the current status | ||
app = qapplication() | ||
restarter = Restarter() | ||
resample = not IS_WINDOWS | ||
# Resampling SVG icon only on non-Windows platforms (see Issue 1314): | ||
icon = ima.icon('spyder', resample=resample) | ||
app.setWindowIcon(icon) | ||
restarter.set_splash_message(_('Closing Spyder')) | ||
|
||
# Get variables | ||
# Note: Variables defined in spyderlib\spyder.py 'restart()' method | ||
spyder_args = os.environ.pop('SPYDER_ARGS', None) | ||
pid = os.environ.pop('SPYDER_PID', None) | ||
is_bootstrap = os.environ.pop('SPYDER_IS_BOOTSTRAP', None) | ||
reset = os.environ.pop('SPYDER_RESET', None) | ||
|
||
# Get the spyder base folder based on this file | ||
spyder_folder = osp.split(osp.dirname(osp.abspath(__file__)))[0] | ||
|
||
if any([not spyder_args, not pid, not is_bootstrap]): | ||
if not any([spyder_args, pid, is_bootstrap, reset]): | ||
error = "This script can only be called from within a Spyder instance" | ||
raise RuntimeError(error) | ||
|
||
# Variables were stored as string literals in the environment, so to use | ||
# them we need to parse them in a safe manner. | ||
is_bootstrap = ast.literal_eval(is_bootstrap) | ||
pid = int(pid) | ||
pid = ast.literal_eval(pid) | ||
args = ast.literal_eval(spyder_args) | ||
reset = ast.literal_eval(reset) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem, I just saw below that this is the case ;-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :-) |
||
|
||
# Enforce the --new-instance flag when running spyder | ||
if '--new-instance' not in args: | ||
if is_bootstrap and not '--' in args: | ||
if is_bootstrap and '--' not in args: | ||
args = args + ['--', '--new-instance'] | ||
else: | ||
args.append('--new-instance') | ||
|
||
# Arrange arguments to be passed to the restarter subprocess | ||
# Create the arguments needed for reseting | ||
if '--' in args: | ||
args_reset = ['--', '--reset'] | ||
else: | ||
args_reset = ['--reset'] | ||
|
||
# Arrange arguments to be passed to the restarter and reset subprocess | ||
args = ' '.join(args) | ||
args_reset = ' '.join(args_reset) | ||
|
||
# Get python excutable running this script | ||
python = sys.executable | ||
|
@@ -126,21 +232,63 @@ def main(): | |
command = '"{0}" "{1}" {2}'.format(python, spyder, args) | ||
|
||
# Adjust the command and/or arguments to subprocess depending on the OS | ||
shell = os.name != 'nt' | ||
shell = not IS_WINDOWS | ||
|
||
# Wait for original process to end before launching the new instance | ||
while True: | ||
# Before launching a new Spyder instance we need to make sure that the | ||
# previous one has closed. We wait for a fixed and "reasonable" amount of | ||
# time and check, otherwise an error is launched | ||
wait_time = 90 if IS_WINDOWS else 30 # Seconds | ||
for counter in range(int(wait_time/SLEEP_TIME)): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this
to say something about it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a comment one line above :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, but I'd like to see a more detailed explanation. |
||
if not is_pid_running(pid): | ||
break | ||
time.sleep(0.2) # Throttling control | ||
time.sleep(SLEEP_TIME) # Throttling control | ||
QApplication.processEvents() # Needed to refresh the splash | ||
else: | ||
# The old spyder instance took too long to close and restart aborts | ||
restarter.launch_error_message(error_type=CLOSE_ERROR) | ||
|
||
env = os.environ.copy() | ||
|
||
# Reset Spyder (if required) | ||
# ------------------------------------------------------------------------- | ||
if reset: | ||
restarter.set_splash_message(_('Resetting Spyder to defaults')) | ||
command_reset = '"{0}" "{1}" {2}'.format(python, spyder, args_reset) | ||
|
||
try: | ||
p = subprocess.Popen(command_reset, shell=shell, env=env) | ||
except Exception as error: | ||
restarter.launch_error_message(error_type=RESET_ERROR, error=error) | ||
else: | ||
p.communicate() | ||
pid_reset = p.pid | ||
|
||
# Before launching a new Spyder instance we need to make sure that the | ||
# reset subprocess has closed. We wait for a fixed and "reasonable" | ||
# amount of time and check, otherwise an error is launched. | ||
wait_time = 60 # Seconds | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we're waiting the same amount of time in all OSes? I thought we decided to wait more on Windows (90 sec) and less on Linux and Mac (30 sec). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the reset process, the reset process normally is fast on windows as well. Is on waiting or the spyder instance to close that takes long, check this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say we don't know exactly what order they added things to the path. Worst case we have duplicated entries with the current method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So what you say is that we should leave There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm saying leave the PR as it is. 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I concur with blinky |
||
for counter in range(int(wait_time/SLEEP_TIME)): | ||
if not is_pid_running(pid_reset): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there was an exception before, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, I will make the adjustments, good suggestions |
||
break | ||
time.sleep(SLEEP_TIME) # Throttling control | ||
QApplication.processEvents() # Needed to refresh the splash | ||
else: | ||
# The reset subprocess took too long and it is killed | ||
try: | ||
p.kill() | ||
except OSError as error: | ||
restarter.launch_error_message(error_type=RESET_ERROR, | ||
error=error) | ||
else: | ||
restarter.launch_error_message(error_type=RESET_ERROR) | ||
|
||
# Restart | ||
# ------------------------------------------------------------------------- | ||
restarter.set_splash_message(_('Restarting')) | ||
try: | ||
subprocess.Popen(command, shell=shell, env=env) | ||
except Exception as error: | ||
print(command) | ||
print(error) | ||
time.sleep(15) | ||
restarter.launch_error_message(error_type=RESTART_ERROR, error=error) | ||
|
||
|
||
if __name__ == '__main__': | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -677,6 +677,9 @@ def create_edit_action(text, tr_text, icon): | |
module_completion.reset(), | ||
tip=_("Refresh list of module names " | ||
"available in PYTHONPATH")) | ||
reset_spyder_action = create_action( | ||
self, _("Reset Spyder to factory defaults"), | ||
triggered=self.reset_spyder) | ||
self.tools_menu_actions = [prefs_action, spyder_path_action] | ||
if WinUserEnvDialog is not None: | ||
winenv_action = create_action(self, | ||
|
@@ -687,7 +690,8 @@ def create_edit_action(text, tr_text, icon): | |
"(i.e. for all sessions)"), | ||
triggered=self.win_env) | ||
self.tools_menu_actions.append(winenv_action) | ||
self.tools_menu_actions += [None, update_modules_action] | ||
self.tools_menu_actions += [reset_spyder_action, None, | ||
update_modules_action] | ||
|
||
# External Tools submenu | ||
self.external_tools_menu = QMenu(_("External Tools")) | ||
|
@@ -2707,11 +2711,26 @@ def start_open_files_server(self): | |
self.sig_open_external_file.emit(fname) | ||
req.sendall(b' ') | ||
|
||
# ---- Quit and restart | ||
def restart(self): | ||
"""Quit and Restart Spyder application""" | ||
# ---- Quit and restart, and reset spyder defaults | ||
def reset_spyder(self): | ||
""" | ||
Quit and reset Spyder and then Restart application. | ||
""" | ||
answer = QMessageBox.warning(self, _("Warning"), | ||
_("Spyder will restart and reset to default settings: <br><br>" | ||
"Do you want to continue?"), | ||
QMessageBox.Yes | QMessageBox.No) | ||
if answer == QMessageBox.Yes: | ||
self.restart(reset=True) | ||
|
||
def restart(self, reset=False): | ||
""" | ||
Quit and Restart Spyder application. | ||
|
||
If reset True it allows to reset spyder on restart. | ||
""" | ||
# Get start path to use in restart script | ||
spyder_start_directory = get_module_path('spyderlib') | ||
spyder_start_directory = get_module_path('spyderlib') | ||
restart_script = osp.join(spyder_start_directory, 'restart_app.py') | ||
|
||
# Get any initial argument passed when spyder was started | ||
|
@@ -2735,6 +2754,13 @@ def restart(self): | |
env['SPYDER_ARGS'] = spyder_args | ||
env['SPYDER_PID'] = str(pid) | ||
env['SPYDER_IS_BOOTSTRAP'] = str(is_bootstrap) | ||
env['SPYDER_RESET'] = str(reset) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the problem was only with the reset then,, or even if you just try to restart spyder it was failing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @blink1073 can you give it a shot in windows? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it was working on windows? @blink1073 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It works with the latest version on Windows. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great :-), thanks for checking. |
||
|
||
if DEV: | ||
if os.name == 'nt': | ||
env['PYTHONPATH'] = ';'.join(sys.path) | ||
else: | ||
env['PYTHONPATH'] = ':'.join(sys.path) | ||
|
||
# Build the command and popen arguments depending on the OS | ||
if os.name == 'nt': | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this necessary?