diff options
author | dweller <dweller@cabin.digital> | 2024-03-09 00:55:36 +0200 |
---|---|---|
committer | dweller <dweller@cabin.digital> | 2024-03-09 00:55:36 +0200 |
commit | 86d3f93ee338b28ab7d40aa83c129cf6b97ef4b7 (patch) | |
tree | 507a8d66932e6dea9b121dfcbf980f7925575c9f /c/sources |
Initial commit, 2 years later
Diffstat (limited to '')
-rw-r--r-- | c/sources/client.c | 96 | ||||
-rw-r--r-- | c/sources/common.h | 27 | ||||
-rw-r--r-- | c/sources/ezipc.h | 543 | ||||
-rw-r--r-- | c/sources/server.c | 66 |
4 files changed, 732 insertions, 0 deletions
diff --git a/c/sources/client.c b/c/sources/client.c new file mode 100644 index 0000000..672e3e8 --- /dev/null +++ b/c/sources/client.c @@ -0,0 +1,96 @@ +#define EZIPC_IMPL +#include "ezipc.h" + +#include "common.h" + + +int main(void) +{ + printf("EZIPC C Client\n"); + + + printf("Connecting..."); + fflush(stdout); + + ezi_conn* conn = ezi_connect(EZIPC_TEST_PATH); + assert(conn); + + printf(" DONE!\n"); + + msg msg_txt = { .type = MSG_TEXT }; + msg msg_exit = { .type = MSG_EXIT }; + + bool running = true; + while(running) + { + char in[1024] = {0}; + printf("send> "); + scanf("%[^\n]%*c", in); + + if(strcmp(in, "exit") != 0) + { + strcpy((char*)msg_txt.data, in); + if(!ezi_send(conn, &msg_txt, sizeof(msg_txt))) + { + printf("Send error, resetting...\n"); + + ezi_disconnect(conn); + conn = ezi_connect(EZIPC_TEST_PATH); + assert(conn); + + continue; + } + } + else + { + if(!ezi_send(conn, &msg_exit, sizeof(msg_exit))) + { + ezi_disconnect(conn); + exit(1); + } + + break; + } + + msg rmsg = {0}; + size_t rsz = sizeof(rmsg); + if(!ezi_recv(conn, &rmsg, &rsz)) + { + printf("Recv error, resetting...\n"); + + ezi_disconnect(conn); + conn = ezi_connect(EZIPC_TEST_PATH); + assert(conn); + + continue; + } + + switch(rmsg.type) + { + case MSG_OK: + { + printf("acknowledged!\n"); + + } break; + + case MSG_EXIT: + { + running = false; + printf("told to exit...\n"); + + } break; + + case MSG_TEXT: + { + printf("received: '%s'\n", (char*)rmsg.data); + + } break; + + default: exit(1); + } + } + + ezi_disconnect(conn); + return 0; +} + diff --git a/c/sources/common.h b/c/sources/common.h new file mode 100644 index 0000000..bc3ae30 --- /dev/null +++ b/c/sources/common.h @@ -0,0 +1,27 @@ +#pragma once + +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <assert.h> +#include <string.h> + + +#define EZIPC_TEST_PATH "/tmp/ezi_conn_test" + + +typedef enum msg_type_e +{ + MSG_TEXT, + MSG_OK, + MSG_EXIT, + +} msg_type; + + +typedef struct msg_s +{ + msg_type type; + uint8_t data[128]; + +} msg; diff --git a/c/sources/ezipc.h b/c/sources/ezipc.h new file mode 100644 index 0000000..ff69fe1 --- /dev/null +++ b/c/sources/ezipc.h @@ -0,0 +1,543 @@ +/* + * BSD 3-Clause License (BSD-3-Clause) + * + * Copyright (C) 2022 - dweller <dweller@cabin.digital> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + +/* + * EZIPC + * EZ(easy) Inter-Process Communication single-file library + * + * + * At the moment, this is just a wrapper around POSIX named pipes (aka FIFOs) + * that creates duplex communication channel between 2 processes. + * + * + * Brief Description + * + * Just create the connections with ezi_create() on a "server" and ezi_connect() + * on a "client" process. Then use ezi_{send,recv}() functions to push data + * around. There is no functional difference between "server" and "client" + * except that "server" is ultimately responsible for OS resources (the created + * named pipes) clean up. + * When you're done, ezi_destroy() the connection on the "server" and + * ezi_disconnect() it on the "client" side. + * + * The library provides a reliable way to send sized frames between 2 processes. + * The data size is enforced, but other than that there are no assumptions about + * possible higher-level usage of this code. If the reader cannot receive full + * frame at once, it is truncated and the reset is discarded. Send and receive + * failures are considered fatal and restart (disconnected, reconnect) of the + * connection is required. + * + * All functions are blocking on their specific action. Create/Connect wait for + * the other end of the connection, send waits for there to be room in the OS + * buffers, recv waits for something to read from OS buffers. + * + * While more than 2 process are able to read from same connection + * (read: named pipes), THIS IS *HIGHLY* INADVISABLE, as there are no guarantees + * about data interleaving in such case. + * + * + * Single-file Usage + * + * Define "EZIPC_IMPL" before this header inclusion into any *.c file to get + * the implementation code injected into said .c file. Compile and run. + * + * + * Examples + * + * See files server.c, client.c, and common.h for a simple usage example. + * + * + * Symbol List + * + * EZIPC_VERSION + * + * typedef struct ezi_conn_s ezi_conn + * + * ezi_conn* ezi_create(const char* conn_path) + * void ezi_destroy(ezi_conn* conn) + * + * ezi_conn* ezi_connect(const char* conn_path) + * void ezi_disconnect(ezi_conn* conn) + * + * bool ezi_send(ezi_conn* conn, const void* data, size_t size) + * bool ezi_recv(ezi_conn* conn, void* data, size_t* size); + * + * See bellow for specific function descriptions. + * + * + * Compliance + * + * This code adheres to C99 standard, and was tested with following command: + * > $ gcc -std=c99 -Wall -Wextra -pedantic + * + * + * Known Issues + * + * 1. When "server" abruptly disconnects "client" gets SIGPIPE "broken pipe" + * signal, which is annoying because it would be way better to just return + * false from last called function and perror() for information. + * + * Workaround: "client" can insert a signal handler and have a callback, or + * accept quick death as a positive. + * + * + * Changelog + * + * v1.2 - ezi_recv() now fails on receiving a size of 0 bytes + * v1.1 - C++ support (extern and void* casts) + * v1 - release + */ + + +#ifndef EZIPC_H +#define EZIPC_H + + +#define EZIPC_VERSION "v1.2" + + +#include <stddef.h> +#include <stdbool.h> + + +#ifdef __cplusplus +extern "C" { +#endif + + +/* + * Opaque pointer to the structure representing the connection. + */ +typedef struct ezi_conn_s ezi_conn; + + +/* + * Creates the "server" (controlling/initial) end of the connection. This end + * of the connection may create underlying OS resources if they do not exist, + * AND IS responsible for cleaning them. + * resources. + * + * BLOCKS: waits for other end to connect. + * + * Arguments: + * IN `conn_path` - path to store the implementation-related resources, + * doubles as a connection identifier. + * + * Returns the connection object as an opaque pointer. + */ +extern ezi_conn* ezi_create(const char* conn_path); + +/* + * Destroys the connection and frees the underlying OS resources if connection + * was established with `ezi_create()` function. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * + * Returns: nothing. + */ +extern void ezi_destroy(ezi_conn* conn); + +/* + * Creates the "client" end of the connection. This end of the connection may + * create underlying OS resources if they do not exist, but IS NOT responsible + * for them. + * + * BLOCKS: waits for other end to connect. + * + * Arguments: + * IN `conn_path` - path to store the implementation-related resources, + * doubles as a connection identifier. + * + * Returns the connection object as an opaque pointer. + */ + +extern ezi_conn* ezi_connect(const char* conn_path); + +/* + * Destroys the connection and frees the underlying OS resources if connection + * was established with `ezi_create()` function. + * Is an alias of `ezi_destroy`. Exists for symmetry in the API. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * + * Returns: nothing. + */ +#define ezi_disconnect(conn) ezi_destroy(conn) + + +/* + * Sends `size` bytes to the other endpoint. + * + * BLOCKS: waits for room in the underlying buffer. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * IN `data` - buffer of `size` bytes to be sent. + * IN `size` - size of the data to be sent, in bytes. + * + * Returns: true on success, false on failure. If failed, the connection is + * thought to be in undefined state and has to be closed and reopened + * for communication to continue. + */ +extern bool ezi_send(ezi_conn* conn, const void* data, size_t size); + +/* + * Receives up to `size` bytes from the other endpoint. If there is more data + * available that `size`, it is *discarded*! + * + * BLOCKS: waits for data in the underlying buffer. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * OUT `data` - buffer of `size` bytes to be filled with incoming data; + * on success holds the received data, which may be truncated + * to the input `size`; + * on failure data is undefined. + * INOUT `size` - as an input: specifies the size of `data` buffer in bytes; + * as an output: + * on success holds the size of the received data, which may + * be smaller than the `data` buffer size passed as an + * input; + * on failure is unchanged from input. + * + * Returns: true on success, false on failure. If failed, the connection is + * thought to be in undefined state and has to be closed and reopened + * for communication to continue. + */ +extern bool ezi_recv(ezi_conn* conn, void* data, size_t* size); + +#ifdef __cplusplus +} +#endif // EXTERN C + + +#endif // EZIPC_H + + +// +// ========================================================================= +// +// What follows are implementation details +// +// + +#ifdef EZIPC_IMPL +#ifndef EZIPC_IMPLEMENTED +#define EZIPC_IMPLEMENTED // redefinition guard +#ifdef __linux__ + +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <stdint.h> + +#include <unistd.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> + + +#ifdef __cplusplus +extern "C" { +#endif + + +struct ezi_conn_s +{ + char* path_c2s; + char* path_s2c; + int c2s, s2c; + + bool server; +}; + + +static ezi_conn* _ezi_internal_create(const char* conn_path, + const mode_t modes[2]) +{ + if(!conn_path) return NULL; + + ezi_conn* conn = (ezi_conn*)calloc(1, sizeof(*conn)); + if(!conn) return NULL; + + conn->c2s = -1; + conn->s2c = -1; + + // Populate path strings + { + const size_t path_len = strlen(conn_path); + + conn->path_c2s = (char*)malloc(path_len + 5); + if(!conn->path_c2s) goto cleanup; + + strcat(conn->path_c2s, conn_path); + strcat(conn->path_c2s, "_c2s"); + + conn->path_s2c = (char*)malloc(path_len + 5); + if(!conn->path_s2c) goto cleanup; + + strcat(conn->path_s2c, conn_path); + strcat(conn->path_s2c, "_s2c"); + } + + + // Create POSIX FIFOs + { + if(mkfifo(conn->path_c2s, 0660) != 0) + { + if(errno != EEXIST) + { + perror("[EZIPC] Could not create client->server pipe"); + goto cleanup; + } + } + + if(mkfifo(conn->path_s2c, 0660) != 0) + { + if(errno != EEXIST) + { + perror("[EZIPC] Could not create server->client pipe"); + goto cleanup; + } + } + } + + // Open the FIFOs + { + conn->c2s = open(conn->path_c2s, modes[0]); + if(conn->c2s < 0) + { + perror("[EZIPC] Could not open client->server pipe"); + goto cleanup; + } + + conn->s2c = open(conn->path_s2c, modes[1]); + if(conn->s2c < 0) + { + perror("[EZIPC] Could not open server->client pipe"); + goto cleanup; + } + } + + return conn; + +cleanup: + + if(conn) + { + ezi_destroy(conn); + + if(conn->path_c2s) free(conn->path_c2s); + if(conn->path_s2c) free(conn->path_s2c); + + free(conn); + } + + return NULL; +} + + +ezi_conn* ezi_create(const char* conn_path) +{ + const mode_t modes[2] = { O_RDONLY, O_WRONLY}; + + ezi_conn* conn = _ezi_internal_create(conn_path, modes); + if(conn) conn->server = true; + + return conn; +} + +ezi_conn* ezi_connect(const char* conn_path) +{ + const mode_t modes[2] = { O_WRONLY, O_RDONLY}; + + ezi_conn* conn = _ezi_internal_create(conn_path, modes); + if(conn) conn->server = false; + + return conn; +} + +void ezi_destroy(ezi_conn* conn) +{ + if(!conn) return; + + // NOTE: even if close() errors out, DO NOT retry (even if EINTR) as per + // man 2 close. Just report the error and clobber the FDs. + + if(conn->c2s >= 0) + { + int rc = close(conn->c2s); + if(rc < 0) perror("[EZIPC] Could not close client->server pipe"); + + conn->c2s = -1; + } + + if(conn->s2c >= 0) + { + int rc = close(conn->s2c); + if(rc < 0) perror("[EZIPC] Could not close server->client pipe"); + + conn->s2c = -1; + } + + // I could check and handle remove error here, but I'd rather tell the user + // and not accidentally delete something else + + if(conn->server) + { + if(conn->path_c2s && (remove(conn->path_c2s) != 0)) + perror("[EZIPC] Could not delete client->server pipe"); + + if(conn->path_s2c && (remove(conn->path_s2c) != 0)) + perror("[EZIPC] Could not delete server->client pipe"); + } +} + +// wrapper over write() to handle interrupted syscall +static bool _ezi_write(int ep, const void* buffer, size_t size) +{ + const uint8_t* bytes = (uint8_t*)buffer; + + size_t written = 0; + while(written < size) + { + size_t got = write(ep, bytes + written, size - written); + if(got <= 0)// FIXME: technically < 0 but I don't want an infinite loop + { + perror("[EZIPC] Could not write to pipe"); + return false; + } + + written += size; + } + + return true; +} + + +bool ezi_send(ezi_conn* conn, const void* data, size_t size) +{ + if(!conn) return false; + if(!data) return false; + if(size == 0) return true; + + const int ep = conn->server ? conn->s2c : conn->c2s; + if(ep >= 0) + { + const uint64_t out_size = (uint64_t)size; + if(!_ezi_write(ep, &out_size, sizeof(out_size))) return false; + if(!_ezi_write(ep, data, size)) return false; + + return true; + } + else return false; + +} + +// wrapper over read() to handle interrupted syscall +static bool _ezi_read(int ep, void* buffer, size_t size) +{ + uint8_t* bytes = (uint8_t*)buffer; + + size_t readback = 0; + while(readback < size) + { + ssize_t got = read(ep, bytes + readback, size - readback); + if(got == 0) + { + // NOTE: I think reading 0 bytes in non-blocking mode is fine, + // but since I don't provide non-blocking mode, this is + // still an error. + return false; + } + else if(got < 0) + { + perror("[EZIPC] Could not read from pipe"); + return false; + } + + readback += got; + } + + return true; +} + + +bool ezi_recv(ezi_conn* conn, void* data, size_t* size) +{ + if(!conn) return false; + if(!data) return false; + if(!size) return false; + if(*size == 0) return true; + + const int ep = conn->server ? conn->c2s : conn->s2c; + if(ep >= 0) + { + // NOTE: since this is IPC, we're on the same machine so no need to + // ensure endianness + uint64_t in_size = 0; + if(!_ezi_read(ep, &in_size, sizeof(in_size))) return false; + + if(in_size == 0) return false; + + const uint64_t read_size = in_size > *size ? *size : in_size; + if(!_ezi_read(ep, data, read_size)) return false; + + // If we have left over, get rid of it to guarantee correct read next + // time + for(uint64_t i = 0; i < (in_size - read_size); i++) + { + uint8_t temp; // I know I could read in bigger chunks, but honestly + // I don't care, and assume this is an edge case + if(!_ezi_read(ep, &temp, 1)) return false; + } + + *size = read_size; + return true; + + } else return false; + +} + +#ifdef __cplusplus +} +#endif // EXTERN C + +#else // __linux__ + #error "EZ IPC library is not supported on your platform. Want to port?" +#endif // platforms +#endif // EZIPC_IMPLEMENTED +#endif // EZIPC_IMPL diff --git a/c/sources/server.c b/c/sources/server.c new file mode 100644 index 0000000..57c2492 --- /dev/null +++ b/c/sources/server.c @@ -0,0 +1,66 @@ +#include "ezipc.h" +#include "common.h" + + +int main(void) +{ + ezi_conn* conn = ezi_create(EZIPC_TEST_PATH); + assert(conn); + + msg ok; + ok.type = MSG_OK; + strcpy((char*)ok.data, "ok"); + + msg quit; + quit.type = MSG_EXIT; + strcpy((char*)quit.data, "exit"); + + int counter = 0; + bool running = true; + while(running) + { + msg rmsg; + size_t rsz = sizeof(rmsg) - 100; // truncate test + if(ezi_recv(conn, &rmsg, &rsz)) + { + switch(rmsg.type) + { + case MSG_TEXT: + { + printf("got: '%s'\n", (char*)rmsg.data); + + counter++; + if(counter >= 10) + { + if(!ezi_send(conn, &quit, sizeof(quit))) + printf("Could not send msg to client\n"); + + counter = 0; + } + else + { + if(!ezi_send(conn, &ok, sizeof(ok))) + printf("Could not send msg to client\n"); + } + + } break; + + default: break; + } + } + else + { + printf("Recv error, resetting...\n"); + + ezi_destroy(conn); + conn = ezi_create(EZIPC_TEST_PATH); + assert(conn); + } + } + + ezi_destroy(conn); + return 0; +} + +#define EZIPC_IMPL +#include "ezipc.h" |