Friday 13 February 2015

Finding xterm Terminal Window Size for serial console resize

I use a networked serial console displaying in an xterm (TERM=xterm), and to my frustration it can't cope when I resize the xterm, instead either garbling the output or just using the original portion.

I don't know why this should be.
# shopt | grep checkwinsize
checkwinsize on


Various combinations of # kill -SIGWINCH $$ and # kill -s WINCH $$ had no effect at all, and # stty size proved to be 0 0, and thus not very useful.

Edit: It turns out that bash does not directly query the terminal size directly as a result of SIGWINCH or anything else. Changing the size of the tty with stty cols $COLUMNS rows $LINES is what causes the tty driver to send SIGWINCH to the foreground application which then queries the tty driver. Resizing an xterm is what causes xterm to modify the tty, thus sending the SIGWINCH and caused bash to take notice.
With a little help from the source to xterm's resize command and also this python interpretation I came up with this 1-liner for bash, to read the xterm width and height into LINES and COLUMNS and set these in the tty driver ready for vi or other programs to pick up:

IFS=$';\x1B[' read -p $'\x1B7\x1B[r\x1B[999;999H\x1B[6n\x1B8' -d R -rst 1 _ _ LINES COLUMNS _ </dev/tty \
&& stty cols "$COLUMNS" rows "$LINES"

It's worth looking at how this works.

$'...' is a bash quoted string that allows character entities to be expressed in hexadecimal, thus $'\x1B' is the character named ESC.

The string emitted as a prompt instructs the xterm to save the current cursor position, move the cursor to 999,999 which should be beyond the terminal bounds and so instead move to the bottom right corner; it then instructs the xterm to report the current cursor position, and then restore the previous cursor position.

The current cursor position is returned as a string (without newline) in this form: \x1B[lines;columnsR and so this can be read into two variables LINES and COLUMNS that bash uses. But how to do that?

The bash read function will emit the prompt, and then read the response. As the response is terminated in R instead of a newline, -d R is passed to read.

Other useless characters are \x1B [and ; so we put these into IFS causing read to use these to split the input. This gives us 2 empty strings, which we read into the bash underscore variable which gets overwritten every line anyway, so our read variable specification is _ _ LINES COLUMNS _ causing LINES and COLUMNS to take the 3rd and 4th values, with a final _ to take any potential junk that otherwise would have been appended to COLUMNS. Raw mode and silent mode are advised obviously, hence -r -s and a timeout of 1 second is set in case an xterm isn't in use and there will be no response.

So why not define this as a function...

resize() {
  IFS=$';\x1B[' read -p $'\x1B7\x1B[r\x1B[999;999H\x1B[6n\x1B8' \
                     -d R -rst 1 _ _ LINES COLUMNS _ 2>&1 &&
  stty cols "$COLUMNS" rows "$LINES"
} </dev/tty >/dev/tty 2>/dev/null

The stdio redirections are to ensure that stderr is thrown away (in case set -x debug is active, which could mess this up), to ensure that the tty is being accessed whatever stdin and stdout are, and to ensure that the read command has access to stderr (for the prompt) to the tty.

Depending when/how you invoke it you may wish to not loose the previous value of $?, saved here without the use of local variables (which through dynamic scoping might affect the called function).

just() {
  set -- $? "$@"
  "${@:2}" 
  return $1
}

so now you can invoke: just resize without affecting $?, perhaps from something ghastly like:

export PS1="$PS1"'$(just resize)'

or as part of PROMPT_COMMAND although ideally it would run before executing a command.

It now remains to learn why (although a serial terminal cannot be expected to send a SIGWINCH) bash was not responding to SIGWINCH. (Note: Explained here, the signal was not to signify to basg that it should query the terminal using the methods described here, but to tell bash that the STTY rows and columns had already been changed and that it should read those. As we set those using STTY in the resize function, the signal is then sent to bash).

update: a colleague points out the a malicious terminal (or spoofed user input) could emit a bad cursor position response, so $COLUMNS and $LINES must be quoted where used to avoid some stty argument injection, or perhaps worse to other programs which might make careless use of COLUMNS or LINES without validating them as sane integers