1 Introduction
ExternalProcess
is a simple ns-3 module to facilitate
running external processes within ns-3 simulations.
Aim & Features
Custom program execution as a process parallel to ns-3 simulations
Parallel process is started and kept alive until required
Bi-directional communication with processes based on TCP sockets
Multiple parallel processes supported (each with own
ExternalProcess
instance)Non-interfering with Unix signals: watchdog thread for process supervision (Thanks @vincenzosu)
Note: This module is currently NOT intended for processes that have carry out operations asynchronously to the ns-3 simulation.
2 Installation guidelines
2.1 Requirements
This module has been developed on Ubuntu 18.04 LTS and Ubuntu 22.04 LTS with installed ns-3 versions respectively being ns-3.33 and ns-3.40.
System requirements:
Default ns-3 requirements (more details)
OS supporting POSIX standard with GNU extensions
- Dependencies:
pthread_timedjoin_np()
(more details)
- Dependencies:
Boost libraries 1.66 or later (more details)
Dependencies: ASIO (
io_context
), Boost.SystemPlease ensure that ns-3 is built against the correct version of the library prior to installing
ExternalProcess
2.2 Installation
This module supports both ns-3 build systems (namely Waf, used until version 3.35, and CMake, from 3.36 onwards), and the following instructions apply to either.
Download or clone the contents of this repository
git clone https://github.com/emanuelegiona/ns3-ext-process.git
Enter the cloned directory and copy the
ext-process
directory into your ns-3 source tree, under thesrc
orcontrib
directorycd ns3-ext-process cp -r <path/to/ns3/installation>/contrib/
Configure & build ns-3
From ns-3.36 and later versions (CMake)
cd <path/to/ns3/installation> ./ns3 configure ./ns3 build
Versions up to ns-3.35 (Waf)
cd <path/to/ns3/installation> ./waf configure ./waf build
2.3 Test your installation
This module includes an ns-3 test suite named
ext-process
, which also doubles as usage example.
The external process is a simple echo TCP client implemented in Python.
Test requirements
Correct path to the external process’s launcher script in file
ext-process/test/ext-process-test-suite.cc
:// Path to the launcher script handling external process's execution std::string launcherPath = "<path/to/ns3/installation>/contrib/ext-process/launcher-py.sh";
Python 3
Note that this requirement is only necessary due to implementation of
ext-process/echo.py
, i.e. the external process used in this example. Processes leveraging binaries of a different nature may not enforce this requirement.
Launching the test
The test suite can be executed by prompting the following commands into a terminal:
cd <path/to/ns3/installation>
./test.py -n -s ext-process
A correct installation would present the following output:
[0/1] PASS: TestSuite ext-process
1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
2.4 Compatibility across OS and ns-3 versions
ExternalProcess
is tested in development environments as
well as within Docker images.
Current version has successfully passed tests in the following environments:
Ubuntu 18.04, ns-3.35 (Docker)
- Note:
libboost
is available in version 1.65.1; for testing purposes only, it has been upgraded to version 1.74 via unofficial repositoryppa:mhier/libboost-latest
(more details)
- Note:
Ubuntu 20.04, ns-3.40 (Docker)
Ubuntu 22.04, ns-3.41 (Docker)
More details on Docker images used at this page.
3 Usage
3.1 Handbook
This section will briefly present how to use
ExternalProcess
for your ns-3 simulation scripts or
modules.
Creation of an
ExternalProcess
instance<ExternalProcess> myExtProc = CreateObjectWithAttributes<ExternalProcess>( Ptr"Launcher", StringValue("<path/to/executable>"), // Mandatory (preferably an absolute path) "CliArgs", StringValue("--attempts 10 --debug True"), // Optional: CLI arguments for launcher script (depend on the executable) "Port", UintegerValue(0), // Optional: default value (0) lets the OS pick a free port automatically "Timeout", TimeValue(MilliSeconds(150)), // Optional: enables timeout on socket operations (e.g. accept, write, read) "Attempts", UintegerValue(10), // Optional: enables multiple attempts for socket operations (only if timeout is non-zero) "TimedAccept", BooleanValue(true), // Optional: enables timeout on socket accept operations (see above, 'Attempts') "TimedWrite", BooleanValue(true), // Optional: enables timeout on socket write operations (see above, 'Attempts') "TimedRead", BooleanValue(true) // Optional: enables timeout on socket read operations (see above, 'Attempts') );
Explanation:
ns-3 attribute
Launcher
is mandatory at the time of invokingExternalProcess::Create(void)
: it should contain the path to an existing executable file (e.g. bash script or similar).ns-3 attribute
CliArgs
is optional and it represents a single string containing additional CLI arguments to the external process (default: empty string).The first argument passed to every executable is the TCP port used for communication (see below); the string hereby provided should contain arguments and their respective values considering that whitespace is used as a delimiter when splitting to tokens.
e.g. the string
"--attempts 10 --debug True"
results in 4 additional tokens passed to the executable:--attempts
,10
,--debug
, andTrue
ns-3 attribute
Port
is optional and it represents the port to use to accept communications via a TCP socket (default: 0, i.e. allows the OS to pick a free port).This value is automatically passed as the first parameter to your executable. The external process is thus expected to set up a TCP client and connect to IP address
127.0.0.1:<Port>
orlocalhost:<port>
.ns-3 attributes
TimedAccept
,TimedWrite
, andTimedRead
change the default behavior (i.e. blocking socket operations, also indefinitely) into a using a timeout and repeated attempts; respectively, they refer to socket’saccept()
(used inExternalProcess::Create()
),write()
, andread()
.ns-3 attributes
Timeout
andAttempts
allow customization of socket operations using timeout and repeated attempts.Note: the same settings are used throughout socket operations; in necessity of specifying different settings for each operation, users may change these values using
Object::SetAttribute()
, which is allowed during ongoing simulations too.
Execution of the external process
bool ExternalProcess::Create(void);
The return value should be checked for error handling in unsuccessful executions.
Communication towards the external process
bool ExternalProcess::Write( const std::string &str, bool first = true, bool flush = true, bool last = true );
This function sends
str
to the external process, with remaining arguments enabling some degree of optimization (e.g. in a series ofWrite
s, only flushing at the last one).Communication from the external process
bool ExternalProcess::Read(std::string &str, bool &hasNext);
This function attempts to read
str
from the external process:str
should be ignored if the return value of thisRead
equalsfalse
. If multipleRead
s are expected, thehasNext
value indicates whether to continue reading or not, proving useful to its usage as exit condition in loops.Termination of an external process
Deletion of an
ExternalProcess
instance automatically takes of this task, but it is possible to explicitly perform it at any point of the simulation.bool ExternalProcess::Teardown(void); bool ExternalProcess::Teardown(pid_t childPid); // Discouraged
The
childPid
value may be obtained from the sameExternalProcess
instance by invoking theExternalProcess::GetPid(void)
function, as implemented by functionExternalProcess::Teardown(void)
.Note: while
ExternalProcess::Teardown(void)
is thread-safe with the rest ofExternalProcess
mechanisms,ExternalProcess::Teardown(pid_t childPid)
is not; users are advised to terminate external processes only through the first function, invoked on the appropriateExternalProcess
object. More details are provided below.Termination of all external processes (e.g. simulation fatal errors)
In order to prevent external processes from living on in cases of
NS_FATAL_ERROR
being invoked by the simulation, it is possible to explicitly kill all processes via a static function.static void ExternalProcess::GracefulExit(void);
Being a static function, there is no need to retrieve any instance of
ExternalProcess
for this instruction.
3.2 Programs compatible with
ExternalProcess
In order to properly execute external processes via this module, the following considerations should be taken:
At least 1 CLI argument must be supported:
TCP port for socket connection (required)
Any number of additional arguments; see discussion above for attribute
CliArgs
(optional)
Socket connectivity as a client (
ExternalProcess
acts as server)- The TCP socket should be opened at the beginning of execution and kept alive throughout the process lifetime
Properly handle the following messaging prefixes:
// Macros for process messaging #define MSG_KILL "PROCESS_KILL" #define MSG_DELIM "<ENDSTR>" #define MSG_EOL '\n'
In particular,
MSG_KILL
is sent by the ns-3 simulation towards the external process viaWrite
s and indicates a gentle request to terminate execution. Ignoring this message results in abrupt termination using akill()
syscall.MSG_DELIM
andMSG_EOL
are used for implementing a line-based socket communication s.t.:MSG_DELIM
represents a delimiter between multiple tokens within the same line;MSG_EOL
represents the end-of-line delimiter, used any time a line is intended to be flushed through the socket.
3.3 API Documentation
3.3.1 Macros
General utility
#define CURRENT_TIME Now().As(Time::S)
Retrieves the current simulation time; typically used for logging.
Interprocess communication (IPC)
#define MSG_KILL "PROCESS_KILL"
Represents a gentle request to terminate process execution. If properly handled by the external process, it may be useful to save any temporary data before exiting.
#define MSG_DELIM "<ENDSTR>"
Represents a delimiter between multiple tokens contained within the
same line sent through the socket. This delimiter is added between any
pair of strings resulting from invoking
ExternalProcess::Write()
consecutively twice.
Example
<ExternalProcess> ep = [...];
Ptr->Create();
ep[...]
->Write("str1", true, false, false);
ep->Write("str2", false, false, true);
ep[...]
The effective string sent through the socket will be exactly 1, with
value: str1<ENDSTR>str2\n
.
#define MSG_EOL '\n'
Represents a end-of-line delimiter, used any time a line is intended to be flushed through the socket.
Example
<ExternalProcess> ep = [...];
Ptr->Create();
ep[...]
->Write("str1", true, false, false);
ep->Write("str2", false, true, false);
ep->Write("strX", false, false, false);
ep->Write("strY", false, false, true);
ep[...]
The effective strings sent through the socket will be exactly 2, with values:
str1<ENDSTR>str2\n
, andstrX<ENDSTR>strY\n
.
3.3.2 Global functions
void* WatchdogFunction(void* arg);
Function to use in the watchdog thread (POSIX). Its usage is typically transparent to the user.
void* AcceptorFunction(void* arg);
Function to use in acceptor threads (POSIX). Its usage is typically transparent to the user.
void* WriterFunction(void* arg);
Function to use in writer threads (POSIX). Its usage is typically transparent to the user.
void* ReaderFunction(void* arg);
Function to use in reader threads (POSIX). Its usage is typically transparent to the user.
3.3.3 Global variables
static WatchdogSupport g_watchdogArgs;
Represents the global variable holding arguments for the watchdog thread.
static pthread_t g_watchdog;
Watchdog thread for checking running instances.
static bool g_watchdogExit = false;
Flag indicating the exit condition for the watchdog thread.
static std::map<pid_t, ExternalProcess*> g_runnerMap;
Map associating PID to instances that spawned them.
static pthread_mutex_t g_watchdogExitMutex = PTHREAD_MUTEX_INITIALIZER;
Mutex for exit condition for the watchdog thread.
static pthread_mutex_t g_watchdogMapMutex = PTHREAD_MUTEX_INITIALIZER;
Mutex for runner map for the watchdog thread.
static pthread_mutex_t g_watchdogTeardownMutex = PTHREAD_MUTEX_INITIALIZER;
Mutex for accessing ExternalProcess::Teardown() from multiple threads.
static pthread_mutex_t g_gracefulExitMutex = PTHREAD_MUTEX_INITIALIZER;
Mutex for accessing ExternalProcess::GracefulExit() from multiple threads.
3.3.4 Struct: WatchdogSupport
struct WatchdogSupport {
bool m_initialized = false; //!< Flag indicating whether the support variabled is initialized.
::WatchdogData* m_args = nullptr; //!< Pointer to the watchdog arguments to use.
ExternalProcess};
3.3.5 Class: ExternalProcess
class ExternalProcess: public Object {};
Class for handling an external side process interacting with ns-3.
This class creates a new process upon initialization and sets up communication channels via a TCP socket. Employing a leader/follower classification of roles, ns-3 acts as a leader, with the external process taking the role of the follower. As such, objects of this class shall set up the TCP server, with the client necessarily implemented by the external process.
A watchdog thread is spawned upon the first instance of this class
being created, running until the last ExternalProcess object goes out of
scope. This thread periodically checks whether external processes are
still alive by means of their PID. Watchdog settings may be customized
via attributes ‘CrashOnFailure’ and ‘WatchdogPeriod’.
By default, socket operations are blocking, possibly for an indefinite
time. Attributes ‘TimedAccept’, ‘TimedWrite’, and ‘TimedRead’ change the
behavior of respective socket operations – i.e. accept(), write(), and
read() – into using a timeout and repeated attempts. Attributes
‘Timeout’ and ‘Attempts’ allow customization of such behavior, but are
applied to any enabled timed operation equally.
All socket operations are blocking, even in their timed versions. Asynchronous mode using callbacks is out of the scope of the current implementation.
3.3.5.1 Attributes
ExternalProcess
supports the following attributes within
ns-3 Object
’s attribute system.
"Launcher"
Absolute path to the process launcher script.
Default value:
StringValue("")
Mandatory
Must be a non-empty string representing a valid path within the filesystem
"CliArgs"
String containing command-line arguments for the launcher script; tokens will be split by whitespace first.
Default value:
StringValue("")
Optional
Command-line arguments specified here are passed after the TCP port number the external process has to use for communicating with ns-3
"CrashOnFailure"
Flag indicating whether to raise a fatal exeception if the external process fails.
Default value:
BooleanValue(true)
Optional
"WatchdogPeriod"
Time period spent sleeping by the watchdog thread at the beginning of the PID checking loop; lower values will allow detection of process errors quicker, longer values greatly reduce busy waits.
Default value:
TimeValue(MilliSeconds(100))
Optional
Range: min =
MilliSeconds(1)
, max =Minutes(60)
The first instance of
ExternalProcess
invokingCreate()
will set the watchdog thread with this value; any subsequentExternalProcess
instance will not affect the watchdog polling period while there is a live watchdog thread
"GracePeriod"
Time period spent sleeping after killing a process, potentially allowing any temporary data on the process to be stored.
Default value:
TimeValue(MilliSeconds(100))
Optional
Range: min =
MilliSeconds(0)
"Port"
Port number for communicating with external process; if 0, a free port will be automatically selected by the OS.
Default value:
UintegerValue(0)
Optional
"Timeout"
Maximum waiting time for socket operations (e.g. accept); if 0, no timeout is implemented.
Default value:
TimeValue(MilliSeconds(0))
Optional
Range: min =
MilliSeconds(0)
"Attempts"
Maximum attempts for socket operations (e.g. accept); only if a non-zero timeout is specified.
Default value:
UintegerValue(1)
Optional
Range: min = 1
"TimedAccept"
Flag indicating whether to apply a timeout on socket accept(), implementing ‘Timeout’ and ‘Attempts’ settings; only if a non-zero timeout is specified.
Default value:
BooleanValue(false)
Optional
"TimedWrite"
Flag indicating whether to apply a timeout on socket write(), implementing ‘Timeout’ and ‘Attempts’ settings; only if a non-zero timeout is specified.
Default value:
BooleanValue(false)
Optional
"TimedRead"
Flag indicating whether to apply a timeout on socket read_until(), implementing ‘Timeout’ and ‘Attempts’ settings; only if a non-zero timeout is specified.
Default value:
BooleanValue(false)
Optional
"ThrottleWrites"
Minimum time between a read and a subsequent write; this delay is applied before writing.
Default value:
TimeValue(MilliSeconds(0))
Optional
Range: min =
MilliSeconds(0)
"ThrottleReads"
Minimum time between a write and a subsequent read; this delay is applied before reading.
Default value:
TimeValue(MilliSeconds(0))
Optional
Range: min =
MilliSeconds(0)
3.3.5.2 Public API
struct WatchdogData {
bool m_crashOnFailure; //!< [in] Flag indicating whether to raise a fatal exeception if the external process fails.
m_period; //!< [in] Time period spent sleeping by the watchdog thread at the beginning of the PID checking loop.
Time };
Represents the argument for a watchdog thread. Its usage is typically transparent to the user.
struct BlockingArgs {
pthread_t* m_threadId = nullptr; //!< [inout] Pointer to the blocking thread ID to set to -1.
bool* m_exitNormal = nullptr; //!< [inout] Pointer to flag indicating normal exit from thread.
pthread_mutex_t* m_mutex = nullptr; //!< [in] Pointer to the mutex to use.
pthread_cond_t* m_cond = nullptr;
/** [Constructor] */
};
Represents additional arguments for implementing BlockingSocketOperation() in thread functions. Its usage is typically transparent to the user.
struct AcceptorData {
boost::asio::ip::tcp::acceptor* m_acceptor = nullptr; //!< [in] Pointer to boost::asio acceptor.
boost::asio::ip::tcp::socket* m_sock = nullptr; //!< [in] Pointer to boost::asio socket.
boost::system::error_code* m_errc = nullptr; //!< [out] Pointer to boost::system error code.
* m_blockingArgs = nullptr; //!< [in] Pointer to additional args for blocking operations, if provided; if nullptr, timed operation is assumed.
BlockingArgs
/** [Constructor] */
};
Represents the argument for an acceptor thread, if ‘TimedAccept’ is used. Its usage is typically transparent to the user.
struct WriterData {
boost::asio::ip::tcp::socket* m_sock = nullptr; //!< [in] Pointer to boost::asio socket.
boost::asio::mutable_buffer* m_buf = nullptr; //!< [in] Pointer to boost::asio::streambuf to write data from.
boost::system::error_code* m_errc = nullptr; //!< [out] Pointer to boost::system error code.
* m_blockingArgs = nullptr; //!< [in] Pointer to additional args for blocking operations, if provided; if nullptr, timed operation is assumed.
BlockingArgs
/** [Constructor] */
};
Represents the argument for a writer thread, if ‘TimedWrite’ is used. Its usage is typically transparent to the user.
struct ReaderData {
boost::asio::ip::tcp::socket* m_sock = nullptr; //!< [in] Pointer to boost::asio socket.
boost::asio::streambuf* m_buf = nullptr; //!< [out] Pointer to boost::asio::streambuf to read data to.
boost::system::error_code* m_errc = nullptr; //!< [out] Pointer to boost::system error code.
* m_blockingArgs = nullptr; //!< [in] Pointer to additional args for blocking operations, if provided; if nullptr, timed operation is assumed.
BlockingArgs
/** [Constructor] */
};
Represents the argument for a reader thread, if ‘TimedRead’ is used. Its usage is typically transparent to the user.
static void ExternalProcess::GracefulExit(void);
Terminates all external processes spawned during this simulation.
This function should be invoked whenever
NS_FATAL_ERROR
is used, preventing external processes to remain alive despite no chance of further communication.
This function is thread-safe.
::ExternalProcess(); ExternalProcess
Default constructor.
virtual ExternalProcess::~ExternalProcess();
Default destructor.
static TypeId ExternalProcess::GetTypeId(void);
Registers this type.
Returns:
- The TypeId.
bool ExternalProcess::Create(void);
Creates a side process given a launcher script, accepting connections from it.
Returns:
- True if the creation has been successful, False otherwise.
This operation may be blocking.
bool ExternalProcess::IsRunning(void) const;
Retrieves whether the side process is running or not.
Returns:
- True if the side process is running, False otherwise.
pid_t ExternalProcess::GetPid(void) const;
Retrieves the PID of the side process.
Returns:
- The PID of the side process previously set up via
ExternalProcess::Create()
.
bool ExternalProcess::Teardown(void);
Performs process teardown operations using the result of
ExternalProcess::GetPid()
.
Returns:
- True if this external process is no longer tracked by the watchdog, False otherwise.
This function is thread-safe.
bool ExternalProcess::Teardown(pid_t childPid);
Performs process teardown operations.
Parameters:
[in] childPid
PID of the child process associated with this teardown procedure. If different than-1
, it will send aSIGKILL
signal to the provided PID.
Returns:
- True if this external process is no longer tracked by the watchdog, False otherwise.
This function is NOT thread-safe: a lock shall be acquired on
g_watchdogTeardownMutex
previously to the invocation of this function.
Its usage is discouraged; users should invoke
ExternalProcess::Teardown(void)
instead, on the instance associated to the process intended to be shutdown.
bool ExternalProcess::Write(const std::string &str, bool first = true, bool flush = true, bool last = true);
Writes a string to the external process through the socket.
Parameters:
[in] str
String to write.[in] first
Whether the string is the first of a series of writes (Default: true).[in] flush
Whether to flush after writing this string or not (Default: true).[in] last
Whether the string is the last of a series of writes (Default: true).
Returns:
- True if the operation is successful, False otherwise.
This operation may be blocking.
bool ExternalProcess::Read(std::string &str, bool &hasNext);
Reads a string from the external process through the socket.
Parameters:
[out] str
String read (if return is True; discard otherwise).[out] hasNext
Whether there is going to be a next line or not.
Returns:
- True if the operation is successful, False otherwise.
This operation may be blocking.
4 Citing this work
If you use the module in this repository, please cite this work using any of the following methods:
APA
Giona, E. ns3-ext-process [Computer software]. https://doi.org/10.5281/zenodo.8172121
BibTeX
@software{Giona_ns3-ext-process,
author = {Giona, Emanuele},
doi = {10.5281/zenodo.8172121},
license = {GPL-2.0},
title = {{ns3-ext-process}},
url = {https://github.com/emanuelegiona/ns3-ext-process}
}
Bibliography entries generated using Citation File Format described in the CITATION.cff file.
5 License
Copyright (c) 2023 Emanuele Giona (SENSES Lab, Sapienza University of Rome)
This repository is distributed under GPLv2 license.
ns-3 is distributed via its own license and shall not be considered part of this work.