Implementing a Freenet plugin

Table of Contents

I wanted to try building my own Freenet plugin for years. Last year Thomas wrote the Practical Plugin Development Tutorial. This finally gave me what I needed: How to build a plugin with a simple web frontend. So it’s time to try.

You can get this repo with infocalypse using

hg clone freenet://USK@Sf3FBTeLwsLuK0o8u~gcFi9uFw~Dt6O718Qreixv43w,NE-xojH2srg4n7Up9bEAehR5WyP6ea-5S4rLwd6HwrQ,AQACAAE/freenet-plugin-bare.R1/0

Or from github.

If you want to join in, please clone the repo and just start writing. You likely have more free time for it that me, so just go for it!

Bare Need

You could ask, “why this site?”

Isn’t there lots of documentation on writing plugins? Well, actually not. And where it is, it is often misleading. The link points to a rant from the developer of Fritter. I agree with that rant.

That’s what this tutorial intends to change.

Bare Emacs

The tutorial from Thomas does not list imports and all that, so I assume that Thomas works in a fully preconfigured Eclipse. I just use a bare emacs. Also Thomas lists a working brain and sufficient Java knowledge as prerequisites. While my brain currently halfways works, I cannot really claim lots of Java skills.

So I will actually spell out what I needed to do.

Bare Bones

The bare bones plugin can only be started and shut down. Thomas says I need this:

package hello.world;

public class MyApplication extends FredPlugin {

    private final static Logger LOGGER = Logger.getLogger(MyApplication.class.getName());

    public void runPlugin(PluginRespirator pr)
    {}

    public void terminate()
    {}
}

Let’s look at an example plugin what to do with this…

There is plugin-HelloWorld. It puts the code into src/plugins/HelloWorld. But since we’re using package hello.world, I guess we have to put our code into src/plugins/hello/world.

It’s time to start committing.

The code above is now in src/plugins/hello/world. I changed the package to plugins.hello.world.

Now let’s add the build.xml. By just copying it from the HelloWorld plugin:

wget https://raw.githubusercontent.com/freenet/plugin-HelloWorld/master/build.xml

My freenet.jar is in a different place. I adjust the build.xml to use the freenet.jar from my freenet install:

sed -i s/location=\\\"..\\/fred\\/dist/location=\\\"..\\/freenet/ build.xml
sed -i s/location=\\\"..\\/fred\\/lib/location=\\\"..\\/freenet/ build.xml

Also I have to change all instances of HelloWorld in the build.xml by my names.

sed -i s/plugins\\/HelloWorld/plugins\\/hello\\/world/ build.xml
sed -i s/HelloWorld\\.jar/MyApplication\\.jar/ build.xml
sed -i s/plugins\\.HelloWorld\\.HelloWorld/plugins\\.hello\\.world\\.MyApplication/ build.xml

Now run ant.

