Monitor and alert user when stty settings change

awkstracesttyterminal

A particularly large (~10^6 LOC) program causes my stty settings to change from echo ixon icanon to -echo -ixon -icanon and I would like to find the function in this massive program that causes this change.

I obviously would not like to trace execution through this mess of OOP spaghetti code.

How can I monitor stty settings and log what changes them? I'm thinking strace with awk can probably get me the info I need, but I don't know what system calls to filter for.

Best Answer

If you think that you can cause the event by a specific action or interaction, by far the simplest method is something like:

 watch -d -n1 "stty -F /dev/pts/106 -a | grep -Eo '.(icanon|ixon)'"

Run this on a new terminal, the option to -F is the terminal you will run the program on (run tty to see what it is before starting it). Omit | grep .. if you want to observe the complete terminal state.

Next option, if you are using Linux, is to use ltrace to trace library calls, this similar to strace (and it incorporates some strace capability) but it works with user-space libraries, not just the kernel system calls:

ltrace -tt -e tcgetattr+tcsetattr myprogram ...

This will show and timestamp calls to tcgetattr() and tcsetattr(), the libc functions to get and set terminal attributes.

Ultimately, those libc calls will use the ioctl() system call, this you can trace with strace or truss, here's how to use strace on linux:

strace -tt -e trace=ioctl myprogram [...]

A big advantage here is that strace will happily decode various parameters to syscalls for you.

None of the above will tell you very much about logically where in the program the problem might occur though, to do that you have two options: a debugger, or DLL injection.

Within gdb you can easily set a breakpoint on tcsetattr(), then check the call stack, but this may be tedious if there are many calls (and will require a debug build or symbols for libc to get the best results).

The most comprehensive option (assuming that the target program is dynamically linked) is to inject your own DLL which intercepts or wraps the functions you need to track, in this case tcsetattr().

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
#include <termios.h>

/* 
 * gcc -nostartfiles -shared -ldl -Wl,-soname,tcsetattr -o tc.so wraptc.c
 * LD_PRELOAD=./tc.so stty -icanon ixon
 *
 */

#define DEBUG 1
#define dfprintf(fmt, ...) \
    do { if (DEBUG) fprintf(stderr, "[%14s#%04d:%8s()] " fmt, \
          __FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)


typedef int tcsetattr_fp(int fd, int optional_actions, 
                     const struct termios *termios_p);
static tcsetattr_fp    *real_tcsetattr;

void _init()
{
    dfprintf("It's alive!\n","");
    real_tcsetattr = dlsym(RTLD_NEXT, "tcsetattr");
    dfprintf("Hooked %p tcsetattr()\n",(void *)real_tcsetattr);
}

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p)
{
     void *bt[20];
     size_t btsz;

     int rc,stacktr=0;
     dfprintf("Caught tcsetattr(%i,%04x,...)\n",fd,optional_actions);

     if ( (fd==0) && !((termios_p->c_lflag) & ICANON)) {
         dfprintf("ICANON off!\n","");
         stacktr=1;
     }
     if ( (fd==0) && !((termios_p->c_iflag) & IXON)) {
         dfprintf("IXON off!\n","");
         stacktr=1;
     }
     if (stacktr) {
         btsz=backtrace(bt,sizeof(bt));
         backtrace_symbols_fd(bt,btsz,STDERR_FILENO);
     }

     rc=real_tcsetattr(fd,optional_actions, termios_p);
     return rc;
}

Compile and invoke as indicated in the comments. This code locates the real libc tcsetattr() function, and contains an alternative version which is used instead. This code calls backtrace() when it sees possibly interesting activity on FD 0, then calls the real libc version. It may require minor adjustment.

Related Question