jquast / blessed

Blessed is an easy, practical library for making python terminal apps
http://pypi.python.org/pypi/blessed
MIT License
1.2k stars 72 forks source link

Blessed is not thread safe which messes the ouput on multiple terminal.location #214

Closed hash3liZer closed 4 months ago

hash3liZer commented 3 years ago

First of all, it's really a great library and kudos to the development. I've integrated this library in a project of mine where multiple threads are printing on the terminal at the same time. Consider these two functions:

import progressbar
from blessed import Terminal
import time

term = Terminal()

def fucn1():
    with term.location(0, 5):
        for n in progressbar.progressbar(range(100)):
            time.sleep(0.3)

def func2():
    with term.location(0, 10):
        for n in progressbar.progressbar(range(10000)):
            time.sleep(0.3)

Now, if you run these functions simulataneously using threads (a couple times to make sure), you will see sometimes each bar is overlapped on the other bar. Which means that the functions being called are not thread safe. And calling multiple lines at the same time will mess the output.

jquast commented 3 years ago

Well, blessed is thread safe, I have successfully used it in a threaded terminal server project serving each client ip it’s own output (telnet 1984.ws a public server), It hasn’t any thing to do with threads, just that the file descriptor (sys.stdout) is shared. If you removed the threads and used asyncio, for example, it would have the same problem.

https://ejrh.wordpress.com/2011/09/18/output-synchronisation-in-python/

in blessed docs we say, “calls to location() may not be nested”, calling it twice on the same file descriptor is an example of “nesting”, the use of threads just makes it appear logically separate, I’m sorry this wasn’t clear, maybe we can add some documentation

These context managers have the side effect of printing hidden terminal sequences to the screen that are interpreted by the terminal, it can only handle one location sequence

hash3liZer commented 3 years ago

Well, i guess that explains the messed up terminal. I found a quick solution though by putting all the location related statements under the same class and used a class variable to determine if another location context wrapper is being used somewhere. If it is, then while True: pass. Otherwise, go on.

I think adding that to the documentation would be very helpful. I don't know if i should close this right now or not though i found my answer. On to you from here.

hash3liZer commented 3 years ago

Also, one more question, i had. It's a different one. Anyways, i am asking here. When i compile my script pyinstaller --noconsole option with blessed being used in the script, i get this error:

Failed to execute script

Without creating the terminal instance, i.e. term = Terminal(), it works all fine.

jquast commented 4 months ago

Sorry I missed the PyInstaller issue, I haven't used or contributed to PyInstaller in over 15 years, but if you can reduce your problem to a simple set of files I'm sure PyInstaller or StackOverflow can help. I was able to make an executable using blessed with PyInstaller On MacOS using 3.12 without any issue.

file cli.spec,

cli.spec
# -*- mode: python -*-

block_cipher = None

a = Analysis(['cli/__main__.py'],
             pathex=[], binaries=[], datas=[], hiddenimports=[], hookspath=[], runtime_hooks=[],
             excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False,
             cipher=block_cipher, noarchive=False)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz, a.scripts, [], exclude_binaries=True, name='cli', debug=False, bootloader_ignore_signals=False,
          strip=False, upx=True, console=True )
coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, name='cli')

file setup.py,

from setuptools import setup
setup(
    name="cli", version="1.0.0", description="", license="MIT",
    packages=["cli"], include_package_data=True,
    install_requires=["blessed"], entry_points={"console_scripts": ["cli=cli.__main__:main"]},
)

file cli/__main__.py,

import blessed

def main():
    term = blessed.Terminal()
    print(term.blue_on_white("hello, world"))

if __name__ == '__main__':
    main()

And this executed just fine, so I will close this issue

image