Novell Home

AppNote: Writing Command Parsers for NetWare

Novell Cool Solutions: AppNote
By Russell Bateman

Digg This - Slashdot This

Posted: 30 Jun 2004
 

Russell Bateman
Senior Software Engineer
Server-library Development
rbateman@novell.com

This How-to AppNote discusses how to register a command parser from a NetWare-loadable Module (NLM).

Contents

Introduction
Command-line Handler and Parser
Conclusion
Topics NLM development, kernel and low-level code
Products NetWare 5, NetWare 6
Audience Developers
Level Intermediate

Introduction

Adding commands via NetWare's system console command parser is a useful, but little understood process. This article will present the details of doing this.

Underlying this capability are hooks in the OS that allow NLMs to register a parsing function to be called whenever a command is entered at the console.

Most of the interfaces we will discuss in this article come from the LibC header, netware.h.

This sort of programming on NetWare is purely kernel-bound. You cannot register a parsing function from an NLM running in a protected-address space because the functions to register it are not callable there and, even if they were, the OS would abend if it tried to pass execution from the kernel along to the registered function. (The problem of writing NLMs that cooperate between rings is a large one and the subject of another article.)

In this article, we're going to implement, just as LibC does, the setenv command for NetWare. This code mostly comes from LibC.

Command-line Handler and Parser

Many NLMs add commands of their own to use from toolbox.nlm which is a sort of primitive shell to LibC which supports the setenv command.

Registration
Registration is achieved by calling RegisterCommand starting in NetWare 5. Prior to that, RegisterConsoleCommand was used, but this legacy call will not be discussed for lack of space and relevance. First, a special resource tag to mark the structure containing the registered command parser must be allocated:

void	*NLMHandle;
rtag_t	cmdRTag;

cmdRTag = AllocateResourceTag(NLMHandle, "Environment Variables", 
                          CommandLineServicesSignature);

The old call required the use of a resource tag allocated using ConsoleCommandSignature.

NLMHandle comes from the first argument of your start-up code. If you link LibC's prelude code, this would be the first argument to _NonAppStart, whose employ is discussed in the articles on How to Write Start-up Code for NLMs and How to Write NetWare Loadable Modules as Dynamic Libraries , if you link one or the third argument to DllMain if you link that function instead. For those NLMs more properly termed applications because they provide a main, , the easiest way to get this is to call getnlmhandle. In fact, this latter can usually be called in any situation including the other two mentioned.

The call to register the command parser specifies a number of pieces of useful information important in the behavior of that parser. Note, in particular, that the command keyword is a length-preceded string. (This is a fragment to be placed somewhere in your start-up code.)

#define SETENV_STRING	"\x6setenv"
#define SETENV_SIG		(void *) ?Env '

{
  uint32_t	keywordFlags, handlerFlags, insertFlags;

  keywordFlags = CMD_LENGTH_PREC;
  handlerFlags = CCMD_SERVER_RUNNING
               | CMD_LEGAL_SERVER
               | CMD_NO_CMD_CHAIN
               | CMD_PASS_ON_ERROR;
  insertFlags =  CMD_INSERT_AT_HEAD
               | CMD_PERM_POSITION;
  err = RegisterCommand(NLMHandle, cmdRTag, keywordFlags,
           SETENV_STRING, handlerFlags, insertFlags,
           SetEnvHandler, SETENV_SIG);
}
	

Break when our command is parsed at the command line, in a moment. Let's examine the details of various arguments. Following are tables describing all the possible options.

First, the keyword or command has to be specified by means of flags. We could keep it among the messages for easy localization. However, in practice one would not do this because it would in most instances become very annoying for a command to change every time the NetWare server language does.

keywordFlags
CMD_MSG_NUMBER keyword isn't a string, but a message number referring to a particular message linked into the NLM.
CMD_LENGTH_PREC keyword is a length-preceded (Pascal) string.

