diff --git a/src/github.com/coreos/coreos-cloudinit b/src/github.com/coreos/coreos-cloudinit index a8a4f8c..1b20c9f 120000 --- a/src/github.com/coreos/coreos-cloudinit +++ b/src/github.com/coreos/coreos-cloudinit @@ -1 +1 @@ -../../.. \ No newline at end of file +../../../ \ No newline at end of file diff --git a/src/github.com/coreos/go-systemd b/src/github.com/coreos/go-systemd deleted file mode 160000 index 8abd78f..0000000 --- a/src/github.com/coreos/go-systemd +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8abd78f0ac040cd0e8972866f912c03ba78945ee diff --git a/src/github.com/coreos/go-systemd/.travis.yml b/src/github.com/coreos/go-systemd/.travis.yml new file mode 100644 index 0000000..8c9f56e --- /dev/null +++ b/src/github.com/coreos/go-systemd/.travis.yml @@ -0,0 +1,8 @@ +language: go +go: 1.2 + +install: + - echo "Skip install" + +script: + - ./test diff --git a/src/github.com/coreos/go-systemd/LICENSE b/src/github.com/coreos/go-systemd/LICENSE new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/src/github.com/coreos/go-systemd/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/github.com/coreos/go-systemd/README.md b/src/github.com/coreos/go-systemd/README.md new file mode 100644 index 0000000..0ee09fe --- /dev/null +++ b/src/github.com/coreos/go-systemd/README.md @@ -0,0 +1,44 @@ +# go-systemd + +Go bindings to systemd. The project has three packages: + +- activation - for writing and using socket activation from Go +- journal - for writing to systemd's logging service, journal +- dbus - for starting/stopping/inspecting running services and units + +Go docs for the entire project are here: + +http://godoc.org/github.com/coreos/go-systemd + +## Socket Activation + +An example HTTP server using socket activation can be quickly setup by +following this README on a Linux machine running systemd: + +https://github.com/coreos/go-systemd/tree/master/examples/activation/httpserver + +## Journal + +Using this package you can submit journal entries directly to systemd's journal taking advantage of features like indexed key/value pairs for each log entry. + +## 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/src/github.com/coreos/go-systemd/activation/files.go b/src/github.com/coreos/go-systemd/activation/files.go new file mode 100644 index 0000000..74b4fc1 --- /dev/null +++ b/src/github.com/coreos/go-systemd/activation/files.go @@ -0,0 +1,56 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package activation implements primitives for systemd socket activation. +package activation + +import ( + "os" + "strconv" + "syscall" +) + +// based on: https://gist.github.com/alberts/4640792 +const ( + listenFdsStart = 3 +) + +func Files(unsetEnv bool) []*os.File { + if unsetEnv { + // there is no way to unset env in golang os package for now + // https://code.google.com/p/go/issues/detail?id=6423 + defer os.Setenv("LISTEN_PID", "") + defer os.Setenv("LISTEN_FDS", "") + } + + pid, err := strconv.Atoi(os.Getenv("LISTEN_PID")) + if err != nil || pid != os.Getpid() { + return nil + } + + nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) + if err != nil || nfds == 0 { + return nil + } + + var files []*os.File + for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ { + syscall.CloseOnExec(fd) + files = append(files, os.NewFile(uintptr(fd), "LISTEN_FD_"+strconv.Itoa(fd))) + } + + return files +} diff --git a/src/github.com/coreos/go-systemd/activation/files_test.go b/src/github.com/coreos/go-systemd/activation/files_test.go new file mode 100644 index 0000000..a1c6948 --- /dev/null +++ b/src/github.com/coreos/go-systemd/activation/files_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package activation + +import ( + "bytes" + "io" + "os" + "os/exec" + "testing" +) + +// correctStringWritten fails the text if the correct string wasn't written +// to the other side of the pipe. +func correctStringWritten(t *testing.T, r *os.File, expected string) bool { + bytes := make([]byte, len(expected)) + io.ReadAtLeast(r, bytes, len(expected)) + + if string(bytes) != expected { + t.Fatalf("Unexpected string %s", string(bytes)) + } + + return true +} + +// TestActivation forks out a copy of activation.go example and reads back two +// strings from the pipes that are passed in. +func TestActivation(t *testing.T) { + cmd := exec.Command("go", "run", "../examples/activation/activation.go") + + r1, w1, _ := os.Pipe() + r2, w2, _ := os.Pipe() + cmd.ExtraFiles = []*os.File{ + w1, + w2, + } + + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "LISTEN_FDS=2", "FIX_LISTEN_PID=1") + + err := cmd.Run() + if err != nil { + t.Fatalf(err.Error()) + } + + correctStringWritten(t, r1, "Hello world") + correctStringWritten(t, r2, "Goodbye world") +} + +func TestActivationNoFix(t *testing.T) { + cmd := exec.Command("go", "run", "../examples/activation/activation.go") + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "LISTEN_FDS=2") + + out, _ := cmd.CombinedOutput() + if bytes.Contains(out, []byte("No files")) == false { + t.Fatalf("Child didn't error out as expected") + } +} + +func TestActivationNoFiles(t *testing.T) { + cmd := exec.Command("go", "run", "../examples/activation/activation.go") + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "LISTEN_FDS=0", "FIX_LISTEN_PID=1") + + out, _ := cmd.CombinedOutput() + if bytes.Contains(out, []byte("No files")) == false { + t.Fatalf("Child didn't error out as expected") + } +} diff --git a/src/github.com/coreos/go-systemd/activation/listeners.go b/src/github.com/coreos/go-systemd/activation/listeners.go new file mode 100644 index 0000000..cdb2cf4 --- /dev/null +++ b/src/github.com/coreos/go-systemd/activation/listeners.go @@ -0,0 +1,38 @@ +/* +Copyright 2014 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package activation + +import ( + "fmt" + "net" +) + +// Listeners returns net.Listeners for all socket activated fds passed to this process. +func Listeners(unsetEnv bool) ([]net.Listener, error) { + files := Files(unsetEnv) + listeners := make([]net.Listener, len(files)) + + for i, f := range files { + var err error + listeners[i], err = net.FileListener(f) + if err != nil { + return nil, fmt.Errorf("Error setting up FileListener for fd %d: %s", f.Fd(), err.Error()) + } + } + + return listeners, nil +} diff --git a/src/github.com/coreos/go-systemd/activation/listeners_test.go b/src/github.com/coreos/go-systemd/activation/listeners_test.go new file mode 100644 index 0000000..c3627d6 --- /dev/null +++ b/src/github.com/coreos/go-systemd/activation/listeners_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2014 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package activation + +import ( + "io" + "net" + "os" + "os/exec" + "testing" +) + +// correctStringWritten fails the text if the correct string wasn't written +// to the other side of the pipe. +func correctStringWrittenNet(t *testing.T, r net.Conn, expected string) bool { + bytes := make([]byte, len(expected)) + io.ReadAtLeast(r, bytes, len(expected)) + + if string(bytes) != expected { + t.Fatalf("Unexpected string %s", string(bytes)) + } + + return true +} + +// TestActivation forks out a copy of activation.go example and reads back two +// strings from the pipes that are passed in. +func TestListeners(t *testing.T) { + cmd := exec.Command("go", "run", "../examples/activation/listen.go") + + l1, err := net.Listen("tcp", ":9999") + if err != nil { + t.Fatalf(err.Error()) + } + l2, err := net.Listen("tcp", ":1234") + if err != nil { + t.Fatalf(err.Error()) + } + + t1 := l1.(*net.TCPListener) + t2 := l2.(*net.TCPListener) + + f1, _ := t1.File() + f2, _ := t2.File() + + cmd.ExtraFiles = []*os.File{ + f1, + f2, + } + + r1, err := net.Dial("tcp", "127.0.0.1:9999") + if err != nil { + t.Fatalf(err.Error()) + } + r1.Write([]byte("Hi")) + + r2, err := net.Dial("tcp", "127.0.0.1:1234") + if err != nil { + t.Fatalf(err.Error()) + } + r2.Write([]byte("Hi")) + + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "LISTEN_FDS=2", "FIX_LISTEN_PID=1") + + out, err := cmd.Output() + if err != nil { + println(string(out)) + t.Fatalf(err.Error()) + } + + correctStringWrittenNet(t, r1, "Hello world") + correctStringWrittenNet(t, r2, "Goodbye world") +} diff --git a/src/github.com/coreos/go-systemd/dbus/dbus.go b/src/github.com/coreos/go-systemd/dbus/dbus.go new file mode 100644 index 0000000..ad28039 --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/dbus.go @@ -0,0 +1,97 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Integration with the systemd D-Bus API. See http://www.freedesktop.org/wiki/Software/systemd/dbus/ +package dbus + +import ( + "strings" + "sync" + + "github.com/guelfey/go.dbus" +) + +const signalBuffer = 100 + +// ObjectPath creates a dbus.ObjectPath using the rules that systemd uses for +// serializing special characters. +func ObjectPath(path string) dbus.ObjectPath { + path = strings.Replace(path, ".", "_2e", -1) + path = strings.Replace(path, "-", "_2d", -1) + path = strings.Replace(path, "@", "_40", -1) + + return dbus.ObjectPath(path) +} + +// Conn is a connection to systemds dbus endpoint. +type Conn struct { + sysconn *dbus.Conn + sysobj *dbus.Object + jobListener struct { + jobs map[dbus.ObjectPath]chan string + sync.Mutex + } + subscriber struct { + updateCh chan<- *SubStateUpdate + errCh chan<- error + sync.Mutex + ignore map[dbus.ObjectPath]int64 + cleanIgnore int64 + } + dispatch map[string]func(dbus.Signal) +} + +// New() establishes a connection to the system bus and authenticates. +func New() (*Conn, error) { + c := new(Conn) + + if err := c.initConnection(); err != nil { + return nil, err + } + + c.initJobs() + return c, nil +} + +func (c *Conn) initConnection() error { + var err error + c.sysconn, err = dbus.SystemBusPrivate() + if err != nil { + return err + } + + err = c.sysconn.Auth(nil) + if err != nil { + c.sysconn.Close() + return err + } + + err = c.sysconn.Hello() + if err != nil { + c.sysconn.Close() + return err + } + + 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/src/github.com/coreos/go-systemd/dbus/dbus_test.go b/src/github.com/coreos/go-systemd/dbus/dbus_test.go new file mode 100644 index 0000000..2e80f73 --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/dbus_test.go @@ -0,0 +1,41 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +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) + expected := "/silly_2dpath/to_40a/unit_2e_2eservice" + + if string(output) != expected { + 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/src/github.com/coreos/go-systemd/dbus/methods.go b/src/github.com/coreos/go-systemd/dbus/methods.go new file mode 100644 index 0000000..37d82b1 --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/methods.go @@ -0,0 +1,263 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +import ( + "errors" + "github.com/guelfey/go.dbus" +) + +func (c *Conn) initJobs() { + c.jobListener.jobs = make(map[dbus.ObjectPath]chan string) +} + +func (c *Conn) jobComplete(signal *dbus.Signal) { + var id uint32 + var job dbus.ObjectPath + var unit string + var result string + dbus.Store(signal.Body, &id, &job, &unit, &result) + c.jobListener.Lock() + out, ok := c.jobListener.jobs[job] + if ok { + out <- result + } + c.jobListener.Unlock() +} + +func (c *Conn) startJob(job string, args ...interface{}) (<-chan string, error) { + c.jobListener.Lock() + defer c.jobListener.Unlock() + + ch := make(chan string, 1) + var path dbus.ObjectPath + err := c.sysobj.Call(job, 0, args...).Store(&path) + if err != nil { + return nil, err + } + c.jobListener.jobs[path] = ch + return ch, nil +} + +func (c *Conn) runJob(job string, args ...interface{}) (string, error) { + respCh, err := c.startJob(job, args...) + if err != nil { + return "", err + } + return <-respCh, nil +} + +// StartUnit enqeues a start job and depending jobs, if any (unless otherwise +// specified by the mode string). +// +// Takes the unit to activate, plus a mode string. The mode needs to be one of +// replace, fail, isolate, ignore-dependencies, ignore-requirements. If +// "replace" the call will start the unit and its dependencies, possibly +// replacing already queued jobs that conflict with this. If "fail" the call +// will start the unit and its dependencies, but will fail if this would change +// an already queued job. If "isolate" the call will start the unit in question +// and terminate all units that aren't dependencies of it. If +// "ignore-dependencies" it will start a unit but ignore all its dependencies. +// If "ignore-requirements" it will start a unit but only ignore the +// requirement dependencies. It is not recommended to make use of the latter +// two options. +// +// Result string: one of done, canceled, timeout, failed, dependency, skipped. +// done indicates successful execution of a job. canceled indicates that a job +// has been canceled before it finished execution. timeout indicates that the +// job timeout was reached. failed indicates that the job failed. dependency +// indicates that a job this job has been depending on failed and the job hence +// has been removed too. skipped indicates that a job was skipped because it +// didn't apply to the units current state. +func (c *Conn) StartUnit(name string, mode string) (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.StartUnit", name, mode) +} + +// StopUnit is similar to StartUnit but stops the specified unit rather +// than starting it. +func (c *Conn) StopUnit(name string, mode string) (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.StopUnit", name, mode) +} + +// ReloadUnit reloads a unit. Reloading is done only if the unit is already running and fails otherwise. +func (c *Conn) ReloadUnit(name string, mode string) (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.ReloadUnit", name, mode) +} + +// RestartUnit restarts a service. If a service is restarted that isn't +// running it will be started. +func (c *Conn) RestartUnit(name string, mode string) (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.RestartUnit", name, mode) +} + +// TryRestartUnit is like RestartUnit, except that a service that isn't running +// is not affected by the restart. +func (c *Conn) TryRestartUnit(name string, mode string) (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.TryRestartUnit", name, mode) +} + +// ReloadOrRestart attempts a reload if the unit supports it and use a restart +// otherwise. +func (c *Conn) ReloadOrRestartUnit(name string, mode string) (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.ReloadOrRestartUnit", name, mode) +} + +// ReloadOrTryRestart attempts a reload if the unit supports it and use a "Try" +// flavored restart otherwise. +func (c *Conn) ReloadOrTryRestartUnit(name string, mode string) (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.ReloadOrTryRestartUnit", name, mode) +} + +// StartTransientUnit() may be used to create and start a transient unit, which +// will be released as soon as it is not running or referenced anymore or the +// system is rebooted. name is the unit name including suffix, and must be +// unique. mode is the same as in StartUnit(), properties contains properties +// of the unit. +func (c *Conn) StartTransientUnit(name string, mode string, properties ...Property) (string, error) { + // the dbus interface for this method does not use the last argument and + // should simply be given an empty list. We use a concrete type here + // (instead of the more appropriate interface{}) to satisfy the dbus library. + return c.runJob("org.freedesktop.systemd1.Manager.StartTransientUnit", name, mode, properties, make([]string, 0)) +} + +// KillUnit takes the unit name and a UNIX signal number to send. All of the unit's +// processes are killed. +func (c *Conn) KillUnit(name string, signal int32) { + c.sysobj.Call("org.freedesktop.systemd1.Manager.KillUnit", 0, name, "all", signal).Store() +} + +// GetUnitProperties takes the unit name and returns all of its dbus object properties. +func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) { + var err error + var props map[string]dbus.Variant + + path := ObjectPath("/org/freedesktop/systemd1/unit/" + unit) + if !path.IsValid() { + return nil, errors.New("invalid unit name: " + unit) + } + + obj := c.sysconn.Object("org.freedesktop.systemd1", path) + err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, "org.freedesktop.systemd1.Unit").Store(&props) + if err != nil { + return nil, err + } + + out := make(map[string]interface{}, len(props)) + for k, v := range props { + out[k] = v.Value() + } + + return out, nil +} + +// ListUnits returns an array with all currently loaded units. Note that +// units may be known by multiple names at the same time, and hence there might +// be more unit names loaded than actual units behind them. +func (c *Conn) ListUnits() ([]UnitStatus, error) { + result := make([][]interface{}, 0) + err := c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnits", 0).Store(&result) + if err != nil { + return nil, err + } + + resultInterface := make([]interface{}, len(result)) + for i := range result { + resultInterface[i] = result[i] + } + + status := make([]UnitStatus, len(result)) + statusInterface := make([]interface{}, len(status)) + for i := range status { + statusInterface[i] = &status[i] + } + + err = dbus.Store(resultInterface, statusInterface...) + if err != nil { + return nil, err + } + + return status, nil +} + +type UnitStatus struct { + Name string // The primary unit name as string + Description string // The human readable description string + LoadState string // The load state (i.e. whether the unit file has been loaded successfully) + ActiveState string // The active state (i.e. whether the unit is currently started or not) + SubState string // The sub state (a more fine-grained version of the active state that is specific to the unit type, which the active state is not) + Followed string // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string. + Path dbus.ObjectPath // The unit object path + JobId uint32 // If there is a job queued for the job unit the numeric job id, 0 otherwise + JobType string // The job type as string + JobPath dbus.ObjectPath // The job object path +} + +// EnableUnitFiles() may be used to enable one or more units in the system (by +// creating symlinks to them in /etc or /run). +// +// It takes a list of unit files to enable (either just file names or full +// absolute paths if the unit files are residing outside the usual unit +// search paths), and two booleans: the first controls whether the unit shall +// be enabled for runtime only (true, /run), or persistently (false, /etc). +// The second one controls whether symlinks pointing to other units shall +// be replaced if necessary. +// +// This call returns one boolean and an array with the changes made. The +// boolean signals whether the unit files contained any enablement +// information (i.e. an [Install]) section. The changes list consists of +// structures with three strings: the type of the change (one of symlink +// or unlink), the file name of the symlink and the destination of the +// symlink. +func (c *Conn) EnableUnitFiles(files []string, runtime bool, force bool) (bool, []EnableUnitFileChange, error) { + var carries_install_info bool + + result := make([][]interface{}, 0) + err := c.sysobj.Call("org.freedesktop.systemd1.Manager.EnableUnitFiles", 0, files, runtime, force).Store(&carries_install_info, &result) + if err != nil { + return false, nil, err + } + + resultInterface := make([]interface{}, len(result)) + for i := range result { + resultInterface[i] = result[i] + } + + changes := make([]EnableUnitFileChange, len(result)) + changesInterface := make([]interface{}, len(changes)) + for i := range changes { + changesInterface[i] = &changes[i] + } + + err = dbus.Store(resultInterface, changesInterface...) + if err != nil { + return false, nil, err + } + + return carries_install_info, changes, nil +} + +type EnableUnitFileChange struct { + Type string // Type of the change (one of symlink or unlink) + Filename string // File name of the symlink + Destination string // Destination of the symlink +} + +// Reload instructs systemd to scan for and reload unit files. This is +// equivalent to a 'systemctl daemon-reload'. +func (c *Conn) Reload() (string, error) { + return c.runJob("org.freedesktop.systemd1.Manager.Reload") +} diff --git a/src/github.com/coreos/go-systemd/dbus/methods_test.go b/src/github.com/coreos/go-systemd/dbus/methods_test.go new file mode 100644 index 0000000..09bd212 --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/methods_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +import ( + "os" + "path/filepath" + "testing" +) + +func setupConn(t *testing.T) *Conn { + conn, err := New() + if err != nil { + t.Fatal(err) + } + + 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" + + info, err := conn.GetUnitProperties(unit) + if err != nil { + t.Fatal(err) + } + + names := info["Wants"].([]string) + + if len(names) < 1 { + t.Fatal("/ is unwanted") + } + + if names[0] != "system.slice" { + t.Fatal("unexpected wants for /") + } +} + +// TestGetUnitPropertiesRejectsInvalidName attempts to get the properties for a +// unit with an invalid name. This test should be run with --test.timeout set, +// as a fail will manifest as GetUnitProperties hanging indefinitely. +func TestGetUnitPropertiesRejectsInvalidName(t *testing.T) { + conn := setupConn(t) + + unit := "//invalid#$^/" + + _, err := conn.GetUnitProperties(unit) + + if err == nil { + t.Fatal("Expected an error, got nil") + } +} diff --git a/src/github.com/coreos/go-systemd/dbus/properties.go b/src/github.com/coreos/go-systemd/dbus/properties.go new file mode 100644 index 0000000..6347f0d --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/properties.go @@ -0,0 +1,206 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +import ( + "github.com/guelfey/go.dbus" +) + +// From the systemd docs: +// +// The properties array of StartTransientUnit() may take many of the settings +// that may also be configured in unit files. Not all parameters are currently +// accepted though, but we plan to cover more properties with future release. +// Currently you may set the Description, Slice and all dependency types of +// units, as well as RemainAfterExit, ExecStart for service units, +// TimeoutStopUSec and PIDs for scope units, and CPUAccounting, CPUShares, +// BlockIOAccounting, BlockIOWeight, BlockIOReadBandwidth, +// BlockIOWriteBandwidth, BlockIODeviceWeight, MemoryAccounting, MemoryLimit, +// DevicePolicy, DeviceAllow for services/scopes/slices. These fields map +// directly to their counterparts in unit files and as normal D-Bus object +// properties. The exception here is the PIDs field of scope units which is +// used for construction of the scope only and specifies the initial PIDs to +// add to the scope object. + +type Property struct { + Name string + Value dbus.Variant +} + +type execStart struct { + Path string // the binary path to execute + Args []string // an array with all arguments to pass to the executed command, starting with argument 0 + UncleanIsFailure bool // a boolean whether it should be considered a failure if the process exits uncleanly +} + +// PropExecStart sets the ExecStart service property. The first argument is a +// slice with the binary path to execute followed by the arguments to pass to +// the executed command. See +// http://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= +func PropExecStart(command []string, uncleanIsFailure bool) Property { + execStarts := []execStart{ + execStart{ + Path: command[0], + Args: command, + UncleanIsFailure: uncleanIsFailure, + }, + } + + return Property{ + Name: "ExecStart", + Value: dbus.MakeVariant(execStarts), + } +} + +// PropRemainAfterExit sets the RemainAfterExit service property. See +// http://www.freedesktop.org/software/systemd/man/systemd.service.html#RemainAfterExit= +func PropRemainAfterExit(b bool) Property { + return Property{ + Name: "RemainAfterExit", + Value: dbus.MakeVariant(b), + } +} + +// PropDescription sets the Description unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit#Description= +func PropDescription(desc string) Property { + return Property{ + Name: "Description", + Value: dbus.MakeVariant(desc), + } +} + +func propDependency(name string, units []string) Property { + return Property{ + Name: name, + Value: dbus.MakeVariant(units), + } +} + +// PropRequires sets the Requires unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requires= +func PropRequires(units ...string) Property { + return propDependency("Requires", units) +} + +// PropRequiresOverridable sets the RequiresOverridable unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresOverridable= +func PropRequiresOverridable(units ...string) Property { + return propDependency("RequiresOverridable", units) +} + +// PropRequisite sets the Requisite unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requisite= +func PropRequisite(units ...string) Property { + return propDependency("Requisite", units) +} + +// PropRequisiteOverridable sets the RequisiteOverridable unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequisiteOverridable= +func PropRequisiteOverridable(units ...string) Property { + return propDependency("RequisiteOverridable", units) +} + +// PropWants sets the Wants unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Wants= +func PropWants(units ...string) Property { + return propDependency("Wants", units) +} + +// PropBindsTo sets the BindsTo unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#BindsTo= +func PropBindsTo(units ...string) Property { + return propDependency("BindsTo", units) +} + +// PropRequiredBy sets the RequiredBy unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredBy= +func PropRequiredBy(units ...string) Property { + return propDependency("RequiredBy", units) +} + +// PropRequiredByOverridable sets the RequiredByOverridable unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredByOverridable= +func PropRequiredByOverridable(units ...string) Property { + return propDependency("RequiredByOverridable", units) +} + +// PropWantedBy sets the WantedBy unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#WantedBy= +func PropWantedBy(units ...string) Property { + return propDependency("WantedBy", units) +} + +// PropBoundBy sets the BoundBy unit property. See +// http://www.freedesktop.org/software/systemd/main/systemd.unit.html#BoundBy= +func PropBoundBy(units ...string) Property { + return propDependency("BoundBy", units) +} + +// PropConflicts sets the Conflicts unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Conflicts= +func PropConflicts(units ...string) Property { + return propDependency("Conflicts", units) +} + +// PropConflictedBy sets the ConflictedBy unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#ConflictedBy= +func PropConflictedBy(units ...string) Property { + return propDependency("ConflictedBy", units) +} + +// PropBefore sets the Before unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before= +func PropBefore(units ...string) Property { + return propDependency("Before", units) +} + +// PropAfter sets the After unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#After= +func PropAfter(units ...string) Property { + return propDependency("After", units) +} + +// PropOnFailure sets the OnFailure unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#OnFailure= +func PropOnFailure(units ...string) Property { + return propDependency("OnFailure", units) +} + +// PropTriggers sets the Triggers unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Triggers= +func PropTriggers(units ...string) Property { + return propDependency("Triggers", units) +} + +// PropTriggeredBy sets the TriggeredBy unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#TriggeredBy= +func PropTriggeredBy(units ...string) Property { + return propDependency("TriggeredBy", units) +} + +// PropPropagatesReloadTo sets the PropagatesReloadTo unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#PropagatesReloadTo= +func PropPropagatesReloadTo(units ...string) Property { + return propDependency("PropagatesReloadTo", units) +} + +// PropRequiresMountsFor sets the RequiresMountsFor unit property. See +// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresMountsFor= +func PropRequiresMountsFor(units ...string) Property { + return propDependency("RequiresMountsFor", units) +} diff --git a/src/github.com/coreos/go-systemd/dbus/set.go b/src/github.com/coreos/go-systemd/dbus/set.go new file mode 100644 index 0000000..88378b2 --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/set.go @@ -0,0 +1,26 @@ +package dbus + +type set struct { + data map[string]bool +} + +func (s *set) Add(value string) { + s.data[value] = true +} + +func (s *set) Remove(value string) { + delete(s.data, value) +} + +func (s *set) Contains(value string) (exists bool) { + _, exists = s.data[value] + return +} + +func (s *set) Length() (int) { + return len(s.data) +} + +func newSet() (*set) { + return &set{make(map[string] bool)} +} diff --git a/src/github.com/coreos/go-systemd/dbus/set_test.go b/src/github.com/coreos/go-systemd/dbus/set_test.go new file mode 100644 index 0000000..d8d174d --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/set_test.go @@ -0,0 +1,26 @@ +package dbus + +import ( + "testing" +) + +// TestBasicSetActions asserts that Add & Remove behavior is correct +func TestBasicSetActions(t *testing.T) { + s := newSet() + + if s.Contains("foo") { + t.Fatal("set should not contain 'foo'") + } + + s.Add("foo") + + if !s.Contains("foo") { + t.Fatal("set should contain 'foo'") + } + + s.Remove("foo") + + if s.Contains("foo") { + t.Fatal("set should not contain 'foo'") + } +} diff --git a/src/github.com/coreos/go-systemd/dbus/subscription.go b/src/github.com/coreos/go-systemd/dbus/subscription.go new file mode 100644 index 0000000..f538fdb --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/subscription.go @@ -0,0 +1,249 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +import ( + "errors" + "time" + + "github.com/guelfey/go.dbus" +) + +const ( + cleanIgnoreInterval = int64(10 * time.Second) + ignoreInterval = int64(30 * time.Millisecond) +) + +// 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='UnitNew'") + c.sysconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, + "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'") + + err := c.sysobj.Call("org.freedesktop.systemd1.Manager.Subscribe", 0).Store() + if err != nil { + c.sysconn.Close() + return err + } + + 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 +} + +func (c *Conn) initSubscription() { + c.subscriber.ignore = make(map[dbus.ObjectPath]int64) +} + +func (c *Conn) initDispatch() { + ch := make(chan *dbus.Signal, signalBuffer) + + c.sysconn.Signal(ch) + + go func() { + for { + signal := <-ch + switch signal.Name { + case "org.freedesktop.systemd1.Manager.JobRemoved": + c.jobComplete(signal) + + unitName := signal.Body[2].(string) + var unitPath dbus.ObjectPath + c.sysobj.Call("org.freedesktop.systemd1.Manager.GetUnit", 0, unitName).Store(&unitPath) + if unitPath != dbus.ObjectPath("") { + c.sendSubStateUpdate(unitPath) + } + case "org.freedesktop.systemd1.Manager.UnitNew": + c.sendSubStateUpdate(signal.Body[1].(dbus.ObjectPath)) + case "org.freedesktop.DBus.Properties.PropertiesChanged": + if signal.Body[0].(string) == "org.freedesktop.systemd1.Unit" { + // we only care about SubState updates, which are a Unit property + c.sendSubStateUpdate(signal.Path) + } + } + } + }() +} + +// Returns two unbuffered channels which will receive all changed units every +// interval. Deleted units are sent as nil. +func (c *Conn) SubscribeUnits(interval time.Duration) (<-chan map[string]*UnitStatus, <-chan error) { + return c.SubscribeUnitsCustom(interval, 0, func(u1, u2 *UnitStatus) bool { return *u1 != *u2 }, nil) +} + +// SubscribeUnitsCustom is like SubscribeUnits but lets you specify the buffer +// size of the channels, the comparison function for detecting changes and a filter +// function for cutting down on the noise that your channel receives. +func (c *Conn) SubscribeUnitsCustom(interval time.Duration, buffer int, isChanged func(*UnitStatus, *UnitStatus) bool, filterUnit func (string) bool) (<-chan map[string]*UnitStatus, <-chan error) { + old := make(map[string]*UnitStatus) + statusChan := make(chan map[string]*UnitStatus, buffer) + errChan := make(chan error, buffer) + + go func() { + for { + timerChan := time.After(interval) + + units, err := c.ListUnits() + if err == nil { + cur := make(map[string]*UnitStatus) + for i := range units { + if filterUnit != nil && filterUnit(units[i].Name) { + continue + } + cur[units[i].Name] = &units[i] + } + + // add all new or changed units + changed := make(map[string]*UnitStatus) + for n, u := range cur { + if oldU, ok := old[n]; !ok || isChanged(oldU, u) { + changed[n] = u + } + delete(old, n) + } + + // add all deleted units + for oldN := range old { + changed[oldN] = nil + } + + old = cur + + if len(changed) != 0 { + statusChan <- changed + } + } else { + errChan <- err + } + + <-timerChan + } + }() + + return statusChan, errChan +} + +type SubStateUpdate struct { + UnitName string + SubState string +} + +// SetSubStateSubscriber writes to updateCh when any unit's substate changes. +// 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 +// show every state transition they go through. Furthermore, state changes +// will only be written to the channel with non-blocking writes. If updateCh +// is full, it attempts to write an error to errCh; if errCh is full, the error +// passes silently. +func (c *Conn) SetSubStateSubscriber(updateCh chan<- *SubStateUpdate, errCh chan<- error) { + c.subscriber.Lock() + defer c.subscriber.Unlock() + c.subscriber.updateCh = updateCh + c.subscriber.errCh = errCh +} + +func (c *Conn) sendSubStateUpdate(path dbus.ObjectPath) { + c.subscriber.Lock() + defer c.subscriber.Unlock() + if c.subscriber.updateCh == nil { + return + } + + if c.shouldIgnore(path) { + return + } + + info, err := c.GetUnitProperties(string(path)) + if err != nil { + select { + case c.subscriber.errCh <- err: + default: + } + } + + name := info["Id"].(string) + substate := info["SubState"].(string) + + update := &SubStateUpdate{name, substate} + select { + case c.subscriber.updateCh <- update: + default: + select { + case c.subscriber.errCh <- errors.New("update channel full!"): + default: + } + } + + c.updateIgnore(path, info) +} + +// The ignore functions work around a wart in the systemd dbus interface. +// Requesting the properties of an unloaded unit will cause systemd to send a +// pair of UnitNew/UnitRemoved signals. Because we need to get a unit's +// properties on UnitNew (as that's the only indication of a new unit coming up +// for the first time), we would enter an infinite loop if we did not attempt +// to detect and ignore these spurious signals. The signal themselves are +// indistinguishable from relevant ones, so we (somewhat hackishly) ignore an +// unloaded unit's signals for a short time after requesting its properties. +// This means that we will miss e.g. a transient unit being restarted +// *immediately* upon failure and also a transient unit being started +// immediately after requesting its status (with systemctl status, for example, +// because this causes a UnitNew signal to be sent which then causes us to fetch +// the properties). + +func (c *Conn) shouldIgnore(path dbus.ObjectPath) bool { + t, ok := c.subscriber.ignore[path] + return ok && t >= time.Now().UnixNano() +} + +func (c *Conn) updateIgnore(path dbus.ObjectPath, info map[string]interface{}) { + c.cleanIgnore() + + // unit is unloaded - it will trigger bad systemd dbus behavior + if info["LoadState"].(string) == "not-found" { + c.subscriber.ignore[path] = time.Now().UnixNano() + ignoreInterval + } +} + +// without this, ignore would grow unboundedly over time +func (c *Conn) cleanIgnore() { + now := time.Now().UnixNano() + if c.subscriber.cleanIgnore < now { + c.subscriber.cleanIgnore = now + cleanIgnoreInterval + + for p, t := range c.subscriber.ignore { + if t < now { + delete(c.subscriber.ignore, p) + } + } + } +} diff --git a/src/github.com/coreos/go-systemd/dbus/subscription_set.go b/src/github.com/coreos/go-systemd/dbus/subscription_set.go new file mode 100644 index 0000000..2625786 --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/subscription_set.go @@ -0,0 +1,32 @@ +package dbus + +import ( + "time" +) + +// SubscriptionSet returns a subscription set which is like conn.Subscribe but +// can filter to only return events for a set of units. +type SubscriptionSet struct { + *set + conn *Conn +} + + +func (s *SubscriptionSet) filter(unit string) bool { + return !s.Contains(unit) +} + +// Subscribe starts listening for dbus events for all of the units in the set. +// Returns channels identical to conn.SubscribeUnits. +func (s *SubscriptionSet) Subscribe() (<-chan map[string]*UnitStatus, <-chan error) { + // TODO: Make fully evented by using systemd 209 with properties changed values + return s.conn.SubscribeUnitsCustom(time.Second, 0, + func(u1, u2 *UnitStatus) bool { return *u1 != *u2 }, + func(unit string) bool { return s.filter(unit) }, + ) +} + +// NewSubscriptionSet returns a new subscription set. +func (conn *Conn) NewSubscriptionSet() (*SubscriptionSet) { + return &SubscriptionSet{newSet(), conn} +} diff --git a/src/github.com/coreos/go-systemd/dbus/subscription_set_test.go b/src/github.com/coreos/go-systemd/dbus/subscription_set_test.go new file mode 100644 index 0000000..db60085 --- /dev/null +++ b/src/github.com/coreos/go-systemd/dbus/subscription_set_test.go @@ -0,0 +1,67 @@ +package dbus + +import ( + "testing" + "time" +) + +// TestSubscribeUnit exercises the basics of subscription of a particular unit. +func TestSubscriptionSetUnit(t *testing.T) { + target := "subscribe-events-set.service" + + conn, err := New() + + if err != nil { + t.Fatal(err) + } + + err = conn.Subscribe() + if err != nil { + t.Fatal(err) + } + + subSet := conn.NewSubscriptionSet() + evChan, errChan := subSet.Subscribe() + + subSet.Add(target) + 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] + + if !ok { + t.Fatal("Unexpected event %v", changes) + } + + 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/src/github.com/coreos/go-systemd/dbus/subscription_test.go b/src/github.com/coreos/go-systemd/dbus/subscription_test.go new file mode 100644 index 0000000..6f4d0b3 --- /dev/null +++ b/src/github.com/coreos/go-systemd/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/src/github.com/coreos/go-systemd/examples/activation/activation.go b/src/github.com/coreos/go-systemd/examples/activation/activation.go new file mode 100644 index 0000000..b3cf70e --- /dev/null +++ b/src/github.com/coreos/go-systemd/examples/activation/activation.go @@ -0,0 +1,44 @@ +// Activation example used by the activation unit tests. +package main + +import ( + "fmt" + "os" + + "github.com/coreos/go-systemd/activation" +) + +func fixListenPid() { + if os.Getenv("FIX_LISTEN_PID") != "" { + // HACK: real systemd would set LISTEN_PID before exec'ing but + // this is too difficult in golang for the purpose of a test. + // Do not do this in real code. + os.Setenv("LISTEN_PID", fmt.Sprintf("%d", os.Getpid())) + } +} + +func main() { + fixListenPid() + + files := activation.Files(false) + + if len(files) == 0 { + panic("No files") + } + + if os.Getenv("LISTEN_PID") == "" || os.Getenv("LISTEN_FDS") == "" { + panic("Should not unset envs") + } + + files = activation.Files(true) + + if os.Getenv("LISTEN_PID") != "" || os.Getenv("LISTEN_FDS") != "" { + panic("Can not unset envs") + } + + // Write out the expected strings to the two pipes + files[0].Write([]byte("Hello world")) + files[1].Write([]byte("Goodbye world")) + + return +} diff --git a/src/github.com/coreos/go-systemd/examples/activation/httpserver/README.md b/src/github.com/coreos/go-systemd/examples/activation/httpserver/README.md new file mode 100644 index 0000000..a350cca --- /dev/null +++ b/src/github.com/coreos/go-systemd/examples/activation/httpserver/README.md @@ -0,0 +1,19 @@ +## socket activated http server + +This is a simple example of using socket activation with systemd to serve a +simple HTTP server on http://127.0.0.1:8076 + +To try it out `go get` the httpserver and run it under the systemd-activate helper + +``` +export GOPATH=`pwd` +go get github.com/coreos/go-systemd/examples/activation/httpserver +sudo /usr/lib/systemd/systemd-activate -l 127.0.0.1:8076 ./bin/httpserver +``` + +Then curl the URL and you will notice that it starts up: + +``` +curl 127.0.0.1:8076 +hello socket activated world! +``` diff --git a/src/github.com/coreos/go-systemd/examples/activation/httpserver/hello.service b/src/github.com/coreos/go-systemd/examples/activation/httpserver/hello.service new file mode 100644 index 0000000..c8dea0f --- /dev/null +++ b/src/github.com/coreos/go-systemd/examples/activation/httpserver/hello.service @@ -0,0 +1,11 @@ +[Unit] +Description=Hello World HTTP +Requires=network.target +After=multi-user.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/httpserver + +[Install] +WantedBy=multi-user.target diff --git a/src/github.com/coreos/go-systemd/examples/activation/httpserver/hello.socket b/src/github.com/coreos/go-systemd/examples/activation/httpserver/hello.socket new file mode 100644 index 0000000..723ed7e --- /dev/null +++ b/src/github.com/coreos/go-systemd/examples/activation/httpserver/hello.socket @@ -0,0 +1,5 @@ +[Socket] +ListenStream=127.0.0.1:8076 + +[Install] +WantedBy=sockets.target diff --git a/src/github.com/coreos/go-systemd/examples/activation/httpserver/httpserver.go b/src/github.com/coreos/go-systemd/examples/activation/httpserver/httpserver.go new file mode 100644 index 0000000..380c325 --- /dev/null +++ b/src/github.com/coreos/go-systemd/examples/activation/httpserver/httpserver.go @@ -0,0 +1,26 @@ +package main + +import ( + "io" + "net/http" + + "github.com/coreos/go-systemd/activation" +) + +func HelloServer(w http.ResponseWriter, req *http.Request) { + io.WriteString(w, "hello socket activated world!\n") +} + +func main() { + listeners, err := activation.Listeners(true) + if err != nil { + panic(err) + } + + if len(listeners) != 1 { + panic("Unexpected number of socket activation fds") + } + + http.HandleFunc("/", HelloServer) + http.Serve(listeners[0], nil) +} diff --git a/src/github.com/coreos/go-systemd/examples/activation/listen.go b/src/github.com/coreos/go-systemd/examples/activation/listen.go new file mode 100644 index 0000000..5850a8b --- /dev/null +++ b/src/github.com/coreos/go-systemd/examples/activation/listen.go @@ -0,0 +1,50 @@ +// Activation example used by the activation unit tests. +package main + +import ( + "fmt" + "os" + + "github.com/coreos/go-systemd/activation" +) + +func fixListenPid() { + if os.Getenv("FIX_LISTEN_PID") != "" { + // HACK: real systemd would set LISTEN_PID before exec'ing but + // this is too difficult in golang for the purpose of a test. + // Do not do this in real code. + os.Setenv("LISTEN_PID", fmt.Sprintf("%d", os.Getpid())) + } +} + +func main() { + fixListenPid() + + listeners, _ := activation.Listeners(false) + + if len(listeners) == 0 { + panic("No listeners") + } + + if os.Getenv("LISTEN_PID") == "" || os.Getenv("LISTEN_FDS") == "" { + panic("Should not unset envs") + } + + listeners, err := activation.Listeners(true) + if err != nil { + panic(err) + } + + if os.Getenv("LISTEN_PID") != "" || os.Getenv("LISTEN_FDS") != "" { + panic("Can not unset envs") + } + + c0, _ := listeners[0].Accept() + c1, _ := listeners[1].Accept() + + // Write out the expected strings to the two pipes + c0.Write([]byte("Hello world")) + c1.Write([]byte("Goodbye world")) + + return +} diff --git a/src/github.com/coreos/go-systemd/fixtures/start-stop.service b/src/github.com/coreos/go-systemd/fixtures/start-stop.service new file mode 100644 index 0000000..a1f8c36 --- /dev/null +++ b/src/github.com/coreos/go-systemd/fixtures/start-stop.service @@ -0,0 +1,5 @@ +[Unit] +Description=start stop test + +[Service] +ExecStart=/bin/sleep 400 diff --git a/src/github.com/coreos/go-systemd/fixtures/subscribe-events-set.service b/src/github.com/coreos/go-systemd/fixtures/subscribe-events-set.service new file mode 100644 index 0000000..a1f8c36 --- /dev/null +++ b/src/github.com/coreos/go-systemd/fixtures/subscribe-events-set.service @@ -0,0 +1,5 @@ +[Unit] +Description=start stop test + +[Service] +ExecStart=/bin/sleep 400 diff --git a/src/github.com/coreos/go-systemd/fixtures/subscribe-events.service b/src/github.com/coreos/go-systemd/fixtures/subscribe-events.service new file mode 100644 index 0000000..a1f8c36 --- /dev/null +++ b/src/github.com/coreos/go-systemd/fixtures/subscribe-events.service @@ -0,0 +1,5 @@ +[Unit] +Description=start stop test + +[Service] +ExecStart=/bin/sleep 400 diff --git a/src/github.com/coreos/go-systemd/journal/send.go b/src/github.com/coreos/go-systemd/journal/send.go new file mode 100644 index 0000000..a29bcbf --- /dev/null +++ b/src/github.com/coreos/go-systemd/journal/send.go @@ -0,0 +1,168 @@ +/* +Copyright 2013 CoreOS Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package journal provides write bindings to the systemd journal +package journal + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "strconv" + "strings" + "syscall" +) + +// Priority of a journal message +type Priority int + +const ( + PriEmerg Priority = iota + PriAlert + PriCrit + PriErr + PriWarning + PriNotice + PriInfo + PriDebug +) + +var conn net.Conn + +func init() { + var err error + conn, err = net.Dial("unixgram", "/run/systemd/journal/socket") + if err != nil { + conn = nil + } +} + +// Enabled returns true iff the systemd journal is available for logging +func Enabled() bool { + return conn != nil +} + +// Send a message to the systemd journal. vars is a map of journald fields to +// values. Fields must be composed of uppercase letters, numbers, and +// underscores, but must not start with an underscore. Within these +// restrictions, any arbitrary field name may be used. Some names have special +// significance: see the journalctl documentation +// (http://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html) +// for more details. vars may be nil. +func Send(message string, priority Priority, vars map[string]string) error { + if conn == nil { + return journalError("could not connect to journald socket") + } + + data := new(bytes.Buffer) + appendVariable(data, "PRIORITY", strconv.Itoa(int(priority))) + appendVariable(data, "MESSAGE", message) + for k, v := range vars { + appendVariable(data, k, v) + } + + _, err := io.Copy(conn, data) + if err != nil && isSocketSpaceError(err) { + file, err := tempFd() + if err != nil { + return journalError(err.Error()) + } + _, err = io.Copy(file, data) + if err != nil { + return journalError(err.Error()) + } + + rights := syscall.UnixRights(int(file.Fd())) + + /* this connection should always be a UnixConn, but better safe than sorry */ + unixConn, ok := conn.(*net.UnixConn) + if !ok { + return journalError("can't send file through non-Unix connection") + } + unixConn.WriteMsgUnix([]byte{}, rights, nil) + } else if err != nil { + return journalError(err.Error()) + } + return nil +} + +func appendVariable(w io.Writer, name, value string) { + if !validVarName(name) { + journalError("variable name contains invalid character, ignoring") + } + if strings.ContainsRune(value, '\n') { + /* When the value contains a newline, we write: + * - the variable name, followed by a newline + * - the size (in 64bit little endian format) + * - the data, followed by a newline + */ + fmt.Fprintln(w, name) + binary.Write(w, binary.LittleEndian, uint64(len(value))) + fmt.Fprintln(w, value) + } else { + /* just write the variable and value all on one line */ + fmt.Fprintln(w, "%s=%s", name, value) + } +} + +func validVarName(name string) bool { + /* The variable name must be in uppercase and consist only of characters, + * numbers and underscores, and may not begin with an underscore. (from the docs) + */ + + valid := name[0] != '_' + for _, c := range name { + valid = valid && ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '_' + } + return valid +} + +func isSocketSpaceError(err error) bool { + opErr, ok := err.(*net.OpError) + if !ok { + return false + } + + sysErr, ok := opErr.Err.(syscall.Errno) + if !ok { + return false + } + + return sysErr == syscall.EMSGSIZE || sysErr == syscall.ENOBUFS +} + +func tempFd() (*os.File, error) { + file, err := ioutil.TempFile("/dev/shm/", "journal.XXXXX") + if err != nil { + return nil, err + } + syscall.Unlink(file.Name()) + if err != nil { + return nil, err + } + return file, nil +} + +func journalError(s string) error { + s = "journal error: " + s + fmt.Fprintln(os.Stderr, s) + return errors.New(s) +} diff --git a/src/github.com/coreos/go-systemd/test b/src/github.com/coreos/go-systemd/test new file mode 100755 index 0000000..6e04365 --- /dev/null +++ b/src/github.com/coreos/go-systemd/test @@ -0,0 +1,3 @@ +#!/bin/sh -e + +go test -v ./... diff --git a/src/github.com/guelfey/go.dbus b/src/github.com/guelfey/go.dbus deleted file mode 160000 index f6a3a23..0000000 --- a/src/github.com/guelfey/go.dbus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f6a3a2366cc39b8479cadc499d3c735fb10fbdda diff --git a/src/github.com/guelfey/go.dbus/LICENSE b/src/github.com/guelfey/go.dbus/LICENSE new file mode 100644 index 0000000..06b252b --- /dev/null +++ b/src/github.com/guelfey/go.dbus/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2013, Georg Reinke () +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/github.com/guelfey/go.dbus/README.markdown b/src/github.com/guelfey/go.dbus/README.markdown new file mode 100644 index 0000000..dc467fd --- /dev/null +++ b/src/github.com/guelfey/go.dbus/README.markdown @@ -0,0 +1,36 @@ +go.dbus +------- + +go.dbus is a simple library that implements native Go client bindings for the +D-Bus message bus system. + +### Features + +* Complete native implementation of the D-Bus message protocol +* Go-like API (channels for signals / asynchronous method calls, Goroutine-safe connections) +* Subpackages that help with the introspection / property interfaces + +### Installation + +This packages requires Go 1.1. If you installed it and set up your GOPATH, just run: + +``` +go get github.com/guelfey/go.dbus +``` + +If you want to use the subpackages, you can install them the same way. + +### Usage + +The complete package documentation and some simple examples are available at +[godoc.org](http://godoc.org/github.com/guelfey/go.dbus). Also, the +[_examples](https://github.com/guelfey/go.dbus/tree/master/_examples) directory +gives a short overview over the basic usage. + +Please note that the API is considered unstable for now and may change without +further notice. + +### License + +go.dbus is available under the Simplified BSD License; see LICENSE for the full +text. diff --git a/src/github.com/guelfey/go.dbus/_examples/eavesdrop.go b/src/github.com/guelfey/go.dbus/_examples/eavesdrop.go new file mode 100644 index 0000000..6f4512c --- /dev/null +++ b/src/github.com/guelfey/go.dbus/_examples/eavesdrop.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "github.com/guelfey/go.dbus" + "os" +) + +func main() { + conn, err := dbus.SessionBus() + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to connect to session bus:", err) + os.Exit(1) + } + + for _, v := range []string{"method_call", "method_return", "error", "signal"} { + call := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, + "eavesdrop='true',type='"+v+"'") + if call.Err != nil { + fmt.Fprintln(os.Stderr, "Failed to add match:", call.Err) + os.Exit(1) + } + } + c := make(chan *dbus.Message, 10) + conn.Eavesdrop(c) + fmt.Println("Listening for everything") + for v := range c { + fmt.Println(v) + } +} diff --git a/src/github.com/guelfey/go.dbus/_examples/introspect.go b/src/github.com/guelfey/go.dbus/_examples/introspect.go new file mode 100644 index 0000000..fb258ca --- /dev/null +++ b/src/github.com/guelfey/go.dbus/_examples/introspect.go @@ -0,0 +1,21 @@ +package main + +import ( + "encoding/json" + "github.com/guelfey/go.dbus" + "github.com/guelfey/go.dbus/introspect" + "os" +) + +func main() { + conn, err := dbus.SessionBus() + if err != nil { + panic(err) + } + node, err := introspect.Call(conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")) + if err != nil { + panic(err) + } + data, _ := json.MarshalIndent(node, "", " ") + os.Stdout.Write(data) +} diff --git a/src/github.com/guelfey/go.dbus/_examples/list-names.go b/src/github.com/guelfey/go.dbus/_examples/list-names.go new file mode 100644 index 0000000..c2f48ce --- /dev/null +++ b/src/github.com/guelfey/go.dbus/_examples/list-names.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "github.com/guelfey/go.dbus" + "os" +) + +func main() { + conn, err := dbus.SessionBus() + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to connect to session bus:", err) + os.Exit(1) + } + + var s []string + err = conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&s) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to get list of owned names:", err) + os.Exit(1) + } + + fmt.Println("Currently owned names on the session bus:") + for _, v := range s { + fmt.Println(v) + } +} diff --git a/src/github.com/guelfey/go.dbus/_examples/notification.go b/src/github.com/guelfey/go.dbus/_examples/notification.go new file mode 100644 index 0000000..2fbb4a5 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/_examples/notification.go @@ -0,0 +1,17 @@ +package main + +import "github.com/guelfey/go.dbus" + +func main() { + conn, err := dbus.SessionBus() + if err != nil { + panic(err) + } + obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + call := obj.Call("org.freedesktop.Notifications.Notify", 0, "", uint32(0), + "", "Test", "This is a test of the DBus bindings for go.", []string{}, + map[string]dbus.Variant{}, int32(5000)) + if call.Err != nil { + panic(call.Err) + } +} diff --git a/src/github.com/guelfey/go.dbus/_examples/prop.go b/src/github.com/guelfey/go.dbus/_examples/prop.go new file mode 100644 index 0000000..2ee553a --- /dev/null +++ b/src/github.com/guelfey/go.dbus/_examples/prop.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "github.com/guelfey/go.dbus" + "github.com/guelfey/go.dbus/introspect" + "github.com/guelfey/go.dbus/prop" + "os" +) + +type foo string + +func (f foo) Foo() (string, *dbus.Error) { + fmt.Println(f) + return string(f), nil +} + +func main() { + conn, err := dbus.SessionBus() + if err != nil { + panic(err) + } + reply, err := conn.RequestName("com.github.guelfey.Demo", + dbus.NameFlagDoNotQueue) + if err != nil { + panic(err) + } + if reply != dbus.RequestNameReplyPrimaryOwner { + fmt.Fprintln(os.Stderr, "name already taken") + os.Exit(1) + } + propsSpec := map[string]map[string]*prop.Prop{ + "com.github.guelfey.Demo": { + "SomeInt": { + int32(0), + true, + prop.EmitTrue, + func(c *prop.Change) *dbus.Error { + fmt.Println(c.Name, "changed to", c.Value) + return nil + }, + }, + }, + } + f := foo("Bar") + conn.Export(f, "/com/github/guelfey/Demo", "com.github.guelfey.Demo") + props := prop.New(conn, "/com/github/guelfey/Demo", propsSpec) + n := &introspect.Node{ + Name: "/com/github/guelfey/Demo", + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + { + Name: "com.github.guelfey.Demo", + Methods: introspect.Methods(f), + Properties: props.Introspection("com.github.guelfey.Demo"), + }, + }, + } + conn.Export(introspect.NewIntrospectable(n), "/com/github/guelfey/Demo", + "org.freedesktop.DBus.Introspectable") + fmt.Println("Listening on com.github.guelfey.Demo / /com/github/guelfey/Demo ...") + + c := make(chan *dbus.Signal) + conn.Signal(c) + for _ = range c { + } +} diff --git a/src/github.com/guelfey/go.dbus/_examples/server.go b/src/github.com/guelfey/go.dbus/_examples/server.go new file mode 100644 index 0000000..69fa287 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/_examples/server.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "github.com/guelfey/go.dbus" + "github.com/guelfey/go.dbus/introspect" + "os" +) + +const intro = ` + + + + + + ` + introspect.IntrospectDataString + ` ` + +type foo string + +func (f foo) Foo() (string, *dbus.Error) { + fmt.Println(f) + return string(f), nil +} + +func main() { + conn, err := dbus.SessionBus() + if err != nil { + panic(err) + } + reply, err := conn.RequestName("com.github.guelfey.Demo", + dbus.NameFlagDoNotQueue) + if err != nil { + panic(err) + } + if reply != dbus.RequestNameReplyPrimaryOwner { + fmt.Fprintln(os.Stderr, "name already taken") + os.Exit(1) + } + f := foo("Bar!") + conn.Export(f, "/com/github/guelfey/Demo", "com.github.guelfey.Demo") + conn.Export(introspect.Introspectable(intro), "/com/github/guelfey/Demo", + "org.freedesktop.DBus.Introspectable") + fmt.Println("Listening on com.github.guelfey.Demo / /com/github/guelfey/Demo ...") + select {} +} diff --git a/src/github.com/guelfey/go.dbus/_examples/signal.go b/src/github.com/guelfey/go.dbus/_examples/signal.go new file mode 100644 index 0000000..6373b64 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/_examples/signal.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "github.com/guelfey/go.dbus" + "os" +) + +func main() { + conn, err := dbus.SessionBus() + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to connect to session bus:", err) + os.Exit(1) + } + + conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, + "type='signal',path='/org/freedesktop/DBus',interface='org.freedesktop.DBus',sender='org.freedesktop.DBus'") + + c := make(chan *dbus.Signal, 10) + conn.Signal(c) + for v := range c { + fmt.Println(v) + } +} diff --git a/src/github.com/guelfey/go.dbus/auth.go b/src/github.com/guelfey/go.dbus/auth.go new file mode 100644 index 0000000..bf70a89 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/auth.go @@ -0,0 +1,255 @@ +package dbus + +import ( + "bufio" + "bytes" + "errors" + "io" + "os/user" +) + +// AuthStatus represents the Status of an authentication mechanism. +type AuthStatus byte + +const ( + // AuthOk signals that authentication is finished; the next command + // from the server should be an OK. + AuthOk AuthStatus = iota + + // AuthContinue signals that additional data is needed; the next command + // from the server should be a DATA. + AuthContinue + + // AuthError signals an error; the server sent invalid data or some + // other unexpected thing happened and the current authentication + // process should be aborted. + AuthError +) + +type authState byte + +const ( + waitingForData authState = iota + waitingForOk + waitingForReject +) + +// Auth defines the behaviour of an authentication mechanism. +type Auth interface { + // Return the name of the mechnism, the argument to the first AUTH command + // and the next status. + FirstData() (name, resp []byte, status AuthStatus) + + // Process the given DATA command, and return the argument to the DATA + // command and the next status. If len(resp) == 0, no DATA command is sent. + HandleData(data []byte) (resp []byte, status AuthStatus) +} + +// Auth authenticates the connection, trying the given list of authentication +// mechanisms (in that order). If nil is passed, the EXTERNAL and +// DBUS_COOKIE_SHA1 mechanisms are tried for the current user. For private +// connections, this method must be called before sending any messages to the +// bus. Auth must not be called on shared connections. +func (conn *Conn) Auth(methods []Auth) error { + if methods == nil { + u, err := user.Current() + if err != nil { + return err + } + methods = []Auth{AuthExternal(u.Username), AuthCookieSha1(u.Username, u.HomeDir)} + } + in := bufio.NewReader(conn.transport) + err := conn.transport.SendNullByte() + if err != nil { + return err + } + err = authWriteLine(conn.transport, []byte("AUTH")) + if err != nil { + return err + } + s, err := authReadLine(in) + if err != nil { + return err + } + if len(s) < 2 || !bytes.Equal(s[0], []byte("REJECTED")) { + return errors.New("dbus: authentication protocol error") + } + s = s[1:] + for _, v := range s { + for _, m := range methods { + if name, data, status := m.FirstData(); bytes.Equal(v, name) { + var ok bool + err = authWriteLine(conn.transport, []byte("AUTH"), []byte(v), data) + if err != nil { + return err + } + switch status { + case AuthOk: + err, ok = conn.tryAuth(m, waitingForOk, in) + case AuthContinue: + err, ok = conn.tryAuth(m, waitingForData, in) + default: + panic("dbus: invalid authentication status") + } + if err != nil { + return err + } + if ok { + if conn.transport.SupportsUnixFDs() { + err = authWriteLine(conn, []byte("NEGOTIATE_UNIX_FD")) + if err != nil { + return err + } + line, err := authReadLine(in) + if err != nil { + return err + } + switch { + case bytes.Equal(line[0], []byte("AGREE_UNIX_FD")): + conn.EnableUnixFDs() + conn.unixFD = true + case bytes.Equal(line[0], []byte("ERROR")): + default: + return errors.New("dbus: authentication protocol error") + } + } + err = authWriteLine(conn.transport, []byte("BEGIN")) + if err != nil { + return err + } + go conn.inWorker() + go conn.outWorker() + return nil + } + } + } + } + return errors.New("dbus: authentication failed") +} + +// tryAuth tries to authenticate with m as the mechanism, using state as the +// initial authState and in for reading input. It returns (nil, true) on +// success, (nil, false) on a REJECTED and (someErr, false) if some other +// error occured. +func (conn *Conn) tryAuth(m Auth, state authState, in *bufio.Reader) (error, bool) { + for { + s, err := authReadLine(in) + if err != nil { + return err, false + } + switch { + case state == waitingForData && string(s[0]) == "DATA": + if len(s) != 2 { + err = authWriteLine(conn.transport, []byte("ERROR")) + if err != nil { + return err, false + } + continue + } + data, status := m.HandleData(s[1]) + switch status { + case AuthOk, AuthContinue: + if len(data) != 0 { + err = authWriteLine(conn.transport, []byte("DATA"), data) + if err != nil { + return err, false + } + } + if status == AuthOk { + state = waitingForOk + } + case AuthError: + err = authWriteLine(conn.transport, []byte("ERROR")) + if err != nil { + return err, false + } + } + case state == waitingForData && string(s[0]) == "REJECTED": + return nil, false + case state == waitingForData && string(s[0]) == "ERROR": + err = authWriteLine(conn.transport, []byte("CANCEL")) + if err != nil { + return err, false + } + state = waitingForReject + case state == waitingForData && string(s[0]) == "OK": + if len(s) != 2 { + err = authWriteLine(conn.transport, []byte("CANCEL")) + if err != nil { + return err, false + } + state = waitingForReject + } + conn.uuid = string(s[1]) + return nil, true + case state == waitingForData: + err = authWriteLine(conn.transport, []byte("ERROR")) + if err != nil { + return err, false + } + case state == waitingForOk && string(s[0]) == "OK": + if len(s) != 2 { + err = authWriteLine(conn.transport, []byte("CANCEL")) + if err != nil { + return err, false + } + state = waitingForReject + } + conn.uuid = string(s[1]) + return nil, true + case state == waitingForOk && string(s[0]) == "REJECTED": + return nil, false + case state == waitingForOk && (string(s[0]) == "DATA" || + string(s[0]) == "ERROR"): + + err = authWriteLine(conn.transport, []byte("CANCEL")) + if err != nil { + return err, false + } + state = waitingForReject + case state == waitingForOk: + err = authWriteLine(conn.transport, []byte("ERROR")) + if err != nil { + return err, false + } + case state == waitingForReject && string(s[0]) == "REJECTED": + return nil, false + case state == waitingForReject: + return errors.New("dbus: authentication protocol error"), false + default: + panic("dbus: invalid auth state") + } + } +} + +// authReadLine reads a line and separates it into its fields. +func authReadLine(in *bufio.Reader) ([][]byte, error) { + data, err := in.ReadBytes('\n') + if err != nil { + return nil, err + } + data = bytes.TrimSuffix(data, []byte("\r\n")) + return bytes.Split(data, []byte{' '}), nil +} + +// authWriteLine writes the given line in the authentication protocol format +// (elements of data separated by a " " and terminated by "\r\n"). +func authWriteLine(out io.Writer, data ...[]byte) error { + buf := make([]byte, 0) + for i, v := range data { + buf = append(buf, v...) + if i != len(data)-1 { + buf = append(buf, ' ') + } + } + buf = append(buf, '\r') + buf = append(buf, '\n') + n, err := out.Write(buf) + if err != nil { + return err + } + if n != len(buf) { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/src/github.com/guelfey/go.dbus/auth_external.go b/src/github.com/guelfey/go.dbus/auth_external.go new file mode 100644 index 0000000..7e376d3 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/auth_external.go @@ -0,0 +1,26 @@ +package dbus + +import ( + "encoding/hex" +) + +// AuthExternal returns an Auth that authenticates as the given user with the +// EXTERNAL mechanism. +func AuthExternal(user string) Auth { + return authExternal{user} +} + +// AuthExternal implements the EXTERNAL authentication mechanism. +type authExternal struct { + user string +} + +func (a authExternal) FirstData() ([]byte, []byte, AuthStatus) { + b := make([]byte, 2*len(a.user)) + hex.Encode(b, []byte(a.user)) + return []byte("EXTERNAL"), b, AuthOk +} + +func (a authExternal) HandleData(b []byte) ([]byte, AuthStatus) { + return nil, AuthError +} diff --git a/src/github.com/guelfey/go.dbus/auth_sha1.go b/src/github.com/guelfey/go.dbus/auth_sha1.go new file mode 100644 index 0000000..df15b46 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/auth_sha1.go @@ -0,0 +1,102 @@ +package dbus + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/hex" + "os" +) + +// AuthCookieSha1 returns an Auth that authenticates as the given user with the +// DBUS_COOKIE_SHA1 mechanism. The home parameter should specify the home +// directory of the user. +func AuthCookieSha1(user, home string) Auth { + return authCookieSha1{user, home} +} + +type authCookieSha1 struct { + user, home string +} + +func (a authCookieSha1) FirstData() ([]byte, []byte, AuthStatus) { + b := make([]byte, 2*len(a.user)) + hex.Encode(b, []byte(a.user)) + return []byte("DBUS_COOKIE_SHA1"), b, AuthContinue +} + +func (a authCookieSha1) HandleData(data []byte) ([]byte, AuthStatus) { + challenge := make([]byte, len(data)/2) + _, err := hex.Decode(challenge, data) + if err != nil { + return nil, AuthError + } + b := bytes.Split(challenge, []byte{' '}) + if len(b) != 3 { + return nil, AuthError + } + context := b[0] + id := b[1] + svchallenge := b[2] + cookie := a.getCookie(context, id) + if cookie == nil { + return nil, AuthError + } + clchallenge := a.generateChallenge() + if clchallenge == nil { + return nil, AuthError + } + hash := sha1.New() + hash.Write(bytes.Join([][]byte{svchallenge, clchallenge, cookie}, []byte{':'})) + hexhash := make([]byte, 2*hash.Size()) + hex.Encode(hexhash, hash.Sum(nil)) + data = append(clchallenge, ' ') + data = append(data, hexhash...) + resp := make([]byte, 2*len(data)) + hex.Encode(resp, data) + return resp, AuthOk +} + +// getCookie searches for the cookie identified by id in context and returns +// the cookie content or nil. (Since HandleData can't return a specific error, +// but only whether an error occured, this function also doesn't bother to +// return an error.) +func (a authCookieSha1) getCookie(context, id []byte) []byte { + file, err := os.Open(a.home + "/.dbus-keyrings/" + string(context)) + if err != nil { + return nil + } + defer file.Close() + rd := bufio.NewReader(file) + for { + line, err := rd.ReadBytes('\n') + if err != nil { + return nil + } + line = line[:len(line)-1] + b := bytes.Split(line, []byte{' '}) + if len(b) != 3 { + return nil + } + if bytes.Equal(b[0], id) { + return b[2] + } + } +} + +// generateChallenge returns a random, hex-encoded challenge, or nil on error +// (see above). +func (a authCookieSha1) generateChallenge() []byte { + b := make([]byte, 16) + n, err := rand.Read(b) + if err != nil { + return nil + } + if n != 16 { + return nil + } + enc := make([]byte, 32) + hex.Encode(enc, b) + return enc +} diff --git a/src/github.com/guelfey/go.dbus/call.go b/src/github.com/guelfey/go.dbus/call.go new file mode 100644 index 0000000..1d2fbc7 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/call.go @@ -0,0 +1,147 @@ +package dbus + +import ( + "errors" + "strings" +) + +// Call represents a pending or completed method call. +type Call struct { + Destination string + Path ObjectPath + Method string + Args []interface{} + + // Strobes when the call is complete. + Done chan *Call + + // After completion, the error status. If this is non-nil, it may be an + // error message from the peer (with Error as its type) or some other error. + Err error + + // Holds the response once the call is done. + Body []interface{} +} + +var errSignature = errors.New("dbus: mismatched signature") + +// Store stores the body of the reply into the provided pointers. It returns +// an error if the signatures of the body and retvalues don't match, or if +// the error status is not nil. +func (c *Call) Store(retvalues ...interface{}) error { + if c.Err != nil { + return c.Err + } + + return Store(c.Body, retvalues...) +} + +// Object represents a remote object on which methods can be invoked. +type Object struct { + conn *Conn + dest string + path ObjectPath +} + +// Call calls a method with (*Object).Go and waits for its reply. +func (o *Object) Call(method string, flags Flags, args ...interface{}) *Call { + return <-o.Go(method, flags, make(chan *Call, 1), args...).Done +} + +// GetProperty calls org.freedesktop.DBus.Properties.GetProperty on the given +// object. The property name must be given in interface.member notation. +func (o *Object) GetProperty(p string) (Variant, error) { + idx := strings.LastIndex(p, ".") + if idx == -1 || idx+1 == len(p) { + return Variant{}, errors.New("dbus: invalid property " + p) + } + + iface := p[:idx] + prop := p[idx+1:] + + result := Variant{} + err := o.Call("org.freedesktop.DBus.Properties.Get", 0, iface, prop).Store(&result) + + if err != nil { + return Variant{}, err + } + + return result, nil +} + +// Go calls a method with the given arguments asynchronously. It returns a +// Call structure representing this method call. The passed channel will +// return the same value once the call is done. If ch is nil, a new channel +// will be allocated. Otherwise, ch has to be buffered or Go will panic. +// +// If the flags include FlagNoReplyExpected, ch is ignored and a Call structure +// is returned of which only the Err member is valid. +// +// If the method parameter contains a dot ('.'), the part before the last dot +// specifies the interface on which the method is called. +func (o *Object) Go(method string, flags Flags, ch chan *Call, args ...interface{}) *Call { + iface := "" + i := strings.LastIndex(method, ".") + if i != -1 { + iface = method[:i] + } + method = method[i+1:] + msg := new(Message) + msg.Type = TypeMethodCall + msg.serial = o.conn.getSerial() + msg.Flags = flags & (FlagNoAutoStart | FlagNoReplyExpected) + msg.Headers = make(map[HeaderField]Variant) + msg.Headers[FieldPath] = MakeVariant(o.path) + msg.Headers[FieldDestination] = MakeVariant(o.dest) + msg.Headers[FieldMember] = MakeVariant(method) + if iface != "" { + msg.Headers[FieldInterface] = MakeVariant(iface) + } + msg.Body = args + if len(args) > 0 { + msg.Headers[FieldSignature] = MakeVariant(SignatureOf(args...)) + } + if msg.Flags&FlagNoReplyExpected == 0 { + if ch == nil { + ch = make(chan *Call, 10) + } else if cap(ch) == 0 { + panic("dbus: unbuffered channel passed to (*Object).Go") + } + call := &Call{ + Destination: o.dest, + Path: o.path, + Method: method, + Args: args, + Done: ch, + } + o.conn.callsLck.Lock() + o.conn.calls[msg.serial] = call + o.conn.callsLck.Unlock() + o.conn.outLck.RLock() + if o.conn.closed { + call.Err = ErrClosed + call.Done <- call + } else { + o.conn.out <- msg + } + o.conn.outLck.RUnlock() + return call + } + o.conn.outLck.RLock() + defer o.conn.outLck.RUnlock() + if o.conn.closed { + return &Call{Err: ErrClosed} + } + o.conn.out <- msg + return &Call{Err: nil} +} + +// Destination returns the destination that calls on o are sent to. +func (o *Object) Destination() string { + return o.dest +} + +// Path returns the path that calls on o are sent to. +func (o *Object) Path() ObjectPath { + return o.path +} diff --git a/src/github.com/guelfey/go.dbus/conn.go b/src/github.com/guelfey/go.dbus/conn.go new file mode 100644 index 0000000..b38f852 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/conn.go @@ -0,0 +1,597 @@ +package dbus + +import ( + "errors" + "io" + "os" + "reflect" + "strings" + "sync" +) + +const defaultSystemBusAddress = "unix:path=/var/run/dbus/system_bus_socket" + +var ( + systemBus *Conn + systemBusLck sync.Mutex + sessionBus *Conn + sessionBusLck sync.Mutex +) + +// ErrClosed is the error returned by calls on a closed connection. +var ErrClosed = errors.New("dbus: connection closed by user") + +// Conn represents a connection to a message bus (usually, the system or +// session bus). +// +// Connections are either shared or private. Shared connections +// are shared between calls to the functions that return them. As a result, +// the methods Close, Auth and Hello must not be called on them. +// +// Multiple goroutines may invoke methods on a connection simultaneously. +type Conn struct { + transport + + busObj *Object + unixFD bool + uuid string + + names []string + namesLck sync.RWMutex + + serialLck sync.Mutex + nextSerial uint32 + serialUsed map[uint32]bool + + calls map[uint32]*Call + callsLck sync.RWMutex + + handlers map[ObjectPath]map[string]interface{} + handlersLck sync.RWMutex + + out chan *Message + closed bool + outLck sync.RWMutex + + signals []chan<- *Signal + signalsLck sync.Mutex + + eavesdropped chan<- *Message + eavesdroppedLck sync.Mutex +} + +// SessionBus returns a shared connection to the session bus, connecting to it +// if not already done. +func SessionBus() (conn *Conn, err error) { + sessionBusLck.Lock() + defer sessionBusLck.Unlock() + if sessionBus != nil { + return sessionBus, nil + } + defer func() { + if conn != nil { + sessionBus = conn + } + }() + conn, err = SessionBusPrivate() + if err != nil { + return + } + if err = conn.Auth(nil); err != nil { + conn.Close() + conn = nil + return + } + if err = conn.Hello(); err != nil { + conn.Close() + conn = nil + } + return +} + +// SessionBusPrivate returns a new private connection to the session bus. +func SessionBusPrivate() (*Conn, error) { + address := os.Getenv("DBUS_SESSION_BUS_ADDRESS") + if address != "" && address != "autolaunch:" { + return Dial(address) + } + + return sessionBusPlatform() +} + +// SystemBus returns a shared connection to the system bus, connecting to it if +// not already done. +func SystemBus() (conn *Conn, err error) { + systemBusLck.Lock() + defer systemBusLck.Unlock() + if systemBus != nil { + return systemBus, nil + } + defer func() { + if conn != nil { + systemBus = conn + } + }() + conn, err = SystemBusPrivate() + if err != nil { + return + } + if err = conn.Auth(nil); err != nil { + conn.Close() + conn = nil + return + } + if err = conn.Hello(); err != nil { + conn.Close() + conn = nil + } + return +} + +// SystemBusPrivate returns a new private connection to the system bus. +func SystemBusPrivate() (*Conn, error) { + address := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") + if address != "" { + return Dial(address) + } + return Dial(defaultSystemBusAddress) +} + +// Dial establishes a new private connection to the message bus specified by address. +func Dial(address string) (*Conn, error) { + tr, err := getTransport(address) + if err != nil { + return nil, err + } + return newConn(tr) +} + +// NewConn creates a new private *Conn from an already established connection. +func NewConn(conn io.ReadWriteCloser) (*Conn, error) { + return newConn(genericTransport{conn}) +} + +// newConn creates a new *Conn from a transport. +func newConn(tr transport) (*Conn, error) { + conn := new(Conn) + conn.transport = tr + conn.calls = make(map[uint32]*Call) + conn.out = make(chan *Message, 10) + conn.handlers = make(map[ObjectPath]map[string]interface{}) + conn.nextSerial = 1 + conn.serialUsed = map[uint32]bool{0: true} + conn.busObj = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus") + return conn, nil +} + +// BusObject returns the object owned by the bus daemon which handles +// administrative requests. +func (conn *Conn) BusObject() *Object { + return conn.busObj +} + +// Close closes the connection. Any blocked operations will return with errors +// and the channels passed to Eavesdrop and Signal are closed. This method must +// not be called on shared connections. +func (conn *Conn) Close() error { + conn.outLck.Lock() + close(conn.out) + conn.closed = true + conn.outLck.Unlock() + conn.signalsLck.Lock() + for _, ch := range conn.signals { + close(ch) + } + conn.signalsLck.Unlock() + conn.eavesdroppedLck.Lock() + if conn.eavesdropped != nil { + close(conn.eavesdropped) + } + conn.eavesdroppedLck.Unlock() + return conn.transport.Close() +} + +// Eavesdrop causes conn to send all incoming messages to the given channel +// without further processing. Method replies, errors and signals will not be +// sent to the appropiate channels and method calls will not be handled. If nil +// is passed, the normal behaviour is restored. +// +// The caller has to make sure that ch is sufficiently buffered; +// if a message arrives when a write to ch is not possible, the message is +// discarded. +func (conn *Conn) Eavesdrop(ch chan<- *Message) { + conn.eavesdroppedLck.Lock() + conn.eavesdropped = ch + conn.eavesdroppedLck.Unlock() +} + +// getSerial returns an unused serial. +func (conn *Conn) getSerial() uint32 { + conn.serialLck.Lock() + defer conn.serialLck.Unlock() + n := conn.nextSerial + for conn.serialUsed[n] { + n++ + } + conn.serialUsed[n] = true + conn.nextSerial = n + 1 + return n +} + +// Hello sends the initial org.freedesktop.DBus.Hello call. This method must be +// called after authentication, but before sending any other messages to the +// bus. Hello must not be called for shared connections. +func (conn *Conn) Hello() error { + var s string + err := conn.busObj.Call("org.freedesktop.DBus.Hello", 0).Store(&s) + if err != nil { + return err + } + conn.namesLck.Lock() + conn.names = make([]string, 1) + conn.names[0] = s + conn.namesLck.Unlock() + return nil +} + +// inWorker runs in an own goroutine, reading incoming messages from the +// transport and dispatching them appropiately. +func (conn *Conn) inWorker() { + for { + msg, err := conn.ReadMessage() + if err == nil { + conn.eavesdroppedLck.Lock() + if conn.eavesdropped != nil { + select { + case conn.eavesdropped <- msg: + default: + } + conn.eavesdroppedLck.Unlock() + continue + } + conn.eavesdroppedLck.Unlock() + dest, _ := msg.Headers[FieldDestination].value.(string) + found := false + if dest == "" { + found = true + } else { + conn.namesLck.RLock() + if len(conn.names) == 0 { + found = true + } + for _, v := range conn.names { + if dest == v { + found = true + break + } + } + conn.namesLck.RUnlock() + } + if !found { + // Eavesdropped a message, but no channel for it is registered. + // Ignore it. + continue + } + switch msg.Type { + case TypeMethodReply, TypeError: + serial := msg.Headers[FieldReplySerial].value.(uint32) + conn.callsLck.Lock() + if c, ok := conn.calls[serial]; ok { + if msg.Type == TypeError { + name, _ := msg.Headers[FieldErrorName].value.(string) + c.Err = Error{name, msg.Body} + } else { + c.Body = msg.Body + } + c.Done <- c + conn.serialLck.Lock() + delete(conn.serialUsed, serial) + conn.serialLck.Unlock() + delete(conn.calls, serial) + } + conn.callsLck.Unlock() + case TypeSignal: + iface := msg.Headers[FieldInterface].value.(string) + member := msg.Headers[FieldMember].value.(string) + // as per http://dbus.freedesktop.org/doc/dbus-specification.html , + // sender is optional for signals. + sender, _ := msg.Headers[FieldSender].value.(string) + if iface == "org.freedesktop.DBus" && member == "NameLost" && + sender == "org.freedesktop.DBus" { + + name, _ := msg.Body[0].(string) + conn.namesLck.Lock() + for i, v := range conn.names { + if v == name { + copy(conn.names[i:], conn.names[i+1:]) + conn.names = conn.names[:len(conn.names)-1] + } + } + conn.namesLck.Unlock() + } + signal := &Signal{ + Sender: sender, + Path: msg.Headers[FieldPath].value.(ObjectPath), + Name: iface + "." + member, + Body: msg.Body, + } + conn.signalsLck.Lock() + for _, ch := range conn.signals { + // don't block trying to send a signal + select { + case ch <- signal: + default: + } + } + conn.signalsLck.Unlock() + case TypeMethodCall: + go conn.handleCall(msg) + } + } else if _, ok := err.(InvalidMessageError); !ok { + // Some read error occured (usually EOF); we can't really do + // anything but to shut down all stuff and returns errors to all + // pending replies. + conn.Close() + conn.callsLck.RLock() + for _, v := range conn.calls { + v.Err = err + v.Done <- v + } + conn.callsLck.RUnlock() + return + } + // invalid messages are ignored + } +} + +// Names returns the list of all names that are currently owned by this +// connection. The slice is always at least one element long, the first element +// being the unique name of the connection. +func (conn *Conn) Names() []string { + conn.namesLck.RLock() + // copy the slice so it can't be modified + s := make([]string, len(conn.names)) + copy(s, conn.names) + conn.namesLck.RUnlock() + return s +} + +// Object returns the object identified by the given destination name and path. +func (conn *Conn) Object(dest string, path ObjectPath) *Object { + return &Object{conn, dest, path} +} + +// outWorker runs in an own goroutine, encoding and sending messages that are +// sent to conn.out. +func (conn *Conn) outWorker() { + for msg := range conn.out { + err := conn.SendMessage(msg) + conn.callsLck.RLock() + if err != nil { + if c := conn.calls[msg.serial]; c != nil { + c.Err = err + c.Done <- c + } + conn.serialLck.Lock() + delete(conn.serialUsed, msg.serial) + conn.serialLck.Unlock() + } else if msg.Type != TypeMethodCall { + conn.serialLck.Lock() + delete(conn.serialUsed, msg.serial) + conn.serialLck.Unlock() + } + conn.callsLck.RUnlock() + } +} + +// Send sends the given message to the message bus. You usually don't need to +// use this; use the higher-level equivalents (Call / Go, Emit and Export) +// instead. If msg is a method call and NoReplyExpected is not set, a non-nil +// call is returned and the same value is sent to ch (which must be buffered) +// once the call is complete. Otherwise, ch is ignored and a Call structure is +// returned of which only the Err member is valid. +func (conn *Conn) Send(msg *Message, ch chan *Call) *Call { + var call *Call + + msg.serial = conn.getSerial() + if msg.Type == TypeMethodCall && msg.Flags&FlagNoReplyExpected == 0 { + if ch == nil { + ch = make(chan *Call, 5) + } else if cap(ch) == 0 { + panic("dbus: unbuffered channel passed to (*Conn).Send") + } + call = new(Call) + call.Destination, _ = msg.Headers[FieldDestination].value.(string) + call.Path, _ = msg.Headers[FieldPath].value.(ObjectPath) + iface, _ := msg.Headers[FieldInterface].value.(string) + member, _ := msg.Headers[FieldMember].value.(string) + call.Method = iface + "." + member + call.Args = msg.Body + call.Done = ch + conn.callsLck.Lock() + conn.calls[msg.serial] = call + conn.callsLck.Unlock() + conn.outLck.RLock() + if conn.closed { + call.Err = ErrClosed + call.Done <- call + } else { + conn.out <- msg + } + conn.outLck.RUnlock() + } else { + conn.outLck.RLock() + if conn.closed { + call = &Call{Err: ErrClosed} + } else { + conn.out <- msg + call = &Call{Err: nil} + } + conn.outLck.RUnlock() + } + return call +} + +// sendError creates an error message corresponding to the parameters and sends +// it to conn.out. +func (conn *Conn) sendError(e Error, dest string, serial uint32) { + msg := new(Message) + msg.Type = TypeError + msg.serial = conn.getSerial() + msg.Headers = make(map[HeaderField]Variant) + msg.Headers[FieldDestination] = MakeVariant(dest) + msg.Headers[FieldErrorName] = MakeVariant(e.Name) + msg.Headers[FieldReplySerial] = MakeVariant(serial) + msg.Body = e.Body + if len(e.Body) > 0 { + msg.Headers[FieldSignature] = MakeVariant(SignatureOf(e.Body...)) + } + conn.outLck.RLock() + if !conn.closed { + conn.out <- msg + } + conn.outLck.RUnlock() +} + +// sendReply creates a method reply message corresponding to the parameters and +// sends it to conn.out. +func (conn *Conn) sendReply(dest string, serial uint32, values ...interface{}) { + msg := new(Message) + msg.Type = TypeMethodReply + msg.serial = conn.getSerial() + msg.Headers = make(map[HeaderField]Variant) + msg.Headers[FieldDestination] = MakeVariant(dest) + msg.Headers[FieldReplySerial] = MakeVariant(serial) + msg.Body = values + if len(values) > 0 { + msg.Headers[FieldSignature] = MakeVariant(SignatureOf(values...)) + } + conn.outLck.RLock() + if !conn.closed { + conn.out <- msg + } + conn.outLck.RUnlock() +} + +// Signal registers the given channel to be passed all received signal messages. +// The caller has to make sure that ch is sufficiently buffered; if a message +// arrives when a write to c is not possible, it is discarded. +// +// Multiple of these channels can be registered at the same time. Passing a +// channel that already is registered will remove it from the list of the +// registered channels. +// +// These channels are "overwritten" by Eavesdrop; i.e., if there currently is a +// channel for eavesdropped messages, this channel receives all signals, and +// none of the channels passed to Signal will receive any signals. +func (conn *Conn) Signal(ch chan<- *Signal) { + conn.signalsLck.Lock() + conn.signals = append(conn.signals, ch) + conn.signalsLck.Unlock() +} + +// SupportsUnixFDs returns whether the underlying transport supports passing of +// unix file descriptors. If this is false, method calls containing unix file +// descriptors will return an error and emitted signals containing them will +// not be sent. +func (conn *Conn) SupportsUnixFDs() bool { + return conn.unixFD +} + +// Error represents a D-Bus message of type Error. +type Error struct { + Name string + Body []interface{} +} + +func (e Error) Error() string { + if len(e.Body) >= 1 { + s, ok := e.Body[0].(string) + if ok { + return s + } + } + return e.Name +} + +// Signal represents a D-Bus message of type Signal. The name member is given in +// "interface.member" notation, e.g. org.freedesktop.D-Bus.NameLost. +type Signal struct { + Sender string + Path ObjectPath + Name string + Body []interface{} +} + +// transport is a D-Bus transport. +type transport interface { + // Read and Write raw data (for example, for the authentication protocol). + io.ReadWriteCloser + + // Send the initial null byte used for the EXTERNAL mechanism. + SendNullByte() error + + // Returns whether this transport supports passing Unix FDs. + SupportsUnixFDs() bool + + // Signal the transport that Unix FD passing is enabled for this connection. + EnableUnixFDs() + + // Read / send a message, handling things like Unix FDs. + ReadMessage() (*Message, error) + SendMessage(*Message) error +} + +func getTransport(address string) (transport, error) { + var err error + var t transport + + m := map[string]func(string) (transport, error){ + "unix": newUnixTransport, + } + addresses := strings.Split(address, ";") + for _, v := range addresses { + i := strings.IndexRune(v, ':') + if i == -1 { + err = errors.New("dbus: invalid bus address (no transport)") + continue + } + f := m[v[:i]] + if f == nil { + err = errors.New("dbus: invalid bus address (invalid or unsupported transport)") + } + t, err = f(v[i+1:]) + if err == nil { + return t, nil + } + } + return nil, err +} + +// dereferenceAll returns a slice that, assuming that vs is a slice of pointers +// of arbitrary types, containes the values that are obtained from dereferencing +// all elements in vs. +func dereferenceAll(vs []interface{}) []interface{} { + for i := range vs { + v := reflect.ValueOf(vs[i]) + v = v.Elem() + vs[i] = v.Interface() + } + return vs +} + +// getKey gets a key from a the list of keys. Returns "" on error / not found... +func getKey(s, key string) string { + i := strings.Index(s, key) + if i == -1 { + return "" + } + if i+len(key)+1 >= len(s) || s[i+len(key)] != '=' { + return "" + } + j := strings.Index(s, ",") + if j == -1 { + j = len(s) + } + return s[i+len(key)+1 : j] +} diff --git a/src/github.com/guelfey/go.dbus/conn_darwin.go b/src/github.com/guelfey/go.dbus/conn_darwin.go new file mode 100644 index 0000000..b67bb1b --- /dev/null +++ b/src/github.com/guelfey/go.dbus/conn_darwin.go @@ -0,0 +1,21 @@ +package dbus + +import ( + "errors" + "os/exec" +) + +func sessionBusPlatform() (*Conn, error) { + cmd := exec.Command("launchctl", "getenv", "DBUS_LAUNCHD_SESSION_BUS_SOCKET") + b, err := cmd.CombinedOutput() + + if err != nil { + return nil, err + } + + if len(b) == 0 { + return nil, errors.New("dbus: couldn't determine address of session bus") + } + + return Dial("unix:path=" + string(b[:len(b)-1])) +} diff --git a/src/github.com/guelfey/go.dbus/conn_other.go b/src/github.com/guelfey/go.dbus/conn_other.go new file mode 100644 index 0000000..f74b875 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/conn_other.go @@ -0,0 +1,27 @@ +// +build !darwin + +package dbus + +import ( + "bytes" + "errors" + "os/exec" +) + +func sessionBusPlatform() (*Conn, error) { + cmd := exec.Command("dbus-launch") + b, err := cmd.CombinedOutput() + + if err != nil { + return nil, err + } + + i := bytes.IndexByte(b, '=') + j := bytes.IndexByte(b, '\n') + + if i == -1 || j == -1 { + return nil, errors.New("dbus: couldn't determine address of session bus") + } + + return Dial(string(b[i+1 : j])) +} diff --git a/src/github.com/guelfey/go.dbus/conn_test.go b/src/github.com/guelfey/go.dbus/conn_test.go new file mode 100644 index 0000000..a2b14e8 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/conn_test.go @@ -0,0 +1,199 @@ +package dbus + +import "testing" + +func TestSessionBus(t *testing.T) { + _, err := SessionBus() + if err != nil { + t.Error(err) + } +} + +func TestSystemBus(t *testing.T) { + _, err := SystemBus() + if err != nil { + t.Error(err) + } +} + +func TestSend(t *testing.T) { + bus, err := SessionBus() + if err != nil { + t.Error(err) + } + ch := make(chan *Call, 1) + msg := &Message{ + Type: TypeMethodCall, + Flags: 0, + Headers: map[HeaderField]Variant{ + FieldDestination: MakeVariant(bus.Names()[0]), + FieldPath: MakeVariant(ObjectPath("/org/freedesktop/DBus")), + FieldInterface: MakeVariant("org.freedesktop.DBus.Peer"), + FieldMember: MakeVariant("Ping"), + }, + } + call := bus.Send(msg, ch) + <-ch + if call.Err != nil { + t.Error(call.Err) + } +} + +type server struct{} + +func (server) Double(i int64) (int64, *Error) { + return 2 * i, nil +} + +func BenchmarkCall(b *testing.B) { + b.StopTimer() + var s string + bus, err := SessionBus() + if err != nil { + b.Fatal(err) + } + name := bus.Names()[0] + obj := bus.BusObject() + b.StartTimer() + for i := 0; i < b.N; i++ { + err := obj.Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&s) + if err != nil { + b.Fatal(err) + } + if s != name { + b.Errorf("got %s, wanted %s", s, name) + } + } +} + +func BenchmarkCallAsync(b *testing.B) { + b.StopTimer() + bus, err := SessionBus() + if err != nil { + b.Fatal(err) + } + name := bus.Names()[0] + obj := bus.BusObject() + c := make(chan *Call, 50) + done := make(chan struct{}) + go func() { + for i := 0; i < b.N; i++ { + v := <-c + if v.Err != nil { + b.Error(v.Err) + } + s := v.Body[0].(string) + if s != name { + b.Errorf("got %s, wanted %s", s, name) + } + } + close(done) + }() + b.StartTimer() + for i := 0; i < b.N; i++ { + obj.Go("org.freedesktop.DBus.GetNameOwner", 0, c, name) + } + <-done +} + +func BenchmarkServe(b *testing.B) { + b.StopTimer() + srv, err := SessionBus() + if err != nil { + b.Fatal(err) + } + cli, err := SessionBusPrivate() + if err != nil { + b.Fatal(err) + } + if err = cli.Auth(nil); err != nil { + b.Fatal(err) + } + if err = cli.Hello(); err != nil { + b.Fatal(err) + } + benchmarkServe(b, srv, cli) +} + +func BenchmarkServeAsync(b *testing.B) { + b.StopTimer() + srv, err := SessionBus() + if err != nil { + b.Fatal(err) + } + cli, err := SessionBusPrivate() + if err != nil { + b.Fatal(err) + } + if err = cli.Auth(nil); err != nil { + b.Fatal(err) + } + if err = cli.Hello(); err != nil { + b.Fatal(err) + } + benchmarkServeAsync(b, srv, cli) +} + +func BenchmarkServeSameConn(b *testing.B) { + b.StopTimer() + bus, err := SessionBus() + if err != nil { + b.Fatal(err) + } + + benchmarkServe(b, bus, bus) +} + +func BenchmarkServeSameConnAsync(b *testing.B) { + b.StopTimer() + bus, err := SessionBus() + if err != nil { + b.Fatal(err) + } + + benchmarkServeAsync(b, bus, bus) +} + +func benchmarkServe(b *testing.B, srv, cli *Conn) { + var r int64 + var err error + dest := srv.Names()[0] + srv.Export(server{}, "/org/guelfey/DBus/Test", "org.guelfey.DBus.Test") + obj := cli.Object(dest, "/org/guelfey/DBus/Test") + b.StartTimer() + for i := 0; i < b.N; i++ { + err = obj.Call("org.guelfey.DBus.Test.Double", 0, int64(i)).Store(&r) + if err != nil { + b.Fatal(err) + } + if r != 2*int64(i) { + b.Errorf("got %d, wanted %d", r, 2*int64(i)) + } + } +} + +func benchmarkServeAsync(b *testing.B, srv, cli *Conn) { + dest := srv.Names()[0] + srv.Export(server{}, "/org/guelfey/DBus/Test", "org.guelfey.DBus.Test") + obj := cli.Object(dest, "/org/guelfey/DBus/Test") + c := make(chan *Call, 50) + done := make(chan struct{}) + go func() { + for i := 0; i < b.N; i++ { + v := <-c + if v.Err != nil { + b.Fatal(v.Err) + } + i, r := v.Args[0].(int64), v.Body[0].(int64) + if 2*i != r { + b.Errorf("got %d, wanted %d", r, 2*i) + } + } + close(done) + }() + b.StartTimer() + for i := 0; i < b.N; i++ { + obj.Go("org.guelfey.DBus.Test.Double", 0, c, int64(i)) + } + <-done +} diff --git a/src/github.com/guelfey/go.dbus/dbus.go b/src/github.com/guelfey/go.dbus/dbus.go new file mode 100644 index 0000000..2ce6873 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/dbus.go @@ -0,0 +1,258 @@ +package dbus + +import ( + "errors" + "reflect" + "strings" +) + +var ( + byteType = reflect.TypeOf(byte(0)) + boolType = reflect.TypeOf(false) + uint8Type = reflect.TypeOf(uint8(0)) + int16Type = reflect.TypeOf(int16(0)) + uint16Type = reflect.TypeOf(uint16(0)) + int32Type = reflect.TypeOf(int32(0)) + uint32Type = reflect.TypeOf(uint32(0)) + int64Type = reflect.TypeOf(int64(0)) + uint64Type = reflect.TypeOf(uint64(0)) + float64Type = reflect.TypeOf(float64(0)) + stringType = reflect.TypeOf("") + signatureType = reflect.TypeOf(Signature{""}) + objectPathType = reflect.TypeOf(ObjectPath("")) + variantType = reflect.TypeOf(Variant{Signature{""}, nil}) + interfacesType = reflect.TypeOf([]interface{}{}) + unixFDType = reflect.TypeOf(UnixFD(0)) + unixFDIndexType = reflect.TypeOf(UnixFDIndex(0)) +) + +// An InvalidTypeError signals that a value which cannot be represented in the +// D-Bus wire format was passed to a function. +type InvalidTypeError struct { + Type reflect.Type +} + +func (e InvalidTypeError) Error() string { + return "dbus: invalid type " + e.Type.String() +} + +// Store copies the values contained in src to dest, which must be a slice of +// pointers. It converts slices of interfaces from src to corresponding structs +// in dest. An error is returned if the lengths of src and dest or the types of +// their elements don't match. +func Store(src []interface{}, dest ...interface{}) error { + if len(src) != len(dest) { + return errors.New("dbus.Store: length mismatch") + } + + for i := range src { + if err := store(src[i], dest[i]); err != nil { + return err + } + } + return nil +} + +func store(src, dest interface{}) error { + if reflect.TypeOf(dest).Elem() == reflect.TypeOf(src) { + reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(src)) + return nil + } else if hasStruct(dest) { + rv := reflect.ValueOf(dest).Elem() + switch rv.Kind() { + case reflect.Struct: + vs, ok := src.([]interface{}) + if !ok { + return errors.New("dbus.Store: type mismatch") + } + t := rv.Type() + ndest := make([]interface{}, 0, rv.NumField()) + for i := 0; i < rv.NumField(); i++ { + field := t.Field(i) + if field.PkgPath == "" && field.Tag.Get("dbus") != "-" { + ndest = append(ndest, rv.Field(i).Addr().Interface()) + } + } + if len(vs) != len(ndest) { + return errors.New("dbus.Store: type mismatch") + } + err := Store(vs, ndest...) + if err != nil { + return errors.New("dbus.Store: type mismatch") + } + case reflect.Slice: + sv := reflect.ValueOf(src) + if sv.Kind() != reflect.Slice { + return errors.New("dbus.Store: type mismatch") + } + rv.Set(reflect.MakeSlice(rv.Type(), sv.Len(), sv.Len())) + for i := 0; i < sv.Len(); i++ { + if err := store(sv.Index(i).Interface(), rv.Index(i).Addr().Interface()); err != nil { + return err + } + } + case reflect.Map: + sv := reflect.ValueOf(src) + if sv.Kind() != reflect.Map { + return errors.New("dbus.Store: type mismatch") + } + keys := sv.MapKeys() + rv.Set(reflect.MakeMap(sv.Type())) + for _, key := range keys { + v := reflect.New(sv.Type().Elem()) + if err := store(v, sv.MapIndex(key).Interface()); err != nil { + return err + } + rv.SetMapIndex(key, v.Elem()) + } + default: + return errors.New("dbus.Store: type mismatch") + } + return nil + } else { + return errors.New("dbus.Store: type mismatch") + } +} + +func hasStruct(v interface{}) bool { + t := reflect.TypeOf(v) + for { + switch t.Kind() { + case reflect.Struct: + return true + case reflect.Slice, reflect.Ptr, reflect.Map: + t = t.Elem() + default: + return false + } + } +} + +// An ObjectPath is an object path as defined by the D-Bus spec. +type ObjectPath string + +// IsValid returns whether the object path is valid. +func (o ObjectPath) IsValid() bool { + s := string(o) + if len(s) == 0 { + return false + } + if s[0] != '/' { + return false + } + if s[len(s)-1] == '/' && len(s) != 1 { + return false + } + // probably not used, but technically possible + if s == "/" { + return true + } + split := strings.Split(s[1:], "/") + for _, v := range split { + if len(v) == 0 { + return false + } + for _, c := range v { + if !isMemberChar(c) { + return false + } + } + } + return true +} + +// A UnixFD is a Unix file descriptor sent over the wire. See the package-level +// documentation for more information about Unix file descriptor passsing. +type UnixFD int32 + +// A UnixFDIndex is the representation of a Unix file descriptor in a message. +type UnixFDIndex uint32 + +// alignment returns the alignment of values of type t. +func alignment(t reflect.Type) int { + switch t { + case variantType: + return 1 + case objectPathType: + return 4 + case signatureType: + return 1 + case interfacesType: // sometimes used for structs + return 8 + } + switch t.Kind() { + case reflect.Uint8: + return 1 + case reflect.Uint16, reflect.Int16: + return 2 + case reflect.Uint32, reflect.Int32, reflect.String, reflect.Array, reflect.Slice, reflect.Map: + return 4 + case reflect.Uint64, reflect.Int64, reflect.Float64, reflect.Struct: + return 8 + case reflect.Ptr: + return alignment(t.Elem()) + } + return 1 +} + +// isKeyType returns whether t is a valid type for a D-Bus dict. +func isKeyType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float64, + reflect.String: + + return true + } + return false +} + +// isValidInterface returns whether s is a valid name for an interface. +func isValidInterface(s string) bool { + if len(s) == 0 || len(s) > 255 || s[0] == '.' { + return false + } + elem := strings.Split(s, ".") + if len(elem) < 2 { + return false + } + for _, v := range elem { + if len(v) == 0 { + return false + } + if v[0] >= '0' && v[0] <= '9' { + return false + } + for _, c := range v { + if !isMemberChar(c) { + return false + } + } + } + return true +} + +// isValidMember returns whether s is a valid name for a member. +func isValidMember(s string) bool { + if len(s) == 0 || len(s) > 255 { + return false + } + i := strings.Index(s, ".") + if i != -1 { + return false + } + if s[0] >= '0' && s[0] <= '9' { + return false + } + for _, c := range s { + if !isMemberChar(c) { + return false + } + } + return true +} + +func isMemberChar(c rune) bool { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || c == '_' +} diff --git a/src/github.com/guelfey/go.dbus/decoder.go b/src/github.com/guelfey/go.dbus/decoder.go new file mode 100644 index 0000000..ef50dca --- /dev/null +++ b/src/github.com/guelfey/go.dbus/decoder.go @@ -0,0 +1,228 @@ +package dbus + +import ( + "encoding/binary" + "io" + "reflect" +) + +type decoder struct { + in io.Reader + order binary.ByteOrder + pos int +} + +// newDecoder returns a new decoder that reads values from in. The input is +// expected to be in the given byte order. +func newDecoder(in io.Reader, order binary.ByteOrder) *decoder { + dec := new(decoder) + dec.in = in + dec.order = order + return dec +} + +// align aligns the input to the given boundary and panics on error. +func (dec *decoder) align(n int) { + if dec.pos%n != 0 { + newpos := (dec.pos + n - 1) & ^(n - 1) + empty := make([]byte, newpos-dec.pos) + if _, err := io.ReadFull(dec.in, empty); err != nil { + panic(err) + } + dec.pos = newpos + } +} + +// Calls binary.Read(dec.in, dec.order, v) and panics on read errors. +func (dec *decoder) binread(v interface{}) { + if err := binary.Read(dec.in, dec.order, v); err != nil { + panic(err) + } +} + +func (dec *decoder) Decode(sig Signature) (vs []interface{}, err error) { + defer func() { + var ok bool + v := recover() + if err, ok = v.(error); ok { + if err == io.EOF || err == io.ErrUnexpectedEOF { + err = FormatError("unexpected EOF") + } + } + }() + vs = make([]interface{}, 0) + s := sig.str + for s != "" { + err, rem := validSingle(s, 0) + if err != nil { + return nil, err + } + v := dec.decode(s[:len(s)-len(rem)], 0) + vs = append(vs, v) + s = rem + } + return vs, nil +} + +func (dec *decoder) decode(s string, depth int) interface{} { + dec.align(alignment(typeFor(s))) + switch s[0] { + case 'y': + var b [1]byte + if _, err := dec.in.Read(b[:]); err != nil { + panic(err) + } + dec.pos++ + return b[0] + case 'b': + i := dec.decode("u", depth).(uint32) + switch { + case i == 0: + return false + case i == 1: + return true + default: + panic(FormatError("invalid value for boolean")) + } + case 'n': + var i int16 + dec.binread(&i) + dec.pos += 2 + return i + case 'i': + var i int32 + dec.binread(&i) + dec.pos += 4 + return i + case 'x': + var i int64 + dec.binread(&i) + dec.pos += 8 + return i + case 'q': + var i uint16 + dec.binread(&i) + dec.pos += 2 + return i + case 'u': + var i uint32 + dec.binread(&i) + dec.pos += 4 + return i + case 't': + var i uint64 + dec.binread(&i) + dec.pos += 8 + return i + case 'd': + var f float64 + dec.binread(&f) + dec.pos += 8 + return f + case 's': + length := dec.decode("u", depth).(uint32) + b := make([]byte, int(length)+1) + if _, err := io.ReadFull(dec.in, b); err != nil { + panic(err) + } + dec.pos += int(length) + 1 + return string(b[:len(b)-1]) + case 'o': + return ObjectPath(dec.decode("s", depth).(string)) + case 'g': + length := dec.decode("y", depth).(byte) + b := make([]byte, int(length)+1) + if _, err := io.ReadFull(dec.in, b); err != nil { + panic(err) + } + dec.pos += int(length) + 1 + sig, err := ParseSignature(string(b[:len(b)-1])) + if err != nil { + panic(err) + } + return sig + case 'v': + if depth >= 64 { + panic(FormatError("input exceeds container depth limit")) + } + var variant Variant + sig := dec.decode("g", depth).(Signature) + if len(sig.str) == 0 { + panic(FormatError("variant signature is empty")) + } + err, rem := validSingle(sig.str, 0) + if err != nil { + panic(err) + } + if rem != "" { + panic(FormatError("variant signature has multiple types")) + } + variant.sig = sig + variant.value = dec.decode(sig.str, depth+1) + return variant + case 'h': + return UnixFDIndex(dec.decode("u", depth).(uint32)) + case 'a': + if len(s) > 1 && s[1] == '{' { + ksig := s[2:3] + vsig := s[3 : len(s)-1] + v := reflect.MakeMap(reflect.MapOf(typeFor(ksig), typeFor(vsig))) + if depth >= 63 { + panic(FormatError("input exceeds container depth limit")) + } + length := dec.decode("u", depth).(uint32) + // Even for empty maps, the correct padding must be included + dec.align(8) + spos := dec.pos + for dec.pos < spos+int(length) { + dec.align(8) + if !isKeyType(v.Type().Key()) { + panic(InvalidTypeError{v.Type()}) + } + kv := dec.decode(ksig, depth+2) + vv := dec.decode(vsig, depth+2) + v.SetMapIndex(reflect.ValueOf(kv), reflect.ValueOf(vv)) + } + return v.Interface() + } + if depth >= 64 { + panic(FormatError("input exceeds container depth limit")) + } + length := dec.decode("u", depth).(uint32) + v := reflect.MakeSlice(reflect.SliceOf(typeFor(s[1:])), 0, int(length)) + // Even for empty arrays, the correct padding must be included + dec.align(alignment(typeFor(s[1:]))) + spos := dec.pos + for dec.pos < spos+int(length) { + ev := dec.decode(s[1:], depth+1) + v = reflect.Append(v, reflect.ValueOf(ev)) + } + return v.Interface() + case '(': + if depth >= 64 { + panic(FormatError("input exceeds container depth limit")) + } + dec.align(8) + v := make([]interface{}, 0) + s = s[1 : len(s)-1] + for s != "" { + err, rem := validSingle(s, 0) + if err != nil { + panic(err) + } + ev := dec.decode(s[:len(s)-len(rem)], depth+1) + v = append(v, ev) + s = rem + } + return v + default: + panic(SignatureError{Sig: s}) + } +} + +// A FormatError is an error in the wire format. +type FormatError string + +func (e FormatError) Error() string { + return "dbus: wire format error: " + string(e) +} diff --git a/src/github.com/guelfey/go.dbus/doc.go b/src/github.com/guelfey/go.dbus/doc.go new file mode 100644 index 0000000..deff554 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/doc.go @@ -0,0 +1,63 @@ +/* +Package dbus implements bindings to the D-Bus message bus system. + +To use the message bus API, you first need to connect to a bus (usually the +session or system bus). The acquired connection then can be used to call methods +on remote objects and emit or receive signals. Using the Export method, you can +arrange D-Bus methods calls to be directly translated to method calls on a Go +value. + +Conversion Rules + +For outgoing messages, Go types are automatically converted to the +corresponding D-Bus types. The following types are directly encoded as their +respective D-Bus equivalents: + + Go type | D-Bus type + ------------+----------- + byte | BYTE + bool | BOOLEAN + int16 | INT16 + uint16 | UINT16 + int32 | INT32 + uint32 | UINT32 + int64 | INT64 + uint64 | UINT64 + float64 | DOUBLE + string | STRING + ObjectPath | OBJECT_PATH + Signature | SIGNATURE + Variant | VARIANT + UnixFDIndex | UNIX_FD + +Slices and arrays encode as ARRAYs of their element type. + +Maps encode as DICTs, provided that their key type can be used as a key for +a DICT. + +Structs other than Variant and Signature encode as a STRUCT containing their +exported fields. Fields whose tags contain `dbus:"-"` and unexported fields will +be skipped. + +Pointers encode as the value they're pointed to. + +Trying to encode any other type or a slice, map or struct containing an +unsupported type will result in an InvalidTypeError. + +For incoming messages, the inverse of these rules are used, with the exception +of STRUCTs. Incoming STRUCTS are represented as a slice of empty interfaces +containing the struct fields in the correct order. The Store function can be +used to convert such values to Go structs. + +Unix FD passing + +Handling Unix file descriptors deserves special mention. To use them, you should +first check that they are supported on a connection by calling SupportsUnixFDs. +If it returns true, all method of Connection will translate messages containing +UnixFD's to messages that are accompanied by the given file descriptors with the +UnixFD values being substituted by the correct indices. Similarily, the indices +of incoming messages are automatically resolved. It shouldn't be necessary to use +UnixFDIndex. + +*/ +package dbus diff --git a/src/github.com/guelfey/go.dbus/encoder.go b/src/github.com/guelfey/go.dbus/encoder.go new file mode 100644 index 0000000..f9d2f05 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/encoder.go @@ -0,0 +1,179 @@ +package dbus + +import ( + "bytes" + "encoding/binary" + "io" + "reflect" +) + +// An encoder encodes values to the D-Bus wire format. +type encoder struct { + out io.Writer + order binary.ByteOrder + pos int +} + +// NewEncoder returns a new encoder that writes to out in the given byte order. +func newEncoder(out io.Writer, order binary.ByteOrder) *encoder { + enc := new(encoder) + enc.out = out + enc.order = order + return enc +} + +// Aligns the next output to be on a multiple of n. Panics on write errors. +func (enc *encoder) align(n int) { + if enc.pos%n != 0 { + newpos := (enc.pos + n - 1) & ^(n - 1) + empty := make([]byte, newpos-enc.pos) + if _, err := enc.out.Write(empty); err != nil { + panic(err) + } + enc.pos = newpos + } +} + +// Calls binary.Write(enc.out, enc.order, v) and panics on write errors. +func (enc *encoder) binwrite(v interface{}) { + if err := binary.Write(enc.out, enc.order, v); err != nil { + panic(err) + } +} + +// Encode encodes the given values to the underyling reader. All written values +// are aligned properly as required by the D-Bus spec. +func (enc *encoder) Encode(vs ...interface{}) (err error) { + defer func() { + err, _ = recover().(error) + }() + for _, v := range vs { + enc.encode(reflect.ValueOf(v), 0) + } + return nil +} + +// encode encodes the given value to the writer and panics on error. depth holds +// the depth of the container nesting. +func (enc *encoder) encode(v reflect.Value, depth int) { + enc.align(alignment(v.Type())) + switch v.Kind() { + case reflect.Uint8: + var b [1]byte + b[0] = byte(v.Uint()) + if _, err := enc.out.Write(b[:]); err != nil { + panic(err) + } + enc.pos++ + case reflect.Bool: + if v.Bool() { + enc.encode(reflect.ValueOf(uint32(1)), depth) + } else { + enc.encode(reflect.ValueOf(uint32(0)), depth) + } + case reflect.Int16: + enc.binwrite(int16(v.Int())) + enc.pos += 2 + case reflect.Uint16: + enc.binwrite(uint16(v.Uint())) + enc.pos += 2 + case reflect.Int32: + enc.binwrite(int32(v.Int())) + enc.pos += 4 + case reflect.Uint32: + enc.binwrite(uint32(v.Uint())) + enc.pos += 4 + case reflect.Int64: + enc.binwrite(v.Int()) + enc.pos += 8 + case reflect.Uint64: + enc.binwrite(v.Uint()) + enc.pos += 8 + case reflect.Float64: + enc.binwrite(v.Float()) + enc.pos += 8 + case reflect.String: + enc.encode(reflect.ValueOf(uint32(len(v.String()))), depth) + b := make([]byte, v.Len()+1) + copy(b, v.String()) + b[len(b)-1] = 0 + n, err := enc.out.Write(b) + if err != nil { + panic(err) + } + enc.pos += n + case reflect.Ptr: + enc.encode(v.Elem(), depth) + case reflect.Slice, reflect.Array: + if depth >= 64 { + panic(FormatError("input exceeds container depth limit")) + } + var buf bytes.Buffer + bufenc := newEncoder(&buf, enc.order) + + for i := 0; i < v.Len(); i++ { + bufenc.encode(v.Index(i), depth+1) + } + enc.encode(reflect.ValueOf(uint32(buf.Len())), depth) + length := buf.Len() + enc.align(alignment(v.Type().Elem())) + if _, err := buf.WriteTo(enc.out); err != nil { + panic(err) + } + enc.pos += length + case reflect.Struct: + if depth >= 64 && v.Type() != signatureType { + panic(FormatError("input exceeds container depth limit")) + } + switch t := v.Type(); t { + case signatureType: + str := v.Field(0) + enc.encode(reflect.ValueOf(byte(str.Len())), depth+1) + b := make([]byte, str.Len()+1) + copy(b, str.String()) + b[len(b)-1] = 0 + n, err := enc.out.Write(b) + if err != nil { + panic(err) + } + enc.pos += n + case variantType: + variant := v.Interface().(Variant) + enc.encode(reflect.ValueOf(variant.sig), depth+1) + enc.encode(reflect.ValueOf(variant.value), depth+1) + default: + for i := 0; i < v.Type().NumField(); i++ { + field := t.Field(i) + if field.PkgPath == "" && field.Tag.Get("dbus") != "-" { + enc.encode(v.Field(i), depth+1) + } + } + } + case reflect.Map: + // Maps are arrays of structures, so they actually increase the depth by + // 2. + if depth >= 63 { + panic(FormatError("input exceeds container depth limit")) + } + if !isKeyType(v.Type().Key()) { + panic(InvalidTypeError{v.Type()}) + } + keys := v.MapKeys() + var buf bytes.Buffer + bufenc := newEncoder(&buf, enc.order) + for _, k := range keys { + bufenc.align(8) + bufenc.encode(k, depth+2) + bufenc.encode(v.MapIndex(k), depth+2) + } + enc.encode(reflect.ValueOf(uint32(buf.Len())), depth) + length := buf.Len() + enc.align(8) + if _, err := buf.WriteTo(enc.out); err != nil { + panic(err) + } + enc.pos += length + default: + panic(InvalidTypeError{v.Type()}) + } +} diff --git a/src/github.com/guelfey/go.dbus/examples_test.go b/src/github.com/guelfey/go.dbus/examples_test.go new file mode 100644 index 0000000..0218ac5 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/examples_test.go @@ -0,0 +1,50 @@ +package dbus + +import "fmt" + +func ExampleConn_Emit() { + conn, err := SessionBus() + if err != nil { + panic(err) + } + + conn.Emit("/foo/bar", "foo.bar.Baz", uint32(0xDAEDBEEF)) +} + +func ExampleObject_Call() { + var list []string + + conn, err := SessionBus() + if err != nil { + panic(err) + } + + err = conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&list) + if err != nil { + panic(err) + } + for _, v := range list { + fmt.Println(v) + } +} + +func ExampleObject_Go() { + conn, err := SessionBus() + if err != nil { + panic(err) + } + + ch := make(chan *Call, 10) + conn.BusObject().Go("org.freedesktop.DBus.ListActivatableNames", 0, ch) + select { + case call := <-ch: + if call.Err != nil { + panic(err) + } + list := call.Body[0].([]string) + for _, v := range list { + fmt.Println(v) + } + // put some other cases here + } +} diff --git a/src/github.com/guelfey/go.dbus/export.go b/src/github.com/guelfey/go.dbus/export.go new file mode 100644 index 0000000..d95b092 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/export.go @@ -0,0 +1,300 @@ +package dbus + +import ( + "errors" + "reflect" + "strings" + "unicode" +) + +var ( + errmsgInvalidArg = Error{ + "org.freedesktop.DBus.Error.InvalidArgs", + []interface{}{"Invalid type / number of args"}, + } + errmsgNoObject = Error{ + "org.freedesktop.DBus.Error.NoSuchObject", + []interface{}{"No such object"}, + } + errmsgUnknownMethod = Error{ + "org.freedesktop.DBus.Error.UnknownMethod", + []interface{}{"Unknown / invalid method"}, + } +) + +// Sender is a type which can be used in exported methods to receive the message +// sender. +type Sender string + +func exportedMethod(v interface{}, name string) reflect.Value { + if v == nil { + return reflect.Value{} + } + m := reflect.ValueOf(v).MethodByName(name) + if !m.IsValid() { + return reflect.Value{} + } + t := m.Type() + if t.NumOut() == 0 || + t.Out(t.NumOut()-1) != reflect.TypeOf(&errmsgInvalidArg) { + + return reflect.Value{} + } + return m +} + +// handleCall handles the given method call (i.e. looks if it's one of the +// pre-implemented ones and searches for a corresponding handler if not). +func (conn *Conn) handleCall(msg *Message) { + name := msg.Headers[FieldMember].value.(string) + path := msg.Headers[FieldPath].value.(ObjectPath) + ifaceName, hasIface := msg.Headers[FieldInterface].value.(string) + sender := msg.Headers[FieldSender].value.(string) + serial := msg.serial + if ifaceName == "org.freedesktop.DBus.Peer" { + switch name { + case "Ping": + conn.sendReply(sender, serial) + case "GetMachineId": + conn.sendReply(sender, serial, conn.uuid) + default: + conn.sendError(errmsgUnknownMethod, sender, serial) + } + return + } + if len(name) == 0 || unicode.IsLower([]rune(name)[0]) { + conn.sendError(errmsgUnknownMethod, sender, serial) + } + var m reflect.Value + if hasIface { + conn.handlersLck.RLock() + obj, ok := conn.handlers[path] + if !ok { + conn.sendError(errmsgNoObject, sender, serial) + conn.handlersLck.RUnlock() + return + } + iface := obj[ifaceName] + conn.handlersLck.RUnlock() + m = exportedMethod(iface, name) + } else { + conn.handlersLck.RLock() + if _, ok := conn.handlers[path]; !ok { + conn.sendError(errmsgNoObject, sender, serial) + conn.handlersLck.RUnlock() + return + } + for _, v := range conn.handlers[path] { + m = exportedMethod(v, name) + if m.IsValid() { + break + } + } + conn.handlersLck.RUnlock() + } + if !m.IsValid() { + conn.sendError(errmsgUnknownMethod, sender, serial) + return + } + t := m.Type() + vs := msg.Body + pointers := make([]interface{}, t.NumIn()) + decode := make([]interface{}, 0, len(vs)) + for i := 0; i < t.NumIn(); i++ { + tp := t.In(i) + val := reflect.New(tp) + pointers[i] = val.Interface() + if tp == reflect.TypeOf((*Sender)(nil)).Elem() { + val.Elem().SetString(sender) + } else { + decode = append(decode, pointers[i]) + } + } + if len(decode) != len(vs) { + conn.sendError(errmsgInvalidArg, sender, serial) + return + } + if err := Store(vs, decode...); err != nil { + conn.sendError(errmsgInvalidArg, sender, serial) + return + } + params := make([]reflect.Value, len(pointers)) + for i := 0; i < len(pointers); i++ { + params[i] = reflect.ValueOf(pointers[i]).Elem() + } + ret := m.Call(params) + if em := ret[t.NumOut()-1].Interface().(*Error); em != nil { + conn.sendError(*em, sender, serial) + return + } + if msg.Flags&FlagNoReplyExpected == 0 { + reply := new(Message) + reply.Type = TypeMethodReply + reply.serial = conn.getSerial() + reply.Headers = make(map[HeaderField]Variant) + reply.Headers[FieldDestination] = msg.Headers[FieldSender] + reply.Headers[FieldReplySerial] = MakeVariant(msg.serial) + reply.Body = make([]interface{}, len(ret)-1) + for i := 0; i < len(ret)-1; i++ { + reply.Body[i] = ret[i].Interface() + } + if len(ret) != 1 { + reply.Headers[FieldSignature] = MakeVariant(SignatureOf(reply.Body...)) + } + conn.outLck.RLock() + if !conn.closed { + conn.out <- reply + } + conn.outLck.RUnlock() + } +} + +// Emit emits the given signal on the message bus. The name parameter must be +// formatted as "interface.member", e.g., "org.freedesktop.DBus.NameLost". +func (conn *Conn) Emit(path ObjectPath, name string, values ...interface{}) error { + if !path.IsValid() { + return errors.New("dbus: invalid object path") + } + i := strings.LastIndex(name, ".") + if i == -1 { + return errors.New("dbus: invalid method name") + } + iface := name[:i] + member := name[i+1:] + if !isValidMember(member) { + return errors.New("dbus: invalid method name") + } + if !isValidInterface(iface) { + return errors.New("dbus: invalid interface name") + } + msg := new(Message) + msg.Type = TypeSignal + msg.serial = conn.getSerial() + msg.Headers = make(map[HeaderField]Variant) + msg.Headers[FieldInterface] = MakeVariant(iface) + msg.Headers[FieldMember] = MakeVariant(member) + msg.Headers[FieldPath] = MakeVariant(path) + msg.Body = values + if len(values) > 0 { + msg.Headers[FieldSignature] = MakeVariant(SignatureOf(values...)) + } + conn.outLck.RLock() + defer conn.outLck.RUnlock() + if conn.closed { + return ErrClosed + } + conn.out <- msg + return nil +} + +// Export registers the given value to be exported as an object on the +// message bus. +// +// If a method call on the given path and interface is received, an exported +// method with the same name is called with v as the receiver if the +// parameters match and the last return value is of type *Error. If this +// *Error is not nil, it is sent back to the caller as an error. +// Otherwise, a method reply is sent with the other return values as its body. +// +// Any parameters with the special type Sender are set to the sender of the +// dbus message when the method is called. Parameters of this type do not +// contribute to the dbus signature of the method (i.e. the method is exposed +// as if the parameters of type Sender were not there). +// +// Every method call is executed in a new goroutine, so the method may be called +// in multiple goroutines at once. +// +// Method calls on the interface org.freedesktop.DBus.Peer will be automatically +// handled for every object. +// +// Passing nil as the first parameter will cause conn to cease handling calls on +// the given combination of path and interface. +// +// Export returns an error if path is not a valid path name. +func (conn *Conn) Export(v interface{}, path ObjectPath, iface string) error { + if !path.IsValid() { + return errors.New("dbus: invalid path name") + } + conn.handlersLck.Lock() + if v == nil { + if _, ok := conn.handlers[path]; ok { + delete(conn.handlers[path], iface) + if len(conn.handlers[path]) == 0 { + delete(conn.handlers, path) + } + } + return nil + } + if _, ok := conn.handlers[path]; !ok { + conn.handlers[path] = make(map[string]interface{}) + } + conn.handlers[path][iface] = v + conn.handlersLck.Unlock() + return nil +} + +// ReleaseName calls org.freedesktop.DBus.ReleaseName. You should use only this +// method to release a name (see below). +func (conn *Conn) ReleaseName(name string) (ReleaseNameReply, error) { + var r uint32 + err := conn.busObj.Call("org.freedesktop.DBus.ReleaseName", 0, name).Store(&r) + if err != nil { + return 0, err + } + if r == uint32(ReleaseNameReplyReleased) { + conn.namesLck.Lock() + for i, v := range conn.names { + if v == name { + copy(conn.names[i:], conn.names[i+1:]) + conn.names = conn.names[:len(conn.names)-1] + } + } + conn.namesLck.Unlock() + } + return ReleaseNameReply(r), nil +} + +// RequestName calls org.freedesktop.DBus.RequestName. You should use only this +// method to request a name because package dbus needs to keep track of all +// names that the connection has. +func (conn *Conn) RequestName(name string, flags RequestNameFlags) (RequestNameReply, error) { + var r uint32 + err := conn.busObj.Call("org.freedesktop.DBus.RequestName", 0, name, flags).Store(&r) + if err != nil { + return 0, err + } + if r == uint32(RequestNameReplyPrimaryOwner) { + conn.namesLck.Lock() + conn.names = append(conn.names, name) + conn.namesLck.Unlock() + } + return RequestNameReply(r), nil +} + +// ReleaseNameReply is the reply to a ReleaseName call. +type ReleaseNameReply uint32 + +const ( + ReleaseNameReplyReleased ReleaseNameReply = 1 + iota + ReleaseNameReplyNonExistent + ReleaseNameReplyNotOwner +) + +// RequestNameFlags represents the possible flags for a RequestName call. +type RequestNameFlags uint32 + +const ( + NameFlagAllowReplacement RequestNameFlags = 1 << iota + NameFlagReplaceExisting + NameFlagDoNotQueue +) + +// RequestNameReply is the reply to a RequestName call. +type RequestNameReply uint32 + +const ( + RequestNameReplyPrimaryOwner RequestNameReply = 1 + iota + RequestNameReplyInQueue + RequestNameReplyExists + RequestNameReplyAlreadyOwner +) diff --git a/src/github.com/guelfey/go.dbus/introspect/call.go b/src/github.com/guelfey/go.dbus/introspect/call.go new file mode 100644 index 0000000..180ba58 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/introspect/call.go @@ -0,0 +1,27 @@ +package introspect + +import ( + "encoding/xml" + "github.com/guelfey/go.dbus" + "strings" +) + +// Call calls org.freedesktop.Introspectable.Introspect on a remote object +// and returns the introspection data. +func Call(o *dbus.Object) (*Node, error) { + var xmldata string + var node Node + + err := o.Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&xmldata) + if err != nil { + return nil, err + } + err = xml.NewDecoder(strings.NewReader(xmldata)).Decode(&node) + if err != nil { + return nil, err + } + if node.Name == "" { + node.Name = string(o.Path()) + } + return &node, nil +} diff --git a/src/github.com/guelfey/go.dbus/introspect/introspect.go b/src/github.com/guelfey/go.dbus/introspect/introspect.go new file mode 100644 index 0000000..dafcdb8 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/introspect/introspect.go @@ -0,0 +1,80 @@ +// Package introspect provides some utilities for dealing with the DBus +// introspection format. +package introspect + +import "encoding/xml" + +// The introspection data for the org.freedesktop.DBus.Introspectable interface. +var IntrospectData = Interface{ + Name: "org.freedesktop.DBus.Introspectable", + Methods: []Method{ + { + Name: "Introspect", + Args: []Arg{ + {"out", "s", "out"}, + }, + }, + }, +} + +// The introspection data for the org.freedesktop.DBus.Introspectable interface, +// as a string. +const IntrospectDataString = ` + + + + + +` + +// Node is the root element of an introspection. +type Node struct { + XMLName xml.Name `xml:"node"` + Name string `xml:"name,attr,omitempty"` + Interfaces []Interface `xml:"interface"` + Children []Node `xml:"node,omitempty"` +} + +// Interface describes a DBus interface that is available on the message bus. +type Interface struct { + Name string `xml:"name,attr"` + Methods []Method `xml:"method"` + Signals []Signal `xml:"signal"` + Properties []Property `xml:"property"` + Annotations []Annotation `xml:"annotation"` +} + +// Method describes a Method on an Interface as retured by an introspection. +type Method struct { + Name string `xml:"name,attr"` + Args []Arg `xml:"arg"` + Annotations []Annotation `xml:"annotation"` +} + +// Signal describes a Signal emitted on an Interface. +type Signal struct { + Name string `xml:"name,attr"` + Args []Arg `xml:"arg"` + Annotations []Annotation `xml:"annotation"` +} + +// Property describes a property of an Interface. +type Property struct { + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Access string `xml:"access,attr"` + Annotations []Annotation `xml:"annotation"` +} + +// Arg represents an argument of a method or a signal. +type Arg struct { + Name string `xml:"name,attr,omitempty"` + Type string `xml:"type,attr"` + Direction string `xml:"direction,attr,omitempty"` +} + +// Annotation is an annotation in the introspection format. +type Annotation struct { + Name string `xml:"name,attr"` + Value string `xml:"value,attr"` +} diff --git a/src/github.com/guelfey/go.dbus/introspect/introspectable.go b/src/github.com/guelfey/go.dbus/introspect/introspectable.go new file mode 100644 index 0000000..ca1f8b5 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/introspect/introspectable.go @@ -0,0 +1,74 @@ +package introspect + +import ( + "encoding/xml" + "github.com/guelfey/go.dbus" + "reflect" +) + +// Introspectable implements org.freedesktop.Introspectable. +// +// You can create it by converting the XML-formatted introspection data from a +// string to an Introspectable or call NewIntrospectable with a Node. Then, +// export it as org.freedesktop.Introspectable on you object. +type Introspectable string + +// NewIntrospectable returns an Introspectable that returns the introspection +// data that corresponds to the given Node. If n.Interfaces doesn't contain the +// data for org.freedesktop.DBus.Introspectable, it is added automatically. +func NewIntrospectable(n *Node) Introspectable { + found := false + for _, v := range n.Interfaces { + if v.Name == "org.freedesktop.DBus.Introspectable" { + found = true + break + } + } + if !found { + n.Interfaces = append(n.Interfaces, IntrospectData) + } + b, err := xml.Marshal(n) + if err != nil { + panic(err) + } + return Introspectable(b) +} + +// Introspect implements org.freedesktop.Introspectable.Introspect. +func (i Introspectable) Introspect() (string, *dbus.Error) { + return string(i), nil +} + +// Methods returns the description of the methods of v. This can be used to +// create a Node which can be passed to NewIntrospectable. +func Methods(v interface{}) []Method { + t := reflect.TypeOf(v) + ms := make([]Method, 0, t.NumMethod()) + for i := 0; i < t.NumMethod(); i++ { + if t.Method(i).PkgPath != "" { + continue + } + mt := t.Method(i).Type + if mt.NumOut() == 0 || + mt.Out(mt.NumOut()-1) != reflect.TypeOf(&dbus.Error{"", nil}) { + + continue + } + var m Method + m.Name = t.Method(i).Name + m.Args = make([]Arg, 0, mt.NumIn()+mt.NumOut()-2) + for j := 1; j < mt.NumIn(); j++ { + if mt.In(j) != reflect.TypeOf((*dbus.Sender)(nil)).Elem() { + arg := Arg{"", dbus.SignatureOfType(mt.In(j)).String(), "in"} + m.Args = append(m.Args, arg) + } + } + for j := 0; j < mt.NumOut()-1; j++ { + arg := Arg{"", dbus.SignatureOfType(mt.Out(j)).String(), "out"} + m.Args = append(m.Args, arg) + } + m.Annotations = make([]Annotation, 0) + ms = append(ms, m) + } + return ms +} diff --git a/src/github.com/guelfey/go.dbus/message.go b/src/github.com/guelfey/go.dbus/message.go new file mode 100644 index 0000000..075d6e3 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/message.go @@ -0,0 +1,346 @@ +package dbus + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "reflect" + "strconv" +) + +const protoVersion byte = 1 + +// Flags represents the possible flags of a D-Bus message. +type Flags byte + +const ( + // FlagNoReplyExpected signals that the message is not expected to generate + // a reply. If this flag is set on outgoing messages, any possible reply + // will be discarded. + FlagNoReplyExpected Flags = 1 << iota + // FlagNoAutoStart signals that the message bus should not automatically + // start an application when handling this message. + FlagNoAutoStart +) + +// Type represents the possible types of a D-Bus message. +type Type byte + +const ( + TypeMethodCall Type = 1 + iota + TypeMethodReply + TypeError + TypeSignal + typeMax +) + +func (t Type) String() string { + switch t { + case TypeMethodCall: + return "method call" + case TypeMethodReply: + return "reply" + case TypeError: + return "error" + case TypeSignal: + return "signal" + } + return "invalid" +} + +// HeaderField represents the possible byte codes for the headers +// of a D-Bus message. +type HeaderField byte + +const ( + FieldPath HeaderField = 1 + iota + FieldInterface + FieldMember + FieldErrorName + FieldReplySerial + FieldDestination + FieldSender + FieldSignature + FieldUnixFDs + fieldMax +) + +// An InvalidMessageError describes the reason why a D-Bus message is regarded as +// invalid. +type InvalidMessageError string + +func (e InvalidMessageError) Error() string { + return "dbus: invalid message: " + string(e) +} + +// fieldType are the types of the various header fields. +var fieldTypes = [fieldMax]reflect.Type{ + FieldPath: objectPathType, + FieldInterface: stringType, + FieldMember: stringType, + FieldErrorName: stringType, + FieldReplySerial: uint32Type, + FieldDestination: stringType, + FieldSender: stringType, + FieldSignature: signatureType, + FieldUnixFDs: uint32Type, +} + +// requiredFields lists the header fields that are required by the different +// message types. +var requiredFields = [typeMax][]HeaderField{ + TypeMethodCall: {FieldPath, FieldMember}, + TypeMethodReply: {FieldReplySerial}, + TypeError: {FieldErrorName, FieldReplySerial}, + TypeSignal: {FieldPath, FieldInterface, FieldMember}, +} + +// Message represents a single D-Bus message. +type Message struct { + Type + Flags + Headers map[HeaderField]Variant + Body []interface{} + + serial uint32 +} + +type header struct { + Field byte + Variant +} + +// DecodeMessage tries to decode a single message in the D-Bus wire format +// from the given reader. The byte order is figured out from the first byte. +// The possibly returned error can be an error of the underlying reader, an +// InvalidMessageError or a FormatError. +func DecodeMessage(rd io.Reader) (msg *Message, err error) { + var order binary.ByteOrder + var hlength, length uint32 + var typ, flags, proto byte + var headers []header + + b := make([]byte, 1) + _, err = rd.Read(b) + if err != nil { + return + } + switch b[0] { + case 'l': + order = binary.LittleEndian + case 'B': + order = binary.BigEndian + default: + return nil, InvalidMessageError("invalid byte order") + } + + dec := newDecoder(rd, order) + dec.pos = 1 + + msg = new(Message) + vs, err := dec.Decode(Signature{"yyyuu"}) + if err != nil { + return nil, err + } + if err = Store(vs, &typ, &flags, &proto, &length, &msg.serial); err != nil { + return nil, err + } + msg.Type = Type(typ) + msg.Flags = Flags(flags) + + // get the header length separately because we need it later + b = make([]byte, 4) + _, err = io.ReadFull(rd, b) + if err != nil { + return nil, err + } + binary.Read(bytes.NewBuffer(b), order, &hlength) + if hlength+length+16 > 1<<27 { + return nil, InvalidMessageError("message is too long") + } + dec = newDecoder(io.MultiReader(bytes.NewBuffer(b), rd), order) + dec.pos = 12 + vs, err = dec.Decode(Signature{"a(yv)"}) + if err != nil { + return nil, err + } + if err = Store(vs, &headers); err != nil { + return nil, err + } + + msg.Headers = make(map[HeaderField]Variant) + for _, v := range headers { + msg.Headers[HeaderField(v.Field)] = v.Variant + } + + dec.align(8) + body := make([]byte, int(length)) + if length != 0 { + _, err := io.ReadFull(rd, body) + if err != nil { + return nil, err + } + } + + if err = msg.IsValid(); err != nil { + return nil, err + } + sig, _ := msg.Headers[FieldSignature].value.(Signature) + if sig.str != "" { + buf := bytes.NewBuffer(body) + dec = newDecoder(buf, order) + vs, err := dec.Decode(sig) + if err != nil { + return nil, err + } + msg.Body = vs + } + + return +} + +// EncodeTo encodes and sends a message to the given writer. The byte order must +// be either binary.LittleEndian or binary.BigEndian. If the message is not +// valid or an error occurs when writing, an error is returned. +func (msg *Message) EncodeTo(out io.Writer, order binary.ByteOrder) error { + if err := msg.IsValid(); err != nil { + return err + } + var vs [7]interface{} + switch order { + case binary.LittleEndian: + vs[0] = byte('l') + case binary.BigEndian: + vs[0] = byte('B') + default: + return errors.New("dbus: invalid byte order") + } + body := new(bytes.Buffer) + enc := newEncoder(body, order) + if len(msg.Body) != 0 { + enc.Encode(msg.Body...) + } + vs[1] = msg.Type + vs[2] = msg.Flags + vs[3] = protoVersion + vs[4] = uint32(len(body.Bytes())) + vs[5] = msg.serial + headers := make([]header, 0, len(msg.Headers)) + for k, v := range msg.Headers { + headers = append(headers, header{byte(k), v}) + } + vs[6] = headers + var buf bytes.Buffer + enc = newEncoder(&buf, order) + enc.Encode(vs[:]...) + enc.align(8) + body.WriteTo(&buf) + if buf.Len() > 1<<27 { + return InvalidMessageError("message is too long") + } + if _, err := buf.WriteTo(out); err != nil { + return err + } + return nil +} + +// IsValid checks whether msg is a valid message and returns an +// InvalidMessageError if it is not. +func (msg *Message) IsValid() error { + if msg.Flags & ^(FlagNoAutoStart|FlagNoReplyExpected) != 0 { + return InvalidMessageError("invalid flags") + } + if msg.Type == 0 || msg.Type >= typeMax { + return InvalidMessageError("invalid message type") + } + for k, v := range msg.Headers { + if k == 0 || k >= fieldMax { + return InvalidMessageError("invalid header") + } + if reflect.TypeOf(v.value) != fieldTypes[k] { + return InvalidMessageError("invalid type of header field") + } + } + for _, v := range requiredFields[msg.Type] { + if _, ok := msg.Headers[v]; !ok { + return InvalidMessageError("missing required header") + } + } + if path, ok := msg.Headers[FieldPath]; ok { + if !path.value.(ObjectPath).IsValid() { + return InvalidMessageError("invalid path name") + } + } + if iface, ok := msg.Headers[FieldInterface]; ok { + if !isValidInterface(iface.value.(string)) { + return InvalidMessageError("invalid interface name") + } + } + if member, ok := msg.Headers[FieldMember]; ok { + if !isValidMember(member.value.(string)) { + return InvalidMessageError("invalid member name") + } + } + if errname, ok := msg.Headers[FieldErrorName]; ok { + if !isValidInterface(errname.value.(string)) { + return InvalidMessageError("invalid error name") + } + } + if len(msg.Body) != 0 { + if _, ok := msg.Headers[FieldSignature]; !ok { + return InvalidMessageError("missing signature") + } + } + return nil +} + +// Serial returns the message's serial number. The returned value is only valid +// for messages received by eavesdropping. +func (msg *Message) Serial() uint32 { + return msg.serial +} + +// String returns a string representation of a message similar to the format of +// dbus-monitor. +func (msg *Message) String() string { + if err := msg.IsValid(); err != nil { + return "" + } + s := msg.Type.String() + if v, ok := msg.Headers[FieldSender]; ok { + s += " from " + v.value.(string) + } + if v, ok := msg.Headers[FieldDestination]; ok { + s += " to " + v.value.(string) + } + s += " serial " + strconv.FormatUint(uint64(msg.serial), 10) + if v, ok := msg.Headers[FieldReplySerial]; ok { + s += " reply_serial " + strconv.FormatUint(uint64(v.value.(uint32)), 10) + } + if v, ok := msg.Headers[FieldUnixFDs]; ok { + s += " unixfds " + strconv.FormatUint(uint64(v.value.(uint32)), 10) + } + if v, ok := msg.Headers[FieldPath]; ok { + s += " path " + string(v.value.(ObjectPath)) + } + if v, ok := msg.Headers[FieldInterface]; ok { + s += " interface " + v.value.(string) + } + if v, ok := msg.Headers[FieldErrorName]; ok { + s += " error " + v.value.(string) + } + if v, ok := msg.Headers[FieldMember]; ok { + s += " member " + v.value.(string) + } + if len(msg.Body) != 0 { + s += "\n" + } + for i, v := range msg.Body { + s += " " + MakeVariant(v).String() + if i != len(msg.Body)-1 { + s += "\n" + } + } + return s +} diff --git a/src/github.com/guelfey/go.dbus/prop/prop.go b/src/github.com/guelfey/go.dbus/prop/prop.go new file mode 100644 index 0000000..bc69520 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/prop/prop.go @@ -0,0 +1,264 @@ +// Package prop provides the Properties struct which can be used to implement +// org.freedesktop.DBus.Properties. +package prop + +import ( + "github.com/guelfey/go.dbus" + "github.com/guelfey/go.dbus/introspect" + "sync" +) + +// EmitType controls how org.freedesktop.DBus.Properties.PropertiesChanged is +// emitted for a property. If it is EmitTrue, the signal is emitted. If it is +// EmitInvalidates, the signal is also emitted, but the new value of the property +// is not disclosed. +type EmitType byte + +const ( + EmitFalse EmitType = iota + EmitTrue + EmitInvalidates +) + +// ErrIfaceNotFound is the error returned to peers who try to access properties +// on interfaces that aren't found. +var ErrIfaceNotFound = &dbus.Error{"org.freedesktop.DBus.Properties.Error.InterfaceNotFound", nil} + +// ErrPropNotFound is the error returned to peers trying to access properties +// that aren't found. +var ErrPropNotFound = &dbus.Error{"org.freedesktop.DBus.Properties.Error.PropertyNotFound", nil} + +// ErrReadOnly is the error returned to peers trying to set a read-only +// property. +var ErrReadOnly = &dbus.Error{"org.freedesktop.DBus.Properties.Error.ReadOnly", nil} + +// ErrInvalidArg is returned to peers if the type of the property that is being +// changed and the argument don't match. +var ErrInvalidArg = &dbus.Error{"org.freedesktop.DBus.Properties.Error.InvalidArg", nil} + +// The introspection data for the org.freedesktop.DBus.Properties interface. +var IntrospectData = introspect.Interface{ + Name: "org.freedesktop.DBus.Properties", + Methods: []introspect.Method{ + { + Name: "Get", + Args: []introspect.Arg{ + {"interface", "s", "in"}, + {"property", "s", "in"}, + {"value", "v", "out"}, + }, + }, + { + Name: "GetAll", + Args: []introspect.Arg{ + {"interface", "s", "in"}, + {"props", "a{sv}", "out"}, + }, + }, + { + Name: "Set", + Args: []introspect.Arg{ + {"interface", "s", "in"}, + {"property", "s", "in"}, + {"value", "v", "in"}, + }, + }, + }, + Signals: []introspect.Signal{ + { + Name: "PropertiesChanged", + Args: []introspect.Arg{ + {"interface", "s", "out"}, + {"changed_properties", "a{sv}", "out"}, + {"invalidates_properties", "as", "out"}, + }, + }, + }, +} + +// The introspection data for the org.freedesktop.DBus.Properties interface, as +// a string. +const IntrospectDataString = ` + + + + + + + + + + + + + + + + + + + + + +` + +// Prop represents a single property. It is used for creating a Properties +// value. +type Prop struct { + // Initial value. Must be a DBus-representable type. + Value interface{} + + // If true, the value can be modified by calls to Set. + Writable bool + + // Controls how org.freedesktop.DBus.Properties.PropertiesChanged is + // emitted if this property changes. + Emit EmitType + + // If not nil, anytime this property is changed by Set, this function is + // called with an appropiate Change as its argument. If the returned error + // is not nil, it is sent back to the caller of Set and the property is not + // changed. + Callback func(*Change) *dbus.Error +} + +// Change represents a change of a property by a call to Set. +type Change struct { + Props *Properties + Iface string + Name string + Value interface{} +} + +// Properties is a set of values that can be made available to the message bus +// using the org.freedesktop.DBus.Properties interface. It is safe for +// concurrent use by multiple goroutines. +type Properties struct { + m map[string]map[string]*Prop + mut sync.RWMutex + conn *dbus.Conn + path dbus.ObjectPath +} + +// New returns a new Properties structure that manages the given properties. +// The key for the first-level map of props is the name of the interface; the +// second-level key is the name of the property. The returned structure will be +// exported as org.freedesktop.DBus.Properties on path. +func New(conn *dbus.Conn, path dbus.ObjectPath, props map[string]map[string]*Prop) *Properties { + p := &Properties{m: props, conn: conn, path: path} + conn.Export(p, path, "org.freedesktop.DBus.Properties") + return p +} + +// Get implements org.freedesktop.DBus.Properties.Get. +func (p *Properties) Get(iface, property string) (dbus.Variant, *dbus.Error) { + p.mut.RLock() + defer p.mut.RUnlock() + m, ok := p.m[iface] + if !ok { + return dbus.Variant{}, ErrIfaceNotFound + } + prop, ok := m[property] + if !ok { + return dbus.Variant{}, ErrPropNotFound + } + return dbus.MakeVariant(prop.Value), nil +} + +// GetAll implements org.freedesktop.DBus.Properties.GetAll. +func (p *Properties) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) { + p.mut.RLock() + defer p.mut.RUnlock() + m, ok := p.m[iface] + if !ok { + return nil, ErrIfaceNotFound + } + rm := make(map[string]dbus.Variant, len(m)) + for k, v := range m { + rm[k] = dbus.MakeVariant(v.Value) + } + return rm, nil +} + +// GetMust returns the value of the given property and panics if either the +// interface or the property name are invalid. +func (p *Properties) GetMust(iface, property string) interface{} { + p.mut.RLock() + defer p.mut.RUnlock() + return p.m[iface][property].Value +} + +// Introspection returns the introspection data that represents the properties +// of iface. +func (p *Properties) Introspection(iface string) []introspect.Property { + p.mut.RLock() + defer p.mut.RUnlock() + m := p.m[iface] + s := make([]introspect.Property, 0, len(m)) + for k, v := range m { + p := introspect.Property{Name: k, Type: dbus.SignatureOf(v.Value).String()} + if v.Writable { + p.Access = "readwrite" + } else { + p.Access = "read" + } + s = append(s, p) + } + return s +} + +// set sets the given property and emits PropertyChanged if appropiate. p.mut +// must already be locked. +func (p *Properties) set(iface, property string, v interface{}) { + prop := p.m[iface][property] + prop.Value = v + switch prop.Emit { + case EmitFalse: + // do nothing + case EmitInvalidates: + p.conn.Emit(p.path, "org.freedesktop.DBus.Properties.PropertiesChanged", + iface, map[string]dbus.Variant{}, []string{property}) + case EmitTrue: + p.conn.Emit(p.path, "org.freedesktop.DBus.Properties.PropertiesChanged", + iface, map[string]dbus.Variant{property: dbus.MakeVariant(v)}, + []string{}) + default: + panic("invalid value for EmitType") + } +} + +// Set implements org.freedesktop.Properties.Set. +func (p *Properties) Set(iface, property string, newv dbus.Variant) *dbus.Error { + p.mut.Lock() + defer p.mut.Unlock() + m, ok := p.m[iface] + if !ok { + return ErrIfaceNotFound + } + prop, ok := m[property] + if !ok { + return ErrPropNotFound + } + if !prop.Writable { + return ErrReadOnly + } + if newv.Signature() != dbus.SignatureOf(prop.Value) { + return ErrInvalidArg + } + if prop.Callback != nil { + err := prop.Callback(&Change{p, iface, property, newv.Value()}) + if err != nil { + return err + } + } + p.set(iface, property, newv.Value()) + return nil +} + +// SetMust sets the value of the given property and panics if the interface or +// the property name are invalid. +func (p *Properties) SetMust(iface, property string, v interface{}) { + p.mut.Lock() + p.set(iface, property, v) + p.mut.Unlock() +} diff --git a/src/github.com/guelfey/go.dbus/proto_test.go b/src/github.com/guelfey/go.dbus/proto_test.go new file mode 100644 index 0000000..608a770 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/proto_test.go @@ -0,0 +1,369 @@ +package dbus + +import ( + "bytes" + "encoding/binary" + "io/ioutil" + "math" + "reflect" + "testing" +) + +var protoTests = []struct { + vs []interface{} + bigEndian []byte + littleEndian []byte +}{ + { + []interface{}{int32(0)}, + []byte{0, 0, 0, 0}, + []byte{0, 0, 0, 0}, + }, + { + []interface{}{true, false}, + []byte{0, 0, 0, 1, 0, 0, 0, 0}, + []byte{1, 0, 0, 0, 0, 0, 0, 0}, + }, + { + []interface{}{byte(0), uint16(12), int16(32), uint32(43)}, + []byte{0, 0, 0, 12, 0, 32, 0, 0, 0, 0, 0, 43}, + []byte{0, 0, 12, 0, 32, 0, 0, 0, 43, 0, 0, 0}, + }, + { + []interface{}{int64(-1), uint64(1<<64 - 1)}, + bytes.Repeat([]byte{255}, 16), + bytes.Repeat([]byte{255}, 16), + }, + { + []interface{}{math.Inf(+1)}, + []byte{0x7f, 0xf0, 0, 0, 0, 0, 0, 0}, + []byte{0, 0, 0, 0, 0, 0, 0xf0, 0x7f}, + }, + { + []interface{}{"foo"}, + []byte{0, 0, 0, 3, 'f', 'o', 'o', 0}, + []byte{3, 0, 0, 0, 'f', 'o', 'o', 0}, + }, + { + []interface{}{Signature{"ai"}}, + []byte{2, 'a', 'i', 0}, + []byte{2, 'a', 'i', 0}, + }, + { + []interface{}{[]int16{42, 256}}, + []byte{0, 0, 0, 4, 0, 42, 1, 0}, + []byte{4, 0, 0, 0, 42, 0, 0, 1}, + }, + { + []interface{}{MakeVariant("foo")}, + []byte{1, 's', 0, 0, 0, 0, 0, 3, 'f', 'o', 'o', 0}, + []byte{1, 's', 0, 0, 3, 0, 0, 0, 'f', 'o', 'o', 0}, + }, + { + []interface{}{MakeVariant(MakeVariant(Signature{"v"}))}, + []byte{1, 'v', 0, 1, 'g', 0, 1, 'v', 0}, + []byte{1, 'v', 0, 1, 'g', 0, 1, 'v', 0}, + }, + { + []interface{}{map[int32]bool{42: true}}, + []byte{0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 1}, + []byte{8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 1, 0, 0, 0}, + }, + { + []interface{}{map[string]Variant{}, byte(42)}, + []byte{0, 0, 0, 0, 0, 0, 0, 0, 42}, + []byte{0, 0, 0, 0, 0, 0, 0, 0, 42}, + }, + { + []interface{}{[]uint64{}, byte(42)}, + []byte{0, 0, 0, 0, 0, 0, 0, 0, 42}, + []byte{0, 0, 0, 0, 0, 0, 0, 0, 42}, + }, +} + +func TestProto(t *testing.T) { + for i, v := range protoTests { + buf := new(bytes.Buffer) + bigEnc := newEncoder(buf, binary.BigEndian) + bigEnc.Encode(v.vs...) + marshalled := buf.Bytes() + if bytes.Compare(marshalled, v.bigEndian) != 0 { + t.Errorf("test %d (marshal be): got '%v', but expected '%v'\n", i+1, marshalled, + v.bigEndian) + } + buf.Reset() + litEnc := newEncoder(buf, binary.LittleEndian) + litEnc.Encode(v.vs...) + marshalled = buf.Bytes() + if bytes.Compare(marshalled, v.littleEndian) != 0 { + t.Errorf("test %d (marshal le): got '%v', but expected '%v'\n", i+1, marshalled, + v.littleEndian) + } + unmarshalled := reflect.MakeSlice(reflect.TypeOf(v.vs), + 0, 0) + for i := range v.vs { + unmarshalled = reflect.Append(unmarshalled, + reflect.New(reflect.TypeOf(v.vs[i]))) + } + bigDec := newDecoder(bytes.NewReader(v.bigEndian), binary.BigEndian) + vs, err := bigDec.Decode(SignatureOf(v.vs...)) + if err != nil { + t.Errorf("test %d (unmarshal be): %s\n", i+1, err) + continue + } + if !reflect.DeepEqual(vs, v.vs) { + t.Errorf("test %d (unmarshal be): got %#v, but expected %#v\n", i+1, vs, v.vs) + } + litDec := newDecoder(bytes.NewReader(v.littleEndian), binary.LittleEndian) + vs, err = litDec.Decode(SignatureOf(v.vs...)) + if err != nil { + t.Errorf("test %d (unmarshal le): %s\n", i+1, err) + continue + } + if !reflect.DeepEqual(vs, v.vs) { + t.Errorf("test %d (unmarshal le): got %#v, but expected %#v\n", i+1, vs, v.vs) + } + + } +} + +func TestProtoMap(t *testing.T) { + m := map[string]uint8{ + "foo": 23, + "bar": 2, + } + var n map[string]uint8 + buf := new(bytes.Buffer) + enc := newEncoder(buf, binary.LittleEndian) + enc.Encode(m) + dec := newDecoder(buf, binary.LittleEndian) + vs, err := dec.Decode(Signature{"a{sy}"}) + if err != nil { + t.Fatal(err) + } + if err = Store(vs, &n); err != nil { + t.Fatal(err) + } + if len(n) != 2 || n["foo"] != 23 || n["bar"] != 2 { + t.Error("got", n) + } +} + +func TestProtoVariantStruct(t *testing.T) { + var variant Variant + v := MakeVariant(struct { + A int32 + B int16 + }{1, 2}) + buf := new(bytes.Buffer) + enc := newEncoder(buf, binary.LittleEndian) + enc.Encode(v) + dec := newDecoder(buf, binary.LittleEndian) + vs, err := dec.Decode(Signature{"v"}) + if err != nil { + t.Fatal(err) + } + if err = Store(vs, &variant); err != nil { + t.Fatal(err) + } + sl := variant.Value().([]interface{}) + v1, v2 := sl[0].(int32), sl[1].(int16) + if v1 != int32(1) { + t.Error("got", v1, "as first int") + } + if v2 != int16(2) { + t.Error("got", v2, "as second int") + } +} + +func TestProtoStructTag(t *testing.T) { + type Bar struct { + A int32 + B chan interface{} `dbus:"-"` + C int32 + } + var bar1, bar2 Bar + bar1.A = 234 + bar2.C = 345 + buf := new(bytes.Buffer) + enc := newEncoder(buf, binary.LittleEndian) + enc.Encode(bar1) + dec := newDecoder(buf, binary.LittleEndian) + vs, err := dec.Decode(Signature{"(ii)"}) + if err != nil { + t.Fatal(err) + } + if err = Store(vs, &bar2); err != nil { + t.Fatal(err) + } + if bar1 != bar2 { + t.Error("struct tag test: got", bar2) + } +} + +func TestProtoStoreStruct(t *testing.T) { + var foo struct { + A int32 + B string + c chan interface{} + D interface{} `dbus:"-"` + } + src := []interface{}{[]interface{}{int32(42), "foo"}} + err := Store(src, &foo) + if err != nil { + t.Fatal(err) + } +} + +func TestProtoStoreNestedStruct(t *testing.T) { + var foo struct { + A int32 + B struct { + C string + D float64 + } + } + src := []interface{}{ + []interface{}{ + int32(42), + []interface{}{ + "foo", + 3.14, + }, + }, + } + err := Store(src, &foo) + if err != nil { + t.Fatal(err) + } +} + +func TestMessage(t *testing.T) { + buf := new(bytes.Buffer) + message := new(Message) + message.Type = TypeMethodCall + message.serial = 32 + message.Headers = map[HeaderField]Variant{ + FieldPath: MakeVariant(ObjectPath("/org/foo/bar")), + FieldMember: MakeVariant("baz"), + } + message.Body = make([]interface{}, 0) + err := message.EncodeTo(buf, binary.LittleEndian) + if err != nil { + t.Error(err) + } + _, err = DecodeMessage(buf) + if err != nil { + t.Error(err) + } +} + +func TestProtoStructInterfaces(t *testing.T) { + b := []byte{42} + vs, err := newDecoder(bytes.NewReader(b), binary.LittleEndian).Decode(Signature{"(y)"}) + if err != nil { + t.Fatal(err) + } + if vs[0].([]interface{})[0].(byte) != 42 { + t.Errorf("wrongs results (got %v)", vs) + } +} + +// ordinary org.freedesktop.DBus.Hello call +var smallMessage = &Message{ + Type: TypeMethodCall, + serial: 1, + Headers: map[HeaderField]Variant{ + FieldDestination: MakeVariant("org.freedesktop.DBus"), + FieldPath: MakeVariant(ObjectPath("/org/freedesktop/DBus")), + FieldInterface: MakeVariant("org.freedesktop.DBus"), + FieldMember: MakeVariant("Hello"), + }, +} + +// org.freedesktop.Notifications.Notify +var bigMessage = &Message{ + Type: TypeMethodCall, + serial: 2, + Headers: map[HeaderField]Variant{ + FieldDestination: MakeVariant("org.freedesktop.Notifications"), + FieldPath: MakeVariant(ObjectPath("/org/freedesktop/Notifications")), + FieldInterface: MakeVariant("org.freedesktop.Notifications"), + FieldMember: MakeVariant("Notify"), + FieldSignature: MakeVariant(Signature{"susssasa{sv}i"}), + }, + Body: []interface{}{ + "app_name", + uint32(0), + "dialog-information", + "Notification", + "This is the body of a notification", + []string{"ok", "Ok"}, + map[string]Variant{ + "sound-name": MakeVariant("dialog-information"), + }, + int32(-1), + }, +} + +func BenchmarkDecodeMessageSmall(b *testing.B) { + var err error + var rd *bytes.Reader + + b.StopTimer() + buf := new(bytes.Buffer) + err = smallMessage.EncodeTo(buf, binary.LittleEndian) + if err != nil { + b.Fatal(err) + } + decoded := buf.Bytes() + b.StartTimer() + for i := 0; i < b.N; i++ { + rd = bytes.NewReader(decoded) + _, err = DecodeMessage(rd) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkDecodeMessageBig(b *testing.B) { + var err error + var rd *bytes.Reader + + b.StopTimer() + buf := new(bytes.Buffer) + err = bigMessage.EncodeTo(buf, binary.LittleEndian) + if err != nil { + b.Fatal(err) + } + decoded := buf.Bytes() + b.StartTimer() + for i := 0; i < b.N; i++ { + rd = bytes.NewReader(decoded) + _, err = DecodeMessage(rd) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkEncodeMessageSmall(b *testing.B) { + var err error + for i := 0; i < b.N; i++ { + err = smallMessage.EncodeTo(ioutil.Discard, binary.LittleEndian) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkEncodeMessageBig(b *testing.B) { + var err error + for i := 0; i < b.N; i++ { + err = bigMessage.EncodeTo(ioutil.Discard, binary.LittleEndian) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/src/github.com/guelfey/go.dbus/sig.go b/src/github.com/guelfey/go.dbus/sig.go new file mode 100644 index 0000000..f45b53c --- /dev/null +++ b/src/github.com/guelfey/go.dbus/sig.go @@ -0,0 +1,257 @@ +package dbus + +import ( + "fmt" + "reflect" + "strings" +) + +var sigToType = map[byte]reflect.Type{ + 'y': byteType, + 'b': boolType, + 'n': int16Type, + 'q': uint16Type, + 'i': int32Type, + 'u': uint32Type, + 'x': int64Type, + 't': uint64Type, + 'd': float64Type, + 's': stringType, + 'g': signatureType, + 'o': objectPathType, + 'v': variantType, + 'h': unixFDIndexType, +} + +// Signature represents a correct type signature as specified by the D-Bus +// specification. The zero value represents the empty signature, "". +type Signature struct { + str string +} + +// SignatureOf returns the concatenation of all the signatures of the given +// values. It panics if one of them is not representable in D-Bus. +func SignatureOf(vs ...interface{}) Signature { + var s string + for _, v := range vs { + s += getSignature(reflect.TypeOf(v)) + } + return Signature{s} +} + +// SignatureOfType returns the signature of the given type. It panics if the +// type is not representable in D-Bus. +func SignatureOfType(t reflect.Type) Signature { + return Signature{getSignature(t)} +} + +// getSignature returns the signature of the given type and panics on unknown types. +func getSignature(t reflect.Type) string { + // handle simple types first + switch t.Kind() { + case reflect.Uint8: + return "y" + case reflect.Bool: + return "b" + case reflect.Int16: + return "n" + case reflect.Uint16: + return "q" + case reflect.Int32: + if t == unixFDType { + return "h" + } + return "i" + case reflect.Uint32: + if t == unixFDIndexType { + return "h" + } + return "u" + case reflect.Int64: + return "x" + case reflect.Uint64: + return "t" + case reflect.Float64: + return "d" + case reflect.Ptr: + return getSignature(t.Elem()) + case reflect.String: + if t == objectPathType { + return "o" + } + return "s" + case reflect.Struct: + if t == variantType { + return "v" + } else if t == signatureType { + return "g" + } + var s string + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.PkgPath == "" && field.Tag.Get("dbus") != "-" { + s += getSignature(t.Field(i).Type) + } + } + return "(" + s + ")" + case reflect.Array, reflect.Slice: + return "a" + getSignature(t.Elem()) + case reflect.Map: + if !isKeyType(t.Key()) { + panic(InvalidTypeError{t}) + } + return "a{" + getSignature(t.Key()) + getSignature(t.Elem()) + "}" + } + panic(InvalidTypeError{t}) +} + +// ParseSignature returns the signature represented by this string, or a +// SignatureError if the string is not a valid signature. +func ParseSignature(s string) (sig Signature, err error) { + if len(s) == 0 { + return + } + if len(s) > 255 { + return Signature{""}, SignatureError{s, "too long"} + } + sig.str = s + for err == nil && len(s) != 0 { + err, s = validSingle(s, 0) + } + if err != nil { + sig = Signature{""} + } + + return +} + +// ParseSignatureMust behaves like ParseSignature, except that it panics if s +// is not valid. +func ParseSignatureMust(s string) Signature { + sig, err := ParseSignature(s) + if err != nil { + panic(err) + } + return sig +} + +// Empty retruns whether the signature is the empty signature. +func (s Signature) Empty() bool { + return s.str == "" +} + +// Single returns whether the signature represents a single, complete type. +func (s Signature) Single() bool { + err, r := validSingle(s.str, 0) + return err != nil && r == "" +} + +// String returns the signature's string representation. +func (s Signature) String() string { + return s.str +} + +// A SignatureError indicates that a signature passed to a function or received +// on a connection is not a valid signature. +type SignatureError struct { + Sig string + Reason string +} + +func (e SignatureError) Error() string { + return fmt.Sprintf("dbus: invalid signature: %q (%s)", e.Sig, e.Reason) +} + +// Try to read a single type from this string. If it was successfull, err is nil +// and rem is the remaining unparsed part. Otherwise, err is a non-nil +// SignatureError and rem is "". depth is the current recursion depth which may +// not be greater than 64 and should be given as 0 on the first call. +func validSingle(s string, depth int) (err error, rem string) { + if s == "" { + return SignatureError{Sig: s, Reason: "empty signature"}, "" + } + if depth > 64 { + return SignatureError{Sig: s, Reason: "container nesting too deep"}, "" + } + switch s[0] { + case 'y', 'b', 'n', 'q', 'i', 'u', 'x', 't', 'd', 's', 'g', 'o', 'v', 'h': + return nil, s[1:] + case 'a': + if len(s) > 1 && s[1] == '{' { + i := findMatching(s[1:], '{', '}') + if i == -1 { + return SignatureError{Sig: s, Reason: "unmatched '{'"}, "" + } + i++ + rem = s[i+1:] + s = s[2:i] + if err, _ = validSingle(s[:1], depth+1); err != nil { + return err, "" + } + err, nr := validSingle(s[1:], depth+1) + if err != nil { + return err, "" + } + if nr != "" { + return SignatureError{Sig: s, Reason: "too many types in dict"}, "" + } + return nil, rem + } + return validSingle(s[1:], depth+1) + case '(': + i := findMatching(s, '(', ')') + if i == -1 { + return SignatureError{Sig: s, Reason: "unmatched ')'"}, "" + } + rem = s[i+1:] + s = s[1:i] + for err == nil && s != "" { + err, s = validSingle(s, depth+1) + } + if err != nil { + rem = "" + } + return + } + return SignatureError{Sig: s, Reason: "invalid type character"}, "" +} + +func findMatching(s string, left, right rune) int { + n := 0 + for i, v := range s { + if v == left { + n++ + } else if v == right { + n-- + } + if n == 0 { + return i + } + } + return -1 +} + +// typeFor returns the type of the given signature. It ignores any left over +// characters and panics if s doesn't start with a valid type signature. +func typeFor(s string) (t reflect.Type) { + err, _ := validSingle(s, 0) + if err != nil { + panic(err) + } + + if t, ok := sigToType[s[0]]; ok { + return t + } + switch s[0] { + case 'a': + if s[1] == '{' { + i := strings.LastIndex(s, "}") + t = reflect.MapOf(sigToType[s[2]], typeFor(s[3:i])) + } else { + t = reflect.SliceOf(typeFor(s[1:])) + } + case '(': + t = interfacesType + } + return +} diff --git a/src/github.com/guelfey/go.dbus/sig_test.go b/src/github.com/guelfey/go.dbus/sig_test.go new file mode 100644 index 0000000..da37bc9 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/sig_test.go @@ -0,0 +1,70 @@ +package dbus + +import ( + "testing" +) + +var sigTests = []struct { + vs []interface{} + sig Signature +}{ + { + []interface{}{new(int32)}, + Signature{"i"}, + }, + { + []interface{}{new(string)}, + Signature{"s"}, + }, + { + []interface{}{new(Signature)}, + Signature{"g"}, + }, + { + []interface{}{new([]int16)}, + Signature{"an"}, + }, + { + []interface{}{new(int16), new(uint32)}, + Signature{"nu"}, + }, + { + []interface{}{new(map[byte]Variant)}, + Signature{"a{yv}"}, + }, + { + []interface{}{new(Variant), new([]map[int32]string)}, + Signature{"vaa{is}"}, + }, +} + +func TestSig(t *testing.T) { + for i, v := range sigTests { + sig := SignatureOf(v.vs...) + if sig != v.sig { + t.Errorf("test %d: got %q, expected %q", i+1, sig.str, v.sig.str) + } + } +} + +var getSigTest = []interface{}{ + []struct { + b byte + i int32 + t uint64 + s string + }{}, + map[string]Variant{}, +} + +func BenchmarkGetSignatureSimple(b *testing.B) { + for i := 0; i < b.N; i++ { + SignatureOf("", int32(0)) + } +} + +func BenchmarkGetSignatureLong(b *testing.B) { + for i := 0; i < b.N; i++ { + SignatureOf(getSigTest...) + } +} diff --git a/src/github.com/guelfey/go.dbus/transport_darwin.go b/src/github.com/guelfey/go.dbus/transport_darwin.go new file mode 100644 index 0000000..1bba0d6 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/transport_darwin.go @@ -0,0 +1,6 @@ +package dbus + +func (t *unixTransport) SendNullByte() error { + _, err := t.Write([]byte{0}) + return err +} diff --git a/src/github.com/guelfey/go.dbus/transport_generic.go b/src/github.com/guelfey/go.dbus/transport_generic.go new file mode 100644 index 0000000..46f8f49 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/transport_generic.go @@ -0,0 +1,35 @@ +package dbus + +import ( + "encoding/binary" + "errors" + "io" +) + +type genericTransport struct { + io.ReadWriteCloser +} + +func (t genericTransport) SendNullByte() error { + _, err := t.Write([]byte{0}) + return err +} + +func (t genericTransport) SupportsUnixFDs() bool { + return false +} + +func (t genericTransport) EnableUnixFDs() {} + +func (t genericTransport) ReadMessage() (*Message, error) { + return DecodeMessage(t) +} + +func (t genericTransport) SendMessage(msg *Message) error { + for _, v := range msg.Body { + if _, ok := v.(UnixFD); ok { + return errors.New("dbus: unix fd passing not enabled") + } + } + return msg.EncodeTo(t, binary.LittleEndian) +} diff --git a/src/github.com/guelfey/go.dbus/transport_unix.go b/src/github.com/guelfey/go.dbus/transport_unix.go new file mode 100644 index 0000000..d16229b --- /dev/null +++ b/src/github.com/guelfey/go.dbus/transport_unix.go @@ -0,0 +1,190 @@ +package dbus + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "net" + "syscall" +) + +type oobReader struct { + conn *net.UnixConn + oob []byte + buf [4096]byte +} + +func (o *oobReader) Read(b []byte) (n int, err error) { + n, oobn, flags, _, err := o.conn.ReadMsgUnix(b, o.buf[:]) + if err != nil { + return n, err + } + if flags&syscall.MSG_CTRUNC != 0 { + return n, errors.New("dbus: control data truncated (too many fds received)") + } + o.oob = append(o.oob, o.buf[:oobn]...) + return n, nil +} + +type unixTransport struct { + *net.UnixConn + hasUnixFDs bool +} + +func newUnixTransport(keys string) (transport, error) { + var err error + + t := new(unixTransport) + abstract := getKey(keys, "abstract") + path := getKey(keys, "path") + switch { + case abstract == "" && path == "": + return nil, errors.New("dbus: invalid address (neither path nor abstract set)") + case abstract != "" && path == "": + t.UnixConn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: "@" + abstract, Net: "unix"}) + if err != nil { + return nil, err + } + return t, nil + case abstract == "" && path != "": + t.UnixConn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: path, Net: "unix"}) + if err != nil { + return nil, err + } + return t, nil + default: + return nil, errors.New("dbus: invalid address (both path and abstract set)") + } +} + +func (t *unixTransport) EnableUnixFDs() { + t.hasUnixFDs = true +} + +func (t *unixTransport) ReadMessage() (*Message, error) { + var ( + blen, hlen uint32 + csheader [16]byte + headers []header + order binary.ByteOrder + unixfds uint32 + ) + // To be sure that all bytes of out-of-band data are read, we use a special + // reader that uses ReadUnix on the underlying connection instead of Read + // and gathers the out-of-band data in a buffer. + rd := &oobReader{conn: t.UnixConn} + // read the first 16 bytes (the part of the header that has a constant size), + // from which we can figure out the length of the rest of the message + if _, err := io.ReadFull(rd, csheader[:]); err != nil { + return nil, err + } + switch csheader[0] { + case 'l': + order = binary.LittleEndian + case 'B': + order = binary.BigEndian + default: + return nil, InvalidMessageError("invalid byte order") + } + // csheader[4:8] -> length of message body, csheader[12:16] -> length of + // header fields (without alignment) + binary.Read(bytes.NewBuffer(csheader[4:8]), order, &blen) + binary.Read(bytes.NewBuffer(csheader[12:]), order, &hlen) + if hlen%8 != 0 { + hlen += 8 - (hlen % 8) + } + + // decode headers and look for unix fds + headerdata := make([]byte, hlen+4) + copy(headerdata, csheader[12:]) + if _, err := io.ReadFull(t, headerdata[4:]); err != nil { + return nil, err + } + dec := newDecoder(bytes.NewBuffer(headerdata), order) + dec.pos = 12 + vs, err := dec.Decode(Signature{"a(yv)"}) + if err != nil { + return nil, err + } + Store(vs, &headers) + for _, v := range headers { + if v.Field == byte(FieldUnixFDs) { + unixfds, _ = v.Variant.value.(uint32) + } + } + all := make([]byte, 16+hlen+blen) + copy(all, csheader[:]) + copy(all[16:], headerdata[4:]) + if _, err := io.ReadFull(rd, all[16+hlen:]); err != nil { + return nil, err + } + if unixfds != 0 { + if !t.hasUnixFDs { + return nil, errors.New("dbus: got unix fds on unsupported transport") + } + // read the fds from the OOB data + scms, err := syscall.ParseSocketControlMessage(rd.oob) + if err != nil { + return nil, err + } + if len(scms) != 1 { + return nil, errors.New("dbus: received more than one socket control message") + } + fds, err := syscall.ParseUnixRights(&scms[0]) + if err != nil { + return nil, err + } + msg, err := DecodeMessage(bytes.NewBuffer(all)) + if err != nil { + return nil, err + } + // substitute the values in the message body (which are indices for the + // array receiver via OOB) with the actual values + for i, v := range msg.Body { + if j, ok := v.(UnixFDIndex); ok { + if uint32(j) >= unixfds { + return nil, InvalidMessageError("invalid index for unix fd") + } + msg.Body[i] = UnixFD(fds[j]) + } + } + return msg, nil + } + return DecodeMessage(bytes.NewBuffer(all)) +} + +func (t *unixTransport) SendMessage(msg *Message) error { + fds := make([]int, 0) + for i, v := range msg.Body { + if fd, ok := v.(UnixFD); ok { + msg.Body[i] = UnixFDIndex(len(fds)) + fds = append(fds, int(fd)) + } + } + if len(fds) != 0 { + if !t.hasUnixFDs { + return errors.New("dbus: unix fd passing not enabled") + } + msg.Headers[FieldUnixFDs] = MakeVariant(uint32(len(fds))) + oob := syscall.UnixRights(fds...) + buf := new(bytes.Buffer) + msg.EncodeTo(buf, binary.LittleEndian) + n, oobn, err := t.UnixConn.WriteMsgUnix(buf.Bytes(), oob, nil) + if err != nil { + return err + } + if n != buf.Len() || oobn != len(oob) { + return io.ErrShortWrite + } + } else { + if err := msg.EncodeTo(t, binary.LittleEndian); err != nil { + return nil + } + } + return nil +} + +func (t *unixTransport) SupportsUnixFDs() bool { + return true +} diff --git a/src/github.com/guelfey/go.dbus/transport_unix_test.go b/src/github.com/guelfey/go.dbus/transport_unix_test.go new file mode 100644 index 0000000..302233f --- /dev/null +++ b/src/github.com/guelfey/go.dbus/transport_unix_test.go @@ -0,0 +1,49 @@ +package dbus + +import ( + "os" + "testing" +) + +const testString = `This is a test! +This text should be read from the file that is created by this test.` + +type unixFDTest struct{} + +func (t unixFDTest) Test(fd UnixFD) (string, *Error) { + var b [4096]byte + file := os.NewFile(uintptr(fd), "testfile") + defer file.Close() + n, err := file.Read(b[:]) + if err != nil { + return "", &Error{"com.github.guelfey.test.Error", nil} + } + return string(b[:n]), nil +} + +func TestUnixFDs(t *testing.T) { + conn, err := SessionBus() + if err != nil { + t.Fatal(err) + } + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer w.Close() + if _, err := w.Write([]byte(testString)); err != nil { + t.Fatal(err) + } + name := conn.Names()[0] + test := unixFDTest{} + conn.Export(test, "/com/github/guelfey/test", "com.github.guelfey.test") + var s string + obj := conn.Object(name, "/com/github/guelfey/test") + err = obj.Call("com.github.guelfey.test.Test", 0, UnixFD(r.Fd())).Store(&s) + if err != nil { + t.Fatal(err) + } + if s != testString { + t.Fatal("got", s, "wanted", testString) + } +} diff --git a/src/github.com/guelfey/go.dbus/transport_unixcred.go b/src/github.com/guelfey/go.dbus/transport_unixcred.go new file mode 100644 index 0000000..42a0e76 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/transport_unixcred.go @@ -0,0 +1,22 @@ +// +build !darwin + +package dbus + +import ( + "io" + "os" + "syscall" +) + +func (t *unixTransport) SendNullByte() error { + ucred := &syscall.Ucred{Pid: int32(os.Getpid()), Uid: uint32(os.Getuid()), Gid: uint32(os.Getgid())} + b := syscall.UnixCredentials(ucred) + _, oobn, err := t.UnixConn.WriteMsgUnix([]byte{0}, b, nil) + if err != nil { + return err + } + if oobn != len(b) { + return io.ErrShortWrite + } + return nil +} diff --git a/src/github.com/guelfey/go.dbus/variant.go b/src/github.com/guelfey/go.dbus/variant.go new file mode 100644 index 0000000..b1b53ce --- /dev/null +++ b/src/github.com/guelfey/go.dbus/variant.go @@ -0,0 +1,129 @@ +package dbus + +import ( + "bytes" + "fmt" + "reflect" + "strconv" +) + +// Variant represents the D-Bus variant type. +type Variant struct { + sig Signature + value interface{} +} + +// MakeVariant converts the given value to a Variant. It panics if v cannot be +// represented as a D-Bus type. +func MakeVariant(v interface{}) Variant { + return Variant{SignatureOf(v), v} +} + +// ParseVariant parses the given string as a variant as described at +// https://developer.gnome.org/glib/unstable/gvariant-text.html. If sig is not +// empty, it is taken to be the expected signature for the variant. +func ParseVariant(s string, sig Signature) (Variant, error) { + tokens := varLex(s) + p := &varParser{tokens: tokens} + n, err := varMakeNode(p) + if err != nil { + return Variant{}, err + } + if sig.str == "" { + sig, err = varInfer(n) + if err != nil { + return Variant{}, err + } + } + v, err := n.Value(sig) + if err != nil { + return Variant{}, err + } + return MakeVariant(v), nil +} + +// format returns a formatted version of v and whether this string can be parsed +// unambigously. +func (v Variant) format() (string, bool) { + switch v.sig.str[0] { + case 'b', 'i': + return fmt.Sprint(v.value), true + case 'n', 'q', 'u', 'x', 't', 'd', 'h': + return fmt.Sprint(v.value), false + case 's': + return strconv.Quote(v.value.(string)), true + case 'o': + return strconv.Quote(string(v.value.(ObjectPath))), false + case 'g': + return strconv.Quote(v.value.(Signature).str), false + case 'v': + s, unamb := v.value.(Variant).format() + if !unamb { + return "<@" + v.value.(Variant).sig.str + " " + s + ">", true + } + return "<" + s + ">", true + case 'y': + return fmt.Sprintf("%#x", v.value.(byte)), false + } + rv := reflect.ValueOf(v.value) + switch rv.Kind() { + case reflect.Slice: + if rv.Len() == 0 { + return "[]", false + } + unamb := true + buf := bytes.NewBuffer([]byte("[")) + for i := 0; i < rv.Len(); i++ { + // TODO: slooow + s, b := MakeVariant(rv.Index(i).Interface()).format() + unamb = unamb && b + buf.WriteString(s) + if i != rv.Len()-1 { + buf.WriteString(", ") + } + } + buf.WriteByte(']') + return buf.String(), unamb + case reflect.Map: + if rv.Len() == 0 { + return "{}", false + } + unamb := true + buf := bytes.NewBuffer([]byte("{")) + for i, k := range rv.MapKeys() { + s, b := MakeVariant(k.Interface()).format() + unamb = unamb && b + buf.WriteString(s) + buf.WriteString(": ") + s, b = MakeVariant(rv.MapIndex(k).Interface()).format() + unamb = unamb && b + buf.WriteString(s) + if i != rv.Len()-1 { + buf.WriteString(", ") + } + } + buf.WriteByte('}') + return buf.String(), unamb + } + return `"INVALID"`, true +} + +// Signature returns the D-Bus signature of the underlying value of v. +func (v Variant) Signature() Signature { + return v.sig +} + +// String returns the string representation of the underlying value of v as +// described at https://developer.gnome.org/glib/unstable/gvariant-text.html. +func (v Variant) String() string { + s, unamb := v.format() + if !unamb { + return "@" + v.sig.str + " " + s + } + return s +} + +// Value returns the underlying value of v. +func (v Variant) Value() interface{} { + return v.value +} diff --git a/src/github.com/guelfey/go.dbus/variant_lexer.go b/src/github.com/guelfey/go.dbus/variant_lexer.go new file mode 100644 index 0000000..332007d --- /dev/null +++ b/src/github.com/guelfey/go.dbus/variant_lexer.go @@ -0,0 +1,284 @@ +package dbus + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// Heavily inspired by the lexer from text/template. + +type varToken struct { + typ varTokenType + val string +} + +type varTokenType byte + +const ( + tokEOF varTokenType = iota + tokError + tokNumber + tokString + tokBool + tokArrayStart + tokArrayEnd + tokDictStart + tokDictEnd + tokVariantStart + tokVariantEnd + tokComma + tokColon + tokType + tokByteString +) + +type varLexer struct { + input string + start int + pos int + width int + tokens []varToken +} + +type lexState func(*varLexer) lexState + +func varLex(s string) []varToken { + l := &varLexer{input: s} + l.run() + return l.tokens +} + +func (l *varLexer) accept(valid string) bool { + if strings.IndexRune(valid, l.next()) >= 0 { + return true + } + l.backup() + return false +} + +func (l *varLexer) backup() { + l.pos -= l.width +} + +func (l *varLexer) emit(t varTokenType) { + l.tokens = append(l.tokens, varToken{t, l.input[l.start:l.pos]}) + l.start = l.pos +} + +func (l *varLexer) errorf(format string, v ...interface{}) lexState { + l.tokens = append(l.tokens, varToken{ + tokError, + fmt.Sprintf(format, v...), + }) + return nil +} + +func (l *varLexer) ignore() { + l.start = l.pos +} + +func (l *varLexer) next() rune { + var r rune + + if l.pos >= len(l.input) { + l.width = 0 + return -1 + } + r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) + l.pos += l.width + return r +} + +func (l *varLexer) run() { + for state := varLexNormal; state != nil; { + state = state(l) + } +} + +func (l *varLexer) peek() rune { + r := l.next() + l.backup() + return r +} + +func varLexNormal(l *varLexer) lexState { + for { + r := l.next() + switch { + case r == -1: + l.emit(tokEOF) + return nil + case r == '[': + l.emit(tokArrayStart) + case r == ']': + l.emit(tokArrayEnd) + case r == '{': + l.emit(tokDictStart) + case r == '}': + l.emit(tokDictEnd) + case r == '<': + l.emit(tokVariantStart) + case r == '>': + l.emit(tokVariantEnd) + case r == ':': + l.emit(tokColon) + case r == ',': + l.emit(tokComma) + case r == '\'' || r == '"': + l.backup() + return varLexString + case r == '@': + l.backup() + return varLexType + case unicode.IsSpace(r): + l.ignore() + case unicode.IsNumber(r) || r == '+' || r == '-': + l.backup() + return varLexNumber + case r == 'b': + pos := l.start + if n := l.peek(); n == '"' || n == '\'' { + return varLexByteString + } + // not a byte string; try to parse it as a type or bool below + l.pos = pos + 1 + l.width = 1 + fallthrough + default: + // either a bool or a type. Try bools first. + l.backup() + if l.pos+4 <= len(l.input) { + if l.input[l.pos:l.pos+4] == "true" { + l.pos += 4 + l.emit(tokBool) + continue + } + } + if l.pos+5 <= len(l.input) { + if l.input[l.pos:l.pos+5] == "false" { + l.pos += 5 + l.emit(tokBool) + continue + } + } + // must be a type. + return varLexType + } + } +} + +var varTypeMap = map[string]string{ + "boolean": "b", + "byte": "y", + "int16": "n", + "uint16": "q", + "int32": "i", + "uint32": "u", + "int64": "x", + "uint64": "t", + "double": "f", + "string": "s", + "objectpath": "o", + "signature": "g", +} + +func varLexByteString(l *varLexer) lexState { + q := l.next() +Loop: + for { + switch l.next() { + case '\\': + if r := l.next(); r != -1 { + break + } + fallthrough + case -1: + return l.errorf("unterminated bytestring") + case q: + break Loop + } + } + l.emit(tokByteString) + return varLexNormal +} + +func varLexNumber(l *varLexer) lexState { + l.accept("+-") + digits := "0123456789" + if l.accept("0") { + if l.accept("x") { + digits = "0123456789abcdefABCDEF" + } else { + digits = "01234567" + } + } + for strings.IndexRune(digits, l.next()) >= 0 { + } + l.backup() + if l.accept(".") { + for strings.IndexRune(digits, l.next()) >= 0 { + } + l.backup() + } + if l.accept("eE") { + l.accept("+-") + for strings.IndexRune("0123456789", l.next()) >= 0 { + } + l.backup() + } + if r := l.peek(); unicode.IsLetter(r) { + l.next() + return l.errorf("bad number syntax: %q", l.input[l.start:l.pos]) + } + l.emit(tokNumber) + return varLexNormal +} + +func varLexString(l *varLexer) lexState { + q := l.next() +Loop: + for { + switch l.next() { + case '\\': + if r := l.next(); r != -1 { + break + } + fallthrough + case -1: + return l.errorf("unterminated string") + case q: + break Loop + } + } + l.emit(tokString) + return varLexNormal +} + +func varLexType(l *varLexer) lexState { + at := l.accept("@") + for { + r := l.next() + if r == -1 { + break + } + if unicode.IsSpace(r) { + l.backup() + break + } + } + if at { + if _, err := ParseSignature(l.input[l.start+1 : l.pos]); err != nil { + return l.errorf("%s", err) + } + } else { + if _, ok := varTypeMap[l.input[l.start:l.pos]]; ok { + l.emit(tokType) + return varLexNormal + } + return l.errorf("unrecognized type %q", l.input[l.start:l.pos]) + } + l.emit(tokType) + return varLexNormal +} diff --git a/src/github.com/guelfey/go.dbus/variant_parser.go b/src/github.com/guelfey/go.dbus/variant_parser.go new file mode 100644 index 0000000..d20f5da --- /dev/null +++ b/src/github.com/guelfey/go.dbus/variant_parser.go @@ -0,0 +1,817 @@ +package dbus + +import ( + "bytes" + "errors" + "fmt" + "io" + "reflect" + "strconv" + "strings" + "unicode/utf8" +) + +type varParser struct { + tokens []varToken + i int +} + +func (p *varParser) backup() { + p.i-- +} + +func (p *varParser) next() varToken { + if p.i < len(p.tokens) { + t := p.tokens[p.i] + p.i++ + return t + } + return varToken{typ: tokEOF} +} + +type varNode interface { + Infer() (Signature, error) + String() string + Sigs() sigSet + Value(Signature) (interface{}, error) +} + +func varMakeNode(p *varParser) (varNode, error) { + var sig Signature + + for { + t := p.next() + switch t.typ { + case tokEOF: + return nil, io.ErrUnexpectedEOF + case tokError: + return nil, errors.New(t.val) + case tokNumber: + return varMakeNumNode(t, sig) + case tokString: + return varMakeStringNode(t, sig) + case tokBool: + if sig.str != "" && sig.str != "b" { + return nil, varTypeError{t.val, sig} + } + b, err := strconv.ParseBool(t.val) + if err != nil { + return nil, err + } + return boolNode(b), nil + case tokArrayStart: + return varMakeArrayNode(p, sig) + case tokVariantStart: + return varMakeVariantNode(p, sig) + case tokDictStart: + return varMakeDictNode(p, sig) + case tokType: + if sig.str != "" { + return nil, errors.New("unexpected type annotation") + } + if t.val[0] == '@' { + sig.str = t.val[1:] + } else { + sig.str = varTypeMap[t.val] + } + case tokByteString: + if sig.str != "" && sig.str != "ay" { + return nil, varTypeError{t.val, sig} + } + b, err := varParseByteString(t.val) + if err != nil { + return nil, err + } + return byteStringNode(b), nil + default: + return nil, fmt.Errorf("unexpected %q", t.val) + } + } +} + +type varTypeError struct { + val string + sig Signature +} + +func (e varTypeError) Error() string { + return fmt.Sprintf("dbus: can't parse %q as type %q", e.val, e.sig.str) +} + +type sigSet map[Signature]bool + +func (s sigSet) Empty() bool { + return len(s) == 0 +} + +func (s sigSet) Intersect(s2 sigSet) sigSet { + r := make(sigSet) + for k := range s { + if s2[k] { + r[k] = true + } + } + return r +} + +func (s sigSet) Single() (Signature, bool) { + if len(s) == 1 { + for k := range s { + return k, true + } + } + return Signature{}, false +} + +func (s sigSet) ToArray() sigSet { + r := make(sigSet, len(s)) + for k := range s { + r[Signature{"a" + k.str}] = true + } + return r +} + +type numNode struct { + sig Signature + str string + val interface{} +} + +var numSigSet = sigSet{ + Signature{"y"}: true, + Signature{"n"}: true, + Signature{"q"}: true, + Signature{"i"}: true, + Signature{"u"}: true, + Signature{"x"}: true, + Signature{"t"}: true, + Signature{"d"}: true, +} + +func (n numNode) Infer() (Signature, error) { + if strings.ContainsAny(n.str, ".e") { + return Signature{"d"}, nil + } + return Signature{"i"}, nil +} + +func (n numNode) String() string { + return n.str +} + +func (n numNode) Sigs() sigSet { + if n.sig.str != "" { + return sigSet{n.sig: true} + } + if strings.ContainsAny(n.str, ".e") { + return sigSet{Signature{"d"}: true} + } + return numSigSet +} + +func (n numNode) Value(sig Signature) (interface{}, error) { + if n.sig.str != "" && n.sig != sig { + return nil, varTypeError{n.str, sig} + } + if n.val != nil { + return n.val, nil + } + return varNumAs(n.str, sig) +} + +func varMakeNumNode(tok varToken, sig Signature) (varNode, error) { + if sig.str == "" { + return numNode{str: tok.val}, nil + } + num, err := varNumAs(tok.val, sig) + if err != nil { + return nil, err + } + return numNode{sig: sig, val: num}, nil +} + +func varNumAs(s string, sig Signature) (interface{}, error) { + isUnsigned := false + size := 32 + switch sig.str { + case "n": + size = 16 + case "i": + case "x": + size = 64 + case "y": + size = 8 + isUnsigned = true + case "q": + size = 16 + isUnsigned = true + case "u": + isUnsigned = true + case "t": + size = 64 + isUnsigned = true + case "d": + d, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, err + } + return d, nil + default: + return nil, varTypeError{s, sig} + } + base := 10 + if strings.HasPrefix(s, "0x") { + base = 16 + s = s[2:] + } + if strings.HasPrefix(s, "0") && len(s) != 1 { + base = 8 + s = s[1:] + } + if isUnsigned { + i, err := strconv.ParseUint(s, base, size) + if err != nil { + return nil, err + } + var v interface{} = i + switch sig.str { + case "y": + v = byte(i) + case "q": + v = uint16(i) + case "u": + v = uint32(i) + } + return v, nil + } + i, err := strconv.ParseInt(s, base, size) + if err != nil { + return nil, err + } + var v interface{} = i + switch sig.str { + case "n": + v = int16(i) + case "i": + v = int32(i) + } + return v, nil +} + +type stringNode struct { + sig Signature + str string // parsed + val interface{} // has correct type +} + +var stringSigSet = sigSet{ + Signature{"s"}: true, + Signature{"g"}: true, + Signature{"o"}: true, +} + +func (n stringNode) Infer() (Signature, error) { + return Signature{"s"}, nil +} + +func (n stringNode) String() string { + return n.str +} + +func (n stringNode) Sigs() sigSet { + if n.sig.str != "" { + return sigSet{n.sig: true} + } + return stringSigSet +} + +func (n stringNode) Value(sig Signature) (interface{}, error) { + if n.sig.str != "" && n.sig != sig { + return nil, varTypeError{n.str, sig} + } + if n.val != nil { + return n.val, nil + } + switch { + case sig.str == "g": + return Signature{n.str}, nil + case sig.str == "o": + return ObjectPath(n.str), nil + case sig.str == "s": + return n.str, nil + default: + return nil, varTypeError{n.str, sig} + } +} + +func varMakeStringNode(tok varToken, sig Signature) (varNode, error) { + if sig.str != "" && sig.str != "s" && sig.str != "g" && sig.str != "o" { + return nil, fmt.Errorf("invalid type %q for string", sig.str) + } + s, err := varParseString(tok.val) + if err != nil { + return nil, err + } + n := stringNode{str: s} + if sig.str == "" { + return stringNode{str: s}, nil + } + n.sig = sig + switch sig.str { + case "o": + n.val = ObjectPath(s) + case "g": + n.val = Signature{s} + case "s": + n.val = s + } + return n, nil +} + +func varParseString(s string) (string, error) { + // quotes are guaranteed to be there + s = s[1 : len(s)-1] + buf := new(bytes.Buffer) + for len(s) != 0 { + r, size := utf8.DecodeRuneInString(s) + if r == utf8.RuneError && size == 1 { + return "", errors.New("invalid UTF-8") + } + s = s[size:] + if r != '\\' { + buf.WriteRune(r) + continue + } + r, size = utf8.DecodeRuneInString(s) + if r == utf8.RuneError && size == 1 { + return "", errors.New("invalid UTF-8") + } + s = s[size:] + switch r { + case 'a': + buf.WriteRune(0x7) + case 'b': + buf.WriteRune(0x8) + case 'f': + buf.WriteRune(0xc) + case 'n': + buf.WriteRune('\n') + case 'r': + buf.WriteRune('\r') + case 't': + buf.WriteRune('\t') + case '\n': + case 'u': + if len(s) < 4 { + return "", errors.New("short unicode escape") + } + r, err := strconv.ParseUint(s[:4], 16, 32) + if err != nil { + return "", err + } + buf.WriteRune(rune(r)) + s = s[4:] + case 'U': + if len(s) < 8 { + return "", errors.New("short unicode escape") + } + r, err := strconv.ParseUint(s[:8], 16, 32) + if err != nil { + return "", err + } + buf.WriteRune(rune(r)) + s = s[8:] + default: + buf.WriteRune(r) + } + } + return buf.String(), nil +} + +var boolSigSet = sigSet{Signature{"b"}: true} + +type boolNode bool + +func (boolNode) Infer() (Signature, error) { + return Signature{"b"}, nil +} + +func (b boolNode) String() string { + if b { + return "true" + } + return "false" +} + +func (boolNode) Sigs() sigSet { + return boolSigSet +} + +func (b boolNode) Value(sig Signature) (interface{}, error) { + if sig.str != "b" { + return nil, varTypeError{b.String(), sig} + } + return bool(b), nil +} + +type arrayNode struct { + set sigSet + children []varNode + val interface{} +} + +func (n arrayNode) Infer() (Signature, error) { + for _, v := range n.children { + csig, err := varInfer(v) + if err != nil { + continue + } + return Signature{"a" + csig.str}, nil + } + return Signature{}, fmt.Errorf("can't infer type for %q", n.String()) +} + +func (n arrayNode) String() string { + s := "[" + for i, v := range n.children { + s += v.String() + if i != len(n.children)-1 { + s += ", " + } + } + return s + "]" +} + +func (n arrayNode) Sigs() sigSet { + return n.set +} + +func (n arrayNode) Value(sig Signature) (interface{}, error) { + if n.set.Empty() { + // no type information whatsoever, so this must be an empty slice + return reflect.MakeSlice(typeFor(sig.str), 0, 0).Interface(), nil + } + if !n.set[sig] { + return nil, varTypeError{n.String(), sig} + } + s := reflect.MakeSlice(typeFor(sig.str), len(n.children), len(n.children)) + for i, v := range n.children { + rv, err := v.Value(Signature{sig.str[1:]}) + if err != nil { + return nil, err + } + s.Index(i).Set(reflect.ValueOf(rv)) + } + return s.Interface(), nil +} + +func varMakeArrayNode(p *varParser, sig Signature) (varNode, error) { + var n arrayNode + if sig.str != "" { + n.set = sigSet{sig: true} + } + if t := p.next(); t.typ == tokArrayEnd { + return n, nil + } else { + p.backup() + } +Loop: + for { + t := p.next() + switch t.typ { + case tokEOF: + return nil, io.ErrUnexpectedEOF + case tokError: + return nil, errors.New(t.val) + } + p.backup() + cn, err := varMakeNode(p) + if err != nil { + return nil, err + } + if cset := cn.Sigs(); !cset.Empty() { + if n.set.Empty() { + n.set = cset.ToArray() + } else { + nset := cset.ToArray().Intersect(n.set) + if nset.Empty() { + return nil, fmt.Errorf("can't parse %q with given type information", cn.String()) + } + n.set = nset + } + } + n.children = append(n.children, cn) + switch t := p.next(); t.typ { + case tokEOF: + return nil, io.ErrUnexpectedEOF + case tokError: + return nil, errors.New(t.val) + case tokArrayEnd: + break Loop + case tokComma: + continue + default: + return nil, fmt.Errorf("unexpected %q", t.val) + } + } + return n, nil +} + +type variantNode struct { + n varNode +} + +var variantSet = sigSet{ + Signature{"v"}: true, +} + +func (variantNode) Infer() (Signature, error) { + return Signature{"v"}, nil +} + +func (n variantNode) String() string { + return "<" + n.n.String() + ">" +} + +func (variantNode) Sigs() sigSet { + return variantSet +} + +func (n variantNode) Value(sig Signature) (interface{}, error) { + if sig.str != "v" { + return nil, varTypeError{n.String(), sig} + } + sig, err := varInfer(n.n) + if err != nil { + return nil, err + } + v, err := n.n.Value(sig) + if err != nil { + return nil, err + } + return MakeVariant(v), nil +} + +func varMakeVariantNode(p *varParser, sig Signature) (varNode, error) { + n, err := varMakeNode(p) + if err != nil { + return nil, err + } + if t := p.next(); t.typ != tokVariantEnd { + return nil, fmt.Errorf("unexpected %q", t.val) + } + vn := variantNode{n} + if sig.str != "" && sig.str != "v" { + return nil, varTypeError{vn.String(), sig} + } + return variantNode{n}, nil +} + +type dictEntry struct { + key, val varNode +} + +type dictNode struct { + kset, vset sigSet + children []dictEntry + val interface{} +} + +func (n dictNode) Infer() (Signature, error) { + for _, v := range n.children { + ksig, err := varInfer(v.key) + if err != nil { + continue + } + vsig, err := varInfer(v.val) + if err != nil { + continue + } + return Signature{"a{" + ksig.str + vsig.str + "}"}, nil + } + return Signature{}, fmt.Errorf("can't infer type for %q", n.String()) +} + +func (n dictNode) String() string { + s := "{" + for i, v := range n.children { + s += v.key.String() + ": " + v.val.String() + if i != len(n.children)-1 { + s += ", " + } + } + return s + "}" +} + +func (n dictNode) Sigs() sigSet { + r := sigSet{} + for k := range n.kset { + for v := range n.vset { + sig := "a{" + k.str + v.str + "}" + r[Signature{sig}] = true + } + } + return r +} + +func (n dictNode) Value(sig Signature) (interface{}, error) { + set := n.Sigs() + if set.Empty() { + // no type information -> empty dict + return reflect.MakeMap(typeFor(sig.str)).Interface(), nil + } + if !set[sig] { + return nil, varTypeError{n.String(), sig} + } + m := reflect.MakeMap(typeFor(sig.str)) + ksig := Signature{sig.str[2:3]} + vsig := Signature{sig.str[3 : len(sig.str)-1]} + for _, v := range n.children { + kv, err := v.key.Value(ksig) + if err != nil { + return nil, err + } + vv, err := v.val.Value(vsig) + if err != nil { + return nil, err + } + m.SetMapIndex(reflect.ValueOf(kv), reflect.ValueOf(vv)) + } + return m.Interface(), nil +} + +func varMakeDictNode(p *varParser, sig Signature) (varNode, error) { + var n dictNode + + if sig.str != "" { + if len(sig.str) < 5 { + return nil, fmt.Errorf("invalid signature %q for dict type", sig) + } + ksig := Signature{string(sig.str[2])} + vsig := Signature{sig.str[3 : len(sig.str)-1]} + n.kset = sigSet{ksig: true} + n.vset = sigSet{vsig: true} + } + if t := p.next(); t.typ == tokDictEnd { + return n, nil + } else { + p.backup() + } +Loop: + for { + t := p.next() + switch t.typ { + case tokEOF: + return nil, io.ErrUnexpectedEOF + case tokError: + return nil, errors.New(t.val) + } + p.backup() + kn, err := varMakeNode(p) + if err != nil { + return nil, err + } + if kset := kn.Sigs(); !kset.Empty() { + if n.kset.Empty() { + n.kset = kset + } else { + n.kset = kset.Intersect(n.kset) + if n.kset.Empty() { + return nil, fmt.Errorf("can't parse %q with given type information", kn.String()) + } + } + } + t = p.next() + switch t.typ { + case tokEOF: + return nil, io.ErrUnexpectedEOF + case tokError: + return nil, errors.New(t.val) + case tokColon: + default: + return nil, fmt.Errorf("unexpected %q", t.val) + } + t = p.next() + switch t.typ { + case tokEOF: + return nil, io.ErrUnexpectedEOF + case tokError: + return nil, errors.New(t.val) + } + p.backup() + vn, err := varMakeNode(p) + if err != nil { + return nil, err + } + if vset := vn.Sigs(); !vset.Empty() { + if n.vset.Empty() { + n.vset = vset + } else { + n.vset = n.vset.Intersect(vset) + if n.vset.Empty() { + return nil, fmt.Errorf("can't parse %q with given type information", vn.String()) + } + } + } + n.children = append(n.children, dictEntry{kn, vn}) + t = p.next() + switch t.typ { + case tokEOF: + return nil, io.ErrUnexpectedEOF + case tokError: + return nil, errors.New(t.val) + case tokDictEnd: + break Loop + case tokComma: + continue + default: + return nil, fmt.Errorf("unexpected %q", t.val) + } + } + return n, nil +} + +type byteStringNode []byte + +var byteStringSet = sigSet{ + Signature{"ay"}: true, +} + +func (byteStringNode) Infer() (Signature, error) { + return Signature{"ay"}, nil +} + +func (b byteStringNode) String() string { + return string(b) +} + +func (b byteStringNode) Sigs() sigSet { + return byteStringSet +} + +func (b byteStringNode) Value(sig Signature) (interface{}, error) { + if sig.str != "ay" { + return nil, varTypeError{b.String(), sig} + } + return []byte(b), nil +} + +func varParseByteString(s string) ([]byte, error) { + // quotes and b at start are guaranteed to be there + b := make([]byte, 0, 1) + s = s[2 : len(s)-1] + for len(s) != 0 { + c := s[0] + s = s[1:] + if c != '\\' { + b = append(b, c) + continue + } + c = s[0] + s = s[1:] + switch c { + case 'a': + b = append(b, 0x7) + case 'b': + b = append(b, 0x8) + case 'f': + b = append(b, 0xc) + case 'n': + b = append(b, '\n') + case 'r': + b = append(b, '\r') + case 't': + b = append(b, '\t') + case 'x': + if len(s) < 2 { + return nil, errors.New("short escape") + } + n, err := strconv.ParseUint(s[:2], 16, 8) + if err != nil { + return nil, err + } + b = append(b, byte(n)) + s = s[2:] + case '0': + if len(s) < 3 { + return nil, errors.New("short escape") + } + n, err := strconv.ParseUint(s[:3], 8, 8) + if err != nil { + return nil, err + } + b = append(b, byte(n)) + s = s[3:] + default: + b = append(b, c) + } + } + return append(b, 0), nil +} + +func varInfer(n varNode) (Signature, error) { + if sig, ok := n.Sigs().Single(); ok { + return sig, nil + } + return n.Infer() +} diff --git a/src/github.com/guelfey/go.dbus/variant_test.go b/src/github.com/guelfey/go.dbus/variant_test.go new file mode 100644 index 0000000..da917c8 --- /dev/null +++ b/src/github.com/guelfey/go.dbus/variant_test.go @@ -0,0 +1,78 @@ +package dbus + +import "reflect" +import "testing" + +var variantFormatTests = []struct { + v interface{} + s string +}{ + {int32(1), `1`}, + {"foo", `"foo"`}, + {ObjectPath("/org/foo"), `@o "/org/foo"`}, + {Signature{"i"}, `@g "i"`}, + {[]byte{}, `@ay []`}, + {[]int32{1, 2}, `[1, 2]`}, + {[]int64{1, 2}, `@ax [1, 2]`}, + {[][]int32{{3, 4}, {5, 6}}, `[[3, 4], [5, 6]]`}, + {[]Variant{MakeVariant(int32(1)), MakeVariant(1.0)}, `[<1>, <@d 1>]`}, + {map[string]int32{"one": 1, "two": 2}, `{"one": 1, "two": 2}`}, + {map[int32]ObjectPath{1: "/org/foo"}, `@a{io} {1: "/org/foo"}`}, + {map[string]Variant{}, `@a{sv} {}`}, +} + +func TestFormatVariant(t *testing.T) { + for i, v := range variantFormatTests { + if s := MakeVariant(v.v).String(); s != v.s { + t.Errorf("test %d: got %q, wanted %q", i+1, s, v.s) + } + } +} + +var variantParseTests = []struct { + s string + v interface{} +}{ + {"1", int32(1)}, + {"true", true}, + {"false", false}, + {"1.0", float64(1.0)}, + {"0x10", int32(16)}, + {"1e1", float64(10)}, + {`"foo"`, "foo"}, + {`"\a\b\f\n\r\t"`, "\x07\x08\x0c\n\r\t"}, + {`"\u00e4\U0001f603"`, "\u00e4\U0001f603"}, + {"[1]", []int32{1}}, + {"[1, 2, 3]", []int32{1, 2, 3}}, + {"@ai []", []int32{}}, + {"[1, 5.0]", []float64{1, 5.0}}, + {"[[1, 2], [3, 4.0]]", [][]float64{{1, 2}, {3, 4}}}, + {`[@o "/org/foo", "/org/bar"]`, []ObjectPath{"/org/foo", "/org/bar"}}, + {"<1>", MakeVariant(int32(1))}, + {"[<1>, <2.0>]", []Variant{MakeVariant(int32(1)), MakeVariant(2.0)}}, + {`[[], [""]]`, [][]string{{}, {""}}}, + {`@a{ss} {}`, map[string]string{}}, + {`{"foo": 1}`, map[string]int32{"foo": 1}}, + {`[{}, {"foo": "bar"}]`, []map[string]string{{}, {"foo": "bar"}}}, + {`{"a": <1>, "b": <"foo">}`, + map[string]Variant{"a": MakeVariant(int32(1)), "b": MakeVariant("foo")}}, + {`b''`, []byte{0}}, + {`b"abc"`, []byte{'a', 'b', 'c', 0}}, + {`b"\x01\0002\a\b\f\n\r\t"`, []byte{1, 2, 0x7, 0x8, 0xc, '\n', '\r', '\t', 0}}, + {`[[0], b""]`, [][]byte{{0}, {0}}}, + {"int16 0", int16(0)}, + {"byte 0", byte(0)}, +} + +func TestParseVariant(t *testing.T) { + for i, v := range variantParseTests { + nv, err := ParseVariant(v.s, Signature{}) + if err != nil { + t.Errorf("test %d: parsing failed: %s", i+1, err) + continue + } + if !reflect.DeepEqual(nv.value, v.v) { + t.Errorf("test %d: got %q, wanted %q", i+1, nv, v.v) + } + } +} diff --git a/src/launchpad.net/goyaml/.bzr/README b/src/launchpad.net/goyaml/.bzr/README deleted file mode 100644 index f82dc1c..0000000 --- a/src/launchpad.net/goyaml/.bzr/README +++ /dev/null @@ -1,3 +0,0 @@ -This is a Bazaar control directory. -Do not change any files in this directory. -See http://bazaar.canonical.com/ for more information about Bazaar. diff --git a/src/launchpad.net/goyaml/.bzr/branch-format b/src/launchpad.net/goyaml/.bzr/branch-format deleted file mode 100644 index 9eb09b7..0000000 --- a/src/launchpad.net/goyaml/.bzr/branch-format +++ /dev/null @@ -1 +0,0 @@ -Bazaar-NG meta directory, format 1 diff --git a/src/launchpad.net/goyaml/.bzr/branch/branch.conf b/src/launchpad.net/goyaml/.bzr/branch/branch.conf deleted file mode 100644 index dae50f0..0000000 --- a/src/launchpad.net/goyaml/.bzr/branch/branch.conf +++ /dev/null @@ -1 +0,0 @@ -parent_location = http://bazaar.launchpad.net/~goyaml/goyaml/trunk/ diff --git a/src/launchpad.net/goyaml/.bzr/branch/format b/src/launchpad.net/goyaml/.bzr/branch/format deleted file mode 100644 index dc392f4..0000000 --- a/src/launchpad.net/goyaml/.bzr/branch/format +++ /dev/null @@ -1 +0,0 @@ -Bazaar Branch Format 7 (needs bzr 1.6) diff --git a/src/launchpad.net/goyaml/.bzr/branch/last-revision b/src/launchpad.net/goyaml/.bzr/branch/last-revision deleted file mode 100644 index 60d7078..0000000 --- a/src/launchpad.net/goyaml/.bzr/branch/last-revision +++ /dev/null @@ -1 +0,0 @@ -50 gustavo@niemeyer.net-20131114120802-abe042syx64z2m7s diff --git a/src/launchpad.net/goyaml/.bzr/branch/tags b/src/launchpad.net/goyaml/.bzr/branch/tags deleted file mode 100644 index e69de29..0000000 diff --git a/src/launchpad.net/goyaml/.bzr/checkout/conflicts b/src/launchpad.net/goyaml/.bzr/checkout/conflicts deleted file mode 100644 index 0dc2d3a..0000000 --- a/src/launchpad.net/goyaml/.bzr/checkout/conflicts +++ /dev/null @@ -1 +0,0 @@ -BZR conflict list format 1 diff --git a/src/launchpad.net/goyaml/.bzr/checkout/dirstate b/src/launchpad.net/goyaml/.bzr/checkout/dirstate deleted file mode 100644 index b16c978..0000000 Binary files a/src/launchpad.net/goyaml/.bzr/checkout/dirstate and /dev/null differ diff --git a/src/launchpad.net/goyaml/.bzr/checkout/format b/src/launchpad.net/goyaml/.bzr/checkout/format deleted file mode 100644 index e0261c7..0000000 --- a/src/launchpad.net/goyaml/.bzr/checkout/format +++ /dev/null @@ -1 +0,0 @@ -Bazaar Working Tree Format 6 (bzr 1.14) diff --git a/src/launchpad.net/goyaml/.bzr/checkout/views b/src/launchpad.net/goyaml/.bzr/checkout/views deleted file mode 100644 index e69de29..0000000 diff --git a/src/launchpad.net/goyaml/.bzr/repository/format b/src/launchpad.net/goyaml/.bzr/repository/format deleted file mode 100644 index b200528..0000000 --- a/src/launchpad.net/goyaml/.bzr/repository/format +++ /dev/null @@ -1 +0,0 @@ -Bazaar repository format 2a (needs bzr 1.16 or later) diff --git a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.cix b/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.cix deleted file mode 100644 index 6eafadb..0000000 Binary files a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.cix and /dev/null differ diff --git a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.iix b/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.iix deleted file mode 100644 index 4ae95e2..0000000 Binary files a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.iix and /dev/null differ diff --git a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.rix b/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.rix deleted file mode 100644 index d45bd29..0000000 Binary files a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.rix and /dev/null differ diff --git a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.six b/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.six deleted file mode 100644 index a2afde6..0000000 --- a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.six +++ /dev/null @@ -1,5 +0,0 @@ -B+Tree Graph Index 2 -node_ref_lists=0 -key_elements=1 -len=0 -row_lengths= diff --git a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.tix b/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.tix deleted file mode 100644 index 26bac85..0000000 Binary files a/src/launchpad.net/goyaml/.bzr/repository/indices/b169e2681d8f14f61485de84c0983197.tix and /dev/null differ diff --git a/src/launchpad.net/goyaml/.bzr/repository/pack-names b/src/launchpad.net/goyaml/.bzr/repository/pack-names deleted file mode 100644 index 21dc49d..0000000 --- a/src/launchpad.net/goyaml/.bzr/repository/pack-names +++ /dev/null @@ -1,6 +0,0 @@ -B+Tree Graph Index 2 -node_ref_lists=0 -key_elements=1 -len=1 -row_lengths=1 -x1 PgOCĩ=yy:1"y8oP+].+E Npp1!| \ No newline at end of file diff --git a/src/launchpad.net/goyaml/.bzr/repository/packs/b169e2681d8f14f61485de84c0983197.pack b/src/launchpad.net/goyaml/.bzr/repository/packs/b169e2681d8f14f61485de84c0983197.pack deleted file mode 100644 index 8708f72..0000000 Binary files a/src/launchpad.net/goyaml/.bzr/repository/packs/b169e2681d8f14f61485de84c0983197.pack and /dev/null differ