Blog

How To: XPCOM-Zugriff und INI-Parser mit dem Firefox Add-on SDK

Verfasst von: Sören Hentzschel

How To: XPCOM-Zugriff und INI-Parser mit dem Firefox Add-on SDK

Vor einigen Monaten hatte ich bereits ein erstes Tutorial geschrieben, welches eine kleine Einführung in die Add-on-Entwicklung für Firefox mit dem Add-on SDK gegeben hat. In diesem haben wir gelernt, Funktionen in der Add-on Leiste von Firefox hinzuzufügen (Widget-API) und Seiten auf verschiedene Weisen zu öffnen (Tabs-API). Durch die Self-API waren wir in der Lage, Dateien mit unserer Erweiterung mitzuliefern und schließlich konnten wir durch das preferences-service-Modul auch noch Entscheidungen in Abhängigkeit von about:config-Schaltern treffen.

Hierauf möchte ich nun aufbauen und einen Schritt weiter gehen. Wir können auch direkten Zugriff auf das Components-Objekt von Firefox erhalten, welches uns Zugriff auf sämtliche XPCOM-Interfaces gibt, was beinahe unbegrenzte Möglichkeiten eröffnet. So werden wir heute lernen, Applikations-Informationen wie die Versionsnummer von Firefox auszulesen und schließlich eine *.ini-Datei aus dem Installationsverzeichnis von Firefox auslesen. Am Ende des Tutorials werden wir wieder eine kleine Erweiterung erstellt haben.

Die erste Überlegung ist eine Konzeptionelle, die nach dem Sinn unserer Erweiterung. Und hier möchte ich die Idee meiner Erweiterung Current Pushlog aufgreifen und die Erstellung einer vereinfachten Version erklären. Dazu muss man wissen, dass Mozilla ein Versionskontrollsystem (Mercurial) nutzt, über welches sich alle Änderungen von einer Firefox zur nächsten nachvollziehen lassen. Diese Änderungen zwischen zwei Versionen sind in sogenannten Pushlogs zusammengefasst und den jeweils passenden Link wollen wir über unsere Erweiterung bereitstellen. Der Link zu dieser Seite entspricht einem festen Schema und hierfür benötigen wir das Quellverzeichnis sowie die Bezeichnung des sogenannten Changesets sowohl von der aktuellen als auch der vorherigen Version.

Der erste Schritt ist wie immer das Erstellen eines neuen Projekts im Add-on Builder von Mozilla. In den Projekteinstellungen geben wir der Erweiterungen einen sinnvollen Namen sowie eine Beschreibung. Dann starten wir mit einem ersten Grundgerüst der Erweiterung, welches erst einmal noch nichts weiter macht, als eine init()-Funktion bereitzustellen, in welcher wir über console.error() Text in die Konsole schreiben, welche wir per Klick auf das Symbol mit dem Warndreieck im Add-on Builder öffnen können. Diese Funktion rufen wir schließlich in der main()-Funktion auf. Alles, was innerhalb von exports.main = function() {} geschrieben steht, wird ausgeführt, sobald die Erweiterung bereit ist.

var pushlog = {
    init : function() {
        console.error('Wir starten ein neues Addon.');
    }
}

exports.main = function() {    
    pushlog.init();
};

Wirklich spannend wird es jetzt: Wir wollen Informationen aus Firefox auslesen. Als kleine Aufwärmübung lesen wir ein paar Anwendungsinformationen zu Firefox aus. Hierfür brauchen wir chrome-Privilegien und das erreichen wir auf ganz ähnliche Weise, wie wir APIs einbinden, wie wir bereits im letzten Tutorial gelernt haben. Wir schreiben in die erste Zeile unserer main.js:

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

Das Cc steht hierbei für Components.classes und das Ci für Components.interfaces. Was wir hier schreiben, hängt davon ab, was wir benötigen und was wir benötigen, erfahren wir aus der Dokumentation in dem Moment, indem wir den Code schreiben. Eine Übersicht über die möglichen Symbole gibt es hier.

