0

I'm converting an XUL-based Firefox add-on to an SDK based version for simplicity. The XPCOM modules I use in the XUL-based version seem to work but ci.nsIFile is behaving differently.

I can't figure out how to navigate to smartProxy.py which currently sits on the highest level of the directory.

In the XUL version smartProxy.py sits on chrome/bin/smartproxy.py. I use the commands below to execute the program and it works without error.

getExeFile: function() {
    var file = cc["@mozilla.org/file/directory_service;1"].getService(ci.nsIProperties).get("ProfD", ci.nsIFile);
    file.append("smartProxy.py");
    return file;
},

This below is where it's executed which should give a full picture of how the add-on works.

start: function() {
    if (this.process && this.process.isRunning)
        return;
    this.process = cc["@mozilla.org/process/util;1"].createInstance(ci.nsIProcess);
    this.process.init(this.getExeFile());
    this.process.runAsync([], 0, this.processObserver);
    this.setProxy();
    this.executeObservers();
},

How do I go about finding smartProxy.py so that it can be executed?

nmaier
  • 32,336
  • 5
  • 63
  • 78
Josh Horowitz
  • 655
  • 2
  • 6
  • 15
  • What exactly is not working? Does the reutnred file not have the proper path to smartProxy.py? Is that what you mean by "cant figure out how to navigate to smartyProxy.py that sits at highest level in direcotry"? Try: `Cu.import("resource://gre/modules/FileUtils.jsm"); var py = FileUtils.getFile('ProfD', ['extensions', 'smartProxy.py']);` – Noitidart Jul 23 '14 at 03:04
  • What's not working: It's navigating to a folder that isn't my extension.It doesn't even seem to be associated with firefox. I tried your solution but it wasn't working as I intended so I checked py.path and navigated to it. I found my extension by it was packaged as a .xpi. The actual folders and resources weren't there. I could unpack it but that's not a solution that will work for production. – Josh Horowitz Jul 23 '14 at 05:03
  • Maybe it is associated with firefox. Your solution brings me to /var/folders/2j/qmz1s35x5tx49_27qcbg81sr0000gn/T/tmpiK6nWF.mozrunner/extensions, the solution I was using brought me to /var/folders/2j/qmz1s35x5tx49_27qcbg81sr0000gn/T – Josh Horowitz Jul 23 '14 at 05:05
  • 1
    Oh whoa, thats weird. Is this file packed with your extension? If this is true, your extension is an xpi, its not a folder. If you want to navigate to the extension that is your folder I'm not sure how to do it with sdk, but its definitely not a folder ,its an xpi. Unless did you set `unpack true` in install.rdf? – Noitidart Jul 23 '14 at 05:29
  • 1
    I figured it out! Thanks so much. In the SDK they use package.json which is used to generate install.rdf. They allow you to set unpack to be true but they didn't put it in the documentation because of "performance issues." https://bugzilla.mozilla.org/show_bug.cgi?id=885673 – Josh Horowitz Jul 23 '14 at 06:13
  • Cool man! What was your final code to get the path, and what was your final path, I'm interested because usually `ProfD` doesn't give you that path. Plus rep for that bug link, i didnt know it had perf issues. – Noitidart Jul 23 '14 at 06:40
  • Also you don't have to unpack, you can use nsIZipReader or NetUtils to extract it from your xpi and put it somewhere, so you don't suffer the performance issues, whatever they are, its not made clear on bugzilla or anywhere that i could find. – Noitidart Jul 23 '14 at 06:43
  • 1
    var file = FileUtils.getFile('ProfD', ['extensions', 'jid1-xS8bcmiRpHSixA@jetpack', 'resources', 'my-addon', smartProxy.py]); Now I just need to figure out how to stop the SDK from deleting smartProxy.py when it compiles it and test to see what influences the name of jid1-xS8bcmiRpHSixA@jetpack. If it's something that changes often like with the version number I may need to write a script that handles continuity automatically. – Josh Horowitz Jul 23 '14 at 08:24

2 Answers2

5

Running an external command from Firefox right now is only supported with the nsIProcess API. You figured out this bit for yourself already.

Packaging a python script.

The SDK will only package certain files/locations. It would be easiest to just place the python script in the data/ folder, because that is one of the locations the SDK will package all files from.

nsIProcess

nsIProcess needs an executable file, and that file actually needs to be a real file (not just something contained within the XPI, which by default is not unpacked).

So there are two cases we might want to handle:

  1. File is a real file - Just execute it.
  2. File is packaged - Need to extract/copy the data into a (temporary) real file and execute that.

The following code deals with both. I tested and it works for me on OSX (*nix, so should work on Linux or BSDs as well) and Windows, with and without em:unpack (and you should avoid em:unpack).

const self = require("sdk/self");

const {Cc, Ci, Cu} = require("chrome");

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");

const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].
                       getService(Ci.nsIChromeRegistry);
const ResProtoHandler = Services.io.getProtocolHandler("resource").
                        QueryInterface(Ci.nsIResProtocolHandler);

