commit 96f8ff2e9872050a9c9c27bd0167c1a6f5bd2f89 Author: Mikael Lövqvist Date: Sat Feb 14 02:00:29 2026 +0100 Initial commit diff --git a/fanotify-projekt.service b/fanotify-projekt.service new file mode 100644 index 0000000..8197af6 --- /dev/null +++ b/fanotify-projekt.service @@ -0,0 +1,18 @@ +[Unit] +Description=Fanotify → Tag Event → Message Hub +Requires=message-hub.service +After=message-hub.service +RequiresMountsFor=/srv/Projekt + +[Service] +Type=simple +User=messagehub +Group=messagehub +WorkingDirectory=/srv/Projekt/message-hub/system2 +ExecStart=/bin/sh -c '/srv/Projekt/fanotify-logger/fa2json /srv/Projekt | tee -a /srv/project-fs.log | node tag-event.mjs' +AmbientCapabilities=CAP_SYS_ADMIN CAP_DAC_READ_SEARCH +NoNewPrivileges=yes +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/fs-watcher.c b/fs-watcher.c new file mode 100644 index 0000000..726ce7d --- /dev/null +++ b/fs-watcher.c @@ -0,0 +1,169 @@ +#define _GNU_SOURCE /* Needed to get O_LARGEFILE definition */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//json-writer.c prototypes +void json_write_filename_bytes(const char *input, size_t length, FILE *out); + +size_t get_parent_dir(char* path, int mount_fd, struct file_handle* fh) { + int parent_fd = open_by_handle_at(mount_fd, fh, O_PATH | O_DIRECTORY); + + if (parent_fd == -1) { + if (errno == ESTALE) { + fprintf(stderr, "File handle is no longer valid. File has been deleted\n"); + return 0; + } else { + perror("open_by_handle_at"); + exit(EXIT_FAILURE); + } + } + + char procfd_path[PATH_MAX]; + snprintf(procfd_path, sizeof(procfd_path), "/proc/self/fd/%d", parent_fd); + + size_t path_len = readlink(procfd_path, path, PATH_MAX); + close(parent_fd); + return path_len; +} + + + +static void handle_events(int fafd, int mount_fd) { + struct fanotify_event_metadata buf[200]; + while (true) { + ssize_t size = read(fafd, buf, sizeof(buf)); + if (size == -1 && errno != EAGAIN) { + perror("read"); + exit(EXIT_FAILURE); + } + + if (size > 0) { + const struct fanotify_event_metadata* metadata = buf; + + while (FAN_EVENT_OK(metadata, size)) { + if (metadata->vers != FANOTIFY_METADATA_VERSION) { + fprintf(stderr, "Mismatch of fanotify metadata version.\n"); + exit(EXIT_FAILURE); + } + + struct timespec mono, wall; + clock_gettime(CLOCK_MONOTONIC, &mono); + clock_gettime(CLOCK_REALTIME, &wall); + + fprintf(stdout, "{\"ts\": [%i, %i, %i, %i]", wall.tv_sec, wall.tv_nsec, mono.tv_sec, mono.tv_nsec); + + char *ptr = (char *)(metadata + 1); + char *end = (char *)metadata + metadata->event_len; + int entry_index = 0; + while (ptr < end) { + struct fanotify_event_info_header *hdr = (struct fanotify_event_info_header *)ptr; + + if (hdr->info_type == FAN_EVENT_INFO_TYPE_DFID_NAME || + hdr->info_type == FAN_EVENT_INFO_TYPE_OLD_DFID_NAME || + hdr->info_type == FAN_EVENT_INFO_TYPE_NEW_DFID_NAME) { + + struct fanotify_event_info_fid *fid = (struct fanotify_event_info_fid *)hdr; + struct file_handle *fh = (struct file_handle *)fid->handle; + char parent_path[PATH_MAX]; + size_t parent_path_length = get_parent_dir(parent_path, mount_fd, fh); + char *name = (char *)fh->f_handle + fh->handle_bytes; + + if (hdr->info_type == FAN_EVENT_INFO_TYPE_OLD_DFID_NAME) { + fprintf(stdout, ", \"old\": "); + } else if (hdr->info_type == FAN_EVENT_INFO_TYPE_NEW_DFID_NAME) { + fprintf(stdout, ", \"new\": "); + } else { + fprintf(stdout, ", \"name\": "); + } + + fputc('"', stdout); + json_write_filename_bytes(parent_path, parent_path_length, stdout); + fputc('/', stdout); + json_write_filename_bytes(name, strlen(name), stdout); + fputc('"', stdout); + + } + + + + ptr += hdr->len; + } + + if (entry_index++) { fprintf(stdout, ", "); } + fprintf(stdout, ", \"mask\": %i}\n", metadata->mask); + + metadata = FAN_EVENT_NEXT(metadata, size); + + } + } else { + break; + } + + } + +} + +int main(int argc, char *argv[]) { + + // Acquire arguments + if (argc != 2) { + fprintf(stderr, "Usage: %s MOUNT\n", argv[0]); + exit(EXIT_FAILURE); + } + char* mount_point = argv[1]; + + // Open mount point for reading + int mount_fd = open(mount_point, O_DIRECTORY | O_RDONLY); + if (mount_fd == -1) { + perror(mount_point); + exit(EXIT_FAILURE); + } + + // Create file descriptor for fanotify API + int fafd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_NOTIF | FAN_NONBLOCK | FAN_REPORT_FID | FAN_REPORT_DFID_NAME, O_RDONLY | O_LARGEFILE); + if (fafd == -1) { + perror("fanotify_init"); + exit(EXIT_FAILURE); + } + + // Mark events for logging + if (fanotify_mark(fafd, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, FAN_ATTRIB | FAN_RENAME | FAN_CREATE | FAN_DELETE | FAN_CLOSE_WRITE | FAN_ONDIR, AT_FDCWD, mount_point) == -1) { + perror("fanotify_mark"); + exit(EXIT_FAILURE); + } + + // Setup file descriptors for epoll + #define N_POLL_FDS 1 + struct pollfd poll_fds[N_POLL_FDS] = { + {.fd = fafd, .events = POLLIN}, + }; + + fprintf(stderr, "Listening for events.\n"); + fprintf(stderr, "Issue interrupt signal to terminate.\n"); + + while (1) { + int poll_num = poll(poll_fds, N_POLL_FDS, -1); + if (poll_num == -1) { + if (errno == EINTR) // Interrupted by signal + continue; + perror("poll"); // Unexpected error + exit(EXIT_FAILURE); + } + if (poll_num >= 0) { + if (poll_fds[0].revents & POLLIN) { // fanotify events available + handle_events(fafd, mount_fd); + fflush(stdout); + } + } + } + fprintf(stderr, "Listening for events stopped.\n"); + exit(EXIT_SUCCESS); +} diff --git a/json-writer.c b/json-writer.c new file mode 100644 index 0000000..68b6083 --- /dev/null +++ b/json-writer.c @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include + + +//TODO - move to header +size_t json_compute_unicode(const char* input); +char* json_instance_unicode(const char* input); +void json_write_unicode(const char* input, FILE *out); +int json_write_unicode_to_buffer(const char* input, char* buffer, size_t buffer_size); + +char* json_instance_unicode(const char* input) { + size_t req = json_compute_unicode(input) + 1; // +1 for sentinel + char* result = calloc(1, req); + json_write_unicode_to_buffer(input, result, req); //Possibly return null if req != written but for now we just ignore + return result; +} + + +size_t json_compute_unicode(const char* input) { + setlocale(LC_CTYPE, ""); // Enable locale-aware decoding + size_t result = 2; + + const char *p = input; + mbstate_t st; + memset(&st, 0, sizeof(st)); + + while (*p) { + wchar_t wc; + size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &st); + + if (len == (size_t)-1 || len == (size_t)-2) { + // Invalid UTF-8 sequence — fallback to hex escape + result += 4; + p++; + memset(&st, 0, sizeof(st)); + continue; + } + + if (wc == L'"') result += 2; + else if (wc == L'\\') result += 2; + else if (wc == L'\b') result += 2; + else if (wc == L'\f') result += 2; + else if (wc == L'\n') result += 2; + else if (wc == L'\r') result += 2; + else if (wc == L'\t') result += 2; + else if (wc < 0x20) { + // Control characters: output as \u0000..\u001F + result += 6; + } else { + // Write UTF-8 bytes directly + result += len; + } + + p += len; + } + return result; + +} + +int json_write_unicode_to_buffer(const char* input, char* buffer, size_t buffer_size) { + setlocale(LC_CTYPE, ""); // Enable locale-aware decoding + + const char *p = input; + mbstate_t st; + memset(&st, 0, sizeof(st)); + size_t written = 0; + + // Open quote + written += snprintf(buffer + written, buffer_size - written, "\""); + + while (*p) { + wchar_t wc; + size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &st); + + if (len == (size_t)-1 || len == (size_t)-2) { + // Invalid UTF-8 sequence — fallback to hex escape + written += snprintf(buffer + written, buffer_size - written, "\\u00%02X", (unsigned char)*p); + p++; + memset(&st, 0, sizeof(st)); + continue; + } + + if (wc == L'"') written += snprintf(buffer + written, buffer_size - written, "\\\""); + else if (wc == L'\\') written += snprintf(buffer + written, buffer_size - written, "\\\\"); + else if (wc == L'\b') written += snprintf(buffer + written, buffer_size - written, "\\b"); + else if (wc == L'\f') written += snprintf(buffer + written, buffer_size - written, "\\f"); + else if (wc == L'\n') written += snprintf(buffer + written, buffer_size - written, "\\n"); + else if (wc == L'\r') written += snprintf(buffer + written, buffer_size - written, "\\r"); + else if (wc == L'\t') written += snprintf(buffer + written, buffer_size - written, "\\t"); + else if (wc < 0x20) { + // Control characters: output as \u0000..\u001F + written += snprintf(buffer + written, buffer_size - written, "\\u00%02X", wc); + } else { + // Write UTF-8 bytes directly + written += snprintf(buffer + written, buffer_size - written, "%.*s", (int)len, p); + } + + p += len; + } + + // Close quote + written += snprintf(buffer + written, buffer_size - written, "\""); + return written; +} + + +void json_write_unicode(const char* input, FILE *out) { + setlocale(LC_CTYPE, ""); // Enable locale-aware decoding + + const char *p = input; + mbstate_t st; + memset(&st, 0, sizeof(st)); + + fputc('"', out); // Open quote + + while (*p) { + wchar_t wc; + size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &st); + + if (len == (size_t)-1 || len == (size_t)-2) { + // Invalid UTF-8 sequence — fallback to hex escape + fprintf(out, "\\u00%02X", (unsigned char)*p); + p++; + memset(&st, 0, sizeof(st)); + continue; + } + + if (wc == L'"') fputs("\\\"", out); + else if (wc == L'\\') fputs("\\\\", out); + else if (wc == L'\b') fputs("\\b", out); + else if (wc == L'\f') fputs("\\f", out); + else if (wc == L'\n') fputs("\\n", out); + else if (wc == L'\r') fputs("\\r", out); + else if (wc == L'\t') fputs("\\t", out); + else if (wc < 0x20) { + // Control characters: output as \u0000..\u001F + fprintf(out, "\\u00%02X", wc); + } else { + // Write UTF-8 bytes directly + fwrite(p, 1, len, out); + } + + p += len; + } + + fputc('"', out); // Close quote +} + + + + +void json_write_filename_bytes(const char *input, size_t length, FILE *out) { + for (size_t i = 0; i < length; i++) { + unsigned char c = (unsigned char)input[i]; + + switch (c) { + case '"': fputs("\\\"", out); break; + case '\\': fputs("\\\\", out); break; + case '\b': fputs("\\b", out); break; + case '\f': fputs("\\f", out); break; + case '\n': fputs("\\n", out); break; + case '\r': fputs("\\r", out); break; + case '\t': fputs("\\t", out); break; + + default: + if (c < 0x20 || c >= 0x80) { + // surrogateescape encoding + fprintf(out, "\\uDC%02X", c); + } else { + fputc(c, out); + } + } + } +}