3

I think I may have a threading problem in c, but I'm not sure.

My goal is to execute two separate functions inside a while(1) loop as such: One of these functions is kbget() to retrieve the key pressed in a terminal in non-canonical mode. The second one is to constantly get the terminal window size with the ioctl(1, TIOCGWINSZ,...) function.

It normally doesn't work because the while(1) loop stops to get a keypress from the user before executing the second function to reevaluate the terminal window size. If the terminal window is resized before a key is pressed, the function to evaluate its size isn't executed unless a random key is pressed again.

In other words, resizing the terminal window doesn't update the size values in the Window struct below unless a key is pressed. I want the program to update the y_size & x_size values 'live' as the terminal is resized.

Here's the issue in code without POSIX threads: Executing with :

gcc -Wall scr.h main.c -o main && ./main 

(scr.h below has kbget() to change terminal mode):

main.c:

#include "scr.h"
#include <sys/ioctl.h>

#define gotoyx(y, x)       printf("\033[%d;%dH", (y), (x))  // equivalent to move(y, x) in ncurses
#define del_from_cursor(x) printf("\033[%dX", (x))          // delete x characters from cursor position

typedef struct {
    int y_size;
    int x_size;
} Window;

int main(void) 
{
    printf("\033[?1049h\033[2J\033[H"); // remember position & clear screen 

    gotoyx(1, 10);
    printf("Press <ESC> to stop program.");
    gotoyx(2, 10);
    printf("Resizing the terminal window does not 'automatically' update the size shown on screen");

    Window w;
    struct winsize w_s;

    while (1) {

        // evaluate terminal size
        if (!ioctl(1, TIOCGWINSZ, &w_s)) {
            w.y_size = w_s.ws_row;
            w.x_size = w_s.ws_col;
        }

        // print terminal size and center it
        gotoyx(w.y_size / 2, w.x_size / 2);
        del_from_cursor(5);
        printf("w.y_size: %d", w.y_size);
        gotoyx((w.y_size / 2) + 1, w.x_size / 2);
        del_from_cursor(5);
        printf("w.x_size: %d", w.x_size);

        // get key pressed by user in terminal & exit if <ESC> is pressed
        if (kbget() == 0x001b) { break; }       
    }
    printf("\033[2J\033[H\033[?1049l"); // clear screen & restore
    return 0;
}

I have tried solving this using threads but I was unsuccessful so far.

I have modified the main.c file above by adding 2 functions (get_window_size & get_key): (scr.h has the kbget() function in get_key() to change the terminal to canonical mode)

main.c:

#include "scr.h"
#include <sys/ioctl.h>
#include <pthread.h>

#define gotoyx(y, x)       printf("\033[%d;%dH", (y), (x))
#define del_from_cursor(x) printf("\033[%dX", (x))

typedef struct {
    int y_size;
    int x_size;
} Window;

void *get_window_size(void *arg)
{
    Window *w = (Window *)arg;
    struct winsize w_s;
    if (!ioctl(1, TIOCGWINSZ, &w_s)) {
        w->y_size = w_s.ws_row;
        w->x_size = w_s.ws_col;
    }
    pthread_exit(0);
}

void *get_key(void *arg)
{
    int *key = (int *)arg;
    free(arg);
    *key = kbget();
    int *entered_key = malloc(sizeof(*key));

    *entered_key = *key;
    pthread_exit(entered_key);
}

int main(void)
{
    printf("\033[?1049h\033[2J\033[H");

    Window w;

    pthread_t tid[3];
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_create(&tid[0], &attr, get_window_size, &w);

    int *c = malloc(sizeof(*c));
    int *key_pressed;

    while (1) {
        // for initial size
        pthread_join(tid[0], NULL);

        // printing size to screen
        gotoyx(w.y_size / 2, w.x_size / 2);
        del_from_cursor(5);
        printf("w.y_size: %d", w.y_size);
        gotoyx((w.y_size / 2) + 1, w.x_size / 2);
        del_from_cursor(5);
        printf("w.x_size: %d", w.x_size);

        // get window size
        pthread_attr_t attr1;
        pthread_attr_init(&attr1);
        pthread_create(&tid[1], &attr1, get_window_size, &w);

        // get key entered by user
        pthread_attr_t attr2;
        pthread_attr_init(&attr2);
        pthread_create(&tid[2], &attr2, get_key, c);

        pthread_join(tid[1], NULL);
        pthread_join(tid[2], (void **)&key_pressed);

        if (*key_pressed == 0x001b) {
            break;
        } else {
            free(key_pressed);
        }
    }
    if (key_pressed != NULL) {
        free(key_pressed);
    }
    printf("\033[2J\033[H\033[?1049l");
    return 0;
}

The scr.h file changes the terminal mode to non-canonical (the kbget() function above is called from here): I don't think there's any problems in scr.h as it is taken from here (Move the cursor in a C program).

scr.h:

#ifndef SCR_H
#define SCR_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

struct termios term, oterm;

int getch(void)
{
    int c = 0;
    tcgetattr(STDIN_FILENO, &oterm);
    memcpy(&term, &oterm, sizeof(term));
    term.c_lflag &= ~(ICANON | ECHO);
    term.c_cc[VMIN] = 1;
    term.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSANOW, &term);
    c = getchar();
    tcsetattr(STDIN_FILENO, TCSANOW, &oterm);
    return c;
}

int kbhit(void)
{
    int c = 0;

    tcgetattr(STDIN_FILENO, &oterm);
    memcpy(&term, &oterm, sizeof(term));
    term.c_lflag &= ~(ICANON | ECHO);
    term.c_cc[VMIN] = 0;
    term.c_cc[VTIME] = 1;
    tcsetattr(STDIN_FILENO, TCSANOW, &term);
    c = getchar();
    tcsetattr(STDIN_FILENO, TCSANOW, &oterm);
    if (c != -1) { ungetc(c, stdin); }
    return c != -1 ? 1 : 0;
}

