diff --git a/RPC.md b/RPC.md new file mode 100644 index 0000000..4459b78 --- /dev/null +++ b/RPC.md @@ -0,0 +1,95 @@ +# RPC + +icsscand contains an RPC endpoint that can be used to control the libicsneo +instance within the daemon. To enable the RPC channel, add `--fifo-path ` +during launching. The provided path will be used to open a named FIFO, the path +should not exist prior to launching icsscand. + +## lock_networks + +Invokes `icsneo::Device::lockNetworks()` with the provided space separated +arguments and writes the results of the call to the provided client FIFO. This +call in non-blocking, with the return type indicating that the request was sent +to the device (it does not indicate that the network was successfully locked). +Use `get_network_mutex_status` to check the status of the lock request. + +### Usage + +- Request: ` lock_networks ` +- Response: `<0 or 1, with 0 indicating an error and 1 indicating success>` + - On error, check the icsscand logs for more detailed information + +### Example + +- API version: 1 +- Service serial: ON0123 +- Networks: `ETHERNET_01` & `ETHERNET_02` + - See `icsneo::Network::NetID` for values +- Priority: 2 +- TTL (in ms): 1s +- Opened client FIFO path: `/tmp/tmp.AYkIg0b3np` + - This FIFO must be opened prior to invoking the RPC + +`1 lock_networks ON0123 93,520 2 1000 /tmp/tmp.AYkIg0b3np` + +## unlock_networks + +Invokes `icsneo::Device::unlockNetworks()` with the provided space separated +arguments and writes the results of the call to the provided client FIFO. + +### Usage + +- Request: ` unlock_networks ` +- Response: `<0 or 1, with 0 indicating an error and 1 indicating success>` + - On error, check the icsscand logs for more detailed information + +### Example + +- API version: 1 +- Service serial: ON0123 +- Networks: `ETHERNET_01` & `ETHERNET_02` + - See `icsneo::Network::NetID` for values +- Opened client FIFO path: `/tmp/tmp.KXY8SCXtux` + - This FIFO must be opened prior to invoking the RPC + +`1 lock_networks ON0123 93,520 /tmp/tmp.KXY8SCXtux` + +## get_network_mutex_status + +Invokes `icsneo::Device::getNetworkMutexStatus()` with the provided space separated +arguments and writes the results of the call to the provided client FIFO. + +### Usage + +- Request: ` get_network_mutex_status ` +- Response (one of): + - `0`, error, check the icsscand logs for more detailed information + - `1 ` + +### Example + +- API version: 1 +- Service serial: ON0123 +- Network: `ETHERNET_01` + - See `icsneo::Network::NetID` for values +- Opened client FIFO path: `/tmp/tmp.4huaosZjhA` + - This FIFO must be opened prior to invoking the RPC + +`1 get_network_mutex_status ON0123 93 /tmp/tmp.4huaosZjhA` + +## get_serials + +Returns a space separated list of devices serial numbers that isscand has open. + +### Usage + +- Request: ` get_serials ` +- Response: `<0 or 1, with 0 indicating an error and 1 indicating success> [serial]...` + +### Example + +- API version: 1 +- Opened client FIFO path: `/tmp/tmp.bBcUh5obRK` + - This FIFO must be opened prior to invoking the RPC + +`1 get_serials /tmp/tmp.bBcUh5obRK` diff --git a/src/main.cpp b/src/main.cpp index f7105e0..8215ceb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include @@ -58,6 +60,7 @@ int sharedMemSize = 0; // From driver void* sharedMemory = nullptr; std::string serialFilter; int scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS; +std::string fifoPath; std::atomic stopRunning(false); @@ -203,6 +206,327 @@ std::vector openDevices; std::vector failedToOpen; std::mutex openDevicesMutex; +class RPC { +public: + RPC() { + if(::mkfifo(fifoPath.c_str(), 0777) == -1) { + throw std::runtime_error("Error creating RPC FIFO: " + std::string(strerror(errno))); + return; + } + ::chmod(fifoPath.c_str(), 0777); + fifo = ::open(fifoPath.c_str(), O_RDWR); + if(fifo == -1) { + throw std::runtime_error("Error opening RPC FIFO: " + std::string(strerror(errno))); + return; + } + interrupt = ::eventfd(0, 0); + if(interrupt == -1) { + throw std::runtime_error("Error opening eventfd: " + std::string(strerror(errno))); + return; + } + thread = std::thread(&RPC::loop, this); + } + + ~RPC() { + uint64_t u = 1; + ::write(interrupt, &u, sizeof(uint64_t)); + thread.join(); + ::close(fifo); + ::close(interrupt); + ::unlink(fifoPath.c_str()); + } + +private: + int fifo; + int interrupt; + std::thread thread; + + void loop() { + static char buffer[2048]; + struct pollfd fds[2] = {}; + fds[0].fd = fifo; + fds[0].events = POLLIN; + fds[1].fd = interrupt; + fds[1].events = POLLIN; + while (!stopRunning) { + if(::poll(fds, 2, -1) == -1) { + LOGF(LOG_WARNING, "Error polling for RPC: %s\n", strerror(errno)); + break; + } + if(fds[1].revents & POLLIN) { + break; + } + const auto size = ::read(fifo, buffer, sizeof(buffer)); + if (size == -1) { + LOGF(LOG_WARNING, "Error reading from RPC FIFO: %s\n", strerror(errno)); + break; + } + const auto args = split(std::string(buffer, size)); + if(args.size() < 2) { + continue; + } + const auto& apiVersion = args[0]; + const auto& command = args[1]; + if(apiVersion != "1") { + LOGF(LOG_WARNING, "Invalid API version, expected '1' got '%s'\n", apiVersion.c_str()); + continue; + } + if(command == "lock_networks") { + lockNetworks(args); + } else if(command == "unlock_networks") { + unlockNetworks(args); + } else if(command == "get_network_mutex_status") { + getNetworkMutexStatus(args); + } else if(command == "get_devices") { + getSerials(args); + } else { + LOGF(LOG_WARNING, "Unknown command '%s'\n", command.c_str()); + } + } + } + + bool parseNetid(const std::string& arg, icsneo::Network::NetID& netid) { + try { + netid = static_cast(std::stoi(arg)); + return true; + } catch (const std::exception& e) { + LOGF(LOG_WARNING, "Invalid netid '%s': %s\n", arg.c_str(), e.what()); + return false; + } + } + + bool parseNetids(const std::string& arg, std::set& netids) { + for(auto&& str : split(arg, ',')) { + icsneo::Network::NetID nid; + if(!parseNetid(str, nid)) { + return false; + } + netids.emplace(nid); + } + return true; + } + + template + std::string optionalArg(const std::optional& opt) { + return opt ? std::to_string((uint64_t)*opt) : "-1"; + } + + + bool fifoWrite(const std::string& message, const std::string& fifoPath) { + int fifo = ::open(fifoPath.c_str(), O_WRONLY); + if(fifo == -1) { + return false; + } + if(::write(fifo, message.c_str(), message.size()) == -1) { + ::close(fifo); + return false; + } + ::close(fifo); + return true; + } + + std::vector split(const std::string& str, char delim = ' ') { + if(str.empty()) + return {}; + std::vector ret; + size_t tail = 0; + size_t head = 0; + while(head < str.size()) { + if(str[head] == delim) { + ret.emplace_back(&str[tail], head - tail); + tail = head + 1; + } + ++head; + } + ret.emplace_back(&str[tail], head - tail); + return ret; + } + + std::string join(const std::vector& parts, char delim = ' ') { + if(parts.empty()) + return ""; + std::string ret = parts[0]; + for(size_t i = 1; i < parts.size(); i++) { + ret += delim + parts[i]; + } + return ret; + } + + void lockNetworks(const std::vector& args) { + if(args.size() != 8) { + LOGF(LOG_WARNING, "lock_networks requires 8 arguments, got %zu\n", args.size()); + return; + } + const auto& serial = args[2]; + const auto& clientFifoPath = args[7]; + + std::set netids; + if(!parseNetids(args[3], netids)) { + fifoWrite("0", clientFifoPath); + return; + } + + uint32_t priority; + try { + priority = std::stoul(args[4]); + } catch (const std::exception& e) { + LOGF(LOG_WARNING, "Invalid priority '%s': %s\n", args[4].c_str(), e.what()); + fifoWrite("0", clientFifoPath); + return; + } + + uint32_t ttl; + try { + ttl = std::stoul(args[5]); + } catch (const std::exception& e) { + LOGF(LOG_WARNING, "Invalid TTL '%s': %s\n", args[5].c_str(), e.what()); + fifoWrite("0", clientFifoPath); + return; + } + + icsneo::NetworkMutexType type; + try { + type = static_cast(std::stoi(args[6])); + } catch (const std::exception& e) { + LOGF(LOG_WARNING, "Invalid mutex type '%s': %s\n", args[6].c_str(), e.what()); + fifoWrite("0", clientFifoPath); + return; + } + + std::lock_guard lg(openDevicesMutex); + std::shared_ptr device; + for(const auto& dev : openDevices) { + if(dev.device->getSerial() == serial) { + device = dev.device; + break; + } + } + if(!device) { + LOGF(LOG_WARNING, "Device with serial '%s' not found\n", serial.c_str()); + fifoWrite("0", clientFifoPath); + return; + } + const auto locked = device->lockNetworks(netids, priority, ttl, type, [serial](std::shared_ptr msg) -> void { + auto mutexMsg = std::dynamic_pointer_cast(msg); + if(!mutexMsg) { + LOG(LOG_WARNING, "Received a message for the network mutex callback which was not a NetworkMutexMessage\n"); + return; + } + LOGF(LOG_INFO, "Received network mutex event for device %s\n", serial.c_str()); + }); + if(!locked) { + LOGF(LOG_WARNING, "Failed to lock networks for device '%s'\n", icsneo::GetLastError().describe().c_str()); + fifoWrite("0", clientFifoPath); + return; + } + device->removeMessageCallback(*locked); // client will explicitly poll status + fifoWrite("1", clientFifoPath); + } + + void unlockNetworks(const std::vector& args) { + if(args.size() != 5) { + LOGF(LOG_WARNING, "unlock_networks requires 5 arguments, got %zu\n", args.size()); + return; + } + const auto& serial = args[2]; + const auto& clientFifoPath = args[4]; + + std::set netids; + if(!parseNetids(args[3], netids)) { + fifoWrite("0", clientFifoPath); + return; + } + + std::lock_guard lg(openDevicesMutex); + std::shared_ptr device; + for(const auto& dev : openDevices) { + if(dev.device->getSerial() == serial) { + device = dev.device; + break; + } + } + if(!device) { + LOGF(LOG_WARNING, "Device with serial '%s' not found\n", serial.c_str()); + fifoWrite("0", clientFifoPath); + return; + } + + const auto success = device->unlockNetworks(netids); + if(!success) { + LOGF(LOG_WARNING, "Failed to unlock networks for device '%s': %s\n", serial.c_str(), icsneo::GetLastError().describe().c_str()); + fifoWrite("0", clientFifoPath); + return; + } + fifoWrite("1", clientFifoPath); + } + + void getNetworkMutexStatus(const std::vector& args) { + if(args.size() != 5) { + LOGF(LOG_WARNING, "get_network_mutex_status requires 5 arguments, got %zu\n", args.size()); + return; + } + const auto& serial = args[2]; + const auto& clientFifoPath = args[4]; + + icsneo::Network::NetID netid; + if(!parseNetid(args[3], netid)) { + fifoWrite("0", clientFifoPath); + return; + } + + std::lock_guard lg(openDevicesMutex); + std::shared_ptr device; + for(const auto& dev : openDevices) { + if(dev.device->getSerial() == serial) { + device = dev.device; + break; + } + } + if(!device) { + LOGF(LOG_WARNING, "Device with serial '%s' not found\n", serial.c_str()); + fifoWrite("0", clientFifoPath); + return; + } + + const auto status = device->getNetworkMutexStatus(netid); + + if(!status) { + LOGF(LOG_WARNING, "Failed to get network mutex status for device '%s': %s\n", serial.c_str(), icsneo::GetLastError().describe().c_str()); + fifoWrite("0", clientFifoPath); + return; + } + + const std::string id = optionalArg(status->owner_id); + const std::string type = optionalArg(status->type); + const std::string priority = optionalArg(status->priority); + const std::string ttl = optionalArg(status->ttlMs); + std::vector networkStrs; + for(const auto& net : status->networks) { + networkStrs.push_back(std::to_string((neonetid_t)net)); + } + const std::string networks = join(networkStrs, ','); + const std::string event = optionalArg(status->event); + + const std::string response = join({"1", id, type, priority, ttl, networks, event}); + + fifoWrite(response, clientFifoPath); + } + + void getSerials(const std::vector& args) { + if(args.size() != 3) { + LOGF(LOG_WARNING, "get_serials requires 3 arguments, got %zu\n", args.size()); + return; + } + const auto& clientFifoPath = args[2]; + std::string response = "1"; + std::lock_guard lg(openDevicesMutex); + for(const auto& dev : openDevices) { + response += ' ' + dev.device->getSerial(); + } + fifoWrite(response, clientFifoPath); + } +}; + std::string& replaceInPlace(std::string& str, char o, const std::string& n) { size_t start_pos = 0; const size_t new_len = n.length(); @@ -247,6 +571,7 @@ void usage(std::string executableName) { std::cerr << "\t --devices\t\t\tList supported devices\n"; std::cerr << "\t --filter \t\tOnly connect to devices with serial\n\t\t\t\t\t\tnumbers starting with this filter\n"; std::cerr << "\t --scan-interval-ms \tDevice scan interval in ms\n\t\t\t\t\t\tIf 0, only a single scan is performed\n"; + std::cerr << "\t --fifo-path \t\tPath to RPC FIFO for libicsneo control\n"; } void terminateSignal(int signal) { @@ -339,29 +664,23 @@ void searchForDevices() { } // Create rx listener - newDevice.device->addMessageCallback(std::make_shared([serial](std::shared_ptr message) { - const auto frame = std::static_pointer_cast(message); - const auto messageType = frame->network.getType(); - const OpenDevice* openDevice = nullptr; - std::lock_guard lg(openDevicesMutex); - for(const auto& dev : openDevices) { - if(dev.device->getSerial() == serial) { - openDevice = &dev; - break; + for(auto&& interface : newDevice.interfaces) { + newDevice.device->addMessageCallback(std::make_shared([interface = interface.second](std::shared_ptr message) { + const auto frame = std::static_pointer_cast(message); + const auto messageType = frame->network.getType(); + if(frame->type != icsneo::Message::Type::Frame) { + LOG(LOG_ERR, "Dropping message: received invalid message type, expected RawMessage\n"); + return; } - } - if(frame->type != icsneo::Message::Type::Frame) { - LOG(LOG_ERR, "Dropping message: received invalid message type, expected RawMessage\n"); - return; - } - - if(messageType == icsneo::Network::Type::CAN) { - openDevice->interfaces.at(frame->network.getNetID())->addReceivedMessageToQueue(frame); - } else if(messageType == icsneo::Network::Type::Ethernet) { - openDevice->interfaces.at(frame->network.getNetID())->addReceivedMessageToQueue(frame); - } else - LOG(LOG_ERR, "Dropping message, only CAN and Ethernet are currently supported\n"); - })); + if(messageType == icsneo::Network::Type::CAN) { + interface->addReceivedMessageToQueue(frame); + } else if(messageType == icsneo::Network::Type::Ethernet) { + interface->addReceivedMessageToQueue(frame); + } else { + LOG(LOG_ERR, "Dropping message, only CAN and Ethernet are currently supported\n"); + } + }, std::make_shared(interface.first))); + } LOGF(LOG_INFO, "%s connected\n", newDevice.device->describe().c_str()); failedToOpen.erase(std::remove_if(failedToOpen.begin(), failedToOpen.end(), [&serial](const std::string& s) -> bool { @@ -451,6 +770,8 @@ int main(int argc, char** argv) { std::cerr << "Invalid input for scan-interval-ms\n"; return EX_USAGE; } + } else if(arg == "--fifo-path" && i + 1 <= argc) { + fifoPath = argv[++i]; } else { usage(argv[0]); return EX_USAGE; @@ -533,6 +854,16 @@ int main(int argc, char** argv) { LOG(LOG_INFO, "Waiting for connections...\n"); } + std::unique_ptr rpc; + if(!fifoPath.empty()) { + try { + rpc = std::make_unique(); + } catch (const std::exception& e) { + LOGF(LOG_ERR, "Failed to set up RPC: %s\n", e.what()); + return EXIT_FAILURE; + } + } + std::thread searchThread(deviceSearchThread); while(!stopRunning) {