Skip to content
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

Merged
merged 25 commits into from
Jul 17, 2015
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
17395a3
Reset spyder and restart from running application
goanpeca May 7, 2015
01c2188
Fix response for subprocess
goanpeca Jun 11, 2015
037e59d
Add option to preferences dialog
goanpeca Jun 11, 2015
58d3772
Rebased
goanpeca Jun 11, 2015
6c9f61f
Add traceback print error handling and pep8 corrections
goanpeca Jun 11, 2015
e04aca3
Add restart, reset and close extra error handling
goanpeca Jun 11, 2015
519201a
Improve code style
goanpeca Jun 12, 2015
bc95a72
Change timer settings
goanpeca Jun 12, 2015
13330fc
Change timer settings
goanpeca Jun 12, 2015
f0ec3f5
Update reset logic
goanpeca Jun 12, 2015
4003c37
Fix duplicate range
goanpeca Jun 12, 2015
28fc618
Rework exception handling
goanpeca Jun 13, 2015
eb8b585
Update error codes
goanpeca Jun 18, 2015
d341b5c
Update error codes
goanpeca Jun 18, 2015
4e9b789
Improve comments and variable names
goanpeca Jul 1, 2015
a6f8646
Reorganize code in config dialog
goanpeca Jul 1, 2015
a0d7a5f
Add splash screen
goanpeca Jul 2, 2015
3f4b398
Using current sys.path to restart app
SylvainCorlay Jul 8, 2015
a738d31
Using current sys.path to restart app
SylvainCorlay Jul 8, 2015
4931b49
Fix check logic on restarter script
goanpeca Jul 8, 2015
363853c
Fix typo in docstring
goanpeca Jul 8, 2015
63ca7be
Modify path in restart method only in bootstrap mode
goanpeca Jul 11, 2015
73eab81
reduce waiting times on all OSs
goanpeca Jul 11, 2015
37e0535
Reduce max waiting time when resetting
goanpeca Jul 15, 2015
abdb8f9
Remove helper function adn use to_text_string from spyderlib.py3compat
goanpeca Jul 17, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions spyderlib/plugins/configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,36 +124,37 @@ class ConfigDialog(QDialog):

def __init__(self, parent=None):
QDialog.__init__(self, parent)


self.main = parent

# Widgets
self.pages_widget = QStackedWidget()
self.contents_widget = QListWidget()
self.button_reset = QPushButton(_('Reset to defaults'))

bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply |
QDialogButtonBox.Cancel)
self.apply_btn = bbox.button(QDialogButtonBox.Apply)

# Widgets setup
# Destroying the C++ object right after closing the dialog box,
# otherwise it may be garbage-collected in another QThread
# (e.g. the editor's analysis thread in Spyder), thus leading to
# a segmentation fault on UNIX or an application crash on Windows
self.setAttribute(Qt.WA_DeleteOnClose)

self.contents_widget = QListWidget()
self.setWindowTitle(_('Preferences'))
self.setWindowIcon(ima.icon('configure'))
self.contents_widget.setMovement(QListView.Static)
self.contents_widget.setSpacing(1)

bbox = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Apply
|QDialogButtonBox.Cancel)
self.apply_btn = bbox.button(QDialogButtonBox.Apply)
bbox.accepted.connect(self.accept)
bbox.rejected.connect(self.reject)
bbox.clicked.connect(self.button_clicked)

self.pages_widget = QStackedWidget()
self.pages_widget.currentChanged.connect(self.current_page_changed)

self.contents_widget.currentRowChanged.connect(
self.pages_widget.setCurrentIndex)
self.contents_widget.setCurrentRow(0)

# Layout
hsplitter = QSplitter()
hsplitter.addWidget(self.contents_widget)
hsplitter.addWidget(self.pages_widget)

btnlayout = QHBoxLayout()
btnlayout.addWidget(self.button_reset)
btnlayout.addStretch(1)
btnlayout.addWidget(bbox)

Expand All @@ -163,12 +164,18 @@ def __init__(self, parent=None):

self.setLayout(vlayout)

self.setWindowTitle(_('Preferences'))
self.setWindowIcon(ima.icon('configure'))
# Signals and slots
self.button_reset.clicked.connect(self.main.reset_spyder)
self.pages_widget.currentChanged.connect(self.current_page_changed)
self.contents_widget.currentRowChanged.connect(
self.pages_widget.setCurrentIndex)
bbox.accepted.connect(self.accept)
bbox.rejected.connect(self.reject)
bbox.clicked.connect(self.button_clicked)

# Ensures that the config is present on spyder first run
CONF.set('main', 'interface_language', load_lang_conf())

def get_current_index(self):
"""Return current page index"""
return self.contents_widget.currentRow()
Expand Down
172 changes: 160 additions & 12 deletions spyderlib/restart_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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__()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What reset evals to? True or False? Just curious :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, I just saw below that this is the case ;-)

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Expand All @@ -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)):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this for loop exactly do? Could you add a comment below

# Wait for original process ...

to say something about it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a comment one line above :)

Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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).

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what you say is that we should leave sys.path as it is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm saying leave the PR as it is. 😄

Copy link
Member Author

Choose a reason for hiding this comment

The 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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there was an exception before, pid_reset won't be defined.
I think this block should go in an else: statement after the try: and except:, since you shouldn''t have to wait if there was an error (I guess).

Copy link
Member Author

Choose a reason for hiding this comment

The 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__':
Expand Down
36 changes: 31 additions & 5 deletions spyderlib/spyder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"))
Expand Down Expand Up @@ -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
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding env['PYTHONPATH'] = ':'.join(sys.path) solves the issue for me.

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blink1073 can you give it a shot in windows?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it was working on windows? @blink1073 ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works with the latest version on Windows.

Copy link
Member Author

Choose a reason for hiding this comment

The 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':
Expand Down