Linux | Go! D-Bus!

2020 11 28, Sat

上一篇提到D-Bus是一种统一桌面程序通信的手段,可以用来调用其他程序的方法,也可以给其他程序提供服务。

前两天刚刚好有微信群的朋友问到说怎么用,也写好了一个例子,但是比较粗略,今天稍微解释和改进一下这个例子。

目标

写一个服务,提供计数器的功能。

设计

我们的计数器应该可以给不同的程序提供不同的计数器,也可以给几个程序共享一个计数器。

所以需要提供两个接口,一个创建共享的计数器,一个创建私有的计数器。两个接口都会创建一个对象,但是只有共享计数器的这个会把对象发布到路径上。

计数器本身只能增加或者减少一定的值,就需要一个可以管计数器的类,我们就把计数器叫做Counter,把管计数器的叫Controller吧。

既然我博客域名是blog.jeffthecoder.xyz,就用xyz.jeffthecoder.Counter.Controller和xyz.jeffthecoder.Counter作为类名吧。

根据我们上一篇说的几个要素来分析一下我们需要什么:

  • 是哪根bus上的服务
  • 提供服务的连接名是啥
  • 提供服务的对象的路径是啥
  • 需要对象上的哪个接口
  • 需要的是函数、信号还是属性
  • 名字叫啥

大致来说,我们就需要:

  • 随意选一个bus上,system和session都可以
    • 连接名不重要,但是为了统一,还是请求一个xyz.jeffthecoder.Counter
      • 在/xyz/jeffthecoder/Counter上注册一个xyz.jeffthecoder.Counter.Controller接口的对象:
        • xyz.jeffthecoder.Counter.Controller
          • 方法
            • 新建私有计数器
              • 在DBus上导出一个路径是/xyz/jeffthecoder/Counter/{id}的xyz.jeffthecoder.Counter对象
            • 新建共享计数器
              • 在DBus上导出一个路径是/xyz/jeffthecoder/Counter/{id}的xyz.jeffthecoder.Counter对象
              • 更新到Controller的children里面
      • /xyz/jeffthecoder/Counter/{id}
        • xyz.jeffthecoder.Counter
          • 方法
            • 加int64的delta
          • 属性
            • Value int64

实现

连接 dbus

我们起手先简单的连接一下dbus:

package main

import (
	"flag"
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/godbus/dbus/v5"
)

var (
	fSystemBus = flag.Bool("system", false, "connect to system bus")
	fBusName   = flag.String("name", "xyz.jeffthecoder.Counter", "dbus name")
)

func init() {
	flag.Parse()
}

func main() {
	var (
		bus *dbus.Conn
		err error
	)

	if *fSystemBus {
		bus, err = dbus.SystemBus()
	} else {
		bus, err = dbus.SessionBus()
	}
	if err != nil {
		log.Fatal(err)
	}

	if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
		log.Fatal("failure during requesting name: ", err)
	} else if reply != dbus.RequestNameReplyPrimaryOwner {
		log.Fatal("unexpected result when requesting name: ", reply)
	}
	defer bus.ReleaseName(*fBusName)


	busSigChan := make(chan *dbus.Signal)
	bus.Signal(busSigChan)

	procSigChan := make(chan os.Signal)
	signal.Notify(procSigChan, syscall.SIGINT)

MainLoop:
	for {
		select {
		case sig := <-busSigChan:
			log.Println("signal from dbus: ", sig)
		case sig := <-procSigChan:
			log.Println("signal from system: ", sig)
			switch sig {
			case syscall.SIGINT:
				break MainLoop
			}
		}
	}
}

代码比较好懂,流水帐式的稍微解释一下(其他的包有问题建议自己查一下文档)

一开始根据选项。连接SystemBus或者登录用户的SessionBus:

	if *fSystemBus {
		bus, err = dbus.SystemBus()
	} else {
		bus, err = dbus.SessionBus()
    }

