Outline
Purpose
Provide a pipe-based mechanism to process task control messages.
Classes
- See also
- balb_controlmanager, balb_pipecontrolchannel
Description
This component provides a mechanism, balb::PipeTaskManager
, that listens on a named pipe for messages that are typically used to influence the behavior of a (running) task.
For example, a balb::PipeTaskManager
might be configured to listen on a well known named pipe (e.g., myapplication.ctrl
), for the control messages starting with:
- "EXIT",
- "RESTART",
- "LOG", and
- "HELP".
The use of imperative verbs for the first field of a message is a common practice. The first field is called the message "prefix". On receipt of a message with a known prefix, a previously registered handler functor is invoked with two arguments:
- the prefix value, and
- an
bsl::istream
from which the rest of the message (if any) can be read. Thus, we have a mechanism by which a running task can be sent commands and, optionally, arguments for those commands.
Once the relationship between prefix
and handler has been specified, the start
method is used to create the named pipe (or re-open an existing named pipe) and a thread created to listen for messages.
A human user on a console might then use a command line application to send control messages to the myapplication.ctrl
pipe to configure the behavior of the running task. In the example above, the handler for the LOG
prefix expects additional parameters. Thus:
echo "LOG VERBOSITY 4" > $SOCKDIR/myapplication.ctrl
changes the logging verbosity of the task to level "4". See bdls_pipeutil for functions that can be invoked from C++ code to send messages to a named pipe.
Configuring the balbl::PipeTaskManager
A default constructed balbl::PipeTaskManager
has no registered handlers. Users can use the exposed balb::ControlManager
, to register different control message prefixes (typically "verbs") to dispatch received messages to an appropriate functor.
Alternatively, one can construct a balb::PipeTaskManager
using a separately created and configured a balb::ControlManager
object. Doing so allows that single balb::ControlManager
to be shared among several balb::PipeTaskManagrer
objects, each listening on a different named pipe.
Thread Safety
This component is thread-safe but not thread-enabled, meaning that multiple threads may safely use their own instances of balb::PipeTaskManager
, but may not manipulate the same instance of balb::PipeTaskManager
simultaneously. Note that the contained balb::ControlManager
object is available via both const
and non=const
references and that object is safe for multiple threads.
Requirements for the Named Pipe
The balb::PipeTaskManger
objects waits for messages from a named pipe provided to the start
method. The argument to start
– mapped to all lower case, if needed – determines the basename of the named pipe. The directory of that named pipe depends on the platform and environment variables.
- On Windows, that directory is: "\\.\pipe\".
* On Unix systems, that directory is determined by
- the <tt>SOCKDIR</tt> environment variable, if set; otherwise,
- the <tt>TMPDIR</tt> environment variable, if set; otherwise,
- the current directory.
See the <tt>makeCanonicalName</tt> overloads in @ref bdls_pipeutil for details.
Moreover, the <tt>start</tt> method must be able to <em>freshly</em> create a named pipe.
In general, <tt>start</tt> will fail if a named pipe of the calculated canonical
name already exists. On Unix, if that named pipe is not in use (not open
for reading), the <tt>start</tt> attempts to remove and re-create that named pipe.
On Unix systems, named pipes are created having the permission <tt>0666</tt> (read
and write for user, group, and other) limited by the current <tt>umask</tt> value
of the process.
On successful completion of <tt>start</tt>, the (full) pathname of the created
named pipe is provided by the <tt>pipeName</tt> accessor. The full path name must
be passed to sending processes so they can open that named pipe and write
control messages.
@subsection balb_pipetaskmanager-message-requirements Message Requirements
Each message consists of a sequence of fields separated by blanks and/or
tabs and terminated by a newline ('\n') character. The terminating newline
is not passed to the message handler.
The first field is called the message "prefix" and is used to find a
previously registered handler for the message. The handler lookup is case
insensitive. Empty messages (newline only) and messages for which no
handler can be found are silently ignored.
Note that this facility provides a one-way flow of information from the
writer to a named pipe to the registered message handler. There is no
mechanism here for validating message (e.g., a given prefix has required
additional fields) or returning status. Many applications provide output by
writing to the console or to a log.
@subsubsection balb_pipetaskmanager-pipe-atomicity Pipe Atomicity
Users that expect multiple concurrent writers to a single pipe must be aware
that the message content might be corrupted (interleaved) unless:
1. Each message is written to the pipe in a single <tt>write</tt> system call.
2. The length of each message is less than <tt>PIPE_BUF</tt> (the limit for
guaranteed atomicity).
The value <tt>PIPE_BUF</tt> depends on the platform:
@code
+------------------------------+------------------+
| Platform | PIPE_BUF (bytes) |
+------------------------------+------------------+
| POSIX (minimum requirement)) | 512 |
| IBM | 32,768 |
| SUN | 32,768 |
| Linux | 65,536 |
| Windows | 65,536 |
+------------------------------+------------------+
@endcode
Also note that Linux allows the <tt>PIPE_BUF</tt> size to be changed via the
<tt>fcntl</tt> system call.
@subsection balb_pipetaskmanager-usage Usage
This section illustrates intended use of this component.
@subsubsection balb_pipetaskmanager-example-1-basic-usage Example 1: Basic Usage
Suppose one is creating an application that allows for dynamically changing
its logging verbosity level, resetting to its initial state, to shutdown
cleanly, and the listing a description of supported messages.
The <tt>balb::PipeTaskManager</tt> class can be used to provide support for
messages that are sent via a named pipe and have the syntax shown below:
@code
This process responds to the following messages:
EXIT no arguments
Terminate the application.
HELP
Display this message
LOG <GET|SET <level> >
Get/set verbosity level.
RESTART no arguments
Restart the application.
@endcode
Note that the above description corresponds to the output produced by our
application in response to a "HELP" message.
First, define several global, atomic variables that will be used to exchange
information between the thread that monitors the named pipe and the other
threads of the application.
@code
static bsls::AtomicBool done(false);
static bsls::AtomicInt progress(0);
static bsls::AtomicInt myLoggingManagerLevel(0);
@endcode
Then, we define helper functions <tt>myLoggingManagerGet</tt> and
<tt>myLoggingManagerSet</tt> so that the handler for "LOG" messages can delegate
processing the "GET" and "SET" subcommands. The other defined messages have
minimal syntax so use of a delegation pattern is overkill in those cases.
@code
void myLoggingManagerGet()
// Print the current log level to the console.
{
bsl::cout << "LOG LEVEL IS NOW" << ": "
<< myLoggingManagerLevel << bsl::endl;
}
void myLoggingManagerSet(bsl::istream& message)
// Set the log level to the value obtained from the specified
// 'message' and print that value to the console.
{
int newLogLevel;
message >> newLogLevel; // Cannot stream to an 'bsls::AtomicInt'.
myLoggingManagerLevel = newLogLevel;
bsl::cout << "LOG LEVEL SET TO" << ": "
<< myLoggingManagerLevel << bsl::endl;
}
@endcode
Next, define handler functions for the "EXIT", "RESTART", and "LOG"
messages.
@code
void onExit(const bsl::string_view& , bsl::istream& )
// Handle a "EXIT" message.
{
bsl::cout << "onExit" << bsl::endl;
done = true;
}
void onRestart(const bsl::string_view& , bsl::istream& )
// Handle a "RESTART" message.
{
bsl::cout << "onRestart" << bsl::endl;
progress = 0;
}
void onLog(const bsl::string_view& , bsl::istream& message)
// Handle a "LOG" message supporting sub command "GET" and "SET". If
// the subcommand is "SET" the new log level is obtained from the
// specified 'message'.
{
bsl::cout << "onLog" << bsl::endl;
bsl::string subCommand;
message >> subCommand;
// See the registration of the 'onLog' handler below for the
// details of the supported sub commands and their arguments.
if ("GET" == subCommand) {
myLoggingManagerGet();
}
else if ("SET" == subCommand) {
myLoggingManagerSet(message);
}
else {
bsl::cout << "onLog" << ": "
<< "unknown subcommand" << ": "
<< subCommand << bsl::endl;
}
}
@endcode
Notice that no handler is yet defined for the "HELP" message. That
functionally can be provided using methods of the contained
<tt>balb::ControlManager</tt> object. See below.
Then, create a <tt>balb::PipeTaskManger</tt> object and register the above
functions as handlers:
@code
int myApplication1()
// Run Application1 and return status;
{
balb::PipeTaskManager taskManager;
int rc;
rc = taskManager.controlManager().registerHandler(
"EXIT",
"no arguments",
"Terminate the application.",
onExit);
assert(0 == rc);
rc = taskManager.controlManager().registerHandler(
"RESTART",
"no arguments",
"Restart the application.",
onRestart);
assert(0 == rc);
rc = taskManager.controlManager().registerHandler(
"LOG",
"<GET|SET <level> >",
"Get/set verbosity level.",
onLog);
assert(0 == rc);
@endcode
and add an additional handler that provides a list of the registered
messages and the syntax for using them:
@code
rc = taskManager.controlManager().registerUsageHandler(bsl::cout);
assert(0 == rc);
@endcode
Next, if we are on a Unix system, we confirm that our named pipes will be
created in the directory named by the <tt>TMPDIR</tt> environment variable:
@code
#if defined(BSLS_PLATFORM_OS_UNIX)
rc = unsetenv("SOCKDIR"); // 'SOCKDIR' has precedence over 'TMPDIR'.
assert(0 == rc);
const char *expectedDirectory = getenv("TMPDIR");
#elif defined(BSLS_PLATFORM_OS_WINDOWS)
const char *expectedDirectory = "\\.\pipe\";
#else
#error "Unexpected platform."
#endif
@endcode
Then, start listening for incoming messages at pipe having the name based on
on the name "MyApplication.CTRL".
@code
rc = taskManager.start("MyApplication.CTRL");
assert(0 == rc);
@endcode
Next, for expository purposes, confirm that a pipe of that name exists in
the expected directory and is open for reading.
@code
const bsl::string_view pipeName = taskManager.pipeName();
assert(bdls::PathUtil::isAbsolute (pipeName));
#ifdef BSLS_PLATFORM_OS_UNIX
assert(bdls::PipeUtil::isOpenForReading(pipeName));
#endif
bsl::string canonicalDirname, canonicalLeafName;
bdls::PathUtil::getDirname(&canonicalDirname, pipeName);
bdls::PathUtil::getLeaf (&canonicalLeafName, pipeName);
assert(0 == bsl::strcmp(expectedDirectory, canonicalDirname.c_str()));
assert("myapplication.ctrl" == canonicalLeafName);
@endcode
Notice that given <tt>baseName</tt> has been canonically converted to lowercase.
Now, our application can continue doing useful work while the background
thread monitors the named pipe for incoming messages:
@code
while (!done) {
// Do useful work while background thread responds to incoming
// commands from the named pipe.
}
return 0;
}
@endcode
Finally, in some other programming context, say <tt>mySender</tt>, a context in
another process that has been passed the value of <tt>pipeName</tt>, control
messages can be sent to <tt>myApplication1</tt> above.
@code
void mySender(const bsl::string& pipeName)
// Write control messages into the pipe named by the specified
// 'pipeName'.
{
int rc;
rc = bdls::PipeUtil::send(pipeName, "LoG GET
");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "Log SET 4
");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "log GET
");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "
"); // empty
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "RESET
"); // invalid
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "RESTART
");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "EXIT
");
assert(0 == rc);
}
@endcode
Notice that:
* Each message must be terminated by a newline character.
* Although each registered message prefix was all capital letters, the
prefix field in the sent message is case insensitive – "LoG", "Log", and
"log" all invoke the intended handler. If we wanted case insensitivity
for the subcommands "GET" and "SET" we would change of implementation of
<tt>onLog</tt> accordingly.
* The empty message and the unregistered "RESET" message are silently
ignored. The console output (see below) shows no indication that these
were sent.
The console log of our application shows the response for each received
control message. In general, these messages will be interleaved with the
output of the "useful work" done in the for
loop of 'myApplicaton1. onLog
LOG LEVEL IS NOW: 0
onLog
LOG LEVEL SET TO: 4
onLog
LOG LEVEL IS NOW: 4
onRestart
onExit