Thunderbird/Firefox external program run and stdio fetching

Problem:

You want to write a thunderbird plugin function that runs/calls an external program and returns stdio.

Solution:

Due to Clarity, I will present step by step solutions. However if someone finds a solution, requiring less code I would like to hear of it.

First I will show how to run an external program in a mozilla etension. To do so, I will refer to the documentation of the nsIProcess Object, which contains a quite good example to do so.

// create an nsIFile for the executable
var file = Components.classes["@mozilla.org/file/local;1"]
                     .createInstance(Components.interfaces.nsIFile);
file.initWithPath("c:\\myapp.exe");

// create an nsIProcess
var process = Components.classes["@mozilla.org/process/util;1"]
                        .createInstance(Components.interfaces.nsIProcess);
process.init(file);

// Run the process.
// If first param is true, calling thread will be blocked until
// called process terminates.
// Second and third params are used to pass command-line arguments
// to the process.
var args = ["argument1", "argument2"];
process.run(false, args, args.length);

This code is really self explaining. However the code still lacks in the fact, that you don’t get any results. I tried to exploit the arguments given to process.run in the following way

var args = ["argument1", "argument2", ">" + tmpFile];
process.run(false, args, args.length);

to pipe the output of the program to an tmpfile, that I could read in later, but this don’t work. It seems, that all special characters will get escaped. However still we need an file reading and tmpfile creating function, so this code snipped will provide these functions:

function readFile(file) {  // Note: this function takes an nsIFile object as input (see upper code on how to convert)
    // first create a fileuri with nsIOService. Documentation: https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIIOService
    var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
    var fileURI = ioService.newFileURI(file);
    // open the file channel
    var fileChannel = ioService.newChannel(fileURI.asciiSpec, null, null);
    // get the file content stream
    var rawInputStream = fileChannel.open();
    var scriptableInStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
    scriptableInputStream.init(rawInputStream);
    // read all available bytes
    var available = scriptableInputStream.available();
    var fileContents = scriptableInputStream.read(available);
    // close stream and return
    scriptableInputStream.close();
    return fileContents;
}

function createTmpFile() {
    // create a directory service object
    var ds = Components.classes["@mozilla.org/file/directory_service;1"].getService();
    var dsprops = ds.QueryInterface(Components.interfaces.nsIProperties);
    // and get/create a tmp file with normal file attributes
    var tmpFile = dsprops.get("TmpD", Components.interfaces.nsIFile);
    tmpFile.append("TmpFile.tmp");
    tmpFile.createUnique(tmpFile.NORMAL_FILE_TYPE, 0600);
    return tmpFile;
}

So at this point I face a problem, I may call another program, anywhere in the system, but I might not get the stdout of this program, if the program is not designed to write its output to a file, that aferwards could be read in. So in order to get stdout of each program, I can call a wrapper script like:

#! /usr/bin/env bash
# get the output of the bash command stored in $1 and pipe them to the file located at $2
# I then use the calling code to pass the program I really want to execute in param1 and the tmpfile I created for later reading in the output of the program in param2.
$1 > $2

Note, that this wrapper script only works on Linux. In order to support different os and to call different wrapper scripts accordingly, you may or may not use the following code snipped to destinguish between them:

var os = Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS;

Also such a wrapper script is normally not located in any os and it may look funny and daunting to require users to have cerain programs located at precise positions in there file systems. A possible solution to this program, would be to enpack these wrappers in the xpi extension file. But then these files are zipped and like this may not be called. So we have to tell thunderbird/firefox to enpack our addon at installation time. We might do this using the em:unpack attribute in the install.rdf file.

<?xml version="1.0" encoding="UTF-9"?>

<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  <Description about="urn:mozilla:install-manifest">
[...]
    <em:id>[email protected]</em:id> <!-- I will refer to this line later ... -->
[...]
    <em:unpack>true</em:unpack>
[...]
  </Description>
</RDF>

Now the last step of this blog post is to get the installation directory of the plugin. From this step on, there’s no problem to run the wrapper script. The result of this script may then got read in by the readFile function. (The tmpfile to write the output to may easily be created by the above createTmpFile function. In order to delete the tmp file after execution, you may run: tmpFile.remove(false);)

In order to get the installation location of a plugin, we may run a function from the addonManager module. This function returns an addon object for any addon installation id.

var thisAddon;
var addonLocation;
Components.utils.import("resource://gre/modules/AddonManager.jsm");
AddonManager.getAddonByID("[email protected]", // this must be identical to the <em:id>[email protected]</em:id> attribute content in the install.rdf file.
  function (addon) {
    thisAddon = addon;
  }
);

// do something else here!
if(typeof addonLocation == 'undefined' || addonLocation == null) {
    addonLocation = thisAddon.getResourceURI("").QueryInterface(Components.interfaces.nsIFileURL).file.clone();
}
// have a look at addonLocation.path and addonLocation.append("someFileInTheXpi")