然后给自己的这根连接请求一个名字,这个名字我默认就用xyz.jeffthecoder.Counter,万一有程序占着这个名字了(比如说你运行了几遍)我们也退出程序:

	if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
		log.Fatal("failure during requesting name: ", err)
	} else if reply != dbus.RequestNameReplyPrimaryOwner {
		log.Fatal("unexpected result when requesting name: ", reply)
    }
	defer bus.ReleaseName(*fBusName)

建两个channel,存放系统的Signal和DBus的Signal,系统Signal就比如说键盘ctrl c就属于系统信号的中断信号。然后用一个循环来不停的处理这些信号(还记得事件循环吗?):

	busSigChan := make(chan *dbus.Signal)
	bus.Signal(busSigChan)

	procSigChan := make(chan os.Signal)
	signal.Notify(procSigChan, syscall.SIGINT)

MainLoop:
	for {
		select {
		case sig := <-busSigChan:
			log.Println("signal from dbus: ", sig)
		case sig := <-procSigChan:
			log.Println("signal from system: ", sig)
			switch sig {
			case syscall.SIGINT:
				break MainLoop
			}
		}
	}

写一个空Controller

type Controller struct {
	bus     *dbus.Conn
}


func NewController(busConn *dbus.Conn) *Controller {
	return &Controller{
		bus:      busConn,
	}
}

func (controller *Controller) Counter(initValue int64) (string, *dbus.Error) {
	id := uuid.New().String()

	path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v", id)

	return path, nil
}

这里肯定都没问题。那个path会用来做具体计数器的对象路径。

把Controller导出到DBus里面

我们需要改一下main函数

    // ...
	// if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
	// 	log.Fatal("failure during requesting name: ", err)
	// } else if reply != dbus.RequestNameReplyPrimaryOwner {
	// 	log.Fatal("unexpected result when requesting name: ", reply)
	// }
	// defer bus.ReleaseName(*fBusName)

	controller := NewController(bus)
	if err := bus.Export(controller, "/xyz/jeffthecoder/Counter", "xyz.jeffthecoder.Counter.Manager"); err != nil {
		log.Fatal("failed to export object: ", err)
	}

	// busSigChan := make(chan *dbus.Signal)
	// if err := bus.AddMatchSignal(
	// 	dbus.WithMatchInterface("xyz.jeffthecoder.Counter"),
	// 	dbus.WithMatchMember("Changed"),
	// ); err != nil {
	// 	log.Fatal("failed to match signal")
	// }
    // bus.Signal(busSigChan)
    // ...

bus.Export函数就会在我们这个连接上导出controller这个对象。

可是我们并不能看到这个对象,D-Feet甚至给我们报了个错

screenshot

为什么呢?因为大部分的工具都需要用org.freedesktop.DBus.Introspectable这个接口才能看到具体的类型定义,而我们并没有导出这个接口。但是这个时候已经可以正常调用我们的对象了。

有一些对象,是不希望别人可以直接看到的,但是又希望别人可以调用,怎么办呢?解决办法就是像这样不导出Introspectable。比较常见的例子就是NetworkManager常用的SecretAgent了。

那如果我们想在D-Feet或者其他自动解析Introspectable的工具中看到这个对象,要怎么办呢?好办啊,在同一个路径上再导出一个Introspectable接口就好了。借reflect的福,这个接口内容也是godbus可以自动生成的:

    // ...
	// if reply, err := bus.RequestName(*fBusName, dbus.NameFlagAllowReplacement|dbus.NameFlagAllowReplacement); err != nil {
	// 	log.Fatal("failure during requesting name: ", err)
	// } else if reply != dbus.RequestNameReplyPrimaryOwner {
	// 	log.Fatal("unexpected result when requesting name: ", reply)
	// }
	// defer bus.ReleaseName(*fBusName)

	// controller := NewController(bus)
	// if err := bus.Export(controller, "/xyz/jeffthecoder/Counter", "xyz.jeffthecoder.Counter.Manager"); err != nil {
	// 	log.Fatal("failed to export object: ", err)
	// }
	if err := bus.Export(introspect.NewIntrospectable(&introspect.Node{
		Interfaces: []introspect.Interface{
			{
				Name:    "xyz.jeffthecoder.Counter.Manager",
				Methods: introspect.Methods(controller),
			},
		},
	}), "/xyz/jeffthecoder/Counter",
		"org.freedesktop.DBus.Introspectable"); err != nil {
	}

	// busSigChan := make(chan *dbus.Signal)
	// if err := bus.AddMatchSignal(
	// 	dbus.WithMatchInterface("xyz.jeffthecoder.Counter"),
	// 	dbus.WithMatchMember("Changed"),
	// ); err != nil {
	// 	log.Fatal("failed to match signal")
	// }
    // bus.Signal(busSigChan)
    // ...

