sublimehq / sublime_text

Issue tracker for Sublime Text
https://www.sublimetext.com
803 stars 39 forks source link

calling sys.exit from the python console on Ubuntu causes it to freeze #1189

Open Nocturnhabeo opened 8 years ago

Nocturnhabeo commented 8 years ago

Summary

In Sublime Text 3 on Ubuntu 14.04 running import sys sys.exit(0) breaks ST3

Expected behavior

If I call sys.exit(0) it shouldn't freeze Sublime Text 3.

Actual behavior

The application locks up and you have to kill it manually

Steps to reproduce

  1. First step Open python console.
  2. Second step Type import sys
  3. Third step Type sys.exit(0)

    Environment

    • Operating system and version:
    • [ ] Windows ...
    • [ ] Mac OS ...
    • [x] Linux Ubuntu 14.04
    • Sublime Text:
    • Build 3103
FichteFoll commented 8 years ago

If I call sys.exit(0) it shouldn't freeze Sublime Text 3.

Well, what was your intention when doing that anyway?

Edit: Oh, this also happens on Windows and has since ST2.

wbond commented 8 years ago

Calling sys.exit is instructing the Python interpreter in plugin_host to die… which probably seems like a bad idea.

FichteFoll commented 8 years ago

Technically, calling sys.exit should never happen in plugin code for any reason, but if it does, maybe ST could prevent itself from freezing/hanging up? Otherwise I call "wontfix".

evandrocoan commented 7 years ago

I just had a package calling sys.exit(), and Sublime Text hanged. I tried to reproduce the hang again, but I could not. Running the command:

sublime.active_window().active_view().run_command( "example" )

import sys
import sublime
import sublime_plugin

class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        sys.exit()

Opens the window on Sublime Text build 3142:

image

Also, if I put this on plugin_loaded():

import sys
import sublime
import sublime_plugin

def plugin_loaded():
    pass
    sys.exit()

Sublime Text outputs this:

Traceback (most recent call last):
  File "D:\SublimeText\v3142\sublime_plugin.py", line 210, in on_api_ready
    m.plugin_loaded()
  File "C:\Users\Professional\AppData\Roaming\Sublime Text 3\Packages\User\bug.py", line 8, in plugin_loaded
    sys.exit()
SystemExit

``` DPI scale: 1 startup, version: 3142 windows x32 channel: dev executable: /D/User/Dropbox/Applications/SoftwareVersioning/SublimeText/v3142/sublime_text.exe working dir: /D/User/Dropbox/Applications/SoftwareVersioning/SublimeText/v3142 packages path: /C/Users/Professional/AppData/Roaming/Sublime Text 3/Packages state path: /C/Users/Professional/AppData/Roaming/Sublime Text 3/Local zip path: /D/User/Dropbox/Applications/SoftwareVersioning/SublimeText/v3142/Packages zip path: /C/Users/Professional/AppData/Roaming/Sublime Text 3/Installed Packages ignored_packages: ["Vintage"] pre session restore time: 0.341594 startup time: 0.506594 first paint time: 0.526594 reloading plugin Default.auto_indent_tag reloading plugin Default.block reloading plugin Default.comment reloading plugin Default.convert_syntax reloading plugin Default.copy_path reloading plugin Default.delete_word reloading plugin Default.detect_indentation reloading plugin Default.duplicate_line reloading plugin Default.echo reloading plugin Default.exec reloading plugin Default.fold reloading plugin Default.font reloading plugin Default.goto_line reloading plugin Default.history_list reloading plugin Default.indentation reloading plugin Default.install_package_control reloading plugin Default.kill_ring reloading plugin Default.mark reloading plugin Default.new_templates reloading plugin Default.open_context_url reloading plugin Default.open_in_browser reloading plugin Default.pane reloading plugin Default.paragraph reloading plugin Default.paste_from_history reloading plugin Default.profile reloading plugin Default.quick_panel reloading plugin Default.run_syntax_tests reloading plugin Default.save_on_focus_lost reloading plugin Default.scroll reloading plugin Default.set_unsaved_view_name reloading plugin Default.settings reloading plugin Default.show_scope_name reloading plugin Default.side_bar reloading plugin Default.sort reloading plugin Default.swap_line reloading plugin Default.switch_file reloading plugin Default.symbol reloading plugin Default.transform reloading plugin Default.transpose reloading plugin Default.trim_trailing_white_space reloading plugin Default.ui reloading plugin CSS.css_completions reloading plugin Diff.diff reloading plugin HTML.encode_html_entities reloading plugin HTML.html_completions reloading plugin bug reloading plugin User.bug plugins loaded Traceback (most recent call last): File "D:\SublimeText\v3142\sublime_plugin.py", line 210, in on_api_ready m.plugin_loaded() File "C:\Users\Professional\AppData\Roaming\Sublime Text 3\Packages\User\bug.py", line 8, in plugin_loaded sys.exit() SystemExit ```

rwols commented 7 years ago

This is one of those things that you obviously shouldn't do, but it'd be nice if ST could prevent it anyhow. Apparently, sys.exit just throws a SystemExit exception.

This exception is raised by the sys.exit() function. It inherits from BaseException instead of Exception so that it is not accidentally caught by code that catches Exception. This allows the exception to properly propagate up and cause the interpreter to exit. When it is not handled, the Python interpreter exits; no stack traceback is printed. The constructor accepts the same optional argument passed to sys.exit(). If the value is an integer, it specifies the system exit status (passed to C’s exit() function); if it is None, the exit status is zero; if it has another type (such as a string), the object’s value is printed and the exit status is one.

That said, you can still shoot yourself in the foot if you do

import os
os._exit(1)
evandrocoan commented 7 years ago

I was trying to stop the package from loading the remaining instructions, as I as testing somethings. But I found this question:

  1. https://stackoverflow.com/a/45403470/4934640 Stop python script without killing the python process

I can just call raise ValueError() instead of sys.exit() to stop a python script loading.

deathaxe commented 2 months ago

Well, modern python packages provide command line interfaces. A plugin may decide to run a python package's command via CLI interface for compatibility reasons.

If the invoked CLI command calls sys.exit() to terminate script execution due to wrong inputs or any precondition not being met, plugin_host is closed.

A real life example is:

from pip._internal.cli.main import main
main(["--help"])

A possible workaround or solution would be to monkey patch sys.exit.

import sys

from pip._internal.cli.main import main

class ReturnError(Exception):
    pass

def exit(code=0):
    raise ReturnError(f"Got exit code {code}.")

sys.exit = exit

try:
    main(["--help"])
except Exception as e:
    print(str(e))

Maybe that's something which should be handled by ST core.

Steps to reproduce

  1. Start ST in SAFE MODE
  2. Open ST's console
  3. Type import sys; sys.exit(1)
  4. Hit enter

Expected behavior

A running function or script is terminated as if return was programmed, but plugin_host continues running.

Actual behavior

>>> import sys
>>> sys.exit(1)
error: plugin_host-3.8 has exited unexpectedly, some plugin functionality won't be available until Sublime Text has been restarted

Sublime Text build number

4175

BenjaminSchaaf commented 2 months ago

I would consider this expected behavior. You could just as well raise SystemExit, os._exit() or os.kill(os.getpid()). Sublime Text shouldn't lock-up when doing this though.

deathaxe commented 2 months ago

ST4175 doesn't lock up anymore. It's just the plugin_host which exists. I find that a bit weird in a plugin environment. Maybe we should redirect it to sublime.run_command("exit") to really close the app :) instead of breaking plugin functionality.

BenjaminSchaaf commented 2 months ago

I was able to reproduce the locking up when running in the console still.

FichteFoll commented 2 months ago

Note that the best way to prevent this from occurring when invoking third-party libraries within the plugin host in a controlled invocation, e.g. with pip, is to except SystemExit: pass.

evandrocoan commented 2 months ago

Maybe we should redirect it to sublime.run_command("exit") to really close the app

It would be difficult to figure out why Sublime Text was exiting, as I would lose the console output (unless Sublime saved it before). Of course, this is only valid if Sublime Text does not hang completely, as reported in the first post.

Note that the best way to prevent this from occurring when invoking third-party libraries within the plugin host in a controlled invocation, e.g. with pip, is to except SystemExit: pass.

Nice catch. After researching, I see the following:

sys.exit() is identical to raise SystemExit(). It raises a Python exception, which may be caught by the caller.

Calling os._exit(1) does not have this behavior, and the Python interpreter is closed.

os._exit calls the C function _exit() which does an immediate program termination. Exit the process with status n, without calling cleanup handlers, flushing stdio buffers, etc.

A possible workaround or solution would be to monkey patch sys.exit.

I think it would not be nice to patch sys.exit as it already raises SystemExit and the application can catch it. The other possibilities of os._exit() and os.kill(os.getpid()) should not be the standard way to exit, so I do think there are not many packages calling them, unless doing something nasty.

deathaxe commented 2 months ago

It would be difficult to figure out why Sublime Text was exiting,

Sure, this was rather a sarcastic statement as it doesn't make sense for any plugin to kill its plugin_host while keeping the whole app running.

e.g. with pip, is to except SystemExit: pass.

I've actually expected SystemExit to also be caught by catch Exception, but that doesn't seem to be true.

os._exit() is probably rather unimportant as

a) it seems to be a protected function not intendet for general purpose public use (following its name starting with _) b) all cli like scripts I've seen so far just use sys.exit to terminate script execution and return a value to its caller (the shell).

FichteFoll commented 2 months ago

I've actually expected SystemExit to also be caught by catch Exception, but that doesn't seem to be true.

Indeed, SystemExit inherits BaseException but not Exception (same as KeyboardInterrupt btw). This is also why linters complain about bare excepts because those occasionally and unexpectedly catch these two exceptions when the userdeveloper probably didn't want to.

deathaxe commented 2 months ago

Makes sense to separate possible errors from a designed system exit signal. Being able to catch SystemExit is a sufficient solution to handle os.exit calls.

giampaolo commented 2 months ago

@deathaxe wrote:

A real life example is: from pip._internal.cli.main import main main(["--help"])

I would say: just catch SystemExit. Monkey patching by default is rarely a good idea. I would be surprised if sys.exit raised RuntimeError or something.

deathaxe commented 2 months ago

That's conclusion of this issue's discussion, yes.