Die Übersicht der vorhandenen XPCOM-Interfaces hatte ich weiter oben bereits verlinkt, dies sei an dieser Stelle noch einmal getan. Oft lässt sich beim Überfliegen der Namen erahnen, wo sich ein näherer Blick lohnen könnte. Das Ziel ist wie gesagt die Ausgabe von Anwendungsinformationen – da klingt nsIXULAppInfo doch gut! Hier sehen wir bereits gleich zu Beginn ein Beispiel, wie wir dieses Interface verwenden können:

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

Im Prinzip können wir das genau so in unsere Erweiterung übernehmen. Und das machen wir auch, Components.classes ersetzen wir dabei noch durch Cc und Components.interfaces durch Ci und übernehmen das dann anstelle der bisherigen console.error()-Anweisung in unsere init()-Methode. Und deswegen haben wir uns für diese Kürzel auch in der require()-Anweisung in der ersten Zeile entschieden. Über xulAppInfo.version beispielsweise können wir dann auf die Versionsnummer zugreifen, die möglichen Attribute stehen in der Dokumentation direkt darunter.

init : function() {
    var xulAppInfo = Cc['@mozilla.org/xre/app-info;1'].getService(Ci.nsIXULAppInfo);
    console.error(
        'version: ' + xulAppInfo.version + '\n'
        + 'platformVersion: ' + xulAppInfo.platformVersion + '\n'
        + 'ID: ' + xulAppInfo.ID + '\n'
        + 'name: ' + xulAppInfo.name + '\n'
        + 'vendor: ' + xulAppInfo.vendor + '\n'
        + 'platformBuildID: ' + xulAppInfo.platformBuildID
    );
}

Speichern wir jetzt unsere Erweiterung und testen sie, erhalten wir in der Fehlerkonsole diverse Informationen zu unserem Firefox-Build und haben damit bereits erfolgreich auf ein XPCOM-Interface zugegriffen! Aber das soll nur zur Aufwärmung gewesen sein, für unsere gewünschte Erweiterung ist dies nicht weiter relevant, hierfür brauchen wir ein anderes Interface. Also löschen wir den kompletten Inhalt der init()-Methode (aber nicht die Methode selber, die brauchen wir später noch).

Die Informationen, die wir brauchen, liefert Firefox in einer Datei, nämlich in der Datei application.ini, welche sich im Installationsverzeichnis von Firefox befindet. Hierzu bietet die Dokumentation einen schönen Artikel zu File I/O, im Abschnitt “Getting special files” steht der entscheidende Code, um auf diese Datei zuzugreifen. An den entsprechenden Stellen schreiben wir wieder Cc und Ci und dann gibt es noch eine weitere Stelle aus dem Beispiel anzupassen, nämlich das “ProfD”. Dies bedeutet nämlich, dass wir die Datei im Profilverzeichnis suchen. Direkt unter dem Code-Beispiel steht aber bereits eine Tabelle mit allen möglichen Orten. Und da wir das Installationsverzeichnis von Firefox wollen, entscheiden wir uns an dieser Stelle für “CurProcD”. Diese ganze Kette haben wir einer Variablen zugeordnet und auf diese wenden wir die append()-Methode mit dem Namen der Datei als Parameter an, welche wir auslesen möchten, nämlich application.ini. Diesen ganzen Code schreiben wir in eine neue Methode, welche wir getApplicationValue(section, key) nennen. Die beiden Parameter werden wir gleich noch brauchen. Hier der Code bis zu dieser Stelle:

init : function() {
},
    
getApplicationValue : function(section, key) {
    var applicationFile = Cc['@mozilla.org/file/directory_service;1'].getService(Ci.nsIProperties).get('CurProcD', Ci.nsIFile);
	applicationFile.append('application.ini');
}

Schauen wir uns an dieser Stelle den Aufbau der application.ini (Ausschnitt) an:

[App]
Version=10.0.2
BuildID=20120215223356
SourceRepository=http://hg.mozilla.org/releases/mozilla-release
SourceStamp=72ad46d416ce

