sublime_text icon indicating copy to clipboard operation
sublime_text copied to clipboard

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

Open Nocturnhabeo opened this issue 9 years ago • 17 comments
trafficstars

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

Nocturnhabeo avatar Apr 29 '16 00:04 Nocturnhabeo

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.

FichteFoll avatar Apr 29 '16 00:04 FichteFoll

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

wbond avatar Apr 29 '16 13:04 wbond

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

FichteFoll avatar Apr 29 '16 20:04 FichteFoll

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

evandrocoan avatar Jul 30 '17 18:07 evandrocoan

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)

rwols avatar Jul 30 '17 18:07 rwols

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.

evandrocoan avatar Jul 30 '17 18:07 evandrocoan

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

deathaxe avatar Jun 15 '24 09:06 deathaxe

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.

BenjaminSchaaf avatar Jun 17 '24 05:06 BenjaminSchaaf

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.

deathaxe avatar Jun 17 '24 06:06 deathaxe

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

BenjaminSchaaf avatar Jun 17 '24 06:06 BenjaminSchaaf

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.

FichteFoll avatar Jun 17 '24 15:06 FichteFoll

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.

evandrocoan avatar Jun 18 '24 00:06 evandrocoan

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

deathaxe avatar Jun 18 '24 06:06 deathaxe

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 ~~user~~developer probably didn't want to.

FichteFoll avatar Jun 19 '24 07:06 FichteFoll

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.

deathaxe avatar Jun 19 '24 10:06 deathaxe

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

giampaolo avatar Jun 27 '24 05:06 giampaolo

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

deathaxe avatar Jun 27 '24 06:06 deathaxe