├── .gitignore ├── test_odo ├── Makefile ├── types.h ├── man ├── odo.1.ronn ├── odo.1 └── odo.1.html ├── README.md └── main.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | odo 3 | -------------------------------------------------------------------------------- /test_odo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -f foo 3 | ./odo foo 4 | awk 'BEGIN {for (i=1; i<9998; i++) { printf("%d ", i) }}' | 5 | xargs -n 1 -P 32 ./odo foo 6 | 7 | # test value after lots of hopefully concurrent runs 8 | if [ "9999" != $(./odo -p foo) ]; then 9 | exit 1 10 | fi 11 | 12 | # test setting 13 | if [ "1234" != $(./odo -s 1234 -p foo) ]; then 14 | exit 1 15 | fi 16 | 17 | # test incrementing 18 | if [ "1235" != $(./odo -p foo) ]; then 19 | exit 1 20 | fi 21 | 22 | rm -f foo 23 | echo all tests passed 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = odo 2 | OPTIMIZE = -O3 3 | WARN = -Wall -pedantic -Wextra 4 | 5 | # Uncomment this to use 8-byte counters, if supported. 6 | #CDEFS += -DCOUNTER_SIZE=8 7 | 8 | # This is written in C99 and expects POSIX getopt. 9 | CSTD += -std=c99 -D_POSIX_C_SOURCE=200112L -D_C99_SOURCE 10 | 11 | CFLAGS += ${CSTD} -g ${WARN} ${CDEFS} ${CINCS} ${OPTIMIZE} 12 | 13 | all: ${PROJECT} 14 | 15 | # Basic targets 16 | ${PROJECT}: main.o 17 | ${CC} -o $@ main.o ${LDFLAGS} 18 | 19 | test: ${PROJECT} 20 | ./test_${PROJECT} 21 | 22 | clean: 23 | rm -f ${PROJECT} *.o *.a *.core 24 | 25 | main.o: types.h Makefile 26 | 27 | # Regenerate documentation (requires ronn) 28 | docs: man/odo.1 man/odo.1.html 29 | 30 | man/odo.1: man/odo.1.ronn 31 | ronn --roff $< 32 | 33 | man/odo.1.html: man/odo.1.ronn 34 | ronn --html $< 35 | 36 | # Installation 37 | PREFIX ?= /usr/local 38 | INSTALL ?= install 39 | RM ?= rm 40 | MAN_DEST ?= ${PREFIX}/share/man 41 | 42 | install: 43 | ${INSTALL} -c ${PROJECT} ${PREFIX}/bin 44 | ${INSTALL} -c man/${PROJECT}.1 ${MAN_DEST}/man1/ 45 | 46 | uninstall: 47 | ${RM} -f ${PREFIX}/bin/${PROJECT} 48 | ${RM} -f ${MAN_DEST}/man1/${PROJECT}.1 49 | -------------------------------------------------------------------------------- /types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | /* If not defined, determine a platform counter size */ 5 | #ifndef COUNTER_SIZE 6 | #if defined(__GCC_HAVE_SYNC_COMPARE_AND_SWAP_8) 7 | #define COUNTER_SIZE 8 8 | #elif defined(__GCC_HAVE_SYNC_COMPARE_AND_SWAP_4) 9 | #define COUNTER_SIZE 4 10 | #else 11 | /* Use 4 by default */ 12 | #define COUNTER_SIZE 4 13 | #endif 14 | #endif 15 | 16 | #define ATOMIC_BOOL_COMPARE_AND_SWAP(PTR, OLD, NEW) \ 17 | (__sync_bool_compare_and_swap(PTR, OLD, NEW)) 18 | 19 | #if (COUNTER_SIZE == 8) 20 | typedef uint64_t counter_t; 21 | #define COUNTER_FORMAT "%08llu" 22 | #define COUNTER_FORMAT_NO_PAD "%llu" 23 | #define STRTOC strtoll 24 | #define NAME_OF_STRTOC "strtoll" 25 | #elif (COUNTER_SIZE == 4) 26 | typedef uint32_t counter_t; 27 | #define COUNTER_FORMAT "%04u" 28 | #define COUNTER_FORMAT_NO_PAD "%u" 29 | #define STRTOC strtol 30 | #define NAME_OF_STRTOC "strtol" 31 | #endif 32 | 33 | typedef enum { OP_INC, OP_SET, OP_CAT, } op_t; 34 | 35 | typedef struct { 36 | const char *path; 37 | op_t op; /* operation */ 38 | counter_t new_value; /* value to set */ 39 | bool print; 40 | int fd; 41 | uint8_t *p; /* raw memory */ 42 | } config_t; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /man/odo.1.ronn: -------------------------------------------------------------------------------- 1 | odo(1) -- an atomic odometer for the command line 2 | ================================================= 3 | 4 | ## SYNOPSIS 5 | 6 | `odo` [-c | -i | -r | -s COUNT] [-p] 7 | 8 | ## DESCRIPTION 9 | 10 | odo atomically updates a count in a file, which will be created if not 11 | present. The count is text-formatted (e.g. "00012345\n"), and will be 12 | accurately incremented or reset even when multiple processes attempt to 13 | change the counter at the same time. (It uses memory mapping and atomic 14 | compare-and-swap operations to eliminate race conditions.) 15 | 16 | This could be used to track some intermittent event, like services being 17 | restarted. Since the counter is just a number in a text file, it's easy 18 | ls to compose odo with other tools. 19 | 20 | ## OPTIONS 21 | 22 | These options impact how the counter is updated: 23 | 24 | * `-c`: 25 | Print the current counter value without updating. 26 | 27 | * `-i`: 28 | Increment the counter. (This is the default.) 29 | 30 | * `-r`: 31 | Reset the counter to 0. 32 | 33 | * `-s COUNT`: 34 | Update the counter to a specific value. 35 | 36 | * `-p`: 37 | Print the new value of the counter after updating. 38 | 39 | ## EXIT STATUS 40 | 41 | Returns 0 if the counter has been successfully updated. Returns 1 if the 42 | file could not be read, created, or written, or if its current contents 43 | do not match the expected format of a counter file. 44 | 45 | ## EXAMPLES 46 | 47 | This atomically increments a counter in /log/restarts. If the counter 48 | file does not exist, it is created as 0 and incremented to 1. 49 | 50 | $ odo /log/restarts 51 | 52 | Same, but print the updated count: 53 | 54 | $ odo -p /log/restarts 55 | 56 | Reset the count to 0: 57 | 58 | $ ./odo -r /log/restarts 59 | 60 | Set the count to a number (for testing notifications, perhaps): 61 | 62 | $ ./odo -s 12345 /log/restarts 63 | 64 | Print the current counter value without incrementing: 65 | 66 | $ ./odo -c /log/restarts 67 | 68 | Print usage / help: 69 | 70 | $ ./odo -h 71 | 72 | ## BUGS 73 | 74 | **odo**'s atomicity is only as reliable as the underlying filesystem's. 75 | Inconsistencies may still occur if used on a non-local filesystems 76 | such as *nfs*. 77 | 78 | ## COPYRIGHT 79 | 80 | **odo** is Copyright (C) 2014 Scott Vokes . 81 | 82 | ## SEE ALSO 83 | 84 | runit(8), sqlite3(1), nfsd(8) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | odo - an atomic odometer for the command line 2 | 3 | # Atomic Odometer? What? 4 | 5 | odo atomically updates a count in a file, which will be created if not 6 | present. The count is text-formatted (e.g. "00012345\n"), and will be 7 | accurately incremented or reset even when multiple processes attempt to 8 | change the counter at the same time. (It uses [memory mapping and atomic 9 | compare-and-swap operations][1] to eliminate race conditions.) 10 | 11 | [1]: https://spin.atomicobject.com/2014/11/24/odo-atomic-counters-from-the-command-line/ 12 | 13 | 14 | ## Use cases 15 | 16 | This could be used to track some intermittent event, like services being 17 | restarted. (This was the [original inspiration][2].) Since the counter 18 | is just a number in a text file, it's easy to compose odo with other 19 | tools. 20 | 21 | [2]: https://twitter.com/nrr/status/529016501421240322 22 | 23 | 24 | ## Dependencies 25 | 26 | odo depends on atomic compare-and-swap functionality (e.g. 27 | `__sync_bool_compare_and_swap`), which is available on most common 28 | platforms. The build is currently tested on Linux, OpenBSD, and OSX on 29 | x86 and x86-64 systems, as well as on a Raspberry Pi (32-bit ARM). 30 | 31 | If the gcc-specific feature defines in `types.h` are not recognized by 32 | your C99 compiler, you may need to set `COUNTER_SIZE` in the Makefile 33 | yourself: `-DCOUNTER_SIZE=4` for 32-bit systems and `-DCOUNTER_SIZE=8` 34 | for 64-bit systems. 35 | 36 | 37 | ## Getting started 38 | 39 | To build it, just type: 40 | 41 | $ make 42 | 43 | To install it: 44 | 45 | $ make install 46 | 47 | To run the tests: 48 | 49 | $ make test 50 | 51 | 52 | ## Example Use 53 | 54 | This atomically increments a counter in /log/restarts. If the counter 55 | file does not exist, it is created as 0 and incremented to 1. 56 | 57 | $ odo /log/restarts 58 | 59 | Same, but print the updated count: 60 | 61 | $ odo -p /log/restarts 62 | 63 | Reset the count to 0: 64 | 65 | $ odo -r /log/restarts 66 | 67 | Set the count to a number (for testing notifications, perhaps): 68 | 69 | $ odo -s 12345 /log/restarts 70 | 71 | Print the current counter value without incrementing: 72 | 73 | $ odo -c /log/restarts 74 | 75 | Print usage / help: 76 | 77 | $ odo -h 78 | 79 | 80 | ## Note 81 | 82 | odo's atomicity is only as reliable as the underlying filesystem's. 83 | Inconsistencies may still occur if used on a non-local filesystems 84 | such as nfs. 85 | -------------------------------------------------------------------------------- /man/odo.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "ODO" "1" "November 2014" "" "" 5 | . 6 | .SH "NAME" 7 | \fBodo\fR \- an atomic odometer for the command line 8 | . 9 | .SH "SYNOPSIS" 10 | \fBodo\fR [\-c | \-i | \-r | \-s COUNT] [\-p] \fIfile\fR 11 | . 12 | .SH "DESCRIPTION" 13 | odo atomically updates a count in a file, which will be created if not present\. The count is text\-formatted (e\.g\. "00012345\en"), and will be accurately incremented or reset even when multiple processes attempt to change the counter at the same time\. (It uses memory mapping and atomic compare\-and\-swap operations to eliminate race conditions\.) 14 | . 15 | .P 16 | This could be used to track some intermittent event, like services being restarted\. Since the counter is just a number in a text file, it\'s easy ls to compose odo with other tools\. 17 | . 18 | .SH "OPTIONS" 19 | These options impact how the counter is updated: 20 | . 21 | .TP 22 | \fB\-c\fR 23 | Print the current counter value without updating\. 24 | . 25 | .TP 26 | \fB\-i\fR 27 | Increment the counter\. (This is the default\.) 28 | . 29 | .TP 30 | \fB\-r\fR 31 | Reset the counter to 0\. 32 | . 33 | .TP 34 | \fB\-s COUNT\fR 35 | Update the counter to a specific value\. 36 | . 37 | .TP 38 | \fB\-p\fR 39 | Print the new value of the counter after updating\. 40 | . 41 | .SH "EXIT STATUS" 42 | Returns 0 if the counter has been successfully updated\. Returns 1 if the file could not be read, created, or written, or if its current contents do not match the expected format of a counter file\. 43 | . 44 | .SH "EXAMPLES" 45 | This atomically increments a counter in /log/restarts\. If the counter file does not exist, it is created as 0 and incremented to 1\. 46 | . 47 | .IP "" 4 48 | . 49 | .nf 50 | 51 | $ odo /log/restarts 52 | . 53 | .fi 54 | . 55 | .IP "" 0 56 | . 57 | .P 58 | Same, but print the updated count: 59 | . 60 | .IP "" 4 61 | . 62 | .nf 63 | 64 | $ odo \-p /log/restarts 65 | . 66 | .fi 67 | . 68 | .IP "" 0 69 | . 70 | .P 71 | Reset the count to 0: 72 | . 73 | .IP "" 4 74 | . 75 | .nf 76 | 77 | $ \./odo \-r /log/restarts 78 | . 79 | .fi 80 | . 81 | .IP "" 0 82 | . 83 | .P 84 | Set the count to a number (for testing notifications, perhaps): 85 | . 86 | .IP "" 4 87 | . 88 | .nf 89 | 90 | $ \./odo \-s 12345 /log/restarts 91 | . 92 | .fi 93 | . 94 | .IP "" 0 95 | . 96 | .P 97 | Print the current counter value without incrementing: 98 | . 99 | .IP "" 4 100 | . 101 | .nf 102 | 103 | $ \./odo \-c /log/restarts 104 | . 105 | .fi 106 | . 107 | .IP "" 0 108 | . 109 | .P 110 | Print usage / help: 111 | . 112 | .IP "" 4 113 | . 114 | .nf 115 | 116 | $ \./odo \-h 117 | . 118 | .fi 119 | . 120 | .IP "" 0 121 | . 122 | .SH "BUGS" 123 | \fBodo\fR\'s atomicity is only as reliable as the underlying filesystem\'s\. Inconsistencies may still occur if used on a non\-local filesystems such as \fInfs\fR\. 124 | . 125 | .SH "COPYRIGHT" 126 | \fBodo\fR is Copyright (C) 2014 Scott Vokes \fIscott\.vokes@atomicobject\.com\fR\. 127 | . 128 | .SH "SEE ALSO" 129 | runit(8), sqlite3(1), nfsd(8) 130 | -------------------------------------------------------------------------------- /man/odo.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | odo(1) - an atomic odometer for the command line 7 | 44 | 45 | 52 | 53 |
54 | 55 | 66 | 67 |
    68 |
  1. odo(1)
  2. 69 |
  3. 70 |
  4. odo(1)
  5. 71 |
72 | 73 |

NAME

74 |

75 | odo - an atomic odometer for the command line 76 |

77 | 78 |

SYNOPSIS

79 | 80 |

odo [-c | -i | -r | -s COUNT] [-p] file

81 | 82 |

DESCRIPTION

83 | 84 |

odo atomically updates a count in a file, which will be created if not 85 | present. The count is text-formatted (e.g. "00012345\n"), and will be 86 | accurately incremented or reset even when multiple processes attempt to 87 | change the counter at the same time. (It uses memory mapping and atomic 88 | compare-and-swap operations to eliminate race conditions.)

89 | 90 |

This could be used to track some intermittent event, like services being 91 | restarted. Since the counter is just a number in a text file, it's easy 92 | ls to compose odo with other tools.

93 | 94 |

OPTIONS

95 | 96 |

These options impact how the counter is updated:

97 | 98 |
99 |
-c

Print the current counter value without updating.

100 |
-i

Increment the counter. (This is the default.)

101 |
-r

Reset the counter to 0.

102 |
-s COUNT

Update the counter to a specific value.

103 |
-p

Print the new value of the counter after updating.

104 |
105 | 106 | 107 |

EXIT STATUS

108 | 109 |

Returns 0 if the counter has been successfully updated. Returns 1 if the 110 | file could not be read, created, or written, or if its current contents 111 | do not match the expected format of a counter file.

112 | 113 |

EXAMPLES

114 | 115 |

This atomically increments a counter in /log/restarts. If the counter 116 | file does not exist, it is created as 0 and incremented to 1.

117 | 118 |
$ odo /log/restarts
119 | 
120 | 121 |

Same, but print the updated count:

122 | 123 |
$ odo -p /log/restarts
124 | 
125 | 126 |

Reset the count to 0:

127 | 128 |
$ ./odo -r /log/restarts
129 | 
130 | 131 |

Set the count to a number (for testing notifications, perhaps):

132 | 133 |
$ ./odo -s 12345 /log/restarts
134 | 
135 | 136 |

Print the current counter value without incrementing:

137 | 138 |
$ ./odo -c /log/restarts
139 | 
140 | 141 |

Print usage / help:

142 | 143 |
$ ./odo -h
144 | 
145 | 146 |

BUGS

147 | 148 |

odo's atomicity is only as reliable as the underlying filesystem's. 149 | Inconsistencies may still occur if used on a non-local filesystems 150 | such as nfs.

151 | 152 | 153 | 154 |

odo is Copyright (C) 2014 Scott Vokes scott.vokes@atomicobject.com.

155 | 156 |

SEE ALSO

157 | 158 |

runit(8), sqlite3(1), nfsd(8)

