1

What I'm trying to do

I am trying to make myself a modular statusbar for dwm, like i3blocks. In short the statusbar will consist of fundamental units called blocks, each of which represent a command.
For example, in the statusbar shown below, each section separated by arrowheads "<" is a block. Modular statusbar

The idea is to have each of them update independently with some interval.

I could go for synchronous updation of the blocks but I don't want to because some of the scripts in my statusbar take ~5 seconds to execute which stalls the other modules during that period, and I'm hoping to avoid that. This is really noticeable when I boot up my PC and nothing shows up on my statusbar for ~5 seconds because one module just takes forever to execute.

What I've tried

I have tried using pthread.h but I thought of abandoning that approach because using a thread per module seemed like a bad idea and even i3blocks doesn't use it. I used fork() also only to find out that I couldn't modify the parent's global variables from within the child process. I researched online and found out that I could use pipes to make the child process communicate with the parent, but all my efforts were in vain as reading a pipe using read() blocks the parent's flow of control replicating the same sequential behaviour as earlier.

This is the code where I want to make asynchronous calls to the function getCommand:

void getCommands(int time) {
    int pipes[len(blocks)][2];
    for (int i = 0; i < len(blocks); i++) {
        if (pipe(pipes[i]) == -1) {
            printf("Pipe failed\n");
            exit(1);
        }

        const Block *current = blocks + i;
        if (time == 0 || (current->interval != 0 && time % current->interval == 0)) {
            if (fork() == 0) {
                getCommand(current, statusbar[i]);

                close(pipes[i][0]);
                write(pipes[i][1], statusbar[i], strlen(statusbar[i]));
                close(pipes[i][1]);
                exit(0);
            } else {
                close(pipes[i][1]);
                read(pipes[i][0], statusbar[i], CMDLENGTH);
                close(pipes[i][0]);
            }
        }
    }
}

The rest of the code is on my GitHub repository: UtkarshVerma/dwmblocks

tl;dr

I want to know how to make asynchronous calls to functions with a callback. The function should be able to modify global variables as well.

Thanks in advance.

This is my first time writing a software in C, therefore suggestions of any kind on the code are welcome.

Update

I think I have made some progress finally. Here is what I am doing. I execute the commands in a fork, and then watch out for SIGCHLD on the parent. In the SIGCHLD handler, I then read all pipes to the child forks in a non-blocking manner to check if the buffer is non-empty. This works really well except for one caveat. The parent can never tell if the executed command had blank output. I made a hacky attempt like this, but I'm curious if this can be handled more appropriately.

Utkarsh Verma
  • 190
  • 2
  • 15
  • 8
    C doesn't really have asynchronous functions. You can use multiple threads to run code concurrently. – Barmar Apr 17 '21 at 10:28
  • https://stackoverflow.com/questions/2849147/communicating-between-a-parent-and-its-children – Hans Lub Apr 17 '21 at 10:48
  • 1
    Separate functions with shared variables == threads. A simple pattern for async callback is wait on event, trigger event. To update on an interval use a timer thread to trigger the events. – stark Apr 17 '21 at 12:56
  • 1
    The question is very, very broad. To the extent that there is an element of a request for design recommendations, it is also somewhat opinion-based. Possibly the bounty will nevertheless attract an answer that is useful to you, but at this level of generality, I'm not so sure. – John Bollinger Apr 20 '21 at 13:26

3 Answers3

3

My suggestion is to base your application on different approach.

  1. First of all, limit signals usage, they are obsolete IPC method and they might run into synchronization troubles.
  2. If you want to collect data from child processes, I think popen is the thing you are searching for.
  3. As other colleagues noticed, I recommend to use select or poll to check if the child output is ready.

The sample program might look like:

#include <stdio.h>
#include <fcntl.h>
#include <sys/select.h>
#include <stdbool.h>
#include <time.h>
#include <assert.h>

#define len(arr) (sizeof(arr) / sizeof(arr[0]))

typedef struct {
    char *command;
    unsigned int interval;
} Block;

const Block blocks[] = {
    {"ls", 5},
    {"echo abc", 1}
};

int main(int argc, const char * argv[])
{
   FILE* files[len(blocks)] = { NULL };
   int fds[len(blocks)] = { -1, -1 };
   int timeout[len(blocks)] = { 0 };
   fd_set set;
   int fdmax = -1;
   time_t previous = time(NULL);
   FD_ZERO(&set);

   while (true)
   {
      struct timeval tm = { 1, 0 };
      fd_set tmp = set;
      int rc = select(fdmax+1, &tmp, NULL, NULL, &tm);
      //assert(rc >= 0); /* No error handling */

      /* Check what's the time now, the Monotonic clock would be better */
      time_t now = time(NULL);

      /* Child processes loop */
      for (int i = 0; i < len(blocks); ++i)
      {
         /* No matter if it's running or not, update counter */
         timeout[i] += now - previous;
         if (timeout[i]>=blocks[i].interval && fds[i]<0)
         {
            /* Timeout occured and the child is not running */
            files[i] = popen(blocks[i].command, "r");
            fds[i] = fileno(files[i]);
            assert(fds[i] >= 0);
            fcntl(fds[i], F_SETFL, O_NONBLOCK); /* Make sure to not block */
            
            FD_SET(fds[i], &set); /* Add it to descriptors set */
            fdmax = fdmax > fds[i] ? fdmax : fds[i];
            timeout[i] = 0; /* Start interval measurement */
         }
         else if (fds[i]>=0 && FD_ISSET(fds[i], &tmp))
         {
            /* Read the pipe and consume your output */
            char buffer[1024] = { 0 };
            rc = fread(&buffer[0], sizeof(char), sizeof(buffer)-1, files[i]);
            if (rc > 0)
               printf("%s", &buffer[0]); /* Do graphics stuff */
            else if (rc == 0)
            {
               /* Probably child has ended its job */
               /* but read more about it */
               rc = pclose(files[i]); /* Read exit code */
               fdmax = fdmax == fds[i] ? (fdmax-1) : fdmax;
               fds[i] = -1; /* Now wait for next round */
               FD_CLR(fds[i], &set);
               files[i] = NULL;
            }
         }
      }

      previous = now;
   }

   return 0;
}