[Gecko]
MinVersion=10.0.2
MaxVersion=10.0.2

Und von hier kommen wir direkt zu den Parametern unserer getApplicationValue()-Methode – bei [App] und [Gecko] handelt es sich um die “section”, alles links der Gleichheitszeichen sind die “keys”, welche wir auslesen wollen. Was wir konkret auslesen möchten, sind SourceRepository und SourceStamp, beides aus der section [App]. Dafür erstellen wir nun erst einmal zwei weitere Methoden, welche den entsprechenden Eintrag zurückgeben, nämlich getSourceRepository() und getSourceStamp(). Beide rufen die eben erstellte Methode getApplicationValue() auf, die Parameter sind entsprechend “App” und “SourceRepository” sowie “App” und “SourceStamp”.

getApplicationValue : function(section, key) {
    var applicationFile = Cc['@mozilla.org/file/directory_service;1'].getService(Ci.nsIProperties).get('CurProcD', Ci.nsIFile);
    applicationFile.append('application.ini');
},
    
getSourceRepository : function() {
    return this.getApplicationValue('App', 'SourceRepository');
},

getSourceStamp : function() {
	return this.getApplicationValue('App', 'SourceStamp');
}

Damit das so funktioniert, muss die Methode getApplicationValue() auch noch etwas zurückgeben, denn momentan passiert mit unseren Parametern ja überhaupt nichts. Firefox bietet zu diesem Zweck einen INI-Parser, welcher im Interface nsIINIParserFactory implementiert ist. Die createINIParser()-Methode bekommt dabei die zuvor ausgewählte Datei als Parameter auf den Weg. Daher ergänzen wir diese Methode noch um Folgendes:

return Cm.getClassObjectByContractID('@mozilla.org/xpcom/ini-parser-factory;1', Ci.nsIINIParserFactory)
	.createINIParser(applicationFile).getString(section, key);

Da Cm hier wieder ein Symbol ist, nämlich für Components.manager, müssen wir unsere erste Programmzeile noch anpassen:

const {Cc, Ci, Cm} = require('chrome');

Nun wollen wir natürlich wissen, ob wir bis hierhin alles richtig gemacht haben und nutzen dafür unsere init()-Methode, indem wir den Link zum Pushlog in der Konsole ausgeben. Dazu muss die Struktur des Links bekannt sein:

<sourcerepository>/pushloghtml?fromchange=<prevsourcestamp>&tochange=<sourcestamp>
Beispiel: http://hg.mozilla.org/releases/mozilla-release/pushloghtml?fromchange=c581b36e7a12&tochange=72ad46d416ce

Okay, das SourceRepository ist klar, genauso auch der SourceStamp. Aber wir benötigen noch den PrevSourceStamp, sprich den Changeset des vorherigen Builds. Diesen kennt Firefox nicht. Das heißt, uns bleibt nichts anderes übrig, als diesen abzuspeichern und beim Firefox-Update zu aktualisieren. Aber wir wollen ja nun wissen, ob wir bis hierhin richtig gearbeitet haben. Also verwenden wir an dieser Stelle einfach zweimal den SourceStamp:

init : function() {
    console.error(this.getSourceRepository() + '/pushloghtml?fromchange=' + this.getSourceStamp() + '&tochange=' + this.getSourceStamp());
},

