oboeru.c (7120B)
1 /* See LICENSE for license details. */ 2 #include <fcntl.h> 3 #include <limits.h> 4 #include <signal.h> 5 #include <stdint.h> 6 #include <stdio.h> 7 #include <stdlib.h> 8 #include <string.h> 9 #include <sys/stat.h> 10 #include <sys/types.h> 11 #include <time.h> 12 #include <unistd.h> 13 14 #include "arg.h" 15 #include "util.h" 16 #include "config.h" 17 18 #define BUF_SIZE BUFSIZ 19 20 enum { 21 CARD_PASS, 22 CARD_FAIL 23 }; 24 25 typedef struct { 26 int64_t reviewed, due; 27 size_t id, deck; 28 char *extra; 29 uint8_t leeches, nobump; 30 } Card; 31 32 typedef struct node { 33 Card *card; 34 struct node *next; 35 } Node; 36 37 static const char *scanfmt = "%ld" DELIM "%[^"DELIM"]" DELIM "%[^"DELIM"]" DELIM "%d" DELIM "%[^\n]"; 38 static const char *logfmt = CARDID DELIM "%s" DELIM "%s" DELIM "%d" DELIM "%s\n"; 39 40 static Node *head; 41 static size_t n_reviews, n_reviewed; 42 43 /* option parsing variables */ 44 char *argv0; 45 static int cflag; 46 static int dflag; 47 48 static void 49 usage(void) 50 { 51 die("usage: %s [-cd] pipe deck [deck1 ...]\n", argv0); 52 } 53 54 static void 55 sighandler(const int signo) 56 { 57 fprintf(stderr, remfmt, n_reviews - n_reviewed); 58 } 59 60 static void 61 freenodes(Node *node) 62 { 63 if (node->next) 64 freenodes(node->next); 65 66 if (node->card) { 67 free(node->card->extra); 68 free(node->card); 69 } 70 free(node); 71 node = NULL; 72 } 73 74 static void 75 cleanup(Node *node, void *decks, void *reviews) 76 { 77 freenodes(node); 78 free(decks); 79 free(reviews); 80 } 81 82 /* returns a filled out Card * after parsing */ 83 static Card * 84 parse_line(const char *line) 85 { 86 struct tm tm; 87 char reviewed[BUF_SIZE], due[BUF_SIZE]; 88 Card *c = xmalloc(sizeof(Card)); 89 c->extra = xmalloc(BUF_SIZE); 90 91 sscanf(line, scanfmt, &c->id, reviewed, due, &c->leeches, c->extra); 92 93 memset(&tm, 0, sizeof(tm)); 94 strptime(reviewed, timefmt, &tm); 95 c->reviewed = timegm(&tm); 96 97 memset(&tm, 0, sizeof(tm)); 98 strptime(due, timefmt, &tm); 99 c->due = timegm(&tm); 100 101 c->nobump = 0; 102 103 return c; 104 } 105 106 /* returns the latest allocated node with no card */ 107 static Node * 108 parse_file(const char *file, Node *node, size_t deck_id) 109 { 110 FILE *fp; 111 char *line = NULL; 112 size_t len = 0; 113 114 fp = fopen(file, "r"); 115 if (!fp) 116 die("fopen(%s)\n", file); 117 118 for (; getline(&line, &len, fp) != -1; node = node->next = xmalloc(sizeof(Node))) { 119 node->next = NULL; 120 node->card = parse_line(line); 121 if (node->card) 122 node->card->deck = deck_id; 123 } 124 125 free(line); 126 fclose(fp); 127 128 return node; 129 } 130 131 static int 132 needs_review(Card *card) 133 { 134 time_t t; 135 136 if (card->leeches >= MAX_LEECHES) 137 return 0; 138 139 t = time(NULL); 140 141 if (card->due > t) 142 return 0; 143 144 return 1; 145 } 146 147 static Card ** 148 add_review(Card *r[], Card *c) 149 { 150 r = xreallocarray(r, ++n_reviews, sizeof(Card **)); 151 r[n_reviews - 1] = c; 152 153 return r; 154 } 155 156 static Card ** 157 mkreviews(Node *node) 158 { 159 Card **r = NULL; 160 161 for (; node; node = node->next) 162 if (node->card && needs_review(node->card)) 163 r = add_review(r, node->card); 164 165 return r; 166 } 167 168 static int8_t 169 bump_card(Card *card, int8_t status) 170 { 171 int64_t diff; 172 time_t t = time(NULL); 173 174 if (card->nobump && status != CARD_FAIL) 175 return 0; 176 177 diff = card->due - card->reviewed; 178 if (diff < 0) 179 fprintf(stderr, "card id: %ld: malformed review time\n", card->id); 180 181 /* only hit this on the first time through */ 182 if (!card->nobump) 183 card->reviewed = t; 184 185 switch (status) { 186 case CARD_PASS: 187 /* - 1s to avoid rounding error */ 188 if (diff < MINIMUM_INCREASE - 1) { 189 card->due = t + MINIMUM_INCREASE; 190 return 1; 191 } else if (diff > LEECH_AGE) { 192 card->due = t + diff * GROWTH_RATE + 24 * 3600 * (rand() % 2); 193 } else { 194 card->due = t + diff * GROWTH_RATE; 195 } 196 break; 197 case CARD_FAIL: 198 if (diff > LEECH_AGE && !card->nobump) 199 card->leeches++; 200 201 if (diff * SHRINK_RATE < MINIMUM_INCREASE - 1) 202 card->due = t + MINIMUM_INCREASE; 203 else 204 card->due = t + diff * SHRINK_RATE; 205 return 1; 206 } 207 208 return 0; 209 } 210 211 static void 212 shuffle_reviews(Card *r[], size_t n) 213 { 214 size_t i, j; 215 Card *t; 216 217 if (n <= 1) 218 return; 219 220 srand(time(NULL)); 221 222 /* this could be improved */ 223 for (i = 0; i < n; i++) { 224 j = i + rand() % (n - i); 225 226 t = r[j]; 227 r[j] = r[i]; 228 r[i] = t; 229 } 230 } 231 232 static Card ** 233 review_loop(Card *r[], const char *decks[], const char *fifo) 234 { 235 char reply[BUF_SIZE]; 236 int fd; 237 size_t i, j, n; 238 struct timespec wait = { .tv_sec = 0, .tv_nsec = 50e6 }; 239 struct { const char *str; int status; } reply_map[] = { 240 { .str = "pass", .status = CARD_PASS }, 241 { .str = "fail", .status = CARD_FAIL } 242 }; 243 244 for (i = 0; i < n_reviews; i++, n_reviewed++) { 245 fprintf(stdout, "%s\t"CARDID"\n", decks[r[i]->deck], r[i]->id); 246 /* force a flush before blocking in open() */ 247 fflush(stdout); 248 249 reply[0] = 0; 250 fd = open(fifo, O_RDONLY); 251 if (fd == -1) 252 break; 253 n = read(fd, reply, sizeof(reply)); 254 close(fd); 255 256 /* strip a trailing newline */ 257 if (reply[n-1] == '\n') 258 reply[n-1] = 0; 259 260 if (!strcmp(reply, "quit")) 261 break; 262 263 for (j = 0; j < LEN(reply_map); j++) 264 if (!strcmp(reply, reply_map[j].str)) 265 r[i]->nobump = bump_card(r[i], reply_map[j].status); 266 267 /* if the card wasn't bumped it needs an extra review */ 268 if (r[i]->nobump) { 269 r = add_review(r, r[i]); 270 /* r[i+1] exists because we have added a review */ 271 shuffle_reviews(&r[i + 1], n_reviews - i - 1); 272 } 273 274 /* give the writing process time to close its fd */ 275 nanosleep(&wait, NULL); 276 } 277 return r; 278 } 279 280 static void 281 write_deck(const char *deck, size_t deck_id) 282 { 283 FILE *fp; 284 Node *node; 285 Card *c; 286 char reviewed[BUF_SIZE], due[BUF_SIZE]; 287 char path[PATH_MAX]; 288 289 if (dflag) { 290 snprintf(path, sizeof(path), "%s.debug", deck); 291 fp = fopen(path, "w+"); 292 } else { 293 fp = fopen(deck, "w"); 294 } 295 296 if (!fp) 297 return; 298 299 for (node = head; node; node = node->next) { 300 c = node->card; 301 if (!c || (c->deck != deck_id)) 302 continue; 303 304 strftime(reviewed, sizeof(reviewed), timefmt, 305 gmtime((time_t *)&c->reviewed)); 306 strftime(due, sizeof(due), timefmt, 307 gmtime((time_t *)&c->due)); 308 309 fprintf(fp, logfmt, c->id, reviewed, due, 310 c->leeches, c->extra); 311 } 312 fclose(fp); 313 } 314 315 316 int 317 main(int argc, char *argv[]) 318 { 319 Node *tail; 320 Card **reviews; 321 size_t i, n_decks = 0; 322 const char *fifo = NULL, **decks = NULL; 323 struct sigaction sa; 324 struct stat sb; 325 326 ARGBEGIN { 327 case 'c': 328 cflag = 1; 329 break; 330 case 'd': 331 dflag = 1; 332 break; 333 default: 334 usage(); 335 } ARGEND 336 337 if ((cflag && argc < 1) || (!cflag && argc < 2)) 338 usage(); 339 340 if (!cflag) { 341 fifo = *argv++; 342 argc--; 343 344 stat(fifo, &sb); 345 if (!S_ISFIFO(sb.st_mode)) 346 usage(); 347 } 348 349 memset(&sa, 0, sizeof(sa)); 350 sa.sa_flags = SA_RESTART; 351 sa.sa_handler = sighandler; 352 sigaction(SIGUSR1, &sa, NULL); 353 354 tail = head = xmalloc(sizeof(Node)); 355 356 /* remaining argv elements are deck files */ 357 for (i = 0; argc && *argv; argv++, i++, argc--) { 358 decks = xreallocarray(decks, ++n_decks, sizeof(char *)); 359 decks[i] = *argv; 360 361 tail = parse_file(*argv, tail, i); 362 } 363 364 reviews = mkreviews(head); 365 366 if (cflag) { 367 cleanup(head, decks, reviews); 368 printf("Cards Due: %ld\n", n_reviews); 369 return 0; 370 } 371 372 if (reviews == NULL) { 373 cleanup(head, decks, reviews); 374 die("mkreviews()\n"); 375 } 376 377 shuffle_reviews(reviews, n_reviews); 378 reviews = review_loop(reviews, decks, fifo); 379 380 /* write updated data into deck files */ 381 for (i = 0; i < n_decks; i++) 382 write_deck(decks[i], i); 383 384 cleanup(head, decks, reviews); 385 386 return 0; 387 }