int kbesc(void)
{
    int c = 0;
    if (!kbhit()) { return 0x001b; } // 0x001b is the <ESC> key
    c = getch();
    if (c == 0) { while (kbhit()) { getch(); } }
    return c;
}

int kbget(void)
{
    int c = getch();
    return c == 0x001b ? kbesc() : c; // 0x001b is the <ESC> key
}
#endif // SCR_H

I also get errors Invalid write of size 4 in the code above with pthread while executing with valgrind:

gcc -Wall scr.h main.c -pthread -o main
valgrind -v --leak-check=yes ./main

I am aware of the existence of ncurses and pdcurses. I am only doing this as an exercise for myself.

UPDATE

I have changed my code to the following, unfortunately the ret variable never changes to -1:

#include "scr.h"
#include <errno.h>
#include <sys/ioctl.h>

#define gotoyx(y, x)       printf("\033[%d;%dH", (y), (x))
#define del_from_cursor(x) printf("\033[%dX", (x))

typedef struct {
    int y_size;
    int x_size;
} Window;

static int sigwinch_arrived = 0;

void sigwinch_handler(int signum) 
{ sigwinch_arrived = 1; }

void on_window_size_change(Window *w) 
{
    struct winsize w_s;
    // evaluate terminal size
    if (!ioctl(1, TIOCGWINSZ, &w_s)) {
        w->y_size = w_s.ws_row;
        w->x_size = w_s.ws_col;
    }

    // print terminal size in its center
    gotoyx(w->y_size / 2, w->x_size / 2);
    del_from_cursor(15);
    printf("w.y_size: %d", w->y_size);
    gotoyx((w->y_size / 2) + 1, w->x_size / 2);
    del_from_cursor(15);
    printf("w.x_size: %d", w->x_size);
}

int main(void) 
{
    printf("\033[?1049h\033[2J\033[H"); 

    gotoyx(1, 10);
    printf("Press <ESC> to stop program.");
    gotoyx(2, 10);
    printf("Resizing the terminal window does not 'automatically' update the size shown on screen");

    Window w;

    int ret;
    while (1) {

        // get key pressed by user in terminal & exit if <ESC> is pressed
        ret = kbget();
        gotoyx(10, 10);
        del_from_cursor(8);
        printf("ret: %d", ret);
        if (ret == -1) {
            if (errno == EAGAIN) {
                if (sigwinch_arrived) {
                    sigwinch_arrived = 0;
                    on_window_size_change(&w);
                }
            }
        } else if (ret == 0x001b) { 
            break; 
        }       
    }
    printf("\033[2J\033[H\033[?1049l"); 
    return 0;
}
sulla
  • 31
  • 2
  • You could have a main thread, which reads message queue. Then you have threads feeding this queue, one sending keypresses as they occur, another sending screen sizes, third might send timer messages (though having an entire thread for timers is kinda overkill, threads are cheap and it will keep your main loop really simple). – hyde Aug 31 '19 at 09:26
  • IMHO, _termios_ is suitable as an `fgets` in non canonical mode, but if you need full control of the terminal do not reinvent the wheel and consider using [ncurses](http://www.tldp.org/HOWTO/NCURSES-Programming-HOWTO/) or [newt](https://en.wikipedia.org/wiki/Newt_(programming_library)) – David Ranieri Aug 31 '19 at 09:58
  • the majority of the code in the file: `scr.h` should be in a separate file (probably) named `scr.c` Then the file: `scr.h` should only contain the 'interface' to the functionality in `scr.c` – user3629249 Aug 31 '19 at 14:11
  • `volatile static int sigwinch_arrived = 0;` <<-- **volatile** could help here. – wildplasser Sep 01 '19 at 17:57

1 Answers1

4

Extension: as per this answer, if your ncurses was compiled with the --enable-sigwinch flag, it does the solution below automatically (if you did not override SIGWINCH before ncurses_init() yet). In this case, getch() (wgetch()) will simply return KEY_RESIZE if a resize event is happened.


If the size of your controlling character terminal changes, your process should get a SIGWINCH signal (window size change, signal 28 on Linux).

It can be sent by the kernel (if there is mode switch on a character desktop), or by the virtual terminal software (xterm, gnome-terminal, screen, etc).

If your process gets a signal, its blocking kernel calls, including getch(), stop with the -EAGAIN error number. It means, that the blocking call stopped before time due to an arrived signal.

Note, from a signal handler, you can't do too much (for example: no malloc()), and the best to do if you make the least possible. Typical signal handlers change a static, global variable, whose value is checked by the main program.

Untested example code:

static int sigwinch_arrived = 0;

// this is called from the signal handler - nothing complex is allowed here
void sigwinch_handler(int signum) {
  sigwinch_arrived = 1;
}

// callback if there is a window size change
void on_window_size_change() {
   ...
}

// main program
...
while (1) { // your main event handler loop
  int ret = getch();
  if (ret == ERR) {
    if (errno == EAGAIN) {
      if (sigwinch_arrived) {
         sigwinch_arrived = 0;
         on_window_size_change();
      }
    }
  }
  ...
}
peterh
  • 11,875
  • 18
  • 85
  • 108
  • I have changed my code, but your ret value never changes to -1? Do you have any idea why? Thanks – sulla Aug 31 '19 at 22:13
  • @sulla `getch()` returns ERR on error. I fixed it in the post. [man getch](https://linux.die.net/man/3/getch). – peterh Sep 01 '19 at 06:15