再用D-Feet看的时候,就可以正常用这个对象了:

screenshot

写一个计数器

import (
	"github.com/godbus/dbus/v5"
	"github.com/godbus/dbus/v5/prop"
	"sync"
)

type Counter struct {
	path       dbus.ObjectPath
	controller *Controller

	value int64
	locker sync.Locker
}

func (counter *Counter) Add(delta int64) (int64, *dbus.Error) {
	counter.locker.Lock()
	defer counter.locker.Unlock()

	counter.value += delta

	if _, err := prop.Export(counter.controller.bus, counter.path, map[string]map[string]*prop.Prop{
		"xyz.jeffthecoder.Counter": {
			"value": &prop.Prop{
				Value:    counter.value,
				Writable: false,
			},
		},
	}); err != nil {
		return 0, &dbus.Error{}
	}

	return counter.value, nil
}

这里也不难,新建了一个类型。

因为godbus的两个调用可能会并发,存在线程安全问题,所以我们用到了sync包的锁。本来也可以用atomic包的原子操作,但是考虑到增加delta和更新prop也应该同时,算一个不可分割的整体,想了想还是用锁了。

然后我们通过dbus/prop包,在之前这个 bus连接上 的 counter的对象路径 上,为xyz.jeffthecoder.Counter这个接口导出了一个属性value,大小为新值,不可写入。

最后返回了新值,这个Counter的功能就算完整了。

在Controller中导出Counter

在Controller中导出Counter意味着Controller要记录所有Counter的路径和信息,我们通过map和locker来完成:


// type Controller struct {
// 	bus     *dbus.Conn

	counters map[string]*Counter
	locker   sync.Locker
// }

// func NewController(busConn *dbus.Conn) *Controller {
// 	return &Controller{
// 		bus:      busConn,
		counters: make(map[string]*Counter),
		locker:   &sync.Mutex{},
// 	}
// }

// func (controller *Controller) Counter(initValue int64) (string, *dbus.Error) {
	id := strings.ReplaceAll(uuid.New().String(), "-", "")

// 	path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v", id)
	counter := &Counter{path: dbus.ObjectPath(path), controller: controller, value: initValue, locker: &sync.Mutex{}}

	if err := controller.bus.Export(
		counter,
		dbus.ObjectPath(path),
		"xyz.jeffthecoder.Counter",
	); err != nil {
		return "", &dbus.Error{}
	}

	if err := controller.bus.Export(introspect.NewIntrospectable(&introspect.Node{
		Name: path,
		Interfaces: []introspect.Interface{
			{
				Name:    "xyz.jeffthecoder.Counter",
				Methods: introspect.Methods(counter),
				Properties: []introspect.Property{
					{
						Name:   "value",
						Type:   "x",
						Access: "read",
					},
				},
			},
		},
	}), dbus.ObjectPath(path),
		"org.freedesktop.DBus.Introspectable"); err != nil {
		return "", &dbus.Error{}
	}

	if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
		"xyz.jeffthecoder.Counter": {
			"value": &prop.Prop{
				Value:    initValue,
				Writable: false,
			},
		},
	}); err != nil {
		return "", &dbus.Error{}
	}

// 	return path, nil
// }

类似于前面导出Controller,但是稍微有一点点不一样的地方。

我发现路径上有减号"-“的时候,导出函数时会卡死,原因还没有特别明白。

导出counter没有任何差别:

	counter := &Counter{path: dbus.ObjectPath(path), controller: controller, value: initValue, locker: &sync.Mutex{}}

	if err := controller.bus.Export(
		counter,
		dbus.ObjectPath(path),
		"xyz.jeffthecoder.Counter",
	); err != nil {
		return "", &dbus.Error{}
	}

