Plugins provide a way to extend the KOS environment, using programs that have full access to the system and its resources. A plugin behaves much like a Knowbot Program -- it can create services and communicate with other processes. However, there are two key difference between plugins and KPs. First, plugins do not run in the restricted execution environment. Second, plugins are stationary programs installed by the KOS administrator.
Plugins are one of two ways to mediate a KP's access to system resources. The KP's restricted execution (rexec) environment can also be extended to allow in-process access to resources. The extension mechanism, called rexec proxies, is not documented at this time.
There are other differences between plugins and KPs as well. Once started, plugins run as long as the kernel, and they typically are started by the KOS kernel at boot time. Also, the KOS services object used by plugins lacks the mobility services, like migration and cloning, that are used by KPs.
The primary utility of a plugin is its unrestricted environment. While a KP may not be able to open a socket, a plugin can open a socket on behalf of a KP. Plugins can provide controlled access to any system resource -- persistent storage or the server's console window, for example.
Plugins also encapsulate administrative concerns, because plugins must be manually downloaded and configured by a system administrator. Because plugins guard access to system resources, they allow the administrator fine-grained control over those resources. Plugins can be installed only when they are needed by user programs and when they satisfy a site's security policy.
This tutorial describes how to write and run plugin programs. It
describes three plugins that are distributed with the Knowbot
system, covering the basics of plugin programming as well as some
important idioms and nuances.
Running Plugins
Before we actually look at the source code of a plugin, let's look at two ways to configure plugins -- so they are started automatically, by the kernel, or manually.
When a kernel starts, it checks the plugin configuration files to see which plugins to run. We will review the basics here.
The plugin setup files contain entries, like the one in Example 1, for each plugin to be started. The entry begins with the name of the plugin in square brackets, followed by one or more configuration variables, one per line. The configuration variables are entered as the variable name, a colon, and the variable's value.
In Example 1, the plugin is
named "bitbucket", it's source code is in the file
bitbucket.py, and
it should be run at boot time. When the kernel starts, this entry will
cause it to fork a process into the plugins/bitbucket
directory and then execute the command python
bitbucket.py -n localhost
, where localhost is
the name of the
kernel. Plugins use the -n command line argument to
discover the name of the kernel that started them and register with
it.
Example 1: plugin setup file entry[bitbucket] file: bitbucket.py run-at-boot: 1
Instead of having a plugin started by the kernel, you can start it manually. First cd to the plugin's directory. Then run the plugin's main module. If you don't specify a kernel name with the -n argument, the plugin will look for a kernel with the same name as the current hostname.
Let's look at the implementation of a very simple plugin. BitBucket provides a storage utility shared across the entire kernel. It stores named raw strings that can be created, read, and deleted by any process. The BitBucket has a fixed size and will not store new objects if it is full. KPs might use BitBucket to store data that they will only use at one site or to share data among several KPs without creating a specific interface for sharing. The complete source code for BitBucket is here.
Every plugin provides one or more services with an interface specified in the koe/interfaces directory. BitBucket provides one service, BitBucketAPI.BitBucket, shown in Example 2.
Example 2: BitBucketAPI.BitBucket interface specificationTYPE BitBucket = OBJECT METHODS Set(name: ilu.CString, data: BinaryString) RAISES BucketFull END, Get(name: ilu.CString): BinaryString RAISES NotFound END, Delete(name: ilu.CString) RAISES NotFound END, List(): NameList END;
The first step in plugin development is to define the interfaces the plugin will use to offer services. Any interface that will be used by KPs or plugins must be defined by an ISL file in koe/interfaces and added to the Makefile in koe/interfaces/stubs. We'll assume that you are familiar with creating ISL files. It is it is described fully by the ILU documentation in the section Defining Interfaces.
The complete BitBucket interface describes an interface named BitBucketAPI that includes an object type named BitBucket. It's common to use basically the same name for the plugin, its interface, and the object type in the interface that provides the service. We have developed a convention of appending API to the ISL file name and to the INTERFACE name in the interface declaration, to distinguish it from the object type and its implementation.
In this section, we'll walk through the source code of bitbucket.py line-by-line.
The code begins like a typical Python program -- with several
import
statements. The koe package contains
code shared by several KOE components, including stub and skeleton
code generated from ILU interface files.
Bitbucket's main() has two purposes. First, it registers the plugin, giving the plugin access to KOS services and its namespace. Second, it creates an instance of the BitBucket service, registers the service with the kernel, and enters its mainloop.
Example 3: bitbucket.py's main()def main(): kos, ns = pluginlib.register('bitbucket') sid = kos.bind_service('bitbucket', BitBucket(102400), 'BitBucketAPI.BitBucket') try: print "bitbucket plugin registered", sid kos.run() finally: print "bitbucket plugin exiting" kos.unbind_service(sid)
The pluginlib.register() call registers the plugin process with the kernel, establishes the plugin's namespace, and returns two objects -- the KOS object and the plugin's namespace context. The argument passed to register is a name for the plugin, which must be unique among all plugins at a KOS.
The plugin's KOS object has many of the same methods as the KOS object passed to KPs. Methods for migration, cloning, and the like are omitted because they aren't relevant for stationary plugin processes. The plugin uses bind_service() and related operations in exactly the same way that a KP does.
In the example, the plugin creates an instance of the BitBucket class, initialized to store 100K of data, and binds it with the name 'bitbucket'. It doesn't do any error handling, so if another service with the name 'bitbucket' already exists an uncaught exception will be raised and the plugin will fail.
Once the service is instantiated and bound, the plugin calls kos.run() which starts the ILU mainloop and allows clients to use the bitbucket service. The call to kos.run() is wrapped in a try/finally so that the process can unbind its service when it is terminated. The call starts an interruptible ILU mainloop, which will raise an exception when it is terminated.
The implementation of the BitBucket service itself is
straightforward. It is a normal Python class that implements the
interface specified by the BitBucketAPI.BitBucket type. Internally it
use a dictionary (self.entries
) to store the strings, indexed by
name. It separately tracks the total size of the strings
(self.cur_size
).
The use of ILU client and server stubs is a little unusual in the
KOS, and that difference affects how plugins are written. The
bitbucket plugin imports the client stub code 'from
koe.interfaces.stubs import BitBucketAPI',
but not the server skeleton code, even though it implements a server
for the interface. The skeleton isn't needed because the
bind_service()
call will automatically combine the class instance with the stub
code. Interface-specific exceptions are defined in the client code, so
it needs to be imported if the plugin is going to raise those
exceptions. Later on we'll see an example where the skeleton is needed
-- when a new object is the return value of a method call.
Multiplexing resources
A plugin that allows multiple KPs access to a system resource faces a dilemma: It must multiplex access to that resource so that a single program cannot tie up the resource indefinitely. Otherwise the plugin would handle a request by one KP and defer all other requests until the first one was completed. (A multi-threaded plugin would address these problems; however, we haven't used threaded versions of ILU or Python with the current system.)
The http and simpleio plugins multiplex access to resources with a factory interface which generates a new process for each requesting program. The factory doesn't perform http requests or interact with the window system itself; each child process performs the service for a specific client.
The http plugin, for example, uses an interface, HTTPAPI.Factory, that has a single method GetHTTPRequester(). A KP that wants to perform an http request must get an instance of an http requester, which provides the methods for loading URLs.
The interface for the plugin programmer is more complicated in this case because a separate process must be forked for each factory request, and a reference to the newly forked service must be passed back to the requester via the factory process. The pluginlib library supports this behavior with the spawnserver() function.
The plugin needs to be divided into two parts -- the factory part of the plugin, which looks like a normal plugin and is started by the kernel, and the child part, which is started by pluginlib.spawnserver(). It is simplest to separate these parts into two files.
On the factory side, spawnserver() runs the child program and returns the SBH for the new service to the caller, along with some process management information (which we'll ignore for the moment). The factory creates an ILU surrogate for the new service, using ilu.ObjectOfSBH(), and returns the surrogate to the requesting program.
The factory process and the child process share a pipe that the child uses to pass the SBH of the new service back to the factory. A typical child process starts up, creates an instance of the service type, and writes the service instance's SBH to the pipe. Then the child process enters its ILU mainloop and handles service requests.
The http plugin performs Web access on behalf of client programs. It is based on Python's urllib, which loads several kinds of URLs including http, ftp, and gopher. Because URL requests can take a long time to complete and the urllib interface is blocking, it uses a factory interface to give each clients its own URL requester.
The factory interface is shown in Example 4. (You can also look at the entire HTTPAPI ISL specification file.) The Requester return type is an object implemented by a new child process of the plugin.
Example 4: HTTPAPI.Factory type specificationTYPE Factory = OBJECT METHODS GetHTTPRequester(): Requester RAISES FactoryFailed END "Gets a protocol access object" END;
The http plugin's main() is all boilerplate code. It calls pluginlib.register() and then binds an 'HTTPAPI.Factory' service implemented by the Factory class.
The Factory class, shown in Example 5, is more interesting. It uses pluginlib.spawnserver() to create a new process that runs child.py. The spawnserver() call returns a 3-tuple containing the SBH of the spawned requester service, the forked process's id, and a list of child processes that have exited recently. (This plugin ignores the pid and pids return values; they are provided so that a plugin could perform some of its own process management.)
Example 5: Implementation of HTTAPI.Factoryclass Factory: def GetHTTPRequester(self): (sbh, pid, pids) = pluginlib.spawnserver("child.py") return ilu.ObjectOfSBH(HTTPAPI.Requester, sbh)
ObjectOfSBH() produces an ILU surrogate for the new service, using the sbh. Here again we see that the ILU skeleton code isn't needed, because the Factory is acting as an ILU client and creating a surrogate; the skeletons will be necessary in child.py.
Most of the action happens in child.py. It implements an ILU true server for the HTTPAPI.Requester object. This implementation is largely independent of the KOS, because the requester implementation does not use the connector interface and is not published through the KOS namespace. The child ends up being more complicated than other plugins, because it must perform operations that normally occur inside the KOS. In particular, the Requester class must inherit from the ILU skeleton and it must explicitly manage its ILU mainloop.
Some internals of the abstract interface we saw in the Factory code above are exposed on the child side. spawnserver() creates a pipe between the parent and child process, which is used to pass the SBH from child back to parent. The pipe is passed to child.py as an integer file descriptor included in child.py's command line arguments.
Example 6: A child processes main() methoddef main(): fd = -1 opts, args = getopt.getopt(sys.argv[1:], 'f:') for opt, val in opts: if opt == '-f': fd = string.atoi(val) req = Requester() sbh = req.IluSBH() try: fp = os.fdopen(fd, 'w') fp.write(sbh) fp.close() except IOError: print "failed to start" sys.exit(1) ilutools.RunMainLoop() sys.exit(1)
The first four lines of main() (Example 6)
parse sys.argv
to
extract the pipe's file descriptor. The next two lines instantiate the
ILU true object and gets its SBH. The rest of the code opens the pipe
from its file descriptor and writes the SBH across the pipe. Then it
starts the ILU mainloop which makes the Requester available for use by
the client.
The Requester class does more work than the BitBucket class in the previous example. First, it inherits from HTTPAPI__skel.Requester so that it is a proper true object. Second, the interface includes an explicit Close() call that exits the ILU mainloop. If the client doesn't call this method, the child process will continue in its mainloop indefinitely, but it will be inaccessible to the rest of the system. The actual implementation of the Requester is fairly complex; it performs error checking, provides options for viewing http response headers, and performs some security checks. This tutorial presents a simplified implementation, shown in Example 7. (The complete implementation is here.)
Example 7: A simplified version of the HTTP Requester implementationclass MyFileWrapper(FileImpl.FileWrapper): def __init__(self, fp): self._fp = fp def open(self, mode): pass class Requester(HTTPAPI__skel.Requester): def __init__(self): self.http = MyURLopener() def Get(self, url): try: fp = self.http.open(url) except IOError, msg: raise FileAPI.FileIOError, str(msg), sys.exc_traceback return MyFileWrapper(fp) def Close(self): ilutools.ExitMainLoop()
The Requester.Get() method uses MyFileWrapper to return the http response to its client. The object is returned across a remote procedure call; the MyFileWrapper instance is an ILU true object and the client receives a surrogate for the object. The wrapper implements the FileAPI.File interface to provide a file-like interface between an ILU server and its clients. This handy idiom is used in many places within the Knowbot system. FileAPI is one of the standard interfaces in koe/interfaces. It implements Python's "file-like object" interface, which allows user-defined class instances to behave like builtin file objects. The FileAPI.File object supports all the standard methods (read(), write(), tell(), writelines(), etc.) with the exception of open(). The File.open() method takes only a single argument, the file's mode.
In the Requester example,
MyFileWrapper subclasses the standard FileImpl.FileWrapper class, which
implements the b>FileAPI
interface on top of a file-like object. The standard FileWrapper
passes all method calls through to the instance variable
self._fp
. MyFileWrapper's constructor takes an
argument -- a file-like object containing the http response -- and assigns its
self._fp
. The open method doesn't need to do anything, because
the file containing the http response is already open.
Odds and Ends
The simpleio plugin allows a KP to open a talk window on the service station console window. It uses Python's Tkinter module to implement the user interface. Like the http plugin, simpleio uses a factory interface to give each KP its own simpleio window. The only architectural difference between the two is the use of Tk in simpleio.
The child process of the simpleio plugin is implemented by tkstdio.py. As in the previous example, we'll only look at a simplified version of tkstdio.py which illustrates the use of Tk without delving into all the details of the application.
The key difference between a normal plugin and one that uses Tk is use by the latter of a special mainloop which allows ILU and Tk to co-exist. Example 8 shows the basic structure of tkstdio.py. An instance of the TkStdio class creates a Tk window and runs the ilu_tk mainloop. The main() creates the class instance, passes the instance's SBH to the factory process, and calls TkStdio.go().
Example 8: Using Tk in a pluginfrom Tkinter import * from koe.common import tktools import ilu_tk from koe.interfaces.stubs import FileAPI, FileAPI__skel class TkStdio(FileAPI__skel.File): def __init__(self, height=None, width=None): self._master = None self._stayinalive = None self._height = height or self.DEFAULTHEIGHT self._width = width or self.DEFAULTWIDTH self._create_widgets() def go(self): if not self._stayinalive: self._keep_alive() self._stayinalive = 1 self._inp.focus_set() ilu_tk.RunMainLoop() def stop(self): self._master.quit() def _quit(self): self.stop() def _create_widgets(self): self._master = Tk(className='KOEStdio') self._master.title('Tk Stdio') self._master.iconname('Tk Stdio') self._master.protocol('WM_DELETE_WINDOW', self._quit) # Rest of the Tkinter details removed for brevity def _keep_alive(self): # Exercise the Python interpreter regularly so keyboard # interrupts get through try: self._master.tk.createtimerhandler(KEEPALIVE_TIMER, self._keep_alive) except KeyboardInterrupt: sys.exit(0)
What is the difference by a KP and a plugin? Two important differences are a plugin's access to system resources and a KP's mobility. It's possible to imagine the distinction blurring in the future; some class of programs, such as programs run by authenticated users, may have greater access to system resources and still be mobile.
There is also a cosmetic difference between typical KPs and the plugins we've described so far. The KP is organized as a class with a __main__() method that receives the KOS bastion object when it arrives at a new service station, while the plugin is organized as a script with a main() method.
class Network: def __main__(self, kos, ns=None): self.kos = kos if ns: self.ns = ns # we're a plugin else: self.ns = kos.get_namespace() # we're a KP self.service = self.kos.bind_service('foo', self, 'api.bar') def main(): p = Network() apply(p.__main__, pluginlib.register('foo'))Example 9: Using the same class for plugins and KPs
The network plugin, however, is organized like a KP. The implementation, sketched in Example 9, is an alternative to the style of other plugins. The __main__() method of the Network class can be used as either a KP or a plugin; it allows you to easily switch between the two uses of the code as the security policy of a service station dictates.