ant
[javac] /home/arne/freenet-plugin-something/src/plugins/hello/MyApplication.java:3: error: cannot find symbol
[javac] public class MyApplication extends FredPlugin {

which fails - because I don’t have any imports yet. Let’s snarf these from the Plugin API and get some more from the WoT.

import freenet.pluginmanager.*;
import freenet.support.Logger;
// code here

and try again.

ant
[javac] /home/arne/freenet-plugin-something/src/plugins/hello/MyApplication.java:5: error: no interface expected here
[javac] public class MyApplication extends FredPlugin {

A quick look at the Web of Trust plugin (one of the few which avoids maven which as I learned does not allow checking whether dependencies are actually trustworthy) shows that it has to be implements, not extends.

Let’t try again.

ant
[javac] /home/arne/freenet-plugin-something/src/plugins/hello/MyApplication.java:8: error: cannot find symbol
[javac]     private final static Logger LOGGER = Logger.getLogger(MyApplication.class.getName());

Dafug, the logger doesn’t compile?

Just delete it.

ant
      [jar] Building jar: /home/arne/freenet-plugin-something/dist/MyApplication.jar

BUILD SUCCESSFUL

YAY!

Let’s add it to the running plugins and see whether it works.

The plugin loads, however it does not appear in the plugin list. This is because a plugin implementing FredPlugin is considered dead as soon as its run() method exits. To ensure our plugin appears in the list, we should either block in our run() method, or implement the convenient FredPluginThreadless interface. When the plugin is loaded, fred checks whether FredPluginThreadless is implemented or not, and treats them differently, most importantly, not unloading it as soon as our short run() method ends.

package hello.world;

public class MyApplication implements FredPlugin, FredPluginThreadless {

    private final static Logger LOGGER = Logger.getLogger(MyApplication.class.getName());

    public void runPlugin(PluginRespirator pr)
    {}

    public void terminate()
    {}
}

Note that we must still implement FredPlugin, FredPluginThreadless is simply an empty interface that indicates to Fred to treat our plugin differently. Let's also add some logging for further visibility of our plugin.

Let’s steal the Logger from the Web of Trust:

static {
    Logger.registerClass(MyApplication.class);
}

That compiles, but I still don’t see my plugin in the list.

So, let’s log an error on startup:

public void runPlugin(PluginRespirator pr)
{
    Logger.error(this, "FOOBAR MYAPPLICATION HELLO WORLD");
}

Then wait some time until the latest logs are saved, and… drumroll

(plugins.hello.world.MyApplication, pplugins.hello.world.MyApplication_1265709301, ERROR): FOOBAR MYAPPLICATION HELLO WORLD

Success! Our Bare Bones plugin works!

Here’s the full code:

package plugins.hello.world;

import freenet.pluginmanager.*;
import freenet.support.Logger;

public class MyApplication implements FredPlugin, FredPluginThreadless {
    PluginRespirator pr;

    static {
        Logger.registerClass(MyApplication.class);
    }

    public void runPlugin(PluginRespirator pr)
    {
        this.pr = pr;
        Logger.error(this, "FOOBAR MYAPPLICATION HELLO WORLD");
    }

    public void terminate()
    {}
}

Let’s give it a final facelift and call it BareBones

sed -i s/MyApplication/BareBones/ build.xml
hg cp src/plugins/hello/MyApplication.java src/plugins/hello/BareBones.java
sed -i s/MyApplication/BareBones/ src/plugins/hello/BareBones.java

And after that, just add a bones target which will always build Bare Bones.

Now we can go on to something which is actually useful ☺

Bare Skin

The next step for a useful plugin is a user interface. In the case of freenet this is ideally a web interface - integrated directly into the freenet web interface.

Let’s look again what Thomas says: Just add a method setupWebInterface() and run that in runPlugin().

import plugins.WebOfTrust.ui.web.WebInterface;
...
    public void runPlugin(PluginRespirator pr)
    {
        setupWebInterface();
    }
    public void terminate();
    {
        pr.getToadletContainer().unregister(this.oc);
    }
private void setupWebInterface()
{
    PluginContext pluginContext = new PluginContext(pr);
    this.webInterface = new WebInterface(pluginContext);

    pr.getPageMaker().addNavigationCategory(basePath + "/","WebOfTrust.menuName.name", "WebOfTrust.menuName.tooltip", this);
    ToadletContainer tc = pr.getToadletContainer();

    // pages
    this.oc = new Overview(this, pr.getHLSimpleClient(), basePath, db);

    // create fproxy menu items
    tc.register(oc, "WebOfTrust.menuName.name", basePath + "/", true, "WebOfTrust.mainPage", "WebOfTrust.mainPage.tooltip", WebOfTrust.allowFullAccessOnly, oc);
    tc.register(oc, null, basePath + "/", true, WebOfTrust.allowFullAccessOnly);

    // register other toadlets without link in menu but as first item to check
    // so it also works for paths which are included in the above menu links.
    // full access only will be checked inside the specific toadlet
    for(Toadlet curToad : newToadlets) {
        tc.register(curToad, null, curToad.path(), true, false);
    }

    // finally add toadlets which have been registered within the menu to our list
    newToadlets.add(oc);
}

So let’s see whether we can turn this into working code: Just a website which shows its skin. And maybe Hello World ☺.

The plugin now shows Hello World.

And I really need a Java-Setup for Emacs. Without something which can figure out imports and give me the API for every symbol in Freenet, it is close to impossible to work with this.

Also the plugin needs lots of cleaning up. I should have went the sane route from the start and chosen a sane, minimal plugin as base: The plugin-DVCS-WebUI. It shows how a Plugin should be created. Also I was mentor in the GSoC project (Infocalypse Web of Trust) during which Steve created it, so I really have no excuse for not using this really nice code in my bare tutorial.

Bare Java

To go forward, I really need better java integration. There are several options:

  • ECJ for Emacs provides support for using the eclipse compiler to drive flymake.
  • Emacs Eclim provides Eclipse editing features as server, so I can use them from Emacs without having to use the Eclipse User Interface.
  • CEDET provides project management and API docs and such using the Semantic project analyzer.

I prefer using the native Emacs version, so I’ll go with CEDET, but add a bit of flymake with ECJ, because that’s loosely coupled.

(require 'cedet)
(require 'semantic)
(load "semantic/loaddefs.el")
(require 'semantic/bovine/gcc)
(require 'semantic/java)
(add-hook 'java-mode-hook 
          (lambda () (add-to-list 'ac-sources 'ac-source-semantic)))
(add-to-list 'semantic-default-submodes 'global-semantic-decoration-mode)
(add-to-list 'semantic-default-submodes 'global-semantic-idle-completions-mode)
(add-to-list 'semantic-default-submodes 'global-semantic-idle-summary-mode)
(add-to-list 'semantic-default-submodes 'global-semantic-mru-bookmark-mode)
(semantic-mode t)
; projects
(require 'ede)
(global-ede-mode t)
(defvar freenet-plugin-bare-java-classpath '("/home/arne/freenet/freenet.jar" 
                    "/home/arne/freenet/freenet-ext.jar" "/home/arne/Quell/Programme/freenet/freenet.jar" 
                    "/home/arne/Quell/Programme/freenet/freenet-ext.jar"
                    "/home/arne/Quell/Programme/freenet-plugin-bare/src"
                    "/home/arne/Quell/Programme/freenet/fred-staging/src"))
(ede-project "Freenet-Bare")
; this will take some time: It will parse fred-staging.
(semantic-add-system-include "/home/arne/Quell/Programme/freenet/fred-staging/src" 'jde-mode)
(semantic-add-system-include "/home/arne/fred-staging/src" 'jde-mode)
; (ede-java-root-project "Bare"
;        :file "~/freenet-plugin-bare/build.xml"
;        :srcroot '("src")
;        ; :localclasspath '("/relative/path.jar")
;        :classpath freenet-plugin-bare-java-classpath)

; also I want flymake for fast error-detection. I use ECJ for that.
; flymake setup, see http://graflex.org/klotz/weblog/2008/02/java-error-highlighting-in-emacs.html
; and http://tkj.freeshell.org/emacs/java/
(require 'flymake)

(defvar flymake-my-java-classpath (mapconcat 'identity freenet-plugin-bare-java-classpath ":")) 

(defun flymake-java-ecj-init ()
  (let* ((temp-file (flymake-init-create-temp-buffer-copy
                     'flymake-ecj-create-temp-file))
         (local-file (file-relative-name
                      temp-file
                      (file-name-directory buffer-file-name))))
    (list "ecj" (list  "-Xemacs" "-d" "none" 
                       "-proceedOnError"
                       "-classpath" flymake-my-java-classpath
                       "-source" "1.7" "-target" "1.7"
                       "-sourcepath" "/home/arne/freenet-plugin-bare/src/:/home/arne/Quell/Programme/freenet-plugin-bare/src/"
                       local-file))))

(defun flymake-java-ecj-cleanup ()
  "Cleanup after `flymake-java-ecj-init' -- delete temp file and dirs."
  (flymake-safe-delete-file flymake-temp-source-file-name)
  (when flymake-temp-source-file-name
    (flymake-safe-delete-directory (file-name-directory flymake-temp-source-file-name))))

(defun flymake-ecj-create-temp-file (file-name prefix)
  "Create the file FILE-NAME in a unique directory in the temp directory."
  (file-truename (expand-file-name (file-name-nondirectory file-name)
                                   (expand-file-name  (int-to-string (abs (random))) (flymake-get-temp-dir)))))


;   (push '(".+\\.java$" flymake-simple-ant-java-init
;           flymake-simple-java-cleanup) flymake-allowed-file-name-masks)
(push '(".+\\.java$" flymake-java-ecj-init
        flymake-java-ecj-cleanup) flymake-allowed-file-name-masks)

(push '("\\(.*?\\):\\([0-9]+\\): error: \\(.*?\\)\n" 1 2 nil 2 3
        (6 compilation-error-face)) compilation-error-regexp-alist)

(push '("\\(.*?\\):\\([0-9]+\\): warning: \\(.*?\\)\n" 1 2 nil 1 3
        (6 compilation-warning-face)) compilation-error-regexp-alist)

(defun credmp/flymake-display-err-minibuf () 
  "Displays the error/warning for the current line in the minibuffer"
  (interactive)
  (let* ((line-no             (flymake-current-line-no))
         (line-err-info-list  (nth 0 (flymake-find-err-info flymake-err-info line-no)))
         (count               (length line-err-info-list)))
    (while (> count 0)
      (when line-err-info-list
        (let* ((file       (flymake-ler-file (nth (1- count) line-err-info-list)))
               (full-file  (flymake-ler-full-file (nth (1- count) line-err-info-list)))
               (text (flymake-ler-text (nth (1- count) line-err-info-list)))
               (line       (flymake-ler-line (nth (1- count) line-err-info-list))))
          (message "[%s] %s" line text)))
      (setq count (1- count)))))

Bare Face

  • Include the menu: Make the web interface integrate with fproxy.

Bare Memory

  • Use pluginstorage for safe persistent data

Bare Words

  • Include Jython

Bare Handed

  • to be thought about :)

Bare Dance

  • something fun

Bare Back

  • jython-interpreter

Bare Chest

  • FCP API to jython

Author: Arne Babenhauserheide

Created: 2016-07-26 Di 09:00

Emacs 24.5.1 (Org mode 8.2.6)

Validate