UPDATE: Moreover, if you really need signals to handle mouse (I found it in your code), you may use signalfd function, which creates fd for signals handling. This will limit all your logic stuff in the for loop.

Nabuchodonozor
  • 704
  • 1
  • 6
  • 13
2

Create a data structure for each module, with a pipe to a forked child that produces the data in an easily parsed data structure; for example, a letter followed by data. Have the child write the updates to the pipe, and the parent read from the read end.

Set the parent descriptors nonblocking, using fcntl(read_fd, F_SETFL, O_NONBLOCK). That way you can use a single thread and either select() or poll() for the descriptors for updates (readability), and read()s will not block.

I would personally use a thread handling the child processes, reading their output pipes in above nonblocking fashion; with the data structures protected by a mutex (kept only for a minimal duration), and an update flag per module set by the reading thread (whenever data has been updated), and cleared by the GUI thread (whenever the data has been updated to the GUI).

Glärbo
  • 135
  • 2
2

I used fork() also only to find out that I couldn't modify the parent's global variables from within the child process.

Fair enough, but you can use shared memory for that:

/* shm.c, compile with gcc -o shm shm.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>

#define BUFSIZE 256

int main() {
  int protection       = PROT_READ | PROT_WRITE;
  int visibility       = MAP_SHARED | MAP_ANONYMOUS;
  /* use shared memory as a "hole in the wall" between parent and child: */
  void *service_hatch  = mmap(NULL, BUFSIZE, protection, visibility, -1, 0);


  if (fork()) { /* parent */
    sleep(1); /* have an afternoon nap while child is cooking */
    printf("we got served: %s\n", (char *) service_hatch); 
  } else {      /* child */
    /*cook up a simple meal for parent: */ 
    strncpy(service_hatch, "French fries with mayonaise", BUFSIZE);
  }
}

When running this program, you'll see that you were able to modify the content of the parent's global *service_hatch from within the child process:

$ ./shm
we got served: French fries with mayonaise

Edit: To avoid a race between parent and child when writing/reading the shared memory, use a semaphore to arbitrate access to it:

/* shm_sem.c, compile with gcc -o shm_sem shm_sem.c -lpthread */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <semaphore.h>

#define BUFSIZE 256

/* example of a very non-atomic (i.e. piecemeal) modification of a variable */
void slowly_cook(char *meal, char *destination) {
  do {
    *destination++ = *meal++;
    sleep(1);
  } while(*meal);
}       

int main() {
  int protection       = PROT_READ | PROT_WRITE;
  int visibility       = MAP_SHARED | MAP_ANONYMOUS;

  /* use shared memory as a "hole in the wall" between parent and child: */
  void *service_hatch  = mmap(NULL, BUFSIZE, protection, visibility, -1, 0);
  
  /* use a semaphore, like  a red/green light on the outside of the hatch  */
  sem_t *service_hatch_indicator  =  mmap(NULL, sizeof(sem_t), protection, visibility, -1, 0);
  sem_init(service_hatch_indicator, 1, 1);

  /* use it by locking and unlocking the semaphore around each critical section */
  #define CRITICAL_SECTION(sem, code)  {sem_wait(sem); code; sem_post(sem);}
  

  if (fork()) { /* parent */
    sleep(1); /* have an afternoon nap while child is cooking, then wait for the light  to turn green .... */
    CRITICAL_SECTION(service_hatch_indicator, printf("we finally got served: %s\n", (char *) service_hatch)); 
  } else {      /* child */
    /* cook up a simple meal for parent, take your time ...  */ 
    CRITICAL_SECTION(service_hatch_indicator, slowly_cook("French fries with mayonaise", service_hatch));
  }
}
Hans Lub
  • 5,513
  • 1
  • 23
  • 43
  • I have a newbie question. Does following this approach have any setbacks in terms of performance assuming I'm sharing a static block of memory with the fork. Also, how do I avoid a race condition in case two forks try to modify the same shared memory? – Utkarsh Verma Apr 19 '21 at 12:36
  • I don't see how using a static block of memory would slow down your program, especially compared to using e.g. pipes. To avoid races, see my extended example that uses a semaphore. – Hans Lub Apr 19 '21 at 16:45