Merge lp:~chipaca/ubuntu-push/coverage-on-bus-testing into lp:ubuntu-push

Proposed by John Lenton
Status: Superseded
Proposed branch: lp:~chipaca/ubuntu-push/coverage-on-bus-testing
Merge into: lp:ubuntu-push
Diff against target: 7738 lines (+4222/-751)
71 files modified
.bzrignore (+9/-0)
Makefile (+28/-3)
PACKAGE_DEPS (+10/-0)
README (+17/-5)
bus/connectivity/connectivity.go (+8/-7)
bus/connectivity/connectivity_test.go (+80/-3)
bus/endpoint.go (+105/-1)
bus/endpoint_test.go (+1/-1)
bus/notifications/raw.go (+5/-0)
bus/notifications/raw_test.go (+6/-0)
bus/testing/testing_endpoint.go (+41/-3)
bus/testing/testing_endpoint_test.go (+66/-1)
client/client.go (+120/-36)
client/client_test.go (+183/-50)
client/service/service.go (+209/-0)
client/service/service_test.go (+247/-0)
client/session/seenstate/seenstate.go (+47/-21)
client/session/seenstate/seenstate_test.go (+54/-23)
client/session/seenstate/sqlseenstate.go (+46/-11)
client/session/seenstate/sqlseenstate_test.go (+101/-23)
client/session/session.go (+95/-29)
client/session/session_test.go (+253/-33)
config/config.go (+24/-7)
config/config_test.go (+35/-7)
debian/changelog (+6/-0)
debian/control (+2/-0)
debian/rules (+5/-1)
debian/ubuntu-push-client.install (+1/-0)
dependencies.tsv (+1/-0)
nih/cnih/cnih.go (+28/-0)
nih/nih.go (+68/-0)
nih/nih_test.go (+57/-0)
protocol/messages.go (+83/-1)
protocol/messages_test.go (+92/-2)
protocol/state-diag-client.gv (+4/-1)
protocol/state-diag-client.svg (+77/-49)
protocol/state-diag-session.gv (+10/-0)
protocol/state-diag-session.svg (+132/-74)
scripts/broadcast (+59/-0)
scripts/deps.sh (+28/-0)
scripts/unicast (+65/-0)
server/acceptance/acceptance_test.go (+3/-0)
server/acceptance/acceptanceclient.go (+22/-1)
server/acceptance/cmd/acceptanceclient.go (+26/-18)
server/acceptance/suites/broadcast.go (+19/-9)
server/acceptance/suites/suite.go (+12/-0)
server/acceptance/suites/unicast.go (+149/-0)
server/api/handlers.go (+103/-21)
server/api/handlers_test.go (+214/-12)
server/broker/broker.go (+11/-1)
server/broker/exchanges.go (+108/-42)
server/broker/exchanges_test.go (+217/-26)
server/broker/exchg_impl_test.go (+19/-17)
server/broker/simple/simple.go (+61/-32)
server/broker/simple/simple_test.go (+3/-38)
server/broker/simple/suite_test.go (+3/-0)
server/broker/testing/impls.go (+21/-0)
server/broker/testsuite/suite.go (+153/-38)
server/session/session.go (+23/-11)
server/session/session_test.go (+63/-34)
server/session/tracker.go (+12/-5)
server/session/tracker_test.go (+4/-4)
server/store/inmemory.go (+52/-18)
server/store/inmemory_test.go (+72/-3)
server/store/store.go (+74/-3)
server/store/store_test.go (+46/-1)
signing-helper/CMakeLists.txt (+39/-0)
signing-helper/signing-helper.cpp (+97/-0)
signing-helper/signing.h (+76/-0)
testing/helpers.go (+11/-0)
ubuntu-push-client.go (+1/-25)
To merge this branch: bzr merge lp:~chipaca/ubuntu-push/coverage-on-bus-testing
Reviewer Review Type Date Requested Status
Ubuntu Push Hackers Pending
Review via email: mp+221363@code.launchpad.net

This proposal has been superseded by a proposal from 2014-05-29.

Commit message

100% coverage on bus/testing.

Description of the change

100% coverage on bus/testing, fwiw.

To post a comment you must log in.
173. By John Lenton

fixed comment on test

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2014-03-31 16:43:15 +0000
+++ .bzrignore 2014-05-29 12:04:32 +0000
@@ -13,3 +13,12 @@
13debian/*.substvars13debian/*.substvars
14ubuntu-push-client14ubuntu-push-client
15push-server-dev15push-server-dev
16signing-helper/CMakeCache.txt
17signing-helper/CMakeFiles
18signing-helper/Makefile
19signing-helper/cmake_install.cmake
20signing-helper/moc_signing.cpp
21signing-helper/signing-helper
22signing-helper/signing-helper_automoc.cpp
23.has-fetched-deps
24.*.deps
1625
=== modified file 'Makefile'
--- Makefile 2014-03-31 17:58:54 +0000
+++ Makefile 2014-05-29 12:04:32 +0000
@@ -11,10 +11,21 @@
11GODEPS += launchpad.net/go-dbus/v111GODEPS += launchpad.net/go-dbus/v1
12GODEPS += launchpad.net/go-xdg/v012GODEPS += launchpad.net/go-xdg/v0
13GODEPS += code.google.com/p/gosqlite/sqlite313GODEPS += code.google.com/p/gosqlite/sqlite3
14GODEPS += launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid/uuid
1415
15TOTEST = $(shell env GOPATH=$(GOPATH) go list $(PROJECT)/...|grep -v acceptance|grep -v http13client )16TOTEST = $(shell env GOPATH=$(GOPATH) go list $(PROJECT)/...|grep -v acceptance|grep -v http13client )
1617
18fetchdeps: .has-fetched-deps
19
20.has-fetched-deps: PACKAGE_DEPS
21 @$(MAKE) --no-print-directory refetchdeps
22 @touch $@
23
24refetchdeps:
25 sudo apt-get install $$( grep -v '^#' PACKAGE_DEPS )
26
17bootstrap:27bootstrap:
28 $(RM) -r $(GOPATH)/pkg
18 mkdir -p $(GOPATH)/bin29 mkdir -p $(GOPATH)/bin
19 mkdir -p $(GOPATH)/pkg30 mkdir -p $(GOPATH)/pkg
20 go get -u launchpad.net/godeps31 go get -u launchpad.net/godeps
@@ -31,8 +42,21 @@
31acceptance:42acceptance:
32 cd server/acceptance; ./acceptance.sh43 cd server/acceptance; ./acceptance.sh
3344
34build-client:45build-client: ubuntu-push-client signing-helper/signing-helper
35 go build ubuntu-push-client.go46
47.%.deps: %
48 $(SH) scripts/deps.sh $<
49
50%: %.go
51 go build $<
52
53include .ubuntu-push-client.go.deps
54
55signing-helper/Makefile: signing-helper/CMakeLists.txt signing-helper/signing-helper.cpp signing-helper/signing.h
56 cd signing-helper && (make clean || true) && cmake .
57
58signing-helper/signing-helper: signing-helper/Makefile signing-helper/signing-helper.cpp signing-helper/signing.h
59 cd signing-helper && make
3660
37build-server-dev:61build-server-dev:
38 go build -o push-server-dev launchpad.net/ubuntu-push/server/dev62 go build -o push-server-dev launchpad.net/ubuntu-push/server/dev
@@ -68,4 +92,5 @@
6892
69.PHONY: bootstrap check check-race format check-format \93.PHONY: bootstrap check check-race format check-format \
70 acceptance build-client build server-dev run-server-dev \94 acceptance build-client build server-dev run-server-dev \
71 coverage-summary coverage-html protocol-diagrams95 coverage-summary coverage-html protocol-diagrams \
96 fetchdeps refetchdeps
7297
=== added file 'PACKAGE_DEPS'
--- PACKAGE_DEPS 1970-01-01 00:00:00 +0000
+++ PACKAGE_DEPS 2014-05-29 12:04:32 +0000
@@ -0,0 +1,10 @@
1# See the README for what this file is and how to use it.
2build-essential
3cmake
4libdbus-1-dev
5libgcrypt11-dev
6libglib2.0-dev
7libnih-dbus-dev
8libsqlite3-dev
9libubuntuoneauth-2.0-dev
10libwhoopsie-dev
011
=== modified file 'README'
--- README 2014-03-31 17:58:54 +0000
+++ README 2014-05-29 12:04:32 +0000
@@ -6,11 +6,23 @@
6The code expects to be checked out as launchpad.net/ubuntu-push in a Go6The code expects to be checked out as launchpad.net/ubuntu-push in a Go
7workspace, see "go help gopath".7workspace, see "go help gopath".
88
9To setup Go dependencies, install libsqlite3-dev and run:9You need a somewhat long list of dependencies, as well as a working Go
1010development environment. THe Ubuntu packagenames for these are listed
11 make bootstrap11in the file PACKAGE_DEPS.
1212
13To run tests, install libgcrypt11-dev and libwhoopsie-dev and run:13On Ubuntu, if you have sudo, you can have all those installed for you
14by do doing
15
16 make fetchdeps
17
18Once you have the packaged dependencies you can get the Go
19dependencies via
20
21 make bootstrap
22
23and then you're set. Good luck!
24
25To run the tests:
1426
15 make check27 make check
1628
1729
=== modified file 'bus/connectivity/connectivity.go'
--- bus/connectivity/connectivity.go 2014-04-04 11:08:28 +0000
+++ bus/connectivity/connectivity.go 2014-05-29 12:04:32 +0000
@@ -72,19 +72,20 @@
72 cs.connAttempts += ar.Redial()72 cs.connAttempts += ar.Redial()
73 nm := networkmanager.New(cs.endp, cs.log)73 nm := networkmanager.New(cs.endp, cs.log)
7474
75 // set up the watch
76 stateCh, err = nm.WatchState()
77 if err != nil {
78 cs.log.Debugf("failed to set up the state watch: %s", err)
79 goto Continue
80 }
81
75 // Get the current state.82 // Get the current state.
76 initial = nm.GetState()83 initial = nm.GetState()
77 if initial == networkmanager.Unknown {84 if initial == networkmanager.Unknown {
78 cs.log.Debugf("Failed to get state.")85 cs.log.Debugf("Failed to get state.")
79 goto Continue86 goto Continue
80 }87 }
8188 cs.log.Debugf("got initial state of %s", initial)
82 // set up the watch
83 stateCh, err = nm.WatchState()
84 if err != nil {
85 cs.log.Debugf("failed to set up the state watch: %s", err)
86 goto Continue
87 }
8889
89 primary = nm.GetPrimaryConnection()90 primary = nm.GetPrimaryConnection()
90 cs.log.Debugf("primary connection starts as %#v", primary)91 cs.log.Debugf("primary connection starts as %#v", primary)
9192
=== modified file 'bus/connectivity/connectivity_test.go'
--- bus/connectivity/connectivity_test.go 2014-04-04 12:01:42 +0000
+++ bus/connectivity/connectivity_test.go 2014-05-29 12:04:32 +0000
@@ -17,8 +17,15 @@
17package connectivity17package connectivity
1818
19import (19import (
20 "net/http/httptest"
21 "sync"
22 "testing"
23 "time"
24
20 "launchpad.net/go-dbus/v1"25 "launchpad.net/go-dbus/v1"
21 . "launchpad.net/gocheck"26 . "launchpad.net/gocheck"
27
28 "launchpad.net/ubuntu-push/bus"
22 "launchpad.net/ubuntu-push/bus/networkmanager"29 "launchpad.net/ubuntu-push/bus/networkmanager"
23 testingbus "launchpad.net/ubuntu-push/bus/testing"30 testingbus "launchpad.net/ubuntu-push/bus/testing"
24 "launchpad.net/ubuntu-push/config"31 "launchpad.net/ubuntu-push/config"
@@ -26,9 +33,6 @@
26 helpers "launchpad.net/ubuntu-push/testing"33 helpers "launchpad.net/ubuntu-push/testing"
27 "launchpad.net/ubuntu-push/testing/condition"34 "launchpad.net/ubuntu-push/testing/condition"
28 "launchpad.net/ubuntu-push/util"35 "launchpad.net/ubuntu-push/util"
29 "net/http/httptest"
30 "testing"
31 "time"
32)36)
3337
34// hook up gocheck38// hook up gocheck
@@ -115,6 +119,79 @@
115 c.Check(<-cs.networkStateCh, Equals, networkmanager.ConnectedGlobal)119 c.Check(<-cs.networkStateCh, Equals, networkmanager.ConnectedGlobal)
116}120}
117121
122// a racyEndpoint is an endpoint that behaves differently depending on
123// how much time passes between getting the state and setting up the
124// watch
125type racyEndpoint struct {
126 stateGot bool
127 maxTime time.Time
128 delta time.Duration
129 lock sync.RWMutex
130}
131
132func (rep *racyEndpoint) GetProperty(prop string) (interface{}, error) {
133 switch prop {
134 case "state":
135 rep.lock.Lock()
136 defer rep.lock.Unlock()
137 rep.stateGot = true
138 rep.maxTime = time.Now().Add(rep.delta)
139 return uint32(networkmanager.Connecting), nil
140 case "PrimaryConnection":
141 return dbus.ObjectPath("/something"), nil
142 default:
143 return nil, nil
144 }
145}
146
147func (rep *racyEndpoint) WatchSignal(member string, f func(...interface{}), d func()) error {
148 if member == "StateChanged" {
149 // we count never having gotten the state as happening "after" now.
150 rep.lock.RLock()
151 defer rep.lock.RUnlock()
152 ok := !rep.stateGot || time.Now().Before(rep.maxTime)
153 go func() {
154 if ok {
155 f(uint32(networkmanager.ConnectedGlobal))
156 }
157 d()
158 }()
159 }
160 return nil
161}
162
163func (*racyEndpoint) Close() {}
164func (*racyEndpoint) Dial() error { return nil }
165func (*racyEndpoint) String() string { return "racyEndpoint" }
166func (*racyEndpoint) Call(string, []interface{}, ...interface{}) error { return nil }
167func (*racyEndpoint) GrabName(bool) <-chan error { return nil }
168func (*racyEndpoint) WatchMethod(bus.DispatchMap, ...interface{}) {}
169func (*racyEndpoint) Signal(member string, args []interface{}) error { return nil }
170
171var _ bus.Endpoint = (*racyEndpoint)(nil)
172
173// takeNext takes a value from given channel with a 1s timeout
174func takeNext(ch <-chan networkmanager.State) networkmanager.State {
175 select {
176 case <-time.After(time.Second):
177 panic("channel stuck: too long waiting")
178 case v := <-ch:
179 return v
180 }
181}
182
183// test that if the nm state goes from connecting to connected very
184// shortly after calling GetState, we don't lose the event.
185func (s *ConnSuite) TestStartAvoidsRace(c *C) {
186 for delta := time.Second; delta > 1; delta /= 2 {
187 rep := &racyEndpoint{delta: delta}
188 cs := connectedState{config: ConnectivityConfig{}, log: s.log, endp: rep}
189 f := Commentf("when delta=%s", delta)
190 c.Assert(cs.start(), Equals, networkmanager.Connecting, f)
191 c.Assert(takeNext(cs.networkStateCh), Equals, networkmanager.ConnectedGlobal, f)
192 }
193}
194
118/*195/*
119 tests for connectedStateStep()196 tests for connectedStateStep()
120*/197*/
121198
=== modified file 'bus/endpoint.go'
--- bus/endpoint.go 2014-04-02 08:23:15 +0000
+++ bus/endpoint.go 2014-05-29 12:04:32 +0000
@@ -29,9 +29,15 @@
29 * Endpoint (and its implementation)29 * Endpoint (and its implementation)
30 */30 */
3131
32type BusMethod func([]interface{}, []interface{}) ([]interface{}, error)
33type DispatchMap map[string]BusMethod
34
32// bus.Endpoint represents the DBus connection itself.35// bus.Endpoint represents the DBus connection itself.
33type Endpoint interface {36type Endpoint interface {
37 GrabName(allowReplacement bool) <-chan error
34 WatchSignal(member string, f func(...interface{}), d func()) error38 WatchSignal(member string, f func(...interface{}), d func()) error
39 WatchMethod(DispatchMap, ...interface{})
40 Signal(string, []interface{}) error
35 Call(member string, args []interface{}, rvs ...interface{}) error41 Call(member string, args []interface{}, rvs ...interface{}) error
36 GetProperty(property string) (interface{}, error)42 GetProperty(property string) (interface{}, error)
37 Dial() error43 Dial() error
@@ -53,13 +59,18 @@
53}59}
5460
55// ensure endpoint implements Endpoint61// ensure endpoint implements Endpoint
56var _ Endpoint = &endpoint{}62var _ Endpoint = (*endpoint)(nil)
5763
58/*64/*
59 public methods65 public methods
66
67XXX: these are almost entirely untested, as that would need
68XXX: integration tests we are currently missing.
60*/69*/
6170
62// Dial() (re)establishes the connection with dbus71// Dial() (re)establishes the connection with dbus
72//
73// XXX: mostly untested
63func (endp *endpoint) Dial() error {74func (endp *endpoint) Dial() error {
64 bus, err := dbus.Connect(endp.busT.(concreteBus).dbusType())75 bus, err := dbus.Connect(endp.busT.(concreteBus).dbusType())
65 if err != nil {76 if err != nil {
@@ -106,6 +117,8 @@
106// with the unpacked value. If it's unable to set up the watch it returns an117// with the unpacked value. If it's unable to set up the watch it returns an
107// error. If the watch fails once established, d() is called. Typically f()118// error. If the watch fails once established, d() is called. Typically f()
108// sends the values over a channel, and d() would close the channel.119// sends the values over a channel, and d() would close the channel.
120//
121// XXX: untested
109func (endp *endpoint) WatchSignal(member string, f func(...interface{}), d func()) error {122func (endp *endpoint) WatchSignal(member string, f func(...interface{}), d func()) error {
110 watch, err := endp.proxy.WatchSignal(endp.addr.Interface, member)123 watch, err := endp.proxy.WatchSignal(endp.addr.Interface, member)
111 if err != nil {124 if err != nil {
@@ -122,6 +135,8 @@
122// interface provided when creating the endpoint). args can be built135// interface provided when creating the endpoint). args can be built
123// using bus.Args(...). The return value is unpacked into rvs before being136// using bus.Args(...). The return value is unpacked into rvs before being
124// returned.137// returned.
138//
139// XXX: untested
125func (endp *endpoint) Call(member string, args []interface{}, rvs ...interface{}) error {140func (endp *endpoint) Call(member string, args []interface{}, rvs ...interface{}) error {
126 msg, err := endp.proxy.Call(endp.addr.Interface, member, args...)141 msg, err := endp.proxy.Call(endp.addr.Interface, member, args...)
127 if err != nil {142 if err != nil {
@@ -138,6 +153,8 @@
138// to read a given property on the name, path and interface provided when153// to read a given property on the name, path and interface provided when
139// creating the endpoint. The return value is unpacked into a dbus.Variant,154// creating the endpoint. The return value is unpacked into a dbus.Variant,
140// and its value returned.155// and its value returned.
156//
157// XXX: untested
141func (endp *endpoint) GetProperty(property string) (interface{}, error) {158func (endp *endpoint) GetProperty(property string) (interface{}, error) {
142 msg, err := endp.proxy.Call("org.freedesktop.DBus.Properties", "Get", endp.addr.Interface, property)159 msg, err := endp.proxy.Call("org.freedesktop.DBus.Properties", "Get", endp.addr.Interface, property)
143 if err != nil {160 if err != nil {
@@ -160,6 +177,8 @@
160}177}
161178
162// Close the connection to dbus.179// Close the connection to dbus.
180//
181// XXX: untested
163func (endp *endpoint) Close() {182func (endp *endpoint) Close() {
164 if endp.bus != nil {183 if endp.bus != nil {
165 endp.bus.Close()184 endp.bus.Close()
@@ -169,15 +188,98 @@
169}188}
170189
171// String() performs advanced endpoint stringification190// String() performs advanced endpoint stringification
191//
192// XXX: untested
172func (endp *endpoint) String() string {193func (endp *endpoint) String() string {
173 return fmt.Sprintf("<Connection to %s %#v>", endp.bus, endp.addr)194 return fmt.Sprintf("<Connection to %s %#v>", endp.bus, endp.addr)
174}195}
175196
197// GrabName() takes over the name on the bus, reporting errors over the
198// returned channel.
199//
200// While the first result will be nil on success, successive results would
201// typically indicate another process trying to take over the name.
202//
203// XXX: untested
204func (endp *endpoint) GrabName(allowReplacement bool) <-chan error {
205 flags := dbus.NameFlagAllowReplacement | dbus.NameFlagReplaceExisting
206 if !allowReplacement {
207 flags = 0
208 }
209 return endp.bus.RequestName(endp.addr.Name, flags).C
210}
211
212// Signal() sends out a signal called <member> containing <args>.
213//
214// XXX: untested
215func (endp *endpoint) Signal(member string, args []interface{}) error {
216 msg := dbus.NewSignalMessage(dbus.ObjectPath(endp.addr.Path), endp.addr.Interface, member)
217 if args != nil {
218 err := msg.AppendArgs(args...)
219 if err != nil {
220 endp.log.Errorf("unable to build dbus signal message: %v", err)
221 return err
222 }
223 }
224 err := endp.bus.Send(msg)
225 if err != nil {
226 endp.log.Errorf("unable to send dbus signal: %v", err)
227 } else {
228 endp.log.Debugf("sent dbus signal %s(%#v)", member, args)
229 }
230 return nil
231}
232
233// WatchMethod() uses the given DispatchMap to answer incoming method
234// calls.
235//
236// XXX: untested
237func (endp *endpoint) WatchMethod(dispatch DispatchMap, extra ...interface{}) {
238 ch := make(chan *dbus.Message)
239 go func() {
240 var reply *dbus.Message
241
242 err_iface := endp.addr.Interface + ".Error"
243
244 for msg := range ch {
245 meth, ok := dispatch[msg.Member]
246 if !ok || msg.Interface != endp.addr.Interface {
247 reply = dbus.NewErrorMessage(msg,
248 "org.freedesktop.DBus.Error.UnknownMethod", "Unknown method")
249 endp.log.Errorf("WatchMethod: unknown method %s", msg.Member)
250 } else {
251 args := msg.AllArgs()
252 rvals, err := meth(args, extra)
253 if err != nil {
254 reply = dbus.NewErrorMessage(msg, err_iface, err.Error())
255 endp.log.Errorf("WatchMethod: %s(%#v, %#v) failure: %#v", msg.Member, args, extra, err)
256 } else {
257 endp.log.Debugf("WatchMethod: %s(%#v, %#v) success: %#v", msg.Member, args, extra, rvals)
258 reply = dbus.NewMethodReturnMessage(msg)
259 err = reply.AppendArgs(rvals...)
260 if err != nil {
261 endp.log.Errorf("WatchMethod: unable to build dbus response message: %v", err)
262 reply = dbus.NewErrorMessage(msg, err_iface, err.Error())
263 }
264 }
265 }
266 err := endp.bus.Send(reply)
267 if err != nil {
268 endp.log.Errorf("WatchMethod: unable to send reply: %v", err)
269 }
270
271 }
272 }()
273 endp.bus.RegisterObjectPath(dbus.ObjectPath(endp.addr.Path), ch)
274}
275
176/*276/*
177 private methods277 private methods
178*/278*/
179279
180// unpackOneMsg unpacks the value from the response msg280// unpackOneMsg unpacks the value from the response msg
281//
282// XXX: untested
181func (endp *endpoint) unpackOneMsg(msg *dbus.Message, member string) []interface{} {283func (endp *endpoint) unpackOneMsg(msg *dbus.Message, member string) []interface{} {
182 var varmap map[string]dbus.Variant284 var varmap map[string]dbus.Variant
183 if err := msg.Args(&varmap); err != nil {285 if err := msg.Args(&varmap); err != nil {
@@ -187,6 +289,8 @@
187}289}
188290
189// unpackMessages unpacks the value from the watch291// unpackMessages unpacks the value from the watch
292//
293// XXX: untested
190func (endp *endpoint) unpackMessages(watch *dbus.SignalWatch, f func(...interface{}), d func(), member string) {294func (endp *endpoint) unpackMessages(watch *dbus.SignalWatch, f func(...interface{}), d func(), member string) {
191 for {295 for {
192 msg, ok := <-watch.C296 msg, ok := <-watch.C
193297
=== modified file 'bus/endpoint_test.go'
--- bus/endpoint_test.go 2014-02-06 09:57:49 +0000
+++ bus/endpoint_test.go 2014-05-29 12:04:32 +0000
@@ -37,7 +37,7 @@
37// testing amenities (already talked about it with jamesh)37// testing amenities (already talked about it with jamesh)
3838
39// Tests that we can connect to the *actual* system bus.39// Tests that we can connect to the *actual* system bus.
40// XXX maybe connect to a mock/fake/etc bus?40// XXX: maybe connect to a mock/fake/etc bus?
41func (s *EndpointSuite) TestDial(c *C) {41func (s *EndpointSuite) TestDial(c *C) {
42 // if somebody's set up the env var, assume it's "live"42 // if somebody's set up the env var, assume it's "live"
43 if os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") == "" {43 if os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") == "" {
4444
=== modified file 'bus/notifications/raw.go'
--- bus/notifications/raw.go 2014-04-02 08:23:15 +0000
+++ bus/notifications/raw.go 2014-05-29 12:04:32 +0000
@@ -22,6 +22,8 @@
22// this is the lower-level api22// this is the lower-level api
2323
24import (24import (
25 "errors"
26
25 "launchpad.net/go-dbus/v1"27 "launchpad.net/go-dbus/v1"
26 "launchpad.net/ubuntu-push/bus"28 "launchpad.net/ubuntu-push/bus"
27 "launchpad.net/ubuntu-push/logger"29 "launchpad.net/ubuntu-push/logger"
@@ -68,6 +70,9 @@
68 timeout int32) (uint32, error) {70 timeout int32) (uint32, error) {
69 // that's a long argument list! Take a breather.71 // that's a long argument list! Take a breather.
70 //72 //
73 if raw.bus == nil {
74 return 0, errors.New("unconfigured (missing bus)")
75 }
71 var res uint3276 var res uint32
72 err := raw.bus.Call("Notify", bus.Args(app_name, reuse_id, icon,77 err := raw.bus.Call("Notify", bus.Args(app_name, reuse_id, icon,
73 summary, body, actions, hints, timeout), &res)78 summary, body, actions, hints, timeout), &res)
7479
=== modified file 'bus/notifications/raw_test.go'
--- bus/notifications/raw_test.go 2014-02-05 18:17:26 +0000
+++ bus/notifications/raw_test.go 2014-05-29 12:04:32 +0000
@@ -57,6 +57,12 @@
57 c.Check(err, NotNil)57 c.Check(err, NotNil)
58}58}
5959
60func (s *RawSuite) TestNotifyFailsIfNoBus(c *C) {
61 raw := Raw(nil, s.log)
62 _, err := raw.Notify("", 0, "", "", "", nil, nil, 0)
63 c.Check(err, ErrorMatches, `.*unconfigured .*`)
64}
65
60func (s *RawSuite) TestNotifiesFailsWeirdly(c *C) {66func (s *RawSuite) TestNotifiesFailsWeirdly(c *C) {
61 endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{1, 2})67 endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{1, 2})
62 raw := Raw(endp, s.log)68 raw := Raw(endp, s.log)
6369
=== modified file 'bus/testing/testing_endpoint.go'
--- bus/testing/testing_endpoint.go 2014-04-04 11:08:28 +0000
+++ bus/testing/testing_endpoint.go 2014-05-29 12:04:32 +0000
@@ -166,8 +166,46 @@
166 endp.dialCond, endp.callCond, endp.retvals)166 endp.dialCond, endp.callCond, endp.retvals)
167}167}
168168
169// see Endpoint's Close. This one does nothing.169// see Endpoint's Close. This one does nothing beyond registering
170func (tc *testingEndpoint) Close() {}170// being called.
171func (tc *testingEndpoint) Close() {
172 tc.callArgsLck.Lock()
173 defer tc.callArgsLck.Unlock()
174
175 args := callArgs{Member: "::Close"}
176 tc.callArgs = append(tc.callArgs, args)
177}
178
179func (tc *testingEndpoint) GrabName(allowReplacement bool) <-chan error {
180 tc.callArgsLck.Lock()
181 defer tc.callArgsLck.Unlock()
182
183 args := callArgs{Member: "::GrabName"}
184 args.Args = append(args.Args, allowReplacement)
185 tc.callArgs = append(tc.callArgs, args)
186
187 return nil
188}
189
190func (tc *testingEndpoint) WatchMethod(dispatch bus.DispatchMap, extra ...interface{}) {
191 tc.callArgsLck.Lock()
192 defer tc.callArgsLck.Unlock()
193
194 args := callArgs{Member: "::WatchMethod"}
195 args.Args = append(args.Args, dispatch, extra)
196 tc.callArgs = append(tc.callArgs, args)
197}
198
199func (tc *testingEndpoint) Signal(member string, args []interface{}) error {
200 tc.callArgsLck.Lock()
201 defer tc.callArgsLck.Unlock()
202
203 callargs := callArgs{Member: "::Signal"}
204 callargs.Args = append(callargs.Args, member, args)
205 tc.callArgs = append(tc.callArgs, callargs)
206
207 return nil
208}
171209
172// ensure testingEndpoint implements bus.Endpoint210// ensure testingEndpoint implements bus.Endpoint
173var _ bus.Endpoint = &testingEndpoint{}211var _ bus.Endpoint = (*testingEndpoint)(nil)
174212
=== modified file 'bus/testing/testing_endpoint_test.go'
--- bus/testing/testing_endpoint_test.go 2014-04-02 08:23:15 +0000
+++ bus/testing/testing_endpoint_test.go 2014-05-29 12:04:32 +0000
@@ -76,6 +76,24 @@
76 []callArgs{{"what", []interface{}{"is", "this", "thing"}}})76 []callArgs{{"what", []interface{}{"is", "this", "thing"}}})
77}77}
7878
79// Test that Call() fails but does not explode when asked to return
80// values that can't be packed into a dbus message.
81func (s *TestingEndpointSuite) TestCallFailsOnBadRetval(c *C) {
82 endp := NewTestingEndpoint(nil, condition.Work(true), Equals)
83 var r uint32
84 e := endp.Call("what", bus.Args(), &r)
85 c.Check(e, NotNil)
86}
87
88// Test that Call() fails but does not explode when given argument
89// that can't be packed into a dbus message.
90func (s *TestingEndpointSuite) TestCallFailsOnBadArg(c *C) {
91 endp := NewTestingEndpoint(nil, condition.Work(true), 1)
92 r := func() {}
93 e := endp.Call("what", bus.Args(), &r)
94 c.Check(e, NotNil)
95}
96
79// Test that WatchSignal() with a positive condition sends the provided return97// Test that WatchSignal() with a positive condition sends the provided return
80// values over the channel.98// values over the channel.
81func (s *TestingEndpointSuite) TestWatch(c *C) {99func (s *TestingEndpointSuite) TestWatch(c *C) {
@@ -102,7 +120,11 @@
102func (s *TestingEndpointSuite) TestCloser(c *C) {120func (s *TestingEndpointSuite) TestCloser(c *C) {
103 endp := NewTestingEndpoint(nil, condition.Work(true))121 endp := NewTestingEndpoint(nil, condition.Work(true))
104 endp.Close()122 endp.Close()
105 // ... yay?123 c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
124 {
125 Member: "::Close",
126 Args: nil,
127 }})
106}128}
107129
108// Test that WatchSignal() with a negative condition returns an error.130// Test that WatchSignal() with a negative condition returns an error.
@@ -173,3 +195,46 @@
173 endp := NewTestingEndpoint(condition.Fail2Work(2), nil, "hello there")195 endp := NewTestingEndpoint(condition.Fail2Work(2), nil, "hello there")
174 c.Check(endp.String(), Matches, ".*Still Broken.*hello there.*")196 c.Check(endp.String(), Matches, ".*Still Broken.*hello there.*")
175}197}
198
199// Test that GrabName updates callArgs
200func (s *TestingEndpointSuite) TestGrabNameUpdatesCallArgs(c *C) {
201 endp := NewTestingEndpoint(nil, condition.Work(true))
202 endp.GrabName(false)
203 endp.GrabName(true)
204 c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
205 {
206 Member: "::GrabName",
207 Args: []interface{}{false},
208 }, {
209 Member: "::GrabName",
210 Args: []interface{}{true},
211 }})
212}
213
214// Test that Signal updates callArgs
215func (s *TestingEndpointSuite) TestSignalUpdatesCallArgs(c *C) {
216 endp := NewTestingEndpoint(nil, condition.Work(true))
217 endp.Signal("hello", []interface{}{"world"})
218 endp.Signal("hello", []interface{}{"there"})
219 c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
220 {
221 Member: "::Signal",
222 Args: []interface{}{"hello", []interface{}{"world"}},
223 }, {
224 Member: "::Signal",
225 Args: []interface{}{"hello", []interface{}{"there"}},
226 }})
227}
228
229// Test that WatchMethod updates callArgs
230func (s *TestingEndpointSuite) TestWatchMethodUpdatesCallArgs(c *C) {
231 endp := NewTestingEndpoint(nil, condition.Work(true))
232 foo := func([]interface{}, []interface{}) ([]interface{}, error) { return nil, nil }
233 foomp := bus.DispatchMap{"foo": foo}
234 endp.WatchMethod(foomp)
235 c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
236 {
237 Member: "::WatchMethod",
238 Args: []interface{}{foomp, []interface{}(nil)},
239 }})
240}
176241
=== modified file 'client/client.go'
--- client/client.go 2014-04-18 09:35:59 +0000
+++ client/client.go 2014-05-29 12:04:32 +0000
@@ -19,6 +19,10 @@
19package client19package client
2020
21import (21import (
22 "crypto/sha256"
23 "encoding/base64"
24 "encoding/hex"
25 "encoding/json"
22 "encoding/pem"26 "encoding/pem"
23 "errors"27 "errors"
24 "fmt"28 "fmt"
@@ -34,10 +38,12 @@
34 "launchpad.net/ubuntu-push/bus/notifications"38 "launchpad.net/ubuntu-push/bus/notifications"
35 "launchpad.net/ubuntu-push/bus/systemimage"39 "launchpad.net/ubuntu-push/bus/systemimage"
36 "launchpad.net/ubuntu-push/bus/urldispatcher"40 "launchpad.net/ubuntu-push/bus/urldispatcher"
41 "launchpad.net/ubuntu-push/client/service"
37 "launchpad.net/ubuntu-push/client/session"42 "launchpad.net/ubuntu-push/client/session"
38 "launchpad.net/ubuntu-push/client/session/levelmap"43 "launchpad.net/ubuntu-push/client/session/seenstate"
39 "launchpad.net/ubuntu-push/config"44 "launchpad.net/ubuntu-push/config"
40 "launchpad.net/ubuntu-push/logger"45 "launchpad.net/ubuntu-push/logger"
46 "launchpad.net/ubuntu-push/protocol"
41 "launchpad.net/ubuntu-push/util"47 "launchpad.net/ubuntu-push/util"
42 "launchpad.net/ubuntu-push/whoopsie/identifier"48 "launchpad.net/ubuntu-push/whoopsie/identifier"
43)49)
@@ -56,6 +62,8 @@
56 ExpectAllRepairedTime config.ConfigTimeDuration `json:"expect_all_repaired"` // worth retrying all servers after62 ExpectAllRepairedTime config.ConfigTimeDuration `json:"expect_all_repaired"` // worth retrying all servers after
57 // The PEM-encoded server certificate63 // The PEM-encoded server certificate
58 CertPEMFile string `json:"cert_pem_file"`64 CertPEMFile string `json:"cert_pem_file"`
65 // How to invoke the auth helper
66 AuthHelper []string `json:"auth_helper"`
59 // The logging level (one of "debug", "info", "error")67 // The logging level (one of "debug", "info", "error")
60 LogLevel logger.ConfigLogLevel `json:"log_level"`68 LogLevel logger.ConfigLogLevel `json:"log_level"`
61}69}
@@ -79,9 +87,15 @@
79 actionsCh <-chan notifications.RawActionReply87 actionsCh <-chan notifications.RawActionReply
80 session *session.ClientSession88 session *session.ClientSession
81 sessionConnectedCh chan uint3289 sessionConnectedCh chan uint32
90 serviceEndpoint bus.Endpoint
91 service *service.Service
82}92}
8393
84var ACTION_ID_SNOWFLAKE = "::ubuntu-push-client::"94var (
95 system_update_url = "settings:///system/system-update"
96 ACTION_ID_SNOWFLAKE = "::ubuntu-push-client::"
97 ACTION_ID_BROADCAST = ACTION_ID_SNOWFLAKE + system_update_url
98)
8599
86// Creates a new Ubuntu Push Notifications client-side daemon that will use100// Creates a new Ubuntu Push Notifications client-side daemon that will use
87// the given configuration file.101// the given configuration file.
@@ -144,8 +158,9 @@
144 ExchangeTimeout: client.config.ExchangeTimeout.TimeDuration(),158 ExchangeTimeout: client.config.ExchangeTimeout.TimeDuration(),
145 HostsCachingExpiryTime: client.config.HostsCachingExpiryTime.TimeDuration(),159 HostsCachingExpiryTime: client.config.HostsCachingExpiryTime.TimeDuration(),
146 ExpectAllRepairedTime: client.config.ExpectAllRepairedTime.TimeDuration(),160 ExpectAllRepairedTime: client.config.ExpectAllRepairedTime.TimeDuration(),
147 PEM: client.pem,161 PEM: client.pem,
148 Info: info,162 Info: info,
163 AuthHelper: client.config.AuthHelper,
149 }164 }
150}165}
151166
@@ -155,7 +170,13 @@
155 if err != nil {170 if err != nil {
156 return err171 return err
157 }172 }
158 client.deviceId = client.idder.String()173 baseId := client.idder.String()
174 b, err := hex.DecodeString(baseId)
175 if err != nil {
176 return fmt.Errorf("whoopsie id should be hex: %v", err)
177 }
178 h := sha256.Sum224(b)
179 client.deviceId = base64.StdEncoding.EncodeToString(h[:])
159 return nil180 return nil
160}181}
161182
@@ -192,7 +213,7 @@
192 }213 }
193 sess, err := session.NewSession(client.config.Addr,214 sess, err := session.NewSession(client.config.Addr,
194 client.deriveSessionConfig(info), client.deviceId,215 client.deriveSessionConfig(info), client.deviceId,
195 client.levelMapFactory, client.log)216 client.seenStateFactory, client.log)
196 if err != nil {217 if err != nil {
197 return err218 return err
198 }219 }
@@ -200,12 +221,12 @@
200 return nil221 return nil
201}222}
202223
203// levelmapFactory returns a levelMap for the session224// seenStateFactory returns a SeenState for the session
204func (client *PushClient) levelMapFactory() (levelmap.LevelMap, error) {225func (client *PushClient) seenStateFactory() (seenstate.SeenState, error) {
205 if client.leveldbPath == "" {226 if client.leveldbPath == "" {
206 return levelmap.NewLevelMap()227 return seenstate.NewSeenState()
207 } else {228 } else {
208 return levelmap.NewSqliteLevelMap(client.leveldbPath)229 return seenstate.NewSqliteSeenState(client.leveldbPath)
209 }230 }
210}231}
211232
@@ -232,7 +253,7 @@
232 }253 }
233}254}
234255
235// filterNotification finds out if the notification is about an actual256// filterBroadcastNotification finds out if the notification is about an actual
236// upgrade for the device. It expects msg.Decoded entries to look257// upgrade for the device. It expects msg.Decoded entries to look
237// like:258// like:
238//259//
@@ -240,7 +261,7 @@
240// "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]261// "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]
241// ...262// ...
242// }263// }
243func (client *PushClient) filterNotification(msg *session.Notification) bool {264func (client *PushClient) filterBroadcastNotification(msg *session.BroadcastNotification) bool {
244 n := len(msg.Decoded)265 n := len(msg.Decoded)
245 if n == 0 {266 if n == 0 {
246 return false267 return false
@@ -275,26 +296,30 @@
275 return false296 return false
276}297}
277298
278// handleNotification deals with receiving a notification299func (client *PushClient) sendNotification(action_id, icon, summary, body string) (uint32, error) {
279func (client *PushClient) handleNotification(msg *session.Notification) error {300 a := []string{action_id, "Switch to app"} // action value not visible on the phone
280 if !client.filterNotification(msg) {
281 return nil
282 }
283 action_id := ACTION_ID_SNOWFLAKE
284 a := []string{action_id, "Go get it!"} // action value not visible on the phone
285 h := map[string]*dbus.Variant{"x-canonical-switch-to-application": &dbus.Variant{true}}301 h := map[string]*dbus.Variant{"x-canonical-switch-to-application": &dbus.Variant{true}}
286 nots := notifications.Raw(client.notificationsEndp, client.log)302 nots := notifications.Raw(client.notificationsEndp, client.log)
287 body := "Tap to open the system updater."303 return nots.Notify(
288 not_id, err := nots.Notify(304 "ubuntu-push-client", // app name
289 "ubuntu-push-client", // app name305 uint32(0), // id
290 uint32(0), // id306 icon, // icon
291 "update_manager_icon", // icon307 summary, // summary
292 "There's an updated system image.", // summary308 body, // body
293 body, // body309 a, // actions
294 a, // actions310 h, // hints
295 h, // hints311 int32(10*1000), // timeout (ms)
296 int32(10*1000), // timeout (ms)
297 )312 )
313}
314
315// handleBroadcastNotification deals with receiving a broadcast notification
316func (client *PushClient) handleBroadcastNotification(msg *session.BroadcastNotification) error {
317 if !client.filterBroadcastNotification(msg) {
318 return nil
319 }
320 not_id, err := client.sendNotification(ACTION_ID_BROADCAST,
321 "update_manager_icon", "There's an updated system image.",
322 "Tap to open the system updater.")
298 if err != nil {323 if err != nil {
299 client.log.Errorf("showing notification: %s", err)324 client.log.Errorf("showing notification: %s", err)
300 return err325 return err
@@ -303,26 +328,42 @@
303 return nil328 return nil
304}329}
305330
331// handleUnicastNotification deals with receiving a unicast notification
332func (client *PushClient) handleUnicastNotification(msg *protocol.Notification) error {
333 client.log.Debugf("sending notification %#v for %#v.", msg.MsgId, msg.AppId)
334 return client.service.Inject(msg.AppId, string(msg.Payload))
335}
336
306// handleClick deals with the user clicking a notification337// handleClick deals with the user clicking a notification
307func (client *PushClient) handleClick(action_id string) error {338func (client *PushClient) handleClick(action_id string) error {
308 if action_id != ACTION_ID_SNOWFLAKE {339 // “The string is a stark data structure and everywhere it is passed
340 // there is much duplication of process. It is a perfect vehicle for
341 // hiding information.”
342 //
343 // From ACM's SIGPLAN publication, (September, 1982), Article
344 // "Epigrams in Programming", by Alan J. Perlis of Yale University.
345 url := strings.TrimPrefix(action_id, ACTION_ID_SNOWFLAKE)
346 if len(url) == len(action_id) || len(url) == 0 {
347 // it didn't start with the prefix
309 return nil348 return nil
310 }349 }
311 // it doesn't get much simpler...350 // it doesn't get much simpler...
312 urld := urldispatcher.New(client.urlDispatcherEndp, client.log)351 urld := urldispatcher.New(client.urlDispatcherEndp, client.log)
313 return urld.DispatchURL("settings:///system/system-update")352 return urld.DispatchURL(url)
314}353}
315354
316// doLoop connects events with their handlers355// doLoop connects events with their handlers
317func (client *PushClient) doLoop(connhandler func(bool), clickhandler func(string) error, notifhandler func(*session.Notification) error, errhandler func(error)) {356func (client *PushClient) doLoop(connhandler func(bool), clickhandler func(string) error, bcasthandler func(*session.BroadcastNotification) error, ucasthandler func(*protocol.Notification) error, errhandler func(error)) {
318 for {357 for {
319 select {358 select {
320 case state := <-client.connCh:359 case state := <-client.connCh:
321 connhandler(state)360 connhandler(state)
322 case action := <-client.actionsCh:361 case action := <-client.actionsCh:
323 clickhandler(action.ActionId)362 clickhandler(action.ActionId)
324 case msg := <-client.session.MsgCh:363 case bcast := <-client.session.BroadcastCh:
325 notifhandler(msg)364 bcasthandler(bcast)
365 case ucast := <-client.session.NotificationsCh:
366 ucasthandler(ucast)
326 case err := <-client.session.ErrCh:367 case err := <-client.session.ErrCh:
327 errhandler(err)368 errhandler(err)
328 case count := <-client.sessionConnectedCh:369 case count := <-client.sessionConnectedCh:
@@ -344,14 +385,57 @@
344385
345// Loop calls doLoop with the "real" handlers386// Loop calls doLoop with the "real" handlers
346func (client *PushClient) Loop() {387func (client *PushClient) Loop() {
347 client.doLoop(client.handleConnState, client.handleClick,388 client.doLoop(client.handleConnState,
348 client.handleNotification, client.handleErr)389 client.handleClick,
390 client.handleBroadcastNotification,
391 client.handleUnicastNotification,
392 client.handleErr)
393}
394
395// these are the currently supported fields of a unicast message
396type UnicastMessage struct {
397 Icon string `json:"icon"`
398 Body string `json:"body"`
399 Summary string `json:"summary"`
400 URL string `json:"url"`
401 Blob json.RawMessage `json:"blob"`
402}
403
404func (client *PushClient) messageHandler(message []byte) error {
405 var umsg = new(UnicastMessage)
406 err := json.Unmarshal(message, &umsg)
407 if err != nil {
408 client.log.Errorf("unable to unmarshal message: %v", err)
409 return err
410 }
411
412 not_id, err := client.sendNotification(
413 ACTION_ID_SNOWFLAKE+umsg.URL,
414 umsg.Icon, umsg.Summary, umsg.Body)
415
416 if err != nil {
417 client.log.Errorf("showing notification: %s", err)
418 return err
419 }
420 client.log.Debugf("got notification id %d", not_id)
421 return nil
422}
423
424func (client *PushClient) startService() error {
425 if client.serviceEndpoint == nil {
426 client.serviceEndpoint = bus.SessionBus.Endpoint(service.BusAddress, client.log)
427 }
428
429 client.service = service.NewService(client.serviceEndpoint, client.log)
430 client.service.SetMessageHandler(client.messageHandler)
431 return client.service.Start()
349}432}
350433
351// Start calls doStart with the "real" starters434// Start calls doStart with the "real" starters
352func (client *PushClient) Start() error {435func (client *PushClient) Start() error {
353 return client.doStart(436 return client.doStart(
354 client.configure,437 client.configure,
438 client.startService,
355 client.getDeviceId,439 client.getDeviceId,
356 client.takeTheBus,440 client.takeTheBus,
357 client.initSession,441 client.initSession,
358442
=== modified file 'client/client_test.go'
--- client/client_test.go 2014-04-18 16:31:04 +0000
+++ client/client_test.go 2014-05-29 12:04:32 +0000
@@ -38,8 +38,9 @@
38 "launchpad.net/ubuntu-push/bus/systemimage"38 "launchpad.net/ubuntu-push/bus/systemimage"
39 testibus "launchpad.net/ubuntu-push/bus/testing"39 testibus "launchpad.net/ubuntu-push/bus/testing"
40 "launchpad.net/ubuntu-push/client/session"40 "launchpad.net/ubuntu-push/client/session"
41 "launchpad.net/ubuntu-push/client/session/levelmap"41 "launchpad.net/ubuntu-push/client/session/seenstate"
42 "launchpad.net/ubuntu-push/config"42 "launchpad.net/ubuntu-push/config"
43 "launchpad.net/ubuntu-push/protocol"
43 helpers "launchpad.net/ubuntu-push/testing"44 helpers "launchpad.net/ubuntu-push/testing"
44 "launchpad.net/ubuntu-push/testing/condition"45 "launchpad.net/ubuntu-push/testing/condition"
45 "launchpad.net/ubuntu-push/util"46 "launchpad.net/ubuntu-push/util"
@@ -105,6 +106,7 @@
105 "addr": ":0",106 "addr": ":0",
106 "cert_pem_file": pem_file,107 "cert_pem_file": pem_file,
107 "recheck_timeout": "3h",108 "recheck_timeout": "3h",
109 "auth_helper": []string{},
108 "log_level": "debug",110 "log_level": "debug",
109 }111 }
110 for k, v := range overrides {112 for k, v := range overrides {
@@ -254,6 +256,9 @@
254******************************************************************/256******************************************************************/
255257
256func (cs *clientSuite) TestDeriveSessionConfig(c *C) {258func (cs *clientSuite) TestDeriveSessionConfig(c *C) {
259 cs.writeTestConfig(map[string]interface{}{
260 "auth_helper": []string{"auth", "helper"},
261 })
257 info := map[string]interface{}{262 info := map[string]interface{}{
258 "foo": 1,263 "foo": 1,
259 }264 }
@@ -265,8 +270,9 @@
265 ExchangeTimeout: 10 * time.Millisecond,270 ExchangeTimeout: 10 * time.Millisecond,
266 HostsCachingExpiryTime: 1 * time.Hour,271 HostsCachingExpiryTime: 1 * time.Hour,
267 ExpectAllRepairedTime: 30 * time.Minute,272 ExpectAllRepairedTime: 30 * time.Minute,
268 PEM: cli.pem,273 PEM: cli.pem,
269 Info: info,274 Info: info,
275 AuthHelper: []string{"auth", "helper"},
270 }276 }
271 // sanity check that we are looking at all fields277 // sanity check that we are looking at all fields
272 vExpected := reflect.ValueOf(expected)278 vExpected := reflect.ValueOf(expected)
@@ -282,6 +288,35 @@
282}288}
283289
284/*****************************************************************290/*****************************************************************
291 startService tests
292******************************************************************/
293
294func (cs *clientSuite) TestStartServiceWorks(c *C) {
295 cli := NewPushClient(cs.configPath, cs.leveldbPath)
296 cli.log = cs.log
297 cli.serviceEndpoint = testibus.NewTestingEndpoint(condition.Work(true), nil)
298 c.Check(cli.service, IsNil)
299 c.Check(cli.startService(), IsNil)
300 c.Assert(cli.service, NotNil)
301 c.Check(cli.service.IsRunning(), Equals, true)
302 c.Check(cli.service.GetMessageHandler(), NotNil)
303 cli.service.Stop()
304}
305
306func (cs *clientSuite) TestStartServiceErrorsOnNilLog(c *C) {
307 cli := NewPushClient(cs.configPath, cs.leveldbPath)
308 c.Check(cli.log, IsNil)
309 c.Check(cli.startService(), NotNil)
310}
311
312func (cs *clientSuite) TestStartServiceErrorsOnBusDialFail(c *C) {
313 cli := NewPushClient(cs.configPath, cs.leveldbPath)
314 cli.log = cs.log
315 cli.serviceEndpoint = testibus.NewTestingEndpoint(condition.Work(false), nil)
316 c.Check(cli.startService(), NotNil)
317}
318
319/*****************************************************************
285 getDeviceId tests320 getDeviceId tests
286******************************************************************/321******************************************************************/
287322
@@ -291,7 +326,7 @@
291 cli.idder = identifier.New()326 cli.idder = identifier.New()
292 c.Check(cli.deviceId, Equals, "")327 c.Check(cli.deviceId, Equals, "")
293 c.Check(cli.getDeviceId(), IsNil)328 c.Check(cli.getDeviceId(), IsNil)
294 c.Check(cli.deviceId, HasLen, 128)329 c.Check(cli.deviceId, HasLen, 40)
295}330}
296331
297func (cs *clientSuite) TestGetDeviceIdCanFail(c *C) {332func (cs *clientSuite) TestGetDeviceIdCanFail(c *C) {
@@ -302,6 +337,16 @@
302 c.Check(cli.getDeviceId(), NotNil)337 c.Check(cli.getDeviceId(), NotNil)
303}338}
304339
340func (cs *clientSuite) TestGetDeviceIdWhoopsieDoesTheUnexpected(c *C) {
341 cli := NewPushClient(cs.configPath, cs.leveldbPath)
342 cli.log = cs.log
343 settable := idtesting.Settable()
344 cli.idder = settable
345 settable.Set("not-hex")
346 c.Check(cli.deviceId, Equals, "")
347 c.Check(cli.getDeviceId(), ErrorMatches, "whoopsie id should be hex: .*")
348}
349
305/*****************************************************************350/*****************************************************************
306 takeTheBus tests351 takeTheBus tests
307******************************************************************/352******************************************************************/
@@ -389,21 +434,21 @@
389}434}
390435
391/*****************************************************************436/*****************************************************************
392 levelmapFactory tests437 seenStateFactory tests
393******************************************************************/438******************************************************************/
394439
395func (cs *clientSuite) TestLevelMapFactoryNoDbPath(c *C) {440func (cs *clientSuite) TestSeenStateFactoryNoDbPath(c *C) {
396 cli := NewPushClient(cs.configPath, "")441 cli := NewPushClient(cs.configPath, "")
397 ln, err := cli.levelMapFactory()442 ln, err := cli.seenStateFactory()
398 c.Assert(err, IsNil)443 c.Assert(err, IsNil)
399 c.Check(fmt.Sprintf("%T", ln), Equals, "*levelmap.mapLevelMap")444 c.Check(fmt.Sprintf("%T", ln), Equals, "*seenstate.memSeenState")
400}445}
401446
402func (cs *clientSuite) TestLevelMapFactoryWithDbPath(c *C) {447func (cs *clientSuite) TestSeenStateFactoryWithDbPath(c *C) {
403 cli := NewPushClient(cs.configPath, ":memory:")448 cli := NewPushClient(cs.configPath, ":memory:")
404 ln, err := cli.levelMapFactory()449 ln, err := cli.seenStateFactory()
405 c.Assert(err, IsNil)450 c.Assert(err, IsNil)
406 c.Check(fmt.Sprintf("%T", ln), Equals, "*levelmap.sqliteLevelMap")451 c.Check(fmt.Sprintf("%T", ln), Equals, "*seenstate.sqliteSeenState")
407}452}
408453
409/*****************************************************************454/*****************************************************************
@@ -439,7 +484,7 @@
439func (cs *clientSuite) TestHandleConnStateC2D(c *C) {484func (cs *clientSuite) TestHandleConnStateC2D(c *C) {
440 cli := NewPushClient(cs.configPath, cs.leveldbPath)485 cli := NewPushClient(cs.configPath, cs.leveldbPath)
441 cli.log = cs.log486 cli.log = cs.log
442 cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, levelmap.NewLevelMap, cs.log)487 cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, seenstate.NewSeenState, cs.log)
443 cli.session.Dial()488 cli.session.Dial()
444 cli.hasConnectivity = true489 cli.hasConnectivity = true
445490
@@ -452,7 +497,7 @@
452func (cs *clientSuite) TestHandleConnStateC2DPending(c *C) {497func (cs *clientSuite) TestHandleConnStateC2DPending(c *C) {
453 cli := NewPushClient(cs.configPath, cs.leveldbPath)498 cli := NewPushClient(cs.configPath, cs.leveldbPath)
454 cli.log = cs.log499 cli.log = cs.log
455 cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, levelmap.NewLevelMap, cs.log)500 cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, seenstate.NewSeenState, cs.log)
456 cli.hasConnectivity = true501 cli.hasConnectivity = true
457502
458 cli.handleConnState(false)503 cli.handleConnState(false)
@@ -460,7 +505,7 @@
460}505}
461506
462/*****************************************************************507/*****************************************************************
463 filterNotification tests508 filterBroadcastNotification tests
464******************************************************************/509******************************************************************/
465510
466var siInfoRes = &systemimage.InfoResult{511var siInfoRes = &systemimage.InfoResult{
@@ -470,23 +515,23 @@
470 LastUpdate: "Unknown",515 LastUpdate: "Unknown",
471}516}
472517
473func (cs *clientSuite) TestFilterNotification(c *C) {518func (cs *clientSuite) TestFilterBroadcastNotification(c *C) {
474 cli := NewPushClient(cs.configPath, cs.leveldbPath)519 cli := NewPushClient(cs.configPath, cs.leveldbPath)
475 cli.systemImageInfo = siInfoRes520 cli.systemImageInfo = siInfoRes
476 // empty521 // empty
477 msg := &session.Notification{}522 msg := &session.BroadcastNotification{}
478 c.Check(cli.filterNotification(msg), Equals, false)523 c.Check(cli.filterBroadcastNotification(msg), Equals, false)
479 // same build number524 // same build number
480 msg = &session.Notification{525 msg = &session.BroadcastNotification{
481 Decoded: []map[string]interface{}{526 Decoded: []map[string]interface{}{
482 map[string]interface{}{527 map[string]interface{}{
483 "daily/mako": []interface{}{float64(102), "tubular"},528 "daily/mako": []interface{}{float64(102), "tubular"},
484 },529 },
485 },530 },
486 }531 }
487 c.Check(cli.filterNotification(msg), Equals, false)532 c.Check(cli.filterBroadcastNotification(msg), Equals, false)
488 // higher build number and pick last533 // higher build number and pick last
489 msg = &session.Notification{534 msg = &session.BroadcastNotification{
490 Decoded: []map[string]interface{}{535 Decoded: []map[string]interface{}{
491 map[string]interface{}{536 map[string]interface{}{
492 "daily/mako": []interface{}{float64(102), "tubular"},537 "daily/mako": []interface{}{float64(102), "tubular"},
@@ -496,9 +541,9 @@
496 },541 },
497 },542 },
498 }543 }
499 c.Check(cli.filterNotification(msg), Equals, true)544 c.Check(cli.filterBroadcastNotification(msg), Equals, true)
500 // going backward by a margin, assume switch of alias545 // going backward by a margin, assume switch of alias
501 msg = &session.Notification{546 msg = &session.BroadcastNotification{
502 Decoded: []map[string]interface{}{547 Decoded: []map[string]interface{}{
503 map[string]interface{}{548 map[string]interface{}{
504 "daily/mako": []interface{}{float64(102), "tubular"},549 "daily/mako": []interface{}{float64(102), "tubular"},
@@ -508,47 +553,47 @@
508 },553 },
509 },554 },
510 }555 }
511 c.Check(cli.filterNotification(msg), Equals, true)556 c.Check(cli.filterBroadcastNotification(msg), Equals, true)
512}557}
513558
514func (cs *clientSuite) TestFilterNotificationRobust(c *C) {559func (cs *clientSuite) TestFilterBroadcastNotificationRobust(c *C) {
515 cli := NewPushClient(cs.configPath, cs.leveldbPath)560 cli := NewPushClient(cs.configPath, cs.leveldbPath)
516 cli.systemImageInfo = siInfoRes561 cli.systemImageInfo = siInfoRes
517 msg := &session.Notification{562 msg := &session.BroadcastNotification{
518 Decoded: []map[string]interface{}{563 Decoded: []map[string]interface{}{
519 map[string]interface{}{},564 map[string]interface{}{},
520 },565 },
521 }566 }
522 c.Check(cli.filterNotification(msg), Equals, false)567 c.Check(cli.filterBroadcastNotification(msg), Equals, false)
523 for _, broken := range []interface{}{568 for _, broken := range []interface{}{
524 5,569 5,
525 []interface{}{},570 []interface{}{},
526 []interface{}{55},571 []interface{}{55},
527 } {572 } {
528 msg := &session.Notification{573 msg := &session.BroadcastNotification{
529 Decoded: []map[string]interface{}{574 Decoded: []map[string]interface{}{
530 map[string]interface{}{575 map[string]interface{}{
531 "daily/mako": broken,576 "daily/mako": broken,
532 },577 },
533 },578 },
534 }579 }
535 c.Check(cli.filterNotification(msg), Equals, false)580 c.Check(cli.filterBroadcastNotification(msg), Equals, false)
536 }581 }
537}582}
538583
539/*****************************************************************584/*****************************************************************
540 handleNotification tests585 handleBroadcastNotification tests
541******************************************************************/586******************************************************************/
542587
543var (588var (
544 positiveNotification = &session.Notification{589 positiveBroadcastNotification = &session.BroadcastNotification{
545 Decoded: []map[string]interface{}{590 Decoded: []map[string]interface{}{
546 map[string]interface{}{591 map[string]interface{}{
547 "daily/mako": []interface{}{float64(103), "tubular"},592 "daily/mako": []interface{}{float64(103), "tubular"},
548 },593 },
549 },594 },
550 }595 }
551 negativeNotification = &session.Notification{596 negativeBroadcastNotification = &session.BroadcastNotification{
552 Decoded: []map[string]interface{}{597 Decoded: []map[string]interface{}{
553 map[string]interface{}{598 map[string]interface{}{
554 "daily/mako": []interface{}{float64(102), "tubular"},599 "daily/mako": []interface{}{float64(102), "tubular"},
@@ -557,13 +602,13 @@
557 }602 }
558)603)
559604
560func (cs *clientSuite) TestHandleNotification(c *C) {605func (cs *clientSuite) TestHandleBroadcastNotification(c *C) {
561 cli := NewPushClient(cs.configPath, cs.leveldbPath)606 cli := NewPushClient(cs.configPath, cs.leveldbPath)
562 cli.systemImageInfo = siInfoRes607 cli.systemImageInfo = siInfoRes
563 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))608 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
564 cli.notificationsEndp = endp609 cli.notificationsEndp = endp
565 cli.log = cs.log610 cli.log = cs.log
566 c.Check(cli.handleNotification(positiveNotification), IsNil)611 c.Check(cli.handleBroadcastNotification(positiveBroadcastNotification), IsNil)
567 // check we sent the notification612 // check we sent the notification
568 args := testibus.GetCallArgs(endp)613 args := testibus.GetCallArgs(endp)
569 c.Assert(args, HasLen, 1)614 c.Assert(args, HasLen, 1)
@@ -571,26 +616,48 @@
571 c.Check(cs.log.Captured(), Matches, `.* got notification id \d+\s*`)616 c.Check(cs.log.Captured(), Matches, `.* got notification id \d+\s*`)
572}617}
573618
574func (cs *clientSuite) TestHandleNotificationNothingToDo(c *C) {619func (cs *clientSuite) TestHandleBroadcastNotificationNothingToDo(c *C) {
575 cli := NewPushClient(cs.configPath, cs.leveldbPath)620 cli := NewPushClient(cs.configPath, cs.leveldbPath)
576 cli.systemImageInfo = siInfoRes621 cli.systemImageInfo = siInfoRes
577 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))622 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
578 cli.notificationsEndp = endp623 cli.notificationsEndp = endp
579 cli.log = cs.log624 cli.log = cs.log
580 c.Check(cli.handleNotification(negativeNotification), IsNil)625 c.Check(cli.handleBroadcastNotification(negativeBroadcastNotification), IsNil)
581 // check we sent the notification626 // check we sent the notification
582 args := testibus.GetCallArgs(endp)627 args := testibus.GetCallArgs(endp)
583 c.Assert(args, HasLen, 0)628 c.Assert(args, HasLen, 0)
584 c.Check(cs.log.Captured(), Matches, "")629 c.Check(cs.log.Captured(), Matches, "")
585}630}
586631
587func (cs *clientSuite) TestHandleNotificationFail(c *C) {632func (cs *clientSuite) TestHandleBroadcastNotificationFail(c *C) {
588 cli := NewPushClient(cs.configPath, cs.leveldbPath)633 cli := NewPushClient(cs.configPath, cs.leveldbPath)
589 cli.systemImageInfo = siInfoRes634 cli.systemImageInfo = siInfoRes
590 cli.log = cs.log635 cli.log = cs.log
591 endp := testibus.NewTestingEndpoint(nil, condition.Work(false))636 endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
592 cli.notificationsEndp = endp637 cli.notificationsEndp = endp
593 c.Check(cli.handleNotification(positiveNotification), NotNil)638 c.Check(cli.handleBroadcastNotification(positiveBroadcastNotification), NotNil)
639}
640
641/*****************************************************************
642 handleUnicastNotification tests
643******************************************************************/
644
645var notif = &protocol.Notification{AppId: "hello", Payload: []byte(`{"url": "xyzzy"}`), MsgId: "42"}
646
647func (cs *clientSuite) TestHandleUcastNotification(c *C) {
648 cli := NewPushClient(cs.configPath, cs.leveldbPath)
649 svcEndp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(1))
650 cli.log = cs.log
651 cli.serviceEndpoint = svcEndp
652 notsEndp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
653 cli.notificationsEndp = notsEndp
654 c.Assert(cli.startService(), IsNil)
655 c.Check(cli.handleUnicastNotification(notif), IsNil)
656 // check we sent the notification
657 args := testibus.GetCallArgs(svcEndp)
658 c.Assert(len(args), Not(Equals), 0)
659 c.Check(args[len(args)-1].Member, Equals, "::Signal")
660 c.Check(cs.log.Captured(), Matches, `(?m).*sending notification "42" for "hello".*`)
594}661}
595662
596/*****************************************************************663/*****************************************************************
@@ -608,18 +675,31 @@
608 args := testibus.GetCallArgs(endp)675 args := testibus.GetCallArgs(endp)
609 c.Assert(args, HasLen, 0)676 c.Assert(args, HasLen, 0)
610 // check we worked with the right action id677 // check we worked with the right action id
611 c.Check(cli.handleClick(ACTION_ID_SNOWFLAKE), IsNil)678 c.Check(cli.handleClick(ACTION_ID_BROADCAST), IsNil)
612 // check we sent the notification679 // check we sent the notification
613 args = testibus.GetCallArgs(endp)680 args = testibus.GetCallArgs(endp)
614 c.Assert(args, HasLen, 1)681 c.Assert(args, HasLen, 1)
615 c.Check(args[0].Member, Equals, "DispatchURL")682 c.Check(args[0].Member, Equals, "DispatchURL")
616 c.Check(args[0].Args, DeepEquals, []interface{}{"settings:///system/system-update"})683 c.Check(args[0].Args, DeepEquals, []interface{}{system_update_url})
684 // check we worked with the right action id
685 c.Check(cli.handleClick(ACTION_ID_SNOWFLAKE+"foo"), IsNil)
686 // check we sent the notification
687 args = testibus.GetCallArgs(endp)
688 c.Assert(args, HasLen, 2)
689 c.Check(args[1].Member, Equals, "DispatchURL")
690 c.Check(args[1].Args, DeepEquals, []interface{}{"foo"})
617}691}
618692
619/*****************************************************************693/*****************************************************************
620 doLoop tests694 doLoop tests
621******************************************************************/695******************************************************************/
622696
697var nopConn = func(bool) {}
698var nopClick = func(string) error { return nil }
699var nopBcast = func(*session.BroadcastNotification) error { return nil }
700var nopUcast = func(*protocol.Notification) error { return nil }
701var nopError = func(error) {}
702
623func (cs *clientSuite) TestDoLoopConn(c *C) {703func (cs *clientSuite) TestDoLoopConn(c *C) {
624 cli := NewPushClient(cs.configPath, cs.leveldbPath)704 cli := NewPushClient(cs.configPath, cs.leveldbPath)
625 cli.log = cs.log705 cli.log = cs.log
@@ -629,7 +709,7 @@
629 c.Assert(cli.initSession(), IsNil)709 c.Assert(cli.initSession(), IsNil)
630710
631 ch := make(chan bool, 1)711 ch := make(chan bool, 1)
632 go cli.doLoop(func(bool) { ch <- true }, func(_ string) error { return nil }, func(_ *session.Notification) error { return nil }, func(error) {})712 go cli.doLoop(func(bool) { ch <- true }, nopClick, nopBcast, nopUcast, nopError)
633 c.Check(takeNextBool(ch), Equals, true)713 c.Check(takeNextBool(ch), Equals, true)
634}714}
635715
@@ -643,7 +723,20 @@
643 cli.actionsCh = aCh723 cli.actionsCh = aCh
644724
645 ch := make(chan bool, 1)725 ch := make(chan bool, 1)
646 go cli.doLoop(func(bool) {}, func(_ string) error { ch <- true; return nil }, func(_ *session.Notification) error { return nil }, func(error) {})726 go cli.doLoop(nopConn, func(_ string) error { ch <- true; return nil }, nopBcast, nopUcast, nopError)
727 c.Check(takeNextBool(ch), Equals, true)
728}
729
730func (cs *clientSuite) TestDoLoopBroadcast(c *C) {
731 cli := NewPushClient(cs.configPath, cs.leveldbPath)
732 cli.log = cs.log
733 cli.systemImageInfo = siInfoRes
734 c.Assert(cli.initSession(), IsNil)
735 cli.session.BroadcastCh = make(chan *session.BroadcastNotification, 1)
736 cli.session.BroadcastCh <- &session.BroadcastNotification{}
737
738 ch := make(chan bool, 1)
739 go cli.doLoop(nopConn, nopClick, func(_ *session.BroadcastNotification) error { ch <- true; return nil }, nopUcast, nopError)
647 c.Check(takeNextBool(ch), Equals, true)740 c.Check(takeNextBool(ch), Equals, true)
648}741}
649742
@@ -652,11 +745,11 @@
652 cli.log = cs.log745 cli.log = cs.log
653 cli.systemImageInfo = siInfoRes746 cli.systemImageInfo = siInfoRes
654 c.Assert(cli.initSession(), IsNil)747 c.Assert(cli.initSession(), IsNil)
655 cli.session.MsgCh = make(chan *session.Notification, 1)748 cli.session.NotificationsCh = make(chan *protocol.Notification, 1)
656 cli.session.MsgCh <- &session.Notification{}749 cli.session.NotificationsCh <- &protocol.Notification{}
657750
658 ch := make(chan bool, 1)751 ch := make(chan bool, 1)
659 go cli.doLoop(func(bool) {}, func(_ string) error { return nil }, func(_ *session.Notification) error { ch <- true; return nil }, func(error) {})752 go cli.doLoop(nopConn, nopClick, nopBcast, func(*protocol.Notification) error { ch <- true; return nil }, nopError)
660 c.Check(takeNextBool(ch), Equals, true)753 c.Check(takeNextBool(ch), Equals, true)
661}754}
662755
@@ -669,7 +762,7 @@
669 cli.session.ErrCh <- nil762 cli.session.ErrCh <- nil
670763
671 ch := make(chan bool, 1)764 ch := make(chan bool, 1)
672 go cli.doLoop(func(bool) {}, func(_ string) error { return nil }, func(_ *session.Notification) error { return nil }, func(error) { ch <- true })765 go cli.doLoop(nopConn, nopClick, nopBcast, nopUcast, func(error) { ch <- true })
673 c.Check(takeNextBool(ch), Equals, true)766 c.Check(takeNextBool(ch), Equals, true)
674}767}
675768
@@ -719,7 +812,7 @@
719 cli.systemImageInfo = siInfoRes812 cli.systemImageInfo = siInfoRes
720 c.Assert(cli.initSession(), IsNil)813 c.Assert(cli.initSession(), IsNil)
721814
722 cli.session.MsgCh = make(chan *session.Notification)815 cli.session.BroadcastCh = make(chan *session.BroadcastNotification)
723 cli.session.ErrCh = make(chan error)816 cli.session.ErrCh = make(chan error)
724817
725 // we use tick() to make sure things have been through the818 // we use tick() to make sure things have been through the
@@ -736,7 +829,7 @@
736 c.Check(cs.log.Captured(), Matches, "(?ms).*Session connected after 42 attempts$")829 c.Check(cs.log.Captured(), Matches, "(?ms).*Session connected after 42 attempts$")
737830
738 // * actionsCh to the click handler/url dispatcher831 // * actionsCh to the click handler/url dispatcher
739 aCh <- notifications.RawActionReply{ActionId: ACTION_ID_SNOWFLAKE}832 aCh <- notifications.RawActionReply{ActionId: ACTION_ID_BROADCAST}
740 tick()833 tick()
741 uargs := testibus.GetCallArgs(cli.urlDispatcherEndp)834 uargs := testibus.GetCallArgs(cli.urlDispatcherEndp)
742 c.Assert(uargs, HasLen, 1)835 c.Assert(uargs, HasLen, 1)
@@ -752,8 +845,8 @@
752 tick()845 tick()
753 c.Check(cli.hasConnectivity, Equals, false)846 c.Check(cli.hasConnectivity, Equals, false)
754847
755 // * session.MsgCh to the notifications handler848 // * session.BroadcastCh to the notifications handler
756 cli.session.MsgCh <- positiveNotification849 cli.session.BroadcastCh <- positiveBroadcastNotification
757 tick()850 tick()
758 nargs := testibus.GetCallArgs(cli.notificationsEndp)851 nargs := testibus.GetCallArgs(cli.notificationsEndp)
759 c.Check(nargs, HasLen, 1)852 c.Check(nargs, HasLen, 1)
@@ -785,6 +878,8 @@
785878
786 cli := NewPushClient(cs.configPath, cs.leveldbPath)879 cli := NewPushClient(cs.configPath, cs.leveldbPath)
787 // before start, everything sucks:880 // before start, everything sucks:
881 // no service,
882 c.Check(cli.service, IsNil)
788 // no config,883 // no config,
789 c.Check(string(cli.config.Addr), Equals, "")884 c.Check(string(cli.config.Addr), Equals, "")
790 // no device id,885 // no device id,
@@ -803,12 +898,15 @@
803 // and now everthing is better! We have a config,898 // and now everthing is better! We have a config,
804 c.Check(string(cli.config.Addr), Equals, ":0")899 c.Check(string(cli.config.Addr), Equals, ":0")
805 // and a device id,900 // and a device id,
806 c.Check(cli.deviceId, HasLen, 128)901 c.Check(cli.deviceId, HasLen, 40)
807 // and a session,902 // and a session,
808 c.Check(cli.session, NotNil)903 c.Check(cli.session, NotNil)
809 // and a bus,904 // and a bus,
810 c.Check(cli.notificationsEndp, NotNil)905 c.Check(cli.notificationsEndp, NotNil)
906 // and a service,
907 c.Check(cli.service, NotNil)
811 // and everthying us just peachy!908 // and everthying us just peachy!
909 cli.service.Stop() // cleanup
812}910}
813911
814func (cs *clientSuite) TestStartCanFail(c *C) {912func (cs *clientSuite) TestStartCanFail(c *C) {
@@ -818,3 +916,38 @@
818 // and it works. Err. Doesn't.916 // and it works. Err. Doesn't.
819 c.Check(err, NotNil)917 c.Check(err, NotNil)
820}918}
919
920func (cs *clientSuite) TestMessageHandler(c *C) {
921 cli := NewPushClient(cs.configPath, cs.leveldbPath)
922 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
923 cli.notificationsEndp = endp
924 cli.log = cs.log
925 err := cli.messageHandler([]byte(`{"icon": "icon-value", "summary": "summary-value", "body": "body-value"}`))
926 c.Assert(err, IsNil)
927 args := testibus.GetCallArgs(endp)
928 c.Assert(args, HasLen, 1)
929 c.Check(args[0].Member, Equals, "Notify")
930 c.Check(args[0].Args[0], Equals, "ubuntu-push-client")
931 c.Check(args[0].Args[2], Equals, "icon-value")
932 c.Check(args[0].Args[3], Equals, "summary-value")
933 c.Check(args[0].Args[4], Equals, "body-value")
934}
935
936func (cs *clientSuite) TestMessageHandlerReportsUnmarshalErrors(c *C) {
937 cli := NewPushClient(cs.configPath, cs.leveldbPath)
938 cli.log = cs.log
939
940 err := cli.messageHandler([]byte(`{"broken`))
941 c.Check(err, NotNil)
942 c.Check(cs.log.Captured(), Matches, "(?msi).*unable to unmarshal message:.*")
943}
944
945func (cs *clientSuite) TestMessageHandlerReportsFailedNotifies(c *C) {
946 cli := NewPushClient(cs.configPath, cs.leveldbPath)
947 endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
948 cli.notificationsEndp = endp
949 cli.log = cs.log
950 err := cli.messageHandler([]byte(`{}`))
951 c.Assert(err, NotNil)
952 c.Check(cs.log.Captured(), Matches, "(?msi).*showing notification: no way$")
953}
821954
=== added directory 'client/service'
=== added file 'client/service/service.go'
--- client/service/service.go 1970-01-01 00:00:00 +0000
+++ client/service/service.go 2014-05-29 12:04:32 +0000
@@ -0,0 +1,209 @@
1/*
2 Copyright 2013-2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17// package service implements the dbus-level service with which client
18// applications are expected to interact.
19package service
20
21import (
22 "errors"
23 "os"
24 "sync"
25
26 "launchpad.net/ubuntu-push/bus"
27 "launchpad.net/ubuntu-push/logger"
28)
29
30// Service is the dbus api
31type Service struct {
32 lock sync.RWMutex
33 state ServiceState
34 mbox map[string][]string
35 msgHandler func([]byte) error
36 Log logger.Logger
37 Bus bus.Endpoint
38}
39
40// the service can be in a numnber of states
41type ServiceState uint8
42
43const (
44 StateUnknown ServiceState = iota
45 StateRunning // Start() has been successfully called
46 StateFinished // Stop() has been successfully called
47)
48
49var (
50 NotConfigured = errors.New("not configured")
51 AlreadyStarted = errors.New("already started")
52 BusAddress = bus.Address{
53 Interface: "com.ubuntu.PushNotifications",
54 Path: "/com/ubuntu/PushNotifications",
55 Name: "com.ubuntu.PushNotifications",
56 }
57)
58
59// NewService() builds a new service and returns it.
60func NewService(bus bus.Endpoint, log logger.Logger) *Service {
61 return &Service{Log: log, Bus: bus}
62}
63
64// SetMessageHandler() sets the message-handling callback
65func (svc *Service) SetMessageHandler(callback func([]byte) error) {
66 svc.lock.Lock()
67 defer svc.lock.Unlock()
68 svc.msgHandler = callback
69}
70
71// GetMessageHandler() returns the (possibly nil) messaging handler callback
72func (svc *Service) GetMessageHandler() func([]byte) error {
73 svc.lock.RLock()
74 defer svc.lock.RUnlock()
75 return svc.msgHandler
76}
77
78// IsRunning() returns whether the service's state is StateRunning
79func (svc *Service) IsRunning() bool {
80 svc.lock.RLock()
81 defer svc.lock.RUnlock()
82 return svc.state == StateRunning
83}
84
85// Start() dials the bus, grab the name, and listens for method calls.
86func (svc *Service) Start() error {
87 svc.lock.Lock()
88 defer svc.lock.Unlock()
89 if svc.state != StateUnknown {
90 return AlreadyStarted
91 }
92 if svc.Log == nil || svc.Bus == nil {
93 return NotConfigured
94 }
95 err := svc.Bus.Dial()
96 if err != nil {
97 return err
98 }
99 ch := svc.Bus.GrabName(true)
100 log := svc.Log
101 go func() {
102 for err := range ch {
103 if !svc.IsRunning() {
104 break
105 }
106 if err != nil {
107 log.Fatalf("name channel for %s got: %v",
108 BusAddress.Name, err)
109 }
110 }
111 }()
112 svc.Bus.WatchMethod(bus.DispatchMap{
113 "Register": svc.register,
114 "Notifications": svc.notifications,
115 "Inject": svc.inject,
116 }, svc)
117 svc.state = StateRunning
118 return nil
119}
120
121// Stop() closes the bus and sets the state to StateFinished
122func (svc *Service) Stop() {
123 svc.lock.Lock()
124 defer svc.lock.Unlock()
125 if svc.Bus != nil {
126 svc.Bus.Close()
127 }
128 svc.state = StateFinished
129}
130
131var (
132 BadArgCount = errors.New("Wrong number of arguments")
133 BadArgType = errors.New("Bad argument type")
134)
135
136func (svc *Service) register(args []interface{}, _ []interface{}) ([]interface{}, error) {
137 if len(args) != 1 {
138 return nil, BadArgCount
139 }
140 appname, ok := args[0].(string)
141 if !ok {
142 return nil, BadArgType
143 }
144
145 rv := os.Getenv("PUSH_REG_" + appname)
146 if rv == "" {
147 rv = "this-is-an-opaque-block-of-random-bits-i-promise"
148 }
149
150 return []interface{}{rv}, nil
151}
152
153func (svc *Service) notifications(args []interface{}, _ []interface{}) ([]interface{}, error) {
154 if len(args) != 1 {
155 return nil, BadArgCount
156 }
157 appname, ok := args[0].(string)
158 if !ok {
159 return nil, BadArgType
160 }
161
162 svc.lock.Lock()
163 defer svc.lock.Unlock()
164
165 if svc.mbox == nil {
166 return []interface{}{[]string(nil)}, nil
167 }
168 msgs := svc.mbox[appname]
169 delete(svc.mbox, appname)
170
171 return []interface{}{msgs}, nil
172}
173
174func (svc *Service) inject(args []interface{}, _ []interface{}) ([]interface{}, error) {
175 if len(args) != 2 {
176 return nil, BadArgCount
177 }
178 appname, ok := args[0].(string)
179 if !ok {
180 return nil, BadArgType
181 }
182 notif, ok := args[1].(string)
183 if !ok {
184 return nil, BadArgType
185 }
186
187 return nil, svc.Inject(appname, notif)
188}
189
190// Inject() signals to an application over dbus that a notification
191// has arrived.
192func (svc *Service) Inject(appname string, notif string) error {
193 svc.lock.Lock()
194 defer svc.lock.Unlock()
195 if svc.mbox == nil {
196 svc.mbox = make(map[string][]string)
197 }
198 svc.mbox[appname] = append(svc.mbox[appname], notif)
199 if svc.msgHandler != nil {
200 err := svc.msgHandler([]byte(notif))
201 if err != nil {
202 svc.Log.Errorf("msgHandler returned %v", err)
203 return err
204 }
205 svc.Log.Debugf("call to msgHandler successful")
206 }
207
208 return svc.Bus.Signal("Notification", []interface{}{appname})
209}
0210
=== added file 'client/service/service_test.go'
--- client/service/service_test.go 1970-01-01 00:00:00 +0000
+++ client/service/service_test.go 2014-05-29 12:04:32 +0000
@@ -0,0 +1,247 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package service
18
19import (
20 "errors"
21 "os"
22 "testing"
23
24 . "launchpad.net/gocheck"
25
26 "launchpad.net/ubuntu-push/bus"
27 testibus "launchpad.net/ubuntu-push/bus/testing"
28 "launchpad.net/ubuntu-push/logger"
29 helpers "launchpad.net/ubuntu-push/testing"
30 "launchpad.net/ubuntu-push/testing/condition"
31)
32
33func TestService(t *testing.T) { TestingT(t) }
34
35type serviceSuite struct {
36 log logger.Logger
37 bus bus.Endpoint
38}
39
40var _ = Suite(&serviceSuite{})
41
42func (ss *serviceSuite) SetUpTest(c *C) {
43 ss.log = helpers.NewTestLogger(c, "debug")
44 ss.bus = testibus.NewTestingEndpoint(condition.Work(true), nil)
45}
46
47func (ss *serviceSuite) TestStart(c *C) {
48 svc := NewService(ss.bus, ss.log)
49 c.Check(svc.IsRunning(), Equals, false)
50 c.Check(svc.Start(), IsNil)
51 c.Check(svc.IsRunning(), Equals, true)
52 svc.Stop()
53}
54
55func (ss *serviceSuite) TestStartTwice(c *C) {
56 svc := NewService(ss.bus, ss.log)
57 c.Check(svc.Start(), IsNil)
58 c.Check(svc.Start(), Equals, AlreadyStarted)
59 svc.Stop()
60}
61
62func (ss *serviceSuite) TestStartNoLog(c *C) {
63 svc := NewService(ss.bus, nil)
64 c.Check(svc.Start(), Equals, NotConfigured)
65}
66
67func (ss *serviceSuite) TestStartNoBus(c *C) {
68 svc := NewService(nil, ss.log)
69 c.Check(svc.Start(), Equals, NotConfigured)
70}
71
72func (ss *serviceSuite) TestStartFailsOnBusDialFailure(c *C) {
73 bus := testibus.NewTestingEndpoint(condition.Work(false), nil)
74 svc := NewService(bus, ss.log)
75 c.Check(svc.Start(), ErrorMatches, `.*(?i)cond said no.*`)
76 svc.Stop()
77}
78
79func (ss *serviceSuite) TestStartGrabsName(c *C) {
80 svc := NewService(ss.bus, ss.log)
81 c.Assert(svc.Start(), IsNil)
82 callArgs := testibus.GetCallArgs(ss.bus)
83 defer svc.Stop()
84 c.Assert(callArgs, NotNil)
85 c.Check(callArgs[0].Member, Equals, "::GrabName")
86}
87
88func (ss *serviceSuite) TestStopClosesBus(c *C) {
89 svc := NewService(ss.bus, ss.log)
90 c.Assert(svc.Start(), IsNil)
91 svc.Stop()
92 callArgs := testibus.GetCallArgs(ss.bus)
93 c.Assert(callArgs, NotNil)
94 c.Check(callArgs[len(callArgs)-1].Member, Equals, "::Close")
95}
96
97// registration tests
98
99func (ss *serviceSuite) TestRegistrationFailsIfBadArgs(c *C) {
100 for i, s := range []struct {
101 args []interface{}
102 errt error
103 }{
104 {nil, BadArgCount}, // no args
105 {[]interface{}{}, BadArgCount}, // still no args
106 {[]interface{}{42}, BadArgType}, // bad arg type
107 {[]interface{}{1, 2}, BadArgCount}, // too many args
108 } {
109 reg, err := new(Service).register(s.args, nil)
110 c.Check(reg, IsNil, Commentf("iteration #%d", i))
111 c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
112 }
113}
114
115func (ss *serviceSuite) TestRegistrationWorks(c *C) {
116 reg, err := new(Service).register([]interface{}{"this"}, nil)
117 c.Assert(reg, HasLen, 1)
118 regs, ok := reg[0].(string)
119 c.Check(ok, Equals, true)
120 c.Check(regs, Not(Equals), "")
121 c.Check(err, IsNil)
122}
123
124func (ss *serviceSuite) TestRegistrationOverrideWorks(c *C) {
125 os.Setenv("PUSH_REG_stuff", "42")
126 defer os.Setenv("PUSH_REG_stuff", "")
127
128 reg, err := new(Service).register([]interface{}{"stuff"}, nil)
129 c.Assert(reg, HasLen, 1)
130 regs, ok := reg[0].(string)
131 c.Check(ok, Equals, true)
132 c.Check(regs, Equals, "42")
133 c.Check(err, IsNil)
134}
135
136//
137// Injection tests
138
139func (ss *serviceSuite) TestInjectWorks(c *C) {
140 svc := NewService(ss.bus, ss.log)
141 rvs, err := svc.inject([]interface{}{"hello", "world"}, nil)
142 c.Assert(err, IsNil)
143 c.Check(rvs, IsNil)
144 rvs, err = svc.inject([]interface{}{"hello", "there"}, nil)
145 c.Assert(err, IsNil)
146 c.Check(rvs, IsNil)
147 c.Assert(svc.mbox, HasLen, 1)
148 c.Assert(svc.mbox["hello"], HasLen, 2)
149 c.Check(svc.mbox["hello"][0], Equals, "world")
150 c.Check(svc.mbox["hello"][1], Equals, "there")
151
152 // and check it fired the right signal (twice)
153 callArgs := testibus.GetCallArgs(ss.bus)
154 c.Assert(callArgs, HasLen, 2)
155 c.Check(callArgs[0].Member, Equals, "::Signal")
156 c.Check(callArgs[0].Args, DeepEquals, []interface{}{"Notification", []interface{}{"hello"}})
157 c.Check(callArgs[1], DeepEquals, callArgs[0])
158}
159
160func (ss *serviceSuite) TestInjectFailsIfInjectFails(c *C) {
161 bus := testibus.NewTestingEndpoint(condition.Work(true),
162 condition.Work(false))
163 svc := NewService(bus, ss.log)
164 svc.SetMessageHandler(func([]byte) error { return errors.New("fail") })
165 _, err := svc.inject([]interface{}{"hello", "xyzzy"}, nil)
166 c.Check(err, NotNil)
167}
168
169func (ss *serviceSuite) TestInjectFailsIfBadArgs(c *C) {
170 for i, s := range []struct {
171 args []interface{}
172 errt error
173 }{
174 {nil, BadArgCount},
175 {[]interface{}{}, BadArgCount},
176 {[]interface{}{1}, BadArgCount},
177 {[]interface{}{1, 2}, BadArgType},
178 {[]interface{}{"1", 2}, BadArgType},
179 {[]interface{}{1, "2"}, BadArgType},
180 {[]interface{}{1, 2, 3}, BadArgCount},
181 } {
182 reg, err := new(Service).inject(s.args, nil)
183 c.Check(reg, IsNil, Commentf("iteration #%d", i))
184 c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
185 }
186}
187
188//
189// Notifications tests
190func (ss *serviceSuite) TestNotificationsWorks(c *C) {
191 svc := NewService(ss.bus, ss.log)
192 nots, err := svc.notifications([]interface{}{"hello"}, nil)
193 c.Assert(err, IsNil)
194 c.Assert(nots, NotNil)
195 c.Assert(nots, HasLen, 1)
196 c.Check(nots[0], HasLen, 0)
197 if svc.mbox == nil {
198 svc.mbox = make(map[string][]string)
199 }
200 svc.mbox["hello"] = append(svc.mbox["hello"], "this", "thing")
201 nots, err = svc.notifications([]interface{}{"hello"}, nil)
202 c.Assert(err, IsNil)
203 c.Assert(nots, NotNil)
204 c.Assert(nots, HasLen, 1)
205 c.Check(nots[0], DeepEquals, []string{"this", "thing"})
206}
207
208func (ss *serviceSuite) TestNotificationsFailsIfBadArgs(c *C) {
209 for i, s := range []struct {
210 args []interface{}
211 errt error
212 }{
213 {nil, BadArgCount}, // no args
214 {[]interface{}{}, BadArgCount}, // still no args
215 {[]interface{}{42}, BadArgType}, // bad arg type
216 {[]interface{}{1, 2}, BadArgCount}, // too many args
217 } {
218 reg, err := new(Service).notifications(s.args, nil)
219 c.Check(reg, IsNil, Commentf("iteration #%d", i))
220 c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
221 }
222}
223
224func (ss *serviceSuite) TestMessageHandler(c *C) {
225 svc := new(Service)
226 c.Assert(svc.msgHandler, IsNil)
227 var ext = []byte{}
228 e := errors.New("Hello")
229 f := func(s []byte) error { ext = s; return e }
230 c.Check(svc.GetMessageHandler(), IsNil)
231 svc.SetMessageHandler(f)
232 c.Check(svc.GetMessageHandler(), NotNil)
233 c.Check(svc.msgHandler([]byte("37")), Equals, e)
234 c.Check(ext, DeepEquals, []byte("37"))
235}
236
237func (ss *serviceSuite) TestInjectCallsMessageHandler(c *C) {
238 var ext = []byte{}
239 svc := NewService(ss.bus, ss.log)
240 f := func(s []byte) error { ext = s; return nil }
241 svc.SetMessageHandler(f)
242 c.Check(svc.Inject("stuff", "{}"), IsNil)
243 c.Check(ext, DeepEquals, []byte("{}"))
244 err := errors.New("ouch")
245 svc.SetMessageHandler(func([]byte) error { return err })
246 c.Check(svc.Inject("stuff", "{}"), Equals, err)
247}
0248
=== renamed directory 'client/session/levelmap' => 'client/session/seenstate'
=== renamed file 'client/session/levelmap/levelmap.go' => 'client/session/seenstate/seenstate.go'
--- client/session/levelmap/levelmap.go 2014-02-21 16:17:28 +0000
+++ client/session/seenstate/seenstate.go 2014-05-29 12:04:32 +0000
@@ -14,31 +14,57 @@
14 with this program. If not, see <http://www.gnu.org/licenses/>.14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/15*/
1616
17// Package levelmap holds implementations of the LevelMap that the client17// Package seenstate holds implementations of the SeenState that the client
18// session uses to keep track of what messages it has seen.18// session uses to keep track of what messages it has seen.
19package levelmap19package seenstate
2020
21type LevelMap interface {21import (
22 "launchpad.net/ubuntu-push/protocol"
23)
24
25type SeenState interface {
22 // Set() (re)sets the given level to the given value.26 // Set() (re)sets the given level to the given value.
23 Set(level string, top int64) error27 SetLevel(level string, top int64) error
24 // GetAll() returns a "simple" map of the current levels.28 // GetAll() returns a "simple" map of the current levels.
25 GetAll() (map[string]int64, error)29 GetAllLevels() (map[string]int64, error)
26}30 // FilterBySeen filters notifications already seen, keep track
2731 // of them as well
28type mapLevelMap map[string]int6432 FilterBySeen([]protocol.Notification) ([]protocol.Notification, error)
2933}
30func (m *mapLevelMap) Set(level string, top int64) error {34
31 (*m)[level] = top35type memSeenState struct {
36 levels map[string]int64
37 seenMsgs map[string]bool
38}
39
40func (m *memSeenState) SetLevel(level string, top int64) error {
41 m.levels[level] = top
32 return nil42 return nil
33}43}
34func (m *mapLevelMap) GetAll() (map[string]int64, error) {44func (m *memSeenState) GetAllLevels() (map[string]int64, error) {
35 return map[string]int64(*m), nil45 return m.levels, nil
36}46}
3747
38var _ LevelMap = &mapLevelMap{}48func (m *memSeenState) FilterBySeen(notifs []protocol.Notification) ([]protocol.Notification, error) {
3949 acc := make([]protocol.Notification, 0, len(notifs))
40// NewLevelMap returns an implementation of LevelMap that is memory-based and50 for _, notif := range notifs {
51 seen := m.seenMsgs[notif.MsgId]
52 if seen {
53 continue
54 }
55 m.seenMsgs[notif.MsgId] = true
56 acc = append(acc, notif)
57 }
58 return acc, nil
59}
60
61var _ SeenState = (*memSeenState)(nil)
62
63// NewSeenState returns an implementation of SeenState that is memory-based and
41// does not save state.64// does not save state.
42func NewLevelMap() (LevelMap, error) {65func NewSeenState() (SeenState, error) {
43 return &mapLevelMap{}, nil66 return &memSeenState{
67 levels: make(map[string]int64),
68 seenMsgs: make(map[string]bool),
69 }, nil
44}70}
4571
=== renamed file 'client/session/levelmap/levelmap_test.go' => 'client/session/seenstate/seenstate_test.go'
--- client/session/levelmap/levelmap_test.go 2014-02-08 13:50:58 +0000
+++ client/session/seenstate/seenstate_test.go 2014-05-29 12:04:32 +0000
@@ -14,42 +14,73 @@
14 with this program. If not, see <http://www.gnu.org/licenses/>.14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/15*/
1616
17package levelmap17package seenstate
1818
19import (19import (
20 "testing"
21
20 . "launchpad.net/gocheck"22 . "launchpad.net/gocheck"
21 "testing"23
24 "launchpad.net/ubuntu-push/protocol"
22)25)
2326
24func TestLevelMap(t *testing.T) { TestingT(t) }27func TestSeenState(t *testing.T) { TestingT(t) }
2528
26type lmSuite struct {29type ssSuite struct {
27 constructor func() (LevelMap, error)30 constructor func() (SeenState, error)
28}31}
2932
30var _ = Suite(&lmSuite{})33var _ = Suite(&ssSuite{})
3134
32func (s *lmSuite) SetUpSuite(c *C) {35func (s *ssSuite) SetUpSuite(c *C) {
33 s.constructor = NewLevelMap36 s.constructor = NewSeenState
34}37}
3538
36func (s *lmSuite) TestAllTheThings(c *C) {39func (s *ssSuite) TestAllTheLevelThings(c *C) {
37 var err error40 var err error
38 var lm LevelMap41 var ss SeenState
39 // checks NewLevelMap returns a LevelMap42 // checks NewSeenState returns a SeenState
40 lm, err = s.constructor()43 ss, err = s.constructor()
41 // and that it works44 // and that it works
42 c.Assert(err, IsNil)45 c.Assert(err, IsNil)
43 // setting a couple of things, sets them46 // setting a couple of things, sets them
44 c.Check(lm.Set("this", 12), IsNil)47 c.Check(ss.SetLevel("this", 12), IsNil)
45 c.Check(lm.Set("that", 42), IsNil)48 c.Check(ss.SetLevel("that", 42), IsNil)
46 all, err := lm.GetAll()49 all, err := ss.GetAllLevels()
47 c.Check(err, IsNil)50 c.Check(err, IsNil)
48 c.Check(all, DeepEquals, map[string]int64{"this": 12, "that": 42})51 c.Check(all, DeepEquals, map[string]int64{"this": 12, "that": 42})
49 // re-setting one of them, resets it52 // re-setting one of them, resets it
50 c.Check(lm.Set("this", 999), IsNil)53 c.Check(ss.SetLevel("this", 999), IsNil)
51 all, err = lm.GetAll()54 all, err = ss.GetAllLevels()
52 c.Check(err, IsNil)55 c.Check(err, IsNil)
53 c.Check(all, DeepEquals, map[string]int64{"this": 999, "that": 42})56 c.Check(all, DeepEquals, map[string]int64{"this": 999, "that": 42})
54 // huzzah57 // huzzah
55}58}
59
60func (s *ssSuite) TestFilterBySeen(c *C) {
61 var err error
62 var ss SeenState
63 ss, err = s.constructor()
64 // and that it works
65 c.Assert(err, IsNil)
66 n1 := protocol.Notification{MsgId: "m1"}
67 n2 := protocol.Notification{MsgId: "m2"}
68 n3 := protocol.Notification{MsgId: "m3"}
69 n4 := protocol.Notification{MsgId: "m4"}
70 n5 := protocol.Notification{MsgId: "m5"}
71
72 res, err := ss.FilterBySeen([]protocol.Notification{n1, n2, n3})
73 c.Assert(err, IsNil)
74 // everything wasn't seen yet
75 c.Check(res, DeepEquals, []protocol.Notification{n1, n2, n3})
76
77 res, err = ss.FilterBySeen([]protocol.Notification{n1, n3, n4, n5})
78 c.Assert(err, IsNil)
79 // already seen n1-n3 removed
80 c.Check(res, DeepEquals, []protocol.Notification{n4, n5})
81
82 // corner case
83 res, err = ss.FilterBySeen([]protocol.Notification{})
84 c.Assert(err, IsNil)
85 c.Assert(res, HasLen, 0)
86}
5687
=== renamed file 'client/session/levelmap/sqlevelmap.go' => 'client/session/seenstate/sqlseenstate.go'
--- client/session/levelmap/sqlevelmap.go 2014-02-12 13:52:19 +0000
+++ client/session/seenstate/sqlseenstate.go 2014-05-29 12:04:32 +0000
@@ -14,21 +14,25 @@
14 with this program. If not, see <http://www.gnu.org/licenses/>.14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/15*/
1616
17package levelmap17package seenstate
1818
19import (19import (
20 _ "code.google.com/p/gosqlite/sqlite3"
21 "database/sql"20 "database/sql"
22 "fmt"21 "fmt"
22 "strings"
23
24 _ "code.google.com/p/gosqlite/sqlite3"
25
26 "launchpad.net/ubuntu-push/protocol"
23)27)
2428
25type sqliteLevelMap struct {29type sqliteSeenState struct {
26 db *sql.DB30 db *sql.DB
27}31}
2832
29// NewSqliteLevelMap returns an implementation of LevelMap that33// NewSqliteSeenState returns an implementation of SeenState that
30// persists the map in an sqlite database.34// keeps and persists the state in an sqlite database.
31func NewSqliteLevelMap(filename string) (LevelMap, error) {35func NewSqliteSeenState(filename string) (SeenState, error) {
32 db, err := sql.Open("sqlite3", filename)36 db, err := sql.Open("sqlite3", filename)
33 if err != nil {37 if err != nil {
34 return nil, fmt.Errorf("cannot open sqlite level map %#v: %v", filename, err)38 return nil, fmt.Errorf("cannot open sqlite level map %#v: %v", filename, err)
@@ -37,18 +41,22 @@
37 if err != nil {41 if err != nil {
38 return nil, fmt.Errorf("cannot (re)create sqlite level map table: %v", err)42 return nil, fmt.Errorf("cannot (re)create sqlite level map table: %v", err)
39 }43 }
40 return &sqliteLevelMap{db}, nil44 _, err = db.Exec("CREATE TABLE IF NOT EXISTS seen_msgs (id text primary key)")
45 if err != nil {
46 return nil, fmt.Errorf("cannot (re)create sqlite seen msgs table: %v", err)
47 }
48 return &sqliteSeenState{db}, nil
41}49}
4250
43func (pm *sqliteLevelMap) Set(level string, top int64) error {51func (ps *sqliteSeenState) SetLevel(level string, top int64) error {
44 _, err := pm.db.Exec("REPLACE INTO level_map (level, top) VALUES (?, ?)", level, top)52 _, err := ps.db.Exec("REPLACE INTO level_map (level, top) VALUES (?, ?)", level, top)
45 if err != nil {53 if err != nil {
46 return fmt.Errorf("cannot set %#v to %#v in level map: %v", level, top, err)54 return fmt.Errorf("cannot set %#v to %#v in level map: %v", level, top, err)
47 }55 }
48 return nil56 return nil
49}57}
50func (pm *sqliteLevelMap) GetAll() (map[string]int64, error) {58func (ps *sqliteSeenState) GetAllLevels() (map[string]int64, error) {
51 rows, err := pm.db.Query("SELECT * FROM level_map")59 rows, err := ps.db.Query("SELECT * FROM level_map")
52 if err != nil {60 if err != nil {
53 return nil, fmt.Errorf("cannot retrieve levels from sqlite level map: %v", err)61 return nil, fmt.Errorf("cannot retrieve levels from sqlite level map: %v", err)
54 }62 }
@@ -64,3 +72,30 @@
64 }72 }
65 return m, nil73 return m, nil
66}74}
75
76func (ps *sqliteSeenState) dropPrevThan(msgId string) error {
77 _, err := ps.db.Exec("DELETE FROM seen_msgs WHERE rowid < (SELECT rowid FROM seen_msgs WHERE id = ?)", msgId)
78 return err
79}
80
81func (ps *sqliteSeenState) FilterBySeen(notifs []protocol.Notification) ([]protocol.Notification, error) {
82 if len(notifs) == 0 {
83 return nil, nil
84 }
85 acc := make([]protocol.Notification, 0, len(notifs))
86 for _, notif := range notifs {
87 _, err := ps.db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", notif.MsgId)
88 if err != nil {
89 if strings.HasSuffix(err.Error(), "UNIQUE constraint failed: seen_msgs.id") {
90 continue
91 }
92 return nil, fmt.Errorf("cannot insert %#v in seen msgs: %v", notif.MsgId, err)
93 }
94 acc = append(acc, notif)
95 }
96 err := ps.dropPrevThan(notifs[0].MsgId)
97 if err != nil {
98 return nil, fmt.Errorf("cannot delete obsolete seen msgs: %v", err)
99 }
100 return acc, nil
101}
67102
=== renamed file 'client/session/levelmap/sqlevelmap_test.go' => 'client/session/seenstate/sqlseenstate_test.go'
--- client/session/levelmap/sqlevelmap_test.go 2014-02-08 18:08:55 +0000
+++ client/session/seenstate/sqlseenstate_test.go 2014-05-29 12:04:32 +0000
@@ -14,29 +14,32 @@
14 with this program. If not, see <http://www.gnu.org/licenses/>.14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/15*/
1616
17package levelmap17package seenstate
1818
19import (19import (
20 "database/sql"
21
20 _ "code.google.com/p/gosqlite/sqlite3"22 _ "code.google.com/p/gosqlite/sqlite3"
21 "database/sql"
22 . "launchpad.net/gocheck"23 . "launchpad.net/gocheck"
24
25 "launchpad.net/ubuntu-push/protocol"
23)26)
2427
25type sqlmSuite struct{ lmSuite }28type sqlsSuite struct{ ssSuite }
2629
27var _ = Suite(&sqlmSuite{})30var _ = Suite(&sqlsSuite{})
2831
29func (s *sqlmSuite) SetUpSuite(c *C) {32func (s *sqlsSuite) SetUpSuite(c *C) {
30 s.constructor = func() (LevelMap, error) { return NewSqliteLevelMap(":memory:") }33 s.constructor = func() (SeenState, error) { return NewSqliteSeenState(":memory:") }
31}34}
3235
33func (s *sqlmSuite) TestNewCanFail(c *C) {36func (s *sqlsSuite) TestNewCanFail(c *C) {
34 m, err := NewSqliteLevelMap("/does/not/exist")37 sqls, err := NewSqliteSeenState("/does/not/exist")
35 c.Assert(m, IsNil)38 c.Assert(sqls, IsNil)
36 c.Check(err, NotNil)39 c.Check(err, NotNil)
37}40}
3841
39func (s *sqlmSuite) TestSetCanFail(c *C) {42func (s *sqlsSuite) TestSetCanFail(c *C) {
40 dir := c.MkDir()43 dir := c.MkDir()
41 filename := dir + "test.db"44 filename := dir + "test.db"
42 db, err := sql.Open("sqlite3", filename)45 db, err := sql.Open("sqlite3", filename)
@@ -45,14 +48,14 @@
45 _, err = db.Exec("CREATE TABLE level_map (foo)")48 _, err = db.Exec("CREATE TABLE level_map (foo)")
46 c.Assert(err, IsNil)49 c.Assert(err, IsNil)
47 // <evil laughter>50 // <evil laughter>
48 m, err := NewSqliteLevelMap(filename)51 sqls, err := NewSqliteSeenState(filename)
49 c.Check(err, IsNil)52 c.Check(err, IsNil)
50 c.Assert(m, NotNil)53 c.Assert(sqls, NotNil)
51 err = m.Set("foo", 42)54 err = sqls.SetLevel("foo", 42)
52 c.Check(err, ErrorMatches, "cannot set .*")55 c.Check(err, ErrorMatches, "cannot set .*")
53}56}
5457
55func (s *sqlmSuite) TestGetAllCanFail(c *C) {58func (s *sqlsSuite) TestGetAllCanFail(c *C) {
56 dir := c.MkDir()59 dir := c.MkDir()
57 filename := dir + "test.db"60 filename := dir + "test.db"
58 db, err := sql.Open("sqlite3", filename)61 db, err := sql.Open("sqlite3", filename)
@@ -61,15 +64,15 @@
61 _, err = db.Exec("CREATE TABLE level_map AS SELECT 'what'")64 _, err = db.Exec("CREATE TABLE level_map AS SELECT 'what'")
62 c.Assert(err, IsNil)65 c.Assert(err, IsNil)
63 // <evil laughter>66 // <evil laughter>
64 m, err := NewSqliteLevelMap(filename)67 sqls, err := NewSqliteSeenState(filename)
65 c.Check(err, IsNil)68 c.Check(err, IsNil)
66 c.Assert(m, NotNil)69 c.Assert(sqls, NotNil)
67 all, err := m.GetAll()70 all, err := sqls.GetAllLevels()
68 c.Check(all, IsNil)71 c.Check(all, IsNil)
69 c.Check(err, ErrorMatches, "cannot read level .*")72 c.Check(err, ErrorMatches, "cannot read level .*")
70}73}
7174
72func (s *sqlmSuite) TestGetAllCanFailDifferently(c *C) {75func (s *sqlsSuite) TestGetAllCanFailDifferently(c *C) {
73 dir := c.MkDir()76 dir := c.MkDir()
74 filename := dir + "test.db"77 filename := dir + "test.db"
75 db, err := sql.Open("sqlite3", filename)78 db, err := sql.Open("sqlite3", filename)
@@ -83,10 +86,85 @@
83 _, err = db.Exec("DROP TABLE foo")86 _, err = db.Exec("DROP TABLE foo")
84 c.Assert(err, IsNil)87 c.Assert(err, IsNil)
85 // <evil laughter>88 // <evil laughter>
86 m, err := NewSqliteLevelMap(filename)89 sqls, err := NewSqliteSeenState(filename)
87 c.Check(err, IsNil)90 c.Check(err, IsNil)
88 c.Assert(m, NotNil)91 c.Assert(sqls, NotNil)
89 all, err := m.GetAll()92 all, err := sqls.GetAllLevels()
90 c.Check(all, IsNil)93 c.Check(all, IsNil)
91 c.Check(err, ErrorMatches, "cannot retrieve levels .*")94 c.Check(err, ErrorMatches, "cannot retrieve levels .*")
92}95}
96
97func (s *sqlsSuite) TestFilterBySeenCanFail(c *C) {
98 dir := c.MkDir()
99 filename := dir + "test.db"
100 db, err := sql.Open("sqlite3", filename)
101 c.Assert(err, IsNil)
102 // create the wrong kind of table
103 _, err = db.Exec("CREATE TABLE seen_msgs AS SELECT 'what'")
104 c.Assert(err, IsNil)
105 // <evil laughter>
106 sqls, err := NewSqliteSeenState(filename)
107 c.Check(err, IsNil)
108 c.Assert(sqls, NotNil)
109 n1 := protocol.Notification{MsgId: "m1"}
110 res, err := sqls.FilterBySeen([]protocol.Notification{n1})
111 c.Check(res, IsNil)
112 c.Check(err, ErrorMatches, "cannot insert .*")
113}
114
115func (s *sqlsSuite) TestDropPrevThan(c *C) {
116 dir := c.MkDir()
117 filename := dir + "test.db"
118 db, err := sql.Open("sqlite3", filename)
119 c.Assert(err, IsNil)
120 sqls, err := NewSqliteSeenState(filename)
121 c.Check(err, IsNil)
122 c.Assert(sqls, NotNil)
123
124 _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m1")
125 c.Assert(err, IsNil)
126 _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m2")
127 c.Assert(err, IsNil)
128 _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m3")
129 c.Assert(err, IsNil)
130 _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m4")
131 c.Assert(err, IsNil)
132 _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m5")
133 c.Assert(err, IsNil)
134
135 rows, err := db.Query("SELECT COUNT(*) FROM seen_msgs")
136 c.Assert(err, IsNil)
137 rows.Next()
138 var i int
139 err = rows.Scan(&i)
140 c.Assert(err, IsNil)
141 c.Check(i, Equals, 5)
142 rows.Close()
143
144 err = sqls.(*sqliteSeenState).dropPrevThan("m3")
145 c.Assert(err, IsNil)
146
147 rows, err = db.Query("SELECT COUNT(*) FROM seen_msgs")
148 c.Assert(err, IsNil)
149 rows.Next()
150 err = rows.Scan(&i)
151 c.Assert(err, IsNil)
152 c.Check(i, Equals, 3)
153 rows.Close()
154
155 var msgId string
156 rows, err = db.Query("SELECT * FROM seen_msgs")
157 rows.Next()
158 err = rows.Scan(&msgId)
159 c.Assert(err, IsNil)
160 c.Check(msgId, Equals, "m3")
161 rows.Next()
162 err = rows.Scan(&msgId)
163 c.Assert(err, IsNil)
164 c.Check(msgId, Equals, "m4")
165 rows.Next()
166 err = rows.Scan(&msgId)
167 c.Assert(err, IsNil)
168 c.Check(msgId, Equals, "m5")
169 rows.Close()
170}
93171
=== modified file 'client/session/session.go'
--- client/session/session.go 2014-04-18 16:37:31 +0000
+++ client/session/session.go 2014-05-29 12:04:32 +0000
@@ -26,21 +26,24 @@
26 "fmt"26 "fmt"
27 "math/rand"27 "math/rand"
28 "net"28 "net"
29 "os/exec"
29 "strings"30 "strings"
30 "sync"31 "sync"
31 "sync/atomic"32 "sync/atomic"
32 "time"33 "time"
3334
34 "launchpad.net/ubuntu-push/client/gethosts"35 "launchpad.net/ubuntu-push/client/gethosts"
35 "launchpad.net/ubuntu-push/client/session/levelmap"36 "launchpad.net/ubuntu-push/client/session/seenstate"
36 "launchpad.net/ubuntu-push/logger"37 "launchpad.net/ubuntu-push/logger"
37 "launchpad.net/ubuntu-push/protocol"38 "launchpad.net/ubuntu-push/protocol"
38 "launchpad.net/ubuntu-push/util"39 "launchpad.net/ubuntu-push/util"
39)40)
4041
41var wireVersionBytes = []byte{protocol.ProtocolWireVersion}42var (
43 wireVersionBytes = []byte{protocol.ProtocolWireVersion}
44)
4245
43type Notification struct {46type BroadcastNotification struct {
44 TopLevel int6447 TopLevel int64
45 Decoded []map[string]interface{}48 Decoded []map[string]interface{}
46}49}
@@ -84,6 +87,7 @@
84 ExpectAllRepairedTime time.Duration87 ExpectAllRepairedTime time.Duration
85 PEM []byte88 PEM []byte
86 Info map[string]interface{}89 Info map[string]interface{}
90 AuthHelper []string
87}91}
8892
89// ClientSession holds a client<->server session and its configuration.93// ClientSession holds a client<->server session and its configuration.
@@ -91,7 +95,7 @@
91 // configuration95 // configuration
92 DeviceId string96 DeviceId string
93 ClientSessionConfig97 ClientSessionConfig
94 Levels levelmap.LevelMap98 SeenState seenstate.SeenState
95 Protocolator func(net.Conn) protocol.Protocol99 Protocolator func(net.Conn) protocol.Protocol
96 // hosts100 // hosts
97 getHost hostGetter101 getHost hostGetter
@@ -112,9 +116,12 @@
112 pingInterval time.Duration116 pingInterval time.Duration
113 retrier util.AutoRedialer117 retrier util.AutoRedialer
114 // status118 // status
115 stateP *uint32119 stateP *uint32
116 ErrCh chan error120 ErrCh chan error
117 MsgCh chan *Notification121 BroadcastCh chan *BroadcastNotification
122 NotificationsCh chan *protocol.Notification
123 // authorization
124 auth string
118 // autoredial knobs125 // autoredial knobs
119 shouldDelayP *uint32126 shouldDelayP *uint32
120 lastAutoRedial time.Time127 lastAutoRedial time.Time
@@ -138,10 +145,10 @@
138}145}
139146
140func NewSession(serverAddrSpec string, conf ClientSessionConfig,147func NewSession(serverAddrSpec string, conf ClientSessionConfig,
141 deviceId string, levelmapFactory func() (levelmap.LevelMap, error),148 deviceId string, seenStateFactory func() (seenstate.SeenState, error),
142 log logger.Logger) (*ClientSession, error) {149 log logger.Logger) (*ClientSession, error) {
143 state := uint32(Disconnected)150 state := uint32(Disconnected)
144 levels, err := levelmapFactory()151 seenState, err := seenStateFactory()
145 if err != nil {152 if err != nil {
146 return nil, err153 return nil, err
147 }154 }
@@ -159,7 +166,7 @@
159 DeviceId: deviceId,166 DeviceId: deviceId,
160 Log: log,167 Log: log,
161 Protocolator: protocol.NewProtocol0,168 Protocolator: protocol.NewProtocol0,
162 Levels: levels,169 SeenState: seenState,
163 TLS: &tls.Config{},170 TLS: &tls.Config{},
164 stateP: &state,171 stateP: &state,
165 timeSince: time.Since,172 timeSince: time.Since,
@@ -234,6 +241,27 @@
234 return nil241 return nil
235}242}
236243
244// addAuthorization gets the authorization blob to send to the server
245// and adds it to the session.
246func (sess *ClientSession) addAuthorization() error {
247 sess.Log.Debugf("adding authorization")
248 // using a helper, for now at least
249 if len(sess.AuthHelper) == 0 {
250 // do nothing if helper is unset or empty
251 return nil
252 }
253
254 auth, err := exec.Command(sess.AuthHelper[0], sess.AuthHelper[1:]...).Output()
255 if err != nil {
256 // For now we just log the error, as we don't want to block unauthorized users
257 sess.Log.Errorf("unable to get the authorization token from the account: %v", err)
258 } else {
259 sess.auth = strings.TrimSpace(string(auth))
260 }
261
262 return nil
263}
264
237func (sess *ClientSession) resetHosts() {265func (sess *ClientSession) resetHosts() {
238 sess.deliveryHosts = nil266 sess.deliveryHosts = nil
239}267}
@@ -343,7 +371,7 @@
343 return err371 return err
344}372}
345373
346func (sess *ClientSession) decodeBroadcast(bcast *serverMsg) *Notification {374func (sess *ClientSession) decodeBroadcast(bcast *serverMsg) *BroadcastNotification {
347 decoded := make([]map[string]interface{}, 0)375 decoded := make([]map[string]interface{}, 0)
348 for _, p := range bcast.Payloads {376 for _, p := range bcast.Payloads {
349 var v map[string]interface{}377 var v map[string]interface{}
@@ -354,7 +382,7 @@
354 }382 }
355 decoded = append(decoded, v)383 decoded = append(decoded, v)
356 }384 }
357 return &Notification{385 return &BroadcastNotification{
358 TopLevel: bcast.TopLevel,386 TopLevel: bcast.TopLevel,
359 Decoded: decoded,387 Decoded: decoded,
360 }388 }
@@ -362,7 +390,7 @@
362390
363// handle "broadcast" messages391// handle "broadcast" messages
364func (sess *ClientSession) handleBroadcast(bcast *serverMsg) error {392func (sess *ClientSession) handleBroadcast(bcast *serverMsg) error {
365 err := sess.Levels.Set(bcast.ChanId, bcast.TopLevel)393 err := sess.SeenState.SetLevel(bcast.ChanId, bcast.TopLevel)
366 if err != nil {394 if err != nil {
367 sess.setState(Error)395 sess.setState(Error)
368 sess.Log.Errorf("unable to set level: %v", err)396 sess.Log.Errorf("unable to set level: %v", err)
@@ -382,15 +410,44 @@
382 bcast.ChanId, bcast.AppId, bcast.TopLevel, bcast.Payloads)410 bcast.ChanId, bcast.AppId, bcast.TopLevel, bcast.Payloads)
383 if bcast.ChanId == protocol.SystemChannelId {411 if bcast.ChanId == protocol.SystemChannelId {
384 // the system channel id, the only one we care about for now412 // the system channel id, the only one we care about for now
385 sess.Log.Debugf("sending it over")413 sess.Log.Debugf("sending bcast over")
386 sess.MsgCh <- sess.decodeBroadcast(bcast)414 sess.BroadcastCh <- sess.decodeBroadcast(bcast)
387 sess.Log.Debugf("sent it over")415 sess.Log.Debugf("sent bcast over")
388 } else {416 } else {
389 sess.Log.Debugf("what is this weird channel, %#v?", bcast.ChanId)417 sess.Log.Debugf("what is this weird channel, %#v?", bcast.ChanId)
390 }418 }
391 return nil419 return nil
392}420}
393421
422// handle "notifications" messages
423func (sess *ClientSession) handleNotifications(ucast *serverMsg) error {
424 notifs, err := sess.SeenState.FilterBySeen(ucast.Notifications)
425 if err != nil {
426 sess.setState(Error)
427 sess.Log.Errorf("unable to record msgs seen: %v", err)
428 sess.proto.WriteMessage(protocol.AckMsg{"nak"})
429 return err
430 }
431 // the server assumes if we ack the broadcast, we've updated
432 // our state. Hence the order.
433 err = sess.proto.WriteMessage(protocol.AckMsg{"ack"})
434 if err != nil {
435 sess.setState(Error)
436 sess.Log.Errorf("unable to ack notifications: %s", err)
437 return err
438 }
439 sess.clearShouldDelay()
440 for i := range notifs {
441 notif := &notifs[i]
442 sess.Log.Debugf("unicast app:%v msg:%s payload:%s",
443 notif.AppId, notif.MsgId, notif.Payload)
444 sess.Log.Debugf("sending ucast over")
445 sess.NotificationsCh <- notif
446 sess.Log.Debugf("sent ucast over")
447 }
448 return nil
449}
450
394// handle "connbroken" messages451// handle "connbroken" messages
395func (sess *ClientSession) handleConnBroken(connBroken *serverMsg) error {452func (sess *ClientSession) handleConnBroken(connBroken *serverMsg) error {
396 sess.setState(Error)453 sess.setState(Error)
@@ -422,8 +479,15 @@
422 err = sess.handlePing()479 err = sess.handlePing()
423 case "broadcast":480 case "broadcast":
424 err = sess.handleBroadcast(&recv)481 err = sess.handleBroadcast(&recv)
482 case "notifications":
483 err = sess.handleNotifications(&recv)
425 case "connbroken":484 case "connbroken":
426 err = sess.handleConnBroken(&recv)485 err = sess.handleConnBroken(&recv)
486 case "warn":
487 // XXX: current message "warn" should be "connwarn"
488 fallthrough
489 case "connwarn":
490 sess.Log.Errorf("server sent warning: %s", recv.Reason)
427 }491 }
428 if err != nil {492 if err != nil {
429 return err493 return err
@@ -450,17 +514,16 @@
450 }514 }
451 proto := sess.Protocolator(conn)515 proto := sess.Protocolator(conn)
452 proto.SetDeadline(time.Now().Add(sess.ExchangeTimeout))516 proto.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
453 levels, err := sess.Levels.GetAll()517 levels, err := sess.SeenState.GetAllLevels()
454 if err != nil {518 if err != nil {
455 sess.setState(Error)519 sess.setState(Error)
456 sess.Log.Errorf("unable to start: get levels: %v", err)520 sess.Log.Errorf("unable to start: get levels: %v", err)
457 return err521 return err
458 }522 }
459 err = proto.WriteMessage(protocol.ConnectMsg{523 err = proto.WriteMessage(protocol.ConnectMsg{
460 Type: "connect",524 Type: "connect",
461 DeviceId: sess.DeviceId,525 DeviceId: sess.DeviceId,
462 // xxx get the SSO Authorization string from the phone526 Authorization: sess.auth,
463 Authorization: "",
464 Levels: levels,527 Levels: levels,
465 Info: sess.Info,528 Info: sess.Info,
466 })529 })
@@ -495,18 +558,21 @@
495558
496// run calls connect, and if it works it calls start, and if it works559// run calls connect, and if it works it calls start, and if it works
497// it runs loop in a goroutine, and ships its return value over ErrCh.560// it runs loop in a goroutine, and ships its return value over ErrCh.
498func (sess *ClientSession) run(closer func(), hostGetter, connecter, starter, looper func() error) error {561func (sess *ClientSession) run(closer func(), authChecker, hostGetter, connecter, starter, looper func() error) error {
499 closer()562 closer()
500 err := hostGetter()563 if err := authChecker(); err != nil {
501 if err != nil {564 return err
502 return err565 }
503 }566 if err := hostGetter(); err != nil {
504 err = connecter()567 return err
568 }
569 err := connecter()
505 if err == nil {570 if err == nil {
506 err = starter()571 err = starter()
507 if err == nil {572 if err == nil {
508 sess.ErrCh = make(chan error, 1)573 sess.ErrCh = make(chan error, 1)
509 sess.MsgCh = make(chan *Notification)574 sess.BroadcastCh = make(chan *BroadcastNotification)
575 sess.NotificationsCh = make(chan *protocol.Notification)
510 go func() { sess.ErrCh <- looper() }()576 go func() { sess.ErrCh <- looper() }()
511 }577 }
512 }578 }
@@ -531,7 +597,7 @@
531 // keep on trying.597 // keep on trying.
532 panic("can't Dial() without a protocol constructor.")598 panic("can't Dial() without a protocol constructor.")
533 }599 }
534 return sess.run(sess.doClose, sess.getHosts, sess.connect, sess.start, sess.loop)600 return sess.run(sess.doClose, sess.addAuthorization, sess.getHosts, sess.connect, sess.start, sess.loop)
535}601}
536602
537func init() {603func init() {
538604
=== modified file 'client/session/session_test.go'
--- client/session/session_test.go 2014-04-18 16:37:31 +0000
+++ client/session/session_test.go 2014-05-29 12:04:32 +0000
@@ -33,8 +33,7 @@
33 . "launchpad.net/gocheck"33 . "launchpad.net/gocheck"
3434
35 "launchpad.net/ubuntu-push/client/gethosts"35 "launchpad.net/ubuntu-push/client/gethosts"
36 "launchpad.net/ubuntu-push/client/session/levelmap"36 "launchpad.net/ubuntu-push/client/session/seenstate"
37 "launchpad.net/ubuntu-push/logger"
38 "launchpad.net/ubuntu-push/protocol"37 "launchpad.net/ubuntu-push/protocol"
39 helpers "launchpad.net/ubuntu-push/testing"38 helpers "launchpad.net/ubuntu-push/testing"
40 "launchpad.net/ubuntu-push/testing/condition"39 "launchpad.net/ubuntu-push/testing/condition"
@@ -157,17 +156,20 @@
157 return nil156 return nil
158}157}
159158
160// brokenLevelMap is a LevelMap that always breaks159// brokenSeenState is a SeenState that always breaks
161type brokenLevelMap struct{}160type brokenSeenState struct{}
162161
163func (*brokenLevelMap) Set(string, int64) error { return errors.New("broken.") }162func (*brokenSeenState) SetLevel(string, int64) error { return errors.New("broken.") }
164func (*brokenLevelMap) GetAll() (map[string]int64, error) { return nil, errors.New("broken.") }163func (*brokenSeenState) GetAllLevels() (map[string]int64, error) { return nil, errors.New("broken.") }
164func (*brokenSeenState) FilterBySeen([]protocol.Notification) ([]protocol.Notification, error) {
165 return nil, errors.New("broken.")
166}
165167
166/////168/////
167169
168type clientSessionSuite struct {170type clientSessionSuite struct {
169 log logger.Logger171 log *helpers.TestLogger
170 lvls func() (levelmap.LevelMap, error)172 lvls func() (seenstate.SeenState, error)
171}173}
172174
173func (cs *clientSessionSuite) SetUpTest(c *C) {175func (cs *clientSessionSuite) SetUpTest(c *C) {
@@ -175,7 +177,7 @@
175}177}
176178
177// in-memory level map testing179// in-memory level map testing
178var _ = Suite(&clientSessionSuite{lvls: levelmap.NewLevelMap})180var _ = Suite(&clientSessionSuite{lvls: seenstate.NewSeenState})
179181
180// sqlite level map testing182// sqlite level map testing
181type clientSqlevelsSessionSuite struct{ clientSessionSuite }183type clientSqlevelsSessionSuite struct{ clientSessionSuite }
@@ -183,7 +185,7 @@
183var _ = Suite(&clientSqlevelsSessionSuite{})185var _ = Suite(&clientSqlevelsSessionSuite{})
184186
185func (cs *clientSqlevelsSessionSuite) SetUpSuite(c *C) {187func (cs *clientSqlevelsSessionSuite) SetUpSuite(c *C) {
186 cs.lvls = func() (levelmap.LevelMap, error) { return levelmap.NewSqliteLevelMap(":memory:") }188 cs.lvls = func() (seenstate.SeenState, error) { return seenstate.NewSqliteSeenState(":memory:") }
187}189}
188190
189/****************************************************************191/****************************************************************
@@ -249,8 +251,8 @@
249 c.Check(err, NotNil)251 c.Check(err, NotNil)
250}252}
251253
252func (cs *clientSessionSuite) TestNewSessionBadLevelMapFails(c *C) {254func (cs *clientSessionSuite) TestNewSessionBadSeenStateFails(c *C) {
253 ferr := func() (levelmap.LevelMap, error) { return nil, errors.New("Busted.") }255 ferr := func() (seenstate.SeenState, error) { return nil, errors.New("Busted.") }
254 sess, err := NewSession("", dummyConf, "wah", ferr, cs.log)256 sess, err := NewSession("", dummyConf, "wah", ferr, cs.log)
255 c.Check(sess, IsNil)257 c.Check(sess, IsNil)
256 c.Assert(err, NotNil)258 c.Assert(err, NotNil)
@@ -347,6 +349,43 @@
347}349}
348350
349/****************************************************************351/****************************************************************
352 addAuthorization() tests
353****************************************************************/
354
355func (cs *clientSessionSuite) TestAddAuthorizationAddsAuthorization(c *C) {
356 sess := &ClientSession{Log: cs.log}
357 sess.AuthHelper = []string{"echo", "some auth"}
358 c.Assert(sess.auth, Equals, "")
359 err := sess.addAuthorization()
360 c.Assert(err, IsNil)
361 c.Check(sess.auth, Equals, "some auth")
362}
363
364func (cs *clientSessionSuite) TestAddAuthorizationIgnoresErrors(c *C) {
365 sess := &ClientSession{Log: cs.log}
366 sess.AuthHelper = []string{"sh", "-c", "echo hello; false"}
367
368 c.Assert(sess.auth, Equals, "")
369 err := sess.addAuthorization()
370 c.Assert(err, IsNil)
371 c.Check(sess.auth, Equals, "")
372}
373
374func (cs *clientSessionSuite) TestAddAuthorizationSkipsIfUnsetOrNil(c *C) {
375 sess := &ClientSession{Log: cs.log}
376 sess.AuthHelper = nil
377 c.Assert(sess.auth, Equals, "")
378 err := sess.addAuthorization()
379 c.Assert(err, IsNil)
380 c.Check(sess.auth, Equals, "")
381
382 sess.AuthHelper = []string{}
383 err = sess.addAuthorization()
384 c.Assert(err, IsNil)
385 c.Check(sess.auth, Equals, "")
386}
387
388/****************************************************************
350 startConnectionAttempt()/nextHostToTry()/started tests389 startConnectionAttempt()/nextHostToTry()/started tests
351****************************************************************/390****************************************************************/
352391
@@ -601,7 +640,7 @@
601 conf := ClientSessionConfig{640 conf := ClientSessionConfig{
602 ExchangeTimeout: time.Millisecond,641 ExchangeTimeout: time.Millisecond,
603 }642 }
604 s.sess, err = NewSession("", conf, "wah", levelmap.NewLevelMap, helpers.NewTestLogger(c, "debug"))643 s.sess, err = NewSession("", conf, "wah", seenstate.NewSeenState, helpers.NewTestLogger(c, "debug"))
605 c.Assert(err, IsNil)644 c.Assert(err, IsNil)
606 s.sess.Connection = &testConn{Name: "TestHandle*"}645 s.sess.Connection = &testConn{Name: "TestHandle*"}
607 s.errCh = make(chan error, 1)646 s.errCh = make(chan error, 1)
@@ -609,7 +648,8 @@
609 s.downCh = make(chan interface{}, 5)648 s.downCh = make(chan interface{}, 5)
610 s.sess.proto = &testProtocol{up: s.upCh, down: s.downCh}649 s.sess.proto = &testProtocol{up: s.upCh, down: s.downCh}
611 // make the message channel buffered650 // make the message channel buffered
612 s.sess.MsgCh = make(chan *Notification, 5)651 s.sess.BroadcastCh = make(chan *BroadcastNotification, 5)
652 s.sess.NotificationsCh = make(chan *protocol.Notification, 5)
613}653}
614654
615func (s *msgSuite) TestHandlePingWorks(c *C) {655func (s *msgSuite) TestHandlePingWorks(c *C) {
@@ -668,8 +708,8 @@
668 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})708 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
669 s.upCh <- nil // ack ok709 s.upCh <- nil // ack ok
670 c.Check(<-s.errCh, Equals, nil)710 c.Check(<-s.errCh, Equals, nil)
671 c.Assert(len(s.sess.MsgCh), Equals, 1)711 c.Assert(len(s.sess.BroadcastCh), Equals, 1)
672 c.Check(<-s.sess.MsgCh, DeepEquals, &Notification{712 c.Check(<-s.sess.BroadcastCh, DeepEquals, &BroadcastNotification{
673 TopLevel: 2,713 TopLevel: 2,
674 Decoded: []map[string]interface{}{714 Decoded: []map[string]interface{}{
675 map[string]interface{}{715 map[string]interface{}{
@@ -681,7 +721,7 @@
681 },721 },
682 })722 })
683 // and finally, the session keeps track of the levels723 // and finally, the session keeps track of the levels
684 levels, err := s.sess.Levels.GetAll()724 levels, err := s.sess.SeenState.GetAllLevels()
685 c.Check(err, IsNil)725 c.Check(err, IsNil)
686 c.Check(levels, DeepEquals, map[string]int64{"0": 2})726 c.Check(levels, DeepEquals, map[string]int64{"0": 2})
687}727}
@@ -716,11 +756,11 @@
716 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})756 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
717 s.upCh <- nil // ack ok757 s.upCh <- nil // ack ok
718 c.Check(<-s.errCh, IsNil)758 c.Check(<-s.errCh, IsNil)
719 c.Check(len(s.sess.MsgCh), Equals, 0)759 c.Check(len(s.sess.BroadcastCh), Equals, 0)
720}760}
721761
722func (s *msgSuite) TestHandleBroadcastWrongBrokenLevelmap(c *C) {762func (s *msgSuite) TestHandleBroadcastBrokenSeenState(c *C) {
723 s.sess.Levels = &brokenLevelMap{}763 s.sess.SeenState = &brokenSeenState{}
724 msg := serverMsg{"broadcast",764 msg := serverMsg{"broadcast",
725 protocol.BroadcastMsg{765 protocol.BroadcastMsg{
726 Type: "broadcast",766 Type: "broadcast",
@@ -733,8 +773,9 @@
733 s.upCh <- nil // ack ok773 s.upCh <- nil // ack ok
734 // start returns with error774 // start returns with error
735 c.Check(<-s.errCh, Not(Equals), nil)775 c.Check(<-s.errCh, Not(Equals), nil)
776 c.Check(s.sess.State(), Equals, Error)
736 // no message sent out777 // no message sent out
737 c.Check(len(s.sess.MsgCh), Equals, 0)778 c.Check(len(s.sess.BroadcastCh), Equals, 0)
738 // and nak'ed it779 // and nak'ed it
739 c.Check(len(s.downCh), Equals, 1)780 c.Check(len(s.downCh), Equals, 1)
740 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"})781 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"})
@@ -767,6 +808,118 @@
767}808}
768809
769/****************************************************************810/****************************************************************
811 handleNotifications() tests
812****************************************************************/
813
814func (s *msgSuite) TestHandleNotificationsWorks(c *C) {
815 s.sess.setShouldDelay()
816 n1 := protocol.Notification{
817 AppId: "app1",
818 MsgId: "a",
819 Payload: json.RawMessage(`{"m": 1}`),
820 }
821 n2 := protocol.Notification{
822 AppId: "app2",
823 MsgId: "b",
824 Payload: json.RawMessage(`{"m": 2}`),
825 }
826 msg := serverMsg{"notifications",
827 protocol.BroadcastMsg{},
828 protocol.NotificationsMsg{
829 Notifications: []protocol.Notification{n1, n2},
830 }, protocol.ConnBrokenMsg{}}
831 go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
832 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
833 s.upCh <- nil // ack ok
834 c.Check(<-s.errCh, Equals, nil)
835 c.Check(s.sess.ShouldDelay(), Equals, false)
836 c.Assert(len(s.sess.NotificationsCh), Equals, 2)
837 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n1)
838 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n2)
839}
840
841func (s *msgSuite) TestHandleNotificationsFiltersSeen(c *C) {
842 n1 := protocol.Notification{
843 AppId: "app1",
844 MsgId: "a",
845 Payload: json.RawMessage(`{"m": 1}`),
846 }
847 n2 := protocol.Notification{
848 AppId: "app2",
849 MsgId: "b",
850 Payload: json.RawMessage(`{"m": 2}`),
851 }
852 msg := serverMsg{"notifications",
853 protocol.BroadcastMsg{},
854 protocol.NotificationsMsg{
855 Notifications: []protocol.Notification{n1, n2},
856 }, protocol.ConnBrokenMsg{}}
857 go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
858 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
859 s.upCh <- nil // ack ok
860 c.Check(<-s.errCh, Equals, nil)
861 c.Assert(len(s.sess.NotificationsCh), Equals, 2)
862 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n1)
863 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n2)
864
865 // second time they get ignored
866 go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
867 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
868 s.upCh <- nil // ack ok
869 c.Check(<-s.errCh, Equals, nil)
870 c.Assert(len(s.sess.NotificationsCh), Equals, 0)
871}
872
873func (s *msgSuite) TestHandleNotificationsBadAckWrite(c *C) {
874 s.sess.setShouldDelay()
875 n1 := protocol.Notification{
876 AppId: "app1",
877 MsgId: "a",
878 Payload: json.RawMessage(`{"m": 1}`),
879 }
880 msg := serverMsg{"notifications",
881 protocol.BroadcastMsg{},
882 protocol.NotificationsMsg{
883 Notifications: []protocol.Notification{n1},
884 }, protocol.ConnBrokenMsg{}}
885 go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
886 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
887 failure := errors.New("ACK ACK ACK")
888 s.upCh <- failure
889 c.Assert(<-s.errCh, Equals, failure)
890 c.Check(s.sess.State(), Equals, Error)
891 // didn't get to clear
892 c.Check(s.sess.ShouldDelay(), Equals, true)
893}
894
895func (s *msgSuite) TestHandleNotificationsBrokenSeenState(c *C) {
896 s.sess.setShouldDelay()
897 s.sess.SeenState = &brokenSeenState{}
898 n1 := protocol.Notification{
899 AppId: "app1",
900 MsgId: "a",
901 Payload: json.RawMessage(`{"m": 1}`),
902 }
903 msg := serverMsg{"notifications",
904 protocol.BroadcastMsg{},
905 protocol.NotificationsMsg{
906 Notifications: []protocol.Notification{n1},
907 }, protocol.ConnBrokenMsg{}}
908 go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
909 s.upCh <- nil // ack ok
910 // start returns with error
911 c.Check(<-s.errCh, Not(Equals), nil)
912 c.Check(s.sess.State(), Equals, Error)
913 // no message sent out
914 c.Check(len(s.sess.NotificationsCh), Equals, 0)
915 // and nak'ed it
916 c.Check(len(s.downCh), Equals, 1)
917 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"})
918 // didn't get to clear
919 c.Check(s.sess.ShouldDelay(), Equals, true)
920}
921
922/****************************************************************
770 handleConnBroken() tests923 handleConnBroken() tests
771****************************************************************/924****************************************************************/
772925
@@ -861,6 +1014,26 @@
861 c.Check(<-s.errCh, Equals, failure)1014 c.Check(<-s.errCh, Equals, failure)
862}1015}
8631016
1017func (s *loopSuite) TestLoopNotifications(c *C) {
1018 c.Check(s.sess.State(), Equals, Running)
1019
1020 n1 := protocol.Notification{
1021 AppId: "app1",
1022 MsgId: "a",
1023 Payload: json.RawMessage(`{"m": 1}`),
1024 }
1025 msg := &protocol.NotificationsMsg{
1026 Type: "notifications",
1027 Notifications: []protocol.Notification{n1},
1028 }
1029 c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
1030 s.upCh <- msg
1031 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
1032 failure := errors.New("ack")
1033 s.upCh <- failure
1034 c.Check(<-s.errCh, Equals, failure)
1035}
1036
864func (s *loopSuite) TestLoopConnBroken(c *C) {1037func (s *loopSuite) TestLoopConnBroken(c *C) {
865 c.Check(s.sess.State(), Equals, Running)1038 c.Check(s.sess.State(), Equals, Running)
866 broken := protocol.ConnBrokenMsg{1039 broken := protocol.ConnBrokenMsg{
@@ -872,6 +1045,31 @@
872 c.Check(<-s.errCh, NotNil)1045 c.Check(<-s.errCh, NotNil)
873}1046}
8741047
1048func (s *loopSuite) TestLoopConnWarn(c *C) {
1049 warn := protocol.ConnWarnMsg{
1050 Type: "warn",
1051 Reason: "XXX",
1052 }
1053 connwarn := protocol.ConnWarnMsg{
1054 Type: "connwarn",
1055 Reason: "REASON",
1056 }
1057 failure := errors.New("warn")
1058 log := s.sess.Log.(*helpers.TestLogger)
1059
1060 c.Check(s.sess.State(), Equals, Running)
1061 c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
1062 log.ResetCapture()
1063 s.upCh <- warn
1064 s.upCh <- connwarn
1065 s.upCh <- failure
1066 c.Check(<-s.errCh, Equals, failure)
1067 c.Check(log.Captured(),
1068 Matches, `(?ms).* warning: XXX$.*`)
1069 c.Check(log.Captured(),
1070 Matches, `(?ms).* warning: REASON$`)
1071}
1072
875/****************************************************************1073/****************************************************************
876 start() tests1074 start() tests
877****************************************************************/1075****************************************************************/
@@ -898,7 +1096,7 @@
898func (cs *clientSessionSuite) TestStartFailsIfGetLevelsFails(c *C) {1096func (cs *clientSessionSuite) TestStartFailsIfGetLevelsFails(c *C) {
899 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)1097 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
900 c.Assert(err, IsNil)1098 c.Assert(err, IsNil)
901 sess.Levels = &brokenLevelMap{}1099 sess.SeenState = &brokenSeenState{}
902 sess.Connection = &testConn{Name: "TestStartConnectMessageFails"}1100 sess.Connection = &testConn{Name: "TestStartConnectMessageFails"}
903 errCh := make(chan error, 1)1101 errCh := make(chan error, 1)
904 upCh := make(chan interface{}, 5)1102 upCh := make(chan interface{}, 5)
@@ -931,9 +1129,10 @@
9311129
932 c.Check(takeNext(downCh), Equals, "deadline 0")1130 c.Check(takeNext(downCh), Equals, "deadline 0")
933 c.Check(takeNext(downCh), DeepEquals, protocol.ConnectMsg{1131 c.Check(takeNext(downCh), DeepEquals, protocol.ConnectMsg{
934 Type: "connect",1132 Type: "connect",
935 DeviceId: sess.DeviceId,1133 DeviceId: sess.DeviceId,
936 Levels: map[string]int64{},1134 Levels: map[string]int64{},
1135 Authorization: "",
937 })1136 })
938 upCh <- errors.New("Overflow error in /dev/null")1137 upCh <- errors.New("Overflow error in /dev/null")
939 err = <-errCh1138 err = <-errCh
@@ -1038,6 +1237,7 @@
1038 msg, ok := takeNext(downCh).(protocol.ConnectMsg)1237 msg, ok := takeNext(downCh).(protocol.ConnectMsg)
1039 c.Check(ok, Equals, true)1238 c.Check(ok, Equals, true)
1040 c.Check(msg.DeviceId, Equals, "wah")1239 c.Check(msg.DeviceId, Equals, "wah")
1240 c.Check(msg.Authorization, Equals, "")
1041 c.Check(msg.Info, DeepEquals, info)1241 c.Check(msg.Info, DeepEquals, info)
1042 upCh <- nil // no error1242 upCh <- nil // no error
1043 upCh <- protocol.ConnAckMsg{1243 upCh <- protocol.ConnAckMsg{
@@ -1054,6 +1254,22 @@
1054 run() tests1254 run() tests
1055****************************************************************/1255****************************************************************/
10561256
1257func (cs *clientSessionSuite) TestRunBailsIfAuthCheckFails(c *C) {
1258 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
1259 c.Assert(err, IsNil)
1260 failure := errors.New("TestRunBailsIfAuthCheckFails")
1261 has_closed := false
1262 err = sess.run(
1263 func() { has_closed = true },
1264 func() error { return failure },
1265 nil,
1266 nil,
1267 nil,
1268 nil)
1269 c.Check(err, Equals, failure)
1270 c.Check(has_closed, Equals, true)
1271}
1272
1057func (cs *clientSessionSuite) TestRunBailsIfHostGetterFails(c *C) {1273func (cs *clientSessionSuite) TestRunBailsIfHostGetterFails(c *C) {
1058 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)1274 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
1059 c.Assert(err, IsNil)1275 c.Assert(err, IsNil)
@@ -1061,6 +1277,7 @@
1061 has_closed := false1277 has_closed := false
1062 err = sess.run(1278 err = sess.run(
1063 func() { has_closed = true },1279 func() { has_closed = true },
1280 func() error { return nil },
1064 func() error { return failure },1281 func() error { return failure },
1065 nil,1282 nil,
1066 nil,1283 nil,
@@ -1076,6 +1293,7 @@
1076 err = sess.run(1293 err = sess.run(
1077 func() {},1294 func() {},
1078 func() error { return nil },1295 func() error { return nil },
1296 func() error { return nil },
1079 func() error { return failure },1297 func() error { return failure },
1080 nil,1298 nil,
1081 nil)1299 nil)
@@ -1090,6 +1308,7 @@
1090 func() {},1308 func() {},
1091 func() error { return nil },1309 func() error { return nil },
1092 func() error { return nil },1310 func() error { return nil },
1311 func() error { return nil },
1093 func() error { return failure },1312 func() error { return failure },
1094 nil)1313 nil)
1095 c.Check(err, Equals, failure)1314 c.Check(err, Equals, failure)
@@ -1098,23 +1317,24 @@
1098func (cs *clientSessionSuite) TestRunRunsEvenIfLoopFails(c *C) {1317func (cs *clientSessionSuite) TestRunRunsEvenIfLoopFails(c *C) {
1099 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)1318 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
1100 c.Assert(err, IsNil)1319 c.Assert(err, IsNil)
1101 // just to make a point: until here we haven't set ErrCh & MsgCh (no1320 // just to make a point: until here we haven't set ErrCh & BroadcastCh (no
1102 // biggie if this stops being true)1321 // biggie if this stops being true)
1103 c.Check(sess.ErrCh, IsNil)1322 c.Check(sess.ErrCh, IsNil)
1104 c.Check(sess.MsgCh, IsNil)1323 c.Check(sess.BroadcastCh, IsNil)
1105 failureCh := make(chan error) // must be unbuffered1324 failureCh := make(chan error) // must be unbuffered
1106 notf := &Notification{}1325 notf := &BroadcastNotification{}
1107 err = sess.run(1326 err = sess.run(
1108 func() {},1327 func() {},
1109 func() error { return nil },1328 func() error { return nil },
1110 func() error { return nil },1329 func() error { return nil },
1111 func() error { return nil },1330 func() error { return nil },
1112 func() error { sess.MsgCh <- notf; return <-failureCh })1331 func() error { return nil },
1332 func() error { sess.BroadcastCh <- notf; return <-failureCh })
1113 c.Check(err, Equals, nil)1333 c.Check(err, Equals, nil)
1114 // if run doesn't error it sets up the channels1334 // if run doesn't error it sets up the channels
1115 c.Assert(sess.ErrCh, NotNil)1335 c.Assert(sess.ErrCh, NotNil)
1116 c.Assert(sess.MsgCh, NotNil)1336 c.Assert(sess.BroadcastCh, NotNil)
1117 c.Check(<-sess.MsgCh, Equals, notf)1337 c.Check(<-sess.BroadcastCh, Equals, notf)
1118 failure := errors.New("TestRunRunsEvenIfLoopFails")1338 failure := errors.New("TestRunRunsEvenIfLoopFails")
1119 failureCh <- failure1339 failureCh <- failure
1120 c.Check(<-sess.ErrCh, Equals, failure)1340 c.Check(<-sess.ErrCh, Equals, failure)
@@ -1317,9 +1537,9 @@
1317 c.Check(takeNext(downCh), Equals, protocol.AckMsg{"ack"})1537 c.Check(takeNext(downCh), Equals, protocol.AckMsg{"ack"})
1318 upCh <- nil1538 upCh <- nil
1319 // ...get bubbled up,1539 // ...get bubbled up,
1320 c.Check(<-sess.MsgCh, NotNil)1540 c.Check(<-sess.BroadcastCh, NotNil)
1321 // and their TopLevel remembered1541 // and their TopLevel remembered
1322 levels, err := sess.Levels.GetAll()1542 levels, err := sess.SeenState.GetAllLevels()
1323 c.Check(err, IsNil)1543 c.Check(err, IsNil)
1324 c.Check(levels, DeepEquals, map[string]int64{"0": 2})1544 c.Check(levels, DeepEquals, map[string]int64{"0": 2})
13251545
13261546
=== modified file 'config/config.go'
--- config/config.go 2014-04-12 08:44:39 +0000
+++ config/config.go 2014-05-29 12:04:32 +0000
@@ -268,14 +268,16 @@
268268
269// used to implement -cfg@=269// used to implement -cfg@=
270type readConfigAtVal struct {270type readConfigAtVal struct {
271 path string
271 accu map[string]json.RawMessage272 accu map[string]json.RawMessage
272}273}
273274
274func (v *readConfigAtVal) String() string {275func (v *readConfigAtVal) String() string {
275 return "<config.json>"276 return v.path
276}277}
277278
278func (v *readConfigAtVal) Set(path string) error {279func (v *readConfigAtVal) Set(path string) error {
280 v.path = path
279 return readOneConfig(v.accu, path)281 return readOneConfig(v.accu, path)
280}282}
281283
@@ -292,7 +294,7 @@
292 help := destField.fld.Tag.Get("help")294 help := destField.fld.Tag.Get("help")
293 flag.Var(&val{destField, accu}, destField.configName(), help)295 flag.Var(&val{destField, accu}, destField.configName(), help)
294 }296 }
295 flag.Var(&readConfigAtVal{accu}, "cfg@", "get config values from file")297 flag.Var(&readConfigAtVal{"<config.json>", accu}, "cfg@", "get config values from file")
296 flag.Parse()298 flag.Parse()
297 return nil299 return nil
298}300}
@@ -301,17 +303,25 @@
301// command line was already parsed.303// command line was already parsed.
302var IgnoreParsedFlags = false304var IgnoreParsedFlags = false
303305
304// ReadFiles reads configuration from a set of files. The string306// ReadFilesDefaults reads configuration from a set of files. The
305// "<flags>" can be used as a pseudo file-path, it will consider307// string "<flags>" can be used as a pseudo file-path, it will
306// command line flags, invoking flag.Parse(). Among those the flag308// consider command line flags, invoking flag.Parse(). Among those the
307// -cfg@=FILE can be used to get further config values from FILE.309// flag -cfg@=FILE can be used to get further config values from FILE.
308func ReadFiles(destConfig interface{}, cfgFpaths ...string) error {310// Defaults for fields can be given through a map[string]interface{}.
311func ReadFilesDefaults(destConfig interface{}, defls map[string]interface{}, cfgFpaths ...string) error {
309 destValue, err := checkDestConfig("destConfig", destConfig)312 destValue, err := checkDestConfig("destConfig", destConfig)
310 if err != nil {313 if err != nil {
311 return err314 return err
312 }315 }
313 // do the parsing in two phases for better error handling316 // do the parsing in two phases for better error handling
314 p1 := make(map[string]json.RawMessage)317 p1 := make(map[string]json.RawMessage)
318 for field, value := range defls {
319 b, err := json.Marshal(value)
320 if err != nil {
321 return err
322 }
323 p1[field] = json.RawMessage(b)
324 }
315 readOne := false325 readOne := false
316 for _, cfgPath := range cfgFpaths {326 for _, cfgPath := range cfgFpaths {
317 if cfgPath == "<flags>" {327 if cfgPath == "<flags>" {
@@ -336,6 +346,13 @@
336 return fillDestConfig(destValue, p1)346 return fillDestConfig(destValue, p1)
337}347}
338348
349// ReadFiles reads configuration from a set of files exactly like
350// ReadFilesDefaults but no defaults can be given making all fields
351// mandatory.
352func ReadFiles(destConfig interface{}, cfgFpaths ...string) error {
353 return ReadFilesDefaults(destConfig, nil, cfgFpaths...)
354}
355
339// CompareConfigs compares the two given configuration structures. It returns a list of differing fields or nil if the config contents are the same.356// CompareConfigs compares the two given configuration structures. It returns a list of differing fields or nil if the config contents are the same.
340func CompareConfig(config1, config2 interface{}) ([]string, error) {357func CompareConfig(config1, config2 interface{}) ([]string, error) {
341 v1, err := checkDestConfig("config1", config1)358 v1, err := checkDestConfig("config1", config1)
342359
=== modified file 'config/config_test.go'
--- config/config_test.go 2014-04-12 08:43:32 +0000
+++ config/config_test.go 2014-05-29 12:04:32 +0000
@@ -173,6 +173,40 @@
173 c.Check(err, NotNil)173 c.Check(err, NotNil)
174}174}
175175
176type testConfig2 struct {
177 A int
178 B string
179 C []string `json:"c_list"`
180 D ConfigTimeDuration
181}
182
183func (s *configSuite) TestReadFilesDefaults(c *C) {
184 var cfg testConfig2
185 tmpDir := c.MkDir()
186 emptyCfgPath := filepath.Join(tmpDir, "e.json")
187 err := ioutil.WriteFile(emptyCfgPath, []byte("{}"), os.ModePerm)
188 c.Assert(err, IsNil)
189 err = ReadFilesDefaults(&cfg, map[string]interface{}{
190 "a": 42,
191 "b": "foo",
192 "c_list": []string{"bar", "baz"},
193 "d": "3s",
194 }, emptyCfgPath)
195 c.Check(err, IsNil)
196 c.Check(cfg.A, Equals, 42)
197 c.Check(cfg.B, Equals, "foo")
198 c.Check(cfg.C, DeepEquals, []string{"bar", "baz"})
199 c.Check(cfg.D.TimeDuration(), Equals, 3*time.Second)
200}
201
202func (s *configSuite) TestReadFilesDefaultsError(c *C) {
203 var cfg testConfig2
204 err := ReadFilesDefaults(&cfg, map[string]interface{}{
205 "a": make(chan int),
206 })
207 c.Assert(err, NotNil)
208}
209
176type B struct {210type B struct {
177 BFld int211 BFld int
178}212}
@@ -193,13 +227,6 @@
193 c.Check(a, DeepEquals, A{1, B{2}, 0})227 c.Check(a, DeepEquals, A{1, B{2}, 0})
194}228}
195229
196type testConfig2 struct {
197 A int
198 B string
199 C []string `json:"c_list"`
200 D ConfigTimeDuration
201}
202
203func (s *configSuite) TestCompareConfig(c *C) {230func (s *configSuite) TestCompareConfig(c *C) {
204 var cfg1 = testConfig2{231 var cfg1 = testConfig2{
205 A: 1,232 A: 1,
@@ -304,6 +331,7 @@
304 c.Check(cfg.A, Equals, 42)331 c.Check(cfg.A, Equals, 42)
305 c.Check(cfg.B, Equals, "x")332 c.Check(cfg.B, Equals, "x")
306 c.Check(cfg.C, DeepEquals, []string{"y", "z"})333 c.Check(cfg.C, DeepEquals, []string{"y", "z"})
334 c.Check(flag.Lookup("cfg@").Value.String(), Equals, cfgPath)
307}335}
308336
309func (s *configFlagsSuite) TestReadUsingFlagsHelp(c *C) {337func (s *configFlagsSuite) TestReadUsingFlagsHelp(c *C) {
310338
=== modified file 'debian/changelog'
--- debian/changelog 2014-04-23 11:54:00 +0000
+++ debian/changelog 2014-05-29 12:04:32 +0000
@@ -1,3 +1,9 @@
1ubuntu-push (0.21-0.ubuntu1) UNRELEASED; urgency=medium
2
3 * New upstream release: first auth bits, and Qt dependency.
4
5 -- John Lenton <john.lenton@canonical.com> Tue, 15 Apr 2014 14:04:35 +0100
6
1ubuntu-push (0.2.1+14.04.20140423.1-0ubuntu1) trusty; urgency=high7ubuntu-push (0.2.1+14.04.20140423.1-0ubuntu1) trusty; urgency=high
28
3 [ Samuele Pedroni ]9 [ Samuele Pedroni ]
410
=== modified file 'debian/control'
--- debian/control 2014-03-25 16:26:20 +0000
+++ debian/control 2014-05-29 12:04:32 +0000
@@ -14,6 +14,8 @@
14 libgcrypt11-dev,14 libgcrypt11-dev,
15 libglib2.0-dev (>= 2.31.6),15 libglib2.0-dev (>= 2.31.6),
16 libwhoopsie-dev,16 libwhoopsie-dev,
17 libubuntuoneauth-2.0-dev,
18 cmake,
17Standards-Version: 3.9.519Standards-Version: 3.9.5
18Homepage: http://launchpad.net/ubuntu-push20Homepage: http://launchpad.net/ubuntu-push
19Vcs-Bzr: lp:ubuntu-push21Vcs-Bzr: lp:ubuntu-push
2022
=== modified file 'debian/rules'
--- debian/rules 2014-03-24 12:22:55 +0000
+++ debian/rules 2014-05-29 12:04:32 +0000
@@ -2,9 +2,13 @@
2# -*- makefile -*-2# -*- makefile -*-
33
4export DH_GOPKG := launchpad.net/ubuntu-push4export DH_GOPKG := launchpad.net/ubuntu-push
5export DEB_BUILD_OPTIONS := nostrip
6export UBUNTU_PUSH_TEST_RESOURCES_ROOT := $(CURDIR)5export UBUNTU_PUSH_TEST_RESOURCES_ROOT := $(CURDIR)
76
7override_dh_auto_build:
8 cd $$( find ./ -type d -regex '\./[^/]*/src/launchpad.net' -printf "%h\n" | head -n1)
9 dh_auto_build --buildsystem=golang
10 (cd signing-helper && cmake . && make)
11
8override_dh_install:12override_dh_install:
9 dh_install -Xusr/bin/cmd -Xusr/bin/dev --fail-missing13 dh_install -Xusr/bin/cmd -Xusr/bin/dev --fail-missing
1014
1115
=== modified file 'debian/ubuntu-push-client.install'
--- debian/ubuntu-push-client.install 2014-03-26 16:27:19 +0000
+++ debian/ubuntu-push-client.install 2014-05-29 12:04:32 +0000
@@ -1,4 +1,5 @@
1#!/usr/bin/dh-exec1#!/usr/bin/dh-exec
2debian/config.json /etc/xdg/ubuntu-push-client2debian/config.json /etc/xdg/ubuntu-push-client
3debian/ubuntu-push-client.conf /usr/share/upstart/sessions3debian/ubuntu-push-client.conf /usr/share/upstart/sessions
4signing-helper/signing-helper /usr/lib/ubuntu-push-client
4usr/bin/ubuntu-push => /usr/lib/ubuntu-push-client/ubuntu-push-client5usr/bin/ubuntu-push => /usr/lib/ubuntu-push-client/ubuntu-push-client
56
=== modified file 'dependencies.tsv'
--- dependencies.tsv 2014-03-12 13:23:26 +0000
+++ dependencies.tsv 2014-05-29 12:04:32 +0000
@@ -2,3 +2,4 @@
2launchpad.net/go-dbus/v1 bzr james@jamesh.id.au-20140206110213-pbzcr6ucaz3rqmnw 1252launchpad.net/go-dbus/v1 bzr james@jamesh.id.au-20140206110213-pbzcr6ucaz3rqmnw 125
3launchpad.net/go-xdg/v0 bzr john.lenton@canonical.com-20140208094800-gubd5md7cro3mtxa 103launchpad.net/go-xdg/v0 bzr john.lenton@canonical.com-20140208094800-gubd5md7cro3mtxa 10
4launchpad.net/gocheck bzr gustavo@niemeyer.net-20140127131816-zshobk1qqme626xw 864launchpad.net/gocheck bzr gustavo@niemeyer.net-20140127131816-zshobk1qqme626xw 86
5launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid bzr samuele.pedroni@canonical.com-20140130122455-pm9h8etl4owp90lg 1
56
=== added directory 'nih'
=== added directory 'nih/cnih'
=== added file 'nih/cnih/cnih.go'
--- nih/cnih/cnih.go 1970-01-01 00:00:00 +0000
+++ nih/cnih/cnih.go 2014-05-29 12:04:32 +0000
@@ -0,0 +1,28 @@
1package cnih
2
3/*
4#cgo pkg-config: dbus-1 libnih libnih-dbus
5#include <stdlib.h>
6#include <nih/alloc.h>
7#include <libnih-dbus.h>
8
9// a small wrapper because cgo doesn't handle varargs
10char *cuote (const char *id) {
11 return nih_dbus_path (NULL, "", id, NULL);
12}
13*/
14import "C"
15
16import (
17 "unsafe"
18)
19
20func Quote(s []byte) string {
21 cs := C.CString(string(s))
22 defer C.free(unsafe.Pointer(cs))
23
24 cq := C.cuote(cs)
25 defer C.nih_free(unsafe.Pointer(cq))
26
27 return C.GoString(cq)[1:]
28}
029
=== added file 'nih/nih.go'
--- nih/nih.go 1970-01-01 00:00:00 +0000
+++ nih/nih.go 2014-05-29 12:04:32 +0000
@@ -0,0 +1,68 @@
1/*
2 Copyright 2013-2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17// package nih reimplements libnih-dbus's nih_dbus_path's path element
18// quoting.
19//
20// Reimplementing libnih is a wonderful exercise that everybody should persue
21// at least thrice.
22package nih
23
24import "strconv"
25
26// Quote() takes a byte slice and quotes it á la libnih.
27func Quote(s []byte) []byte {
28 if len(s) == 0 {
29 return []byte{'_'}
30 }
31 out := make([]byte, 0, 2*len(s))
32 for _, c := range s {
33 if ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') {
34 out = append(out, c)
35 } else {
36 if c < 16 {
37 out = append(out, '_', '0')
38 } else {
39 out = append(out, '_')
40 }
41 out = strconv.AppendUint(out, uint64(c), 16)
42 }
43 }
44
45 return out
46}
47
48// Quote() takes a byte slice and undoes the damage done to it by the quoting.
49func Unquote(s []byte) []byte {
50 out := make([]byte, 0, len(s))
51
52 for i := 0; i < len(s); i++ {
53 if s[i] == '_' {
54 if len(s) < i+3 {
55 break
56 }
57 num, err := strconv.ParseUint(string(s[i+1:i+3]), 16, 8)
58 if err == nil {
59 out = append(out, byte(num))
60 }
61 i += 2
62 } else {
63 out = append(out, s[i])
64 }
65 }
66
67 return out
68}
069
=== added file 'nih/nih_test.go'
--- nih/nih_test.go 1970-01-01 00:00:00 +0000
+++ nih/nih_test.go 2014-05-29 12:04:32 +0000
@@ -0,0 +1,57 @@
1/*
2 Copyright 2013-2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package nih
18
19import (
20 "testing"
21
22 . "launchpad.net/gocheck"
23
24 "launchpad.net/ubuntu-push/nih/cnih"
25)
26
27func TestNIH(t *testing.T) { TestingT(t) }
28
29type nihSuite struct{}
30
31var _ = Suite(&nihSuite{})
32
33func (ns *nihSuite) TestQuote(c *C) {
34 for i, s := range []struct {
35 raw []byte
36 quoted []byte
37 }{
38 {[]byte("test"), []byte("test")},
39 {[]byte("foo/bar.baz"), []byte("foo_2fbar_2ebaz")},
40 {[]byte("test_thing"), []byte("test_5fthing")},
41 {[]byte("\x01\x0f\x10\xff"), []byte("_01_0f_10_ff")},
42 {[]byte{}, []byte{'_'}},
43 } {
44 c.Check(string(s.quoted), Equals, cnih.Quote(s.raw), Commentf("iter %d (%s)", i, string(s.quoted)))
45 c.Check(string(Quote(s.raw)), DeepEquals, string(s.quoted), Commentf("iter %d (%s)", i, string(s.quoted)))
46 c.Check(Unquote(s.quoted), DeepEquals, s.raw, Commentf("iter %d (%s)", i, string(s.quoted)))
47 c.Check(string(Quote(s.raw)), Equals, cnih.Quote(s.raw), Commentf("iter %d (%s)", i, string(s.quoted)))
48 }
49
50 // check one cnih doesn't like
51 c.Check(Quote([]byte{0}), DeepEquals, []byte("_00"))
52
53 // check we don't panic with some weird ones
54 for i, s := range []string{"foo_", "foo_a", "foo_zz"} {
55 c.Check(Unquote([]byte(s)), DeepEquals, []byte("foo"), Commentf("iter %d (%s)", i, s))
56 }
57}
058
=== modified file 'protocol/messages.go'
--- protocol/messages.go 2014-04-04 13:54:45 +0000
+++ protocol/messages.go 2014-05-29 12:04:32 +0000
@@ -20,6 +20,7 @@
2020
21import (21import (
22 "encoding/json"22 "encoding/json"
23 "fmt"
23)24)
2425
25// System channel id using a shortened hex-encoded form for the NIL UUID.26// System channel id using a shortened hex-encoded form for the NIL UUID.
@@ -54,6 +55,14 @@
54 Split() (done bool)55 Split() (done bool)
55}56}
5657
58// OnewayMsg are messages that are not to be followed by a response,
59// after sending them the session either aborts or continues.
60type OnewayMsg interface {
61 SplittableMsg
62 // continue session after the message?
63 OnewayContinue() bool
64}
65
57// CONNBROKEN message, server side is breaking the connection for reason.66// CONNBROKEN message, server side is breaking the connection for reason.
58type ConnBrokenMsg struct {67type ConnBrokenMsg struct {
59 Type string `json:"T"`68 Type string `json:"T"`
@@ -65,11 +74,35 @@
65 return true74 return true
66}75}
6776
77func (m *ConnBrokenMsg) OnewayContinue() bool {
78 return false
79}
80
68// CONNBROKEN reasons81// CONNBROKEN reasons
69const (82const (
70 BrokenHostMismatch = "host-mismatch"83 BrokenHostMismatch = "host-mismatch"
71)84)
7285
86// CONNWARN message, server side is warning about partial functionality
87// because reason.
88type ConnWarnMsg struct {
89 Type string `json:"T"`
90 // reason
91 Reason string
92}
93
94func (m *ConnWarnMsg) Split() bool {
95 return true
96}
97func (m *ConnWarnMsg) OnewayContinue() bool {
98 return true
99}
100
101// CONNWARN reasons
102const (
103 WarnUnauthorized = "unauthorized"
104)
105
73// PING/PONG messages106// PING/PONG messages
74type PingPongMsg struct {107type PingPongMsg struct {
75 Type string `json:"T"`108 Type string `json:"T"`
@@ -111,8 +144,9 @@
111}144}
112145
113// Reset resets the splitting state if the message storage is to be146// Reset resets the splitting state if the message storage is to be
114// reused.147// reused and sets the proper Type.
115func (b *BroadcastMsg) Reset() {148func (b *BroadcastMsg) Reset() {
149 b.Type = "broadcast"
116 b.splitting = 0150 b.splitting = 0
117}151}
118152
@@ -120,6 +154,41 @@
120type NotificationsMsg struct {154type NotificationsMsg struct {
121 Type string `json:"T"`155 Type string `json:"T"`
122 Notifications []Notification156 Notifications []Notification
157 splitting int
158}
159
160// Reset resets the splitting state if the message storage is to be
161// reused and sets the proper Type.
162func (m *NotificationsMsg) Reset() {
163 m.Type = "notifications"
164 m.splitting = 0
165}
166
167func (m *NotificationsMsg) Split() bool {
168 if m.splitting != 0 {
169 m.Notifications = m.Notifications[len(m.Notifications):m.splitting]
170 }
171 notifs := m.Notifications
172 var size int
173 for i, notif := range notifs {
174 size += len(notif.Payload) + len(notif.AppId) + len(notif.MsgId) + notificationOverhead
175 if size > maxPayloadSize {
176 m.splitting = len(notifs)
177 m.Notifications = notifs[:i]
178 return false
179 }
180 }
181 return true
182}
183
184var notificationOverhead int
185
186func init() {
187 buf, err := json.Marshal(Notification{})
188 if err != nil {
189 panic(fmt.Errorf("failed to compute Notification marshal overhead: %v", err))
190 }
191 notificationOverhead = len(buf) - 4 // - 4 for the null from P(ayload)
123}192}
124193
125// A single unicast notification194// A single unicast notification
@@ -130,6 +199,19 @@
130 Payload json.RawMessage `json:"P"`199 Payload json.RawMessage `json:"P"`
131}200}
132201
202// ExtractPayloads gets only the payloads out of a slice of notications.
203func ExtractPayloads(notifications []Notification) []json.RawMessage {
204 n := len(notifications)
205 if n == 0 {
206 return nil
207 }
208 payloads := make([]json.RawMessage, n)
209 for i := 0; i < n; i++ {
210 payloads[i] = notifications[i].Payload
211 }
212 return payloads
213}
214
133// ACKnowledgement message215// ACKnowledgement message
134type AckMsg struct {216type AckMsg struct {
135 Type string `json:"T"`217 Type string `json:"T"`
136218
=== modified file 'protocol/messages_test.go'
--- protocol/messages_test.go 2014-04-04 13:19:10 +0000
+++ protocol/messages_test.go 2014-05-29 12:04:32 +0000
@@ -100,10 +100,100 @@
100 c.Check(b.TopLevel, Equals, int64(n))100 c.Check(b.TopLevel, Equals, int64(n))
101 c.Check(n1+n2+n3, Equals, n)101 c.Check(n1+n2+n3, Equals, n)
102 // reset102 // reset
103 b.Type = ""
103 b.Reset()104 b.Reset()
105 c.Check(b.Type, Equals, "broadcast")
104 c.Check(b.splitting, Equals, 0)106 c.Check(b.splitting, Equals, 0)
105}107}
106108
107func (s *messagesSuite) TestSplitConnBrokenMsg(c *C) {109func (s *messagesSuite) TestConnBrokenMsg(c *C) {
108 c.Check((&ConnBrokenMsg{}).Split(), Equals, true)110 m := &ConnBrokenMsg{}
111 c.Check(m.Split(), Equals, true)
112 c.Check(m.OnewayContinue(), Equals, false)
113}
114
115func (s *messagesSuite) TestConnWarnMsg(c *C) {
116 m := &ConnWarnMsg{}
117 c.Check(m.Split(), Equals, true)
118 c.Check(m.OnewayContinue(), Equals, true)
119}
120
121func (s *messagesSuite) TestExtractPayloads(c *C) {
122 c.Check(ExtractPayloads(nil), IsNil)
123 p1 := json.RawMessage(`{"a":1}`)
124 p2 := json.RawMessage(`{"b":2}`)
125 ns := []Notification{Notification{Payload: p1}, Notification{Payload: p2}}
126 c.Check(ExtractPayloads(ns), DeepEquals, []json.RawMessage{p1, p2})
127}
128
129func (s *messagesSuite) TestSplitNotificationsMsgNop(c *C) {
130 n := &NotificationsMsg{
131 Type: "notifications",
132 Notifications: []Notification{
133 Notification{"app1", "msg1", json.RawMessage(`{m:1}`)},
134 Notification{"app1", "msg1", json.RawMessage(`{m:2}`)},
135 },
136 }
137 done := n.Split()
138 c.Check(done, Equals, true)
139 c.Check(cap(n.Notifications), Equals, 2)
140 c.Check(len(n.Notifications), Equals, 2)
141}
142
143var payloadFmt2 = fmt.Sprintf(`{"b":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2-notificationOverhead-4-6)) // 4 = app1 6 = msg%03d
144
145func manyNotifications(c int) []Notification {
146 notifs := make([]Notification, 0, 1)
147 for i := 0; i < c; i++ {
148 notifs = append(notifs, Notification{
149 "app1",
150 fmt.Sprintf("msg%03d", i),
151 json.RawMessage(fmt.Sprintf(payloadFmt2, i)),
152 })
153 }
154 return notifs
155}
156
157func (s *messagesSuite) TestSplitNotificationsMsgMany(c *C) {
158 notifs := manyNotifications(33)
159 n := len(notifs)
160 // more interesting this way
161 c.Assert(cap(notifs), Not(Equals), n)
162 nm := &NotificationsMsg{
163 Type: "notifications",
164 Notifications: notifs,
165 }
166 done := nm.Split()
167 c.Assert(done, Equals, false)
168 n1 := len(nm.Notifications)
169 buf, err := json.Marshal(nm)
170 c.Assert(err, IsNil)
171 c.Assert(len(buf) <= 65535, Equals, true)
172 c.Check(len(buf)+len(notifs[n1].Payload) > maxPayloadSize, Equals, true)
173 done = nm.Split()
174 c.Assert(done, Equals, true)
175 n2 := len(nm.Notifications)
176 c.Check(n1+n2, Equals, n)
177
178 notifs = manyNotifications(61)
179 n = len(notifs)
180 nm = &NotificationsMsg{
181 Type: "notifications",
182 Notifications: notifs,
183 }
184 done = nm.Split()
185 c.Assert(done, Equals, false)
186 n1 = len(nm.Notifications)
187 done = nm.Split()
188 c.Assert(done, Equals, false)
189 n2 = len(nm.Notifications)
190 done = nm.Split()
191 c.Assert(done, Equals, true)
192 n3 := len(nm.Notifications)
193 c.Check(n1+n2+n3, Equals, n)
194 // reset
195 nm.Type = ""
196 nm.Reset()
197 c.Check(nm.Type, Equals, "notifications")
198 c.Check(nm.splitting, Equals, 0)
109}199}
110200
=== modified file 'protocol/state-diag-client.gv'
--- protocol/state-diag-client.gv 2014-01-16 20:07:13 +0000
+++ protocol/state-diag-client.gv 2014-05-29 12:04:32 +0000
@@ -2,7 +2,7 @@
2 label = "State diagram for client";2 label = "State diagram for client";
3 size="12,6";3 size="12,6";
4 rankdir=LR;4 rankdir=LR;
5 node [shape = doublecircle]; pingTimeout;5 node [shape = doublecircle]; pingTimeout; connBroken;
6 node [shape = circle];6 node [shape = circle];
7 start1 -> start2 [ label = "Write wire version" ];7 start1 -> start2 [ label = "Write wire version" ];
8 start2 -> start3 [ label = "Write CONNECT" ];8 start2 -> start3 [ label = "Write CONNECT" ];
@@ -13,4 +13,7 @@
13 broadcast -> loop [label = "Write ACK"];13 broadcast -> loop [label = "Write ACK"];
14 loop -> pingTimeout [14 loop -> pingTimeout [
15 label = "Elapsed ping interval + exchange interval"];15 label = "Elapsed ping interval + exchange interval"];
16 loop -> connBroken [label = "Read CONNBROKEN"];
17 loop -> warn [label = "Read CONNWARN"];
18 warn -> loop;
16}19}
1720
=== modified file 'protocol/state-diag-client.svg'
--- protocol/state-diag-client.svg 2014-01-16 19:37:57 +0000
+++ protocol/state-diag-client.svg 2014-05-29 12:04:32 +0000
@@ -4,95 +4,123 @@
4<!-- Generated by graphviz version 2.26.3 (20100126.1600)4<!-- Generated by graphviz version 2.26.3 (20100126.1600)
5 -->5 -->
6<!-- Title: state_diagram_client Pages: 1 -->6<!-- Title: state_diagram_client Pages: 1 -->
7<svg width="864pt" height="279pt"7<svg width="822pt" height="432pt"
8 viewBox="0.00 0.00 864.00 278.89" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">8 viewBox="0.00 0.00 822.36 432.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
9<g id="graph1" class="graph" transform="scale(0.683544 0.683544) rotate(0) translate(4 404)">9<g id="graph1" class="graph" transform="scale(0.650602 0.650602) rotate(0) translate(4 660)">
10<title>state_diagram_client</title>10<title>state_diagram_client</title>
11<polygon fill="white" stroke="white" points="-4,5 -4,-404 1261,-404 1261,5 -4,5"/>11<polygon fill="white" stroke="white" points="-4,5 -4,-660 1261,-660 1261,5 -4,5"/>
12<text text-anchor="middle" x="628" y="-9.4" font-family="Times Roman,serif" font-size="14.00">State diagram for client</text>12<text text-anchor="middle" x="628" y="-9.4" font-family="Times Roman,serif" font-size="14.00">State diagram for client</text>
13<!-- pingTimeout -->13<!-- pingTimeout -->
14<g id="node1" class="node"><title>pingTimeout</title>14<g id="node1" class="node"><title>pingTimeout</title>
15<ellipse fill="none" stroke="black" cx="1180" cy="-324" rx="72.1249" ry="72.1249"/>15<ellipse fill="none" stroke="black" cx="1180" cy="-580" rx="72.1249" ry="72.1249"/>
16<ellipse fill="none" stroke="black" cx="1180" cy="-324" rx="76.1249" ry="76.1249"/>16<ellipse fill="none" stroke="black" cx="1180" cy="-580" rx="76.1249" ry="76.1249"/>
17<text text-anchor="middle" x="1180" y="-320.4" font-family="Times Roman,serif" font-size="14.00">pingTimeout</text>17<text text-anchor="middle" x="1180" y="-576.4" font-family="Times Roman,serif" font-size="14.00">pingTimeout</text>
18</g>
19<!-- connBroken -->
20<g id="node2" class="node"><title>connBroken</title>
21<ellipse fill="none" stroke="black" cx="1180" cy="-413" rx="68.8251" ry="69.2965"/>
22<ellipse fill="none" stroke="black" cx="1180" cy="-413" rx="72.7978" ry="73.2965"/>
23<text text-anchor="middle" x="1180" y="-409.4" font-family="Times Roman,serif" font-size="14.00">connBroken</text>
18</g>24</g>
19<!-- start1 -->25<!-- start1 -->
20<g id="node2" class="node"><title>start1</title>26<g id="node3" class="node"><title>start1</title>
21<ellipse fill="none" stroke="black" cx="42" cy="-166" rx="41.2167" ry="41.7193"/>27<ellipse fill="none" stroke="black" cx="42" cy="-231" rx="41.2167" ry="41.7193"/>
22<text text-anchor="middle" x="42" y="-162.4" font-family="Times Roman,serif" font-size="14.00">start1</text>28<text text-anchor="middle" x="42" y="-227.4" font-family="Times Roman,serif" font-size="14.00">start1</text>
23</g>29</g>
24<!-- start2 -->30<!-- start2 -->
25<g id="node4" class="node"><title>start2</title>31<g id="node5" class="node"><title>start2</title>
26<ellipse fill="none" stroke="black" cx="292" cy="-166" rx="41.2167" ry="41.7193"/>32<ellipse fill="none" stroke="black" cx="292" cy="-231" rx="41.2167" ry="41.7193"/>
27<text text-anchor="middle" x="292" y="-162.4" font-family="Times Roman,serif" font-size="14.00">start2</text>33<text text-anchor="middle" x="292" y="-227.4" font-family="Times Roman,serif" font-size="14.00">start2</text>
28</g>34</g>
29<!-- start1&#45;&gt;start2 -->35<!-- start1&#45;&gt;start2 -->
30<g id="edge2" class="edge"><title>start1&#45;&gt;start2</title>36<g id="edge2" class="edge"><title>start1&#45;&gt;start2</title>
31<path fill="none" stroke="black" d="M83.5631,-166C126.547,-166 193.757,-166 240.181,-166"/>37<path fill="none" stroke="black" d="M83.5631,-231C126.547,-231 193.757,-231 240.181,-231"/>
32<polygon fill="black" stroke="black" points="240.338,-169.5 250.338,-166 240.338,-162.5 240.338,-169.5"/>38<polygon fill="black" stroke="black" points="240.338,-234.5 250.338,-231 240.338,-227.5 240.338,-234.5"/>
33<text text-anchor="middle" x="167" y="-171.4" font-family="Times Roman,serif" font-size="14.00">Write wire version</text>39<text text-anchor="middle" x="167" y="-236.4" font-family="Times Roman,serif" font-size="14.00">Write wire version</text>
34</g>40</g>
35<!-- start3 -->41<!-- start3 -->
36<g id="node6" class="node"><title>start3</title>42<g id="node7" class="node"><title>start3</title>
37<ellipse fill="none" stroke="black" cx="526" cy="-166" rx="41.2167" ry="41.7193"/>43<ellipse fill="none" stroke="black" cx="526" cy="-231" rx="41.2167" ry="41.7193"/>
38<text text-anchor="middle" x="526" y="-162.4" font-family="Times Roman,serif" font-size="14.00">start3</text>44<text text-anchor="middle" x="526" y="-227.4" font-family="Times Roman,serif" font-size="14.00">start3</text>
39</g>45</g>
40<!-- start2&#45;&gt;start3 -->46<!-- start2&#45;&gt;start3 -->
41<g id="edge4" class="edge"><title>start2&#45;&gt;start3</title>47<g id="edge4" class="edge"><title>start2&#45;&gt;start3</title>
42<path fill="none" stroke="black" d="M333.565,-166C372.875,-166 431.992,-166 474.321,-166"/>48<path fill="none" stroke="black" d="M333.565,-231C372.875,-231 431.992,-231 474.321,-231"/>
43<polygon fill="black" stroke="black" points="474.429,-169.5 484.429,-166 474.429,-162.5 474.429,-169.5"/>49<polygon fill="black" stroke="black" points="474.429,-234.5 484.429,-231 474.429,-227.5 474.429,-234.5"/>
44<text text-anchor="middle" x="409" y="-171.4" font-family="Times Roman,serif" font-size="14.00">Write CONNECT</text>50<text text-anchor="middle" x="409" y="-236.4" font-family="Times Roman,serif" font-size="14.00">Write CONNECT</text>
45</g>51</g>
46<!-- loop -->52<!-- loop -->
47<g id="node8" class="node"><title>loop</title>53<g id="node9" class="node"><title>loop</title>
48<ellipse fill="none" stroke="black" cx="746" cy="-166" rx="31.8198" ry="31.8198"/>54<ellipse fill="none" stroke="black" cx="746" cy="-231" rx="31.8198" ry="31.8198"/>
49<text text-anchor="middle" x="746" y="-162.4" font-family="Times Roman,serif" font-size="14.00">loop</text>55<text text-anchor="middle" x="746" y="-227.4" font-family="Times Roman,serif" font-size="14.00">loop</text>
50</g>56</g>
51<!-- start3&#45;&gt;loop -->57<!-- start3&#45;&gt;loop -->
52<g id="edge6" class="edge"><title>start3&#45;&gt;loop</title>58<g id="edge6" class="edge"><title>start3&#45;&gt;loop</title>
53<path fill="none" stroke="black" d="M567.639,-166C606.633,-166 664.616,-166 703.793,-166"/>59<path fill="none" stroke="black" d="M567.639,-231C606.633,-231 664.616,-231 703.793,-231"/>
54<polygon fill="black" stroke="black" points="703.818,-169.5 713.818,-166 703.818,-162.5 703.818,-169.5"/>60<polygon fill="black" stroke="black" points="703.818,-234.5 713.818,-231 703.818,-227.5 703.818,-234.5"/>
55<text text-anchor="middle" x="641" y="-171.4" font-family="Times Roman,serif" font-size="14.00">Read CONNACK</text>61<text text-anchor="middle" x="641" y="-236.4" font-family="Times Roman,serif" font-size="14.00">Read CONNACK</text>
56</g>62</g>
57<!-- loop&#45;&gt;pingTimeout -->63<!-- loop&#45;&gt;pingTimeout -->
58<g id="edge16" class="edge"><title>loop&#45;&gt;pingTimeout</title>64<g id="edge16" class="edge"><title>loop&#45;&gt;pingTimeout</title>
59<path fill="none" stroke="black" d="M763.666,-192.937C772.211,-204.042 783.361,-216.128 796,-224 888.06,-281.339 1012.12,-305.973 1094,-316.443"/>65<path fill="none" stroke="black" d="M750.211,-262.971C757.458,-313.528 773.689,-408.79 796,-434 872.806,-520.784 1006.81,-556.22 1094.46,-570.528"/>
60<polygon fill="black" stroke="black" points="1093.67,-319.928 1104.02,-317.68 1094.53,-312.981 1093.67,-319.928"/>66<polygon fill="black" stroke="black" points="1093.96,-573.992 1104.39,-572.09 1095.05,-567.078 1093.96,-573.992"/>
61<text text-anchor="middle" x="941" y="-319.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval + exchange interval</text>67<text text-anchor="middle" x="941" y="-572.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval + exchange interval</text>
68</g>
69<!-- loop&#45;&gt;connBroken -->
70<g id="edge18" class="edge"><title>loop&#45;&gt;connBroken</title>
71<path fill="none" stroke="black" d="M755.1,-261.824C762.755,-282.438 775.756,-308.526 796,-324 883.382,-390.791 1012.39,-408.797 1096.33,-412.948"/>
72<polygon fill="black" stroke="black" points="1096.19,-416.445 1106.33,-413.388 1096.5,-409.452 1096.19,-416.445"/>
73<text text-anchor="middle" x="941" y="-417.4" font-family="Times Roman,serif" font-size="14.00">Read CONNBROKEN</text>
62</g>74</g>
63<!-- pong -->75<!-- pong -->
64<g id="node10" class="node"><title>pong</title>76<g id="node11" class="node"><title>pong</title>
65<ellipse fill="none" stroke="black" cx="1180" cy="-195" rx="34.8574" ry="35.3553"/>77<ellipse fill="none" stroke="black" cx="1180" cy="-287" rx="34.8574" ry="35.3553"/>
66<text text-anchor="middle" x="1180" y="-191.4" font-family="Times Roman,serif" font-size="14.00">pong</text>78<text text-anchor="middle" x="1180" y="-283.4" font-family="Times Roman,serif" font-size="14.00">pong</text>
67</g>79</g>
68<!-- loop&#45;&gt;pong -->80<!-- loop&#45;&gt;pong -->
69<g id="edge8" class="edge"><title>loop&#45;&gt;pong</title>81<g id="edge8" class="edge"><title>loop&#45;&gt;pong</title>
70<path fill="none" stroke="black" d="M775.392,-179.044C782.046,-181.465 789.167,-183.653 796,-185 916.362,-208.722 1062.02,-203.515 1134.48,-198.706"/>82<path fill="none" stroke="black" d="M768.467,-253.959C776.476,-260.698 786.005,-267.259 796,-271 911.696,-314.31 1060.9,-303.343 1134.62,-293.955"/>
71<polygon fill="black" stroke="black" points="1134.89,-202.186 1144.62,-198.003 1134.4,-195.203 1134.89,-202.186"/>83<polygon fill="black" stroke="black" points="1135.49,-297.371 1144.94,-292.588 1134.57,-290.432 1135.49,-297.371"/>
72<text text-anchor="middle" x="941" y="-207.4" font-family="Times Roman,serif" font-size="14.00">Read PING</text>84<text text-anchor="middle" x="941" y="-307.4" font-family="Times Roman,serif" font-size="14.00">Read PING</text>
73</g>85</g>
74<!-- broadcast -->86<!-- broadcast -->
75<g id="node12" class="node"><title>broadcast</title>87<g id="node13" class="node"><title>broadcast</title>
76<ellipse fill="none" stroke="black" cx="1180" cy="-84" rx="58.1882" ry="58.6899"/>88<ellipse fill="none" stroke="black" cx="1180" cy="-176" rx="58.1882" ry="58.6899"/>
77<text text-anchor="middle" x="1180" y="-80.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>89<text text-anchor="middle" x="1180" y="-172.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>
78</g>90</g>
79<!-- loop&#45;&gt;broadcast -->91<!-- loop&#45;&gt;broadcast -->
80<g id="edge10" class="edge"><title>loop&#45;&gt;broadcast</title>92<g id="edge10" class="edge"><title>loop&#45;&gt;broadcast</title>
81<path fill="none" stroke="black" d="M770.52,-145.1C778.217,-139.607 787.053,-134.301 796,-131 917.482,-86.1746 957.924,-122.075 1086,-103 1094.61,-101.717 1103.63,-100.165 1112.53,-98.5074"/>93<path fill="none" stroke="black" d="M775.45,-218.228C782.1,-215.791 789.205,-213.528 796,-212 922.145,-183.64 957.464,-202.973 1086,-189 1094.36,-188.091 1103.12,-187.028 1111.79,-185.909"/>
82<polygon fill="black" stroke="black" points="1113.34,-101.917 1122.5,-96.5998 1112.02,-95.0419 1113.34,-101.917"/>94<polygon fill="black" stroke="black" points="1112.44,-189.353 1121.9,-184.574 1111.53,-182.413 1112.44,-189.353"/>
83<text text-anchor="middle" x="941" y="-136.4" font-family="Times Roman,serif" font-size="14.00">Read BROADCAST</text>95<text text-anchor="middle" x="941" y="-217.4" font-family="Times Roman,serif" font-size="14.00">Read BROADCAST</text>
96</g>
97<!-- warn -->
98<g id="node19" class="node"><title>warn</title>
99<ellipse fill="none" stroke="black" cx="1180" cy="-63" rx="36.7696" ry="36.7696"/>
100<text text-anchor="middle" x="1180" y="-59.4" font-family="Times Roman,serif" font-size="14.00">warn</text>
101</g>
102<!-- loop&#45;&gt;warn -->
103<g id="edge20" class="edge"><title>loop&#45;&gt;warn</title>
104<path fill="none" stroke="black" d="M753.357,-199.767C760.401,-177.027 773.396,-147.441 796,-131 901.425,-54.3166 958.242,-112.935 1086,-87 1101.84,-83.7841 1119.02,-79.6061 1134.3,-75.6396"/>
105<polygon fill="black" stroke="black" points="1135.26,-79.0068 1144.04,-73.0757 1133.48,-72.2376 1135.26,-79.0068"/>
106<text text-anchor="middle" x="941" y="-136.4" font-family="Times Roman,serif" font-size="14.00">Read CONNWARN</text>
84</g>107</g>
85<!-- pong&#45;&gt;loop -->108<!-- pong&#45;&gt;loop -->
86<g id="edge12" class="edge"><title>pong&#45;&gt;loop</title>109<g id="edge12" class="edge"><title>pong&#45;&gt;loop</title>
87<path fill="none" stroke="black" d="M1147.19,-180.867C1129.44,-173.986 1106.92,-166.463 1086,-163 980.081,-145.465 853.051,-154.36 788.368,-160.981"/>110<path fill="none" stroke="black" d="M1148.22,-271.079C1130.39,-262.942 1107.48,-253.77 1086,-249 1030.54,-236.684 866.695,-232.715 788.482,-231.502"/>
88<polygon fill="black" stroke="black" points="787.736,-157.528 778.16,-162.06 788.472,-164.489 787.736,-157.528"/>111<polygon fill="black" stroke="black" points="788.085,-227.996 778.035,-231.348 787.982,-234.995 788.085,-227.996"/>
89<text text-anchor="middle" x="941" y="-168.4" font-family="Times Roman,serif" font-size="14.00">Write PONG</text>112<text text-anchor="middle" x="941" y="-254.4" font-family="Times Roman,serif" font-size="14.00">Write PONG</text>
90</g>113</g>
91<!-- broadcast&#45;&gt;loop -->114<!-- broadcast&#45;&gt;loop -->
92<g id="edge14" class="edge"><title>broadcast&#45;&gt;loop</title>115<g id="edge14" class="edge"><title>broadcast&#45;&gt;loop</title>
93<path fill="none" stroke="black" d="M1123.8,-67.0114C1044.83,-46.6166 899.156,-22.0001 796,-81 778.946,-90.7538 767.135,-108.842 759.293,-125.833"/>116<path fill="none" stroke="black" d="M1121.7,-168.205C1028.72,-156.837 851.665,-139.849 796,-167 784,-172.853 774.037,-183.132 766.245,-193.762"/>
94<polygon fill="black" stroke="black" points="756.044,-124.528 755.336,-135.099 762.482,-127.277 756.044,-124.528"/>117<polygon fill="black" stroke="black" points="763.182,-192.043 760.465,-202.284 768.975,-195.973 763.182,-192.043"/>
95<text text-anchor="middle" x="941" y="-86.4" font-family="Times Roman,serif" font-size="14.00">Write ACK</text>118<text text-anchor="middle" x="941" y="-172.4" font-family="Times Roman,serif" font-size="14.00">Write ACK</text>
119</g>
120<!-- warn&#45;&gt;loop -->
121<g id="edge22" class="edge"><title>warn&#45;&gt;loop</title>
122<path fill="none" stroke="black" d="M1144.07,-53.3553C1070.4,-35.8873 900.397,-7.71825 796,-87 779.313,-99.6722 764.14,-151.763 754.991,-189.659"/>
123<polygon fill="black" stroke="black" points="751.574,-188.904 752.686,-199.44 758.387,-190.51 751.574,-188.904"/>
96</g>124</g>
97</g>125</g>
98</svg>126</svg>
99127
=== modified file 'protocol/state-diag-session.gv'
--- protocol/state-diag-session.gv 2014-01-16 20:07:13 +0000
+++ protocol/state-diag-session.gv 2014-05-29 12:04:32 +0000
@@ -2,6 +2,7 @@
2 label = "State diagram for session";2 label = "State diagram for session";
3 size="12,6";3 size="12,6";
4 rankdir=LR;4 rankdir=LR;
5 node [shape = doublecircle]; stop;
5 node [shape = circle];6 node [shape = circle];
6 start1 -> start2 [ label = "Read wire version" ];7 start1 -> start2 [ label = "Read wire version" ];
7 start2 -> start3 [ label = "Read CONNECT" ];8 start2 -> start3 [ label = "Read CONNECT" ];
@@ -17,4 +18,13 @@
17 split_broadcast -> split_ack_wait [label = "Write split BROADCAST"];18 split_broadcast -> split_ack_wait [label = "Write split BROADCAST"];
18 split_ack_wait -> split_broadcast [label = "Read ACK"];19 split_ack_wait -> split_broadcast [label = "Read ACK"];
19 split_broadcast -> loop [label = "All split msgs written"];20 split_broadcast -> loop [label = "All split msgs written"];
21 // other
22 loop -> conn_broken [label = "Receive connbroken request"];
23 loop -> conn_warn [label = "Receive connwarn request"];
24 conn_broken -> stop [label = "Write CONNBROKEN"];
25 conn_warn -> loop [label = "Write CONNWARN"];
26 // timeouts
27 ack_wait -> stop [label = "Elapsed exhange timeout"];
28 split_ack_wait -> stop [label = "Elapsed exhange timeout"];
29 pong_wait -> stop [label = "Elapsed exhange timeout"];
20}30}
2131
=== modified file 'protocol/state-diag-session.svg'
--- protocol/state-diag-session.svg 2014-01-16 19:37:57 +0000
+++ protocol/state-diag-session.svg 2014-05-29 12:04:32 +0000
@@ -4,139 +4,197 @@
4<!-- Generated by graphviz version 2.26.3 (20100126.1600)4<!-- Generated by graphviz version 2.26.3 (20100126.1600)
5 -->5 -->
6<!-- Title: state_diagram_session Pages: 1 -->6<!-- Title: state_diagram_session Pages: 1 -->
7<svg width="864pt" height="208pt"7<svg width="864pt" height="266pt"
8 viewBox="0.00 0.00 864.00 207.94" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">8 viewBox="0.00 0.00 864.00 265.73" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
9<g id="graph1" class="graph" transform="scale(0.435923 0.435923) rotate(0) translate(4 473)">9<g id="graph1" class="graph" transform="scale(0.367035 0.367035) rotate(0) translate(4 720)">
10<title>state_diagram_session</title>10<title>state_diagram_session</title>
11<polygon fill="white" stroke="white" points="-4,5 -4,-473 1979,-473 1979,5 -4,5"/>11<polygon fill="white" stroke="white" points="-4,5 -4,-720 2351,-720 2351,5 -4,5"/>
12<text text-anchor="middle" x="987" y="-9.4" font-family="Times Roman,serif" font-size="14.00">State diagram for session</text>12<text text-anchor="middle" x="1173" y="-9.4" font-family="Times Roman,serif" font-size="14.00">State diagram for session</text>
13<!-- stop -->
14<g id="node1" class="node"><title>stop</title>
15<ellipse fill="none" stroke="black" cx="2309" cy="-335" rx="32.0813" ry="32.5269"/>
16<ellipse fill="none" stroke="black" cx="2309" cy="-335" rx="36.0265" ry="36.5269"/>
17<text text-anchor="middle" x="2309" y="-331.4" font-family="Times Roman,serif" font-size="14.00">stop</text>
18</g>
13<!-- start1 -->19<!-- start1 -->
14<g id="node1" class="node"><title>start1</title>20<g id="node2" class="node"><title>start1</title>
15<ellipse fill="none" stroke="black" cx="42" cy="-294" rx="41.2167" ry="41.7193"/>21<ellipse fill="none" stroke="black" cx="42" cy="-395" rx="41.2167" ry="41.7193"/>
16<text text-anchor="middle" x="42" y="-290.4" font-family="Times Roman,serif" font-size="14.00">start1</text>22<text text-anchor="middle" x="42" y="-391.4" font-family="Times Roman,serif" font-size="14.00">start1</text>
17</g>23</g>
18<!-- start2 -->24<!-- start2 -->
19<g id="node3" class="node"><title>start2</title>25<g id="node4" class="node"><title>start2</title>
20<ellipse fill="none" stroke="black" cx="286" cy="-294" rx="41.2167" ry="41.7193"/>26<ellipse fill="none" stroke="black" cx="286" cy="-395" rx="41.2167" ry="41.7193"/>
21<text text-anchor="middle" x="286" y="-290.4" font-family="Times Roman,serif" font-size="14.00">start2</text>27<text text-anchor="middle" x="286" y="-391.4" font-family="Times Roman,serif" font-size="14.00">start2</text>
22</g>28</g>
23<!-- start1&#45;&gt;start2 -->29<!-- start1&#45;&gt;start2 -->
24<g id="edge2" class="edge"><title>start1&#45;&gt;start2</title>30<g id="edge2" class="edge"><title>start1&#45;&gt;start2</title>
25<path fill="none" stroke="black" d="M83.6679,-294C125.213,-294 189.13,-294 233.981,-294"/>31<path fill="none" stroke="black" d="M83.6679,-395C125.213,-395 189.13,-395 233.981,-395"/>
26<polygon fill="black" stroke="black" points="234.096,-297.5 244.096,-294 234.096,-290.5 234.096,-297.5"/>32<polygon fill="black" stroke="black" points="234.096,-398.5 244.096,-395 234.096,-391.5 234.096,-398.5"/>
27<text text-anchor="middle" x="164" y="-299.4" font-family="Times Roman,serif" font-size="14.00">Read wire version</text>33<text text-anchor="middle" x="164" y="-400.4" font-family="Times Roman,serif" font-size="14.00">Read wire version</text>
28</g>34</g>
29<!-- start3 -->35<!-- start3 -->
30<g id="node5" class="node"><title>start3</title>36<g id="node6" class="node"><title>start3</title>
31<ellipse fill="none" stroke="black" cx="516" cy="-294" rx="41.2167" ry="41.7193"/>37<ellipse fill="none" stroke="black" cx="537" cy="-395" rx="41.2167" ry="41.7193"/>
32<text text-anchor="middle" x="516" y="-290.4" font-family="Times Roman,serif" font-size="14.00">start3</text>38<text text-anchor="middle" x="537" y="-391.4" font-family="Times Roman,serif" font-size="14.00">start3</text>
33</g>39</g>
34<!-- start2&#45;&gt;start3 -->40<!-- start2&#45;&gt;start3 -->
35<g id="edge4" class="edge"><title>start2&#45;&gt;start3</title>41<g id="edge4" class="edge"><title>start2&#45;&gt;start3</title>
36<path fill="none" stroke="black" d="M327.651,-294C365.959,-294 422.903,-294 464.145,-294"/>42<path fill="none" stroke="black" d="M327.729,-395C370.886,-395 438.364,-395 484.973,-395"/>
37<polygon fill="black" stroke="black" points="464.271,-297.5 474.271,-294 464.271,-290.5 464.271,-297.5"/>43<polygon fill="black" stroke="black" points="485.171,-398.5 495.171,-395 485.171,-391.5 485.171,-398.5"/>
38<text text-anchor="middle" x="401" y="-299.4" font-family="Times Roman,serif" font-size="14.00">Read CONNECT</text>44<text text-anchor="middle" x="401" y="-400.4" font-family="Times Roman,serif" font-size="14.00">Read CONNECT</text>
39</g>45</g>
40<!-- loop -->46<!-- loop -->
41<g id="node7" class="node"><title>loop</title>47<g id="node8" class="node"><title>loop</title>
42<ellipse fill="none" stroke="black" cx="740" cy="-294" rx="31.8198" ry="31.8198"/>48<ellipse fill="none" stroke="black" cx="790" cy="-395" rx="31.8198" ry="31.8198"/>
43<text text-anchor="middle" x="740" y="-290.4" font-family="Times Roman,serif" font-size="14.00">loop</text>49<text text-anchor="middle" x="790" y="-391.4" font-family="Times Roman,serif" font-size="14.00">loop</text>
44</g>50</g>
45<!-- start3&#45;&gt;loop -->51<!-- start3&#45;&gt;loop -->
46<g id="edge6" class="edge"><title>start3&#45;&gt;loop</title>52<g id="edge6" class="edge"><title>start3&#45;&gt;loop</title>
47<path fill="none" stroke="black" d="M557.608,-294C597.53,-294 657.517,-294 697.677,-294"/>53<path fill="none" stroke="black" d="M578.778,-395C625.49,-395 700.728,-395 747.665,-395"/>
48<polygon fill="black" stroke="black" points="697.687,-297.5 707.687,-294 697.687,-290.5 697.687,-297.5"/>54<polygon fill="black" stroke="black" points="747.805,-398.5 757.805,-395 747.805,-391.5 747.805,-398.5"/>
49<text text-anchor="middle" x="633" y="-299.4" font-family="Times Roman,serif" font-size="14.00">Write CONNACK</text>55<text text-anchor="middle" x="675" y="-400.4" font-family="Times Roman,serif" font-size="14.00">Write CONNACK</text>
50</g>56</g>
51<!-- ping -->57<!-- ping -->
52<g id="node9" class="node"><title>ping</title>58<g id="node10" class="node"><title>ping</title>
53<ellipse fill="none" stroke="black" cx="1063" cy="-416" rx="32.0265" ry="32.5269"/>59<ellipse fill="none" stroke="black" cx="1135" cy="-593" rx="32.0265" ry="32.5269"/>
54<text text-anchor="middle" x="1063" y="-412.4" font-family="Times Roman,serif" font-size="14.00">ping</text>60<text text-anchor="middle" x="1135" y="-589.4" font-family="Times Roman,serif" font-size="14.00">ping</text>
55</g>61</g>
56<!-- loop&#45;&gt;ping -->62<!-- loop&#45;&gt;ping -->
57<g id="edge8" class="edge"><title>loop&#45;&gt;ping</title>63<g id="edge8" class="edge"><title>loop&#45;&gt;ping</title>
58<path fill="none" stroke="black" d="M754.564,-322.853C763.046,-336.78 775.035,-352.491 790,-362 861.597,-407.491 963.396,-415.983 1020.29,-416.829"/>64<path fill="none" stroke="black" d="M800.39,-425.317C809.609,-448.006 825.187,-478.237 848,-497 920.691,-556.785 1032.18,-579.907 1092.58,-588.403"/>
59<polygon fill="black" stroke="black" points="1020.35,-420.33 1030.38,-416.906 1020.4,-413.33 1020.35,-420.33"/>65<polygon fill="black" stroke="black" points="1092.15,-591.877 1102.53,-589.734 1093.08,-584.939 1092.15,-591.877"/>
60<text text-anchor="middle" x="881" y="-418.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval</text>66<text text-anchor="middle" x="946" y="-583.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval</text>
61</g>67</g>
62<!-- broadcast -->68<!-- broadcast -->
63<g id="node11" class="node"><title>broadcast</title>69<g id="node12" class="node"><title>broadcast</title>
64<ellipse fill="none" stroke="black" cx="1063" cy="-200" rx="58.1882" ry="58.6899"/>70<ellipse fill="none" stroke="black" cx="1135" cy="-281" rx="58.1882" ry="58.6899"/>
65<text text-anchor="middle" x="1063" y="-196.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>71<text text-anchor="middle" x="1135" y="-277.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>
66</g>72</g>
67<!-- loop&#45;&gt;broadcast -->73<!-- loop&#45;&gt;broadcast -->
68<g id="edge10" class="edge"><title>loop&#45;&gt;broadcast</title>74<g id="edge10" class="edge"><title>loop&#45;&gt;broadcast</title>
69<path fill="none" stroke="black" d="M766.046,-274.934C773.498,-270.155 781.824,-265.421 790,-262 856.828,-234.035 938.382,-217.617 994.86,-208.779"/>75<path fill="none" stroke="black" d="M811.332,-370.953C821.492,-360.892 834.388,-349.946 848,-343 917.32,-307.628 1006.03,-292.395 1066.35,-285.86"/>
70<polygon fill="black" stroke="black" points="995.396,-212.238 1004.75,-207.269 994.34,-205.318 995.396,-212.238"/>76<polygon fill="black" stroke="black" points="1066.94,-289.319 1076.53,-284.811 1066.22,-282.355 1066.94,-289.319"/>
71<text text-anchor="middle" x="881" y="-267.4" font-family="Times Roman,serif" font-size="14.00">Receive broadcast request</text>77<text text-anchor="middle" x="946" y="-348.4" font-family="Times Roman,serif" font-size="14.00">Receive broadcast request</text>
78</g>
79<!-- conn_broken -->
80<g id="node26" class="node"><title>conn_broken</title>
81<ellipse fill="none" stroke="black" cx="1361" cy="-99" rx="73.0388" ry="73.5391"/>
82<text text-anchor="middle" x="1361" y="-95.4" font-family="Times Roman,serif" font-size="14.00">conn_broken</text>
83</g>
84<!-- loop&#45;&gt;conn_broken -->
85<g id="edge28" class="edge"><title>loop&#45;&gt;conn_broken</title>
86<path fill="none" stroke="black" d="M793.216,-363.054C799.833,-304.219 817.014,-182.243 848,-155 967.196,-50.2026 1167.08,-63.6291 1278.91,-81.8408"/>
87<polygon fill="black" stroke="black" points="1278.34,-85.2954 1288.79,-83.4998 1279.5,-78.392 1278.34,-85.2954"/>
88<text text-anchor="middle" x="946" y="-160.4" font-family="Times Roman,serif" font-size="14.00">Receive connbroken request</text>
89</g>
90<!-- conn_warn -->
91<g id="node28" class="node"><title>conn_warn</title>
92<ellipse fill="none" stroke="black" cx="1135" cy="-477" rx="65.7609" ry="65.7609"/>
93<text text-anchor="middle" x="1135" y="-473.4" font-family="Times Roman,serif" font-size="14.00">conn_warn</text>
94</g>
95<!-- loop&#45;&gt;conn_warn -->
96<g id="edge30" class="edge"><title>loop&#45;&gt;conn_warn</title>
97<path fill="none" stroke="black" d="M814.092,-416.512C823.957,-424.185 835.89,-432.126 848,-437 915.942,-464.343 999.421,-473.523 1058.8,-476.355"/>
98<polygon fill="black" stroke="black" points="1058.71,-479.855 1068.85,-476.786 1059.01,-472.861 1058.71,-479.855"/>
99<text text-anchor="middle" x="946" y="-480.4" font-family="Times Roman,serif" font-size="14.00">Receive connwarn request</text>
72</g>100</g>
73<!-- pong_wait -->101<!-- pong_wait -->
74<g id="node13" class="node"><title>pong_wait</title>102<g id="node14" class="node"><title>pong_wait</title>
75<ellipse fill="none" stroke="black" cx="1526" cy="-406" rx="62.9325" ry="62.9325"/>103<ellipse fill="none" stroke="black" cx="537" cy="-653" rx="62.9325" ry="62.9325"/>
76<text text-anchor="middle" x="1526" y="-402.4" font-family="Times Roman,serif" font-size="14.00">pong_wait</text>104<text text-anchor="middle" x="537" y="-649.4" font-family="Times Roman,serif" font-size="14.00">pong_wait</text>
77</g>105</g>
78<!-- ping&#45;&gt;pong_wait -->106<!-- ping&#45;&gt;pong_wait -->
79<g id="edge12" class="edge"><title>ping&#45;&gt;pong_wait</title>107<g id="edge12" class="edge"><title>ping&#45;&gt;pong_wait</title>
80<path fill="none" stroke="black" d="M1095.56,-415.297C1169.19,-413.707 1350.04,-409.8 1452.36,-407.591"/>108<path fill="none" stroke="black" d="M1103.4,-600.831C1035.81,-617.134 871.913,-654.261 732,-667 681.542,-671.594 668.481,-671.33 618,-667 615.134,-666.754 612.217,-666.46 609.275,-666.127"/>
81<polygon fill="black" stroke="black" points="1452.69,-411.084 1462.61,-407.369 1452.54,-404.086 1452.69,-411.084"/>109<polygon fill="black" stroke="black" points="609.573,-662.637 599.214,-664.858 608.697,-669.582 609.573,-662.637"/>
82<text text-anchor="middle" x="1289" y="-418.4" font-family="Times Roman,serif" font-size="14.00">Write PING</text>110<text text-anchor="middle" x="790" y="-670.4" font-family="Times Roman,serif" font-size="14.00">Write PING</text>
83</g>111</g>
84<!-- ack_wait -->112<!-- ack_wait -->
85<g id="node15" class="node"><title>ack_wait</title>113<g id="node16" class="node"><title>ack_wait</title>
86<ellipse fill="none" stroke="black" cx="1526" cy="-269" rx="55.1543" ry="55.1543"/>114<ellipse fill="none" stroke="black" cx="1598" cy="-373" rx="55.1543" ry="55.1543"/>
87<text text-anchor="middle" x="1526" y="-265.4" font-family="Times Roman,serif" font-size="14.00">ack_wait</text>115<text text-anchor="middle" x="1598" y="-369.4" font-family="Times Roman,serif" font-size="14.00">ack_wait</text>
88</g>116</g>
89<!-- broadcast&#45;&gt;ack_wait -->117<!-- broadcast&#45;&gt;ack_wait -->
90<g id="edge14" class="edge"><title>broadcast&#45;&gt;ack_wait</title>118<g id="edge14" class="edge"><title>broadcast&#45;&gt;ack_wait</title>
91<path fill="none" stroke="black" d="M1121.17,-208.669C1207.93,-221.599 1370.7,-245.856 1461.17,-259.339"/>119<path fill="none" stroke="black" d="M1193.25,-288.88C1264.95,-299.067 1390.23,-318.45 1496,-343 1508.9,-345.993 1522.57,-349.664 1535.58,-353.397"/>
92<polygon fill="black" stroke="black" points="1460.9,-262.837 1471.3,-260.849 1461.93,-255.913 1460.9,-262.837"/>120<polygon fill="black" stroke="black" points="1534.79,-356.813 1545.37,-356.254 1536.75,-350.093 1534.79,-356.813"/>
93<text text-anchor="middle" x="1289" y="-257.4" font-family="Times Roman,serif" font-size="14.00">Write BROADCAST [fits one wire msg]</text>121<text text-anchor="middle" x="1361" y="-348.4" font-family="Times Roman,serif" font-size="14.00">Write BROADCAST [fits one wire msg]</text>
94</g>122</g>
95<!-- split_broadcast -->123<!-- split_broadcast -->
96<g id="node17" class="node"><title>split_broadcast</title>124<g id="node18" class="node"><title>split_broadcast</title>
97<ellipse fill="none" stroke="black" cx="1526" cy="-110" rx="84.1457" ry="84.1457"/>125<ellipse fill="none" stroke="black" cx="1598" cy="-216" rx="84.1457" ry="84.1457"/>
98<text text-anchor="middle" x="1526" y="-106.4" font-family="Times Roman,serif" font-size="14.00">split_broadcast</text>126<text text-anchor="middle" x="1598" y="-212.4" font-family="Times Roman,serif" font-size="14.00">split_broadcast</text>
99</g>127</g>
100<!-- broadcast&#45;&gt;split_broadcast -->128<!-- broadcast&#45;&gt;split_broadcast -->
101<g id="edge16" class="edge"><title>broadcast&#45;&gt;split_broadcast</title>129<g id="edge16" class="edge"><title>broadcast&#45;&gt;split_broadcast</title>
102<path fill="none" stroke="black" d="M1120.7,-188.783C1199.06,-173.553 1340.01,-146.154 1433.29,-128.021"/>130<path fill="none" stroke="black" d="M1193.17,-272.834C1271.44,-261.846 1411.56,-242.174 1504.66,-229.104"/>
103<polygon fill="black" stroke="black" points="1434.15,-131.421 1443.29,-126.077 1432.81,-124.549 1434.15,-131.421"/>131<polygon fill="black" stroke="black" points="1505.23,-232.558 1514.65,-227.702 1504.26,-225.626 1505.23,-232.558"/>
104<text text-anchor="middle" x="1289" y="-185.4" font-family="Times Roman,serif" font-size="14.00">BROADCAST does not fit one wire msg</text>132<text text-anchor="middle" x="1361" y="-272.4" font-family="Times Roman,serif" font-size="14.00">BROADCAST does not fit one wire msg</text>
133</g>
134<!-- pong_wait&#45;&gt;stop -->
135<g id="edge40" class="edge"><title>pong_wait&#45;&gt;stop</title>
136<path fill="none" stroke="black" d="M600.164,-653C651.344,-653 725.322,-653 790,-653 790,-653 790,-653 1972,-653 2131.11,-653 2245.53,-463.168 2289.33,-376.844"/>
137<polygon fill="black" stroke="black" points="2292.5,-378.343 2293.84,-367.834 2286.24,-375.212 2292.5,-378.343"/>
138<text text-anchor="middle" x="1361" y="-658.4" font-family="Times Roman,serif" font-size="14.00">Elapsed exhange timeout</text>
105</g>139</g>
106<!-- pong_wait&#45;&gt;loop -->140<!-- pong_wait&#45;&gt;loop -->
107<g id="edge18" class="edge"><title>pong_wait&#45;&gt;loop</title>141<g id="edge18" class="edge"><title>pong_wait&#45;&gt;loop</title>
108<path fill="none" stroke="black" d="M1463.29,-398.59C1336,-383.27 1038.34,-346.004 790,-304 787.177,-303.523 784.269,-303.006 781.343,-302.468"/>142<path fill="none" stroke="black" d="M581.359,-607.765C632.774,-555.333 716.085,-470.376 760.273,-425.314"/>
109<polygon fill="black" stroke="black" points="781.898,-299.011 771.42,-300.582 780.59,-305.888 781.898,-299.011"/>143<polygon fill="black" stroke="black" points="762.774,-427.763 767.277,-418.172 757.776,-422.862 762.774,-427.763"/>
110<text text-anchor="middle" x="1063" y="-359.4" font-family="Times Roman,serif" font-size="14.00">Read PONG</text>144<text text-anchor="middle" x="675" y="-574.4" font-family="Times Roman,serif" font-size="14.00">Read PONG</text>
145</g>
146<!-- ack_wait&#45;&gt;stop -->
147<g id="edge36" class="edge"><title>ack_wait&#45;&gt;stop</title>
148<path fill="none" stroke="black" d="M1653.02,-372.757C1765.96,-371.776 2032.03,-366.986 2254,-344 2256.89,-343.701 2259.85,-343.348 2262.84,-342.959"/>
149<polygon fill="black" stroke="black" points="2263.58,-346.389 2272.99,-341.517 2262.59,-339.459 2263.58,-346.389"/>
150<text text-anchor="middle" x="1972" y="-373.4" font-family="Times Roman,serif" font-size="14.00">Elapsed exhange timeout</text>
111</g>151</g>
112<!-- ack_wait&#45;&gt;loop -->152<!-- ack_wait&#45;&gt;loop -->
113<g id="edge20" class="edge"><title>ack_wait&#45;&gt;loop</title>153<g id="edge20" class="edge"><title>ack_wait&#45;&gt;loop</title>
114<path fill="none" stroke="black" d="M1470.96,-271.898C1455.75,-272.644 1439.24,-273.404 1424,-274 1181.02,-283.507 889.323,-290.597 782.144,-293.057"/>154<path fill="none" stroke="black" d="M1542.83,-374.081C1445.66,-375.995 1237.64,-380.146 1062,-384 966.886,-386.087 942.947,-382.989 848,-389 842.829,-389.327 837.406,-389.764 832.04,-390.252"/>
115<polygon fill="black" stroke="black" points="781.977,-289.56 772.059,-293.288 782.136,-296.558 781.977,-289.56"/>155<polygon fill="black" stroke="black" points="831.485,-386.79 821.871,-391.242 832.163,-393.757 831.485,-386.79"/>
116<text text-anchor="middle" x="1063" y="-292.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>156<text text-anchor="middle" x="1135" y="-389.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>
117</g>157</g>
118<!-- split_broadcast&#45;&gt;loop -->158<!-- split_broadcast&#45;&gt;loop -->
119<g id="edge26" class="edge"><title>split_broadcast&#45;&gt;loop</title>159<g id="edge26" class="edge"><title>split_broadcast&#45;&gt;loop</title>
120<path fill="none" stroke="black" d="M1442.56,-99.9981C1336.06,-89.6718 1146.8,-79.5577 990,-115 894.383,-136.612 862.41,-141.921 790,-208 775.817,-220.943 764.522,-238.865 756.283,-254.999"/>160<path fill="none" stroke="black" d="M1515.33,-201.109C1409.22,-184.681 1219.98,-164.458 1062,-196 960.985,-216.168 924.079,-215.554 848,-285 827.351,-303.849 812.751,-331.776 803.364,-354.774"/>
121<polygon fill="black" stroke="black" points="753.014,-253.718 751.785,-264.241 759.308,-256.781 753.014,-253.718"/>161<polygon fill="black" stroke="black" points="800.027,-353.699 799.658,-364.287 806.549,-356.24 800.027,-353.699"/>
122<text text-anchor="middle" x="1063" y="-120.4" font-family="Times Roman,serif" font-size="14.00">All split msgs written</text>162<text text-anchor="middle" x="1135" y="-201.4" font-family="Times Roman,serif" font-size="14.00">All split msgs written</text>
123</g>163</g>
124<!-- split_ack_wait -->164<!-- split_ack_wait -->
125<g id="node21" class="node"><title>split_ack_wait</title>165<g id="node22" class="node"><title>split_ack_wait</title>
126<ellipse fill="none" stroke="black" cx="1893" cy="-110" rx="80.1095" ry="80.6102"/>166<ellipse fill="none" stroke="black" cx="1972" cy="-257" rx="80.1095" ry="80.6102"/>
127<text text-anchor="middle" x="1893" y="-106.4" font-family="Times Roman,serif" font-size="14.00">split_ack_wait</text>167<text text-anchor="middle" x="1972" y="-253.4" font-family="Times Roman,serif" font-size="14.00">split_ack_wait</text>
128</g>168</g>
129<!-- split_broadcast&#45;&gt;split_ack_wait -->169<!-- split_broadcast&#45;&gt;split_ack_wait -->
130<g id="edge22" class="edge"><title>split_broadcast&#45;&gt;split_ack_wait</title>170<g id="edge22" class="edge"><title>split_broadcast&#45;&gt;split_ack_wait</title>
131<path fill="none" stroke="black" d="M1610.2,-110C1667.61,-110 1743.59,-110 1802.33,-110"/>171<path fill="none" stroke="black" d="M1680.55,-232.126C1687.12,-233.18 1693.66,-234.156 1700,-235 1760.22,-243.022 1828.36,-248.528 1881.38,-252.025"/>
132<polygon fill="black" stroke="black" points="1802.35,-113.5 1812.35,-110 1802.34,-106.5 1802.35,-113.5"/>172<polygon fill="black" stroke="black" points="1881.23,-255.523 1891.43,-252.676 1881.68,-248.537 1881.23,-255.523"/>
133<text text-anchor="middle" x="1711" y="-115.4" font-family="Times Roman,serif" font-size="14.00">Write split BROADCAST</text>173<text text-anchor="middle" x="1783" y="-256.4" font-family="Times Roman,serif" font-size="14.00">Write split BROADCAST</text>
174</g>
175<!-- split_ack_wait&#45;&gt;stop -->
176<g id="edge38" class="edge"><title>split_ack_wait&#45;&gt;stop</title>
177<path fill="none" stroke="black" d="M2050.59,-275.189C2116.55,-290.456 2208.51,-311.74 2263.08,-324.372"/>
178<polygon fill="black" stroke="black" points="2262.62,-327.857 2273.15,-326.702 2264.2,-321.037 2262.62,-327.857"/>
179<text text-anchor="middle" x="2166" y="-327.4" font-family="Times Roman,serif" font-size="14.00">Elapsed exhange timeout</text>
134</g>180</g>
135<!-- split_ack_wait&#45;&gt;split_broadcast -->181<!-- split_ack_wait&#45;&gt;split_broadcast -->
136<g id="edge24" class="edge"><title>split_ack_wait&#45;&gt;split_broadcast</title>182<g id="edge24" class="edge"><title>split_ack_wait&#45;&gt;split_broadcast</title>
137<path fill="none" stroke="black" d="M1814.64,-90.9448C1807.69,-89.7544 1800.74,-88.7397 1794,-88 1720.66,-79.9496 1701.36,-80.1783 1628,-88 1624.71,-88.3505 1621.38,-88.7628 1618.01,-89.2264"/>183<path fill="none" stroke="black" d="M1899.33,-222.493C1888.37,-218.573 1877.04,-215.2 1866,-213 1808.89,-201.617 1743.56,-202.081 1691.71,-205.529"/>
138<polygon fill="black" stroke="black" points="1617.23,-85.8043 1607.87,-90.7602 1618.28,-92.7256 1617.23,-85.8043"/>184<polygon fill="black" stroke="black" points="1691.25,-202.053 1681.52,-206.256 1691.74,-209.035 1691.25,-202.053"/>
139<text text-anchor="middle" x="1711" y="-93.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>185<text text-anchor="middle" x="1783" y="-218.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>
186</g>
187<!-- conn_broken&#45;&gt;stop -->
188<g id="edge32" class="edge"><title>conn_broken&#45;&gt;stop</title>
189<path fill="none" stroke="black" d="M1434.61,-95.8477C1564.45,-92.5456 1841.16,-95.6985 2060,-168 2154.32,-199.163 2175.34,-218.326 2254,-279 2262.17,-285.305 2270.33,-292.782 2277.75,-300.185"/>
190<polygon fill="black" stroke="black" points="2275.42,-302.808 2284.91,-307.517 2280.43,-297.917 2275.42,-302.808"/>
191<text text-anchor="middle" x="1783" y="-127.4" font-family="Times Roman,serif" font-size="14.00">Write CONNBROKEN</text>
192</g>
193<!-- conn_warn&#45;&gt;loop -->
194<g id="edge34" class="edge"><title>conn_warn&#45;&gt;loop</title>
195<path fill="none" stroke="black" d="M1083.63,-435.301C1071.29,-427.246 1057.71,-419.822 1044,-415 972.758,-389.933 883.562,-389.406 832.05,-391.836"/>
196<polygon fill="black" stroke="black" points="831.758,-388.346 821.959,-392.373 832.131,-395.337 831.758,-388.346"/>
197<text text-anchor="middle" x="946" y="-420.4" font-family="Times Roman,serif" font-size="14.00">Write CONNWARN</text>
140</g>198</g>
141</g>199</g>
142</svg>200</svg>
143201
=== added file 'scripts/broadcast'
--- scripts/broadcast 1970-01-01 00:00:00 +0000
+++ scripts/broadcast 2014-05-29 12:04:32 +0000
@@ -0,0 +1,59 @@
1#!/usr/bin/python
2"""
3send broadcast to channel with payload data
4"""
5import argparse
6import json
7import requests
8import requests.auth
9import datetime
10import sys
11
12
13def main():
14 parser = argparse.ArgumentParser(description=__doc__)
15 parser.add_argument('channel', nargs=1)
16 parser.add_argument('data', nargs=1)
17 parser.add_argument('-H', '--host',
18 help="host:port (default: %(default)s)",
19 default="localhost:8080")
20 parser.add_argument('-e', '--expire',
21 help="expire after the given amount of time, "
22 "use 'd' suffix for days, 's' for seconds"
23 " (default: %(default)s)", default="1d")
24 parser.add_argument('--no-https', action='store_true', default=False)
25 parser.add_argument('--insecure', action='store_true', default=False,
26 help="don't check host/certs with https")
27 parser.add_argument('-u', '--user', default="")
28 parser.add_argument('-p', '--password', default="")
29 args = parser.parse_args()
30 expire_on = datetime.datetime.utcnow()
31 ex = args.expire
32 if ex.endswith('d'):
33 delta = datetime.timedelta(days=int(ex[:-1]))
34 elif ex.endswith('s'):
35 delta = datetime.timedelta(seconds=int(ex[:-1]))
36 else:
37 print >>sys.stderr, "unknown --expire suffix:", ex
38 sys.exit(1)
39 expire_on += delta
40 scheme = 'https'
41 if args.no_https:
42 scheme = 'http'
43 url = "%s://%s/broadcast" % (scheme, args.host)
44 body = {
45 'channel': args.channel[0],
46 'data': json.loads(args.data[0]),
47 'expire_on': expire_on.replace(microsecond=0).isoformat()+"Z"
48 }
49 xauth = {}
50 if args.user and args.password:
51 xauth = {'auth': requests.auth.HTTPBasicAuth(args.user, args.password)}
52 headers = {'Content-Type': 'application/json'}
53 r = requests.post(url, data=json.dumps(body), headers=headers,
54 verify=not args.insecure, **xauth)
55 print r.status_code
56 print r.text
57
58if __name__ == '__main__':
59 main()
060
=== added file 'scripts/deps.sh'
--- scripts/deps.sh 1970-01-01 00:00:00 +0000
+++ scripts/deps.sh 2014-05-29 12:04:32 +0000
@@ -0,0 +1,28 @@
1#!/bin/sh
2set -eu
3
4PROJECT=launchpad.net/ubuntu-push
5
6mktpl () {
7 for f in GoFiles CgoFiles; do
8 echo '{{join .'$f' "\\n"}}'
9 done
10}
11
12directs () {
13 go list -f "$(mktpl)" $1 | sed -e "s|^|$1|"
14}
15
16indirects () {
17 for i in $(go list -f '{{join .Deps "\n"}}' $1 | grep ^$PROJECT ); do
18 directs $i/
19 done
20 wait
21}
22
23norm () {
24 tr "\n" " " | sed -r -e "s|$PROJECT/?||g" -e 's/ *$//'
25}
26
27out=".$1.deps"
28( echo -n "${1%.go} ${out}: "; indirects $(echo $1 | norm) | norm ) > "$out"
029
=== added file 'scripts/unicast'
--- scripts/unicast 1970-01-01 00:00:00 +0000
+++ scripts/unicast 2014-05-29 12:04:32 +0000
@@ -0,0 +1,65 @@
1#!/usr/bin/python
2"""
3send broadcast to channel with payload data
4"""
5import argparse
6import json
7import requests
8import requests.auth
9import datetime
10import sys
11
12
13def main():
14 parser = argparse.ArgumentParser(description=__doc__)
15 parser.add_argument('reg', nargs=1) # userid:deviceid or reg
16 parser.add_argument('appid', nargs=1)
17 parser.add_argument('data', nargs=1)
18 parser.add_argument('-H', '--host',
19 help="host:port (default: %(default)s)",
20 default="localhost:8080")
21 parser.add_argument('-e', '--expire',
22 help="expire after the given amount of time, "
23 "use 'd' suffix for days, 's' for seconds"
24 " (default: %(default)s)", default="1d")
25 parser.add_argument('--no-https', action='store_true', default=False)
26 parser.add_argument('--insecure', action='store_true', default=False,
27 help="don't check host/certs with https")
28 parser.add_argument('-u', '--user', default="")
29 parser.add_argument('-p', '--password', default="")
30 args = parser.parse_args()
31 expire_on = datetime.datetime.utcnow()
32 ex = args.expire
33 if ex.endswith('d'):
34 delta = datetime.timedelta(days=int(ex[:-1]))
35 elif ex.endswith('s'):
36 delta = datetime.timedelta(seconds=int(ex[:-1]))
37 else:
38 print >>sys.stderr, "unknown --expire suffix:", ex
39 sys.exit(1)
40 expire_on += delta
41 scheme = 'https'
42 if args.no_https:
43 scheme = 'http'
44 url = "%s://%s/notify" % (scheme, args.host)
45 body = {
46 'appid': args.appid[0],
47 'data': json.loads(args.data[0]),
48 'expire_on': expire_on.replace(microsecond=0).isoformat()+"Z"
49 }
50 reg = args.reg[0]
51 if ':' in reg:
52 userid, devid = reg.split(':', 1)
53 body['userid'] = userid
54 body['deviceid'] = devid
55 xauth = {}
56 if args.user and args.password:
57 xauth = {'auth': requests.auth.HTTPBasicAuth(args.user, args.password)}
58 headers = {'Content-Type': 'application/json'}
59 r = requests.post(url, data=json.dumps(body), headers=headers,
60 verify=not args.insecure, **xauth)
61 print r.status_code
62 print r.text
63
64if __name__ == '__main__':
65 main()
066
=== modified file 'server/acceptance/acceptance_test.go'
--- server/acceptance/acceptance_test.go 2014-04-07 19:39:19 +0000
+++ server/acceptance/acceptance_test.go 2014-05-29 12:04:32 +0000
@@ -59,3 +59,6 @@
5959
60// broadcast60// broadcast
61var _ = Suite(&suites.BroadcastAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}})61var _ = Suite(&suites.BroadcastAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}})
62
63// unicast
64var _ = Suite(&suites.UnicastAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}, nil})
6265
=== modified file 'server/acceptance/acceptanceclient.go'
--- server/acceptance/acceptanceclient.go 2014-04-09 19:30:53 +0000
+++ server/acceptance/acceptanceclient.go 2014-05-29 12:04:32 +0000
@@ -24,6 +24,7 @@
24 "errors"24 "errors"
25 "fmt"25 "fmt"
26 "net"26 "net"
27 "strings"
27 "time"28 "time"
2829
29 "launchpad.net/ubuntu-push/protocol"30 "launchpad.net/ubuntu-push/protocol"
@@ -44,6 +45,7 @@
44 Levels map[string]int6445 Levels map[string]int64
45 Insecure bool // don't verify certs46 Insecure bool // don't verify certs
46 Prefix string // prefix for events47 Prefix string // prefix for events
48 Auth string
47 // connection49 // connection
48 Connection net.Conn50 Connection net.Conn
49}51}
@@ -73,6 +75,7 @@
73 Type string `json:"T"`75 Type string `json:"T"`
74 protocol.BroadcastMsg76 protocol.BroadcastMsg
75 protocol.NotificationsMsg77 protocol.NotificationsMsg
78 protocol.ConnWarnMsg
76}79}
7780
78// Run the session with the server, emits a stream of events.81// Run the session with the server, emits a stream of events.
@@ -93,6 +96,7 @@
93 "device": sess.Model,96 "device": sess.Model,
94 "channel": sess.ImageChannel,97 "channel": sess.ImageChannel,
95 },98 },
99 Authorization: sess.Auth,
96 })100 })
97 if err != nil {101 if err != nil {
98 return err102 return err
@@ -125,9 +129,24 @@
125 if sess.ReportPings {129 if sess.ReportPings {
126 events <- sess.Prefix + "ping"130 events <- sess.Prefix + "ping"
127 }131 }
132 case "notifications":
133 conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
134 err := proto.WriteMessage(protocol.AckMsg{Type: "ack"})
135 if err != nil {
136 return err
137 }
138 parts := make([]string, len(recv.Notifications))
139 for i, notif := range recv.Notifications {
140 pack, err := json.Marshal(&notif.Payload)
141 if err != nil {
142 return err
143 }
144 parts[i] = fmt.Sprintf("app:%v payload:%s;", notif.AppId, pack)
145 }
146 events <- fmt.Sprintf("%sunicast %s", sess.Prefix, strings.Join(parts, " "))
128 case "broadcast":147 case "broadcast":
129 conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))148 conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
130 err := proto.WriteMessage(protocol.PingPongMsg{Type: "ack"})149 err := proto.WriteMessage(protocol.AckMsg{Type: "ack"})
131 if err != nil {150 if err != nil {
132 return err151 return err
133 }152 }
@@ -136,6 +155,8 @@
136 return err155 return err
137 }156 }
138 events <- fmt.Sprintf("%sbroadcast chan:%v app:%v topLevel:%d payloads:%s", sess.Prefix, recv.ChanId, recv.AppId, recv.TopLevel, pack)157 events <- fmt.Sprintf("%sbroadcast chan:%v app:%v topLevel:%d payloads:%s", sess.Prefix, recv.ChanId, recv.AppId, recv.TopLevel, pack)
158 case "connwarn":
159 events <- fmt.Sprintf("%sconnwarn %s", sess.Prefix, recv.Reason)
139 }160 }
140 }161 }
141 return nil162 return nil
142163
=== modified file 'server/acceptance/cmd/acceptanceclient.go'
--- server/acceptance/cmd/acceptanceclient.go 2014-04-14 14:54:14 +0000
+++ server/acceptance/cmd/acceptanceclient.go 2014-05-29 12:04:32 +0000
@@ -22,7 +22,9 @@
22 "fmt"22 "fmt"
23 "log"23 "log"
24 "os"24 "os"
25 "os/exec"
25 "path/filepath"26 "path/filepath"
27 "strings"
2628
27 "launchpad.net/ubuntu-push/config"29 "launchpad.net/ubuntu-push/config"
28 "launchpad.net/ubuntu-push/server/acceptance"30 "launchpad.net/ubuntu-push/server/acceptance"
@@ -40,12 +42,13 @@
40 ExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"`42 ExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"`
41 // server connection config43 // server connection config
42 Addr config.ConfigHostPort44 Addr config.ConfigHostPort
43 CertPEMFile string `json:"cert_pem_file"`45 CertPEMFile string `json:"cert_pem_file"`
46 AuthHelper []string `json:"auth_helper"`
44}47}
4548
46func main() {49func main() {
47 flag.Usage = func() {50 flag.Usage = func() {
48 fmt.Fprintf(os.Stderr, "Usage: acceptancclient [options] <config.json> <device id>\n")51 fmt.Fprintf(os.Stderr, "Usage: acceptancclient [options] <device id>\n")
49 flag.PrintDefaults()52 flag.PrintDefaults()
50 }53 }
51 missingArg := func(what string) {54 missingArg := func(what string) {
@@ -53,28 +56,24 @@
53 flag.Usage()56 flag.Usage()
54 os.Exit(2)57 os.Exit(2)
55 }58 }
56 flag.Parse()59 cfg := &configuration{}
60 err := config.ReadFilesDefaults(cfg, map[string]interface{}{
61 "exchange_timeout": "5s",
62 "cert_pem_file": "",
63 "auth_helper": []string{},
64 }, "<flags>")
65 if err != nil {
66 log.Fatalf("reading config: %v", err)
67 }
57 narg := flag.NArg()68 narg := flag.NArg()
58 switch {69 switch {
59 case narg < 1:70 case narg < 1:
60 missingArg("config file")
61 case narg < 2:
62 missingArg("device-id")71 missingArg("device-id")
63 }72 }
64 configFName := flag.Arg(0)
65 f, err := os.Open(configFName)
66 if err != nil {
67 log.Fatalf("reading config: %v", err)
68 }
69 cfg := &configuration{}
70 err = config.ReadConfig(f, cfg)
71 if err != nil {
72 log.Fatalf("reading config: %v", err)
73 }
74 session := &acceptance.ClientSession{73 session := &acceptance.ClientSession{
75 ExchangeTimeout: cfg.ExchangeTimeout.TimeDuration(),74 ExchangeTimeout: cfg.ExchangeTimeout.TimeDuration(),
76 ServerAddr: cfg.Addr.HostPort(),75 ServerAddr: cfg.Addr.HostPort(),
77 DeviceId: flag.Arg(1),76 DeviceId: flag.Arg(0),
78 // flags77 // flags
79 Model: *deviceModel,78 Model: *deviceModel,
80 ImageChannel: *imageChannel,79 ImageChannel: *imageChannel,
@@ -82,12 +81,21 @@
82 Insecure: *insecureFlag,81 Insecure: *insecureFlag,
83 }82 }
84 log.Printf("with: %#v", session)83 log.Printf("with: %#v", session)
85 if !*insecureFlag {84 if !*insecureFlag && cfg.CertPEMFile != "" {
86 session.CertPEMBlock, err = config.LoadFile(cfg.CertPEMFile, filepath.Dir(configFName))85 cfgDir := filepath.Dir(flag.Lookup("cfg@").Value.String())
86 log.Printf("cert: %v relToDir: %v", cfg.CertPEMFile, cfgDir)
87 session.CertPEMBlock, err = config.LoadFile(cfg.CertPEMFile, cfgDir)
87 if err != nil {88 if err != nil {
88 log.Fatalf("reading CertPEMFile: %v", err)89 log.Fatalf("reading CertPEMFile: %v", err)
89 }90 }
90 }91 }
92 if len(cfg.AuthHelper) != 0 {
93 auth, err := exec.Command(cfg.AuthHelper[0], cfg.AuthHelper[1:]...).Output()
94 if err != nil {
95 log.Fatalf("auth helper: %v", err)
96 }
97 session.Auth = strings.TrimSpace(string(auth))
98 }
91 err = session.Dial()99 err = session.Dial()
92 if err != nil {100 if err != nil {
93 log.Fatalln(err)101 log.Fatalln(err)
94102
=== modified file 'server/acceptance/suites/broadcast.go'
--- server/acceptance/suites/broadcast.go 2014-04-07 19:39:19 +0000
+++ server/acceptance/suites/broadcast.go 2014-05-29 12:04:32 +0000
@@ -29,14 +29,11 @@
29 "launchpad.net/ubuntu-push/server/api"29 "launchpad.net/ubuntu-push/server/api"
30)30)
3131
32// BroadCastAcceptanceSuite has tests about broadcast.32// BroadcastAcceptanceSuite has tests about broadcast.
33type BroadcastAcceptanceSuite struct {33type BroadcastAcceptanceSuite struct {
34 AcceptanceSuite34 AcceptanceSuite
35}35}
3636
37// Long after the end of the tests.
38var future = time.Now().Add(9 * time.Hour).Format(time.RFC3339)
39
40func (s *BroadcastAcceptanceSuite) TestBroadcastToConnected(c *C) {37func (s *BroadcastAcceptanceSuite) TestBroadcastToConnected(c *C) {
41 events, errCh, stop := s.StartClient(c, "DEVB", nil)38 events, errCh, stop := s.StartClient(c, "DEVB", nil)
42 got, err := s.PostRequest("/broadcast", &api.Broadcast{39 got, err := s.PostRequest("/broadcast", &api.Broadcast{
@@ -91,7 +88,7 @@
91 c.Check(len(errCh), Equals, 0)88 c.Check(len(errCh), Equals, 0)
92}89}
9390
94func (s *BroadcastAcceptanceSuite) TestBroadcasLargeNeedsSplitting(c *C) {91func (s *BroadcastAcceptanceSuite) TestBroadcastLargeNeedsSplitting(c *C) {
95 // send bunch of broadcasts that will be pending92 // send bunch of broadcasts that will be pending
96 payloadFmt := fmt.Sprintf(`{"img1/m1":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))93 payloadFmt := fmt.Sprintf(`{"img1/m1":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))
97 for i := 0; i < 32; i++ {94 for i := 0; i < 32; i++ {
@@ -106,8 +103,17 @@
106103
107 events, errCh, stop := s.StartClient(c, "DEVC", nil)104 events, errCh, stop := s.StartClient(c, "DEVC", nil)
108 // gettting pending on connect105 // gettting pending on connect
109 c.Check(NextEvent(events, errCh), Matches, `broadcast chan:0 app: topLevel:30 payloads:\[{"img1/m1":0,.*`)106 n := 0
110 c.Check(NextEvent(events, errCh), Matches, `broadcast chan:0 app: topLevel:32 payloads:\[.*`)107 for {
108 evt := NextEvent(events, errCh)
109 c.Check(evt, Matches, "broadcast chan:0 .*")
110 n += 1
111 if strings.Contains(evt, "topLevel:32") {
112 break
113 }
114 }
115 // was split
116 c.Check(n > 1, Equals, true)
111 stop()117 stop()
112 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)118 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
113 c.Check(len(errCh), Equals, 0)119 c.Check(len(errCh), Equals, 0)
@@ -265,7 +271,11 @@
265271
266func (s *BroadcastAcceptanceSuite) TestGetHosts(c *C) {272func (s *BroadcastAcceptanceSuite) TestGetHosts(c *C) {
267 gh := gethosts.New("", s.ServerAPIURL+"/delivery-hosts", 2*time.Second)273 gh := gethosts.New("", s.ServerAPIURL+"/delivery-hosts", 2*time.Second)
268 hosts, err := gh.Get()274 host, err := gh.Get()
269 c.Assert(err, IsNil)275 c.Assert(err, IsNil)
270 c.Check(hosts, DeepEquals, []string{s.ServerAddr})276 expected := &gethosts.Host{
277 Domain: "localhost",
278 Hosts: []string{s.ServerAddr},
279 }
280 c.Check(host, DeepEquals, expected)
271}281}
272282
=== modified file 'server/acceptance/suites/suite.go'
--- server/acceptance/suites/suite.go 2014-04-03 16:47:47 +0000
+++ server/acceptance/suites/suite.go 2014-05-29 12:04:32 +0000
@@ -44,10 +44,19 @@
4444
45// Start a client.45// Start a client.
46func (h *ServerHandle) StartClient(c *C, devId string, levels map[string]int64) (events <-chan string, errorCh <-chan error, stop func()) {46func (h *ServerHandle) StartClient(c *C, devId string, levels map[string]int64) (events <-chan string, errorCh <-chan error, stop func()) {
47 return h.StartClientAuth(c, devId, levels, "")
48}
49
50// Start a client with auth.
51func (h *ServerHandle) StartClientAuth(c *C, devId string, levels map[string]int64, auth string) (events <-chan string, errorCh <-chan error, stop func()) {
47 errCh := make(chan error, 1)52 errCh := make(chan error, 1)
48 cliEvents := make(chan string, 10)53 cliEvents := make(chan string, 10)
49 sess := testClientSession(h.ServerAddr, devId, "m1", "img1", false)54 sess := testClientSession(h.ServerAddr, devId, "m1", "img1", false)
50 sess.Levels = levels55 sess.Levels = levels
56 sess.Auth = auth
57 if auth != "" {
58 sess.ExchangeTimeout = 5 * time.Second
59 }
51 err := sess.Dial()60 err := sess.Dial()
52 c.Assert(err, IsNil)61 c.Assert(err, IsNil)
53 clientShutdown := make(chan bool, 1) // abused as an atomic flag62 clientShutdown := make(chan bool, 1) // abused as an atomic flag
@@ -186,3 +195,6 @@
186 }195 }
187 return196 return
188}197}
198
199// Long after the end of the tests.
200var future = time.Now().Add(9 * time.Hour).Format(time.RFC3339)
189201
=== added file 'server/acceptance/suites/unicast.go'
--- server/acceptance/suites/unicast.go 1970-01-01 00:00:00 +0000
+++ server/acceptance/suites/unicast.go 2014-05-29 12:04:32 +0000
@@ -0,0 +1,149 @@
1/*
2 Copyright 2013-2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package suites
18
19import (
20 "encoding/json"
21 "fmt"
22 "strings"
23
24 . "launchpad.net/gocheck"
25
26 "launchpad.net/ubuntu-push/server/api"
27)
28
29// UnicastAcceptanceSuite has tests about unicast.
30type UnicastAcceptanceSuite struct {
31 AcceptanceSuite
32 AssociatedAuth func(string) (string, string)
33}
34
35func (s *UnicastAcceptanceSuite) associatedAuth(deviceId string) (userId string, auth string) {
36 if s.AssociatedAuth != nil {
37 return s.AssociatedAuth(deviceId)
38 }
39 return deviceId, ""
40}
41
42func (s *UnicastAcceptanceSuite) TestUnicastToConnected(c *C) {
43 userId, auth := s.associatedAuth("DEV1")
44 events, errCh, stop := s.StartClientAuth(c, "DEV1", nil, auth)
45 got, err := s.PostRequest("/notify", &api.Unicast{
46 UserId: userId,
47 DeviceId: "DEV1",
48 AppId: "app1",
49 ExpireOn: future,
50 Data: json.RawMessage(`{"a": 42}`),
51 })
52 c.Assert(err, IsNil)
53 c.Assert(got, Matches, ".*ok.*")
54 c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"a":42};`)
55 stop()
56 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
57 c.Check(len(errCh), Equals, 0)
58}
59
60func (s *UnicastAcceptanceSuite) TestUnicastCorrectDistribution(c *C) {
61 userId1, auth1 := s.associatedAuth("DEV1")
62 userId2, auth2 := s.associatedAuth("DEV2")
63 // start 1st client
64 events1, errCh1, stop1 := s.StartClientAuth(c, "DEV1", nil, auth1)
65 // start 2nd client
66 events2, errCh2, stop2 := s.StartClientAuth(c, "DEV2", nil, auth2)
67 // unicast to one and the other
68 got, err := s.PostRequest("/notify", &api.Unicast{
69 UserId: userId1,
70 DeviceId: "DEV1",
71 AppId: "app1",
72 ExpireOn: future,
73 Data: json.RawMessage(`{"to": 1}`),
74 })
75 c.Assert(err, IsNil)
76 c.Assert(got, Matches, ".*ok.*")
77 got, err = s.PostRequest("/notify", &api.Unicast{
78 UserId: userId2,
79 DeviceId: "DEV2",
80 AppId: "app1",
81 ExpireOn: future,
82 Data: json.RawMessage(`{"to": 2}`),
83 })
84 c.Assert(err, IsNil)
85 c.Assert(got, Matches, ".*ok.*")
86 c.Check(NextEvent(events1, errCh1), Equals, `unicast app:app1 payload:{"to":1};`)
87 c.Check(NextEvent(events2, errCh2), Equals, `unicast app:app1 payload:{"to":2};`)
88 stop1()
89 stop2()
90 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
91 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
92 c.Check(len(errCh1), Equals, 0)
93 c.Check(len(errCh2), Equals, 0)
94}
95
96func (s *UnicastAcceptanceSuite) TestUnicastPending(c *C) {
97 // send unicast that will be pending
98 userId, auth := s.associatedAuth("DEV1")
99 got, err := s.PostRequest("/notify", &api.Unicast{
100 UserId: userId,
101 DeviceId: "DEV1",
102 AppId: "app1",
103 ExpireOn: future,
104 Data: json.RawMessage(`{"a": 42}`),
105 })
106 c.Assert(err, IsNil)
107 c.Assert(got, Matches, ".*ok.*")
108
109 // get pending on connect
110 events, errCh, stop := s.StartClientAuth(c, "DEV1", nil, auth)
111 c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"a":42};`)
112 stop()
113 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
114 c.Check(len(errCh), Equals, 0)
115}
116
117func (s *UnicastAcceptanceSuite) TestUnicastLargeNeedsSplitting(c *C) {
118 userId, auth := s.associatedAuth("DEV2")
119 // send bunch of unicasts that will be pending
120 payloadFmt := fmt.Sprintf(`{"serial":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))
121 for i := 0; i < 32; i++ {
122 got, err := s.PostRequest("/notify", &api.Unicast{
123 UserId: userId,
124 DeviceId: "DEV2",
125 AppId: "app1",
126 ExpireOn: future,
127 Data: json.RawMessage(fmt.Sprintf(payloadFmt, i)),
128 })
129 c.Assert(err, IsNil)
130 c.Assert(got, Matches, ".*ok.*")
131 }
132
133 events, errCh, stop := s.StartClientAuth(c, "DEV2", nil, auth)
134 // gettting pending on connect
135 n := 0
136 for {
137 evt := NextEvent(events, errCh)
138 c.Check(evt, Matches, "unicast app:app1 .*")
139 n += 1
140 if strings.Contains(evt, `"serial":31`) {
141 break
142 }
143 }
144 // was split
145 c.Check(n > 1, Equals, true)
146 stop()
147 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
148 c.Check(len(errCh), Equals, 0)
149}
0150
=== modified file 'server/api/handlers.go'
--- server/api/handlers.go 2014-02-20 17:09:03 +0000
+++ server/api/handlers.go 2014-05-29 12:04:32 +0000
@@ -19,12 +19,15 @@
19package api19package api
2020
21import (21import (
22 "encoding/base64"
22 "encoding/json"23 "encoding/json"
23 "fmt"24 "fmt"
24 "io"25 "io"
25 "net/http"26 "net/http"
26 "time"27 "time"
2728
29 "launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid/uuid"
30
28 "launchpad.net/ubuntu-push/logger"31 "launchpad.net/ubuntu-push/logger"
29 "launchpad.net/ubuntu-push/server/broker"32 "launchpad.net/ubuntu-push/server/broker"
30 "launchpad.net/ubuntu-push/server/store"33 "launchpad.net/ubuntu-push/server/store"
@@ -93,6 +96,11 @@
93 ioError,96 ioError,
94 "Could not read request body",97 "Could not read request body",
95 }98 }
99 ErrMissingIdField = &APIError{
100 http.StatusBadRequest,
101 invalidRequest,
102 "Missing id field",
103 }
96 ErrMissingData = &APIError{104 ErrMissingData = &APIError{
97 http.StatusBadRequest,105 http.StatusBadRequest,
98 invalidRequest,106 invalidRequest,
@@ -130,10 +138,17 @@
130 }138 }
131)139)
132140
133type Message struct {141type castCommon struct {
134 Registration string `json:"registration"`142}
135 CoalesceTag string `json:"coalesce_tag"`143
136 Data json.RawMessage `json:"data"`144type Unicast struct {
145 UserId string `json:"userid"`
146 DeviceId string `json:"deviceid"`
147 AppId string `json:"appid"`
148 //Registration string `json:"registration"`
149 //CoalesceTag string `json:"coalesce_tag"`
150 ExpireOn string `json:"expire_on"`
151 Data json.RawMessage `json:"data"`
137}152}
138153
139// Broadcast request JSON object.154// Broadcast request JSON object.
@@ -198,11 +213,11 @@
198213
199var zeroTime = time.Time{}214var zeroTime = time.Time{}
200215
201func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {216func checkCastCommon(data json.RawMessage, expireOn string) (time.Time, *APIError) {
202 if len(bcast.Data) == 0 {217 if len(data) == 0 {
203 return zeroTime, ErrMissingData218 return zeroTime, ErrMissingData
204 }219 }
205 expire, err := time.Parse(time.RFC3339, bcast.ExpireOn)220 expire, err := time.Parse(time.RFC3339, expireOn)
206 if err != nil {221 if err != nil {
207 return zeroTime, ErrInvalidExpiration222 return zeroTime, ErrInvalidExpiration
208 }223 }
@@ -212,6 +227,10 @@
212 return expire, nil227 return expire, nil
213}228}
214229
230func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {
231 return checkCastCommon(bcast.Data, bcast.ExpireOn)
232}
233
215type StoreForRequest func(w http.ResponseWriter, request *http.Request) (store.PendingStore, error)234type StoreForRequest func(w http.ResponseWriter, request *http.Request) (store.PendingStore, error)
216235
217// context holds the interfaces to delegate to serving requests236// context holds the interfaces to delegate to serving requests
@@ -234,6 +253,20 @@
234 return sto, nil253 return sto, nil
235}254}
236255
256func (ctx *context) prepare(w http.ResponseWriter, request *http.Request, reqObj interface{}) (store.PendingStore, *APIError) {
257 body, apiErr := ReadBody(request, MaxRequestBodyBytes)
258 if apiErr != nil {
259 return nil, apiErr
260 }
261
262 err := json.Unmarshal(body, reqObj)
263 if err != nil {
264 return nil, ErrMalformedJSONObject
265 }
266
267 return ctx.getStore(w, request)
268}
269
237type BroadcastHandler struct {270type BroadcastHandler struct {
238 *context271 *context
239}272}
@@ -270,23 +303,13 @@
270 }303 }
271 }()304 }()
272305
273 body, apiErr := ReadBody(request, MaxRequestBodyBytes)
274 if apiErr != nil {
275 return
276 }
277
278 sto, apiErr := h.getStore(writer, request)
279 if apiErr != nil {
280 return
281 }
282 defer sto.Close()
283
284 broadcast := &Broadcast{}306 broadcast := &Broadcast{}
285 err := json.Unmarshal(body, broadcast)307
286 if err != nil {308 sto, apiErr := h.prepare(writer, request, broadcast)
287 apiErr = ErrMalformedJSONObject309 if apiErr != nil {
288 return310 return
289 }311 }
312 defer sto.Close()
290313
291 apiErr = h.doBroadcast(sto, broadcast)314 apiErr = h.doBroadcast(sto, broadcast)
292 if apiErr != nil {315 if apiErr != nil {
@@ -297,6 +320,64 @@
297 fmt.Fprintf(writer, `{"ok":true}`)320 fmt.Fprintf(writer, `{"ok":true}`)
298}321}
299322
323type UnicastHandler struct {
324 *context
325}
326
327func checkUnicast(ucast *Unicast) (time.Time, *APIError) {
328 if ucast.UserId == "" || ucast.DeviceId == "" || ucast.AppId == "" {
329 return zeroTime, ErrMissingIdField
330 }
331 return checkCastCommon(ucast.Data, ucast.ExpireOn)
332}
333
334// use a base64 encoded TimeUUID
335var generateMsgId = func() string {
336 return base64.StdEncoding.EncodeToString(uuid.NewUUID())
337}
338
339func (h *UnicastHandler) doUnicast(sto store.PendingStore, ucast *Unicast) *APIError {
340 expire, apiErr := checkUnicast(ucast)
341 if apiErr != nil {
342 return apiErr
343 }
344 chanId := store.UnicastInternalChannelId(ucast.UserId, ucast.DeviceId)
345 msgId := generateMsgId()
346 err := sto.AppendToUnicastChannel(chanId, ucast.AppId, ucast.Data, msgId, expire)
347 if err != nil {
348 h.logger.Errorf("could not store notification: %v", err)
349 return ErrCouldNotStoreNotification
350 }
351
352 h.broker.Unicast(chanId)
353 return nil
354}
355
356func (h *UnicastHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
357 var apiErr *APIError
358 defer func() {
359 if apiErr != nil {
360 RespondError(writer, apiErr)
361 }
362 }()
363
364 unicast := &Unicast{}
365
366 sto, apiErr := h.prepare(writer, request, unicast)
367 if apiErr != nil {
368 return
369 }
370 defer sto.Close()
371
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches