diff --git a/bootstrap.py b/bootstrap.py index 78152fb72a9..1541886880a 100755 --- a/bootstrap.py +++ b/bootstrap.py @@ -48,6 +48,9 @@ options, args = parser.parse_args() +# Store variable to be used in self.restart (restart spyder instance) +os.environ['SPYDER_BOOTSTRAP_ARGS'] = str(sys.argv[1:]) + assert options.gui in (None, 'pyqt5', 'pyqt', 'pyside'), \ "Invalid GUI toolkit option '%s'" % options.gui diff --git a/spyderlib/config.py b/spyderlib/config.py index f8a2dc5f0bb..e563e4ada47 100644 --- a/spyderlib/config.py +++ b/spyderlib/config.py @@ -477,6 +477,7 @@ def is_ubuntu(): '_/save current layout': "Shift+Alt+S", '_/toggle default layout': "Shift+Alt+Home", '_/layout preferences': "Shift+Alt+P", + '_/restart': "Shift+Alt+R", '_/quit': "Ctrl+Q", # -- In plugins/editor '_/debug step over': "Ctrl+F10", diff --git a/spyderlib/restart_app.py b/spyderlib/restart_app.py new file mode 100644 index 00000000000..e2f519d8796 --- /dev/null +++ b/spyderlib/restart_app.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright © 2015 The Spyder Development Team +# Licensed under the terms of the MIT License +# (see spyderlib/__init__.py for details) + +""" +Restart Spyder + +A helper script that allows Spyder to restart from within the application. +""" + +import ast +import os +import os.path as osp +import subprocess +import sys +import time + + +PY2 = sys.version[0] == '2' + + +def _is_pid_running_on_windows(pid): + """Check if a process is running on windows systems based on the pid.""" + pid = str(pid) + + # Hide flashing command prompt + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + process = subprocess.Popen(r'tasklist /fi "PID eq {0}"'.format(pid), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + startupinfo=startupinfo) + stdoutdata, stderrdata = process.communicate() + stdoutdata = to_text_string(stdoutdata) + process.kill() + check = pid in stdoutdata + + return check + + +def _is_pid_running_on_unix(pid): + """Check if a process is running on unix systems based on the pid.""" + try: + # On unix systems os.kill with a 0 as second argument only pokes the + # process (if it exists) and does not kill it + os.kill(pid, 0) + except OSError: + return False + else: + return True + + +def is_pid_running(pid): + """Check if a process is running based on the pid.""" + # Select the correct function depending on the OS + if os.name == 'nt': + return _is_pid_running_on_windows(pid) + else: + return _is_pid_running_on_unix(pid) + + +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 + else: + return str(obj, encoding) + + +def main(): + # 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) + + # 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]): + 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) + args = ast.literal_eval(spyder_args) + + # Enforce the --new-instance flag when running spyder + if '--new-instance' 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 + args = ' '.join(args) + + # Get python excutable running this script + python = sys.executable + + # Build the command + if is_bootstrap: + spyder = osp.join(spyder_folder, 'bootstrap.py') + else: + spyderlib = osp.join(spyder_folder, 'spyderlib') + spyder = osp.join(spyderlib, 'start_app.py') + + command = '"{0}" "{1}" {2}'.format(python, spyder, args) + + # Adjust the command and/or arguments to subprocess depending on the OS + shell = os.name != 'nt' + + # Wait for original process to end before launching the new instance + while True: + if not is_pid_running(pid): + break + time.sleep(0.2) # Throttling control + + env = os.environ.copy() + try: + subprocess.Popen(command, shell=shell, env=env) + except Exception as error: + print(command) + print(error) + time.sleep(15) + + +if __name__ == '__main__': + main() diff --git a/spyderlib/spyder.py b/spyderlib/spyder.py index ddff947cc4c..c28c8e2ce02 100644 --- a/spyderlib/spyder.py +++ b/spyderlib/spyder.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright © 2009-2013 Pierre Raybaut +# Copyright © 2013-2015 The Spyder Development Team # Licensed under the terms of the MIT License # (see spyderlib/__init__.py for details) @@ -30,6 +31,7 @@ import re import socket import shutil +import subprocess import sys import threading @@ -118,7 +120,7 @@ from spyderlib.baseconfig import (get_conf_path, get_module_data_path, get_module_source_path, STDERR, DEBUG, DEV, debug_print, TEST, SUBFOLDER, MAC_APP_NAME, - running_in_mac_app) + running_in_mac_app, get_module_path) from spyderlib.config import CONF, EDIT_EXT, IMPORT_EXT, OPEN_FILES_PORT from spyderlib.cli_options import get_options from spyderlib import dependencies @@ -853,10 +855,16 @@ def create_edit_action(text, tr_text, icon_name): icon='exit.png', tip=_("Quit"), triggered=self.console.quit) self.register_shortcut(quit_action, "_", "Quit") + restart_action = create_action(self, _("&Restart"), + icon='restart.png', + tip=_("Restart"), + triggered=self.restart) + self.register_shortcut(restart_action, "_", "Restart") + self.file_menu_actions += [self.load_temp_session_action, self.load_session_action, self.save_session_action, - None, quit_action] + None, restart_action, quit_action] self.set_splash("") self.debug_print(" ..widgets") @@ -2653,6 +2661,59 @@ 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""" + # Get start path to use in restart script + 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 + # Note: Variables defined in bootstrap.py and spyderlib\start_app.py + env = os.environ.copy() + bootstrap_args = env.pop('SPYDER_BOOTSTRAP_ARGS', None) + spyder_args = env.pop('SPYDER_ARGS') + + # Get current process and python running spyder + pid = os.getpid() + python = sys.executable + + # Check if started with bootstrap.py + if bootstrap_args is not None: + spyder_args = bootstrap_args + is_bootstrap = True + else: + is_bootstrap = False + + # Pass variables as environment variables (str) to restarter subprocess + env['SPYDER_ARGS'] = spyder_args + env['SPYDER_PID'] = str(pid) + env['SPYDER_IS_BOOTSTRAP'] = str(is_bootstrap) + + # Build the command and popen arguments depending on the OS + if os.name == 'nt': + # Hide flashing command prompt + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + shell = False + else: + startupinfo = None + shell = True + + command = '"{0}" "{1}"' + command = command.format(python, restart_script) + + try: + if self.closing(True): + subprocess.Popen(command, shell=shell, env=env, + startupinfo=startupinfo) + self.console.quit() + except Exception as error: + # If there is an error with subprocess, Spyder should not quit and + # the error can be inspected in the internal console + print(error) + print(command) + # ---- Interactive Tours def show_tour(self, index): """ """ diff --git a/spyderlib/start_app.py b/spyderlib/start_app.py index b89a5321e3e..341bc9b98ce 100644 --- a/spyderlib/start_app.py +++ b/spyderlib/start_app.py @@ -6,6 +6,7 @@ import os.path as osp import random import socket +import sys import time # Local imports @@ -63,6 +64,9 @@ def main(): # Parse command line options options, args = get_options() + # Store variable to be used in self.restart (restart spyder instance) + os.environ['SPYDER_ARGS'] = str(sys.argv[1:]) + if CONF.get('main', 'single_instance') and not options.new_instance \ and not running_in_mac_app(): # Minimal delay (0.1-0.2 secs) to avoid that several