但是导出counter的Introspectable接口的时候,多出来了value属性:

	if err := controller.bus.Export(introspect.NewIntrospectable(&introspect.Node{
		Name: path,
		Interfaces: []introspect.Interface{
			{
				Name:    "xyz.jeffthecoder.Counter",
				Methods: introspect.Methods(counter),
				Properties: []introspect.Property{
					{
						Name:   "value",
						Type:   "x",
						Access: "read",
					},
				},
			},
		},
	}), dbus.ObjectPath(path),
		"org.freedesktop.DBus.Introspectable"); err != nil {
		return "", &dbus.Error{}
	}

顺便我们更新了Counter对象上value属性的值:

	if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
		"xyz.jeffthecoder.Counter": {
			"value": &prop.Prop{
				Value:    initValue,
				Writable: false,
			},
		},
	}); err != nil {
		return "", &dbus.Error{}
	}

然后就可以正常用我们的Counter了:

screenshot

但是就像图里面我尝试的东西一样,我在dbus里面新建了counter,但是即使刷新以后,dbus也不显示这个counter,为什么呢?

探索了一番以后我发现,我还需要更新Controller的Introspectable接口,提供Children的列表:

	// if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
	// 	"xyz.jeffthecoder.Counter": {
	// 		"value": &prop.Prop{
	// 			Value:    initValue,
	// 			Writable: false,
	// 		},
	// 	},
	// }); err != nil {
	// 	return "", &dbus.Error{}
	// }


	controller.locker.Lock()
	controller.counters[id] = counter
	controller.locker.Unlock()

	node := introspect.Node{
		Name: "/xyz/jeffthecoder/Counter",
		Interfaces: []introspect.Interface{
			{
				Name:    "xyz.jeffthecoder.Counter.Manager",
				Methods: introspect.Methods(controller),
			},
		},
	}
	for k := range controller.counters {
		node.Children = append(node.Children, introspect.Node{
			Name: fmt.Sprint(k),
		})
	}
	if err := controller.bus.Export(introspect.NewIntrospectable(&node), "/xyz/jeffthecoder/Counter",
		"org.freedesktop.DBus.Introspectable"); err != nil {
		return "", &dbus.Error{}
	}


	// return path, nil

我们在counters里面存了各个counter的id,并且重新发布了controller的introspectable接口,于是……

screenshot

销毁Counter

有的同学说,那Counter岂不是越攒越多了,那我们加个Destroy接口吧

func (controller *Controller) Destroy(id string) *dbus.Error {
	controller.locker.Lock()
	defer controller.locker.Unlock()

	if _, ok := controller.counters[id]; !ok {
		return nil
	}

	delete(controller.counters, id)

	path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v", id)

	// unexport
	if err := controller.bus.Export(
		nil,
		dbus.ObjectPath(path),
		"xyz.jeffthecoder.Counter",
	); err != nil {
		return &dbus.Error{}
	}
	if err := controller.bus.Export(
		nil, 
		dbus.ObjectPath(path),
		"org.freedesktop.DBus.Introspectable"); err != nil {
		return &dbus.Error{}
	}

	// re-export controller's introspectable interface
	node := introspect.Node{
		Name: "/xyz/jeffthecoder/Counter",
		Interfaces: []introspect.Interface{
			{
				Name:    "xyz.jeffthecoder.Counter.Manager",
				Methods: introspect.Methods(controller),
			},
		},
	}
	for k := range controller.counters {
		node.Children = append(node.Children, introspect.Node{
			Name: fmt.Sprint(k),
		})
	}
	if err := controller.bus.Export(introspect.NewIntrospectable(&node), "/xyz/jeffthecoder/Counter",
		"org.freedesktop.DBus.Introspectable"); err != nil {
		return &dbus.Error{}
	}


	return nil
}

私有计数器

有的同学说,我就想建一个我这个连接才能使用的计数器,连接断开以后删除计数器。

