This is an archive of the discontinued LLVM Phabricator instance.

[lldb/Driver] Support terminal resizing
ClosedPublic

Authored by friss on May 8 2020, 4:03 PM.

Details

Summary

The comment in the Editine.h header made it sound like editline was
just unable to handle terminal resizing. We were not ever telling
editline that the terminal had changed size, which might explain why
it wasn't working.

This patch threads a TerminalSizeChanged() callback through the
IOHandler and invokes it from the SIGWINCH handler in the driver. Our
Editline class already had a TerminalSizeChanged() method which
was invoked only when editline was configured.

This patch also changes Editline to not apply the changes right away
in TerminalSizeChanged(), but instead defer that to the next
character read. During my testing, it happened once that the signal
was received while our ConnectionFileDescriptor::Read was allocating
memory. As el_resize seems to allocate memory too, this crashed.

Diff Detail

Event Timeline

friss created this revision.May 8 2020, 4:03 PM
Herald added a project: Restricted Project. · View Herald TranscriptMay 8 2020, 4:03 PM
labath accepted this revision.May 11 2020, 3:57 AM

Yes, resizing the window (or doing any other nontrivial task) from within a signal handler is a bad idea. Making a note of the signal and then bailing out is the right approach. Though, to be fully strictly correct, the variable ought to be a volatile std::sig_atomic_t instead of a plain bool. This still won't make the whole thing async-signal-safe (I haven't inspected the whole SIGWINCH call stack, but I am sure there are still some unsafe operations there), but it's a step towards that.

If we wanted to avoid delaying the change to the next keystroke, we could reuse the same mechanism that ^C/SIGINT uses (m_input_connection.InterruptRead()). That would probably require introduction of a new EditorStatus enum, and a careful modification to the code handling the input interruption. I don't expect that to be _too_ hard, but I also don't think that's required for this change.

This revision is now accepted and ready to land.May 11 2020, 3:57 AM
friss updated this revision to Diff 263331.May 11 2020, 6:27 PM

Use sig_atomic_t as Pavel suggested

friss added a comment.May 11 2020, 6:34 PM

Yes, resizing the window (or doing any other nontrivial task) from within a signal handler is a bad idea. Making a note of the signal and then bailing out is the right approach. Though, to be fully strictly correct, the variable ought to be a volatile std::sig_atomic_t instead of a plain bool. This still won't make the whole thing async-signal-safe (I haven't inspected the whole SIGWINCH call stack, but I am sure there are still some unsafe operations there), but it's a step towards that.

I updated the patch with this change. I'll commit it tomorrow unless you can explain this:

If we wanted to avoid delaying the change to the next keystroke, we could reuse the same mechanism that ^C/SIGINT uses (m_input_connection.InterruptRead()). That would probably require introduction of a new EditorStatus enum, and a careful modification to the code handling the input interruption. I don't expect that to be _too_ hard, but I also don't think that's required for this change.

I gave this a try, and indeed the changes do not seem too involved, but there's a bunch of details I don't get. Editline::Interrupt() and Editline::Cancel() take m_output_mutex before interrupting. If I do a similar thing in a resize handler, I don't see what prevents the signal to be delivered while the lock is held by GetLines but before it is released in GetCharacter.

friss added a comment.May 11 2020, 7:45 PM

If we wanted to avoid delaying the change to the next keystroke, we could reuse the same mechanism that ^C/SIGINT uses (m_input_connection.InterruptRead()). That would probably require introduction of a new EditorStatus enum, and a careful modification to the code handling the input interruption. I don't expect that to be _too_ hard, but I also don't think that's required for this change.

I gave this a try, and indeed the changes do not seem too involved, but there's a bunch of details I don't get. Editline::Interrupt() and Editline::Cancel() take m_output_mutex before interrupting. If I do a similar thing in a resize handler, I don't see what prevents the signal to be delivered while the lock is held by GetLines but before it is released in GetCharacter.

Sorry, this really wasn't clear... let's try again with more words: The mutex in Editline::Interrupt() and Editline::Cancel() make it look like there's a need for synchronization (m_output_mutex) before calling m_input_connection.InterruptRead(). However those methods cannot be called on the same thread, or they have a chance to deadlock. I actually don't see how sigint_handler in Driver.cpp is correct in this regard. I suppose we put the terminal in raw mode and thus do not generate SIGINT on Ctrl-C. But it seems like an externally sent SIGINT could deadlock the driver pretty easily.

Yes, you're right that the current implementation of Editline::Interrupt is not correct. It's kind of my fault since I added that locking code (in my defense, the code was pretty messy to begin with). The implementation could indeed deadlock if the signal is delivered on the thread which does the editline reading. The reason why this does not happen in practice is probably because the signal is indeed not delievered on the editline thread. Posix leaves this behavior unspecified, but implementations have to actually make a choice, and I believe most do it in such a way that they first try the main thread, and only if that has the signal blocked, they try searching for other victims. The problem could also be reproduced with an well-timed manual SIGINT, if the os supports sending signals to a specific thread (linux does via non-standard tgkill(2), I don't know about the rest).

Right now, I don't really remember whether that locking was solving any specific problem or if it was a case of "well, this code is touching variables that may be accessed concurrently, so I better lock it...". If I had to guess, I would say it's the latter. The fprintf operation, and messing with m_editor_status are indeed not safe to perform without additional synchronization. But as we've established, a mutex is not a good method of synchronization with a signal handler. There are several ways to fix this. The fix will probably won't require touching a lot of code, but it will need to be done with a steady hand. It will likely involve moving things out of the signal handler (fprintf("^C") could easily be done in the main thread), making things std::atomic/std::sig_atomic or blocking signals in some critical sections. Most likely a combination of approaches would be needed.

So, anyway, I think it's fine to either keep the current form of the patch, do it similarly to how SIGINT is handled (knowing that it's not fully posix compliant, and hoping that someone will eventually make both things compliant), or try to make everything posix compliant right now.

This revision was automatically updated to reflect the committed changes.