[tor-commits] [vidalia/alpha] Write plugin tutorial
chiiph at torproject.org
chiiph at torproject.org
Wed May 9 23:28:52 UTC 2012
commit f10a3d04859a1aed9d6b5e1c7bbad7b2fc29adf5
Author: Tomás Touceda <chiiph at torproject.org>
Date: Sat Apr 14 14:39:15 2012 -0300
Write plugin tutorial
---
changes/pluginTutorial | 2 +
doc/plugin-tutorial.txt | 544 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 546 insertions(+), 0 deletions(-)
diff --git a/changes/pluginTutorial b/changes/pluginTutorial
new file mode 100644
index 0000000..5babab3
--- /dev/null
+++ b/changes/pluginTutorial
@@ -0,0 +1,2 @@
+ Internal cleanups and improvements:
+ o Document how to write plugins in a tutorial fashion.
\ No newline at end of file
diff --git a/doc/plugin-tutorial.txt b/doc/plugin-tutorial.txt
new file mode 100644
index 0000000..4d4fb11
--- /dev/null
+++ b/doc/plugin-tutorial.txt
@@ -0,0 +1,544 @@
+== How to write Vidalia Plugins ===
+
+=== Sections ==
+
+ * Foreword
+ * Directory structure for plugins
+ * How Vidalia executes the plugin
+ * GUI handling
+ * Settings handling
+ * Simple example
+ * I know Qt, how do I use signals and slots?
+ * I made a plugin, what now?
+
+=== Foreword ===
+
+This document intends to be a simple tutorial that will help you write
+Vidalia plugins. It is not intended to be an exhaustive plugin API
+reference, there will be another document for that. This is also not a
+Javascript reference nor a Qt one, although you will need knowledge of
+both in order to write a plugin.
+
+This is not also a complete plugin engine specification, for that
+there is already a doc named plugin-framework.txt inside Vidalia's doc
+directory.
+
+We assume in this document that you have basic Qt and Javascript
+knowledge, and that you have Vidalia with the qtscriptgenerator libs
+available.
+
+To check this last point, you can open Vidalia, go to Plugins->Debug
+output and you should be able to see the following:
+
+{{{
+
+Available extensions:
+ qt
+ qt.core
+ qt.dbus
+ qt.gui
+ qt.network
+ qt.uitools
+ qt.xml
+
+}}}
+
+The actual important extensions are: qt, qt.core, qt.gui and qt.uitool
+(only if it's a GUI plugin). Depending on the functionality you intend
+your plugin to have, you can ignore the rest.
+
+=== Directory structure for plugins ===
+
+If you open vidalia.conf for Vidalia >= 0.3.1, you will notice a
+variable named "PluginPath" in the [General] section. This variable
+points to the root of where all the plugins are placed. So, the plugin
+root will look something like this:
+
+{{{
+
+pluginRoot/
+ \_ plugin1/
+ |_ plugin2/
+ |_ plugin3/
+
+}}}
+
+Vidalia expects every installed plugin to be a directory inside the
+plugins root.
+
+For every plugin, the directory structure should have at least the
+following files:
+
+{{{
+
+plugin1/
+ \_ info.xml
+ |_ plugin1.js
+
+}}}
+
+You can distribute any files other than these, may be as a dependency
+for your plugin (icons, another complete application that is executed
+by your plugin, .ui files, etc).
+
+==== The info.xml file ====
+
+The most important file (besides the actual plugin code) is the
+info.xml one. This contains the basic information that Vidalia needs
+to understand what to do with this plugin, how to execute it, how to
+and if it should display it in the Plugins menu, and what files to
+load.
+
+Lets take a look at the info.xml file from the Tor Browser Bundle
+plugin:
+
+{{{
+
+<VidaliaPlugin>
+ <name>Browser Bundle Configuration</name>
+ <date>15/06/2011</date>
+ <author>chiiph</author>
+ <type persistent="true" gui="true" />
+ <files>
+ <file>tbb.js</file>
+ </files>
+ <namespace>tbb</namespace>
+</VidaliaPlugin>
+
+}}}
+
+For a complete description of each tag, please read secrion 2 of the
+plugin-framework.txt doc.
+
+In practise, the name tag will be used as the menu entry for this
+plugin if it is of type gui="true". Vidalia will load every file
+descripted in the files tag, so they should all be valid Javascript
+files. And the namespace should be unique (i.e. do not create a plugin
+with tbb as its namespace, unless you want to have one of the two
+overriden by the other). This namespace should be the same as the name
+of the main variable you will use inside the javascript files, as we
+will see later on.
+
+=== How Vidalia executes the plugin ===
+
+As a quick reference for how plugins are executed, there are three
+main functions: start(), stop(), and buildGUI(). The functions that
+must be implemented are start and stop, since they will always be
+executed one way or the other. buildGUI depends on whether your plugin
+is of type gui="true" or not.
+
+Here's a basic skeleton for a plugin's main Javascript file:
+
+{{{
+
+importExtension("qt");
+importExtension("qt.core");
+importExtension("qt.gui"); // optional
+importExtension("qt.uitools"); // optional
+
+var tbb = {
+ someStaticVariable: 1,
+ someOtherStaticVar: "Some string",
+
+ start: function() {
+ // Start code here...
+ },
+
+ buildGUI: function() {
+ // GUI creation here...
+ },
+
+ stop: function() {
+ // Stop function in here...
+ },
+};
+
+}}}
+
+The start function is executed when Vidalia starts if type
+persistent="true", or before the GUI is created when type gui="true".
+
+If a plugin is not of type persistent or gui, it will never be started
+in any way.
+
+The idea behind persistent is that you can run things in background,
+like a UNIX daemon (sort of). As an example, you could start a QTimer
+in your start() function and check something periodically. We use this
+in the Thandy plugin, if you want to read a "real world" example.
+
+=== GUI handling ===
+
+If you want your plugin to have a GUI, we have made kind of a template
+that you can follow:
+
+(This is a part of the Tor Browser Bundles complete buildGUI
+implementation, you can see it in tbb.js in the plugins official
+repository.)
+
+{{{
+ buildGUI: function() {
+ this.tab = new VidaliaTab("Browser Bundle Settings", "TBB");
+
+ var file = new QFile(pluginPath+"/tbb/tbb.ui");
+ var loader = new QUiLoader(this.tab);
+ file.open(QIODevice.ReadOnly);
+ this.widget = loader.load(file);
+ var layout = new QVBoxLayout();
+ layout.addWidget(this.widget, 0, Qt.AlignCenter);
+ this.tab.setLayout(layout);
+ file.close();
+
+ var portInfo = this.widget.children()[findWidget(this.widget, "portInfo")];
+ if(portInfo == null) {
+ return this.tab;
+ }
+
+ var groupBox = this.widget.children()[findWidget(this.widget, "browserBox")];
+ if(groupBox == null) {
+ return this.tab;
+ }
+
+ this.btnLaunch = groupBox.children()[findWidget(groupBox, "btnLaunch")];
+ if(this.btnLaunch != null) {
+ this.btnLaunch["clicked()"].connect(this, this.startSubProcess);
+ }
+
+ return this.tab;
+ },
+}}}
+
+If you read Qt's documentation, what this function does is quite
+straight forward, but I will give you a quick summarization:
+
+{{{
+ buildGUI: function() {
+ this.tab = new VidaliaTab("Browser Bundle Settings", "TBB");
+}}}
+
+First, we need to create a new VidaliaTab. The first parameter is its
+name, which can be an arbitrary string. The second one is important,
+because Vidalia will use it as its section inside the vidalia.conf
+file, if you want to store some kind of settings (we will get to this
+in the next section).
+
+{{{
+ var file = new QFile(pluginPath+"/tbb/tbb.ui");
+ var loader = new QUiLoader(this.tab);
+ file.open(QIODevice.ReadOnly);
+ this.widget = loader.load(file);
+}}}
+
+We encourage you to build your GUIs with Qt Designer, but you are free
+to craft them in the code. In this example we use a .ui Designer file.
+
+So we first load the file as a regular one. The pluginPath variable is
+a variable provided by Vidalia for you to know the location of the
+plugin's root directory. From that point, you should know where your
+files are supposed to be located, in this case it is
+pluginsPath+"/tbb/tbb.ui".
+
+We then create the QUiLoader object, open the file and ask the loader
+to do its magic.
+
+{{{
+ var layout = new QVBoxLayout();
+ layout.addWidget(this.widget, 0, Qt.AlignCenter);
+ this.tab.setLayout(layout);
+ file.close();
+}}}
+
+The QUiLoader creates a QWidget derived object in this.widget and now
+we need to put it somewhere the user can see. We could just display
+the widget, but we encourage you to try to integrate your plugin to
+Vidalia as much as possible, so we will add it to a vertical layout,
+and set it as the tab's main layout.
+
+{{{
+ var portInfo = this.widget.children()[findWidget(this.widget, "portInfo")];
+ if(portInfo == null) {
+ return this.tab;
+ }
+
+ var groupBox = this.widget.children()[findWidget(this.widget, "browserBox")];
+ if(groupBox == null) {
+ return this.tab;
+ }
+
+ this.btnLaunch = groupBox.children()[findWidget(groupBox, "btnLaunch")];
+ if(this.btnLaunch != null) {
+ this.btnLaunch["clicked()"].connect(this, this.startSubProcess);
+ }
+}}}
+
+We can access each widget from this.widget, but it is recommended to
+create variables for each component that you'd like to access.
+
+To make that job easier, Vidalia provides a findWidget function that
+takes a widget and finds the one with the provided name inside the
+first one's children.
+
+For example, in this.btnLaunch, we want to find the widget called
+btnLaunch inside the groupBox widget.
+
+{{{
+ return this.tab;
+ },
+}}}
+
+As the last step, we return the tab for Vidalia to display. Depending
+on how you code your buildGUI function, you may want to check if
+this.tab is already created and return it instead of executing the
+whole function every time Vidalia wants to display the plugins GUI.
+
+Keep in mind that Vidalia deletes the returned tab when the user
+closes it, that's why we create the tab every time buildGUI is
+executed..
+
+=== Settings handling ===
+
+For settings handling, Vidalia provides two methods that belong to the
+VidaliaTab class: saveSetting and getSetting. These methods will get
+and set values inside the plugin's section, in the case of the Tor
+Browser Bundle plugin, it will be the TBB section, as we described
+before.
+
+As an example, we will look at an extract of the saveSettings function
+from tbb.js:
+
+{{{
+this.tab.saveSetting(this.BrowserExecutable, this.lineExecutable.text);
+this.tab.saveSetting(this.BrowserDirectory, this.lineDirectory.text);
+}}}
+
+this.BrowserExecutable is a variable defined before, which is
+"BrowserExecutable". this.lineExecutable is a QLineEdit from the GUI.
+
+This first call, assuming the content of this.lineExecutable is
+"/some/path", will lead to the following text in vidalia.conf:
+
+{{{
+
+[TBB]
+BrowserExecutable=/some/path
+
+}}}
+
+Note that there might be other values before and after
+"BrowserExecutable".
+
+For retrieving settings from the plugins own section you can use the
+method getSetting, which receives the name of the setting and the
+default value as parameters.
+
+Here's an example from the buildGUI() method in tbb.js:
+
+{{{
+if(this.lineExecutable != null) {
+ this.lineExecutable.text = this.tab.getSetting(this.BrowserExecutable, "");
+}
+}}}
+
+=== I know Qt, how do I use signals and slots? ===
+
+Signals and slots are available inside the plugins environment. the
+only restriction is that you can't emit signals properly (there are
+some hacks to get around this though).
+
+As an example, lets look at a part of the start() function of tbb.js:
+
+{{{
+torControl["authenticated()"].connect(this, this.showWaitDialog);
+}}}
+
+The torControl variable is one that Vidalia provides to interact with
+the code that is able to talk to Tor inside Vidalia. This object emits
+a signal called "authenticated()" at some point.
+
+So the syntax to use for connecting a certain signal to a certain slot
+is the following:
+
+{{{
+<object that emits the signal>["<signal name>"].connect(<receiver>, <slot>);
+}}}
+
+It is not a complicated issue, but it may not be intuitive at first if
+you are familliar with Qt's way of doing this in C++.
+
+=== Simple example ===
+
+We have been through the most important parts of a plugin, you can
+always look at how the Tor Browser Bundle plugin works and learn from
+it, but if you are going to start a plugin from scratch, it may not be
+the best option since it a bit big. So in this section I will give you
+a really simple plugin that uses everything we've been talking about
+and it is still clear enough for you to have it as a quick reference
+for your own plugin.
+
+The plugin we are going to be implementing for this section is a
+simple bandwidth history recorder. This means that every certain
+amount of bytes tor sends or receives, it notifies the controllers
+through an event how many have been sent or received since the last
+time it notified.
+
+Looking at how Vidalia TorControl class works, we notice a signal
+called:
+
+{{{
+ /** Emitted when Tor sends a bandwidth usage update (roughly once every
+ * second). <b>bytesReceived</b> is the number of bytes read by Tor over
+ * the previous second and <b>bytesWritten</b> is the number of bytes
+ * sent over the same interval.
+ */
+ void bandwidthUpdate(quint64 bytesReceived, quint64 bytesSent);
+}}}
+
+(This is from src/vidalia/plugin/prototypes/TorControlPrototype.h)
+
+What we need to do is connect this signal with a slot that records
+these bytes, and we have half of our work done.
+
+Once we have that, we can create a simple GUI to display this as a tab
+inside Vidalia. For added fun, we added a button to reset the counter
+and the start date.
+
+Here's the complete Javascript source code for this:
+
+{{{
+importExtension("qt");
+importExtension("qt.core");
+importExtension("qt.gui");
+importExtension("qt.uitools");
+
+var tutorial = {
+ From: "From",
+ Sent: "Sent",
+ Recv: "Recv",
+
+ start: function() {
+ vdebug("Tutorial at start");
+ torControl["bandwidthUpdate(quint64, quint64)"].connect(this, this.saveBandwidth);
+ this.tab = new VidaliaTab("Display bandwidth history", "BandwidthHistory");
+
+ this.recv = parseInt(this.tab.getSetting(this.Sent, 0));
+ this.sent = parseInt(this.tab.getSetting(this.Recv, 0));
+ this.from = this.tab.getSetting(this.From, "");
+
+ if(this.from.length == 0)
+ this.from = QDateTime.currentDateTime().toString();
+
+ // null labels so that we don't update them until the GUI is created
+ this.lblFrom = null;
+ this.lblSent = null;
+ this.lblRecv = null;
+ },
+
+ saveBandwidth: function(recv, sent) {
+ vdebug("Tutorial::Recv", recv);
+ vdebug("Totorial::Sent", sent);
+
+ this.recv += recv;
+ this.sent += sent;
+ this.tab.saveSetting(this.Recv, this.recv);
+ this.tab.saveSetting(this.Sent, this.sent);
+ this.tab.saveSetting(this.From, this.from);
+
+ if(this.lblFrom)
+ this.lblFrom.text = this.from;
+ if(this.lblSent)
+ this.lblSent.text = this.sent.toString();
+ if(this.lblRecv)
+ this.lblRecv.text = this.recv.toString();
+ },
+
+ resetCounters: function() {
+ this.recv = 0;
+ this.sent = 0;
+ this.from = QDateTime.currentDateTime().toString();
+ this.saveBandwidth(0,0);
+ },
+
+ buildGUI: function() {
+ vdebug("Tutorial at buildGUI");
+ // Load the GUI file
+
+ if(this.tab)
+ delete this.tab;
+
+ this.tab = new VidaliaTab("Display bandwidth history", "BandwidthHistory");
+
+ var file = new QFile(pluginPath+"/tutorial/tutorial.ui");
+ var loader = new QUiLoader(this.tab);
+ file.open(QIODevice.ReadOnly);
+ this.widget = loader.load(file);
+ var layout = new QVBoxLayout();
+ layout.addWidget(this.widget, 0, Qt.AlignCenter);
+ this.tab.setLayout(layout);
+ file.close();
+
+ var grpBandwidth = this.widget.children()[findWidget(this.widget, "grpBandwidth")];
+ if(grpBandwidth == null)
+ return this.tab;
+
+ this.lblFrom = grpBandwidth.children()[findWidget(grpBandwidth, "lblFrom")];
+ if(this.lblFrom == null)
+ return this.tab;
+
+ this.lblSent = grpBandwidth.children()[findWidget(grpBandwidth, "lblSent")];
+ if(this.lblSent == null)
+ return this.tab;
+
+ this.lblRecv = grpBandwidth.children()[findWidget(grpBandwidth, "lblRecv")];
+ if(this.lblRecv == null)
+ return this.tab;
+
+ this.btnReset = grpBandwidth.children()[findWidget(grpBandwidth, "btnReset")];
+ if(this.btnReset == null)
+ return this.tab;
+
+ this.lblFrom.text = this.from;
+ this.lblSent.text = this.sent.toString();
+ this.lblRecv.text = this.recv.toString();
+
+ this.btnReset["clicked()"].connect(this, this.resetCounters);
+
+ return this.tab;
+ },
+
+ stop: function() {
+ vdebug("Tutorial at stop");
+ this.saveBandwidth(0,0);
+ },
+};
+}}}
+
+With just 105 lines we have a nice byte history recorder. You can find
+the tutorial.ui and info.xml files in our repository:
+
+https://gitweb.torproject.org/vidalia-plugins.git
+
+=== I made a plugin, what now? ===
+
+So you have your cool plugin with a crazy new idea and it works great,
+what is the next step? We have a repository for official plugins
+(official as in "some folk from Tor made it"), and you can give us
+your plugin to put it there and may be convince us why we should
+distribute your plugin along with the rest.
+
+The idea to have plugins though is to let people develop functionality
+in one of the main Tor controllers, and not depend on us to merge new
+stuff into the main line of distribution.
+
+In the (near) future, we will have a Vidalia Plugins web section where
+plugins that we think are good and won't compromise the users privacy
+will be listed.
+
+Right now the plugin distribution and installation is not something
+that any user might be able to do, so the other future plan is to
+create a distribution file format (probably just a zip file with
+certain internal structure) and a graphical way of installing plugins.
+
+If you are interested in helping with the infrastructure for plugins,
+please don't hesitate to join us on IRC at OFTC #vidalia.
+
+If you find any bugs, do not hesitate to report them in
+https://trac.torproject.org with Vidalia as the component.
More information about the tor-commits
mailing list