Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow eavesdrop="true"/>
<allow eavesdrop="true" send_destination="*"/>
</policy>
</busconfig>
```
10 changes: 8 additions & 2 deletions dbus/dbus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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
}


10 changes: 10 additions & 0 deletions dbus/dbus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
2 changes: 1 addition & 1 deletion dbus/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
100 changes: 96 additions & 4 deletions dbus/methods_test.go
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
22 changes: 15 additions & 7 deletions dbus/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions dbus/subscription_test.go
Original file line number Diff line number Diff line change
@@ -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
}


5 changes: 5 additions & 0 deletions fixtures/start-stop.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Unit]
Description=start stop test

[Service]
ExecStart=/bin/sleep 400
5 changes: 5 additions & 0 deletions fixtures/subscribe-events.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Unit]
Description=start stop test

[Service]
ExecStart=/bin/sleep 400