Service Developer's Guide

Copyright: 2010 University of Southampton, IT Innovation Centre

Overview

This guide is for people wishing to write services that can be managed directly by the SERSCIS governance components.

The steps are:

  • Write the functional service
  • Write a plugin for the client to access it
  • Add support for management

Note

This guide is a work-in-progress.

Writing the service

In this tutorial, we will write the service in E and use the CapTP network protocol. It is also possible to write services in other languages and using other protocols, with suitable bridges, but this is the simplest.

A basic service

Here is a complete E program that exposes a simple service over the network:

# Where to save the service endpoint's address
def serviceCap := <file:service.cap>

# Define a simple E object with one operation
def echoService {
	to echo(msg :String) {
		traceln(`echoing: $msg`)
		return msg
	}
}

# Generate a key-pair and go on-line
introducer.onTheAir()

# Create an endpoint for the service and format it as a string
def sturdyRef := makeSturdyRef.temp(echoService)
serviceCap.setText(introducer.sturdyToURI(sturdyRef))
println(`service address written to $serviceCap`)

# Wait for requests
interp.blockAtTop()

[ View Source ]

This service does not use any SERSCIS-specific code.

This program exposes E object which provides an "echo" operation. This takes a single argument, of type String, writes a trace message to the log, and then returns the input argument back to the caller.

The service can be run like this:

$ rune echoService1.e
service address written to <file:/.../service.cap>

Here is a simple client for the service, again using plain E:

Generate a key-pair for the client:

  ? introducer.onTheAir()

Parse the address of the echo service (copy and paste from
the server's output) and generate a proxy for it:

  ? def sr := introducer.sturdyFromURI(<file:service.cap>.getText())
  ? def echoService := sr.getRcvr()

Send the "echo" request message to the remote service, getting a
promise for the result:

  ? def result := echoService<-echo("Hello")
  # value: <Promise>

Wait for the response to arrive and print it:

  ? interp.waitAtTop(result)
  ? result
  # value: "Hello"

[ View Source ]

This client has been written as a test ("updoc") script. Lines starting with "?" are code to be executed. Lines starting "#" are the expected results. Everything else is documentation.

You can run the tests like this (while the service is still running, of course):

$ rune client.updoc
All tests passed.

Making the service persistent

So far, the service details aren't stored to disk. Each time the service is started it generates a new port number, key-pair and object endpoint. SERSCIS provides a persistence framework that stores this information in an SQL database. Here is our echoService extended to use the persistence system:

# Where to save the service endpoint's address
def serviceCap := <file:service.cap>

# A constructor for echo services
def makeEchoService(persistNode) {
	return def echoService {
		to echo(msg :String) {
			traceln(`echoing: $msg`)
			return msg
		}
	}
}

# Initialise the persistence system, storing the state in an HSQL
# database on the filesystem
def makePersistence := <gria:persisterSetup>
def driver := <unsafe:org.hsqldb.makejdbcDriver>()
def connection := driver.connect("jdbc:hsqldb:file:echoService", null)
def sql__quasiParser := <import:org.erights.e.tools.database.makeSQLQuasiParser>(connection)
def persistence := makePersistence(privilegedScope, sql__quasiParser, makeEchoService)

# Create an endpoint for the service and format it as a string
def sturdyRef := persistence.getRootNode().getSturdyRef()
serviceCap.setText(introducer.sturdyToURI(sturdyRef))
println(`service address written to $serviceCap`)

# Wait for requests
interp.blockAtTop()

[ View Source ]

Each time this code it run, it will display the same address. The network settings, key-pair and endpoint are all persisted to the disk.

[ todo: explain how to get the <persistence> library ]

No changes are needed to the client.

Management properties

We'll add a second operation to our service, allowing it to be enabled or disabled:

# A constructor for echo services
def makeEchoService(persistNode) {
	def setSuspended(suspended :boolean) {
		persistNode.put("suspended", suspended)
		persistNode.log(`suspended state set to $suspended`)
	}

	return def echoService {
		to echo(msg :String) {
			if (persistNode.get("suspended", false)) {
				throw("service is suspended!")
			}

			traceln(`echoing: $msg`)
			return msg
		}

		to setProperty(name :String, value :any) {
			switch (name) {
				match =="suspended" { setSuspended(value) }
				match _ { throw(`unknown property: $name`) }
			}
		}
	}
}

[ View Source ]

The persistence system maintains a tree of objects, each represented by a persistNode. Using a plug-in system, various per-node "features" can be added and made available to objects. We have already seen one feature, the "sturdy" feature, which allows each object to expose itself on the network. This code uses two more features:

  • The "state" feature provides persistNode.get(name, default) and persistNode.put(name, value) to read and write arbitrary key-value pairs for this object.
  • The "log" feature provides per-object logging, which we use by calling persistNode.log.

Finally, note the setSuspended function, which is defined directly inside the constructor and not as a method. This is E's equivalent to a private method in some other programming languages.

Here is a test-script for the upgraded service:

  ? introducer.onTheAir()
  ? def sr := introducer.sturdyFromURI(<file:service.cap>.getText())
  ? def echoService := sr.getRcvr()

Suspend the service:

  ? echoService<-setProperty("suspended", true)

Try using the service:

  ? def result := echoService<-echo("Hello")
  ? interp.waitAtTop(result)
  ? result
  # value: <ref broken by problem: service is suspended!>

Unsuspend:

  ? echoService<-setProperty("suspended", false)

Try again:

  ? def result := echoService<-echo("Hello")
  ? interp.waitAtTop(result)
  ? result
  # value: "Hello"

[ View Source ]

Note that we don't need to wait for the response to setProperty before calling echo because the CapTP protocol ensures messages to the same object are delivered in the order in which they were sent. However, a robust program might wish to check for an error being returned.

Creating a client plugin

[ todo: explain how to start the workbench GUI ]

The test scripts are useful for writing automated tests, but it's also useful sometimes to have an interactive user interface. We will now modify the GUI test client to support our echo service.

screenshots/echoPlugin.png

Create a new directory in the GUI, called src/main/e/plugins/echo and save the code below inside it as echoPlugin.emaker:

# Import some useful user interface objects...

def <swt> := <import:org.eclipse.swt.*>
def <widget> := <swt:widgets.*>
def SWT := <swt:makeSWT>
def <swttools> := <import:org.erights.e.ui.swt.*>
def swtGrid__quasiParser := <swttools:swtGridQParserMaker>()

def ECHO_SERVICE_URI := "urn:echoService"

# Create the plugin itself. <powerbox> contains any authority we were given by the configuration in guiPlugins.emaker
def makeAirportUserPlugin(<powerbox>) {

	# There is one of these for each remote echo service the user has added
	def makeEchoServiceHandler(treeItem, echoServiceSR) {
		return def echoServiceHandler extends treeItem.makeHandler(echoServiceHandler, echoServiceSR) {

			# Build the panel for invoking the service
			to showDetails(panel, tools) {
				def inputMessage := <widget:makeText>(panel, SWT.getBORDER())
				inputMessage.setText("Hello, World!")

				def invokeButton := <widget:makeButton>(panel, SWT.getPUSH())
				invokeButton.setText("Invoke")

				def outputMessage := <widget:makeLabel>(panel, 0)
				outputMessage.setText("(click Invoke to start)")

				invokeButton.addSelectionListener(def _ {
					to widgetSelected(ev) {
						outputMessage.setText("(invoking...)")
						def reply := super.getProxy()<-echo(inputMessage.getText())
						when (reply) -> {
							outputMessage.setText(`Response: $reply`)
						} catch ex {
							outputMessage.setText(`Error: $ex`)
							traceln(`$ex: ${ex.eStack()}`)
						}
					}
				})


				swtGrid`$panel: $inputMessage.X $invokeButton
						$outputMessage.X`
			}

			# Called when the user exports the service (or an authorisation for it)
			to exportMetadata() {
				return [ "type" => ECHO_SERVICE_URI ]
			}
		}
	}

	# There is one of these for each instance of the plugin. Each one can contain a number of remote
	# services.
	def makeEchoPluginHandler(treeItem) {
		def persistNode := treeItem.getPersistNode()

		persistNode.setBuilder(def _ {
			to loadEchoServiceHandler(childNode, [sturdyRef]) {
				return treeItem.createChild(makeEchoServiceHandler, childNode, [sturdyRef])
			}
		})

		return def echoPluginHandler extends treeItem.makeHandler(echoPluginHandler, null) {
			# Displays a helpful message when the user selects the plugin instance
			to showDetails(panel, tools) {
				def label := <widget:makeLabel>(panel, SWT.getWRAP())
				label.setText("Import an echo service...")
				swtGrid`$panel: $label.X.Y`
			}

			# Called when the user drags a service to us (or imports from a file)
			to acceptDrop(sturdyRef, metadata) {
				def type := metadata["type"]

				require(type == ECHO_SERVICE_URI, `Type $type is not $ECHO_SERVICE_URI!`)

				def label := metadata["label"]

				super.createChild("loadEchoServiceHandler", [sturdyRef], [ => label ])
			}
		}
	}

	return def echoPlugin {
		to load(treeItem) {
			return makeEchoPluginHandler(treeItem)
		}
	}
}

[ View Source ]

This code is a bit complicated and we won't go into details here, but the general idea is that each item in the GUI's tree has a corresponding handler object. The handler object renders the details panel when the item is selected and responds to drag-and-drop events, etc. Our plugin creates two types of item in the tree: a top-level item representing one instance of the plugin and a number of remote services under it.

To enable the plugin, you need to add it to src/main/e/plugins/guiPlugins.emaker. Add this line to the table at the end.

	"Echo"			=> loadPlugin("echo",		defaultScope,	guiTools),

[ View Source ]

This causes the GUI to display an "Echo" button in the plugins palette. When clicked, it loads echo/echoPlugin.emaker in the default scope (which includes a couple of useful libraries) and passes it some useful GUI tools as its <powerbox> argument. Note that, by default, plugins run in a restricted environment. They do not have access to the local filesystem, for example.

Finally, we need to change the service slightly to write out the .cap file in a different format. Instead of write out a file containing only the address, we will export a JSON document containing the address and some metadata: the service's type and a default label for it in the GUI. Change the end of the service to this:

# Create an endpoint for the service and format it as a JSON document
def sturdyRef := persistence.getRootNode().getSturdyRef()
def capDoc := [introducer.sturdyToURI(sturdyRef),
		["type" => "echoService",
		 "label" => "Sample echo service"]]
def jsonSurgeon := <elib:serial.deJSONKit>.makeSurgeon()
serviceCap.setText(jsonSurgeon.serialize(capDoc))
println(`service details written to $serviceCap`)

[ View Source ]

You should now be able to test the service from the GUI:

  1. Start the service running, as before.
  2. Start the GUI.
  3. Click on the Echo button to create a new instance of the Echo plugin.
  4. Right-click on the "Echo" tree item and import the service.cap file.
  5. Select the Sample echo service tree item.
  6. Send a message.

You will also find that you can view the service logs using the Logs tab. This should contain a few management events from our earlier testing (logged by the persistNode.log calls).

Role-based access control

If you try to use the GUI's Access tab to create a new authorisation, you'll get the error NoSuchMethodException: <an echoService>.getRoleInfo/0. To enable this, we need to define a getRoleInfo method and get it to return a set of roles and information about them:

	return def echoService {
		to getRoleInfo() {
			return ["owner" => [
					"description" => "Use the service to echo messages.",
					"methods" => ["echo"]],
				"admin" => [
					"description" => "Manage the service",
					"methods" => ["setProperty"]]]
		}

		to echo(msg :String) {
			if (persistNode.get("suspended", false)) {
				throw("service is suspended!")
			}

			traceln(`echoing: $msg`)
			return msg
		}

[ View Source ]

After restarting the service, you will be able to create named authorisations. For example, if you had a customer for the service ("Bob") then you could create an authorisation called "Bob" with the "owner" role. Bob would be able to invoke the echo operation, while only you would be able to suspend the service (using setProperty). If you wanted to give your deputy the power to suspend the service but not to echo messages, you could create an authorisation for them with the "admin" role.