BDE 4.39.x Production Release
Loading...
Searching...
No Matches
balb_pipetaskmanager

Detailed Description

Provide a pipe-based mechanism to process task control messages.

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:

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:

  1. the prefix value, and
  2. 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 balb::PipeTaskManager

A default constructed balb::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::PipeTaskManager 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.

See the makeCanonicalName overloads in bdls_pipeutil for details.

Moreover, the start method must be able to freshly create a named pipe. In general, start 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 start attempts to remove and re-create that named pipe.

On Unix systems, named pipes are created having the permission 0666 (read and write for user, group, and other) limited by the current umask value of the process.

On successful completion of start, the (full) pathname of the created named pipe is provided by the pipeName accessor. The full path name must be passed to sending processes so they can open that named pipe and write control messages.

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.

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 write system call.
  2. The length of each message is less than PIPE_BUF (the limit for guaranteed atomicity).

The value PIPE_BUF depends on the platform:

+------------------------------+------------------+
| Platform | PIPE_BUF (bytes) |
+------------------------------+------------------+
| POSIX (minimum requirement)) | 512 |
| IBM | 32,768 |
| SUN | 32,768 |
| Linux | 65,536 |
| Windows | 65,536 |
+------------------------------+------------------+

Also note that Linux allows the PIPE_BUF size to be changed via the fcntl system call.

Usage

This section illustrates intended use of this component.

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 balb::PipeTaskManager class can be used to provide support for messages that are sent via a named pipe and have the syntax shown below:

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.

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.

static bsls::AtomicBool done(false);
static bsls::AtomicInt progress(0);
static bsls::AtomicInt myLoggingManagerLevel(0);
Definition bsls_atomic.h:1490
Definition bsls_atomic.h:744

Then, we define helper functions myLoggingManagerGet and myLoggingManagerSet 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.

/// Print the current log level to the console.
void myLoggingManagerGet()
{
bsl::cout << "LOG LEVEL IS NOW" << ": "
<< myLoggingManagerLevel << bsl::endl;
}
/// Set the log level to the value obtained from the specified
/// `message` and print that value to the console.
void myLoggingManagerSet(bsl::istream& message)
{
int newLogLevel;
message >> newLogLevel; // Cannot stream to an `bsls::AtomicInt`.
myLoggingManagerLevel = newLogLevel;
bsl::cout << "LOG LEVEL SET TO" << ": "
<< myLoggingManagerLevel << bsl::endl;
}

Next, define handler functions for the "EXIT", "RESTART", and "LOG" messages.

/// Handle a "EXIT" message.
void onExit(const bsl::string_view& , bsl::istream& )
{
bsl::cout << "onExit" << bsl::endl;
done = true;
}
/// Handle a "RESTART" message.
void onRestart(const bsl::string_view& , bsl::istream& )
{
bsl::cout << "onRestart" << bsl::endl;
progress = 0;
}
/// 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`.
void onLog(const bsl::string_view& , bsl::istream& 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;
}
}
Definition bslstl_stringview.h:471
Definition bslstl_string.h:1252

Notice that no handler is yet defined for the "HELP" message. That functionally can be provided using methods of the contained balb::ControlManager object. See below.

Then, create a balb::PipeTaskManger object and register the above functions as handlers:

/// Run Application1 and return status.
int myApplication1()
{
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);
int registerHandler(const bsl::string_view &prefix, const bsl::string_view &arguments, const bsl::string_view &description, const ControlHandler &handler)
Definition balb_pipetaskmanager.h:472
balb::ControlManager & controlManager()
Definition balb_pipetaskmanager.h:597

and add an additional handler that provides a list of the registered messages and the syntax for using them:

rc = taskManager.controlManager().registerUsageHandler(bsl::cout);
assert(0 == rc);
int registerUsageHandler(bsl::ostream &stream)

Next, if we are on a Unix system, we confirm that our named pipes will be created in the directory named by the TMPDIR environment variable:

#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

Then, start listening for incoming messages at pipe having the name based on on the name "MyApplication.CTRL". To avoid collisions when this test case is run simultaneously on a single machine (or on different machines sharing an NFS mount), we will append the hostname and process ID to the base name.

const char *hostname = bsl::getenv("HOSTNAME");
if (!hostname) hostname = "Windows";
ss << "MyApplication.CTRL."
<< hostname << '.'
bsl::string pipeBaseName = ss.str();
rc = taskManager.start(pipeBaseName);
assert(0 == rc);
int start(const bsl::string_view &pipeBasename)
basic_ostringstream< char, char_traits< char >, allocator< char > > ostringstream
Definition bslstl_iosfwd.h:97
static int getProcessId()

Next, for expository purposes, confirm that a pipe of that name exists in the expected directory and is open for reading.

const bsl::string_view pipeName = taskManager.pipeName();
assert(bdls::PathUtil::isAbsolute (pipeName));
#ifdef BSLS_PLATFORM_OS_UNIX
#endif
bsl::string canonicalDirname, canonicalLeafName;
bdls::PathUtil::getDirname(&canonicalDirname, pipeName);
bdls::PathUtil::getLeaf (&canonicalLeafName, pipeName);
assert(0 == bsl::strcmp(expectedDirectory, canonicalDirname.c_str()));
bdlb::String::toLower(&pipeBaseName);
assert(pipeBaseName == canonicalLeafName);
const bsl::string & pipeName() const
Definition balb_pipetaskmanager.h:623
const CHAR_TYPE * c_str() const BSLS_KEYWORD_NOEXCEPT
Definition bslstl_string.h:7405
static void toLower(char *string)
static bool isAbsolute(const bsl::string_view &path, int rootEnd=-1)
static int getDirname(bsl::string *dirname, const bsl::string_view &path, int rootEnd=-1)
static int getLeaf(bsl::string *leaf, const bsl::string_view &path, int rootEnd=-1)
static bool isOpenForReading(const bsl::string_view &pipeName)

Notice that given baseName 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:

while (!done) {
// Do useful work while background thread responds to incoming
// commands from the named pipe.
}
return 0;
}

Finally, in some other programming context, say mySender, a context in another process that has been passed the value of pipeName, control messages can be sent to myApplication1 above.

/// Write control messages into the pipe named by the specified `pipeName`.
void mySender(const bsl::string& pipeName)
{
int rc;
rc = bdls::PipeUtil::send(pipeName, "LoG GET\n");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "Log SET 4\n");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "log GET\n");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "\n"); // empty
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "RESET\n"); // invalid
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "RESTART\n");
assert(0 == rc);
rc = bdls::PipeUtil::send(pipeName, "EXIT\n");
assert(0 == rc);
}
static int send(const bsl::string_view &pipeName, const bsl::string_view &message)

Notice that:

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