Repository: spyder-ide/spyder

Issue:
Spyder fails to restart after selecting Restart from the macOS standalone application menu bar
Spyder fails to restart after selecting File->Restart or File->Restart in debug mode from the macOS standalone application menu bar. This does not manifest when launched from conda, pip, or bootstrap. It appears to be an issue with the standalone application only. I'm not sure yet what the underlying issue is.


Discussion:
#17729 may be related.

The OP in that and this issue do not manifest in 5.2.2. Two updates to `py2app` have occurred: #16651 between 5.2.2 and 5.3.0; and #17612 after 5.3.0. Both issues manifest in 5.3.0 and recent local builds, so it is likely that they are both related to the update to `py2app` in #16651.
#17524 manifests both issues. This PR is immediately prior to #16651, so it seems these issues are not related to any updates to `py2app`. Investigation continues...
I've narrowed it down to #16409; it looks like updating `pyqt` is the culprit. I'll check if changing versions will change anything.
I think this is the culprit (which was added in that PR):

https://github.com/spyder-ide/spyder/blob/dfb3a5f40405d5e72acaeacd73e20aef12dfeddd/spyder/app/mainwindow.py#L1504-L1532

So, we need to force quit the Mac app because if not it closes with a segfault when using PyQt 5.15
#17732 is a necessary but insufficient fix for this issue.
The core problem seems to be related to the way `pkg_resources` obtains entry points after updating to `py2app=0.28`.
Essentially, Spyder seems to startup just fine, but restarting Spyder from the menubar (launching from a subprocess via `start.py`), `pkg_resources` does not obtain entry points. The traceback can be replicated from the command line. See https://github.com/spyder-ide/spyder/pull/17732#issuecomment-1106717746 and https://github.com/spyder-ide/spyder/pull/17732#issuecomment-1106911615.
Pending feedback from @ronaldoussoren, I think we have two options:
- Migrate to `importlib`. I don't even know if this would fix the problem and it would be a _major_ undertaking. I don't know such plans are being considered down the road, but this seems to be a non-starter for this issue.
- Restart the macOS app using `Spyder.app/Contents/MacOS/Spyder [options]` instead of `Spyder.app/Contents/MacOS/python ...start.py [options]`. This seems the most feasible for this issue.
I'd always restart using .../MacOS/Spyder. The launcher binary does some extra work to set up the environment correctly and I'm not 100% sure if the .../MacOS/python command gets this right at the moment.

That said, I don't really know that Spyder's Restart menu actually does and have never looked into the state of the art w.r.t restarting a macOS app. 
> I'd always restart using .../MacOS/Spyder. The launcher binary does some extra work to set up the environment correctly and I'm not 100% sure if the .../MacOS/python command gets this right at the moment.
> 
> That said, I don't really know that Spyder's Restart menu actually does and have never looked into the state of the art w.r.t restarting a macOS app.

@ronaldoussoren, Thank you for your input! Just for my own information, where is the launcher binary created in the `py2app` source code?

Resolving patch:
diff --git a/spyder/app/restart.py b/spyder/app/restart.py
--- a/spyder/app/restart.py
+++ b/spyder/app/restart.py
@@ -27,7 +27,7 @@
 
 # Local imports
 from spyder.app.utils import create_splash_screen
-from spyder.config.base import _
+from spyder.config.base import _, running_in_mac_app
 from spyder.utils.image_path_manager import get_image_path
 from spyder.utils.encoding import to_unicode
 from spyder.utils.qthelpers import qapplication
@@ -163,16 +163,18 @@ def main():
     # Proper high DPI scaling is available in Qt >= 5.6.0. This attribute must
     # be set before creating the application.
     #==========================================================================
+    env = os.environ.copy()
+
     if CONF.get('main', 'high_dpi_custom_scale_factor'):
         factors = str(CONF.get('main', 'high_dpi_custom_scale_factors'))
         f = list(filter(None, factors.split(';')))
         if len(f) == 1:
-            os.environ['QT_SCALE_FACTOR'] = f[0]
+            env['QT_SCALE_FACTOR'] = f[0]
         else:
-            os.environ['QT_SCREEN_SCALE_FACTORS'] = factors
+            env['QT_SCREEN_SCALE_FACTORS'] = factors
     else:
-        os.environ['QT_SCALE_FACTOR'] = ''
-        os.environ['QT_SCREEN_SCALE_FACTORS'] = ''
+        env['QT_SCALE_FACTOR'] = ''
+        env['QT_SCREEN_SCALE_FACTORS'] = ''
 
     # Splash screen
     # -------------------------------------------------------------------------
@@ -185,14 +187,13 @@ def main():
     restarter.set_splash_message(_('Closing Spyder'))
 
     # Get variables
-    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', 'False')
+    spyder_args = env.pop('SPYDER_ARGS', None)
+    pid = env.pop('SPYDER_PID', None)
+    is_bootstrap = env.pop('SPYDER_IS_BOOTSTRAP', None)
+    reset = env.pop('SPYDER_RESET', 'False')
 
     # Get the spyder base folder based on this file
-    this_folder = osp.split(osp.dirname(osp.abspath(__file__)))[0]
-    spyder_folder = osp.split(this_folder)[0]
+    spyder_dir = osp.dirname(osp.dirname(osp.dirname(osp.abspath(__file__))))
 
     if not any([spyder_args, pid, is_bootstrap, reset]):
         error = "This script can only be called from within a Spyder instance"
@@ -226,21 +227,17 @@ def main():
     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 executable running this script
-    python = sys.executable
-
-    # Build the command
-    if is_bootstrap:
-        spyder = osp.join(spyder_folder, 'bootstrap.py')
+    # Build the base command
+    if running_in_mac_app(sys.executable):
+        exe = env['EXECUTABLEPATH']
+        command = [f'"{exe}"']
     else:
-        spyderdir = osp.join(spyder_folder, 'spyder')
-        spyder = osp.join(spyderdir, 'app', 'start.py')
+        if is_bootstrap:
+            script = osp.join(spyder_dir, 'bootstrap.py')
+        else:
+            script = osp.join(spyder_dir, 'spyder', 'app', 'start.py')
 
-    command = '"{0}" "{1}" {2}'.format(python, spyder, args)
+        command = [f'"{sys.executable}"', f'"{script}"']
 
     # Adjust the command and/or arguments to subprocess depending on the OS
     shell = not IS_WINDOWS
@@ -258,16 +255,14 @@ def main():
         # 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)
+            p = subprocess.Popen(' '.join(command + args_reset),
+                                 shell=shell, env=env)
         except Exception as error:
             restarter.launch_error_message(error_type=RESET_ERROR, error=error)
         else:
@@ -297,7 +292,7 @@ def main():
     # -------------------------------------------------------------------------
     restarter.set_splash_message(_('Restarting'))
     try:
-        subprocess.Popen(command, shell=shell, env=env)
+        subprocess.Popen(' '.join(command + args), shell=shell, env=env)
     except Exception as error:
         restarter.launch_error_message(error_type=RESTART_ERROR, error=error)
 


Test patch:
diff --git a/installers/macOS/test_app.sh b/installers/macOS/test_app.sh
--- a/installers/macOS/test_app.sh
+++ b/installers/macOS/test_app.sh
@@ -1,49 +1,30 @@
 #!/bin/bash
-#
-# The Bash shell script executes a command with a time-out.
-# Upon time-out expiration SIGTERM (15) is sent to the process. If the signal
-# is blocked, then the subsequent SIGKILL (9) terminates it.
-#
-# Based on the Bash documentation example.
-#
-# Dmitry V Golovashkin <Dmitry.Golovashkin@sas.com>
-
-scriptName="${0##*/}"
-
-declare -i DEFAULT_TIMEOUT=10
-declare -i DEFAULT_INTERVAL=1
-declare -i DEFAULT_DELAY=2
-
-# Timeout.
-declare -i timeout=DEFAULT_TIMEOUT
-# Interval between checks if the process is still alive.
-declare -i interval=DEFAULT_INTERVAL
-# Delay between posting the SIGTERM signal and destroying the process by SIGKILL.
-declare -i delay=DEFAULT_DELAY
-
-function printUsage() {
-    cat <<EOF
-
-Synopsis
-    $scriptName [-t timeout] [-i interval] [-d delay] distdir
-    Launch Spyder.app in distdir with a time-out.
-    Upon time-out expiration SIGTERM (15) is sent to the process. If SIGTERM
-    signal is blocked, then the subsequent SIGKILL (9) terminates it.
-
-    -t timeout
-        Number of seconds to wait for command completion.
-        Default value: $DEFAULT_TIMEOUT seconds.
-
-    -i interval
-        Interval between checks if the process is still alive.
-        Positive integer, default value: $DEFAULT_INTERVAL seconds.
-
-    -d delay
-        Delay between posting the SIGTERM signal and destroying the
-        process by SIGKILL. Default value: $DEFAULT_DELAY seconds.
-
-As of today, Bash does not support floating point arithmetic (sleep does),
+
+timeout=60
+interval=1
+delay=10
+
+help() { cat <<EOF
+
+$(basename $0) [-t timeout] [-i interval] [-d delay] distdir
+Launch Spyder.app located in distdir with a time-out.
+Upon time-out expiration SIGTERM (15) is sent to Spyder. If SIGTERM
+signal is blocked, then the subsequent SIGKILL (9) terminates it.
+
+  distdir     Directory in which Spyder.app resides.
+
+  -t timeout  Number of seconds to wait for command completion.
+              Default value: $timeout seconds.
+
+  -i interval Interval between checks if the process is still alive.
+              Positive integer, default value: $interval seconds.
+
+  -d delay    Delay between posting the SIGTERM signal and destroying the
+              process by SIGKILL. Default value: $delay seconds.
+
+Bash does not support floating point arithmetic (sleep does),
 therefore all delay/time values must be integers.
+
 EOF
 }
 
@@ -53,34 +34,42 @@ while getopts ":t:i:d:" option; do
         t) timeout=$OPTARG ;;
         i) interval=$OPTARG ;;
         d) delay=$OPTARG ;;
-        *) printUsage; exit 0 ;;
+        *) help; exit 0 ;;
     esac
 done
-shift $((OPTIND - 1))
+shift $(($OPTIND - 1))
 
-# $# should be at least 1 (the command to execute), however it may be strictly
-# greater than 1 if the command itself has options.
-if (($# == 0 || interval <= 0)); then
-    printUsage
-    exit 1
+distdir=$1
+if [[ -z $distdir ]]; then
+    help && exit 1
 fi
 
-# kill -0 pid   Exit code indicates if a signal may be sent to $pid process.
-(
-    ((t = timeout))
-    while ((t > 0)); do
-        sleep $interval
-        kill -0 $$ || exit 0
-        ((t -= interval))
-    done
-    echo "Timeout reached!"
-
-    # Be nice, post SIGTERM first.
-    kill -s SIGTERM $$ && kill -0 $$ || exit 0
-    sleep $delay
-    kill -s SIGKILL $$
-) 2> /dev/null &
-
 export SPYDER_DEBUG=3
 export INSTALLER_TEST=1
-exec $@/Spyder.app/Contents/MacOS/Spyder
+$1/Spyder.app/Contents/MacOS/Spyder &
+pid=$!
+
+t=$timeout
+while [[ $t > 0 ]]; do
+    sleep $interval
+    if ! $(kill -0 $pid 2> /dev/null); then
+        echo -e "\nSpyder shut down after $((timeout - t))s" && exit 1
+    fi
+    ((t -= $interval))
+done
+echo -e "\nSpyder launched successfully!"
+
+echo -e "Shutting down Spyder...\n"
+kill -s SIGTERM $pid 2> /dev/null
+
+t=$delay
+while [[ $t > 0 ]]; do
+    sleep $interval
+    if ! $(kill -0 $pid 2> /dev/null); then
+        echo -e "\nSpyder shut down successfully in $((delay - t))s" && exit 0
+    fi
+    ((t -= $interval))
+done
+
+echo -e "\nSpyder did not shut down properly; killing..."
+kill -s SIGKILL $pid && exit 1