From f6de3b5bc323e3cb34408feed17a853724f78451 Mon Sep 17 00:00:00 2001 From: Andrei Stan Date: Sun, 3 Nov 2024 15:52:18 +0200 Subject: [PATCH] assignments/parallel-firewall: Add parallel firewall assignment Add parallel firewall assignment for the compute chapter. The goal is to introduce students to parallel programming with a single producer multiple consumer problem that uses a thread-safe ring buffer to transfer data between threads. Signed-off-by: Andrei Stan --- config.yaml | 2 +- .../assignments/parallel-firewall/README.md | 193 ++++++++++ .../parallel-firewall/src/.gitignore | 0 .../parallel-firewall/src/Makefile | 32 ++ .../parallel-firewall/src/consumer.c | 35 ++ .../parallel-firewall/src/consumer.h | 20 + .../parallel-firewall/src/firewall.c | 78 ++++ .../parallel-firewall/src/packet.c | 50 +++ .../parallel-firewall/src/packet.h | 29 ++ .../parallel-firewall/src/producer.c | 29 ++ .../parallel-firewall/src/producer.h | 11 + .../parallel-firewall/src/ring_buffer.c | 44 +++ .../parallel-firewall/src/ring_buffer.h | 27 ++ .../parallel-firewall/src/serial.c | 48 +++ .../parallel-firewall/tests/.gitignore | 1 + .../parallel-firewall/tests/Makefile | 30 ++ .../parallel-firewall/tests/checker.py | 359 ++++++++++++++++++ .../parallel-firewall/tests/gen_packets.py | 93 +++++ .../parallel-firewall/tests/grade.sh | 136 +++++++ .../parallel-firewall/utils/log/CPPLINT.cfg | 1 + .../parallel-firewall/utils/log/log.c | 197 ++++++++++ .../parallel-firewall/utils/log/log.h | 69 ++++ .../parallel-firewall/utils/utils.h | 38 ++ 23 files changed, 1521 insertions(+), 1 deletion(-) create mode 100644 content/assignments/parallel-firewall/README.md create mode 100644 content/assignments/parallel-firewall/src/.gitignore create mode 100644 content/assignments/parallel-firewall/src/Makefile create mode 100644 content/assignments/parallel-firewall/src/consumer.c create mode 100644 content/assignments/parallel-firewall/src/consumer.h create mode 100644 content/assignments/parallel-firewall/src/firewall.c create mode 100644 content/assignments/parallel-firewall/src/packet.c create mode 100644 content/assignments/parallel-firewall/src/packet.h create mode 100644 content/assignments/parallel-firewall/src/producer.c create mode 100644 content/assignments/parallel-firewall/src/producer.h create mode 100644 content/assignments/parallel-firewall/src/ring_buffer.c create mode 100644 content/assignments/parallel-firewall/src/ring_buffer.h create mode 100644 content/assignments/parallel-firewall/src/serial.c create mode 100644 content/assignments/parallel-firewall/tests/.gitignore create mode 100644 content/assignments/parallel-firewall/tests/Makefile create mode 100644 content/assignments/parallel-firewall/tests/checker.py create mode 100755 content/assignments/parallel-firewall/tests/gen_packets.py create mode 100755 content/assignments/parallel-firewall/tests/grade.sh create mode 100644 content/assignments/parallel-firewall/utils/log/CPPLINT.cfg create mode 100644 content/assignments/parallel-firewall/utils/log/log.c create mode 100644 content/assignments/parallel-firewall/utils/log/log.h create mode 100644 content/assignments/parallel-firewall/utils/utils.h diff --git a/config.yaml b/config.yaml index 0a1002a42f..d3e177b5d9 100644 --- a/config.yaml +++ b/config.yaml @@ -303,7 +303,7 @@ docusaurus: subsections: - Mini Libc/: chapters/software-stack/libc/projects/mini-libc/ - Memory Allocator/: content/assignments/memory-allocator/ - - Parallel Graph/: content/assignments/parallel-graph/ + - Parallel Firewall/: content/assignments/parallel-firewall/ - Mini Shell/: content/assignments/minishell/ - Asynchronous Web Server/: content/assignments/async-web-server/ - Exams: diff --git a/content/assignments/parallel-firewall/README.md b/content/assignments/parallel-firewall/README.md new file mode 100644 index 0000000000..238d426960 --- /dev/null +++ b/content/assignments/parallel-firewall/README.md @@ -0,0 +1,193 @@ +# Parallel Firewall + +## Objectives + +- Learn how to design and implement parallel programs +- Get experienced at utilizing the POSIX threading API +- Learn how to convert a serial program into a parallel one + +## Statement + +A firewall is a program that checks network packets against a series of filters which provide a decision regarding dropping or allowing the packets to continue to the upper level in the TCP/IP stack. +In the case of this project, instead of a network card, there will be a producer thread that buffers packets into a circular buffer, out of which consumer threads will take packets and process them in order to decide whether they advance to the upper levels of the stack. +You will have to implement the circular buffer and the consumer threads in order to provide a log file with the firewall's decisions ordered by a timestamp. + +## Support Code + +The support code consists of the directories: + +- `src/` contains the skeleton for the parallelized firewall and the already implemented serial code. + You will have to implement the missing parts marked as `TODO` + +- `utils/` contains utility files used for debugging and logging. + +- `tests/` contains tests used to validate and grade the assignment. + +## Implementation + +### Firewall Threads + +In order to parallelize the firewall we have to distribute the packets to multiple threads. +The packets will be added to a shared data structure (visible to all threads) by a `producer` thread and processed by multiple `consumer` threads. +Each `consumer` thread picks a packet from the shared data structure, checks it against the filter function and writes the packet hash together with the drop/accept decision to a log file. +`consumer` threads stop waiting for new packets from the `producer` thread and exit when the `producer` thread closes the connection to the shared data structure. + +The `consumer` threads **must not do any form of busy waiting**. +When there are new packets that need to be handled, the `consumer` threads must be **notified**. +**Waiting in a `while()` loop or sleeping is not considered a valid synchronization mechanism and points will be deducted.** + +Implement the `consumer` related functions marked with `TODO` in the `src/consumer.c` file. + +### Ring Buffers + +A ring buffer (or a circular buffer) is a data structure that stores its elements in a circular fixed size array. +One of the advantages of using such a data structure as opposed to an array is that it acts as a FIFO, without the overhead of moving the elements to the left as they are consumed. +Thus, the shared ring buffer offers the following fields: + +- `write_pos` index in the buffer used by the `producer` thread for appending new packets. +- `read_pos` index in the buffer used by the `consumer` threads to pick packets. +- `cap` the size of the internal buffer. +- `data` pointer to the internal buffer. + +Apart from these fields you have to add synchronization primitives in order to allow multiple threads to access the ring buffer in a deterministic manner. +You can use mutexes, semaphores, conditional variables and other synchronization mechanisms offered by the `pthread` library. + +You will have to implement the following interface for the ring buffer: + +- `ring_buffer_init()`: initialize the ring buffer (allocate memory and synchronization primitives). +- `ring_buffer_enqueue()`: add elements to the ring buffer. +- `ring_buffer_dequeue()`: remove elements from the ring buffer. +- `ring_buffer_destroy()`: free up the memory used by the ring_buffer. +- `ring_buffer_stop()`: finish up using the ring buffer for the calling thread. + +### Log File + +The output of the firewall will be a log file with the rows containing the firewall's decision, the hash of the packet and its timestamp. +The actual format can be found in the serial implementation (at `src/serial.c`). + +When processing the packets in parallel the threads will finish up the work in a non deterministic order. +We would like the logs to be sorted by the packet timestamp, the order that they came in from the producer. +Thus, the `consumers` should insert the packet information to the log file such as the result is ordered by timestamp. + +The logs must be written to the file in ascending order during packet processing. +**Sorting the log file after the consumer threads have finished processing is not considered a valid synchronization mechanism and points will be deducted.** + +## Operations + +### Building + +To build both the serial and the parallel versions, run `make` in the `src/` directory: + +```console +student@so:~/.../content/assignments/parallel-firewall$ cd src/ + +student@so:~/.../assignments/parallel-firewall/src$ make +``` + +That will create the `serial` and `firewall` binaries. + +## Testing and Grading + +Testing is automated. +Tests are located in the `tests/` directory. + +To test and grade your assignment solution, enter the `tests/` directory and run `grade.sh`. + +```console +student@so:~/.../content/assignments/parallel-firewall$ cd tests/ +``` + +```console +student@so:~/.../content/assignments/parallel-firewall/tests$ ./grade.sh +``` + +Note that this requires linters being available. +The easiest way to test the project is to use a Docker-based setup with everything installed and configured (see the [README.checker.md](README.checker.md) file for instructions). + +To create the tests, run: + +```console +student@so:~/.../content/assignments/parallel-firewall/tests$ make check +``` + +To remove the tests, run: + +```console +student@so:~/.../content/assignments/parallel-firewall/tests$ make distclean +``` + +When using `grade.sh` you will get a maximum of 90/100 points for general correctness and a maximum of 10/100 points for coding style. + +### Restrictions + +- Threads must yield the cpu when waiting for empty/full buffers i.e. not doing `busy waiting`. +- The logs must be written as they are processed and not after the processing is done, in ascending order by the timestamp. + +### Grades + +- 10 points are awarded for a single consumer solution that also implements the ring buffer +- 50 points are awarded for a multi consumer solution +- 30 points are awarded for a multi consumer solution that writes the logs in the sorted manner (bearing in mind the above restrictions) + +### Running the Checker + +Each test is worth a number of points. +The maximum grade is `90`. + +A successful run will show the output: + +```console +student@so:~/.../assignments/parallel-firewall/tests$ make check +[...] +Test [ 10 packets, sort False, 1 thread ] ...................... passed ... 3 +Test [ 1,000 packets, sort False, 1 thread ] ...................... passed ... 3 +Test [20,000 packets, sort False, 1 thread ] ...................... passed ... 4 +Test [ 10 packets, sort True , 2 threads] ...................... passed ... 5 +Test [ 10 packets, sort True , 4 threads] ...................... passed ... 5 +Test [ 100 packets, sort True , 2 threads] ...................... passed ... 5 +Test [ 100 packets, sort True , 4 threads] ...................... passed ... 5 +Test [ 1,000 packets, sort True , 2 threads] ...................... passed ... 5 +Test [ 1,000 packets, sort True , 4 threads] ...................... passed ... 5 +Test [10,000 packets, sort True , 2 threads] ...................... passed ... 5 +Test [10,000 packets, sort True , 4 threads] ...................... passed ... 5 +Test [20,000 packets, sort True , 2 threads] ...................... passed ... 5 +Test [20,000 packets, sort True , 4 threads] ...................... passed ... 5 +Test [ 1,000 packets, sort False, 4 threads] ...................... passed ... 5 +Test [ 1,000 packets, sort False, 8 threads] ...................... passed ... 5 +Test [10,000 packets, sort False, 4 threads] ...................... passed ... 5 +Test [10,000 packets, sort False, 8 threads] ...................... passed ... 5 +Test [20,000 packets, sort False, 4 threads] ...................... passed ... 5 +Test [20,000 packets, sort False, 8 threads] ...................... passed ... 5 + +Checker: 90/100 +``` + +### Running the Linters + +To run the linters, use the `make lint` command in the `tests/` directory: + +```console +student@so:~/.../assignments/parallel-firewall/tests$ make lint +[...] +cd .. && checkpatch.pl -f checker/*.sh tests/*.sh +[...] +cd .. && cpplint --recursive src/ tests/ checker/ +[...] +cd .. && shellcheck checker/*.sh tests/*.sh +``` + +Note that the linters have to be installed on your system: [`checkpatch.pl`](https://.com/torvalds/linux/blob/master/scripts/checkpatch.pl), [`cpplint`](https://github.com/cpplint/cpplint), [`shellcheck`](https://www.shellcheck.net/). +They also need to have certain configuration options. +It's easiest to run them in a Docker-based setup with everything configured. + +### Fine-Grained Testing + +Input tests cases are located in `tests/in/` and are generated by the checker. +The expected results are generated by the checker while running the serial implementation. +If you want to run a single test, use the below commands while in the `src/` directory: + +```console +student@so:~/.../assignments/parallel-firewall/src$ ./firewall ../tests/in/test_.in +``` + +Results provided by the serial and parallel implementation must be the same for the test to successfully pass. diff --git a/content/assignments/parallel-firewall/src/.gitignore b/content/assignments/parallel-firewall/src/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/content/assignments/parallel-firewall/src/Makefile b/content/assignments/parallel-firewall/src/Makefile new file mode 100644 index 0000000000..30cc2bb825 --- /dev/null +++ b/content/assignments/parallel-firewall/src/Makefile @@ -0,0 +1,32 @@ +BUILD_DIR := build +UTILS_PATH ?= ../utils +CPPFLAGS := -I$(UTILS_PATH) +CFLAGS := -Wall -Wextra + +CFLAGS += -ggdb -O0 +LDLIBS := -lpthread + +SRCS:= ring_buffer.c producer.c consumer.c packet.c $(UTILS_PATH)/log/log.c +HDRS := $(patsubst %.c,%.h,$(SRCS)) +OBJS := $(patsubst %.c,%.o,$(SRCS)) + +.PHONY: all pack clean always + +all: firewall serial + +firewall: $(OBJS) firewall.o + $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $^ $(LDLIBS) + +serial: $(OBJS) serial.o + $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $^ $(LDLIBS) + +$(UTILS_PATH)/log/log.o: $(UTILS_PATH)/log/log.c $(UTILS_PATH)/log/log.h + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< + +pack: clean + -rm -f ../src.zip + zip -r ../src.zip * + +clean: + -rm -f $(OBJS) serial.o firewall.o + -rm -f firewall serial diff --git a/content/assignments/parallel-firewall/src/consumer.c b/content/assignments/parallel-firewall/src/consumer.c new file mode 100644 index 0000000000..4c349d271d --- /dev/null +++ b/content/assignments/parallel-firewall/src/consumer.c @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include + +#include "consumer.h" +#include "ring_buffer.h" +#include "packet.h" +#include "utils.h" + +void consumer_thread(so_consumer_ctx_t *ctx) +{ + /* TODO: implement consumer thread */ + (void) ctx; +} + +int create_consumers(pthread_t *tids, + int num_consumers, + struct so_ring_buffer_t *rb, + const char *out_filename) +{ + (void) tids; + (void) num_consumers; + (void) rb; + (void) out_filename; + + for (int i = 0; i < num_consumers; i++) { + /* + * TODO: Launch consumer threads + **/ + } + + return num_consumers; +} diff --git a/content/assignments/parallel-firewall/src/consumer.h b/content/assignments/parallel-firewall/src/consumer.h new file mode 100644 index 0000000000..70a911c05c --- /dev/null +++ b/content/assignments/parallel-firewall/src/consumer.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef __SO_CONSUMER_H__ +#define __SO_CONSUMER_H__ + +#include "ring_buffer.h" +#include "packet.h" + +typedef struct so_consumer_ctx_t { + struct so_ring_buffer_t *producer_rb; + + /* TODO: add synchronization primitives for timestamp ordering */ +} so_consumer_ctx_t; + +int create_consumers(pthread_t *tids, + int num_consumers, + so_ring_buffer_t *rb, + const char *out_filename); + +#endif /* __SO_CONSUMER_H__ */ diff --git a/content/assignments/parallel-firewall/src/firewall.c b/content/assignments/parallel-firewall/src/firewall.c new file mode 100644 index 0000000000..3b4b4a4e81 --- /dev/null +++ b/content/assignments/parallel-firewall/src/firewall.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include + +#include "ring_buffer.h" +#include "consumer.h" +#include "producer.h" +#include "log/log.h" +#include "packet.h" +#include "utils.h" + +#define SO_RING_SZ (PKT_SZ * 1000) + +pthread_mutex_t MUTEX_LOG; + +void log_lock(bool lock, void *udata) +{ + pthread_mutex_t *LOCK = (pthread_mutex_t *) udata; + + if (lock) + pthread_mutex_lock(LOCK); + else + pthread_mutex_unlock(LOCK); +} + +void __attribute__((constructor)) init() +{ + pthread_mutex_init(&MUTEX_LOG, NULL); + log_set_lock(log_lock, &MUTEX_LOG); +} + +void __attribute__((destructor)) dest() +{ + pthread_mutex_destroy(&MUTEX_LOG); +} + +int main(int argc, char **argv) +{ + so_ring_buffer_t ring_buffer; + int num_consumers, threads, rc; + pthread_t *thread_ids = NULL; + + if (argc < 4) { + fprintf(stderr, "Usage %s \n", argv[0]); + exit(EXIT_FAILURE); + } + + rc = ring_buffer_init(&ring_buffer, SO_RING_SZ); + DIE(rc < 0, "ring_buffer_init"); + + num_consumers = strtol(argv[3], NULL, 10); + + if (num_consumers <= 0 || num_consumers > 32) { + fprintf(stderr, "num-consumers [%d] must be in the interval [1-32]\n", num_consumers); + exit(EXIT_FAILURE); + } + + thread_ids = calloc(num_consumers, sizeof(pthread_t)); + DIE(thread_ids == NULL, "calloc pthread_t"); + + /* create consumer threads */ + threads = create_consumers(thread_ids, num_consumers, &ring_buffer, argv[2]); + + /* start publishing data */ + publish_data(&ring_buffer, argv[1]); + + /* TODO: wait for child processes to finish execution*/ + (void) threads; + + free(thread_ids); + + return 0; +} + diff --git a/content/assignments/parallel-firewall/src/packet.c b/content/assignments/parallel-firewall/src/packet.c new file mode 100644 index 0000000000..6d66cc6d75 --- /dev/null +++ b/content/assignments/parallel-firewall/src/packet.c @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include "packet.h" + +#define HASH_ITER 50 + +unsigned long packet_hash(const struct so_packet_t *pkt) +{ + unsigned long hash = 5381; + char *pkt_it; + + for (int iter = 0; iter < HASH_ITER; iter++) { + pkt_it = (char *)pkt; + for (int i = 0; i < PKT_SZ; i++) { + hash = ((hash << 5) + hash) + *pkt_it; + pkt_it++; + } + } + + return hash; +} + +static struct range { + unsigned int start; + unsigned int end; +} allowed_sources_range[] = { + { + .start = 0xf1000000, + .end = 0xf1ffffff, + }, { + .start = 0x1f1f1f1f, + .end = 0x1f1f1f1f, + }, { + .start = 0x80000000, + .end = 0xffffffff, + } +}; + +so_action_t process_packet(const struct so_packet_t *pkt) +{ + const int sz = sizeof(allowed_sources_range) / sizeof(allowed_sources_range[0]); + + for (int i = 0; i < sz; i++) { + if (allowed_sources_range[i].start <= pkt->hdr.source && + pkt->hdr.source <= allowed_sources_range[i].end) + return PASS; + } + + return DROP; +} diff --git a/content/assignments/parallel-firewall/src/packet.h b/content/assignments/parallel-firewall/src/packet.h new file mode 100644 index 0000000000..229f528c72 --- /dev/null +++ b/content/assignments/parallel-firewall/src/packet.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef __SO_PACKET_H__ +#define __SO_PACKET_H__ + +#define PKT_SZ 256 + +typedef enum { + DROP = 0, + PASS = 1, +} so_action_t; + +#define RES_TO_STR(decision) ((decision == PASS) ? "PASS" : "DROP") + +typedef struct __packed so_hdr_t { + unsigned int source; + unsigned int dest; + unsigned long timestamp; +} so_hdr_t; + +typedef struct __packed so_packet_t { + so_hdr_t hdr; + char payload[PKT_SZ - sizeof(so_hdr_t)]; +} so_packet_t; + +unsigned long packet_hash(const so_packet_t *pkt); +so_action_t process_packet(const so_packet_t *pkt); + +#endif /* __SO_PACKET_H__ */ diff --git a/content/assignments/parallel-firewall/src/producer.c b/content/assignments/parallel-firewall/src/producer.c new file mode 100644 index 0000000000..3e8a9bfc67 --- /dev/null +++ b/content/assignments/parallel-firewall/src/producer.c @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include + +#include "ring_buffer.h" +#include "packet.h" +#include "utils.h" +#include "producer.h" + +void publish_data(so_ring_buffer_t *rb, const char *filename) +{ + char buffer[PKT_SZ]; + ssize_t sz; + int fd; + + fd = open(filename, O_RDONLY); + DIE(fd < 0, "open"); + + while ((sz = read(fd, buffer, PKT_SZ)) != 0) { + DIE(sz != PKT_SZ, "packet truncated"); + + /* enequeue packet into ring buffer */ + ring_buffer_enqueue(rb, buffer, sz); + } + + ring_buffer_stop(rb); +} diff --git a/content/assignments/parallel-firewall/src/producer.h b/content/assignments/parallel-firewall/src/producer.h new file mode 100644 index 0000000000..1ee9663927 --- /dev/null +++ b/content/assignments/parallel-firewall/src/producer.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef __SO_PRODUCER_H__ +#define __SO_PRODUCER_H__ + +#include "ring_buffer.h" +#include "packet.h" + +void publish_data(struct so_ring_buffer_t *rb, const char *filename); + +#endif /*__SO_PRODUCER_H__*/ diff --git a/content/assignments/parallel-firewall/src/ring_buffer.c b/content/assignments/parallel-firewall/src/ring_buffer.c new file mode 100644 index 0000000000..f7cb9c2fd1 --- /dev/null +++ b/content/assignments/parallel-firewall/src/ring_buffer.c @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include "ring_buffer.h" + +int ring_buffer_init(so_ring_buffer_t *ring, size_t cap) +{ + /* TODO: implement ring_buffer_init */ + (void) ring; + (void) cap; + + return 1; +} + +ssize_t ring_buffer_enqueue(so_ring_buffer_t *ring, void *data, size_t size) +{ + /* TODO: implement ring_buffer_enqueue */ + (void) ring; + (void) data; + (void) size; + + return -1; +} + +ssize_t ring_buffer_dequeue(so_ring_buffer_t *ring, void *data, size_t size) +{ + /* TODO: Implement ring_buffer_dequeue */ + (void) ring; + (void) data; + (void) size; + + return -1; +} + +void ring_buffer_destroy(so_ring_buffer_t *ring) +{ + /* TODO: Implement ring_buffer_destroy */ + (void) ring; +} + +void ring_buffer_stop(so_ring_buffer_t *ring) +{ + /* TODO: Implement ring_buffer_stop */ + (void) ring; +} diff --git a/content/assignments/parallel-firewall/src/ring_buffer.h b/content/assignments/parallel-firewall/src/ring_buffer.h new file mode 100644 index 0000000000..b3b66afa90 --- /dev/null +++ b/content/assignments/parallel-firewall/src/ring_buffer.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef __SO_RINGBUFFER_H__ +#define __SO_RINGBUFFER_H__ + +#include +#include + +typedef struct so_ring_buffer_t { + char *data; + + size_t read_pos; + size_t write_pos; + + size_t len; + size_t cap; + + /* TODO: Add syncronization primitives */ +} so_ring_buffer_t; + +int ring_buffer_init(so_ring_buffer_t *rb, size_t cap); +ssize_t ring_buffer_enqueue(so_ring_buffer_t *rb, void *data, size_t size); +ssize_t ring_buffer_dequeue(so_ring_buffer_t *rb, void *data, size_t size); +void ring_buffer_destroy(so_ring_buffer_t *rb); +void ring_buffer_stop(so_ring_buffer_t *rb); + +#endif /* __SO_RINGBUFFER_H__ */ diff --git a/content/assignments/parallel-firewall/src/serial.c b/content/assignments/parallel-firewall/src/serial.c new file mode 100644 index 0000000000..9c87ee6516 --- /dev/null +++ b/content/assignments/parallel-firewall/src/serial.c @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include + +#include "consumer.h" +#include "packet.h" +#include "utils.h" + +int main(int argc, char **argv) +{ + char buffer[PKT_SZ], out_buf[PKT_SZ]; + ssize_t sz; + int in_fd, out_fd, len; + + if (argc < 3) { + fprintf(stderr, "Usage %s \n", argv[0]); + exit(EXIT_FAILURE); + } + + in_fd = open(argv[1], O_RDONLY); + DIE(in_fd < 0, "open"); + + out_fd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, 0666); + DIE(out_fd < 0, "open"); + + while ((sz = read(in_fd, buffer, PKT_SZ)) != 0) { + DIE(sz != PKT_SZ, "packet truncated"); + + struct so_packet_t *pkt = (struct so_packet_t *)buffer; + + int action = process_packet(pkt); + unsigned long hash = packet_hash(pkt); + unsigned long timestamp = pkt->hdr.timestamp; + + len = snprintf(out_buf, 256, "%s %016lx %lu\n", + RES_TO_STR(action), hash, timestamp); + write(out_fd, out_buf, len); + } + + close(in_fd); + close(out_fd); + + return 0; +} + diff --git a/content/assignments/parallel-firewall/tests/.gitignore b/content/assignments/parallel-firewall/tests/.gitignore new file mode 100644 index 0000000000..c18dd8d83c --- /dev/null +++ b/content/assignments/parallel-firewall/tests/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/content/assignments/parallel-firewall/tests/Makefile b/content/assignments/parallel-firewall/tests/Makefile new file mode 100644 index 0000000000..9594203177 --- /dev/null +++ b/content/assignments/parallel-firewall/tests/Makefile @@ -0,0 +1,30 @@ +SRC_PATH ?= ../src +UTILS_PATH = $(realpath ../utils) + +.PHONY: all src check lint clean distclean + +all: src + +src: + make -C $(SRC_PATH) UTILS_PATH=$(UTILS_PATH) + +check: clean + make -i SRC_PATH=$(SRC_PATH) + SRC_PATH=$(SRC_PATH) python3 checker.py + +lint: + -cd $(SRC_PATH)/.. && checkpatch.pl -f src/*.c + -cd $(SRC_PATH)/.. && cpplint --recursive src/ + -cd $(SRC_PATH)/.. && shellcheck tests/*.sh + # All tests/*.py are checker related + # -cd $(SRC_PATH)/.. && pylint tests/*.py + +clean: + make -C $(SRC_PATH) clean + -rm -f *~ + -rm -rf __pycache__ + -rm -rf out + +distclean: clean + -rm -rf in + -rm -rf ref diff --git a/content/assignments/parallel-firewall/tests/checker.py b/content/assignments/parallel-firewall/tests/checker.py new file mode 100644 index 0000000000..541b1f85dc --- /dev/null +++ b/content/assignments/parallel-firewall/tests/checker.py @@ -0,0 +1,359 @@ +# SPDX-License-Identifier: BSD-3-Clause + +""" +Checker for the "Parallel Firewall" assignment. + +It walks through the input test files in `in/` and compares the output of the +binary with the appropriate file from `out/` directory. +""" + +import os +import subprocess +import shutil +import time +import signal + +from gen_packets import generate_packets +from typing import List + +src = os.environ.get("SRC_PATH", "../src") +in_dir = os.path.join(os.curdir, "in") +out_dir = os.path.join(os.curdir, "out") +ref_dir = os.path.join(os.curdir, "ref") + +SORT_TIMEOUT = 1 +CMP_TIMEOUT = 1 +SERIAL_TIMEOUT = 5 + +TEST_SIZES = [10, 100, 1_000, 10_000, 20_000] + + +"""Two millisecond for each probe""" +TRACE_LOG_TIMESLICE = 1 / 500 + +TOTAL = 0.0 + +GEN_TEST_ERROR_MSG = """Generating test cases for the checker failed. Please run: +``` +make distclean +``` +Then run the checker again""" + + +def create_dir(dir_path: str): + """Create the directory pointed by `dir_path`. + Remove it before creating if it already exists.""" + shutil.rmtree(dir_path, ignore_errors=True) + + os.mkdir(dir_path) + + +def print_log(text: str, newline=False): + """Print a message in pretty way.""" + if newline: + print("") + + print(30 * "~" + f" {text} ".ljust(50, "~")) + + +def generate_tests(test_sizes) -> bool: + """Generate test cases in ./in and reference files in ./ref""" + print("") + try: + tests = os.listdir(in_dir) + + if len(tests) == len(test_sizes): + print_log("Skipping test generation") + return True + except FileNotFoundError: + pass + + print_log("Generating test cases") + + create_dir(in_dir) + create_dir(ref_dir) + serial_imp = os.path.join(src, "serial") + + # Generate the rest of tests + for test_size in test_sizes: + test_file_in = os.path.join(in_dir, f"test_{test_size:_}.in") + test_file_ref = os.path.join(ref_dir, f"test_{test_size:_}.ref") + + print( + "Generate {} and {}".format(test_file_in.ljust(20), test_file_ref.ljust(21)) + ) + + generate_packets(test_file_in, test_size, seed=test_size) + with subprocess.Popen([serial_imp, test_file_in, test_file_ref]) as serial_proc: + try: + if serial_proc.wait(timeout=SERIAL_TIMEOUT) != 0: + print(GEN_TEST_ERROR_MSG) + return False + except subprocess.TimeoutExpired: + print("Time expired for serial imeplementation !! Killing ...") + serial_proc.kill() + print(GEN_TEST_ERROR_MSG) + return False + + print_log("Tests cases generated") + return True + + +def files_are_identical(out_file: str, ref_file: str, sort_out_file: bool) -> bool: + """Return true if the files are identical with the option of sorting the + output file before comaparing the two. + """ + # Sort output file before comparing + if sort_out_file: + # Sort the file by the numerical value in the third column + with subprocess.Popen( + ["sort", "-n", "-k3", "-o", out_file, out_file] + ) as sort_proc: + try: + sort_proc.wait(timeout=SORT_TIMEOUT) + except subprocess.TimeoutExpired: + sort_proc.kill() + return False + with subprocess.Popen(["cmp", "-s", out_file, ref_file]) as cmp_proc: + try: + return cmp_proc.wait(timeout=CMP_TIMEOUT) == 0 + except subprocess.TimeoutExpired: + cmp_proc.kill() + return False + + return False + + +def file_prefix_identical(out_file_path: str, ref_file_path: str) -> bool: + """Check if the output file is a prefix of the ref file. + + Used to check during the runtime of the firewall if the data is + inserted in a sorted order.""" + with open(out_file_path) as out_file, open(ref_file_path) as ref_file: + for out_line in out_file: + ref_line = ref_file.readline() + if out_line != ref_line: + return False + return True + + +def file_line_count(file_path: str) -> int: + return sum(1 for _ in open(file_path)) + + +def line_count_probe_check(probe: List[int]) -> bool: + double_occurences = 0 + for i in range(1, len(probe)): + if probe[i] == probe[i - 1] and probe[i] != 0 and probe[i] not in TEST_SIZES: + double_occurences += 1 + + return double_occurences / len(probe) <= 0.5 + + +def file_exists_probe_check(probe: List[bool]) -> bool: + return any(probe) + + +def run_once_and_check_output( + binary: str, + in_file_path: str, + out_file_path: str, + ref_file_path: str, + sort_out_file: bool, + threads: int = 1, + test_timeout: float = 1, + trace_logs: bool = False, +) -> bool: + """Run the test once and check the output file with the reference file + + Also, delete the output file before running to ensure all the content + was written during this run.""" + + # Delete output file + try: + os.remove(out_file_path) + except FileNotFoundError: + pass + + with subprocess.Popen( + [binary, in_file_path, out_file_path, f"{threads}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setpgrp, + ) as proc_firewall_res: + try: + """Check periodically if the output file is sorted correctly""" + if trace_logs: + err = "" + line_count_probe, file_exists_probe = [], [] + running_time = 0 + + while running_time < test_timeout and proc_firewall_res.poll() is None: + time.sleep(TRACE_LOG_TIMESLICE) + running_time += TRACE_LOG_TIMESLICE + + file_exists = os.path.isfile(out_file_path) + file_exists_probe.append(file_exists) + if file_exists: + proc_firewall_res.send_signal(signal.SIGSTOP) + + line_count_probe.append(file_line_count(out_file_path)) + if not file_prefix_identical(out_file_path, ref_file_path): + err = ( + f"Log tracing failed: file {ref_file_path} does not" + + f"start with the contents of {out_file_path}." + ) + break + proc_firewall_res.send_signal(signal.SIGCONT) + + if running_time >= test_timeout: + err = "Time expired for process!! Killing ..." + if not line_count_probe_check(line_count_probe): + err = "Line count did not change between two probes too many times" + if not file_exists_probe_check(file_exists_probe): + err = "File was never created during execution" + + if err != "": + print(err) + proc_firewall_res.kill() + return False + + else: + _, _ = proc_firewall_res.communicate(timeout=test_timeout) + except subprocess.TimeoutExpired: + print("Time expired for process!! Killing ...") + proc_firewall_res.kill() + return False + except KeyboardInterrupt: + # Test was CTRL-C'ed + proc_firewall_res.kill() + return False + + return files_are_identical(out_file_path, ref_file_path, sort_out_file) + + +def check( + test_name: str, + sort_out_file: bool, + threads: int, + test_timeout: float = 1, + number_of_runs: int = 1, + trace_logs: bool = False, +): + """Check a test file. + + Pass test filenames `firewall` + """ + # Format output file name. + in_file_name = f"{test_name}.in" + in_file_path = os.path.join("in", in_file_name) + + # Format output file name. + out_file_name = f"{test_name}.out" + out_file_path = os.path.join("out", out_file_name) + + # Format the ref file name + ref_file_name = f"{test_name}.ref" + ref_file_path = os.path.join("ref", ref_file_name) + + firewall_path = os.path.join(src, "firewall") + + # When checking single threader version, there's no need to run the program + # multiple times, as there **should not** be any variance. + # And if there is, it'll get caught during parallel testing. + if threads == 1: + number_of_runs = 1 + + # Running `number_of_runs` times and check the output every time. + # We do this in order to check the program output is consistent. + for _ in range(number_of_runs): + if not run_once_and_check_output( + firewall_path, + in_file_path, + out_file_path, + ref_file_path, + True, + threads=threads, + test_timeout=test_timeout, + ): + return False + + if trace_logs: + if not run_once_and_check_output( + firewall_path, + in_file_path, + out_file_path, + ref_file_path, + sort_out_file, + threads=threads, + test_timeout=test_timeout, + trace_logs=trace_logs, + ): + return False + return True + + +def check_and_grade( + test_size: int, + sort_out_file: bool = False, + threads: int = 1, + test_timeout: int = 2, + points: float = 5.0, + trace_logs: bool = False, +): + """Check and grade a single test case using test size and number of threads""" + + global TOTAL + test_name = f"test_{test_size:_}" + result_format = "Test [{} packets, sort {}, {} thread{}]".format( + f"{test_size:,}".rjust(6), + f"{sort_out_file}".ljust(len("False")), + threads, + "s" if threads > 1 else " ", + ) + result_format += " " + 22 * "." + " " + + if check(test_name, sort_out_file, threads, test_timeout, 20, trace_logs): + print(result_format + "passed ... {}".format(points)) + TOTAL += points + else: + print(result_format + "failed ... 0") + + +def main(): + global TOTAL + if not generate_tests(TEST_SIZES): + # Something bad happened, can't run checker, try again + return + + create_dir(out_dir) + + print_log("Running tests ...", newline=True) + + # Test out serial implementation: 10 points. + check_and_grade(TEST_SIZES[0], threads=1, points=3) + check_and_grade(TEST_SIZES[2], threads=1, points=3) + check_and_grade(TEST_SIZES[4], threads=1, points=4) + + # Test out parallel implementation, but without the restriction of having + # correctly sorted output: 50 points (2 x 5 x 5). + for test_size in TEST_SIZES: + check_and_grade(test_size, threads=2, sort_out_file=True, points=5) + check_and_grade(test_size, threads=4, sort_out_file=True, points=5) + + # Test out parallel implementation, this time with the restriction of having + # correctly sored output: 30 points (5 x 6) + for test_size in TEST_SIZES[2:]: + check_and_grade( + test_size, threads=4, sort_out_file=False, points=5, trace_logs=True + ) + check_and_grade( + test_size, threads=8, sort_out_file=False, points=5, trace_logs=True + ) + + TOTAL = int(TOTAL) + print("\nTotal:" + 67 * " " + f" {TOTAL}/100") + + +if __name__ == "__main__": + main() diff --git a/content/assignments/parallel-firewall/tests/gen_packets.py b/content/assignments/parallel-firewall/tests/gen_packets.py new file mode 100755 index 0000000000..78d8044e13 --- /dev/null +++ b/content/assignments/parallel-firewall/tests/gen_packets.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: BSD-3-Clause + +import struct +import random +import time +import argparse +import os + +PKT_SZ = 256 + + +class SoPacket: + prev_timestamp = 0 + + def __init__(self): + self.source = random.randint(0, 2**32 - 1) + self.dest = random.randint(0, 2**32 - 1) + # Add between 3 and 10 units to the previous timestamp + self.timestamp = SoPacket.prev_timestamp + SoPacket.prev_timestamp += random.randint(3, 10) + + payload_size = PKT_SZ - struct.calcsize("IIQ") + self.payload = os.urandom(payload_size) + + def to_bytes(self): + header = struct.pack("IIQ", self.source, self.dest, self.timestamp) + return header + self.payload + + @classmethod + def from_bytes(cls, data): + packet = cls() + packet.source, packet.dest, packet.timestamp = struct.unpack("IIQ", data[:16]) + packet.payload = data[16:] + return packet + + @classmethod + def reset_timestamp(cls): + cls.prev_timestamp = 0 + + +def generate_packets(filename, count, seed): + # Possibly set the seed for each file + if seed != 0: + random.seed(seed) + + SoPacket.reset_timestamp() + with open(filename, "wb") as f: + for _ in range(count): + packet = SoPacket() + f.write(packet.to_bytes()) + + +def display_packet(filename, index): + with open(filename, "rb") as f: + f.seek(index * PKT_SZ) + data = f.read(PKT_SZ) + if not data: + print(f"Packet {index} not found in file.") + return + packet = SoPacket.from_bytes(data) + print(f"Packet {index}:") + print(f"Source: {packet.source}") + print(f"Destination: {packet.dest}") + print(f"Timestamp: {packet.timestamp}") + print(f"Payload (first 20 bytes): {packet.payload[:20].hex()}") + + +def main(): + parser = argparse.ArgumentParser(description="Generate so_packets.") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + gen_parser = subparsers.add_parser("generate", help="Generate random packets") + gen_parser.add_argument("filename", help="Output file name") + gen_parser.add_argument("count", type=int, help="Number of packets to generate") + + display_parser = subparsers.add_parser("display", help="Display a specific packet") + display_parser.add_argument("filename", help="Input file name") + display_parser.add_argument( + "index", type=int, help="Index of the packet to display (0 indexing)" + ) + + args = parser.parse_args() + + if args.command == "generate": + generate_packets(args.filename, args.count, 0) + elif args.command == "display": + display_packet(args.filename, args.index) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/content/assignments/parallel-firewall/tests/grade.sh b/content/assignments/parallel-firewall/tests/grade.sh new file mode 100755 index 0000000000..e71c0b2788 --- /dev/null +++ b/content/assignments/parallel-firewall/tests/grade.sh @@ -0,0 +1,136 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause + +# Grade style based on build warnings and linter warnings / errors. +# Points are subtracted from the maximum amount of style points (10). +# - For 15 or more build warnings, all points (10) are subtracted. +# - For [10,15) build warnings, 6 points are subtracted. +# - For [5,10) build warnings, 4 points are subtracted. +# - For [1,5) build warnings, 2 points are subtracted. +# - For 25 ore more linter warnings / errors, all points (10) are subtracted. +# - For [15,25) linter warnings / errors, 6 points are subtracted. +# - For [7,15) linter warnings / errors, 4 points are subtracted. +# - For [1,7) linter warnings / errors, 2 points are subtracted. +# Final style points are between 0 and 10. Results cannot be negative. +# +# Result (grade) is stored in style_grade.out file. +# Collect summary in style_summary.out file. + +function grade_style() +{ + compiler_warn=$(< checker.out grep -v 'unused parameter' | grep -v 'unused variable' | \ + grep -v "discards 'const'" | grep -c '[0-9]\+:[0-9]\+: warning:') + + compiler_down=0 + if test "$compiler_warn" -ge 15; then + compiler_down=10 + elif test "$compiler_warn" -ge 10; then + compiler_down=6 + elif test "$compiler_warn" -ge 5; then + compiler_down=4 + elif test "$compiler_warn" -ge 1; then + compiler_down=2 + fi + + cpplint=$(< linter.out grep "Total errors found:" | rev | cut -d ' ' -f 1 | rev) + checkpatch_err=$(< linter.out grep 'total: [0-9]* errors' | grep -o '[0-9]* errors,' | \ + cut -d ' ' -f 1 | paste -s -d '+' | bc) + checkpatch_warn=$(< linter.out grep 'total: [0-9]* errors' | grep -o '[0-9]* warnings,' | \ + cut -d ' ' -f 1 | paste -s -d '+' | bc) + if test -z "$checkpatch_err"; then + checkpatch_err=0 + fi + if test -z "$checkpatch_warn"; then + checkpatch_warn=0 + fi + checkpatch=$((checkpatch_err + checkpatch_warn)) + checker_all=$((cpplint + checkpatch)) + + checker_down=0 + if test "$checker_all" -ge 25; then + checker_down=10 + elif test "$checker_all" -ge 15; then + checker_down=6 + elif test "$checker_all" -ge 7; then + checker_down=4 + elif test "$checker_all" -ge 1; then + checker_down=2 + fi + + full_down=$((compiler_down + checker_down)) + + if test "$full_down" -gt 10; then + full_down=10 + fi + style_grade=$((10 - full_down)) + + echo "$style_grade" > style_grade.out + + { + < linter.out grep -v 'unused parameter' | grep -v 'unused variable' | grep -v "discards 'const'" | \ + grep '[0-9]\+:[0-9]\+: warning:' + < linter.out grep "Total errors found: [1-9]" + < linter.out grep 'total: [1-9]* errors' + < linter.out grep 'total: 0 errors' | grep '[1-9][0-9]* warnings' + } > style_summary.out +} + +# Print grades: total, checker and style. +# Style grade is only awarded for assignments that have past 60 points +# of th checker grade. +print_results() +{ + checker_grade=$(< checker.out sed -n '/^Checker:/s/^.*[ \t]\+\([0-9\.]\+\)\/.*$/\1/p') + if test "$(echo "$checker_grade > 60" | bc)" -eq 1; then + style_grade=$(cat style_grade.out) + else + style_grade=0 + fi + final_grade=$(echo "scale=2; $checker_grade+$style_grade" | bc) + echo -e "\n\n### GRADE\n\n" + printf "Checker: %58s/ 90\n" "$checker_grade" + printf "Style: %60s/ 10\n" "$style_grade" + printf "Total: %60s/100\n" "$final_grade" + + echo -e "\n\n### STYLE SUMMARY\n\n" + cat style_summary.out +} + +run_interactive() +{ + echo -e "\n\n### CHECKER\n\n" + stdbuf -oL make check 2>&1 | stdbuf -oL sed 's/^Total:/Checker:/g' | tee checker.out + + echo -e "\n\n### LINTER\n\n" + stdbuf -oL make lint 2>&1 | tee linter.out + + grade_style + print_results +} + +run_non_interactive() +{ + make check 2>&1 | sed 's/^Total:/Checker:/g' > checker.out + make lint > linter.out 2>&1 + + grade_style + print_results + + echo -e "\n\n### CHECKER\n\n" + cat checker.out + + echo -e "\n\n### LINTER\n\n" + cat linter.out +} + +# In case of a command line argument disable interactive output. +# That is, do not show output as it generated. +# This is useful to collect all output and present final results at the +# beginning of the script output. +# This is because Moodle limits the output results, and the final results +# would otherwise not show up. +if test $# -eq 0; then + run_interactive +else + run_non_interactive +fi diff --git a/content/assignments/parallel-firewall/utils/log/CPPLINT.cfg b/content/assignments/parallel-firewall/utils/log/CPPLINT.cfg new file mode 100644 index 0000000000..5aa9cb376c --- /dev/null +++ b/content/assignments/parallel-firewall/utils/log/CPPLINT.cfg @@ -0,0 +1 @@ +exclude_files=log\.c diff --git a/content/assignments/parallel-firewall/utils/log/log.c b/content/assignments/parallel-firewall/utils/log/log.c new file mode 100644 index 0000000000..ac65f4ed33 --- /dev/null +++ b/content/assignments/parallel-firewall/utils/log/log.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: BSD-3-Clause + +/* + * Copyright (c) 2020 rxi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* Github link: https://github.com/rxi/log.c */ + +#include "log.h" + +#define MAX_CALLBACKS 32 + +typedef struct { + log_LogFn fn; + void *udata; + int level; +} Callback; + +static struct +{ + void *udata; + log_LockFn lock; + int level; + bool quiet; + Callback callbacks[MAX_CALLBACKS]; +} L; + +static const char * const level_strings[] = { "TRACE", "DEBUG", "INFO", + "WARN", "ERROR", "FATAL" }; + +#ifdef LOG_USE_COLOR +static const char * const level_colors[] = { "\x1b[94m", "\x1b[36m", "\x1b[32m", + "\x1b[33m", "\x1b[31m", "\x1b[35m" }; +#endif + +static void +stdout_callback(log_Event *ev) +{ + char buf[16]; + + buf[strftime(buf, sizeof(buf), "%H:%M:%S", ev->time)] = '\0'; +#ifdef LOG_USE_COLOR + fprintf(ev->udata, + "%s %s%-5s\x1b[0m \x1b[90m%s:%d:\x1b[0m ", + buf, + level_colors[ev->level], + level_strings[ev->level], + ev->file, + ev->line); +#else + fprintf(ev->udata, + "%s %-5s %s:%d: ", + buf, + level_strings[ev->level], + ev->file, + ev->line); +#endif + vfprintf(ev->udata, ev->fmt, ev->ap); + fprintf(ev->udata, "\n"); + fflush(ev->udata); +} + +static void +file_callback(log_Event *ev) +{ + char buf[64]; + + buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0'; + fprintf(ev->udata, + "%s %-5s %s:%d: ", + buf, + level_strings[ev->level], + ev->file, + ev->line); + vfprintf(ev->udata, ev->fmt, ev->ap); + fprintf(ev->udata, "\n"); + fflush(ev->udata); +} + +static void +lock(void) +{ + if (L.lock) + L.lock(true, L.udata); +} + +static void +unlock(void) +{ + if (L.lock) + L.lock(false, L.udata); +} + +const char* +log_level_string(int level) +{ + return level_strings[level]; +} + +void +log_set_lock(log_LockFn fn, void *udata) +{ + L.lock = fn; + L.udata = udata; +} + +void +log_set_level(int level) +{ + L.level = level; +} + +void +log_set_quiet(bool enable) +{ + L.quiet = enable; +} + +int +log_add_callback(log_LogFn fn, void *udata, int level) +{ + for (int i = 0; i < MAX_CALLBACKS; i++) { + if (!L.callbacks[i].fn) { + L.callbacks[i] = (Callback) { fn, udata, level }; + return 0; + } + } + return -1; +} + +int +log_add_fp(FILE *fp, int level) +{ + return log_add_callback(file_callback, fp, level); +} + +static void +init_event(log_Event *ev, void *udata) +{ + if (!ev->time) { + time_t t = time(NULL); + + ev->time = localtime(&t); + } + ev->udata = udata; +} + +void +log_log(int level, const char *file, int line, const char *fmt, ...) +{ + log_Event ev = { + .fmt = fmt, + .file = file, + .line = line, + .level = level, + }; + + lock(); + + if (!L.quiet && level >= L.level) { + init_event(&ev, stderr); + va_start(ev.ap, fmt); + stdout_callback(&ev); + va_end(ev.ap); + } + + for (int i = 0; i < MAX_CALLBACKS && L.callbacks[i].fn; i++) { + Callback *cb = &L.callbacks[i]; + + if (level >= cb->level) { + init_event(&ev, cb->udata); + va_start(ev.ap, fmt); + cb->fn(&ev); + va_end(ev.ap); + } + } + + unlock(); +} diff --git a/content/assignments/parallel-firewall/utils/log/log.h b/content/assignments/parallel-firewall/utils/log/log.h new file mode 100644 index 0000000000..c8d1dee06a --- /dev/null +++ b/content/assignments/parallel-firewall/utils/log/log.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +/** + * Copyright (c) 2020 rxi + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MIT license. See `log.c` for details. + */ + +/* Github link: https://github.com/rxi/log.c */ + +#ifndef LOG_H +#define LOG_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define LOG_VERSION "0.1.0" + + typedef struct { + va_list ap; + const char *fmt; + const char *file; + struct tm *time; + void *udata; + int line; + int level; + } log_Event; + + typedef void (*log_LogFn)(log_Event *ev); + typedef void (*log_LockFn)(bool lock, void *udata); + + enum { + LOG_TRACE, + LOG_DEBUG, + LOG_INFO, + LOG_WARN, + LOG_ERROR, + LOG_FATAL + }; + +#define log_trace(...) log_log(LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__) +#define log_debug(...) log_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__) +#define log_info(...) log_log(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__) +#define log_warn(...) log_log(LOG_WARN, __FILE__, __LINE__, __VA_ARGS__) +#define log_error(...) log_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__) +#define log_fatal(...) log_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__) + + const char *log_level_string(int level); + void log_set_lock(log_LockFn fn, void *udata); + void log_set_level(int level); + void log_set_quiet(bool enable); + int log_add_callback(log_LogFn fn, void *udata, int level); + int log_add_fp(FILE *fp, int level); + + void log_log(int level, const char *file, int line, const char *fmt, ...); + +#ifdef __cplusplus +} +#endif + +#endif /* LOG_H */ diff --git a/content/assignments/parallel-firewall/utils/utils.h b/content/assignments/parallel-firewall/utils/utils.h new file mode 100644 index 0000000000..05fff782fa --- /dev/null +++ b/content/assignments/parallel-firewall/utils/utils.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef UTILS_H_ +#define UTILS_H_ 1 + +#include +#include +#include +#include + +#include "log/log.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ERR(assertion, call_description) \ + do { \ + if (assertion) \ + log_error("%s: %s", \ + call_description, strerror(errno)); \ + } while (0) + +#define DIE(assertion, call_description) \ + do { \ + if (assertion) { \ + log_fatal("%s: %s", \ + call_description, strerror(errno)); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +#ifdef __cplusplus +} +#endif + +#endif /* UTILS_H_ */ +