3
\$\begingroup\$

I wrote a Pomodoro timer daemon for BSD systems in C (may work on Linux with -lbsd).

The server uses poll(2) and sockets to communicate with the client and to implement the timer.
The client starts or stops the timer and gets information about it.

For example, in a terminal run $ pomod (the server). In another, run $ pomo start (the client). The server will print pomodoro for a pomodoro has begun. After 25 minutes, the pomodoro will end and it will print short break to indicate that you have to take a short break. Running $ pomo info gets information about the timer (which cycle you're in and the current time).

Compile pomod with cc -o pomod pomod.c util.c.
Compile pomo with cc -o pomo pomo.c util.c.

Here's pomod.c:

#include <sys/socket.h>
#include <sys/time.h>
#include <sys/un.h>

#include <err.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "util.h"

#define BACKLOG     5
#define SECONDS     60          /* seconds in a minute */
#define MINUTES     60          /* minutes in a hour */
#define MILISECONDS 1000        /* miliseconds in a second */
#define NANOPERMILI 1000000
#define MAXCLIENTS  10

enum Duration {
    POMODORO_SECS   = SECONDS * 25,
    SHORTBREAK_SECS = SECONDS * 5,
    LONGBREAK_SECS  = SECONDS * 30
};

enum Cycle {
    STOPPED,
    POMODORO,
    SHORTBREAK,
    LONGBREAK
};

static char *sockpath;
static struct timespec pomodoro = {.tv_sec = POMODORO_SECS};
static struct timespec shortbreak = {.tv_sec = SHORTBREAK_SECS};
static struct timespec longbreak = {.tv_sec = LONGBREAK_SECS};
static char *cyclenames[] = {
    [STOPPED]    = "stopped",
    [POMODORO]   = "pomodoro",
    [SHORTBREAK] = "shortbreak",
    [LONGBREAK]  = "longbreak"
};

static void
usage(void)
{
    (void)fprintf(stderr, "usage: %s [-S socket] [-l time] [-p time] [-s time]\n", getprogname());
    exit(1);
}

static int
gettime(char *s)
{
    char *ep;
    long l;

    l = strtol(s, &ep, 10);
    if (*s == '\0' || *ep != '\0' || l <= 0 || l >= INT_MAX / SECONDS)
        goto error;
    return SECONDS * (int)l;
error:
    errx(1, "%s: invalid time", s);
}

/* parse arguments and set global variables */
static void
parseargs(int argc, char *argv[])
{
    int ch;

    while ((ch = getopt(argc, argv, "l:p:S:s:")) != -1) {
        switch (ch) {
        case 'S':
            sockpath = optarg;
            break;
        case 'l':
            longbreak.tv_sec = gettime(optarg);
            break;
        case 'p':
            pomodoro.tv_sec = gettime(optarg);
            break;
        case 's':
            shortbreak.tv_sec = gettime(optarg);
            break;
        default:
            usage();
            break;
        }
    }
    argc -= optind;
    argv += optind;
    if (argc > 0) {
        usage();
    }
    if (sockpath == NULL) {
        sockpath = getsockpath();
    }
}

static int
createsocket(const char *path, int backlog)
{
    struct sockaddr_un saddr;
    int sd;

    memset(&saddr, 0, sizeof saddr);
    saddr.sun_family = AF_UNIX;
    strncpy(saddr.sun_path, path, (sizeof saddr.sun_path) - 1);
    unlink(path);
    if ((sd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
        goto error;
    if (bind(sd, (struct sockaddr *)&saddr, sizeof saddr) == -1)
        goto error;
    if (listen(sd, backlog) == -1)
        goto error;
    return sd;
error:
    err(1, "%s", path);
}

/* return 1 if we handle client */
static int
acceptclient(struct pollfd *pfds, size_t n)
{
    struct sockaddr_un caddr;
    socklen_t len;
    size_t i;
    int cd;                         /* client file descriptor */

    len = sizeof caddr;
    if ((cd = accept(pfds[0].fd, (struct sockaddr *)&caddr, &len)) == -1)
        err(1, "accept");
    for (i = 1; i <= n; i++) {
        if (pfds[i].fd <= 0) {
            pfds[i].fd = cd;
            pfds[i].events = POLLIN;
            return 1;
        }
    }
    close(cd);      /* ignore if we have MAXCLIENTS or more */
    return 0;
}

/* return 1 if we do not handle client anymore */
static int 
handleclient(int fd)
{
    char cmd;
    int n;

    if ((n = read(fd, &cmd, 1)) != 1)
        return BYE;
    return cmd;
}

static void
gettimespec(struct timespec *ts)
{
    if (clock_gettime(CLOCK_REALTIME, ts) == -1) {
        err(1, "time");
    }
}

static void
notify(int cycle)
{
    printf("%s\n", cyclenames[cycle]);
    fflush(stdout);
}

static void
timesub(struct timespec *a, struct timespec *b, struct timespec *c)
{
    timespecsub(a, b, c);
    if (c->tv_sec < 0) {
        c->tv_sec = 0;
    }
    if (c->tv_nsec < 0) {
        c->tv_nsec = 0;
    }
}

static void
info(int fd, struct timespec *stoptime, int cycle)
{
    struct timespec now, diff;
    time_t mins, secs;
    char buf[INFOSIZ];

    gettimespec(&now);
    timesub(stoptime, &now, &diff);
    switch (cycle) {
    case POMODORO:
        mins = (pomodoro.tv_sec - diff.tv_sec) / SECONDS;
        secs = (pomodoro.tv_sec - diff.tv_sec) - mins;
        break;
    case SHORTBREAK:
        mins = (shortbreak.tv_sec - diff.tv_sec) / SECONDS;
        secs = (shortbreak.tv_sec - diff.tv_sec) - mins;
        break;
    case LONGBREAK:
        mins = (longbreak.tv_sec - diff.tv_sec) / SECONDS;
        secs = (longbreak.tv_sec - diff.tv_sec) - mins;
        break;
    }
    if (cycle == STOPPED)
        snprintf(buf, INFOSIZ, "%s", cyclenames[cycle]);
    else
        snprintf(buf, INFOSIZ, "%s: %02lld:%02lld", cyclenames[cycle], (long long int)mins, (long long int)secs);
    write(fd, buf, INFOSIZ);
}

static int
gettimeout(struct timespec *stoptime)
{
    struct timespec now, diff;

    gettimespec(&now);
    timesub(stoptime, &now, &diff);
    return MILISECONDS * diff.tv_sec + diff.tv_nsec / NANOPERMILI;
}

static void
run(int sd)
{
    struct pollfd pfds[MAXCLIENTS + 1];
    struct timespec now, stoptime;
    size_t i;
    int timeout;
    int nclients;
    int cycle;
    int pomocount;
    int n;

    nclients = 0;
    timeout = -1;
    pfds[0].fd = sd;
    pfds[0].events = POLLIN;
    for (i = 1; i <= MAXCLIENTS; i++)
        pfds[i].fd = -1;
    cycle = STOPPED;
    pomocount = 0;
    for (;;) {
        if ((n = poll(pfds, nclients + 1, timeout)) == -1)
            err(1, "poll");
        if (n > 0) {
            if (pfds[0].revents & POLLHUP) {        /* socket has been disconnected */
                return;
            }
            if (pfds[0].revents & POLLIN) {         /* handle new client */
                if (acceptclient(pfds, MAXCLIENTS)) {
                    nclients++;
                }
            }
            for (i = 1; i <= MAXCLIENTS; i++) {     /* handle existing client */
                if (pfds[i].fd <= 0 || !(pfds[i].events & POLLIN))
                    continue;
                switch (handleclient(pfds[i].fd)) {
                case BYE:
                    pfds[i].fd = -1;
                    nclients--;
                    break;
                case START:
                    pomocount = 0;
                    gettimespec(&stoptime);
                    stoptime.tv_sec += pomodoro.tv_sec;
                    notify(cycle = POMODORO);
                    break;
                case STOP:
                    cycle = STOPPED;
                    break;
                case INFO:
                    info(pfds[i].fd, &stoptime, cycle);
                    break;
                }
            }
        }
        gettimespec(&now);
        switch (cycle) {
        case STOPPED:
            timeout = -1;
            break;
        case POMODORO:
            if (timespeccmp(&now, &stoptime, >=)) {
                pomocount++;
                if (pomocount < 4) {
                    notify(cycle = SHORTBREAK);
                    stoptime.tv_sec += shortbreak.tv_sec;
                    timeout = gettimeout(&stoptime);
                } else {
                    pomocount = 0;
                    notify(cycle = LONGBREAK);
                    stoptime.tv_sec += longbreak.tv_sec;
                    timeout = gettimeout(&stoptime);
                }
            } else {
                timeout = gettimeout(&stoptime);
            }
            break;
        case SHORTBREAK:
            if (timespeccmp(&now, &stoptime, >)) {
                notify(cycle = POMODORO);
                stoptime.tv_sec += pomodoro.tv_sec;
                timeout = gettimeout(&stoptime);
            } else {
                timeout = gettimeout(&stoptime);
            }
            break;
        case LONGBREAK:
            pomocount = 0;
            if (timespeccmp(&now, &stoptime, >)) {
                notify(cycle = POMODORO);
                stoptime.tv_sec += pomodoro.tv_sec;
                timeout = gettimeout(&stoptime);
            } else {
                timeout = gettimeout(&stoptime);
            }
            break;
        }
    }
}

int
main(int argc, char *argv[])
{
    int sd;                         /* socket file descriptor */

    setprogname(argv[0]);
    parseargs(argc, argv);
    sd = createsocket(sockpath, BACKLOG);
    run(sd);
    close(sd);
    unlink(sockpath);
    return 0;
}

Here's pomo.c:

#include <sys/socket.h>
#include <sys/un.h>

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "util.h"

static char *sockpath = NULL;

static void
usage(void)
{
    (void)fprintf(stderr, "usage: %s [-S socket] [start|stop|info]\n", getprogname());
    exit(1);
}

static int
parseargs(int argc, char *argv[])
{
    int ch;

    while ((ch = getopt(argc, argv, "S:")) != -1) {
        switch (ch) {
        case 'S':
            sockpath = optarg;
            break;
        default:
            usage();
            break;
        }
    }
    argc -= optind;
    argv += optind;
    if (argc != 1)
        usage();
    if (sockpath == NULL)
        sockpath = getsockpath();
    if (strcasecmp(*argv, "stop") == 0)
        return STOP;
    else if (strcasecmp(*argv, "start") == 0)
        return START;
    else if (strcasecmp(*argv, "info") == 0)
        return INFO;
    usage();
    return 0;                       /* NOTREACHED */
}

static int
connectsocket(const char *path)
{
    struct sockaddr_un saddr;
    int fd;

    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
        err(1, "socket");
    memset(&saddr, 0, sizeof saddr);
    saddr.sun_family = AF_UNIX;
    strncpy(saddr.sun_path, path, (sizeof saddr.sun_path) - 1);
    if (connect(fd, (struct sockaddr *)&saddr, sizeof saddr) == -1)
        err(1, "connect");
    return fd;
}

static void
sendcommand(char cmd, int fd)
{
    if (write(fd, &cmd, 1) == -1) {
        err(1, "write");
    }
}

static void
printinfo(int fd)
{
    int n;
    char buf[INFOSIZ];

    if ((n = read(fd, buf, INFOSIZ)) != INFOSIZ)
        errx(1, "could not get info");
    buf[INFOSIZ - 1] = '\0';
    printf("%s\n", buf);
}

int
main(int argc, char *argv[])
{
    int fd;                         /* socket file descriptor */
    char cmd;

    setprogname(argv[0]);
    cmd = parseargs(argc, argv);
    fd = connectsocket(sockpath);
    sendcommand(cmd, fd);
    if (cmd == INFO)
        printinfo(fd);
    close(fd);
    return 0;
}

Here's util.c (code shared by both programs):

#include <stdio.h>
#include <unistd.h>

#define PATHSIZ     104
#define PATHPREFIX  "/tmp/pomodoro."

char *
getsockpath(void)
{
    static char buf[PATHSIZ];

    snprintf(buf, PATHSIZ, PATHPREFIX "%ld", (long)geteuid());
    return buf;
}

And here's util.h (the common header).

#define INFOSIZ 128

enum Command {
    BYE   = 0,
    STOP  = 'p',
    START = 's',
    INFO  = 'i',
};

char *getsockpath(void);

It's also on github.

\$\endgroup\$
2
  • \$\begingroup\$ I'm not sure I understand the motivation for using a client-server design for what could be a plain old single-process app. Am I missing something obvious? \$\endgroup\$
    – ggorlen
    Jun 3 '21 at 15:58
  • \$\begingroup\$ @ggorlen I just want to play with sockets and write a simple daemon. But I agree, a client-server design is overwhelming for such small program. \$\endgroup\$
    – phillbush
    Jun 5 '21 at 23:48
1
\$\begingroup\$

Nicely written

Alternative zeroing

// struct sockaddr_un saddr;
// memset(&saddr, 0, sizeof saddr);

struct sockaddr_un saddr = {0};

Off-by-1 limit?

Perhaps

// if (*s == '\0' || *ep != '\0' || l <= 0 || l >= INT_MAX / SECONDS)
   if (*s == '\0' || *ep != '\0' || l <= 0 || l > INT_MAX / SECONDS)

Safer subtraction

After timespecsub(a, b, c) and when c->tv_sec < 0 is true, I am not certain c->tv_nsec will not be positive. Recommend when zeroing the .tv_sec member, also do so to the .tv_nsec.

if (c->tv_sec < 0) {
    c->tv_sec = 0;
    c->tv_nsec = 0; // add
}
if (c->tv_nsec < 0) {
    c->tv_nsec = 0;
}

Better names

Rather than SECONDS to represent "seconds per minute", consider something like SEC_PER_MIN.

Unknown integer widths

Minor: When printing a system integer type (no matching print specifier), I tend to go for the widest specifier.

// snprintf(buf, PATHSIZ, PATHPREFIX "%ld", (long)geteuid());
snprintf(buf, PATHSIZ, PATHPREFIX "%jd", (intmax_t) geteuid());

Missing code guard util.h

Static buffer care

Consider const to protect caller messing with the buffer.

// char *getsockpath(void)
const char *getsockpath(void)

Define & Initialize

Consider initialization at definition.

// int timeout;
// int nclients;
// nclients = 0;
// timeout = -1;

int timeout = -1;
int nclients = 0;

Other places in code too.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.