XUL:Command Line Handling

From MozillaWiki
Jump to: navigation, search

Command-Line Handling in the xulrunner

XUL apps intended to be run by the xulrunner need to be able to flexibly handle command-line arguments. The current system does not allow arbitrary processing by command-line handlers, it is limited to opening XUL windows. Apps (especially utility apps) may wish to exectute arbitrary code and then exit, without ever opening a window.

Command-line handling is currently done through the nsICmdLineService and nsICommandLineHandler interfaces. As you can see, the nsICommandLineHandler interface is so weird that we use a C++ macro to implement it for most cases.

Instead, the command-line handling should be handled through a callback-like interface. Posit a set of interfaces:

[scriptable...]
interface nsICommandLine : nsISupports
{
  /**
   * Number of arguments in the command-line.
   */
  readonly attribute signed short length;

  /**
   * Get an argument from the array of command-line arguments.
   * 
   * @param aIndex The argument to retrieve. This index is 0-based, and does not include
   *               the application name.
   * @return       The indexth argument.
   */
  AUTF8String getArgument(in signed short aIndex);

  /**
   * Find a command-line flag. Flags can begin with a hyphen or double-hyphen,
   * and on Windows/OS2 with a forward-slash.
   * @param aFlag          The flag to locate.
   * @param aCaseSensitive If true, the case of the flag is significant.
   * @return               The position of that flag in the command-line, or -1 if not found.
   */
  signed short findFlag(in AUTF8String aFlag, in boolean aCaseSensitive);

  /**
   * Remove arguments from the command-line. This is normally done when a handler has
   * processed the arguments.
   * @param aStart Index to begin removing arguments.
   * @param aEnd   Index to stop removing arguments.
   */
  void removeArguments(in signed short aStart, in signed short aEnd);

  /**
   * The type of command-line being handled:
   * STATE_INITIAL_LAUNCH is the first launch of the app.
   * STATE_REMOTE_AUTO is a remote command-line automatically redirected to this instance.
   * STATE_REMOTE_EXPLICIT is a remote command-line explicitly requested using winDDE/xremote.
   */
  readonly attribute unsigned short state;

  const unsigned short STATE_INITIAL_LAUNCH  = 0;
  const unsigned short STATE_REMOTE_AUTO     = 1;
  const unsigned short STATE_REMOTE_EXPLICIT = 2; 

  /**
   * The "working directory" for the command-line. A remote command-line may have a different
   * working directory than the current process, so we make sure we remote that also.
   */
  readonly attribute nsIFile workingDirectory;

  /**
   * Resolve a file-path argument into an nsIFile.
   */
  nsIFile resolveFile(in AUTF8String aArgument);

  /**
   * Resolve a URI argument.
   */
  nsIURI resolveURI(in AUTF8String aArgument);
};
interface nsICommandLineHandler : nsISupports
{
  /**
   * Handle the command-line. If the handler finds arguments that it understands, it
   * should perform the appropriate actions (such as opening a window) and remove
   * those arguments from the command-line array.
   *
   * @throws NS_ERROR_ABORT to immediately cease command-line handling
   *         (if this is STATE_INITIAL_LAUNCH, quits the app);
   *         NS_SUCCESS_COMMANDLINE_RESTART_XPCOM to restart the app;
   *         All other exceptions are silently ignored.
   */
  void handle(in nsICommandLine aCommandLine);

  /**
   * When the app is launched with the -help argument, this attribute
   * is queried and displayed to the user (on stdout).
   */
  readonly attribute AUTF8String helpText;
};

Instead of treating the command-line handler as static information about which XUL window to open, it is a dynamic type that can respond to the "event" of starting the application. Arguments can start with -arg, --arg on *nix, /arg on windows; they will be normalized to -arg before handlers are called. If a unix arg is the form --arg=param, it will be split into two arguments -arg <param>. Handlers are registered with the category manager (currently, you have to register with a contractID prefix and a category, which is kinda silly) and would be run in alpha-order thusly:

category entry value (description)
command-line-handler b-jsdebug @mozilla.org/venkman/clh;1 Handles -venkman and -venkman-sync flags to debug JS, even during startup
c-extensions @mozilla.org/extension-manager/clh;1 Handles -install-toolkit-extension -install-app-extension -install-profile-extension flags
m-edit @mozilla.org/composer/clh;1 Handles -edit <URLOrPath>
m-irc @mozilla.org/chatzilla/clh;1 Handles -irc or -chat flags
y-final @mozilla.org/app-startup/clh;1 If there is a bare URL/file on the command-line, this opens a browser window with that URI/file. If there are *no* windows open at this point, it opens a default browser window.

The important thing is that the same command-line handlers would be used for a remote invocation (DDE or xremote) as an initial startup. This means that we can get rid of all the app-specific code in the xremote service. Firefox can keep a backwards-compatibility shim to handle the -remote flag. This would give apps equal-opportunity and silent access to argument handling. This is very similar to the way win32 DDEremoting works now, but somewhat different from the xremote model.

We should also document a convention for the alphabetic letter prefixes, such that important early handlers such as venkman can reliably precede handlers that might need to be debugged.

The command-line-service should not be a service, but rather a per-startup passed to the handlers. You would have a separate "command-line-service" for remote invocation of an app. The service would QI to nsIArray/MutableArray, allowing handlers to manipulate the argument array. This may cause some issues with GTK/X startup, which needs access to the original argv/argc at startup. This is already a thorn in the side of embeddors who have already initialized GTK/X and don't want our widget code trying to do it again.

A sample command-line handler might look like this (with a bit more error-checking, hopefully):

function handle(commandLine) {
  var ww = C[...].getService(I.nsIWindowWatcher);

  var found;
  while ((found = commandLine.findFlag("edit", false)) >= 0) {
    var uriarg = commandLine.getArgument(found + 1);
    var theuri = commandLine.resolveURI(uriarg);

    ww.openWindow(null, "chrome://editor/content/", null, "chrome", null, theuri);
    commandLine.removeArguments(found, found + 1);
  }

  found = commandLine.findFlag("editor", false);
  if (found >= 0) {
    ww.openWindow(null, "chrome://editor/content/", null, "chrome", null, null);
  }
}


Comments XUL:Axel Hecht

I'm not so happy about the numbers, like the

 commandLine.getArgument(found + 1);

and

 commandLine.removeArguments(found, found + 1);

I'd prefer to see just found there instead of found + 1.

bsmedberg says: Why, and how would it work? getArgument(found) returns "-arg" and getArgument(found + 1) return "param".

Could the gtk startup just be an additional command-line handler, disabled in embedding?

bsmedberg says: no. The startup sequencing does not allow for that.

Callek says: I agree with Axel on the ugliness of the look of that. Could we perhaps add a method similar to getArgumentValue(found) which would do what found + 1 does now in this case?

Neil: maybe a function void extractArguments(in string flag, in unsigned long max, out unsigned long count, [retval, array, size_is(count)] out string args); which would remove the extracted args or return null on failure.

Neil: Is there need for a case where a handler doesn't want to open a window, doesn't want to suppress other handlers but may or may not want to suppress default window opening? Currently some of the handlers have hacks that say "don't open a default window if we are remoting".