diff --git a/.gitignore b/.gitignore index 378eac2..fb2e6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build +.conf diff --git a/Makefile b/Makefile index 1c11da9..8d1eb6d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ CC = gcc CFLAGS = -Wall -I. +LDFLAGS = -lconfig all: client server @@ -9,7 +10,7 @@ client: server: mkdir -p build - $(CC) $(CFLAGS) -o build/server server.c util/util.c + $(CC) $(CFLAGS) -o build/server server.c util/util.c $(LDFLAGS) clean: rm -f client server diff --git a/client.c b/client.c index d2b6780..81a93e6 100644 --- a/client.c +++ b/client.c @@ -2,12 +2,21 @@ #include #include #include +#include #include #include #include #include #include "commands.h" +#include "signal.h" + +// Wanted a graceful exit so need to keep this in global state +static int client_queue_id_global = -1; +static int server_queue_id_global = -1; +static char client_id_global[COMMAND_LENGTH]; +static pid_t heartbeat_pid = -1; +static pid_t input_pid = -1; // Make a macro for this cause I'm not typing sizeof twice every time #define send(queue_id, msgbuf_ptr) msgsnd(queue_id, msgbuf_ptr, sizeof(*(msgbuf_ptr)) - sizeof(long), 0) @@ -30,6 +39,27 @@ static void cleanup_queue(int qid) { } } +void cleanup_and_exit(int sig) { + printf("\nLogging out... "); + + if (server_queue_id_global != -1) { + msgbuf_t logout = {.mtype = Logout, .sender = ""}; + strncpy(logout.sender, client_id_global, COMMAND_LENGTH - 1); + msgsnd(server_queue_id_global, &logout, sizeof(logout) - sizeof(long), 0); + } + + // Burn the orphanage + if (heartbeat_pid > 0) + kill(heartbeat_pid, SIGTERM); + if (input_pid > 0) + kill(input_pid, SIGTERM); + + cleanup_queue(client_queue_id_global); + + printf("Bye!\n"); + exit(0); +} + static void input_loop(int server_queue_id, const char *client_id) { while (1) { // Minimum length plus extra space fore command name and seperators @@ -88,9 +118,8 @@ static void input_loop(int server_queue_id, const char *client_id) { } } else if (strncmp(buffer, "/mute ", 6) == 0) { // handle mute - } else if (strcmp(buffer, "/quit") == 0) { - // TODO: Would be nice to have a way to gracefully logout and not just - // depend on heartbeats, maybe + } else if (strcmp(buffer, "/quit") == 0 || strcmp(buffer, "/exit") == 0 || strcmp(buffer, "/q") == 0) { + return; } else if (strncmp(buffer, "/inbox", 8) == 0) { msgbuf_t msg = {.mtype = Inbox, .sender = ""}; strncpy(msg.sender, client_id, COMMAND_LENGTH - 1); @@ -163,16 +192,29 @@ int main(int argc, char *argv[]) { printf("Login accepted. Welcome, %s!\n", resp.command); - if (fork() == 0) { + strncpy(client_id_global, client_id, COMMAND_LENGTH - 1); + client_queue_id_global = client_queue_id; + server_queue_id_global = server_queue_id; + + + heartbeat_pid = fork(); + if (heartbeat_pid == 0) { heartbeat_loop(server_queue_id, client_id); return 0; } - if (fork() == 0) { + input_pid = fork(); + if (input_pid == 0) { input_loop(server_queue_id, client_id); + // The /quit command just returns from the input loop + kill(getppid(), SIGTERM); return 0; } + // Setup them below the forks to avoid multiple triggers + signal(SIGINT, cleanup_and_exit); + signal(SIGTERM, cleanup_and_exit); + while (1) { msgbuf_t incoming; ssize_t got = msgrcv(client_queue_id, &incoming, sizeof(incoming) - sizeof(long), 0, 0); @@ -194,7 +236,7 @@ int main(int argc, char *argv[]) { // Check the command field for the list type if (strncmp(incoming.command, "active", COMMAND_LENGTH) == 0) { printf("Active clients:\n%s\n", incoming.message); - } else if (strncmp(incoming.command, "group", COMMAND_LENGTH) == 0) { + } else if (strncmp(incoming.command, "groups", COMMAND_LENGTH) == 0) { printf("All group members:\n%s\n", incoming.message); } else { printf("All clients:\n%s\n", incoming.message); diff --git a/example.conf b/example.conf new file mode 100644 index 0000000..fff39a2 --- /dev/null +++ b/example.conf @@ -0,0 +1,27 @@ +// Server configuration file + +clients = ( + { + client_id = "test1"; + nickname = "Kregielnia"; + }, + { + client_id = "test2"; + nickname = "Bajzel"; + } +); + +groups = ( + { + name = "HelloChat"; + members = ["test1", "test2"]; + }, + { + name = "Macarena"; + members = []; + }, + { + name = "Bananownia"; + members = ["test2"]; + } +); diff --git a/server.c b/server.c index cccbc67..c11263d 100644 --- a/server.c +++ b/server.c @@ -12,7 +12,7 @@ #include #include -#include "commands.h" +// commands.h already is included by util.h #include "util/util.h" #define send(queue_id, msgbuf_ptr) msgsnd(queue_id, msgbuf_ptr, sizeof(*(msgbuf_ptr)) - sizeof(long), 0) @@ -68,32 +68,57 @@ static void handle_hearbeat_timeouts(Client_t *clients, int client_count, int se } int main(int argc, char **argv) { - // TODO: Load config file with clients and groups - int client_count = 2; - int group_count = 3; + server_config_t config; + // If we are given a path, try to load the config from there + if (argc >= 2) { + load_config(&config, argv[1]); + } else { + fprintf(stderr, "No config file provided, using default location example.conf\n"); + load_config(&config, "./example.conf"); + } + + int client_count = config.client_count; + int group_count = config.group_count; int clients_memory_id = shmget(IPC_PRIVATE, sizeof(Client_t) * client_count, IPC_CREAT | 0666); Client_t *clients = shmat(clients_memory_id, NULL, 0); - Group_t *groups = malloc(sizeof(Group_t) * 3); + Group_t *groups = malloc(sizeof(Group_t) * group_count); int semaphore_id = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666); semctl(semaphore_id, 0, SETVAL, 1); - clients[0] = - (Client_t){.id = "test1", .logged_in = false, .muted_clients = NULL, .nickname = "Kregielnia", .last_seen = 0}; - clients[1] = - (Client_t){.id = "test2", .logged_in = false, .muted_clients = NULL, .nickname = "Bajzel", .last_seen = 0}; + for (int i = 0; i < client_count; i++) { + strncpy(clients[i].id, config.clients[i].client_id, COMMAND_LENGTH - 1); + clients[i].logged_in = false; + clients[i].muted_clients = NULL; + strncpy(clients[i].nickname, config.clients[i].nickname, COMMAND_LENGTH - 1); + clients[i].queue_id = -1; + clients[i].last_seen = 0; + clients[i].saved_message_count = 0; + } - groups[0] = (Group_t){.name = "HelloChat", .client_count = 2, .members = NULL}; - groups[0].members = malloc(sizeof(Client_t *) * client_count); - groups[0].members[0] = &clients[0]; - groups[0].members[1] = &clients[1]; + for (int i = 0; i < group_count; i++) { + strncpy(groups[i].name, config.groups[i].name, COMMAND_LENGTH - 2); + groups[i].client_count = config.groups[i].member_count; + groups[i].members = malloc(sizeof(Client_t *) * config.groups[i].member_count); + for (int j = 0; j < config.groups[i].member_count; j++) { + // Find the client with the given id + for (int k = 0; k < client_count; k++) { + if (strncmp(clients[k].id, config.groups[i].member_ids[j], COMMAND_LENGTH) == 0) { + groups[i].members[j] = &clients[k]; + break; + } + } + } + } - groups[1] = (Group_t){.name = "Macarena", .client_count = 0, .members = NULL}; - groups[1].members = malloc(sizeof(Client_t *) * client_count); - - groups[2] = (Group_t){.name = "Bananownia", .client_count = 1, .members = NULL}; - groups[2].members = malloc(sizeof(Client_t *) * client_count); - groups[2].members[0] = &clients[1]; + // No longer useful after copying the data over + for (int i = 0; i < group_count; i++) { + for (int j = 0; j < config.groups[i].member_count; j++) { + free(config.groups[i].member_ids[j]); + } + } + free(config.clients); + free(config.groups); int server_queue_id = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); @@ -119,6 +144,8 @@ int main(int argc, char **argv) { return 0; } + printf("Server ready.\n"); + msgbuf_t msgbuf; while (1) { // Read from the ipc, then handle the command @@ -305,6 +332,15 @@ int main(int argc, char **argv) { break; } case Logout: { + for (int i = 0; i < client_count; i++) { + if (strncmp(clients[i].id, msgbuf.sender, COMMAND_LENGTH) == 0 && clients[i].logged_in) { + clients[i].logged_in = false; + clients[i].queue_id = -1; + printf("Client %s logged out\n", msgbuf.sender); + break; + } + } + break; } case Hearbeat: for (int i = 0; i < client_count; i++) { diff --git a/util/util.c b/util/util.c index b21a93e..7fb80ca 100644 --- a/util/util.c +++ b/util/util.c @@ -1,5 +1,12 @@ +#include "fcntl.h" +#include "unistd.h" +#include +#include #include #include +#include + +#include "util.h" static char alphs[] = {'-', '_', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', @@ -22,3 +29,93 @@ void get_id(char *id_buf) { } id_buf[15] = '\0'; } + +// Load configuration using libconfig +void load_config(server_config_t *cfg, const char *path) { + config_t config; + config_init(&config); + + if (!config_read_file(&config, path)) { + fprintf(stderr, "Error reading config file %s:%d - %s\n", + config_error_file(&config), + config_error_line(&config), + config_error_text(&config)); + config_destroy(&config); + exit(1); + } + + // Read clients + config_setting_t *clients = config_lookup(&config, "clients"); + if (!clients) { + fprintf(stderr, "No 'clients' section found in config file\n"); + config_destroy(&config); + exit(1); + } + + int client_count = config_setting_length(clients); + cfg->client_count = client_count; + cfg->clients = malloc(sizeof(client_config_t) * client_count); + + for (int i = 0; i < client_count; i++) { + config_setting_t *client = config_setting_get_elem(clients, i); + const char *client_id, *nickname; + + if (!config_setting_lookup_string(client, "client_id", &client_id) || + !config_setting_lookup_string(client, "nickname", &nickname)) { + fprintf(stderr, "Error reading client %d configuration\n", i); + config_destroy(&config); + exit(1); + } + + strncpy(cfg->clients[i].client_id, client_id, COMMAND_LENGTH - 1); + cfg->clients[i].client_id[COMMAND_LENGTH - 1] = '\0'; + strncpy(cfg->clients[i].nickname, nickname, COMMAND_LENGTH - 1); + cfg->clients[i].nickname[COMMAND_LENGTH - 1] = '\0'; + } + + // Read groups + config_setting_t *groups = config_lookup(&config, "groups"); + if (!groups) { + fprintf(stderr, "No 'groups' section found in config file\n"); + config_destroy(&config); + exit(1); + } + + int group_count = config_setting_length(groups); + cfg->group_count = group_count; + cfg->groups = malloc(sizeof(group_config_t) * group_count); + + for (int i = 0; i < group_count; i++) { + config_setting_t *group = config_setting_get_elem(groups, i); + const char *name; + config_setting_t *members; + + if (!config_setting_lookup_string(group, "name", &name)) { + fprintf(stderr, "Error reading group %d name\n", i); + config_destroy(&config); + exit(1); + } + + strncpy(cfg->groups[i].name, name, COMMAND_LENGTH - 2); + cfg->groups[i].name[COMMAND_LENGTH - 2] = '\0'; + + members = config_setting_get_member(group, "members"); + if (!members) { + fprintf(stderr, "Error reading group %d members\n", i); + config_destroy(&config); + exit(1); + } + + int member_count = config_setting_length(members); + cfg->groups[i].member_count = member_count; + + for (int j = 0; j < member_count; j++) { + const char *member_id = config_setting_get_string_elem(members, j); + cfg->groups[i].member_ids[j] = malloc(COMMAND_LENGTH); + strncpy(cfg->groups[i].member_ids[j], member_id, COMMAND_LENGTH - 1); + cfg->groups[i].member_ids[j][COMMAND_LENGTH - 1] = '\0'; + } + } + + config_destroy(&config); +} diff --git a/util/util.h b/util/util.h index 38cc69b..ff76e45 100644 --- a/util/util.h +++ b/util/util.h @@ -1 +1,23 @@ +#include "../commands.h" + void get_id(char *id_buf); + +typedef struct { + char client_id[COMMAND_LENGTH]; + char nickname[COMMAND_LENGTH]; +} client_config_t; + +typedef struct { + char name[COMMAND_LENGTH - 1]; + char *member_ids[COMMAND_LENGTH]; // Array of client IDs + int member_count; +} group_config_t; + +typedef struct { + int client_count; + int group_count; + client_config_t *clients; + group_config_t *groups; +} server_config_t; + +void load_config(server_config_t *config, const char *path);