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.
-
\$\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– ggorlen2021年06月03日 15:58:30 +00:00Commented 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\$phillbush– phillbush2021年06月05日 23:48:13 +00:00Commented Jun 5, 2021 at 23:48
1 Answer 1
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.