Repository navigation
Description
Background
#1790 makes Cmd.set_window_title() safe to call from any thread. While a prompt-toolkit application
is drawing on the terminal, the title write is scheduled on that application's event loop, so it
can't interleave with rendering. When no application is running, the title is written and flushed
directly.
To find the running application, Cmd._get_prompt_loop() checks only two places:
self.active_session.app: the main prompt andread_input()/read_secret()prompts.self._select_app_session.app: theselect()menu.select()sets this field and clears it
again when it finishes.
Any other prompt-toolkit application drawing on the same terminal is invisible to it. A title set from
another thread while such an app is running is written directly from that thread, which can
interleave with the app's rendering. That's the problem #1790 fixes for the two known cases.
This came up in several review rounds on #1790. It was left as a follow-up because fixing it properly
is a design change, not a small patch.
Cases that aren't covered
- The
ipycommand: IPython's own prompt-toolkit application. - A command that calls
prompt_toolkit.prompt(),choice(), or a dialog directly instead of
read_input()orselect(). - A second
Cmdinstance sharingsys.stdout, whosemain_session.outputis the same ambient
Vt100_Output. - Any future application kept alive during commands, such as a command toolbar or pager.
Each newly covered app currently needs another field like _select_app_session plus a cleanup that
writes any title left pending, as select() and _read_raw_input() have.
Possible approaches
- Look up the app through its
AppSession, likeStdoutProxydoes.
prompt_toolkit.patch_stdout.StdoutProxy._get_app_loop()keeps theAppSessionthat was current
when it was created and checksapp_session.app.loop.set_app()setsAppSession.appas a plain
attribute, so other threads can read it. cmd2 could record theAppSessioneach prompt runs in and
check that session'sapp, which covers anything run inside it. Apps that create their own
AppSession(asselect()does) would still need to be registered. - Register every app run in one place. Add a context manager that records the running
AppSessionorApplicationfor thisCmd, used everywhere cmd2 runs an app on its terminal.
_get_prompt_loop()and the cleanup flush would both use it, replacing the special cases. - A combination: option 1 for the ambient session, plus option 2 for sessions cmd2 creates.
Apps cmd2 doesn't start, such as IPython's prompt or a user's direct prompt_toolkit call, can only be
covered by option 1, and only when they run in an AppSession cmd2 can see.
Races on the direct-write path
When _get_prompt_loop() finds no running app, set_window_title() writes and flushes the title
directly on the calling thread. _window_title_lock only serializes title writes with each other.
prompt-toolkit's renderer and patch_stdout()'s writer thread don't take that lock, and
Vt100_Output.flush() joins and then resets its buffer with no lock of its own. So a direct write from
a worker thread can drop or duplicate the other writer's output, or land in the middle of a frame,
in these windows:
- A prompt starts between the check and the write. A worker finds no loop and releases the lock.
The main thread then enterssession.prompt(),app.loopis set, and the renderer starts writing
its first frame. The worker'sset_title()andflush()then run against the same output while
the renderer is using it. patch_stdout()is still active after a prompt clears its loop. When the user presses Enter,
app.loopis cleared andactive_sessiongoes back tomain_session, but_read_raw_input()
hasn't leftpatch_stdout()yet. That context's writer thread sees no app loop and writes queued
stdout text directly to the output. A worker's direct title write at the same moment races it. When
stdout issys.stdout, both use the default app session'sVt100_Output. Flush window title updates without waiting for a prompt #1790 only moved the
main thread's cleanup write outsidepatch_stdout(), not writes from other threads.
The old set_title()-based implementation had these races too, and #1790 narrows the windows. Closing
them properly also depends on knowing which app or writer owns the output at every moment, so it
belongs with the approaches above. For example, a registry entry could be set before the prompt starts
and cleared only after patch_stdout() exits, with direct writes waiting for or deferring to it.
Related
- set_window_title() doesn't flush, so a title set during a command appears late #1782: the original bug, where titles stayed buffered until the next prompt render.
- Flush window title updates without waiting for a prompt #1790: the fix this builds on (
_get_prompt_loop(),_flush_pending_window_title()in
cmd2/cmd2.py).
Activity