diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | ot_accesslist.c | 46 | ||||
| -rw-r--r-- | tests/integration/test_dynamic_accesslist.py | 50 |
3 files changed, 92 insertions, 6 deletions
| @@ -136,7 +136,7 @@ check-sanitize: $(TESTBIN).sanitize | |||
| 136 | PYTHON?=python3 | 136 | PYTHON?=python3 |
| 137 | # The client timeout is cut right down so that tests can reach the expiry | 137 | # The client timeout is cut right down so that tests can reach the expiry |
| 138 | # sweep. tests/integration/test_expiry.py mirrors these two numbers. | 138 | # sweep. tests/integration/test_expiry.py mirrors these two numbers. |
| 139 | FEATURES_integration=$(FEATURES) -DWANT_IP_FROM_QUERY_STRING -DWANT_ACCESSLIST_BLACK -DWANT_KEEPALIVE -DOT_CLIENT_TIMEOUT=5 -DOT_CLIENT_TIMEOUT_CHECKINTERVAL=1 | 139 | FEATURES_integration=$(FEATURES) -DWANT_IP_FROM_QUERY_STRING -DWANT_ACCESSLIST_BLACK -DWANT_DYNAMIC_ACCESSLIST -DWANT_KEEPALIVE -DOT_CLIENT_TIMEOUT=5 -DOT_CLIENT_TIMEOUT_CHECKINTERVAL=1 |
| 140 | CFLAGS_integration=$(CFLAGS) $(OPTS_debug) $(FEATURES_integration) -fsanitize=address,undefined -fno-omit-frame-pointer | 140 | CFLAGS_integration=$(CFLAGS) $(OPTS_debug) $(FEATURES_integration) -fsanitize=address,undefined -fno-omit-frame-pointer |
| 141 | LDFLAGS_integration=$(LDFLAGS) -fsanitize=address,undefined | 141 | LDFLAGS_integration=$(LDFLAGS) -fsanitize=address,undefined |
| 142 | 142 | ||
diff --git a/ot_accesslist.c b/ot_accesslist.c index e8e0476..df084b8 100644 --- a/ot_accesslist.c +++ b/ot_accesslist.c | |||
| @@ -273,7 +273,8 @@ static void *accesslist_worker(void *args) { | |||
| 273 | } | 273 | } |
| 274 | 274 | ||
| 275 | #ifdef WANT_DYNAMIC_ACCESSLIST | 275 | #ifdef WANT_DYNAMIC_ACCESSLIST |
| 276 | static pthread_t thread_adder_id, thread_deleter_id; | 276 | static pthread_t thread_adder_id, thread_deleter_id; |
| 277 | static _Atomic int adder_left, deleter_left; | ||
| 277 | static void *accesslist_adddel_worker(const char *fifoname, ot_accesslist *_Atomic *adding_to, ot_accesslist *_Atomic *removing_from) { | 278 | static void *accesslist_adddel_worker(const char *fifoname, ot_accesslist *_Atomic *adding_to, ot_accesslist *_Atomic *removing_from) { |
| 278 | struct stat st; | 279 | struct stat st; |
| 279 | 280 | ||
| @@ -297,6 +298,9 @@ static void *accesslist_adddel_worker(const char *fifoname, ot_accesslist *_ | |||
| 297 | ssize_t linelen; | 298 | ssize_t linelen; |
| 298 | 299 | ||
| 299 | if (!fifo) { | 300 | if (!fifo) { |
| 301 | /* Woken on purpose: the loop condition above decides what happens next */ | ||
| 302 | if (errno == EINTR) | ||
| 303 | continue; | ||
| 300 | fprintf(stderr, "Error when reading dynamic accesslists: Couldn't open FIFO at %s, error: %s\n", fifoname, strerror(errno)); | 304 | fprintf(stderr, "Error when reading dynamic accesslists: Couldn't open FIFO at %s, error: %s\n", fifoname, strerror(errno)); |
| 301 | return NULL; | 305 | return NULL; |
| 302 | } | 306 | } |
| @@ -324,8 +328,14 @@ static void *accesslist_adddel_worker(const char *fifoname, ot_accesslist *_ | |||
| 324 | ptrdiff_t off = exactmatch - (*removing_from)->list; | 328 | ptrdiff_t off = exactmatch - (*removing_from)->list; |
| 325 | ot_accesslist *accesslist_new = accesslist_make(*removing_from, (*removing_from)->size - 1); | 329 | ot_accesslist *accesslist_new = accesslist_make(*removing_from, (*removing_from)->size - 1); |
| 326 | if (accesslist_new) { | 330 | if (accesslist_new) { |
| 327 | memcpy(accesslist_new->list, (*removing_from)->list, sizeof(ot_hash) * off); | 331 | size_t after = (*removing_from)->size - off - 1; |
| 328 | memcpy(accesslist_new->list + off, (*removing_from)->list + off + 1, sizeof(ot_hash) * ((*removing_from)->size - off - 1)); | 332 | |
| 333 | /* Removing the only entry leaves a list with no room at all, and | ||
| 334 | memcpy may not be handed that even for nothing */ | ||
| 335 | if (off) | ||
| 336 | memcpy(accesslist_new->list, (*removing_from)->list, sizeof(ot_hash) * off); | ||
| 337 | if (after) | ||
| 338 | memcpy(accesslist_new->list + off, (*removing_from)->list + off + 1, sizeof(ot_hash) * after); | ||
| 329 | (*removing_from)->base = time_now_minutes(); | 339 | (*removing_from)->base = time_now_minutes(); |
| 330 | *removing_from = accesslist_new; | 340 | *removing_from = accesslist_new; |
| 331 | } | 341 | } |
| @@ -360,6 +370,7 @@ static void *accesslist_adddel_worker(const char *fifoname, ot_accesslist *_ | |||
| 360 | pthread_mutex_unlock(&g_accesslist_mutex); | 370 | pthread_mutex_unlock(&g_accesslist_mutex); |
| 361 | } | 371 | } |
| 362 | 372 | ||
| 373 | free(line); | ||
| 363 | fclose(fifo); | 374 | fclose(fifo); |
| 364 | } | 375 | } |
| 365 | return NULL; | 376 | return NULL; |
| @@ -367,11 +378,30 @@ static void *accesslist_adddel_worker(const char *fifoname, ot_accesslist *_ | |||
| 367 | 378 | ||
| 368 | static void *accesslist_adder_worker(void *args) { | 379 | static void *accesslist_adder_worker(void *args) { |
| 369 | (void)args; | 380 | (void)args; |
| 370 | return accesslist_adddel_worker(g_accesslist_pipe_add, &g_accesslist_add, &g_accesslist_delete); | 381 | accesslist_adddel_worker(g_accesslist_pipe_add, &g_accesslist_add, &g_accesslist_delete); |
| 382 | atomic_store(&adder_left, 1); | ||
| 383 | return NULL; | ||
| 371 | } | 384 | } |
| 372 | static void *accesslist_deleter_worker(void *args) { | 385 | static void *accesslist_deleter_worker(void *args) { |
| 373 | (void)args; | 386 | (void)args; |
| 374 | return accesslist_adddel_worker(g_accesslist_pipe_delete, &g_accesslist_delete, &g_accesslist_add); | 387 | accesslist_adddel_worker(g_accesslist_pipe_delete, &g_accesslist_delete, &g_accesslist_add); |
| 388 | atomic_store(&deleter_left, 1); | ||
| 389 | return NULL; | ||
| 390 | } | ||
| 391 | |||
| 392 | /* These sit in a blocking open or read on a FIFO, so they are woken the way | ||
| 393 | the other workers are, and the signal is repeated because it only | ||
| 394 | interrupts a call already under way. The pipe name is what says whether | ||
| 395 | the thread was ever started. */ | ||
| 396 | static void accesslist_stop_fifo_worker(pthread_t thread, _Atomic int *left, const char *pipe_name) { | ||
| 397 | if (!pipe_name) | ||
| 398 | return; | ||
| 399 | |||
| 400 | while (!atomic_load(left)) { | ||
| 401 | pthread_kill(thread, OT_SIGNAL_WAKE); | ||
| 402 | usleep(1000); | ||
| 403 | } | ||
| 404 | pthread_join(thread, NULL); | ||
| 375 | } | 405 | } |
| 376 | #endif | 406 | #endif |
| 377 | 407 | ||
| @@ -392,6 +422,12 @@ void accesslist_deinit(void) { | |||
| 392 | pthread_kill(thread_id, SIGHUP); | 422 | pthread_kill(thread_id, SIGHUP); |
| 393 | pthread_join(thread_id, NULL); | 423 | pthread_join(thread_id, NULL); |
| 394 | 424 | ||
| 425 | #ifdef WANT_DYNAMIC_ACCESSLIST | ||
| 426 | /* Before the lists they are reading are freed below */ | ||
| 427 | accesslist_stop_fifo_worker(thread_adder_id, &adder_left, g_accesslist_pipe_add); | ||
| 428 | accesslist_stop_fifo_worker(thread_deleter_id, &deleter_left, g_accesslist_pipe_delete); | ||
| 429 | #endif | ||
| 430 | |||
| 395 | pthread_mutex_lock(&g_accesslist_mutex); | 431 | pthread_mutex_lock(&g_accesslist_mutex); |
| 396 | 432 | ||
| 397 | g_accesslist = accesslist_free(g_accesslist); | 433 | g_accesslist = accesslist_free(g_accesslist); |
diff --git a/tests/integration/test_dynamic_accesslist.py b/tests/integration/test_dynamic_accesslist.py new file mode 100644 index 0000000..7fa5fde --- /dev/null +++ b/tests/integration/test_dynamic_accesslist.py | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | """Hashes added to and removed from the blacklist while the tracker runs. | ||
| 2 | |||
| 3 | The two FIFOs are read by threads of their own, which is also what makes | ||
| 4 | this the test that drives those threads through a shutdown. | ||
| 5 | """ | ||
| 6 | |||
| 7 | import os | ||
| 8 | import time | ||
| 9 | import unittest | ||
| 10 | |||
| 11 | from harness import TrackerTestCase | ||
| 12 | |||
| 13 | BLOCKED = bytes(range(1, 21)) | ||
| 14 | |||
| 15 | |||
| 16 | def as_hex(info_hash): | ||
| 17 | return "".join("%02x" % b for b in info_hash) | ||
| 18 | |||
| 19 | |||
| 20 | class DynamicAccesslist(TrackerTestCase): | ||
| 21 | config = ("access.blacklist {dir}/blacklist\n" | ||
| 22 | "access.fifo_add {dir}/add.fifo\n" | ||
| 23 | "access.fifo_delete {dir}/delete.fifo\n") | ||
| 24 | files = {"blacklist": b""} | ||
| 25 | |||
| 26 | def feed(self, name, info_hash): | ||
| 27 | """Writing to the FIFO blocks until the reader has it, so this also | ||
| 28 | tells us the thread is alive.""" | ||
| 29 | with open(os.path.join(self.tracker.dir, name), "w") as fifo: | ||
| 30 | fifo.write("%s added by a test\n" % as_hex(info_hash)) | ||
| 31 | |||
| 32 | def wait_for(self, info_hash, blocked, patience=5.0): | ||
| 33 | """The reader thread picks the hash up on its own schedule, so this | ||
| 34 | waits for the answer to change rather than assuming it already has.""" | ||
| 35 | deadline = time.time() + patience | ||
| 36 | while time.time() < deadline: | ||
| 37 | answer = self.tracker.announce(info_hash) | ||
| 38 | if (b"failure reason" in answer) == blocked: | ||
| 39 | return True | ||
| 40 | time.sleep(0.05) | ||
| 41 | return False | ||
| 42 | |||
| 43 | def test_a_hash_can_be_blocked_and_released_while_running(self): | ||
| 44 | self.assertNotIn(b"failure reason", self.tracker.announce(BLOCKED)) | ||
| 45 | |||
| 46 | self.feed("add.fifo", BLOCKED) | ||
| 47 | self.assertTrue(self.wait_for(BLOCKED, blocked=True), "the hash was never blocked") | ||
| 48 | |||
| 49 | self.feed("delete.fifo", BLOCKED) | ||
| 50 | self.assertTrue(self.wait_for(BLOCKED, blocked=False), "the hash was never released") | ||