也简单,我们导出对象,但是不更新Controller就好了。但是说不定其他人也能调用呢?我翻看了一下godbus的代码,发现函数可以这样定义:

func (controller *Controller) PrivateCounter(sender dbus.Sender, initValue int64) (string, *dbus.Error) {
	// ...
}

godbus在函数参数中发现dbus.Sender类型参数后,会在这个参数中填上发送者的名字。

依靠这种能力,可以非常简单的给Counter加上控制。新加入的片段类似于这样:


func (controller *Controller) PrivateCounter(sender dbus.Sender, initValue int64) (string, *dbus.Error) {
	id := strings.ReplaceAll(uuid.New().String(), "-", "")

	path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v/%v", string(sender), id)
	counter := &PrivateCounter{
		Counter: Counter{path: dbus.ObjectPath(path), controller: controller, value: initValue, locker: &sync.Mutex{}},
		Sender: sender,
	}

	if err := controller.bus.Export(
		counter,
		dbus.ObjectPath(path),
		"xyz.jeffthecoder.Counter",
	); err != nil {
		return "", &dbus.Error{}
	}

	if err := controller.bus.Export(introspect.NewIntrospectable(&introspect.Node{
		Name: path,
		Interfaces: []introspect.Interface{
			{
				Name:    "xyz.jeffthecoder.Counter",
				Methods: introspect.Methods(counter),
				Properties: []introspect.Property{
					{
						Name:   "value",
						Type:   "x",
						Access: "read",
					},
				},
			},
		},
	}), dbus.ObjectPath(path),
		"org.freedesktop.DBus.Introspectable"); err != nil {
		return "", &dbus.Error{}
	}

	if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
		"xyz.jeffthecoder.Counter": {
			"value": &prop.Prop{
				Value:    initValue,
				Writable: false,
			},
		},
	}); err != nil {
		return "", &dbus.Error{}
	}

	return path, nil
}

type PrivateCounter struct {
	Counter

	Sender dbus.Sender
}

func (privateCounter *PrivateCounter) Add(sender dbus.Sender, delta int64) (int64, *dbus.Error) {
	if sender == privateCounter.Sender {
		return privateCounter.Counter.Add(delta)
	}

	return 0, &dbus.ErrMsgNoObject
}

screenshot

自动销毁私有计数器

和刚才的Destroy问题类似,godbus中注册的object会越来越多,作为第三方又很难去销毁一个私有的counter。怎么办呢?我们可以监控sender消失的事件。

我们通过ps aux | grep ipython拿到刚才python进程的pid,是34270,d-feet里面对应的连接名叫:1.89。

通过dbus-monitor,运行的同时我们退出这个进程,可以看到中间有一个事件是

signal time=1606555271.716596 sender=org.freedesktop.DBus -> destination=:1.89 serial=5 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
   string ":1.89"
signal time=1606555271.716610 sender=org.freedesktop.DBus -> destination=(null destination) serial=522 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
   string ":1.89"
   string ":1.89"
   string ""

其中NameLost看起来是最理想的,但是destination是ipython这个进程自己,说明counter这一侧捕获不到这个signal。那我们只能选择NameOwnerChanged这个广播出去(destination=null)的signal了

也就是说我们监控sender的NameList信号,就可以知道,对方下线了。这个时候,我们在main函数里面的那个MainLoop就有用了,为了把有关的代码移动的更集中一些,我们把这部分代码移动到Controller内:

func (controller *Controller) Run() {
	busSigChan := make(chan *dbus.Signal)
	controller.bus.Signal(busSigChan)

	procSigChan := make(chan os.Signal)
	signal.Notify(procSigChan, syscall.SIGINT)

MainLoop:
	for {
		select {
		case sig := <-busSigChan:
			log.Println("signal from dbus: ", sig)
		case sig := <-procSigChan:
			log.Println("signal from system: ", sig)
			switch sig {
			case syscall.SIGINT:
				break MainLoop
			}
		}
	}
}