159 | 160 | 161 |
    162 |
  1. 163 |
  2. November 2014
  3. 164 |
  4. odo(1)
  5. 165 |
166 | 167 |
168 | 169 | 170 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Scott Vokes 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "types.h" 33 | 34 | #define ODO_VERSION_MAJOR 0 35 | #define ODO_VERSION_MINOR 2 36 | #define ODO_VERSION_PATCH 3 37 | #define ODO_AUTHOR "Scott Vokes " 38 | 39 | /* Forward references */ 40 | static void open_counter_file(config_t *cfg); 41 | static void close_counter_file(config_t *cfg); 42 | static int create_new_counter_file(const char *path); 43 | static bool check_format(const char *file); 44 | static void increment_counter(counter_t *pc, bool print); 45 | static void set_counter(counter_t *pc, counter_t nv, bool print); 46 | static void print_as_decimal(counter_t c); 47 | static void format_counter(counter_t *pc, counter_t v); 48 | 49 | static const char *progname = NULL; 50 | 51 | static void usage(void) { 52 | if (progname == NULL) { progname = "odo"; } 53 | 54 | fprintf(stderr, "odometer version %u.%u.%u by %s\n", 55 | ODO_VERSION_MAJOR, ODO_VERSION_MINOR, ODO_VERSION_PATCH, 56 | ODO_AUTHOR); 57 | fprintf(stderr, 58 | "usage: %s [-c | -i | -r | -s COUNT] [-p] FILE\n" 59 | " -h: print this help\n" 60 | " -c: print current counter without incrementing\n" 61 | " -i: increment the counter (default)\n" 62 | " -p: print count after update\n" 63 | " -r: reset counter to 0\n" 64 | " -s COUNT: set counter to a specific value\n", 65 | progname); 66 | exit(1); 67 | } 68 | 69 | static void parse_args(config_t *cfg, int argc, char **argv) { 70 | int a = 0; 71 | 72 | while ((a = getopt(argc, argv, "chprs:")) != -1) { 73 | switch (a) { 74 | case 'c': 75 | cfg->op = OP_CAT; /* print current w/out update */ 76 | break; 77 | case 'i': /* increment */ 78 | cfg->op = OP_INC; 79 | break; 80 | case 'p': /* print */ 81 | cfg->print = true; 82 | break; 83 | case 'r': /* reset */ 84 | cfg->op = OP_SET; 85 | cfg->new_value = 0; 86 | break; 87 | case 's': /* set */ 88 | cfg->op = OP_SET; 89 | cfg->new_value = atol(optarg); 90 | 91 | break; 92 | case 'h': /* fall through */ 93 | case '?': /* help / bad arg */ 94 | usage(); 95 | } 96 | } 97 | 98 | argc -= optind; 99 | argv += optind; 100 | 101 | if (argc < 1) { 102 | usage(); 103 | } else { 104 | cfg->path = argv[0]; 105 | } 106 | } 107 | 108 | /* Open and mmap a counter file, so it can be updated atomically. */ 109 | static void open_counter_file(config_t *cfg) { 110 | int fd = -1; 111 | 112 | /* Retry until successful safe open/create. It shouldn't be possible 113 | * to get into an infinite loop of "Doesn't exist" / "Already 114 | * exists", and any other errors will exit. */ 115 | while (fd == -1) { 116 | /* Try to open it as an existing file. */ 117 | fd = open(cfg->path, O_RDWR); 118 | if (fd == -1) { 119 | if (errno == ENOENT) { /* does not exist */ 120 | errno = 0; 121 | /* Attempt to atomically create it. */ 122 | fd = create_new_counter_file(cfg->path); 123 | } else { 124 | err(EXIT_FAILURE, "open"); 125 | } 126 | } 127 | } 128 | 129 | /* Check file size: should match counter and '\n', or 0. */ 130 | struct stat sbuf; 131 | if (fstat(fd, &sbuf) == -1) { err(EXIT_FAILURE, "stat"); } 132 | off_t exp_size = sizeof(counter_t) + sizeof(char); 133 | 134 | if (sbuf.st_size == 0) { /* race on counter file initialization */ 135 | /* monotonically ftruncate, expanding to the right size */ 136 | if (0 != ftruncate(fd, exp_size)) { 137 | err(1, "ftruncate"); 138 | } 139 | } else if (sbuf.st_size == (exp_size)) { 140 | /* size is as expected */ 141 | } else if (sbuf.st_size != (exp_size)) { 142 | fprintf(stderr, 143 | "Unexpected size %zd, not a valid counter file.\n", 144 | (size_t)sbuf.st_size); 145 | exit(EXIT_FAILURE); 146 | } 147 | 148 | uint8_t *p = mmap(NULL, sizeof(counter_t), PROT_READ | PROT_WRITE, 149 | MAP_SHARED, fd, 0); 150 | 151 | if (p == MAP_FAILED) { 152 | err(EXIT_FAILURE, "mmap"); 153 | } else { 154 | /* Set last byte to '\n'; idempotent. */ 155 | p[exp_size - 1] = '\n'; 156 | 157 | counter_t *cur = (counter_t *)&p[0]; 158 | 159 | /* If not yet initialized, attempt to CAS from 0 to "0000". 160 | * It's fine if another process beats us to it. */ 161 | while (*cur == 0x0) { 162 | counter_t new_string = 0; 163 | format_counter(&new_string, 0); 164 | if (ATOMIC_BOOL_COMPARE_AND_SWAP(cur, 0, new_string)) { 165 | break; 166 | } 167 | } 168 | 169 | cfg->fd = fd; 170 | cfg->p = p; 171 | } 172 | } 173 | 174 | static void close_counter_file(config_t *cfg) { 175 | int res = munmap(cfg->p, sizeof(counter_t)); 176 | if (res == -1) { 177 | err(EXIT_FAILURE, "munmap"); 178 | } 179 | close(cfg->fd); 180 | } 181 | 182 | /* Create a new counter file, initialized to the appropriate 183 | * number of '0's (platform-specific) and a newline.. */ 184 | static int create_new_counter_file(const char *path) { 185 | /* Create and open, but only if it doesn't already exist. */ 186 | int fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0644); 187 | if (fd == -1) { 188 | if (errno == EEXIST) { /* already exists */ 189 | errno = 0; 190 | return -1; /* retry as existing file */ 191 | } 192 | err(EXIT_FAILURE, "open"); 193 | } 194 | 195 | return fd; 196 | } 197 | 198 | /* Read the current counter, which is a numeric string such as "00001230", 199 | * and convert it to a numeric counter. */ 200 | static void read_current_counter(counter_t *pc, counter_t *val) { 201 | char buf[sizeof(counter_t) + 1]; 202 | buf[sizeof(counter_t)] = '\0'; 203 | memcpy(buf, pc, sizeof(counter_t)); 204 | 205 | counter_t v = (counter_t) STRTOC((const char *)pc, NULL, 10); 206 | 207 | /* If the byte array at *pc isn't readable as a number, abort. 208 | * We're prabably not reading a counter file as expected. */ 209 | if (v == 0 && (errno == EINVAL || errno == ERANGE)) { 210 | err(EXIT_FAILURE, NAME_OF_STRTOC); 211 | } 212 | *val = v; 213 | } 214 | 215 | static bool check_format(const char *buf) { 216 | if (buf[sizeof(counter_t)] != '\n') { 217 | return false; 218 | } 219 | for (uint8_t i = 0; i < sizeof(counter_t); i++) { 220 | if (!isdigit(buf[i])) { 221 | return false; 222 | } 223 | } 224 | return true; 225 | } 226 | 227 | /* Format the counter as a padded decimal number string. */ 228 | static void format_counter(counter_t *pc, counter_t v) { 229 | char buf[sizeof(counter_t) + 1]; 230 | buf[sizeof(counter_t)] = '\0'; 231 | 232 | if ((int)sizeof(buf) < snprintf(buf, sizeof(buf), COUNTER_FORMAT, v)) { 233 | fprintf(stderr, "snprintf error\n"); 234 | exit(1); 235 | } else { 236 | memcpy(pc, buf, sizeof(counter_t)); 237 | } 238 | } 239 | 240 | /* Atomically increment a decimal numeric string. */ 241 | static void increment_counter(counter_t *pc, bool print) { 242 | for (;;) { 243 | counter_t c = *pc; 244 | counter_t old = 0; 245 | read_current_counter(pc, &old); 246 | counter_t new = old + 1; 247 | counter_t new_string = 0; 248 | format_counter(&new_string, new); 249 | 250 | if (ATOMIC_BOOL_COMPARE_AND_SWAP(pc, c, new_string)) { 251 | if (print) { print_as_decimal(new); } 252 | break; 253 | } 254 | } 255 | } 256 | 257 | /* Atomically cat the current value without updating it. */ 258 | static void cat_counter(counter_t *pc) { 259 | counter_t cur = 0; 260 | read_current_counter(pc, &cur); 261 | print_as_decimal(cur); 262 | } 263 | 264 | /* Atomically change the decimal numeric string. */ 265 | static void set_counter(counter_t *pc, counter_t nv, bool print) { 266 | for (;;) { 267 | counter_t c = *pc; 268 | counter_t old = 0; 269 | read_current_counter(pc, &old); 270 | counter_t new_string = 0; 271 | format_counter(&new_string, nv); 272 | 273 | if (ATOMIC_BOOL_COMPARE_AND_SWAP(pc, c, new_string)) { 274 | if (print) { print_as_decimal(nv); } 275 | break; 276 | } 277 | } 278 | } 279 | 280 | /* Print a counter value, which is also a decimal byte string 281 | * (e.g. "00001234") as a string. */ 282 | static void print_as_decimal(counter_t c) { 283 | char buf[sizeof(counter_t) + 1]; 284 | memset(buf, 0, sizeof(buf)); 285 | snprintf(buf, sizeof(buf), COUNTER_FORMAT_NO_PAD, c); 286 | printf("%s\n", buf); 287 | } 288 | 289 | int main(int argc, char **argv) { 290 | config_t cfg = { .op = OP_INC, }; 291 | 292 | /* Grab program name before getopt discards it. */ 293 | if (argc > 0) { progname = argv[0]; } 294 | 295 | parse_args(&cfg, argc, argv); 296 | open_counter_file(&cfg); 297 | 298 | if (!check_format((char *)cfg.p)) { 299 | fprintf(stderr, "Bad format, not a valid counter file.\n"); 300 | exit(EXIT_FAILURE); 301 | } 302 | 303 | switch (cfg.op) { 304 | case OP_INC: 305 | increment_counter((counter_t *)cfg.p, cfg.print); 306 | break; 307 | case OP_CAT: 308 | cat_counter((counter_t *)cfg.p); 309 | break; 310 | case OP_SET: 311 | set_counter((counter_t *)cfg.p, cfg.new_value, cfg.print); 312 | break; 313 | default: 314 | return EXIT_FAILURE; 315 | } 316 | close_counter_file(&cfg); 317 | return EXIT_SUCCESS; 318 | } 319 | --------------------------------------------------------------------------------