Wenn die Fehlerkonsole keinen Fehler wirft und wir bis hierhin also alles richtig gemacht haben, geht es nun an das Speichern der Changesets, das machen wir über about:config-Schalter, wie wir es bereits im ersten Tutorial gelernt haben. Dazu folgende Überlegung: Wir speichern das Changeset des aktuellen Builds in einer Variablen ab. In einer zweiten Variable lesen wir den Inhalt unserer Einstellung für das aktuelle Changeset aus, wir nennen den Schalter in diesem Beispiel extensions.pushlog.changeset.current. Existiert diese Einstellung noch nicht (vor dem ersten Programmupdate nach Installation der Erweiterung) setzen wir hier den aktuellen Wert ein. Anschließend überprüfen wir, ob dieser Schalter noch nicht existiert oder ob beide Variablen unterschiedlich sind. Und wenn eine dieser Bedingungen erfüllt ist, wird extensions.pushlog.changeset.current auf das aktuelle Changeset gesetzt und ein weiterer Schalter, nämlich extensions.pushlog.changeset.previous auf den Wert, welchen wir vorher aus extensions.pushlog.changeset.current ausgelesen und in einer zweiten Variable gespeichert hatten. Das Ganze machen wir in unserer init()-Methode, welche nach Start von Firefox aufgerufen wird. Auf diese Weise stellen wir sicher, dass nach einem Update von Firefox der alte SourceStamp als vorheriger gespeichert wird und der des aktuellen Builds als aktueller SourceStamp gesehen wird. Zur Erinnerung: Der zweite Parameter der prefs.get()-Methode steht für einen Standardwert, welcher angenommen wird, wenn der Schalter nicht existiert.

var changeset = this.getSourceStamp();
var current = prefs.get('extensions.pushlog.changeset.current', this.getSourceStamp());

if(!prefs.has('extensions.pushlog.changeset.current') || changeset != current) {
	prefs.set('extensions.pushlog.changeset.previous', current);
	prefs.set('extensions.pushlog.changeset.current', changeset);
}

Damit das aber überhaupt funktioniert, müssen wir das preferences-service-Modul zu Beginn erst noch einbinden:

const prefs = require('preferences-service');

Nun können wir auch die getPrevSourceStamp()-Methode implementieren. Statt wie bei der getSourceStamp()-Methode etwas aus der ini-Datei auszulesen, lesen wir hir nun die Einstellung aus:

getPrevSourceStamp : function() {
	return prefs.get('extensions.pushlog.changeset.previous', this.getSourceStamp());
},

Die testweise console.error()-Zeile aus der init()-Methode kann wieder entfernt werden, stattdessen packen wir den Inhalt dessen in eine eigene Methode getPushlogUrl() und geben dies zurück, das erste getSourceStamp() ersetzen wir dabei mit getPrevSourceStamp():

getPushlogUrl : function() {
	return this.getSourceRepository() + '/pushloghtml?fromchange=' + this.getPrevSourceStamp() + '&tochange=' + this.getSourceStamp();
}

Jetzt sind wir fast fertig! Wir wollen die URL zum Pushlog ja sinnvoll verwenden. Deswegen wollen wir ein Icon in der Addon-Leiste platzieren, bei Klick auf dieses soll sich der Pushlog in einem neuen Tab öffnen, welcher im Hintergrund geladen wird. Das ist exakt das, was wir bereits aus dem ersten Tutorial kennen, daher nur noch in aller Kürze: Wir benötigen die Widget-API, die Tabs-API sowie die Self-API, da wir ein Icon mitliefern werden:

const self = require('self');
const tabs = require('tabs');
const widget = require('widget');

In unserer main()-Funktion erstellen wir nun das Widget nach der init()-Methode:

widget.Widget({
    id : 'pushlog',
    label : 'Pushlog',
    contentURL : self.data.url('pushlog.png'),
    onClick : function() {
   	tabs.open({
    		url : pushlog.getPushlogUrl(),
		inBackground : true
	});
    }
});

Nun noch im Add-on Builder per Klick auf das [+] unter Data eine Grafik hochladen, welche entsprechend dem self.data.url() benannt ist und die Erweiterung kann gepackt werden.

Hier noch einmal der komplette Code der Demo-Erweiterung:

const {Cc, Ci, Cm} = require('chrome');
const prefs = require('preferences-service');
const self = require('self');
const tabs = require('tabs');
const widget = require('widget');