func main() {
	// ...

	// if err := bus.Export(introspect.NewIntrospectable(&introspect.Node{
	// 	Interfaces: []introspect.Interface{
	// 		{
	// 			Name:    "xyz.jeffthecoder.Counter.Manager",
	// 			Methods: introspect.Methods(controller),
	// 		},
	// 	},
	// }), "/xyz/jeffthecoder/Counter",
	// 	"org.freedesktop.DBus.Introspectable"); err != nil {
	// }

	controller.Run()
}

然后我们在PrivateCounter里面,记录一下哪个sender建了哪些counter,加上监听的选项

// type Controller struct {
// 	bus *dbus.Conn

// 	counters map[string]*Counter
// 	locker   sync.Locker

	privateCounter map[string][]string
	privateLocker  sync.Locker
// }

// func NewController(busConn *dbus.Conn) *Controller {
// 	return &Controller{
// 		bus:      busConn,

// 		counters: make(map[string]*Counter),
// 		locker:   &sync.Mutex{},

		privateCounter: make(map[string][]string),
		privateLocker: &sync.Mutex{},
// 	}
// }

// func (controller *Controller) PrivateCounter(sender dbus.Sender, initValue int64) (string, *dbus.Error) {
	// ...
	// if _, err := prop.Export(controller.bus, dbus.ObjectPath(path), map[string]map[string]*prop.Prop{
	// 	"xyz.jeffthecoder.Counter": {
	// 		"value": &prop.Prop{
	// 			Value:    initValue,
	// 			Writable: false,
	// 		},
	// 	},
	// }); err != nil {
	// 	return "", &dbus.Error{}
	// }

	controller.privateLocker.Lock()
	if arr, ok := controller.privateCounter[string(sender)]; ok {
		controller.privateCounter[string(sender)] = append(arr, id)
	} else {
		controller.privateCounter[string(sender)] = []string{id}
	}
	controller.privateLocker.Unlock()

	if err := controller.bus.AddMatchSignal(
		dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
		dbus.WithMatchInterface("org.freedesktop.DBus"),
		dbus.WithMatchSender("org.freedesktop.DBus"),
		dbus.WithMatchMember("NameLost"),
		dbus.WithMatchOption("arg1", string(sender)),
	); err != nil {
		log.Printf("warning: failed to add match signal for %v, there will be memory leak in the future: %v", sender, err)
	}

//    return path, nil
// }

在Run里面,收集到signal后,unexport一下就好了

// func (controller *Controller) Run() {
// 	busSigChan := make(chan *dbus.Signal)
// 	controller.bus.Signal(busSigChan)

// 	procSigChan := make(chan os.Signal)
// 	signal.Notify(procSigChan, syscall.SIGINT)

// MainLoop:
// 	for {
// 		select {
// 		case sig := <-busSigChan:
// 			log.Println("signal from dbus: ", sig)
			if sig.Name == "org.freedesktop.DBus.NameOwnerChanged" && len(sig.Body) >= 1 {
				if sender, ok := sig.Body[0].(string); ok {
					controller.privateLocker.Lock()
					if counters, ok := controller.privateCounter[sender]; ok {
						for _, counter := range counters {
							path := fmt.Sprintf("/xyz/jeffthecoder/Counter/%v/%v", string(sender), counter)
							_ = controller.bus.Export(nil, dbus.ObjectPath(path), "xyz.jeffthecoder.Counter")
							_ = controller.bus.Export(nil, dbus.ObjectPath(path), "org.freedesktop.DBus.Introspectable")
						}
						delete(controller.privateCounter, sender)
					}
					controller.privateLocker.Unlock()

					_ = controller.bus.RemoveMatchSignal(
						dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
						dbus.WithMatchInterface("org.freedesktop.DBus"),
						dbus.WithMatchSender("org.freedesktop.DBus"),
						dbus.WithMatchMember("NameLost"),
						dbus.WithMatchOption("arg1", sender),
					)
				}
			}
// 		case sig := <-procSigChan:
// 			log.Println("signal from system: ", sig)
// 			switch sig {
// 			case syscall.SIGINT:
// 				break MainLoop
// 			}
// 		}
// 	}
// }

screenshot

完美。

代码在Gist上:Controller.go