From a5fdc24c8f6d93f1f5cb5a78b21af280cba04c6f Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Fri, 27 Dec 2024 11:29:22 -0500 Subject: [PATCH] Add Socket Class (#55) * Network - Add Socket Class * Network - Socket Fixes * App - Use Socket in Linux IPC Implementation * Network - Fix Socket and IPC * Network - Fix Socket Shutdown on Linux * Version Bump * Network - Fix `Socket` Build on Windows * Network - Fix Windows Socket Shutdown * Network - Add Pipe Support to Socket on Windows * Check Spell --- CHANGELOG.md | 9 + CMakeLists.txt | 5 +- Doxyfile | 2 +- include/app/interprocesscommunicator.h | 21 +- include/network/addressfamily.h | 26 ++ include/network/ipv4address.h | 7 +- include/network/socket.h | 88 +++++++ include/network/socketpurpose.h | 16 ++ include/network/sockettype.h | 25 ++ manual/README.md | 10 +- src/app/datafilebase.cpp | 8 +- src/app/interprocesscommunicator.cpp | 192 +++----------- src/network/ipv4address.cpp | 11 +- src/network/socket.cpp | 335 +++++++++++++++++++++++++ src/system/process.cpp | 4 +- tests/ipctests.cpp | 5 +- 16 files changed, 578 insertions(+), 186 deletions(-) create mode 100644 include/network/addressfamily.h create mode 100644 include/network/socket.h create mode 100644 include/network/socketpurpose.h create mode 100644 include/network/sockettype.h create mode 100644 src/network/socket.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index dc41d72..78bdd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2024.12.1 (next) +### Breaking Changes +None +### New APIs +#### Network +- Added `Nickvision::Network::Socket` class +### Fixes +None + ## 2024.12.0 ### Breaking Changes None diff --git a/CMakeLists.txt b/CMakeLists.txt index da2b79e..d5209da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ endif() set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") #libnick Definition -project ("libnick" LANGUAGES C CXX VERSION 2024.12.0 DESCRIPTION "A cross-platform base for native Nickvision applications.") +project ("libnick" LANGUAGES C CXX VERSION 2024.12.1 DESCRIPTION "A cross-platform base for native Nickvision applications.") include(CMakePackageConfigHelpers) include(GNUInstallDirs) include(CTest) @@ -64,6 +64,7 @@ add_library (${PROJECT_NAME} "src/network/macaddress.cpp" "src/network/networkmonitor.cpp" "src/network/networkstatechangedeventargs.cpp" + "src/network/socket.cpp" "src/network/web.cpp" "src/notifications/notificationsenteventargs.cpp" "src/notifications/notifyicon.cpp" @@ -97,7 +98,7 @@ if(USING_VCPKG) endif() if(WIN32) find_package(sqlcipher CONFIG REQUIRED) - target_link_libraries(${PROJECT_NAME} PUBLIC sqlcipher::sqlcipher Advapi32 Dnsapi Dwmapi Gdiplus Kernel32 Shell32 UxTheme) + target_link_libraries(${PROJECT_NAME} PUBLIC sqlcipher::sqlcipher Advapi32 Dnsapi Dwmapi Gdiplus Kernel32 Shell32 UxTheme Ws2_32) elseif(APPLE) set(THREADS_PREFER_PTHREAD_FLAG ON) find_library(CF_LIBRARY CoreFoundation) diff --git a/Doxyfile b/Doxyfile index 22708af..ae5193d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "libnick" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = "2024.12.0" +PROJECT_NUMBER = "2024.12.1" # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/include/app/interprocesscommunicator.h b/include/app/interprocesscommunicator.h index 77b2c6e..2781cb6 100644 --- a/include/app/interprocesscommunicator.h +++ b/include/app/interprocesscommunicator.h @@ -23,16 +23,12 @@ #ifndef INTERPROCESSCOMMUNICATOR_H #define INTERPROCESSCOMMUNICATOR_H +#include #include #include -#include #include "events/event.h" #include "events/parameventargs.h" -#ifdef _WIN32 -#include -#else -#include -#endif +#include "network/socket.h" namespace Nickvision::App { @@ -83,17 +79,12 @@ namespace Nickvision::App * @brief Runs the IPC server loop. */ void runServer(); + std::string m_id; bool m_serverRunning; - Events::Event>> m_commandReceived; + std::unique_ptr m_serverSocket; std::thread m_server; - std::string m_path; -#ifdef _WIN32 - HANDLE m_serverPipe; -#else - struct sockaddr_un m_sockaddr; - int m_serverSocket; -#endif + Events::Event>> m_commandReceived; }; } -#endif //INTERPROCESSCOMMUNICATOR_H \ No newline at end of file +#endif //INTERPROCESSCOMMUNICATOR_H diff --git a/include/network/addressfamily.h b/include/network/addressfamily.h new file mode 100644 index 0000000..b2cf3fd --- /dev/null +++ b/include/network/addressfamily.h @@ -0,0 +1,26 @@ +#ifndef ADDRESSFAMILY_H +#define ADDRESSFAMILY_H + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace Nickvision::Network +{ + /** + * @brief Type of address that can be used by a socket. + */ + enum class AddressFamily + { +#ifdef _WIN32 + Pipe, +#else + Unix = AF_UNIX, +#endif + IPv4 = AF_INET + }; +} + +#endif //ADDRESSFAMILY_H diff --git a/include/network/ipv4address.h b/include/network/ipv4address.h index e4391b0..bfac138 100644 --- a/include/network/ipv4address.h +++ b/include/network/ipv4address.h @@ -67,6 +67,11 @@ namespace Nickvision::Network * @return The fourth byte of the address */ unsigned char getFourth() const; + /** + * @brief Gets the address in network byte order. + * @return The address in network byte order + */ + unsigned long getNetworkByteOrder() const; /** * @brief Gets the string representation of the address. * @return The string representation of the address @@ -87,4 +92,4 @@ namespace Nickvision::Network }; } -#endif //IPV4ADDRESS_H \ No newline at end of file +#endif //IPV4ADDRESS_H diff --git a/include/network/socket.h b/include/network/socket.h new file mode 100644 index 0000000..787e326 --- /dev/null +++ b/include/network/socket.h @@ -0,0 +1,88 @@ +#ifndef SOCKET_H +#define SOCKET_H + +#include +#include +#ifdef _WIN32 +#include +#endif +#include "addressfamily.h" +#include "socketpurpose.h" +#include "sockettype.h" + +namespace Nickvision::Network +{ + /** + * @brief A network socket (an endpoint for communication). + */ + class Socket + { + public: + /** + * @brief Constructs a Socket. + * @param purpose The purpose of the socket + * @param type The SocketType of the socket (Ignored when AddressFamily::Pipe is used) + * @param family The AddressFamily of the socket + * @param address The address to bind/connect the socket to + * @param port The port to bind/connect the socket to (Ignored when AddressFamily::Unix or AddressFamily::Pipe is used) + * @throw std::invalid_argument Thrown if the address is invalid + * @throw std::logic_error Thrown if the socket cannot be binded to (i.e. A server socket already exists) + * @throw std::runtime_error Thrown on Windows if winsock cannot be initialized + * @throw std::runtime_error Thrown if the socket cannot be created or listened + */ + Socket(SocketPurpose purpose, SocketType type, AddressFamily family, const std::string& address, int port); + /** + * @brief Destructs a Socket. + * @brief This will disconnect from a child socket if disconnect was not already called. + */ + ~Socket(); + /** + * @brief Establishes a connection. + * @brief If the socket's purpose is SocketPurpose::Server, this method will block until a client is connected. + * @brief Is the socket's purpose is SocketPurpose::Client, this method will connect to the server. + * @return True if connected, else false + */ + bool connect(); + /** + * @brief Closes a connection. + * @brief If the socket's purpose is SocketPurpose::Server, this method will drop the connection with the client. + * @brief If the socket's purpose is SocketPurpose::Client, this method will have no effect. + * @return True if disconnected, else false + */ + bool disconnect(); + /** + * @brief Receives a message. + * @brief connect() must have been called first and have returned true. + * @brief If the socket's purpose is SocketPurpose::Server, this method will receive a message from the client. + * @brief If the socket's purpose is SocketPurpose::Client, this method will receive a message from the server. + * @return The received message + */ + std::string receiveMessage() const; + /** + * @brief Sends a message. + * @brief connect() must have been called first and have returned true. + * @brief If the socket's purpose is SocketPurpose::Server, this method will send a message to the client. + * @brief If the socket's purpose is SocketPurpose::Client, this method will send a message to the server. + * @param message The message to send + * @return True if message sent successfully, else false + */ + bool sendMessage(const std::string& message) const; + + private: + SocketPurpose m_purpose; + SocketType m_type; + AddressFamily m_family; + std::string m_address; + int m_port; +#ifdef _WIN32 + SOCKET m_socket; + SOCKET m_child; + HANDLE m_pipe; +#else + int m_socket; + int m_child; +#endif + }; +} + +#endif //SOCKET_H diff --git a/include/network/socketpurpose.h b/include/network/socketpurpose.h new file mode 100644 index 0000000..7016d0d --- /dev/null +++ b/include/network/socketpurpose.h @@ -0,0 +1,16 @@ +#ifndef SOCKETPURPOSE_H +#define SOCKETPURPOSE_H + +namespace Nickvision::Network +{ + /** + * @brief Purposes of sockets. + */ + enum class SocketPurpose + { + Server, + Client + }; +} + +#endif // SOCKETPURPOSE_H diff --git a/include/network/sockettype.h b/include/network/sockettype.h new file mode 100644 index 0000000..c22c3e4 --- /dev/null +++ b/include/network/sockettype.h @@ -0,0 +1,25 @@ +#ifndef SOCKETTYPE_H +#define SOCKETTYPE_H + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace Nickvision::Network +{ + /** + * @brief Type of sockets. + */ + enum class SocketType + { + Stream = SOCK_STREAM, + Datagram = SOCK_DGRAM, +#ifdef __linux__ + SequencedPacket = SOCK_SEQPACKET +#endif + }; +} + +#endif // SOCKETTYPE_H diff --git a/manual/README.md b/manual/README.md index 1a6e1a3..8340b10 100644 --- a/manual/README.md +++ b/manual/README.md @@ -6,16 +6,14 @@ libnick provides Nickvision apps with a common set of cross-platform APIs for managing system and desktop app functionality such as network management, taskbar icons, translations, app updates, and more. -## 2024.12.0 +## 2024.12.1 (next) ### Breaking Changes None ### New APIs -None +#### Network +- Added `Nickvision::Network::Socket` class ### Fixes -#### Notifications -- Fixed an issue where `ShellNotification::send()` did not work on non-GTK linux applications -#### System -- Fixed an issue where `Environment::getExecutableDirectory()` did not return the correct path on macOS +None ## Dependencies The following are a list of dependencies used by libnick. diff --git a/src/app/datafilebase.cpp b/src/app/datafilebase.cpp index 68d3752..b9db1f9 100644 --- a/src/app/datafilebase.cpp +++ b/src/app/datafilebase.cpp @@ -21,11 +21,11 @@ namespace Nickvision::App m_path = UserDirectories::get(ApplicationUserDirectory::Config, appName) / (m_key + ".json"); if (std::filesystem::exists(m_path)) { - std::ifstream in{ m_path }; - boost::json::stream_parser parser; - std::string line; try { + std::ifstream in{ m_path }; + boost::json::stream_parser parser; + std::string line; while(std::getline(in, line)) { parser.write(line); @@ -58,4 +58,4 @@ namespace Nickvision::App m_saved({}); return true; } -} \ No newline at end of file +} diff --git a/src/app/interprocesscommunicator.cpp b/src/app/interprocesscommunicator.cpp index af602e7..9295966 100644 --- a/src/app/interprocesscommunicator.cpp +++ b/src/app/interprocesscommunicator.cpp @@ -1,73 +1,33 @@ #include "app/interprocesscommunicator.h" -#include -#include -#include +#include #include "helpers/stringhelpers.h" -#ifndef _WIN32 -#include -#include -#endif using namespace Nickvision::Helpers; +using namespace Nickvision::Network; namespace Nickvision::App { InterProcessCommunicator::InterProcessCommunicator(const std::string& id) - : m_serverRunning{ false } + : m_id{ id }, + m_serverRunning{ false } { -#ifdef _WIN32 - m_path = "\\\\.\\pipe\\" + id; - m_serverPipe = nullptr; - WIN32_FIND_DATAW fd; - std::wstring wPath{ StringHelpers::wstr(m_path) }; - HANDLE find{ FindFirstFileW(wPath.c_str(), &fd) }; - if (find == INVALID_HANDLE_VALUE) //no server exists - { - m_serverPipe = CreateNamedPipeW(wPath.c_str(), PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 0, 0, NMPWAIT_USE_DEFAULT_WAIT, nullptr); - if (m_serverPipe == INVALID_HANDLE_VALUE) - { - throw std::runtime_error("Unable to start IPC server."); - } - m_serverRunning = true; - FindClose(find); - } -#else - m_path = "/tmp/" + id; -#ifdef __linux__ - if (m_path.size() >= 108) -#else - if (m_path.size() >= 104) -#endif + try { - throw std::runtime_error("Unable to create IPC server. Application ID is too long."); - } - memset(&m_sockaddr, 0, sizeof(m_sockaddr)); - m_sockaddr.sun_family = AF_UNIX; - strcpy(m_sockaddr.sun_path, m_path.c_str()); -#ifdef __linux__ - m_serverSocket = socket(AF_UNIX, SOCK_SEQPACKET, 0); +#ifdef _WIN32 + m_serverSocket = std::make_unique(SocketPurpose::Server, SocketType::Stream, AddressFamily::Pipe, m_id, 0); +#elif defined(__linux__) + m_serverSocket = std::make_unique(SocketPurpose::Server, SocketType::SequencedPacket, AddressFamily::Unix, m_id, 0); #else - m_serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); + m_serverSocket = std::make_unique(SocketPurpose::Server, SocketType::Stream, AddressFamily::Unix, m_id, 0); #endif - if (m_serverSocket == -1) - { - throw std::runtime_error("Unable to check IPC server."); - } - if (bind(m_serverSocket, reinterpret_cast(&m_sockaddr), sizeof(m_sockaddr)) == 0) //no server exists - { - if (listen(m_serverSocket, 5) == -1) - { - throw std::runtime_error("Unable to listen to IPC socket."); - } m_serverRunning = true; } - else + catch(const std::logic_error&) { } + catch(const std::exception&) { - close(m_serverSocket); - m_serverSocket = -1; + throw; } -#endif - if (m_serverRunning) + if(m_serverRunning) { m_server = std::thread(&InterProcessCommunicator::runServer, this); } @@ -76,26 +36,7 @@ namespace Nickvision::App InterProcessCommunicator::~InterProcessCommunicator() { m_serverRunning = false; -#ifdef _WIN32 - if (m_serverPipe) - { - CancelSynchronousIo(m_serverPipe); - CloseHandle(m_serverPipe); - } -#else - if (m_serverSocket != -1) - { -#ifdef __linux__ - int connectSocket{ socket(AF_UNIX, SOCK_SEQPACKET, 0) }; -#else - int connectSocket{ socket(AF_UNIX, SOCK_STREAM, 0) }; -#endif - connect(connectSocket, reinterpret_cast(&m_sockaddr), sizeof(m_sockaddr)); - close(connectSocket); - close(m_serverSocket); - unlink(m_path.c_str()); - } -#endif + m_serverSocket.reset(); if(m_server.joinable()) { m_server.join(); @@ -127,44 +68,21 @@ namespace Nickvision::App if (!args.empty()) { #ifdef _WIN32 - std::wstring wPath{ StringHelpers::wstr(m_path) }; - HANDLE connectPipe{ CreateFileW(wPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) }; - if (connectPipe == INVALID_HANDLE_VALUE) - { - return false; - } - for (const std::string& arg : args) - { - WriteFile(connectPipe, arg.c_str(), DWORD(arg.size()), nullptr, nullptr); - } - CloseHandle(connectPipe); + Socket connectionSocket{ SocketPurpose::Client, SocketType::Stream, AddressFamily::Pipe, m_id, 0 }; +#elif defined(__linux__) + Socket connectionSocket{ SocketPurpose::Client, SocketType::SequencedPacket, AddressFamily::Unix, m_id, 0 }; #else -#ifdef __linux__ - int connectSocket{ socket(AF_UNIX, SOCK_SEQPACKET, 0) }; -#else - int connectSocket{ socket(AF_UNIX, SOCK_STREAM, 0) }; + Socket connectionSocket{ SocketPurpose::Client, SocketType::Stream, AddressFamily::Unix, m_id, 0 }; #endif - if (connect(connectSocket, reinterpret_cast(&m_sockaddr), sizeof(m_sockaddr)) == -1) + if(!connectionSocket.connect()) { return false; } -#ifdef __linux__ - for (const std::string& arg : args) -#else - for (std::string arg : args) -#endif + std::string msg{ StringHelpers::join(args, "\n") }; + if(!connectionSocket.sendMessage(msg)) { -#ifndef __linux__ - arg += "\n"; -#endif - if(write(connectSocket, arg.c_str(), arg.size()) == -1) - { - close(connectSocket); - return false; - } + return false; } - close(connectSocket); -#endif } if (exitIfClient) { @@ -175,62 +93,32 @@ namespace Nickvision::App void InterProcessCommunicator::runServer() { - std::vector buffer(1024); while (m_serverRunning) { -#ifdef _WIN32 - if (ConnectNamedPipe(m_serverPipe, nullptr)) + if(m_serverSocket->connect()) { std::vector args; - DWORD read; - do + while(true) { - if(!ReadFile(m_serverPipe, &buffer[0], DWORD(buffer.size()), &read, nullptr)) + std::string message{ m_serverSocket->receiveMessage() }; + if(!message.empty()) { - break; + for(const std::string& arg : StringHelpers::split(message, "\n")) + { + args.push_back(arg); + } } - args.push_back({ &buffer[0], read }); - } while (read > 0); - DisconnectNamedPipe(m_serverPipe); - m_commandReceived({ args }); - } -#else - struct sockaddr_un clientAddr; - memset(&clientAddr, 0, sizeof(clientAddr)); - socklen_t clientAddrLen{ sizeof(clientAddr) }; - int clientSocket{ accept(m_serverSocket, reinterpret_cast(&clientAddr), &clientAddrLen) }; - if (!m_serverRunning) - { - break; - } - if (clientSocket != -1) - { - ssize_t bytes{ 0 }; -#ifdef __linux__ - std::vector args; -#else - std::string info; -#endif - do - { - bytes = read(clientSocket, &buffer[0], buffer.size()); - if (bytes > 0) + else { -#ifdef __linux__ - args.push_back({ &buffer[0], static_cast(bytes) }); -#else - info += { &buffer[0], static_cast(bytes) }; -#endif + break; } - } while (bytes > 0); - close(clientSocket); -#ifdef __linux__ - m_commandReceived({ args }); -#else - m_commandReceived({ StringHelpers::split(info, "\n") }); -#endif + } + m_serverSocket->disconnect(); + if(!args.empty()) + { + m_commandReceived({ args }); + } } -#endif } } -} \ No newline at end of file +} diff --git a/src/network/ipv4address.cpp b/src/network/ipv4address.cpp index f269582..d88b175 100644 --- a/src/network/ipv4address.cpp +++ b/src/network/ipv4address.cpp @@ -45,6 +45,11 @@ namespace Nickvision::Network return m_fourth; } + unsigned long IPv4Address::getNetworkByteOrder() const + { + return (static_cast(m_first) << 24) | (static_cast(m_second) << 16) | (static_cast(m_third) << 8) | static_cast(m_fourth); + } + std::string IPv4Address::str() const { return std::to_string(static_cast(m_first)) + "." + std::to_string(static_cast(m_second)) + "." + std::to_string(static_cast(m_third)) + "." + std::to_string(static_cast(m_fourth)); @@ -52,6 +57,10 @@ namespace Nickvision::Network std::optional IPv4Address::parse(const std::string& address) { + if(address == "localhost") + { + return IPv4Address{ 127, 0, 0, 1 }; + } std::vector parts{ StringHelpers::split(address, ".") }; if(parts.size() != 4) { @@ -75,4 +84,4 @@ namespace Nickvision::Network return std::nullopt; } } -} \ No newline at end of file +} diff --git a/src/network/socket.cpp b/src/network/socket.cpp new file mode 100644 index 0000000..d1ac117 --- /dev/null +++ b/src/network/socket.cpp @@ -0,0 +1,335 @@ +#include "network/socket.h" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif +#include "helpers/stringhelpers.h" +#include "network/ipv4address.h" + +#ifndef _WIN32 +#ifdef __linux__ + #define MAX_UNIX_PATH_LENGTH 108 +#else + #define MAX_UNIX_PATH_LENGTH 104 +#endif +#endif +#define BACKLOG 5 + +using namespace Nickvision::Helpers; + +namespace Nickvision::Network +{ +#ifdef _WIN32 + static bool isSocketValid(SOCKET socket) + { + return socket != INVALID_SOCKET; + } + + static bool isSocketValid(HANDLE pipe) + { + return pipe != INVALID_HANDLE_VALUE; + } +#else + static bool isSocketValid(int socket) + { + return socket != -1; + } +#endif + + Socket::Socket(SocketPurpose purpose, SocketType type, AddressFamily family, const std::string& address, int port) + : m_purpose{ purpose }, + m_type{ type }, + m_family{ family }, + m_address{ address }, + m_port{ port } + { +#ifdef _WIN32 + //Check if winsock is initialized + static bool winsockInitialized{ false }; + if(!winsockInitialized) + { + WSADATA wsaData; + if(WSAStartup(MAKEWORD(2,2), &wsaData) != 0) + { + throw std::runtime_error("Unable to initialize winsock"); + } + winsockInitialized = true; + } + //Create pipe + if(m_family == AddressFamily::Pipe) + { + if(m_purpose == SocketPurpose::Server) + { + std::wstring path{ L"\\\\.\\pipe\\" + StringHelpers::wstr(m_address) }; + WIN32_FIND_DATAW fd; + HANDLE find{ FindFirstFileW(path.c_str(), &fd) }; + if(find == INVALID_HANDLE_VALUE) + { + m_pipe = CreateNamedPipeW(path.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 0, 0, NMPWAIT_USE_DEFAULT_WAIT, nullptr); + if(m_pipe == INVALID_HANDLE_VALUE) + { + throw std::runtime_error("Unable to create pipe"); + } + FindClose(find); + } + else + { + throw std::logic_error("Unable to create pipe. Server already exists"); + } + } + return; + } +#endif + //Create the socket + m_socket = socket(static_cast(m_family), static_cast(m_type), 0); + if(!isSocketValid(m_socket)) + { + throw std::runtime_error("Unable to create socket"); + } + //Create the address struct + int bindResult{ 0 }; + switch(m_family) + { +#ifndef _WIN32 + case AddressFamily::Unix: + { + std::string domainPath{ "/tmp/" + m_address + ".socket" }; + domainPath.resize(MAX_UNIX_PATH_LENGTH); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, domainPath.c_str()); + bindResult = bind(m_socket, reinterpret_cast(&addr), sizeof(addr)); + break; + } +#endif + case AddressFamily::IPv4: + { + std::optional ipv4{ IPv4Address::parse(m_address) }; + if(!ipv4) + { + throw std::invalid_argument("Invalid IPv4 Address"); + } + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = m_port; +#ifdef _WIN32 + addr.sin_addr.S_un.S_un_b.s_b1 = ipv4->getFirst(); + addr.sin_addr.S_un.S_un_b.s_b2 = ipv4->getSecond(); + addr.sin_addr.S_un.S_un_b.s_b3 = ipv4->getThird(); + addr.sin_addr.S_un.S_un_b.s_b4 = ipv4->getFourth(); +#else + addr.sin_addr.s_addr = ipv4->getNetworkByteOrder(); +#endif + bindResult = bind(m_socket, reinterpret_cast(&addr), sizeof(addr)); + break; + } + } + //Bind and listen if server socket + if(m_purpose == SocketPurpose::Server) + { + if(bindResult != 0) + { +#ifdef _WIN32 + closesocket(m_socket); +#else + close(m_socket); +#endif + throw std::logic_error("Unable to bind socket. Server already exists"); + } + if(listen(m_socket, BACKLOG) != 0) + { +#ifdef _WIN32 + closesocket(m_socket); +#else + close(m_socket); +#endif + throw std::runtime_error("Unable to listen on socket"); + } + } + } + + Socket::~Socket() + { + //Disconnect from child + disconnect(); + //Close the socket +#ifdef _WIN32 + if(isSocketValid(m_socket)) + { + shutdown(m_socket, SD_BOTH); + closesocket(m_socket); + } + if(isSocketValid(m_pipe)) + { + if(m_purpose == SocketPurpose::Server) + { + CancelSynchronousIo(m_pipe); + } + CloseHandle(m_pipe); + } +#else + shutdown(m_socket, SHUT_RDWR); + close(m_socket); + if(m_family == AddressFamily::Unix) + { + std::string domainPath{ "/tmp/" + m_address + ".socket" }; + domainPath.resize(MAX_UNIX_PATH_LENGTH); + unlink(domainPath.c_str()); + } +#endif + } + + bool Socket::connect() + { + if(m_purpose == SocketPurpose::Server) + { +#ifdef _WIN32 + if(m_family == AddressFamily::Pipe) + { + return ConnectNamedPipe(m_pipe, nullptr); + } +#endif + m_child = accept(m_socket, nullptr, nullptr); + return isSocketValid(m_child); + } + else if(m_purpose == SocketPurpose::Client) + { + switch(m_family) + { +#ifdef _WIN32 + case AddressFamily::Pipe: + { + std::wstring path{ L"\\\\.\\pipe\\" + StringHelpers::wstr(m_address) }; + m_pipe = CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + return isSocketValid(m_pipe); + } +#else + case AddressFamily::Unix: + { + std::string domainPath{ "/tmp/" + m_address + ".socket" }; + domainPath.resize(MAX_UNIX_PATH_LENGTH); + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + strcpy(address.sun_path, domainPath.c_str()); + return ::connect(m_socket, reinterpret_cast(&address), sizeof(address)) == 0; + } +#endif + case AddressFamily::IPv4: + { + std::optional ipv4{ IPv4Address::parse(m_address) }; + if(!ipv4) + { + throw std::invalid_argument("Invalid IPv4 Address"); + } + struct sockaddr_in address; + memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_port = m_port; +#ifdef _WIN32 + address.sin_addr.S_un.S_un_b.s_b1 = ipv4->getFirst(); + address.sin_addr.S_un.S_un_b.s_b2 = ipv4->getSecond(); + address.sin_addr.S_un.S_un_b.s_b3 = ipv4->getThird(); + address.sin_addr.S_un.S_un_b.s_b4 = ipv4->getFourth(); +#else + address.sin_addr.s_addr = ipv4->getNetworkByteOrder(); +#endif + return ::connect(m_socket, reinterpret_cast(&address), sizeof(address)) == 0; + } + } + } + return false; + } + + bool Socket::disconnect() + { +#ifdef _WIN32 + if(m_family == AddressFamily::Pipe && m_purpose == SocketPurpose::Server) + { + return DisconnectNamedPipe(m_pipe); + } +#endif + if(!isSocketValid(m_child)) + { + return false; + } +#ifdef _WIN32 + closesocket(m_child); + m_child = INVALID_SOCKET; +#else + close(m_child); + m_child = -1; +#endif + return true; + } + + std::string Socket::receiveMessage() const + { + std::vector buffer(1024); + std::string message; +#ifdef _WIN32 + if(m_family == AddressFamily::Pipe) + { + DWORD bytes; + do + { + if(!ReadFile(m_pipe, &buffer[0], DWORD(buffer.size()), &bytes, nullptr)) + { + break; + } + message += std::string(&buffer[0], static_cast(bytes)); + } while (bytes > 0); + return message; + } + SOCKET socket{ m_socket }; +#else + int socket{ m_socket }; +#endif + if(m_purpose == SocketPurpose::Server && isSocketValid(m_child)) + { + socket = m_child; + } +#ifdef _WIN32 + int bytes{ 0 }; +#else + ssize_t bytes{ 0 }; +#endif + do + { + bytes = recv(socket, &buffer[0], buffer.size(), 0); + if(bytes > 0) + { + message += std::string(&buffer[0], static_cast(bytes)); + } + } while(bytes > 0); + return message; + } + + bool Socket::sendMessage(const std::string& message) const + { +#ifdef _WIN32 + if(m_family == AddressFamily::Pipe) + { + return WriteFile(m_pipe, message.c_str(), DWORD(message.size()), nullptr, nullptr); + } + SOCKET socket{ m_socket }; +#else + int socket{ m_socket }; +#endif + if(m_purpose == SocketPurpose::Server && isSocketValid(m_child)) + { + socket = m_child; + } + return static_cast(send(socket, message.c_str(), message.size(), 0)) == message.size(); + } +} diff --git a/src/system/process.cpp b/src/system/process.cpp index a166911..eb07d4f 100644 --- a/src/system/process.cpp +++ b/src/system/process.cpp @@ -157,7 +157,7 @@ namespace Nickvision::System std::cerr << CodeHelpers::getLastSystemError() << std::endl; return false; } - //child + //Child else if(m_pid == 0) { //Create process arguments @@ -305,4 +305,4 @@ namespace Nickvision::System lock.unlock(); m_exited.invoke({ m_exitCode, m_output }); } -} \ No newline at end of file +} diff --git a/tests/ipctests.cpp b/tests/ipctests.cpp index a3987cf..56aa27b 100644 --- a/tests/ipctests.cpp +++ b/tests/ipctests.cpp @@ -24,6 +24,7 @@ class IPCTest : public testing::Test static void TearDownTestSuite() { m_ipc->commandReceived() -= m_handlerId; + m_ipc.reset(); } private: @@ -50,6 +51,6 @@ TEST_F(IPCTest, EnsureServerReceived) { while(!m_received) { - std::this_thread::sleep_for(std::chrono::seconds(2)); + std::this_thread::sleep_for(std::chrono::seconds(1)); } -} \ No newline at end of file +}