Search code examples
cforkncursescurses

Terminal resize in chained ncurses programs (fork/exec/wait)


There are some curses based programs that are launched creating a process chain among them, using fork/exec/wait.

When the xterm is resized and there is only the first program running all works fine. But, when the second (or third) program is running:

savetty{};
endwin();

// forking
if (fork() != 0) {
    // at parent
    wait(&status);
} else {
    // child
    if (execlp(cmd, cmd, (char*) NULL) < 0) {
        exit(1);
    }
}

// child exited: parent refresh its screen
resetty();  refresh();

All programs seems to try refresh their screens at same time. From this point the screen becomes a real mess.

What need to be done in each program before exec the next program, to "freezes" curses until the wait returns ?

EDIT:

When I replace fork/exec/wait to system(), all works fine. But I cannot keep it (it was just a test), it's a big legacy system strongly dependent of fork/exec/wait system calls.

I also tried to run execlp("/bin/sh", "-c", cmd) and the problem is the same.

EDIT 2: A full sample code from scratch:

// Uncomment to switch from fork/exec/wait to system() function
//#define USE_SYSTEM

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

#include <curses.h>
#include <term.h>

#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

void mybox(int y, int x, int height, int width);
void msg(char* format, ...);
void draw();

int myexec(char* cmd);
int spawn_new_instance(char* cmd);

char* argid = NULL;

int main(int argc, char* argv[]) {
   initscr();     
   cbreak();      
   keypad(stdscr, TRUE);
   noecho();  

   draw();
   if (argc > 1) {
       argid=argv[1];
       msg("That is a child process");
   }

   int pid;
   int key;
   do {
       key = getch();

       switch(key) {
           case KEY_RESIZE:
               clear(); draw();
               msg("KEY_RESIZE");
               break;
           case 'r':
           case 'R':
               pid = spawn_new_instance(argv[0]);
               if (argid) {
                   #ifdef USE_SYSTEM
                       msg("Came back from system()");
                   #else
                       msg("Came back from pid %d", pid);
                   #endif
               } else {
                   msg("Came back from pid %d - THAT IS THE ROOT PROCESS", pid);
               }
               break;

           default:
               msg("Unsupported key '%d'. Type '.' (dot) to exit", key);
       }
   } while (key != '.');

   endwin();
}


void fullbox(void) {
    mybox(0, 0, LINES, COLS);
}


void mybox(int y, int x, int height, int width) {
    int x2 = x + width - 1;
    int y2 = y + height - 1;

    for (int ct = x; ct < x2; ct++) {
        mvaddch(y, ct, ACS_HLINE);
        mvaddch(y2, ct, ACS_HLINE);
    }

    for (int ct = y; ct < y2; ct++) {
        mvaddch(ct, x, ACS_VLINE);
        mvaddch(ct, x2, ACS_VLINE);
    }

    mvaddch(y, x, ACS_ULCORNER);
    mvaddch(y, x2, ACS_URCORNER);
    mvaddch(y2, x, ACS_LLCORNER);
    mvaddch(y2, x2, ACS_LRCORNER);
    refresh();
}


void msg(char* format, ...) {
    for (int ct = 2; ct < COLS - 2; ct++) {
        mvaddch(LINES-3, ct, ACS_CKBOARD);
    }

    char buffer[512];
    va_list argptr;
    va_start(argptr, format);
    vsprintf(buffer, format, argptr);

    int msglen = strlen(buffer) + 2;
    int msgx = (COLS - msglen)/2;

    mvprintw(LINES-3, msgx, " %s ", buffer);
}


void draw() {
    mybox(0, 0, LINES, COLS);

    char sbuf[128];
    sprintf(sbuf, "PID: %d, LINES: %d, COLS: %d", getpid(), LINES, COLS);
    int msglen = strlen(sbuf);
    int msgy = (LINES - 1) / 2;
    int msgx = (COLS - msglen)/2;
    mvprintw(msgy, msgx, "%s", sbuf);

    mybox(msgy-2, msgx-2, 1 + 4, msglen + 4);
    mybox((LINES - LINES/3)/2, (COLS - COLS/3)/2, LINES/3, COLS/3);
    mybox(LINES-4, 1, 3, COLS-2);

    msg("Resize the terminal emulator, or type R to chain new process instance");

    refresh();
}


int spawn_new_instance(char* cmd) {
    savetty();
    endwin();

    int pid;
#ifdef USE_SYSTEM
    char buf[512];
    sprintf(buf, "%s child", cmd);
    system(buf); 

    // we haven't pid using system()
    pid=0;
#else
    pid = myexec(cmd);
#endif

    resetty();
    refresh();

    return pid;
}

int myexec(char* cmd) {
    sigset_t blockSigchld;
    sigset_t previousBlock;
    sigaddset(&blockSigchld, SIGCHLD);
    sigprocmask(SIG_BLOCK, &blockSigchld, &previousBlock);

    int ret = 0, status = 0;
    int retries = 4;

    int pid;
    while ((pid = fork()) == -1) {
        if (errno == EAGAIN) {
            if (--retries >= 0) {
                sleep(1);
                continue;
            } else {
                msg("Cannot open the process now.");
                return -1;
            }
        } else if (errno == ENOMEM) {
            msg("Not enough memory.");
            return -1;
        } else {
            msg("Errno = %u", errno);
            return -1;
        }
    }

    if (pid != 0) { /*  Parent  */
        ret = waitpid(pid, &status, 0);
        sigprocmask(SIG_SETMASK, &previousBlock, (sigset_t *) 0);

        if (ret == -1) {
            return -1;
        }

       return pid;
    } else { /* Child */
        sigprocmask(SIG_SETMASK, &previousBlock, (sigset_t *) 0);

        if (execlp(cmd, cmd, "child", (char*) NULL) < 0) {
            exit(1);
        }
    }
}

Instructions:

  • Type "R" to spawn a chained new instance of itself.
  • Type "." (dot) to exit and return to parent process.
  • While in the first (main) process, resize the terminal. Everything works fine.
  • THE PROBLEM: Open a lot of instances (10 or more), resize the terminal. Look that it refresh/redrawn the screen multiple times, probably one for each instance running. Try to return to the parent, seems that from this point all them are trying to read de stdin and redraw its screen at same time, the real mess happens. If you have luck, you'll be able to press CTRL+C either type pkill testprogram.
  • Try the above using system() function. The problem doesn't occur. For test purposes, at first line is a #define to easily switch from fork/exec/wait to system().

Solution

  • Blocking the SIGWINCH fixed the problem, as suggested by @KarstenKoop's comment.

    Checking the signals being listened by processes according to unix.stackexchange.com/q/85364/114939, we can see that no processes blocks the signal SIGWINCH, even the processes lauched by system().

    But, having this solution which blocks the SIGWINCH before fork/exec/wait we can confirm that it's really flagged as blocked in /proc/{pid}/status. Like as SigBlk: 0000000008010000, where the 0x8 is the SIGWINCH being blocked.

    Although it fixed the problem, I don't understand what system() does to launch processes without blocking SIGWINCH and even thus everything works fine.