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.

asked Jun 1, 2021 at 3:42
\$\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\$ Commented Jun 3, 2021 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\$ Commented Jun 5, 2021 at 23:48

1 Answer 1

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.

answered Jun 2, 2021 at 17:18
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.