After the keyword flags, we specify the actual command string–because we're not going to get our command out of our messages. This is done length-preceded meaning that the first byte must carry the value that corresponds to the number of characters in the command not including the length byte itself nor any NULL that the C compiler might add on (which will only be the case where a double-quoted literal is used).

Next, we need to tailor the environment and semantics in which our handler is going to get called. Do we want the OS to convert the command to upper case for us? Do we not want it displayed as a possible command when all the commands are listed out? Do we want others to be able to register the same command so they can cover it if in our parsing we decide it's not really ours to handle? Etc.

handlerFlags
CMD_CONFIG_INFO Requires configuration information.
CMD_CONVERT_UPPER Convert command line to upper case (the whole command, I think).
CMD_SERVER_RUNNING Requires the server to be running.
CMD_LEGAL_SERVER Allows the command on a normal server.
CMD_HIDDEN_CMD Don't display this in a list of commands.
CMD_SUB_CMDS_AVAIL Allows sub-commands of this one.
CMD_NO_CMD_CHAIN Disallow chaining of command keyword
CMD_PASS_ON_ERROR Ignore any errors occurring.
CMD_ANY_PROCESS Can only run on a process with a keyboard.

Insertion flags tell just how the new command-line parser is to relate to the existing list of command-line parsers. Will it be tail-patched, placed in front of all others, etc.?

insertFlags
CMD_INSERT_AT_HEAD Insert in front of all others (so far registered).
CMD_INSERT_AT_TAIL Append to end of all others (so far registered).
CMD_PERM_POSITION Enforce current position (head or tail) with respect to all others.

I admit that I don't know what happens when two command-line parsers are registered with identical insertion flags plus CMD_PERM_POSITION. The use of this last flag really says, "I don't trust anyone else to do a good job of parsing this command" if CMD_INSERT_AT_HEAD is set and, "I sure hope someone parses this command instead of me" in the other case.

After all this, the address of the function handler is passed. In C, a function's address is represented merely by its name. The prototype of this function must correspond to CommandHandler_t as defined in netware.h. This is the important part of this article and we'll discuss this after talking about registration and deregistration.

Finally, a magic reference value can be passed (including 0) that is not consumed by the OS and preserved intact into the parsing function. This is a handy trick for recognizing whatever instance, say whatever command, a registering application might want. The same parsing function could be used for different registered commands with only this value to differentiate between them. Inside the registered function, this value would be used in an if or a switch statement to gate execution appropriately. Presumably this would be done to fold common code to reduce maintenance.

Deregistration
Deregistration is simple, just call DeregisterCommand with the same NLM handle, resource tag and keyword flags as were used in the registration process. This suggests that the same keyword can be registered multiple times by the NLM as long as it allocates a different resource tag for each instance. This is nothing more than the local case of what happens if more than one NLM register a handler for the same command.

err = DeRegisterCommand(NLMHandle, cmdRTag, keywordFlags,
		SETENV_STRING);

NetWare doesn't require us to deallocate resource tags.

The Command Parser
Now that we have taken care of all the bookkeeping that goes into setting up a new command parser and cleaning up after ourselves, let's examine how parsing is done. Here's the function.

Note that we are told when to display help for our command, which we do by imitating help for other commands that we can explore on a working NetWare server. And, we reject handling of a sub-command, a code path that probably won't ever occur since we said at registration that we didn't support sub-commands.

#define is_tab(c)     ((c) == '\t' || (c) == '\v')
#define is_delim(c)   ((c) == '\n' || (c) == '\r' || (c) == '\f')
#define is_white(c)   (isspace(c) || is_tab(c))
#define cmdSETENV     1
#define cmdSHOWVARS   2
#define cmdSHOWVAR    3