function copyToTemp(uri, callback) {
  // Based on https://stackoverflow.com/a/24850643/484441
  let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
  file.append(self.name + "_" + uri.spec.replace(/^.+\//, ""));
  file.createUnique(Ci.nsIFile, 0o0700);
  NetUtil.asyncFetch(uri, function(istream) {
    let ostream = Cc["@mozilla.org/network/file-output-stream;1"].
                  createInstance(Ci.nsIFileOutputStream);
    ostream.init(file, -1, -1, Ci.nsIFileOutputStream.DEFER_OPEN);
    NetUtil.asyncCopy(istream, ostream, function(result) {
      callback && callback(file, result);
    });
  });
}

function runProcessAndThen(file, callback) {
  console.log("running", file.path);

  let proc = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
  try {
    // Set executable bit on unix
    file.permissions = file.permissions | 0o0500;
  }
  catch (ex) {
    // Might throw?!
  }
  proc.init(file);
  proc.runAsync([], 0, callback);
}

function runFromURIWithPotentialCopy(uri, callback) {
  if (!uri.spec) {
    uri = Services.io.newURI(uri, null, null);
  }
  if (uri.scheme === "resource") {
    // Need to resolve futher. Strip one layer of indirection and recursively
    // call ourselves.
    uri = Services.io.newURI(ResProtoHandler.resolveURI(uri), null, null);
    return runFromURIWithPotentialCopy(uri, callback);
  }

  if (uri.scheme === "chrome") {
    // Need to resolve futher. Strip one layer of indirection and recursively
    // call ourselves.
    return runFromURIWithPotentialCopy(ChromeRegistry.convertChromeURL(uri), callback);
  }

  if (uri instanceof Ci.nsIFileURL) {
    // A plain file we can execute directly.
    return runProcessAndThen(uri.file, callback);
  }

  if (uri instanceof Ci.nsIJARURI) {
    // A packaged file (in an XPI most likely).
    // Need to copy the data into some plain file and run the result.
    return copyToTemp(uri, function(f) {
      runProcessAndThen(f, function() {
        try {
          // Clean up after ourselves.
          f.remove(false);
        }
        catch (ex) {
          console.error("Failed to remove tmp file again", ex);
        }
        callback.apply(null, arguments);
      });
    });
  }

  throw new Error("Cannot handle URI");
}

function afterRun(subject, topic, data) {
  console.log(subject, topic, data);
}

function runFileFromDataDirectory(name, callback) {
  try {
    runFromURIWithPotentialCopy(self.data.url(name), callback);
  }
  catch (ex) {
    console.error(ex);
  }
}

runFileFromDataDirectory("test.py", afterRun);

Running a script

Running a script (as opposed to a full-blown binary) can be tricky. In case of Python e.g. the *nix OSes needs to be told that there is an interpreter and what this is, which is done by a shebang. On Windows, Python needs to be installed with .py file type registration, which the default installer will do, but not "portable" versions".

Community
  • 1
  • 1
nmaier
  • 32,336
  • 5
  • 63
  • 78
  • In depth! I had a thought. Is it possible to wrap a `resource://` in `new FileUtils.File`? So like in this guys case `var py = new FileUtils.File(self.data.url('module/script.py'))` then use this in `process.run`? – Noitidart Jul 23 '14 at 21:31
  • How to put this nicely: No! :D – nmaier Jul 23 '14 at 21:32
  • lolol :( ok thx lol ur nice guy :P what about feeding the process thing a fileOutputStream, rather than making a copy of the file? so like after the `ostream.init(file, -1, -1, Ci.nsIFileOutputStream.DEFER_OPEN);` we can skip the `asyncCopy` and just feed the stream somehow? – Noitidart Jul 23 '14 at 21:38
  • This is great! Thanks. I ended up getting it working but this is a better method. – Josh Horowitz Jul 26 '14 at 10:53
2

Hey man I just looked up some stuff on sdk. Don't do the unpacking in the install.rdf because of those "performance issues" that are cited. Instead just put the thing in your data folder. Than access it like this:

https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/self

var self = require("sdk/self");
var py = self.data.url("my-panel-content.html")
Noitidart
  • 35,443
  • 37
  • 154
  • 323
  • That's a good hack for preventing the compiler from removing smartProxy from the package but how would I run it without using nsIProcess which I think requires nsIFile? Even if it's an executable or something which doesn't require the command line to run, I don't see any any way to execute it through self. It works for HTML because it has the panel API to display it – Josh Horowitz Jul 23 '14 at 09:14
  • It works perfectly to prevent the SDK from deleting it. – Josh Horowitz Jul 23 '14 at 09:19
  • 1
    I say use `NetUtils` and copy the file to just the `ProfD` folder and on uninstall of your addon make sure to delete that file. Or use netutils to read it with `file input stream` and then send it to `process` with `file output stream`, this way is actually kind of interesiting i would learn a lot from it. Post up a topic we can have some experts on the forum answer that, im very interested to see that answer. – Noitidart Jul 23 '14 at 09:29
  • While this answer is not wrong per-se, it does not actually attempt to answer the actual question at hand, namely how to run a file contained within the XPI, with the sub-question of why the SDK does not package the `.py` file in the XPI. – nmaier Jul 23 '14 at 12:15