Skip to content

Commit

Permalink
Merge pull request #2423 from goanpeca/reset
Browse files Browse the repository at this point in the history
Reset spyder and restart from within running application
  • Loading branch information
ccordoba12 committed Jul 17, 2015
2 parents 723ba4c + abdb8f9 commit c759852
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 54 deletions.
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
187 changes: 157 additions & 30 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.py3compat import to_text_string
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 @@ -63,55 +75,128 @@ def is_pid_running(pid):
return _is_pid_running_on_unix(pid)


# Note: Copied from py3compat because we can't rely on Spyder
# being installed when using bootstrap.py
def to_text_string(obj, encoding=None):
"""Convert `obj` to (unicode) text string"""
if PY2:
# Python 2
if encoding is None:
return unicode(obj)
else:
return unicode(obj, encoding)
else:
# Python 3
if encoding is None:
return str(obj)
elif isinstance(obj, str):
# In case this function is not used properly, this could happen
return obj
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:
return str(obj, encoding)
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)

# 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 +211,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)):
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 = 20 # Seconds
for counter in range(int(wait_time/SLEEP_TIME)):
if not is_pid_running(pid_reset):
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)

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

0 comments on commit c759852

Please sign in to comment.