int SetEnvHandler
(
   int		funcCode,
   void		*scrID,
   const char	*command,
   const char	*upperCaseCommand,
   void		*callerReference
)
{
#pragma unused(upperCaseCommand)
   size_t	length;

   if (callerReference != SETENV_SIG)
      return CMD_NOT_MY_COMMAND;

   switch (funcCode)
   {
      case CMD_HELP_ON_CMD :      // display help for our command
         if (scrID)
            OutputToScreen(scrID, SETENV_USAGE);
         break;

      case CMD_GET_SUB_CMDS :     // said we had none of these...
         return CMD_NOT_MY_COMMAND;// ...when we registered

      case CMD_PROCESS_CMD :      // what we really want to do...
         rc = ParseSetEnvCommand(cmdLine, variable, value,
						&length);
         switch (rc)
         {
            char	*v, **env;

            case cmdSETENV :     // setenv <var-name> = <value>
               setenv(variable, value, TRUE);

               if (length > 0)   // this wasn't an "unset"...
                  OutputToScreen(scrID, "\t%s = %s\n", variable,
						value);
               return CMD_PROCESSED_OK;

            case cmdSHOWVARS :   // setenv
               OutputToScreen(scrID, "C Library Environment:\n");

               env = environ;

               while (*env)
                  OutputToScreen(scrID, "\t%s\n", *env++);

               return CMD_PROCESSED_OK;

            case cmdSHOWVAR :  // setenv <var-name>
               if (v = getenv(variable))
                  OutputToScreen(scrID, "\t%s\n", v);
               else
                  OutputToScreen(scrID, "\t%s not found.\n",
						variable);
               return CMD_PROCESSED_OK;
         }
   }


   return CMD_CMD_EXECUTED;
}

Processing the Command
For a function code of CMD_PROCESS_CMD, there is nothing to do except examine the rest of the command line (since to get in here in the first place, it's been determined that the command string matches the one we registered).

This function, ParseSetEnvCommand, actually parses the command line in response to being called by SetEnvHandler; the one we've already shown only reacts to what has been parsed and sets (or unsets) an environment variable, displays a variable or displays the list of all variables.

int ParseSetEnvCommand	// for use in here and outside
(			// e.g.: "TZ=MST7MDT"
   const char  *line,	// input (no command) line
   char        *variable,	// return the variable and...
   char        *value,	// ...the value and...
   int         *length	// ...the value's length
)
{
   int		rc = 0;
   register char	*p = line, *q;

   *variable =
   *value    = '\0';
   *length   = 0;

   while (*p && is_white(*p))
      p++;

   // quick, initial check to see if worth pursuing...
   if (!*p)
   {
      rc = cmdSHOWVARS;
      goto Exit;
   }

/*
** 'p' currently points at the potential variable. Skip any white
** space and then parse the variable name and value...
*/
   while (*p && is_white(*p))
      p++;

   q = variable;

   while (!is_white(*p) && *p != '=' && (*q++ = toupper(*p)))
      p++;

   *q = '\0';

   if (!variable[0])
   {
      rc = cmdSHOWVARS;
      goto Exit;
   }

   while (*p && *p != '=')
      p++;

   if (!*p++)		  // no equal sign, just show it
   {
      rc = cmdSHOWVAR;
      goto Exit;
   }

   while (*p && is_white(*p)) // skip initial white space...
      p++;			  // ...on way to value

   q = value;

   while (*q++ = *p++)	 // copy to end of line...
      ;			 // ...including white space

   *length = q - value;

Exit :
   return rc;
}

Conclusion

This is how, then, you can add commands to the NetWare console command line. If you make calls from this sort of code to functions in CLib or LibC that require context, you will need to remember that your call-back (the thread executing your command handler) will be a system thread, usually the console command process, and that you need to handle context issues accordingly. For CLib this means bracketing the handler code with calls to SetThreadGroupID whereas for LibC, this means wrappering the call-back address itself before calling RegisterCommand by using NX_WRAP_INTERFACE (see on-line documentation for this interface).

The rest of the structure fields, values and definitions in the console command interfaces can be found in the online documentation.


Novell Cool Solutions (corporate web communities) are produced by WebWise Solutions. www.webwiseone.com

© 2014 Novell