Commit a2c88fa3 authored by aligungr's avatar aligungr

CLI executable implemented

parent bbe11f57
......@@ -73,4 +73,5 @@ target_link_libraries(nr-cli pthread)
target_compile_options(nr-cli PRIVATE -Wall -Wextra -pedantic)
target_link_libraries(nr-cli app)
target_link_libraries(nr-cli udp)
target_link_libraries(nr-cli utils)
//
// This file is a part of UERANSIM open source project.
// Copyright (c) 2021 ALİ GÜNGÖR.
//
// The software and all associated files are licensed under GPL-3.0
// and subject to the terms and conditions defined in LICENSE file.
//
#include "base_app.hpp"
#include <atomic>
#include <csignal>
#include <cstdio>
#include <exception>
#include <iostream>
#include <utils/constants.hpp>
#include <vector>
static std::atomic_int g_instanceCount{};
static std::vector<void (*)()> g_runAtExit{};
static std::vector<std::string> g_deleteAtExit{};
extern "C" void BaseSignalHandler(int num)
{
for (auto &fun : g_runAtExit)
fun();
for (auto &file : g_deleteAtExit)
std::remove(file.c_str());
if (num == SIGTERM || num == SIGINT)
exit(0);
}
namespace app
{
void Initialize()
{
if (g_instanceCount++ != 0)
std::terminate();
std::cout << cons::Name << std::endl;
std::signal(SIGTERM, BaseSignalHandler);
std::signal(SIGINT, BaseSignalHandler);
}
void RunAtExit(void (*fun)())
{
g_runAtExit.push_back(fun);
}
void DeleteAtExit(const std::string &file)
{
g_deleteAtExit.push_back(file);
}
} // namespace app
//
// This file is a part of UERANSIM open source project.
// Copyright (c) 2021 ALİ GÜNGÖR.
//
// The software and all associated files are licensed under GPL-3.0
// and subject to the terms and conditions defined in LICENSE file.
//
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
namespace app
{
void Initialize();
void RunAtExit(void (*fun)());
void DeleteAtExit(const std::string &file);
} // namespace app
//
// This file is a part of UERANSIM open source project.
// Copyright (c) 2021 ALİ GÜNGÖR.
//
// The software and all associated files are licensed under GPL-3.0
// and subject to the terms and conditions defined in LICENSE file.
//
#include "node_cli.hpp"
#include <utils/octet_string.hpp>
#include <utils/octet_view.hpp>
#define CMD_BUFFER_SIZE 8192
#define CMD_RCV_TIMEOUT 2500
#define CMD_MIN_LENGTH (3 + 4 + 4 + 1)
namespace app
{
InetAddress CliServer::assignedAddress() const
{
return m_socket.getAddress();
}
CliMessage CliServer::receiveMessage()
{
uint8_t buffer[CMD_BUFFER_SIZE] = {0};
InetAddress address;
int size = m_socket.receive(buffer, CMD_BUFFER_SIZE, CMD_RCV_TIMEOUT, address);
if (size < CMD_MIN_LENGTH || size >= CMD_BUFFER_SIZE)
return {};
OctetView v{buffer, static_cast<size_t>(size)};
if (v.readI() != cons::Major)
return {};
if (v.readI() != cons::Minor)
return {};
if (v.readI() != cons::Patch)
return {};
CliMessage res{};
res.type = static_cast<CliMessage::Type>(v.readI());
int nodeNameLength = v.read4I();
res.nodeName = v.readUtf8String(nodeNameLength);
int valueLength = v.read4I();
res.value = v.readUtf8String(valueLength);
res.clientAddr = address;
return res;
}
void CliServer::sendMessage(const CliMessage &msg)
{
OctetString stream{};
stream.appendOctet(cons::Major);
stream.appendOctet(cons::Minor);
stream.appendOctet(cons::Patch);
stream.appendOctet(static_cast<int>(msg.type));
stream.appendOctet4(static_cast<size_t>(msg.nodeName.size()));
for (char c : msg.nodeName)
stream.appendOctet(static_cast<uint8_t>(c));
stream.appendOctet4(static_cast<size_t>(msg.value.size()));
for (char c : msg.value)
stream.appendOctet(static_cast<uint8_t>(c));
m_socket.send(msg.clientAddr, stream.data(), static_cast<size_t>(stream.length()));
}
} // namespace app
//
// This file is a part of UERANSIM open source project.
// Copyright (c) 2021 ALİ GÜNGÖR.
//
// The software and all associated files are licensed under GPL-3.0
// and subject to the terms and conditions defined in LICENSE file.
//
#pragma once
#include <string>
#include <udp/server.hpp>
#include <utility>
#include <utils/constants.hpp>
#include <utils/network.hpp>
#include <vector>
namespace app
{
struct CliMessage
{
enum class Type
{
EMPTY = 0,
ECHO,
ERROR,
RESULT,
COMMAND
} type{};
std::string nodeName{};
std::string value{};
InetAddress clientAddr{};
static CliMessage Error(InetAddress addr, std::string msg, std::string node = "")
{
CliMessage m{};
m.type = Type::ERROR;
m.value = std::move(msg);
m.nodeName = std::move(node);
m.clientAddr = addr;
return m;
}
static CliMessage Result(InetAddress addr, std::string msg, std::string node = "")
{
CliMessage m{};
m.type = Type::RESULT;
m.value = std::move(msg);
m.nodeName = std::move(node);
m.clientAddr = addr;
return m;
}
static CliMessage Echo(InetAddress addr, std::string msg)
{
CliMessage m{};
m.type = Type::ECHO;
m.value = std::move(msg);
m.nodeName = "";
m.clientAddr = addr;
return m;
}
static CliMessage Command(InetAddress addr, std::string msg, std::string node = "")
{
CliMessage m{};
m.type = Type::COMMAND;
m.value = std::move(msg);
m.nodeName = std::move(node);
m.clientAddr = addr;
return m;
}
};
class CliServer
{
private:
Socket m_socket;
public:
explicit CliServer() : m_socket{Socket::CreateAndBindUdp({cons::CMD_SERVER_IP, 0})}
{
}
~CliServer()
{
m_socket.close();
}
[[nodiscard]] InetAddress assignedAddress() const;
CliMessage receiveMessage();
void sendMessage(const CliMessage &msg);
};
} // namespace app
//
// This file is a part of UERANSIM open source project.
// Copyright (c) 2021 ALİ GÜNGÖR.
//
// The software and all associated files are licensed under GPL-3.0
// and subject to the terms and conditions defined in LICENSE file.
//
#include "proc_table.hpp"
#include "base_app.hpp"
#include <filesystem>
#include <fstream>
#include <ostream>
#include <sstream>
#include <sys/types.h>
#include <unistd.h>
#include <utils/common.hpp>
#include <utils/constants.hpp>
#include <utils/io.hpp>
namespace app
{
void CreateProcTable(const std::vector<std::string> &nodes, int cmdPort)
{
try
{
io::CreateDirectory(cons::PROC_TABLE_DIR);
std::string filePath;
while (true)
{
std::string fileName = utils::IntToHex(utils::Random64()) + utils::IntToHex(utils::Random64());
filePath = cons::PROC_TABLE_DIR + fileName;
if (!io::Exists(filePath))
break;
}
ProcTableEntry entry{};
entry.major = cons::Major;
entry.minor = cons::Minor;
entry.patch = cons::Patch;
entry.pid = static_cast<int>(::getpid());
entry.port = cmdPort;
entry.nodes = nodes;
io::WriteAllText(filePath, ProcTableEntry::Encode(entry));
DeleteAtExit(filePath);
}
catch (const std::runtime_error &e)
{
throw std::runtime_error("ProcTable could not be created: " + std::string(e.what()));
}
}
std::string ProcTableEntry::Encode(const ProcTableEntry &e)
{
std::stringstream ss{""};
ss << e.major << " ";
ss << e.minor << " ";
ss << e.patch << " ";
ss << e.pid << " ";
ss << e.port << " ";
ss << e.nodes.size() << " ";
for (auto &n : e.nodes)
{
utils::AssertNodeName(n);
ss << n << " ";
}
return ss.str();
}
ProcTableEntry ProcTableEntry::Decode(const std::string &s)
{
ProcTableEntry e{};
std::stringstream ss{s};
ss >> e.major;
ss >> e.minor;
ss >> e.patch;
ss >> e.pid;
ss >> e.port;
size_t nodeSize = 0;
ss >> nodeSize;
for (size_t i = 0; i < nodeSize; i++)
{
std::string n{};
ss >> n;
e.nodes.push_back(std::move(n));
}
return e;
}
} // namespace app
\ No newline at end of file
//
// This file is a part of UERANSIM open source project.
// Copyright (c) 2021 ALİ GÜNGÖR.
//
// The software and all associated files are licensed under GPL-3.0
// and subject to the terms and conditions defined in LICENSE file.
//
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
namespace app
{
struct ProcTableEntry
{
int major{};
int minor{};
int patch{};
int pid{};
uint16_t port{};
std::vector<std::string> nodes{};
// ... (New members must always be added to the end)
static std::string Encode(const ProcTableEntry &e);
static ProcTableEntry Decode(const std::string &s);
};
void CreateProcTable(const std::vector<std::string> &nodes, int cmdPort);
template <typename T>
inline void CreateProcTable(const std::unordered_map<std::string, T> &nodeMap, int cmdPort)
{
std::vector<std::string> nodes{};
for (auto &node : nodeMap)
nodes.push_back(node.first);
CreateProcTable(nodes, cmdPort);
}
} // namespace app
//
// This file is a part of UERANSIM open source project.
// Copyright (c) 2021 ALİ GÜNGÖR.
//
// The software and all associated files are licensed under GPL-3.0
// and subject to the terms and conditions defined in LICENSE file.
//
#include <algorithm>
#include <app/node_cli.hpp>
#include <app/proc_table.hpp>
#include <filesystem>
#include <iostream>
#include <set>
#include <string>
#include <thread>
#include <unordered_map>
#include <utils/common.hpp>
#include <utils/constants.hpp>
#include <utils/io.hpp>
#include <utils/network.hpp>
#include <utils/options.hpp>
#include <vector>
static struct Options
{
bool dumpNodes{};
std::string nodeName{};
std::string directCmd{};
} g_options{};
static std::set<int> FindProcesses()
{
std::set<int> res{};
for (const auto &file : std::filesystem::directory_iterator(cons::PROCESS_DIR))
{
if (file.is_directory())
{
auto name = file.path().stem().string();
if (!utils::IsNumeric(name))
continue;
int pid = utils::ParseInt(name);
res.insert(pid);
}
}
return res;
}
static uint16_t DiscoverNode(const std::string &node)
{
if (!io::Exists(cons::PROC_TABLE_DIR))
return 0;
// Find all processes in the environment
auto processes = FindProcesses();
// Read and parse ProcTable entries.
std::unordered_map<std::string, app::ProcTableEntry> entries{};
for (const auto &file : std::filesystem::directory_iterator(cons::PROC_TABLE_DIR))
{
if (file.is_directory())
continue;
std::string path = file.path().string();
std::string content = io::ReadAllText(path);
entries[file.path()] = app::ProcTableEntry::Decode(content);
}
for (auto &e : entries)
{
// If no such process, it means that this ProcTable file is outdated
// Therefore that file should be deleted
if (processes.count(e.second.pid) == 0)
{
io::Remove(e.first);
continue;
}
// If searching node exists in this file, extract port number from it.
for (auto &n : e.second.nodes)
{
if (n == node)
return e.second.port;
}
}
// No process found for this node name
return 0;
}
static std::vector<std::string> DumpNames()
{
std::vector<std::string> v{};
if (!io::Exists(cons::PROC_TABLE_DIR))
return v;
// Find all processes in the environment
auto processes = FindProcesses();
// Read and parse ProcTable entries.
std::unordered_map<std::string, app::ProcTableEntry> entries{};
for (const auto &file : std::filesystem::directory_iterator(cons::PROC_TABLE_DIR))
{
if (file.is_directory())
continue;
std::string path = file.path().string();
std::string content = io::ReadAllText(path);
entries[file.path()] = app::ProcTableEntry::Decode(content);
}
for (auto &e : entries)
{
// If no such process, it means that this ProcTable file is outdated
// Therefore that file should be deleted
if (processes.count(e.second.pid) == 0)
{
io::Remove(e.first);
continue;
}
for (auto &n : e.second.nodes)
v.push_back(n);
}
std::sort(v.begin(), v.end());
return v;
}
static void ReadOptions(int argc, char **argv)
{
opt::OptionsDescription desc{"UERANSIM", cons::Tag, "Command Line Interface",
"ALİ GÜNGÖR", "nr-cli", {"<node-name> [option...]", "--dump"}};
opt::OptionItem itemDump = {'d', "dump", "List all UE and gNBs in the environment", std::nullopt};
opt::OptionItem itemExec = {'e', "exec", "Execute the given command directly without an interactive shell",
"command"};
desc.items.push_back(itemDump);
desc.items.push_back(itemExec);
opt::OptionsResult opt{argc, argv, desc};
g_options.dumpNodes = opt.hasFlag(itemDump);
if (!g_options.dumpNodes)
{
if (opt.positionalCount() == 0)
opt.error("Node name is expected");
if (opt.positionalCount() > 1)
opt.error("Only one node name is expected");
g_options.nodeName = opt.getPositional(0);
if (g_options.nodeName.size() < cons::MinNodeName)
opt.error("Node name is too short");
if (g_options.nodeName.size() > cons::MaxNodeName)
opt.error("Node name is too long");
g_options.directCmd = opt.getOption(itemExec);
if (opt.hasFlag(itemExec) && g_options.directCmd.size() < 3)
opt.error("Command is too short");
}
}
static void HandleMessage(const app::CliMessage &msg, bool isOneShot)
{
if (msg.type == app::CliMessage::Type::ERROR)
{
std::cerr << "ERROR: " << msg.value << std::endl;
if (isOneShot)
exit(1);
}
if (msg.type == app::CliMessage::Type::ECHO)
{
std::cout << msg.value << std::endl;
}
if (msg.type == app::CliMessage::Type::RESULT)
{
std::cout << msg.value << std::endl;
if (isOneShot)
exit(0);
}
}
[[noreturn]] static void SendCommand(uint16_t port)
{
app::CliServer server{};
if (g_options.directCmd.empty())
{
std::thread receiver{[&server]() {
while (true)
HandleMessage(server.receiveMessage(), false);
}};
while (true)
{
std::string line{};
std::getline(std::cin, line);
if (!std::cin)
exit(0);
server.sendMessage(
app::CliMessage::Command(InetAddress{cons::CMD_SERVER_IP, port}, line, g_options.nodeName));
}
}
else
{
server.sendMessage(
app::CliMessage::Command(InetAddress{cons::CMD_SERVER_IP, port}, g_options.directCmd, g_options.nodeName));
while (true)
HandleMessage(server.receiveMessage(), true);
}
}
int main(int argc, char **argv)
{
ReadOptions(argc, argv);
// NOTE: This does not guarantee showing the exact realtime status.
if (g_options.dumpNodes)
{
for (auto &n : DumpNames())
std::cout << n << "\n";
std::cout.flush();
exit(0);
}
if (g_options.nodeName.empty())
{
std::cerr << "ERROR: No node name is specified" << std::endl;
exit(1);
}
if (g_options.nodeName.size() > cons::MaxNodeName)
{
std::cerr << "ERROR: Node name is too long" << std::endl;
exit(1);
}
if (g_options.nodeName.size() < cons::MinNodeName)
{
std::cerr << "ERROR: Node name is too short" << std::endl;
exit(1);
}
uint16_t cmdPort;
try
{
cmdPort = DiscoverNode(g_options.nodeName);
}
catch (const std::runtime_error &e)
{
throw std::runtime_error("Node discovery failure: " + std::string{e.what()});
}
if (cmdPort == 0)
{
std::cerr << "ERROR: No node found with name: " << g_options.nodeName << std::endl;
return 1;
}
SendCommand(cmdPort);
return 0;
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment