diff --git a/README.md b/README.md index 2adbac51..5a8c4bab 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,25 @@ Go bindings to systemd socket activation, journal and D-BUS APIs. See an example in `examples/activation/httpserver.go`. For easy debugging use `/usr/lib/systemd/systemd-activate` + +## D-Bus + +The D-Bus API lets you start, stop and introspect systemd units. The API docs are here: + +http://godoc.org/github.com/coreos/go-systemd/dbus + +### Debugging + +Create `/etc/dbus-1/system-local.conf` that looks like this: + +``` + + + + + + + +``` diff --git a/dbus/dbus.go b/dbus/dbus.go index 9a04a813..2d5ec57f 100644 --- a/dbus/dbus.go +++ b/dbus/dbus.go @@ -20,6 +20,7 @@ func ObjectPath(path string) dbus.ObjectPath { return dbus.ObjectPath(path) } +// Conn is a connection to systemds dbus endpoint. type Conn struct { sysconn *dbus.Conn sysobj *dbus.Object @@ -37,6 +38,7 @@ type Conn struct { dispatch map[string]func(dbus.Signal) } +// New() establishes a connection to the system bus and authenticates. func New() (*Conn, error) { c := new(Conn) @@ -69,7 +71,11 @@ func (c *Conn) initConnection() error { c.sysobj = c.sysconn.Object("org.freedesktop.systemd1", dbus.ObjectPath("/org/freedesktop/systemd1")) + // Setup the listeners on jobs so that we can get completions + c.sysconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, + "type='signal', interface='org.freedesktop.systemd1.Manager', member='JobRemoved'") + c.initSubscription() + c.initDispatch() + return nil } - - diff --git a/dbus/dbus_test.go b/dbus/dbus_test.go index 76a14f0f..7907f706 100644 --- a/dbus/dbus_test.go +++ b/dbus/dbus_test.go @@ -4,6 +4,7 @@ import ( "testing" ) +// TestObjectPath ensures path encoding of the systemd rules works. func TestObjectPath(t *testing.T) { input := "/silly-path/to@a/unit..service" output := ObjectPath(input) @@ -13,3 +14,12 @@ func TestObjectPath(t *testing.T) { t.Fatalf("Output '%s' did not match expected '%s'", output, expected) } } + +// TestNew ensures that New() works without errors. +func TestNew(t *testing.T) { + _, err := New() + + if err != nil { + t.Fatal(err) + } +} diff --git a/dbus/methods.go b/dbus/methods.go index c9db48d1..22aaca17 100644 --- a/dbus/methods.go +++ b/dbus/methods.go @@ -137,7 +137,7 @@ func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) { } out := make(map[string]interface{}, len(props)) - for k, v := range(props) { + for k, v := range props { out[k] = v.Value() } diff --git a/dbus/methods_test.go b/dbus/methods_test.go index a5c37101..b1ed801e 100644 --- a/dbus/methods_test.go +++ b/dbus/methods_test.go @@ -1,17 +1,109 @@ package dbus import ( + "os" + "path/filepath" "testing" ) -// TestActivation forks out a copy of activation.go example and reads back two -// strings from the pipes that are passed in. -func TestGetUnitProperties(t *testing.T) { +func setupConn(t *testing.T) *Conn { conn, err := New() if err != nil { t.Fatal(err) } - //defer conn.Close() + + return conn +} + +func setupUnit(target string, conn *Conn, t *testing.T) { + // Blindly stop the unit in case it is running + conn.StopUnit(target, "replace") + + // Blindly remove the symlink in case it exists + targetRun := filepath.Join("/run/systemd/system/", target) + err := os.Remove(targetRun) + + // 1. Enable the unit + abs, err := filepath.Abs("../fixtures/" + target) + if err != nil { + t.Fatal(err) + } + + fixture := []string{abs} + + install, changes, err := conn.EnableUnitFiles(fixture, true, true) + + if install != false { + t.Fatal("Install was true") + } + + if len(changes) < 1 { + t.Fatal("Expected one change, got %v", changes) + } + + if changes[0].Filename != targetRun { + t.Fatal("Unexpected target filename") + } +} + +// Ensure that basic unit starting and stopping works. +func TestStartStopUnit(t *testing.T) { + target := "start-stop.service" + conn := setupConn(t) + + setupUnit(target, conn, t) + + // 2. Start the unit + job, err := conn.StartUnit(target, "replace") + if err != nil { + t.Fatal(err) + } + + if job != "done" { + t.Fatal("Job is not done, %v", job) + } + + units, err := conn.ListUnits() + + var unit *UnitStatus + for _, u := range units { + if u.Name == target { + unit = &u + } + } + + if unit == nil { + t.Fatalf("Test unit not found in list") + } + + if unit.ActiveState != "active" { + t.Fatalf("Test unit not active") + } + + // 3. Stop the unit + job, err = conn.StopUnit(target, "replace") + if err != nil { + t.Fatal(err) + } + + units, err = conn.ListUnits() + + unit = nil + for _, u := range units { + if u.Name == target { + unit = &u + } + } + + if unit != nil { + t.Fatalf("Test unit found in list, should be stopped") + } +} + +// TestGetUnitProperties reads the `-.mount` which should exist on all systemd +// systems and ensures that one of its properties is valid. +func TestGetUnitProperties(t *testing.T) { + conn := setupConn(t) unit := "-.mount" diff --git a/dbus/subscription.go b/dbus/subscription.go index 4f15e3b9..dd0c379f 100644 --- a/dbus/subscription.go +++ b/dbus/subscription.go @@ -12,11 +12,11 @@ const ( ignoreInterval = int64(30 * time.Millisecond) ) -// Subscribe sets up this connection to subscribe to all dbus events. This is -// required before calling SubscribeUnits. +// Subscribe sets up this connection to subscribe to all systemd dbus events. +// This is required before calling SubscribeUnits. When the connection closes +// systemd will automatically stop sending signals so there is no need to +// explicitly call Unsubscribe(). func (c *Conn) Subscribe() error { - c.sysconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, - "type='signal',interface='org.freedesktop.systemd1.Manager',member='JobRemoved'") c.sysconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.systemd1.Manager',member='UnitNew'") c.sysconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, @@ -28,8 +28,16 @@ func (c *Conn) Subscribe() error { return err } - c.initSubscription() - c.initDispatch() + return nil +} + +// Unsubscribe this connection from systemd dbus events. +func (c *Conn) Unsubscribe() error { + err := c.sysobj.Call("org.freedesktop.systemd1.Manager.Unsubscribe", 0).Store() + if err != nil { + c.sysconn.Close() + return err + } return nil } @@ -126,7 +134,7 @@ type SubStateUpdate struct { } // SetSubStateSubscriber writes to updateCh when any unit's substate changes. -// Althrough this writes to updateCh on every state change, the reported state +// Although this writes to updateCh on every state change, the reported state // may be more recent than the change that generated it (due to an unavoidable // race in the systemd dbus interface). That is, this method provides a good // way to keep a current view of all units' states, but is not guaranteed to diff --git a/dbus/subscription_test.go b/dbus/subscription_test.go new file mode 100644 index 00000000..6f4d0b32 --- /dev/null +++ b/dbus/subscription_test.go @@ -0,0 +1,90 @@ +package dbus + +import ( + "testing" + "time" +) + +// TestSubscribe exercises the basics of subscription +func TestSubscribe(t *testing.T) { + conn, err := New() + + if err != nil { + t.Fatal(err) + } + + err = conn.Subscribe() + if err != nil { + t.Fatal(err) + } + + err = conn.Unsubscribe() + if err != nil { + t.Fatal(err) + } +} + +// TestSubscribeUnit exercises the basics of subscription of a particular unit. +func TestSubscribeUnit(t *testing.T) { + target := "subscribe-events.service" + + conn, err := New() + + if err != nil { + t.Fatal(err) + } + + err = conn.Subscribe() + if err != nil { + t.Fatal(err) + } + + err = conn.Unsubscribe() + if err != nil { + t.Fatal(err) + } + + evChan, errChan := conn.SubscribeUnits(time.Second) + + setupUnit(target, conn, t) + + job, err := conn.StartUnit(target, "replace") + if err != nil { + t.Fatal(err) + } + + if job != "done" { + t.Fatal("Couldn't start", target) + } + + timeout := make(chan bool, 1) + go func() { + time.Sleep(3 * time.Second) + close(timeout) + }() + + for { + select { + case changes := <-evChan: + tCh, ok := changes[target] + + // Just continue until we see our event. + if !ok { + continue + } + + if tCh.ActiveState == "active" && tCh.Name == target { + goto success + } + case err = <-errChan: + t.Fatal(err) + case <-timeout: + t.Fatal("Reached timeout") + } + } + +success: + return +} + + diff --git a/fixtures/start-stop.service b/fixtures/start-stop.service new file mode 100644 index 00000000..a1f8c367 --- /dev/null +++ b/fixtures/start-stop.service @@ -0,0 +1,5 @@ +[Unit] +Description=start stop test + +[Service] +ExecStart=/bin/sleep 400 diff --git a/fixtures/subscribe-events.service b/fixtures/subscribe-events.service new file mode 100644 index 00000000..a1f8c367 --- /dev/null +++ b/fixtures/subscribe-events.service @@ -0,0 +1,5 @@ +[Unit] +Description=start stop test + +[Service] +ExecStart=/bin/sleep 400