var pushlog = {
    init : function() {
    	var changeset = this.getSourceStamp();
		var current = prefs.get('extensions.pushlog.changeset.current', this.getSourceStamp());

		if(!prefs.has('extensions.pushlog.changeset.current') || changeset != current) {
			prefs.set('extensions.pushlog.changeset.previous', current);
			prefs.set('extensions.pushlog.changeset.current', changeset);
		}
    },
    
    getApplicationValue : function(section, key) {
    	var applicationFile = Cc['@mozilla.org/file/directory_service;1'].getService(Ci.nsIProperties).get('CurProcD', Ci.nsIFile);
		applicationFile.append('application.ini');

		return Cm.getClassObjectByContractID('@mozilla.org/xpcom/ini-parser-factory;1', Ci.nsIINIParserFactory)
			.createINIParser(applicationFile).getString(section, key);
	},
    
    getSourceRepository : function() {
    	return this.getApplicationValue('App', 'SourceRepository');
	},
    
    getPrevSourceStamp : function() {
    	return prefs.get('extensions.pushlog.changeset.previous', this.getSourceStamp());
	},

	getSourceStamp : function() {
		return this.getApplicationValue('App', 'SourceStamp');
	},
    
    getPushlogUrl : function() {
    	return this.getSourceRepository() + '/pushloghtml?fromchange=' + this.getPrevSourceStamp() + '&tochange=' + this.getSourceStamp();
	}
}

exports.main = function() {    
    pushlog.init();
    
    widget.Widget({
        id : 'pushlog',
        label : 'Pushlog',
    	contentURL : self.data.url('pushlog.png'),
		onClick : function() {
            tabs.open({
    			url : pushlog.getPushlogUrl(),
				inBackground : true
			});
		}
	});
};

Achtung: Dies ist eine vereinfachte Version meiner Current Pushlog-Erweiterung. Die originale Erweiterung unterstützt verschiedene Release-Kanäle und Möglichkeiten, den Pushlog zu öffnen. Außerdem erscheint, wenn noch kein Firefox-Update durchgeführt worden ist, eine Meldung in einem Panel. Im Rahmen dieses Tutorials habe ich den Funktionsumfang bewusst auf das Wesentliche reduziert.

6


Kommentare

  1. Archaeopteryx  24. Februar 2012, 17:25

    Hallo Sören,

    wenn möglich sollte JavaScript modules (jsm) verwendet werden, um auf häufig genutzte Funktionen zuzugreifen. Diese werden dann nämlich nur einmalig geladen. https://developer.mozilla.org/en/JavaScript_code_modules/Services.jsm ließe sich hier oft einsetzen.

  2. Sören Hentzschel  24. Februar 2012, 23:12

    Hi,

    jupp – auf die JavaScript-Module bin ich im dritten SDK-Tutorial eingegangen. Ich wollte in jedem Fall beide Wege erklären. In der dazugehörigen Erweiterung habe ich teilweise bereits auf JSM umgestellt. Danke für die Erklärung, wieso JSM vorzuziehen ist. Gibt es das auch irgendwo schwarz auf weiß? Ich hab leider nichts gefunden, was erklärt, wieso man JSM verwenden sollte. Nun weiß ich immerhin einen Grund, aber vielleicht gibt es ja noch mehr Hintergrund. Ich bin ja selber am Lernen. :) Nun fehlt mir nur noch hierfür eine JSM-Alternative – falls es hierfür eine Alternative gibt:

    Cm.getClassObjectByContractID(‘@mozilla.org/xpcom/ini-parser-factory;1′, Ci.nsIINIParserFactory).createINIParser(applicationFile).getString(section, key);

  3. Archaeopteryx  24. Februar 2012, 23:38

    https://developer.mozilla.org/en/JavaScript_code_modules
    “JavaScript code modules let multiple privileged JavaScript scopes share code. For example, a module could be used by Firefox itself as well as by extensions, in order to avoid code duplication.”
    Den INI-Parser könnte man durch Services.io mit eigenem Parser-Code (regulärer Ausdruck oder Stringoperationen) ersetzen – persönlich würde ich aber dabei bleiben.

  4. Sören Hentzschel  25. Februar 2012, 03:10

    Passt, danke. ;)

Kommentar hinzufügen

E-Mail-Benachrichtigung bei weiteren Kommentaren.