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
1=== modified file '.bzrignore'
2--- .bzrignore 2014-03-31 16:43:15 +0000
3+++ .bzrignore 2014-05-29 12:04:32 +0000
4@@ -13,3 +13,12 @@
5 debian/*.substvars
6 ubuntu-push-client
7 push-server-dev
8+signing-helper/CMakeCache.txt
9+signing-helper/CMakeFiles
10+signing-helper/Makefile
11+signing-helper/cmake_install.cmake
12+signing-helper/moc_signing.cpp
13+signing-helper/signing-helper
14+signing-helper/signing-helper_automoc.cpp
15+.has-fetched-deps
16+.*.deps
17
18=== modified file 'Makefile'
19--- Makefile 2014-03-31 17:58:54 +0000
20+++ Makefile 2014-05-29 12:04:32 +0000
21@@ -11,10 +11,21 @@
22 GODEPS += launchpad.net/go-dbus/v1
23 GODEPS += launchpad.net/go-xdg/v0
24 GODEPS += code.google.com/p/gosqlite/sqlite3
25+GODEPS += launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid/uuid
26
27 TOTEST = $(shell env GOPATH=$(GOPATH) go list $(PROJECT)/...|grep -v acceptance|grep -v http13client )
28
29+fetchdeps: .has-fetched-deps
30+
31+.has-fetched-deps: PACKAGE_DEPS
32+ @$(MAKE) --no-print-directory refetchdeps
33+ @touch $@
34+
35+refetchdeps:
36+ sudo apt-get install $$( grep -v '^#' PACKAGE_DEPS )
37+
38 bootstrap:
39+ $(RM) -r $(GOPATH)/pkg
40 mkdir -p $(GOPATH)/bin
41 mkdir -p $(GOPATH)/pkg
42 go get -u launchpad.net/godeps
43@@ -31,8 +42,21 @@
44 acceptance:
45 cd server/acceptance; ./acceptance.sh
46
47-build-client:
48- go build ubuntu-push-client.go
49+build-client: ubuntu-push-client signing-helper/signing-helper
50+
51+.%.deps: %
52+ $(SH) scripts/deps.sh $<
53+
54+%: %.go
55+ go build $<
56+
57+include .ubuntu-push-client.go.deps
58+
59+signing-helper/Makefile: signing-helper/CMakeLists.txt signing-helper/signing-helper.cpp signing-helper/signing.h
60+ cd signing-helper && (make clean || true) && cmake .
61+
62+signing-helper/signing-helper: signing-helper/Makefile signing-helper/signing-helper.cpp signing-helper/signing.h
63+ cd signing-helper && make
64
65 build-server-dev:
66 go build -o push-server-dev launchpad.net/ubuntu-push/server/dev
67@@ -68,4 +92,5 @@
68
69 .PHONY: bootstrap check check-race format check-format \
70 acceptance build-client build server-dev run-server-dev \
71- coverage-summary coverage-html protocol-diagrams
72+ coverage-summary coverage-html protocol-diagrams \
73+ fetchdeps refetchdeps
74
75=== added file 'PACKAGE_DEPS'
76--- PACKAGE_DEPS 1970-01-01 00:00:00 +0000
77+++ PACKAGE_DEPS 2014-05-29 12:04:32 +0000
78@@ -0,0 +1,10 @@
79+# See the README for what this file is and how to use it.
80+build-essential
81+cmake
82+libdbus-1-dev
83+libgcrypt11-dev
84+libglib2.0-dev
85+libnih-dbus-dev
86+libsqlite3-dev
87+libubuntuoneauth-2.0-dev
88+libwhoopsie-dev
89
90=== modified file 'README'
91--- README 2014-03-31 17:58:54 +0000
92+++ README 2014-05-29 12:04:32 +0000
93@@ -6,11 +6,23 @@
94 The code expects to be checked out as launchpad.net/ubuntu-push in a Go
95 workspace, see "go help gopath".
96
97-To setup Go dependencies, install libsqlite3-dev and run:
98-
99- make bootstrap
100-
101-To run tests, install libgcrypt11-dev and libwhoopsie-dev and run:
102+You need a somewhat long list of dependencies, as well as a working Go
103+development environment. THe Ubuntu packagenames for these are listed
104+in the file PACKAGE_DEPS.
105+
106+On Ubuntu, if you have sudo, you can have all those installed for you
107+by do doing
108+
109+ make fetchdeps
110+
111+Once you have the packaged dependencies you can get the Go
112+dependencies via
113+
114+ make bootstrap
115+
116+and then you're set. Good luck!
117+
118+To run the tests:
119
120 make check
121
122
123=== modified file 'bus/connectivity/connectivity.go'
124--- bus/connectivity/connectivity.go 2014-04-04 11:08:28 +0000
125+++ bus/connectivity/connectivity.go 2014-05-29 12:04:32 +0000
126@@ -72,19 +72,20 @@
127 cs.connAttempts += ar.Redial()
128 nm := networkmanager.New(cs.endp, cs.log)
129
130+ // set up the watch
131+ stateCh, err = nm.WatchState()
132+ if err != nil {
133+ cs.log.Debugf("failed to set up the state watch: %s", err)
134+ goto Continue
135+ }
136+
137 // Get the current state.
138 initial = nm.GetState()
139 if initial == networkmanager.Unknown {
140 cs.log.Debugf("Failed to get state.")
141 goto Continue
142 }
143-
144- // set up the watch
145- stateCh, err = nm.WatchState()
146- if err != nil {
147- cs.log.Debugf("failed to set up the state watch: %s", err)
148- goto Continue
149- }
150+ cs.log.Debugf("got initial state of %s", initial)
151
152 primary = nm.GetPrimaryConnection()
153 cs.log.Debugf("primary connection starts as %#v", primary)
154
155=== modified file 'bus/connectivity/connectivity_test.go'
156--- bus/connectivity/connectivity_test.go 2014-04-04 12:01:42 +0000
157+++ bus/connectivity/connectivity_test.go 2014-05-29 12:04:32 +0000
158@@ -17,8 +17,15 @@
159 package connectivity
160
161 import (
162+ "net/http/httptest"
163+ "sync"
164+ "testing"
165+ "time"
166+
167 "launchpad.net/go-dbus/v1"
168 . "launchpad.net/gocheck"
169+
170+ "launchpad.net/ubuntu-push/bus"
171 "launchpad.net/ubuntu-push/bus/networkmanager"
172 testingbus "launchpad.net/ubuntu-push/bus/testing"
173 "launchpad.net/ubuntu-push/config"
174@@ -26,9 +33,6 @@
175 helpers "launchpad.net/ubuntu-push/testing"
176 "launchpad.net/ubuntu-push/testing/condition"
177 "launchpad.net/ubuntu-push/util"
178- "net/http/httptest"
179- "testing"
180- "time"
181 )
182
183 // hook up gocheck
184@@ -115,6 +119,79 @@
185 c.Check(<-cs.networkStateCh, Equals, networkmanager.ConnectedGlobal)
186 }
187
188+// a racyEndpoint is an endpoint that behaves differently depending on
189+// how much time passes between getting the state and setting up the
190+// watch
191+type racyEndpoint struct {
192+ stateGot bool
193+ maxTime time.Time
194+ delta time.Duration
195+ lock sync.RWMutex
196+}
197+
198+func (rep *racyEndpoint) GetProperty(prop string) (interface{}, error) {
199+ switch prop {
200+ case "state":
201+ rep.lock.Lock()
202+ defer rep.lock.Unlock()
203+ rep.stateGot = true
204+ rep.maxTime = time.Now().Add(rep.delta)
205+ return uint32(networkmanager.Connecting), nil
206+ case "PrimaryConnection":
207+ return dbus.ObjectPath("/something"), nil
208+ default:
209+ return nil, nil
210+ }
211+}
212+
213+func (rep *racyEndpoint) WatchSignal(member string, f func(...interface{}), d func()) error {
214+ if member == "StateChanged" {
215+ // we count never having gotten the state as happening "after" now.
216+ rep.lock.RLock()
217+ defer rep.lock.RUnlock()
218+ ok := !rep.stateGot || time.Now().Before(rep.maxTime)
219+ go func() {
220+ if ok {
221+ f(uint32(networkmanager.ConnectedGlobal))
222+ }
223+ d()
224+ }()
225+ }
226+ return nil
227+}
228+
229+func (*racyEndpoint) Close() {}
230+func (*racyEndpoint) Dial() error { return nil }
231+func (*racyEndpoint) String() string { return "racyEndpoint" }
232+func (*racyEndpoint) Call(string, []interface{}, ...interface{}) error { return nil }
233+func (*racyEndpoint) GrabName(bool) <-chan error { return nil }
234+func (*racyEndpoint) WatchMethod(bus.DispatchMap, ...interface{}) {}
235+func (*racyEndpoint) Signal(member string, args []interface{}) error { return nil }
236+
237+var _ bus.Endpoint = (*racyEndpoint)(nil)
238+
239+// takeNext takes a value from given channel with a 1s timeout
240+func takeNext(ch <-chan networkmanager.State) networkmanager.State {
241+ select {
242+ case <-time.After(time.Second):
243+ panic("channel stuck: too long waiting")
244+ case v := <-ch:
245+ return v
246+ }
247+}
248+
249+// test that if the nm state goes from connecting to connected very
250+// shortly after calling GetState, we don't lose the event.
251+func (s *ConnSuite) TestStartAvoidsRace(c *C) {
252+ for delta := time.Second; delta > 1; delta /= 2 {
253+ rep := &racyEndpoint{delta: delta}
254+ cs := connectedState{config: ConnectivityConfig{}, log: s.log, endp: rep}
255+ f := Commentf("when delta=%s", delta)
256+ c.Assert(cs.start(), Equals, networkmanager.Connecting, f)
257+ c.Assert(takeNext(cs.networkStateCh), Equals, networkmanager.ConnectedGlobal, f)
258+ }
259+}
260+
261 /*
262 tests for connectedStateStep()
263 */
264
265=== modified file 'bus/endpoint.go'
266--- bus/endpoint.go 2014-04-02 08:23:15 +0000
267+++ bus/endpoint.go 2014-05-29 12:04:32 +0000
268@@ -29,9 +29,15 @@
269 * Endpoint (and its implementation)
270 */
271
272+type BusMethod func([]interface{}, []interface{}) ([]interface{}, error)
273+type DispatchMap map[string]BusMethod
274+
275 // bus.Endpoint represents the DBus connection itself.
276 type Endpoint interface {
277+ GrabName(allowReplacement bool) <-chan error
278 WatchSignal(member string, f func(...interface{}), d func()) error
279+ WatchMethod(DispatchMap, ...interface{})
280+ Signal(string, []interface{}) error
281 Call(member string, args []interface{}, rvs ...interface{}) error
282 GetProperty(property string) (interface{}, error)
283 Dial() error
284@@ -53,13 +59,18 @@
285 }
286
287 // ensure endpoint implements Endpoint
288-var _ Endpoint = &endpoint{}
289+var _ Endpoint = (*endpoint)(nil)
290
291 /*
292 public methods
293+
294+XXX: these are almost entirely untested, as that would need
295+XXX: integration tests we are currently missing.
296 */
297
298 // Dial() (re)establishes the connection with dbus
299+//
300+// XXX: mostly untested
301 func (endp *endpoint) Dial() error {
302 bus, err := dbus.Connect(endp.busT.(concreteBus).dbusType())
303 if err != nil {
304@@ -106,6 +117,8 @@
305 // with the unpacked value. If it's unable to set up the watch it returns an
306 // error. If the watch fails once established, d() is called. Typically f()
307 // sends the values over a channel, and d() would close the channel.
308+//
309+// XXX: untested
310 func (endp *endpoint) WatchSignal(member string, f func(...interface{}), d func()) error {
311 watch, err := endp.proxy.WatchSignal(endp.addr.Interface, member)
312 if err != nil {
313@@ -122,6 +135,8 @@
314 // interface provided when creating the endpoint). args can be built
315 // using bus.Args(...). The return value is unpacked into rvs before being
316 // returned.
317+//
318+// XXX: untested
319 func (endp *endpoint) Call(member string, args []interface{}, rvs ...interface{}) error {
320 msg, err := endp.proxy.Call(endp.addr.Interface, member, args...)
321 if err != nil {
322@@ -138,6 +153,8 @@
323 // to read a given property on the name, path and interface provided when
324 // creating the endpoint. The return value is unpacked into a dbus.Variant,
325 // and its value returned.
326+//
327+// XXX: untested
328 func (endp *endpoint) GetProperty(property string) (interface{}, error) {
329 msg, err := endp.proxy.Call("org.freedesktop.DBus.Properties", "Get", endp.addr.Interface, property)
330 if err != nil {
331@@ -160,6 +177,8 @@
332 }
333
334 // Close the connection to dbus.
335+//
336+// XXX: untested
337 func (endp *endpoint) Close() {
338 if endp.bus != nil {
339 endp.bus.Close()
340@@ -169,15 +188,98 @@
341 }
342
343 // String() performs advanced endpoint stringification
344+//
345+// XXX: untested
346 func (endp *endpoint) String() string {
347 return fmt.Sprintf("<Connection to %s %#v>", endp.bus, endp.addr)
348 }
349
350+// GrabName() takes over the name on the bus, reporting errors over the
351+// returned channel.
352+//
353+// While the first result will be nil on success, successive results would
354+// typically indicate another process trying to take over the name.
355+//
356+// XXX: untested
357+func (endp *endpoint) GrabName(allowReplacement bool) <-chan error {
358+ flags := dbus.NameFlagAllowReplacement | dbus.NameFlagReplaceExisting
359+ if !allowReplacement {
360+ flags = 0
361+ }
362+ return endp.bus.RequestName(endp.addr.Name, flags).C
363+}
364+
365+// Signal() sends out a signal called <member> containing <args>.
366+//
367+// XXX: untested
368+func (endp *endpoint) Signal(member string, args []interface{}) error {
369+ msg := dbus.NewSignalMessage(dbus.ObjectPath(endp.addr.Path), endp.addr.Interface, member)
370+ if args != nil {
371+ err := msg.AppendArgs(args...)
372+ if err != nil {
373+ endp.log.Errorf("unable to build dbus signal message: %v", err)
374+ return err
375+ }
376+ }
377+ err := endp.bus.Send(msg)
378+ if err != nil {
379+ endp.log.Errorf("unable to send dbus signal: %v", err)
380+ } else {
381+ endp.log.Debugf("sent dbus signal %s(%#v)", member, args)
382+ }
383+ return nil
384+}
385+
386+// WatchMethod() uses the given DispatchMap to answer incoming method
387+// calls.
388+//
389+// XXX: untested
390+func (endp *endpoint) WatchMethod(dispatch DispatchMap, extra ...interface{}) {
391+ ch := make(chan *dbus.Message)
392+ go func() {
393+ var reply *dbus.Message
394+
395+ err_iface := endp.addr.Interface + ".Error"
396+
397+ for msg := range ch {
398+ meth, ok := dispatch[msg.Member]
399+ if !ok || msg.Interface != endp.addr.Interface {
400+ reply = dbus.NewErrorMessage(msg,
401+ "org.freedesktop.DBus.Error.UnknownMethod", "Unknown method")
402+ endp.log.Errorf("WatchMethod: unknown method %s", msg.Member)
403+ } else {
404+ args := msg.AllArgs()
405+ rvals, err := meth(args, extra)
406+ if err != nil {
407+ reply = dbus.NewErrorMessage(msg, err_iface, err.Error())
408+ endp.log.Errorf("WatchMethod: %s(%#v, %#v) failure: %#v", msg.Member, args, extra, err)
409+ } else {
410+ endp.log.Debugf("WatchMethod: %s(%#v, %#v) success: %#v", msg.Member, args, extra, rvals)
411+ reply = dbus.NewMethodReturnMessage(msg)
412+ err = reply.AppendArgs(rvals...)
413+ if err != nil {
414+ endp.log.Errorf("WatchMethod: unable to build dbus response message: %v", err)
415+ reply = dbus.NewErrorMessage(msg, err_iface, err.Error())
416+ }
417+ }
418+ }
419+ err := endp.bus.Send(reply)
420+ if err != nil {
421+ endp.log.Errorf("WatchMethod: unable to send reply: %v", err)
422+ }
423+
424+ }
425+ }()
426+ endp.bus.RegisterObjectPath(dbus.ObjectPath(endp.addr.Path), ch)
427+}
428+
429 /*
430 private methods
431 */
432
433 // unpackOneMsg unpacks the value from the response msg
434+//
435+// XXX: untested
436 func (endp *endpoint) unpackOneMsg(msg *dbus.Message, member string) []interface{} {
437 var varmap map[string]dbus.Variant
438 if err := msg.Args(&varmap); err != nil {
439@@ -187,6 +289,8 @@
440 }
441
442 // unpackMessages unpacks the value from the watch
443+//
444+// XXX: untested
445 func (endp *endpoint) unpackMessages(watch *dbus.SignalWatch, f func(...interface{}), d func(), member string) {
446 for {
447 msg, ok := <-watch.C
448
449=== modified file 'bus/endpoint_test.go'
450--- bus/endpoint_test.go 2014-02-06 09:57:49 +0000
451+++ bus/endpoint_test.go 2014-05-29 12:04:32 +0000
452@@ -37,7 +37,7 @@
453 // testing amenities (already talked about it with jamesh)
454
455 // Tests that we can connect to the *actual* system bus.
456-// XXX maybe connect to a mock/fake/etc bus?
457+// XXX: maybe connect to a mock/fake/etc bus?
458 func (s *EndpointSuite) TestDial(c *C) {
459 // if somebody's set up the env var, assume it's "live"
460 if os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") == "" {
461
462=== modified file 'bus/notifications/raw.go'
463--- bus/notifications/raw.go 2014-04-02 08:23:15 +0000
464+++ bus/notifications/raw.go 2014-05-29 12:04:32 +0000
465@@ -22,6 +22,8 @@
466 // this is the lower-level api
467
468 import (
469+ "errors"
470+
471 "launchpad.net/go-dbus/v1"
472 "launchpad.net/ubuntu-push/bus"
473 "launchpad.net/ubuntu-push/logger"
474@@ -68,6 +70,9 @@
475 timeout int32) (uint32, error) {
476 // that's a long argument list! Take a breather.
477 //
478+ if raw.bus == nil {
479+ return 0, errors.New("unconfigured (missing bus)")
480+ }
481 var res uint32
482 err := raw.bus.Call("Notify", bus.Args(app_name, reuse_id, icon,
483 summary, body, actions, hints, timeout), &res)
484
485=== modified file 'bus/notifications/raw_test.go'
486--- bus/notifications/raw_test.go 2014-02-05 18:17:26 +0000
487+++ bus/notifications/raw_test.go 2014-05-29 12:04:32 +0000
488@@ -57,6 +57,12 @@
489 c.Check(err, NotNil)
490 }
491
492+func (s *RawSuite) TestNotifyFailsIfNoBus(c *C) {
493+ raw := Raw(nil, s.log)
494+ _, err := raw.Notify("", 0, "", "", "", nil, nil, 0)
495+ c.Check(err, ErrorMatches, `.*unconfigured .*`)
496+}
497+
498 func (s *RawSuite) TestNotifiesFailsWeirdly(c *C) {
499 endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{1, 2})
500 raw := Raw(endp, s.log)
501
502=== modified file 'bus/testing/testing_endpoint.go'
503--- bus/testing/testing_endpoint.go 2014-04-04 11:08:28 +0000
504+++ bus/testing/testing_endpoint.go 2014-05-29 12:04:32 +0000
505@@ -166,8 +166,46 @@
506 endp.dialCond, endp.callCond, endp.retvals)
507 }
508
509-// see Endpoint's Close. This one does nothing.
510-func (tc *testingEndpoint) Close() {}
511+// see Endpoint's Close. This one does nothing beyond registering
512+// being called.
513+func (tc *testingEndpoint) Close() {
514+ tc.callArgsLck.Lock()
515+ defer tc.callArgsLck.Unlock()
516+
517+ args := callArgs{Member: "::Close"}
518+ tc.callArgs = append(tc.callArgs, args)
519+}
520+
521+func (tc *testingEndpoint) GrabName(allowReplacement bool) <-chan error {
522+ tc.callArgsLck.Lock()
523+ defer tc.callArgsLck.Unlock()
524+
525+ args := callArgs{Member: "::GrabName"}
526+ args.Args = append(args.Args, allowReplacement)
527+ tc.callArgs = append(tc.callArgs, args)
528+
529+ return nil
530+}
531+
532+func (tc *testingEndpoint) WatchMethod(dispatch bus.DispatchMap, extra ...interface{}) {
533+ tc.callArgsLck.Lock()
534+ defer tc.callArgsLck.Unlock()
535+
536+ args := callArgs{Member: "::WatchMethod"}
537+ args.Args = append(args.Args, dispatch, extra)
538+ tc.callArgs = append(tc.callArgs, args)
539+}
540+
541+func (tc *testingEndpoint) Signal(member string, args []interface{}) error {
542+ tc.callArgsLck.Lock()
543+ defer tc.callArgsLck.Unlock()
544+
545+ callargs := callArgs{Member: "::Signal"}
546+ callargs.Args = append(callargs.Args, member, args)
547+ tc.callArgs = append(tc.callArgs, callargs)
548+
549+ return nil
550+}
551
552 // ensure testingEndpoint implements bus.Endpoint
553-var _ bus.Endpoint = &testingEndpoint{}
554+var _ bus.Endpoint = (*testingEndpoint)(nil)
555
556=== modified file 'bus/testing/testing_endpoint_test.go'
557--- bus/testing/testing_endpoint_test.go 2014-04-02 08:23:15 +0000
558+++ bus/testing/testing_endpoint_test.go 2014-05-29 12:04:32 +0000
559@@ -76,6 +76,24 @@
560 []callArgs{{"what", []interface{}{"is", "this", "thing"}}})
561 }
562
563+// Test that Call() fails but does not explode when asked to return
564+// values that can't be packed into a dbus message.
565+func (s *TestingEndpointSuite) TestCallFailsOnBadRetval(c *C) {
566+ endp := NewTestingEndpoint(nil, condition.Work(true), Equals)
567+ var r uint32
568+ e := endp.Call("what", bus.Args(), &r)
569+ c.Check(e, NotNil)
570+}
571+
572+// Test that Call() fails but does not explode when given argument
573+// that can't be packed into a dbus message.
574+func (s *TestingEndpointSuite) TestCallFailsOnBadArg(c *C) {
575+ endp := NewTestingEndpoint(nil, condition.Work(true), 1)
576+ r := func() {}
577+ e := endp.Call("what", bus.Args(), &r)
578+ c.Check(e, NotNil)
579+}
580+
581 // Test that WatchSignal() with a positive condition sends the provided return
582 // values over the channel.
583 func (s *TestingEndpointSuite) TestWatch(c *C) {
584@@ -102,7 +120,11 @@
585 func (s *TestingEndpointSuite) TestCloser(c *C) {
586 endp := NewTestingEndpoint(nil, condition.Work(true))
587 endp.Close()
588- // ... yay?
589+ c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
590+ {
591+ Member: "::Close",
592+ Args: nil,
593+ }})
594 }
595
596 // Test that WatchSignal() with a negative condition returns an error.
597@@ -173,3 +195,46 @@
598 endp := NewTestingEndpoint(condition.Fail2Work(2), nil, "hello there")
599 c.Check(endp.String(), Matches, ".*Still Broken.*hello there.*")
600 }
601+
602+// Test that GrabName updates callArgs
603+func (s *TestingEndpointSuite) TestGrabNameUpdatesCallArgs(c *C) {
604+ endp := NewTestingEndpoint(nil, condition.Work(true))
605+ endp.GrabName(false)
606+ endp.GrabName(true)
607+ c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
608+ {
609+ Member: "::GrabName",
610+ Args: []interface{}{false},
611+ }, {
612+ Member: "::GrabName",
613+ Args: []interface{}{true},
614+ }})
615+}
616+
617+// Test that Signal updates callArgs
618+func (s *TestingEndpointSuite) TestSignalUpdatesCallArgs(c *C) {
619+ endp := NewTestingEndpoint(nil, condition.Work(true))
620+ endp.Signal("hello", []interface{}{"world"})
621+ endp.Signal("hello", []interface{}{"there"})
622+ c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
623+ {
624+ Member: "::Signal",
625+ Args: []interface{}{"hello", []interface{}{"world"}},
626+ }, {
627+ Member: "::Signal",
628+ Args: []interface{}{"hello", []interface{}{"there"}},
629+ }})
630+}
631+
632+// Test that WatchMethod updates callArgs
633+func (s *TestingEndpointSuite) TestWatchMethodUpdatesCallArgs(c *C) {
634+ endp := NewTestingEndpoint(nil, condition.Work(true))
635+ foo := func([]interface{}, []interface{}) ([]interface{}, error) { return nil, nil }
636+ foomp := bus.DispatchMap{"foo": foo}
637+ endp.WatchMethod(foomp)
638+ c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
639+ {
640+ Member: "::WatchMethod",
641+ Args: []interface{}{foomp, []interface{}(nil)},
642+ }})
643+}
644
645=== modified file 'client/client.go'
646--- client/client.go 2014-04-18 09:35:59 +0000
647+++ client/client.go 2014-05-29 12:04:32 +0000
648@@ -19,6 +19,10 @@
649 package client
650
651 import (
652+ "crypto/sha256"
653+ "encoding/base64"
654+ "encoding/hex"
655+ "encoding/json"
656 "encoding/pem"
657 "errors"
658 "fmt"
659@@ -34,10 +38,12 @@
660 "launchpad.net/ubuntu-push/bus/notifications"
661 "launchpad.net/ubuntu-push/bus/systemimage"
662 "launchpad.net/ubuntu-push/bus/urldispatcher"
663+ "launchpad.net/ubuntu-push/client/service"
664 "launchpad.net/ubuntu-push/client/session"
665- "launchpad.net/ubuntu-push/client/session/levelmap"
666+ "launchpad.net/ubuntu-push/client/session/seenstate"
667 "launchpad.net/ubuntu-push/config"
668 "launchpad.net/ubuntu-push/logger"
669+ "launchpad.net/ubuntu-push/protocol"
670 "launchpad.net/ubuntu-push/util"
671 "launchpad.net/ubuntu-push/whoopsie/identifier"
672 )
673@@ -56,6 +62,8 @@
674 ExpectAllRepairedTime config.ConfigTimeDuration `json:"expect_all_repaired"` // worth retrying all servers after
675 // The PEM-encoded server certificate
676 CertPEMFile string `json:"cert_pem_file"`
677+ // How to invoke the auth helper
678+ AuthHelper []string `json:"auth_helper"`
679 // The logging level (one of "debug", "info", "error")
680 LogLevel logger.ConfigLogLevel `json:"log_level"`
681 }
682@@ -79,9 +87,15 @@
683 actionsCh <-chan notifications.RawActionReply
684 session *session.ClientSession
685 sessionConnectedCh chan uint32
686+ serviceEndpoint bus.Endpoint
687+ service *service.Service
688 }
689
690-var ACTION_ID_SNOWFLAKE = "::ubuntu-push-client::"
691+var (
692+ system_update_url = "settings:///system/system-update"
693+ ACTION_ID_SNOWFLAKE = "::ubuntu-push-client::"
694+ ACTION_ID_BROADCAST = ACTION_ID_SNOWFLAKE + system_update_url
695+)
696
697 // Creates a new Ubuntu Push Notifications client-side daemon that will use
698 // the given configuration file.
699@@ -144,8 +158,9 @@
700 ExchangeTimeout: client.config.ExchangeTimeout.TimeDuration(),
701 HostsCachingExpiryTime: client.config.HostsCachingExpiryTime.TimeDuration(),
702 ExpectAllRepairedTime: client.config.ExpectAllRepairedTime.TimeDuration(),
703- PEM: client.pem,
704- Info: info,
705+ PEM: client.pem,
706+ Info: info,
707+ AuthHelper: client.config.AuthHelper,
708 }
709 }
710
711@@ -155,7 +170,13 @@
712 if err != nil {
713 return err
714 }
715- client.deviceId = client.idder.String()
716+ baseId := client.idder.String()
717+ b, err := hex.DecodeString(baseId)
718+ if err != nil {
719+ return fmt.Errorf("whoopsie id should be hex: %v", err)
720+ }
721+ h := sha256.Sum224(b)
722+ client.deviceId = base64.StdEncoding.EncodeToString(h[:])
723 return nil
724 }
725
726@@ -192,7 +213,7 @@
727 }
728 sess, err := session.NewSession(client.config.Addr,
729 client.deriveSessionConfig(info), client.deviceId,
730- client.levelMapFactory, client.log)
731+ client.seenStateFactory, client.log)
732 if err != nil {
733 return err
734 }
735@@ -200,12 +221,12 @@
736 return nil
737 }
738
739-// levelmapFactory returns a levelMap for the session
740-func (client *PushClient) levelMapFactory() (levelmap.LevelMap, error) {
741+// seenStateFactory returns a SeenState for the session
742+func (client *PushClient) seenStateFactory() (seenstate.SeenState, error) {
743 if client.leveldbPath == "" {
744- return levelmap.NewLevelMap()
745+ return seenstate.NewSeenState()
746 } else {
747- return levelmap.NewSqliteLevelMap(client.leveldbPath)
748+ return seenstate.NewSqliteSeenState(client.leveldbPath)
749 }
750 }
751
752@@ -232,7 +253,7 @@
753 }
754 }
755
756-// filterNotification finds out if the notification is about an actual
757+// filterBroadcastNotification finds out if the notification is about an actual
758 // upgrade for the device. It expects msg.Decoded entries to look
759 // like:
760 //
761@@ -240,7 +261,7 @@
762 // "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]
763 // ...
764 // }
765-func (client *PushClient) filterNotification(msg *session.Notification) bool {
766+func (client *PushClient) filterBroadcastNotification(msg *session.BroadcastNotification) bool {
767 n := len(msg.Decoded)
768 if n == 0 {
769 return false
770@@ -275,26 +296,30 @@
771 return false
772 }
773
774-// handleNotification deals with receiving a notification
775-func (client *PushClient) handleNotification(msg *session.Notification) error {
776- if !client.filterNotification(msg) {
777- return nil
778- }
779- action_id := ACTION_ID_SNOWFLAKE
780- a := []string{action_id, "Go get it!"} // action value not visible on the phone
781+func (client *PushClient) sendNotification(action_id, icon, summary, body string) (uint32, error) {
782+ a := []string{action_id, "Switch to app"} // action value not visible on the phone
783 h := map[string]*dbus.Variant{"x-canonical-switch-to-application": &dbus.Variant{true}}
784 nots := notifications.Raw(client.notificationsEndp, client.log)
785- body := "Tap to open the system updater."
786- not_id, err := nots.Notify(
787- "ubuntu-push-client", // app name
788- uint32(0), // id
789- "update_manager_icon", // icon
790- "There's an updated system image.", // summary
791- body, // body
792- a, // actions
793- h, // hints
794- int32(10*1000), // timeout (ms)
795+ return nots.Notify(
796+ "ubuntu-push-client", // app name
797+ uint32(0), // id
798+ icon, // icon
799+ summary, // summary
800+ body, // body
801+ a, // actions
802+ h, // hints
803+ int32(10*1000), // timeout (ms)
804 )
805+}
806+
807+// handleBroadcastNotification deals with receiving a broadcast notification
808+func (client *PushClient) handleBroadcastNotification(msg *session.BroadcastNotification) error {
809+ if !client.filterBroadcastNotification(msg) {
810+ return nil
811+ }
812+ not_id, err := client.sendNotification(ACTION_ID_BROADCAST,
813+ "update_manager_icon", "There's an updated system image.",
814+ "Tap to open the system updater.")
815 if err != nil {
816 client.log.Errorf("showing notification: %s", err)
817 return err
818@@ -303,26 +328,42 @@
819 return nil
820 }
821
822+// handleUnicastNotification deals with receiving a unicast notification
823+func (client *PushClient) handleUnicastNotification(msg *protocol.Notification) error {
824+ client.log.Debugf("sending notification %#v for %#v.", msg.MsgId, msg.AppId)
825+ return client.service.Inject(msg.AppId, string(msg.Payload))
826+}
827+
828 // handleClick deals with the user clicking a notification
829 func (client *PushClient) handleClick(action_id string) error {
830- if action_id != ACTION_ID_SNOWFLAKE {
831+ // “The string is a stark data structure and everywhere it is passed
832+ // there is much duplication of process. It is a perfect vehicle for
833+ // hiding information.”
834+ //
835+ // From ACM's SIGPLAN publication, (September, 1982), Article
836+ // "Epigrams in Programming", by Alan J. Perlis of Yale University.
837+ url := strings.TrimPrefix(action_id, ACTION_ID_SNOWFLAKE)
838+ if len(url) == len(action_id) || len(url) == 0 {
839+ // it didn't start with the prefix
840 return nil
841 }
842 // it doesn't get much simpler...
843 urld := urldispatcher.New(client.urlDispatcherEndp, client.log)
844- return urld.DispatchURL("settings:///system/system-update")
845+ return urld.DispatchURL(url)
846 }
847
848 // doLoop connects events with their handlers
849-func (client *PushClient) doLoop(connhandler func(bool), clickhandler func(string) error, notifhandler func(*session.Notification) error, errhandler func(error)) {
850+func (client *PushClient) doLoop(connhandler func(bool), clickhandler func(string) error, bcasthandler func(*session.BroadcastNotification) error, ucasthandler func(*protocol.Notification) error, errhandler func(error)) {
851 for {
852 select {
853 case state := <-client.connCh:
854 connhandler(state)
855 case action := <-client.actionsCh:
856 clickhandler(action.ActionId)
857- case msg := <-client.session.MsgCh:
858- notifhandler(msg)
859+ case bcast := <-client.session.BroadcastCh:
860+ bcasthandler(bcast)
861+ case ucast := <-client.session.NotificationsCh:
862+ ucasthandler(ucast)
863 case err := <-client.session.ErrCh:
864 errhandler(err)
865 case count := <-client.sessionConnectedCh:
866@@ -344,14 +385,57 @@
867
868 // Loop calls doLoop with the "real" handlers
869 func (client *PushClient) Loop() {
870- client.doLoop(client.handleConnState, client.handleClick,
871- client.handleNotification, client.handleErr)
872+ client.doLoop(client.handleConnState,
873+ client.handleClick,
874+ client.handleBroadcastNotification,
875+ client.handleUnicastNotification,
876+ client.handleErr)
877+}
878+
879+// these are the currently supported fields of a unicast message
880+type UnicastMessage struct {
881+ Icon string `json:"icon"`
882+ Body string `json:"body"`
883+ Summary string `json:"summary"`
884+ URL string `json:"url"`
885+ Blob json.RawMessage `json:"blob"`
886+}
887+
888+func (client *PushClient) messageHandler(message []byte) error {
889+ var umsg = new(UnicastMessage)
890+ err := json.Unmarshal(message, &umsg)
891+ if err != nil {
892+ client.log.Errorf("unable to unmarshal message: %v", err)
893+ return err
894+ }
895+
896+ not_id, err := client.sendNotification(
897+ ACTION_ID_SNOWFLAKE+umsg.URL,
898+ umsg.Icon, umsg.Summary, umsg.Body)
899+
900+ if err != nil {
901+ client.log.Errorf("showing notification: %s", err)
902+ return err
903+ }
904+ client.log.Debugf("got notification id %d", not_id)
905+ return nil
906+}
907+
908+func (client *PushClient) startService() error {
909+ if client.serviceEndpoint == nil {
910+ client.serviceEndpoint = bus.SessionBus.Endpoint(service.BusAddress, client.log)
911+ }
912+
913+ client.service = service.NewService(client.serviceEndpoint, client.log)
914+ client.service.SetMessageHandler(client.messageHandler)
915+ return client.service.Start()
916 }
917
918 // Start calls doStart with the "real" starters
919 func (client *PushClient) Start() error {
920 return client.doStart(
921 client.configure,
922+ client.startService,
923 client.getDeviceId,
924 client.takeTheBus,
925 client.initSession,
926
927=== modified file 'client/client_test.go'
928--- client/client_test.go 2014-04-18 16:31:04 +0000
929+++ client/client_test.go 2014-05-29 12:04:32 +0000
930@@ -38,8 +38,9 @@
931 "launchpad.net/ubuntu-push/bus/systemimage"
932 testibus "launchpad.net/ubuntu-push/bus/testing"
933 "launchpad.net/ubuntu-push/client/session"
934- "launchpad.net/ubuntu-push/client/session/levelmap"
935+ "launchpad.net/ubuntu-push/client/session/seenstate"
936 "launchpad.net/ubuntu-push/config"
937+ "launchpad.net/ubuntu-push/protocol"
938 helpers "launchpad.net/ubuntu-push/testing"
939 "launchpad.net/ubuntu-push/testing/condition"
940 "launchpad.net/ubuntu-push/util"
941@@ -105,6 +106,7 @@
942 "addr": ":0",
943 "cert_pem_file": pem_file,
944 "recheck_timeout": "3h",
945+ "auth_helper": []string{},
946 "log_level": "debug",
947 }
948 for k, v := range overrides {
949@@ -254,6 +256,9 @@
950 ******************************************************************/
951
952 func (cs *clientSuite) TestDeriveSessionConfig(c *C) {
953+ cs.writeTestConfig(map[string]interface{}{
954+ "auth_helper": []string{"auth", "helper"},
955+ })
956 info := map[string]interface{}{
957 "foo": 1,
958 }
959@@ -265,8 +270,9 @@
960 ExchangeTimeout: 10 * time.Millisecond,
961 HostsCachingExpiryTime: 1 * time.Hour,
962 ExpectAllRepairedTime: 30 * time.Minute,
963- PEM: cli.pem,
964- Info: info,
965+ PEM: cli.pem,
966+ Info: info,
967+ AuthHelper: []string{"auth", "helper"},
968 }
969 // sanity check that we are looking at all fields
970 vExpected := reflect.ValueOf(expected)
971@@ -282,6 +288,35 @@
972 }
973
974 /*****************************************************************
975+ startService tests
976+******************************************************************/
977+
978+func (cs *clientSuite) TestStartServiceWorks(c *C) {
979+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
980+ cli.log = cs.log
981+ cli.serviceEndpoint = testibus.NewTestingEndpoint(condition.Work(true), nil)
982+ c.Check(cli.service, IsNil)
983+ c.Check(cli.startService(), IsNil)
984+ c.Assert(cli.service, NotNil)
985+ c.Check(cli.service.IsRunning(), Equals, true)
986+ c.Check(cli.service.GetMessageHandler(), NotNil)
987+ cli.service.Stop()
988+}
989+
990+func (cs *clientSuite) TestStartServiceErrorsOnNilLog(c *C) {
991+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
992+ c.Check(cli.log, IsNil)
993+ c.Check(cli.startService(), NotNil)
994+}
995+
996+func (cs *clientSuite) TestStartServiceErrorsOnBusDialFail(c *C) {
997+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
998+ cli.log = cs.log
999+ cli.serviceEndpoint = testibus.NewTestingEndpoint(condition.Work(false), nil)
1000+ c.Check(cli.startService(), NotNil)
1001+}
1002+
1003+/*****************************************************************
1004 getDeviceId tests
1005 ******************************************************************/
1006
1007@@ -291,7 +326,7 @@
1008 cli.idder = identifier.New()
1009 c.Check(cli.deviceId, Equals, "")
1010 c.Check(cli.getDeviceId(), IsNil)
1011- c.Check(cli.deviceId, HasLen, 128)
1012+ c.Check(cli.deviceId, HasLen, 40)
1013 }
1014
1015 func (cs *clientSuite) TestGetDeviceIdCanFail(c *C) {
1016@@ -302,6 +337,16 @@
1017 c.Check(cli.getDeviceId(), NotNil)
1018 }
1019
1020+func (cs *clientSuite) TestGetDeviceIdWhoopsieDoesTheUnexpected(c *C) {
1021+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1022+ cli.log = cs.log
1023+ settable := idtesting.Settable()
1024+ cli.idder = settable
1025+ settable.Set("not-hex")
1026+ c.Check(cli.deviceId, Equals, "")
1027+ c.Check(cli.getDeviceId(), ErrorMatches, "whoopsie id should be hex: .*")
1028+}
1029+
1030 /*****************************************************************
1031 takeTheBus tests
1032 ******************************************************************/
1033@@ -389,21 +434,21 @@
1034 }
1035
1036 /*****************************************************************
1037- levelmapFactory tests
1038+ seenStateFactory tests
1039 ******************************************************************/
1040
1041-func (cs *clientSuite) TestLevelMapFactoryNoDbPath(c *C) {
1042+func (cs *clientSuite) TestSeenStateFactoryNoDbPath(c *C) {
1043 cli := NewPushClient(cs.configPath, "")
1044- ln, err := cli.levelMapFactory()
1045+ ln, err := cli.seenStateFactory()
1046 c.Assert(err, IsNil)
1047- c.Check(fmt.Sprintf("%T", ln), Equals, "*levelmap.mapLevelMap")
1048+ c.Check(fmt.Sprintf("%T", ln), Equals, "*seenstate.memSeenState")
1049 }
1050
1051-func (cs *clientSuite) TestLevelMapFactoryWithDbPath(c *C) {
1052+func (cs *clientSuite) TestSeenStateFactoryWithDbPath(c *C) {
1053 cli := NewPushClient(cs.configPath, ":memory:")
1054- ln, err := cli.levelMapFactory()
1055+ ln, err := cli.seenStateFactory()
1056 c.Assert(err, IsNil)
1057- c.Check(fmt.Sprintf("%T", ln), Equals, "*levelmap.sqliteLevelMap")
1058+ c.Check(fmt.Sprintf("%T", ln), Equals, "*seenstate.sqliteSeenState")
1059 }
1060
1061 /*****************************************************************
1062@@ -439,7 +484,7 @@
1063 func (cs *clientSuite) TestHandleConnStateC2D(c *C) {
1064 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1065 cli.log = cs.log
1066- cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, levelmap.NewLevelMap, cs.log)
1067+ cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, seenstate.NewSeenState, cs.log)
1068 cli.session.Dial()
1069 cli.hasConnectivity = true
1070
1071@@ -452,7 +497,7 @@
1072 func (cs *clientSuite) TestHandleConnStateC2DPending(c *C) {
1073 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1074 cli.log = cs.log
1075- cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, levelmap.NewLevelMap, cs.log)
1076+ cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, seenstate.NewSeenState, cs.log)
1077 cli.hasConnectivity = true
1078
1079 cli.handleConnState(false)
1080@@ -460,7 +505,7 @@
1081 }
1082
1083 /*****************************************************************
1084- filterNotification tests
1085+ filterBroadcastNotification tests
1086 ******************************************************************/
1087
1088 var siInfoRes = &systemimage.InfoResult{
1089@@ -470,23 +515,23 @@
1090 LastUpdate: "Unknown",
1091 }
1092
1093-func (cs *clientSuite) TestFilterNotification(c *C) {
1094+func (cs *clientSuite) TestFilterBroadcastNotification(c *C) {
1095 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1096 cli.systemImageInfo = siInfoRes
1097 // empty
1098- msg := &session.Notification{}
1099- c.Check(cli.filterNotification(msg), Equals, false)
1100+ msg := &session.BroadcastNotification{}
1101+ c.Check(cli.filterBroadcastNotification(msg), Equals, false)
1102 // same build number
1103- msg = &session.Notification{
1104+ msg = &session.BroadcastNotification{
1105 Decoded: []map[string]interface{}{
1106 map[string]interface{}{
1107 "daily/mako": []interface{}{float64(102), "tubular"},
1108 },
1109 },
1110 }
1111- c.Check(cli.filterNotification(msg), Equals, false)
1112+ c.Check(cli.filterBroadcastNotification(msg), Equals, false)
1113 // higher build number and pick last
1114- msg = &session.Notification{
1115+ msg = &session.BroadcastNotification{
1116 Decoded: []map[string]interface{}{
1117 map[string]interface{}{
1118 "daily/mako": []interface{}{float64(102), "tubular"},
1119@@ -496,9 +541,9 @@
1120 },
1121 },
1122 }
1123- c.Check(cli.filterNotification(msg), Equals, true)
1124+ c.Check(cli.filterBroadcastNotification(msg), Equals, true)
1125 // going backward by a margin, assume switch of alias
1126- msg = &session.Notification{
1127+ msg = &session.BroadcastNotification{
1128 Decoded: []map[string]interface{}{
1129 map[string]interface{}{
1130 "daily/mako": []interface{}{float64(102), "tubular"},
1131@@ -508,47 +553,47 @@
1132 },
1133 },
1134 }
1135- c.Check(cli.filterNotification(msg), Equals, true)
1136+ c.Check(cli.filterBroadcastNotification(msg), Equals, true)
1137 }
1138
1139-func (cs *clientSuite) TestFilterNotificationRobust(c *C) {
1140+func (cs *clientSuite) TestFilterBroadcastNotificationRobust(c *C) {
1141 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1142 cli.systemImageInfo = siInfoRes
1143- msg := &session.Notification{
1144+ msg := &session.BroadcastNotification{
1145 Decoded: []map[string]interface{}{
1146 map[string]interface{}{},
1147 },
1148 }
1149- c.Check(cli.filterNotification(msg), Equals, false)
1150+ c.Check(cli.filterBroadcastNotification(msg), Equals, false)
1151 for _, broken := range []interface{}{
1152 5,
1153 []interface{}{},
1154 []interface{}{55},
1155 } {
1156- msg := &session.Notification{
1157+ msg := &session.BroadcastNotification{
1158 Decoded: []map[string]interface{}{
1159 map[string]interface{}{
1160 "daily/mako": broken,
1161 },
1162 },
1163 }
1164- c.Check(cli.filterNotification(msg), Equals, false)
1165+ c.Check(cli.filterBroadcastNotification(msg), Equals, false)
1166 }
1167 }
1168
1169 /*****************************************************************
1170- handleNotification tests
1171+ handleBroadcastNotification tests
1172 ******************************************************************/
1173
1174 var (
1175- positiveNotification = &session.Notification{
1176+ positiveBroadcastNotification = &session.BroadcastNotification{
1177 Decoded: []map[string]interface{}{
1178 map[string]interface{}{
1179 "daily/mako": []interface{}{float64(103), "tubular"},
1180 },
1181 },
1182 }
1183- negativeNotification = &session.Notification{
1184+ negativeBroadcastNotification = &session.BroadcastNotification{
1185 Decoded: []map[string]interface{}{
1186 map[string]interface{}{
1187 "daily/mako": []interface{}{float64(102), "tubular"},
1188@@ -557,13 +602,13 @@
1189 }
1190 )
1191
1192-func (cs *clientSuite) TestHandleNotification(c *C) {
1193+func (cs *clientSuite) TestHandleBroadcastNotification(c *C) {
1194 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1195 cli.systemImageInfo = siInfoRes
1196 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
1197 cli.notificationsEndp = endp
1198 cli.log = cs.log
1199- c.Check(cli.handleNotification(positiveNotification), IsNil)
1200+ c.Check(cli.handleBroadcastNotification(positiveBroadcastNotification), IsNil)
1201 // check we sent the notification
1202 args := testibus.GetCallArgs(endp)
1203 c.Assert(args, HasLen, 1)
1204@@ -571,26 +616,48 @@
1205 c.Check(cs.log.Captured(), Matches, `.* got notification id \d+\s*`)
1206 }
1207
1208-func (cs *clientSuite) TestHandleNotificationNothingToDo(c *C) {
1209+func (cs *clientSuite) TestHandleBroadcastNotificationNothingToDo(c *C) {
1210 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1211 cli.systemImageInfo = siInfoRes
1212 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
1213 cli.notificationsEndp = endp
1214 cli.log = cs.log
1215- c.Check(cli.handleNotification(negativeNotification), IsNil)
1216+ c.Check(cli.handleBroadcastNotification(negativeBroadcastNotification), IsNil)
1217 // check we sent the notification
1218 args := testibus.GetCallArgs(endp)
1219 c.Assert(args, HasLen, 0)
1220 c.Check(cs.log.Captured(), Matches, "")
1221 }
1222
1223-func (cs *clientSuite) TestHandleNotificationFail(c *C) {
1224+func (cs *clientSuite) TestHandleBroadcastNotificationFail(c *C) {
1225 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1226 cli.systemImageInfo = siInfoRes
1227 cli.log = cs.log
1228 endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
1229 cli.notificationsEndp = endp
1230- c.Check(cli.handleNotification(positiveNotification), NotNil)
1231+ c.Check(cli.handleBroadcastNotification(positiveBroadcastNotification), NotNil)
1232+}
1233+
1234+/*****************************************************************
1235+ handleUnicastNotification tests
1236+******************************************************************/
1237+
1238+var notif = &protocol.Notification{AppId: "hello", Payload: []byte(`{"url": "xyzzy"}`), MsgId: "42"}
1239+
1240+func (cs *clientSuite) TestHandleUcastNotification(c *C) {
1241+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1242+ svcEndp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(1))
1243+ cli.log = cs.log
1244+ cli.serviceEndpoint = svcEndp
1245+ notsEndp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
1246+ cli.notificationsEndp = notsEndp
1247+ c.Assert(cli.startService(), IsNil)
1248+ c.Check(cli.handleUnicastNotification(notif), IsNil)
1249+ // check we sent the notification
1250+ args := testibus.GetCallArgs(svcEndp)
1251+ c.Assert(len(args), Not(Equals), 0)
1252+ c.Check(args[len(args)-1].Member, Equals, "::Signal")
1253+ c.Check(cs.log.Captured(), Matches, `(?m).*sending notification "42" for "hello".*`)
1254 }
1255
1256 /*****************************************************************
1257@@ -608,18 +675,31 @@
1258 args := testibus.GetCallArgs(endp)
1259 c.Assert(args, HasLen, 0)
1260 // check we worked with the right action id
1261- c.Check(cli.handleClick(ACTION_ID_SNOWFLAKE), IsNil)
1262+ c.Check(cli.handleClick(ACTION_ID_BROADCAST), IsNil)
1263 // check we sent the notification
1264 args = testibus.GetCallArgs(endp)
1265 c.Assert(args, HasLen, 1)
1266 c.Check(args[0].Member, Equals, "DispatchURL")
1267- c.Check(args[0].Args, DeepEquals, []interface{}{"settings:///system/system-update"})
1268+ c.Check(args[0].Args, DeepEquals, []interface{}{system_update_url})
1269+ // check we worked with the right action id
1270+ c.Check(cli.handleClick(ACTION_ID_SNOWFLAKE+"foo"), IsNil)
1271+ // check we sent the notification
1272+ args = testibus.GetCallArgs(endp)
1273+ c.Assert(args, HasLen, 2)
1274+ c.Check(args[1].Member, Equals, "DispatchURL")
1275+ c.Check(args[1].Args, DeepEquals, []interface{}{"foo"})
1276 }
1277
1278 /*****************************************************************
1279 doLoop tests
1280 ******************************************************************/
1281
1282+var nopConn = func(bool) {}
1283+var nopClick = func(string) error { return nil }
1284+var nopBcast = func(*session.BroadcastNotification) error { return nil }
1285+var nopUcast = func(*protocol.Notification) error { return nil }
1286+var nopError = func(error) {}
1287+
1288 func (cs *clientSuite) TestDoLoopConn(c *C) {
1289 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1290 cli.log = cs.log
1291@@ -629,7 +709,7 @@
1292 c.Assert(cli.initSession(), IsNil)
1293
1294 ch := make(chan bool, 1)
1295- go cli.doLoop(func(bool) { ch <- true }, func(_ string) error { return nil }, func(_ *session.Notification) error { return nil }, func(error) {})
1296+ go cli.doLoop(func(bool) { ch <- true }, nopClick, nopBcast, nopUcast, nopError)
1297 c.Check(takeNextBool(ch), Equals, true)
1298 }
1299
1300@@ -643,7 +723,20 @@
1301 cli.actionsCh = aCh
1302
1303 ch := make(chan bool, 1)
1304- go cli.doLoop(func(bool) {}, func(_ string) error { ch <- true; return nil }, func(_ *session.Notification) error { return nil }, func(error) {})
1305+ go cli.doLoop(nopConn, func(_ string) error { ch <- true; return nil }, nopBcast, nopUcast, nopError)
1306+ c.Check(takeNextBool(ch), Equals, true)
1307+}
1308+
1309+func (cs *clientSuite) TestDoLoopBroadcast(c *C) {
1310+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1311+ cli.log = cs.log
1312+ cli.systemImageInfo = siInfoRes
1313+ c.Assert(cli.initSession(), IsNil)
1314+ cli.session.BroadcastCh = make(chan *session.BroadcastNotification, 1)
1315+ cli.session.BroadcastCh <- &session.BroadcastNotification{}
1316+
1317+ ch := make(chan bool, 1)
1318+ go cli.doLoop(nopConn, nopClick, func(_ *session.BroadcastNotification) error { ch <- true; return nil }, nopUcast, nopError)
1319 c.Check(takeNextBool(ch), Equals, true)
1320 }
1321
1322@@ -652,11 +745,11 @@
1323 cli.log = cs.log
1324 cli.systemImageInfo = siInfoRes
1325 c.Assert(cli.initSession(), IsNil)
1326- cli.session.MsgCh = make(chan *session.Notification, 1)
1327- cli.session.MsgCh <- &session.Notification{}
1328+ cli.session.NotificationsCh = make(chan *protocol.Notification, 1)
1329+ cli.session.NotificationsCh <- &protocol.Notification{}
1330
1331 ch := make(chan bool, 1)
1332- go cli.doLoop(func(bool) {}, func(_ string) error { return nil }, func(_ *session.Notification) error { ch <- true; return nil }, func(error) {})
1333+ go cli.doLoop(nopConn, nopClick, nopBcast, func(*protocol.Notification) error { ch <- true; return nil }, nopError)
1334 c.Check(takeNextBool(ch), Equals, true)
1335 }
1336
1337@@ -669,7 +762,7 @@
1338 cli.session.ErrCh <- nil
1339
1340 ch := make(chan bool, 1)
1341- go cli.doLoop(func(bool) {}, func(_ string) error { return nil }, func(_ *session.Notification) error { return nil }, func(error) { ch <- true })
1342+ go cli.doLoop(nopConn, nopClick, nopBcast, nopUcast, func(error) { ch <- true })
1343 c.Check(takeNextBool(ch), Equals, true)
1344 }
1345
1346@@ -719,7 +812,7 @@
1347 cli.systemImageInfo = siInfoRes
1348 c.Assert(cli.initSession(), IsNil)
1349
1350- cli.session.MsgCh = make(chan *session.Notification)
1351+ cli.session.BroadcastCh = make(chan *session.BroadcastNotification)
1352 cli.session.ErrCh = make(chan error)
1353
1354 // we use tick() to make sure things have been through the
1355@@ -736,7 +829,7 @@
1356 c.Check(cs.log.Captured(), Matches, "(?ms).*Session connected after 42 attempts$")
1357
1358 // * actionsCh to the click handler/url dispatcher
1359- aCh <- notifications.RawActionReply{ActionId: ACTION_ID_SNOWFLAKE}
1360+ aCh <- notifications.RawActionReply{ActionId: ACTION_ID_BROADCAST}
1361 tick()
1362 uargs := testibus.GetCallArgs(cli.urlDispatcherEndp)
1363 c.Assert(uargs, HasLen, 1)
1364@@ -752,8 +845,8 @@
1365 tick()
1366 c.Check(cli.hasConnectivity, Equals, false)
1367
1368- // * session.MsgCh to the notifications handler
1369- cli.session.MsgCh <- positiveNotification
1370+ // * session.BroadcastCh to the notifications handler
1371+ cli.session.BroadcastCh <- positiveBroadcastNotification
1372 tick()
1373 nargs := testibus.GetCallArgs(cli.notificationsEndp)
1374 c.Check(nargs, HasLen, 1)
1375@@ -785,6 +878,8 @@
1376
1377 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1378 // before start, everything sucks:
1379+ // no service,
1380+ c.Check(cli.service, IsNil)
1381 // no config,
1382 c.Check(string(cli.config.Addr), Equals, "")
1383 // no device id,
1384@@ -803,12 +898,15 @@
1385 // and now everthing is better! We have a config,
1386 c.Check(string(cli.config.Addr), Equals, ":0")
1387 // and a device id,
1388- c.Check(cli.deviceId, HasLen, 128)
1389+ c.Check(cli.deviceId, HasLen, 40)
1390 // and a session,
1391 c.Check(cli.session, NotNil)
1392 // and a bus,
1393 c.Check(cli.notificationsEndp, NotNil)
1394+ // and a service,
1395+ c.Check(cli.service, NotNil)
1396 // and everthying us just peachy!
1397+ cli.service.Stop() // cleanup
1398 }
1399
1400 func (cs *clientSuite) TestStartCanFail(c *C) {
1401@@ -818,3 +916,38 @@
1402 // and it works. Err. Doesn't.
1403 c.Check(err, NotNil)
1404 }
1405+
1406+func (cs *clientSuite) TestMessageHandler(c *C) {
1407+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1408+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
1409+ cli.notificationsEndp = endp
1410+ cli.log = cs.log
1411+ err := cli.messageHandler([]byte(`{"icon": "icon-value", "summary": "summary-value", "body": "body-value"}`))
1412+ c.Assert(err, IsNil)
1413+ args := testibus.GetCallArgs(endp)
1414+ c.Assert(args, HasLen, 1)
1415+ c.Check(args[0].Member, Equals, "Notify")
1416+ c.Check(args[0].Args[0], Equals, "ubuntu-push-client")
1417+ c.Check(args[0].Args[2], Equals, "icon-value")
1418+ c.Check(args[0].Args[3], Equals, "summary-value")
1419+ c.Check(args[0].Args[4], Equals, "body-value")
1420+}
1421+
1422+func (cs *clientSuite) TestMessageHandlerReportsUnmarshalErrors(c *C) {
1423+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1424+ cli.log = cs.log
1425+
1426+ err := cli.messageHandler([]byte(`{"broken`))
1427+ c.Check(err, NotNil)
1428+ c.Check(cs.log.Captured(), Matches, "(?msi).*unable to unmarshal message:.*")
1429+}
1430+
1431+func (cs *clientSuite) TestMessageHandlerReportsFailedNotifies(c *C) {
1432+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1433+ endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
1434+ cli.notificationsEndp = endp
1435+ cli.log = cs.log
1436+ err := cli.messageHandler([]byte(`{}`))
1437+ c.Assert(err, NotNil)
1438+ c.Check(cs.log.Captured(), Matches, "(?msi).*showing notification: no way$")
1439+}
1440
1441=== added directory 'client/service'
1442=== added file 'client/service/service.go'
1443--- client/service/service.go 1970-01-01 00:00:00 +0000
1444+++ client/service/service.go 2014-05-29 12:04:32 +0000
1445@@ -0,0 +1,209 @@
1446+/*
1447+ Copyright 2013-2014 Canonical Ltd.
1448+
1449+ This program is free software: you can redistribute it and/or modify it
1450+ under the terms of the GNU General Public License version 3, as published
1451+ by the Free Software Foundation.
1452+
1453+ This program is distributed in the hope that it will be useful, but
1454+ WITHOUT ANY WARRANTY; without even the implied warranties of
1455+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1456+ PURPOSE. See the GNU General Public License for more details.
1457+
1458+ You should have received a copy of the GNU General Public License along
1459+ with this program. If not, see <http://www.gnu.org/licenses/>.
1460+*/
1461+
1462+// package service implements the dbus-level service with which client
1463+// applications are expected to interact.
1464+package service
1465+
1466+import (
1467+ "errors"
1468+ "os"
1469+ "sync"
1470+
1471+ "launchpad.net/ubuntu-push/bus"
1472+ "launchpad.net/ubuntu-push/logger"
1473+)
1474+
1475+// Service is the dbus api
1476+type Service struct {
1477+ lock sync.RWMutex
1478+ state ServiceState
1479+ mbox map[string][]string
1480+ msgHandler func([]byte) error
1481+ Log logger.Logger
1482+ Bus bus.Endpoint
1483+}
1484+
1485+// the service can be in a numnber of states
1486+type ServiceState uint8
1487+
1488+const (
1489+ StateUnknown ServiceState = iota
1490+ StateRunning // Start() has been successfully called
1491+ StateFinished // Stop() has been successfully called
1492+)
1493+
1494+var (
1495+ NotConfigured = errors.New("not configured")
1496+ AlreadyStarted = errors.New("already started")
1497+ BusAddress = bus.Address{
1498+ Interface: "com.ubuntu.PushNotifications",
1499+ Path: "/com/ubuntu/PushNotifications",
1500+ Name: "com.ubuntu.PushNotifications",
1501+ }
1502+)
1503+
1504+// NewService() builds a new service and returns it.
1505+func NewService(bus bus.Endpoint, log logger.Logger) *Service {
1506+ return &Service{Log: log, Bus: bus}
1507+}
1508+
1509+// SetMessageHandler() sets the message-handling callback
1510+func (svc *Service) SetMessageHandler(callback func([]byte) error) {
1511+ svc.lock.Lock()
1512+ defer svc.lock.Unlock()
1513+ svc.msgHandler = callback
1514+}
1515+
1516+// GetMessageHandler() returns the (possibly nil) messaging handler callback
1517+func (svc *Service) GetMessageHandler() func([]byte) error {
1518+ svc.lock.RLock()
1519+ defer svc.lock.RUnlock()
1520+ return svc.msgHandler
1521+}
1522+
1523+// IsRunning() returns whether the service's state is StateRunning
1524+func (svc *Service) IsRunning() bool {
1525+ svc.lock.RLock()
1526+ defer svc.lock.RUnlock()
1527+ return svc.state == StateRunning
1528+}
1529+
1530+// Start() dials the bus, grab the name, and listens for method calls.
1531+func (svc *Service) Start() error {
1532+ svc.lock.Lock()
1533+ defer svc.lock.Unlock()
1534+ if svc.state != StateUnknown {
1535+ return AlreadyStarted
1536+ }
1537+ if svc.Log == nil || svc.Bus == nil {
1538+ return NotConfigured
1539+ }
1540+ err := svc.Bus.Dial()
1541+ if err != nil {
1542+ return err
1543+ }
1544+ ch := svc.Bus.GrabName(true)
1545+ log := svc.Log
1546+ go func() {
1547+ for err := range ch {
1548+ if !svc.IsRunning() {
1549+ break
1550+ }
1551+ if err != nil {
1552+ log.Fatalf("name channel for %s got: %v",
1553+ BusAddress.Name, err)
1554+ }
1555+ }
1556+ }()
1557+ svc.Bus.WatchMethod(bus.DispatchMap{
1558+ "Register": svc.register,
1559+ "Notifications": svc.notifications,
1560+ "Inject": svc.inject,
1561+ }, svc)
1562+ svc.state = StateRunning
1563+ return nil
1564+}
1565+
1566+// Stop() closes the bus and sets the state to StateFinished
1567+func (svc *Service) Stop() {
1568+ svc.lock.Lock()
1569+ defer svc.lock.Unlock()
1570+ if svc.Bus != nil {
1571+ svc.Bus.Close()
1572+ }
1573+ svc.state = StateFinished
1574+}
1575+
1576+var (
1577+ BadArgCount = errors.New("Wrong number of arguments")
1578+ BadArgType = errors.New("Bad argument type")
1579+)
1580+
1581+func (svc *Service) register(args []interface{}, _ []interface{}) ([]interface{}, error) {
1582+ if len(args) != 1 {
1583+ return nil, BadArgCount
1584+ }
1585+ appname, ok := args[0].(string)
1586+ if !ok {
1587+ return nil, BadArgType
1588+ }
1589+
1590+ rv := os.Getenv("PUSH_REG_" + appname)
1591+ if rv == "" {
1592+ rv = "this-is-an-opaque-block-of-random-bits-i-promise"
1593+ }
1594+
1595+ return []interface{}{rv}, nil
1596+}
1597+
1598+func (svc *Service) notifications(args []interface{}, _ []interface{}) ([]interface{}, error) {
1599+ if len(args) != 1 {
1600+ return nil, BadArgCount
1601+ }
1602+ appname, ok := args[0].(string)
1603+ if !ok {
1604+ return nil, BadArgType
1605+ }
1606+
1607+ svc.lock.Lock()
1608+ defer svc.lock.Unlock()
1609+
1610+ if svc.mbox == nil {
1611+ return []interface{}{[]string(nil)}, nil
1612+ }
1613+ msgs := svc.mbox[appname]
1614+ delete(svc.mbox, appname)
1615+
1616+ return []interface{}{msgs}, nil
1617+}
1618+
1619+func (svc *Service) inject(args []interface{}, _ []interface{}) ([]interface{}, error) {
1620+ if len(args) != 2 {
1621+ return nil, BadArgCount
1622+ }
1623+ appname, ok := args[0].(string)
1624+ if !ok {
1625+ return nil, BadArgType
1626+ }
1627+ notif, ok := args[1].(string)
1628+ if !ok {
1629+ return nil, BadArgType
1630+ }
1631+
1632+ return nil, svc.Inject(appname, notif)
1633+}
1634+
1635+// Inject() signals to an application over dbus that a notification
1636+// has arrived.
1637+func (svc *Service) Inject(appname string, notif string) error {
1638+ svc.lock.Lock()
1639+ defer svc.lock.Unlock()
1640+ if svc.mbox == nil {
1641+ svc.mbox = make(map[string][]string)
1642+ }
1643+ svc.mbox[appname] = append(svc.mbox[appname], notif)
1644+ if svc.msgHandler != nil {
1645+ err := svc.msgHandler([]byte(notif))
1646+ if err != nil {
1647+ svc.Log.Errorf("msgHandler returned %v", err)
1648+ return err
1649+ }
1650+ svc.Log.Debugf("call to msgHandler successful")
1651+ }
1652+
1653+ return svc.Bus.Signal("Notification", []interface{}{appname})
1654+}
1655
1656=== added file 'client/service/service_test.go'
1657--- client/service/service_test.go 1970-01-01 00:00:00 +0000
1658+++ client/service/service_test.go 2014-05-29 12:04:32 +0000
1659@@ -0,0 +1,247 @@
1660+/*
1661+ Copyright 2014 Canonical Ltd.
1662+
1663+ This program is free software: you can redistribute it and/or modify it
1664+ under the terms of the GNU General Public License version 3, as published
1665+ by the Free Software Foundation.
1666+
1667+ This program is distributed in the hope that it will be useful, but
1668+ WITHOUT ANY WARRANTY; without even the implied warranties of
1669+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1670+ PURPOSE. See the GNU General Public License for more details.
1671+
1672+ You should have received a copy of the GNU General Public License along
1673+ with this program. If not, see <http://www.gnu.org/licenses/>.
1674+*/
1675+
1676+package service
1677+
1678+import (
1679+ "errors"
1680+ "os"
1681+ "testing"
1682+
1683+ . "launchpad.net/gocheck"
1684+
1685+ "launchpad.net/ubuntu-push/bus"
1686+ testibus "launchpad.net/ubuntu-push/bus/testing"
1687+ "launchpad.net/ubuntu-push/logger"
1688+ helpers "launchpad.net/ubuntu-push/testing"
1689+ "launchpad.net/ubuntu-push/testing/condition"
1690+)
1691+
1692+func TestService(t *testing.T) { TestingT(t) }
1693+
1694+type serviceSuite struct {
1695+ log logger.Logger
1696+ bus bus.Endpoint
1697+}
1698+
1699+var _ = Suite(&serviceSuite{})
1700+
1701+func (ss *serviceSuite) SetUpTest(c *C) {
1702+ ss.log = helpers.NewTestLogger(c, "debug")
1703+ ss.bus = testibus.NewTestingEndpoint(condition.Work(true), nil)
1704+}
1705+
1706+func (ss *serviceSuite) TestStart(c *C) {
1707+ svc := NewService(ss.bus, ss.log)
1708+ c.Check(svc.IsRunning(), Equals, false)
1709+ c.Check(svc.Start(), IsNil)
1710+ c.Check(svc.IsRunning(), Equals, true)
1711+ svc.Stop()
1712+}
1713+
1714+func (ss *serviceSuite) TestStartTwice(c *C) {
1715+ svc := NewService(ss.bus, ss.log)
1716+ c.Check(svc.Start(), IsNil)
1717+ c.Check(svc.Start(), Equals, AlreadyStarted)
1718+ svc.Stop()
1719+}
1720+
1721+func (ss *serviceSuite) TestStartNoLog(c *C) {
1722+ svc := NewService(ss.bus, nil)
1723+ c.Check(svc.Start(), Equals, NotConfigured)
1724+}
1725+
1726+func (ss *serviceSuite) TestStartNoBus(c *C) {
1727+ svc := NewService(nil, ss.log)
1728+ c.Check(svc.Start(), Equals, NotConfigured)
1729+}
1730+
1731+func (ss *serviceSuite) TestStartFailsOnBusDialFailure(c *C) {
1732+ bus := testibus.NewTestingEndpoint(condition.Work(false), nil)
1733+ svc := NewService(bus, ss.log)
1734+ c.Check(svc.Start(), ErrorMatches, `.*(?i)cond said no.*`)
1735+ svc.Stop()
1736+}
1737+
1738+func (ss *serviceSuite) TestStartGrabsName(c *C) {
1739+ svc := NewService(ss.bus, ss.log)
1740+ c.Assert(svc.Start(), IsNil)
1741+ callArgs := testibus.GetCallArgs(ss.bus)
1742+ defer svc.Stop()
1743+ c.Assert(callArgs, NotNil)
1744+ c.Check(callArgs[0].Member, Equals, "::GrabName")
1745+}
1746+
1747+func (ss *serviceSuite) TestStopClosesBus(c *C) {
1748+ svc := NewService(ss.bus, ss.log)
1749+ c.Assert(svc.Start(), IsNil)
1750+ svc.Stop()
1751+ callArgs := testibus.GetCallArgs(ss.bus)
1752+ c.Assert(callArgs, NotNil)
1753+ c.Check(callArgs[len(callArgs)-1].Member, Equals, "::Close")
1754+}
1755+
1756+// registration tests
1757+
1758+func (ss *serviceSuite) TestRegistrationFailsIfBadArgs(c *C) {
1759+ for i, s := range []struct {
1760+ args []interface{}
1761+ errt error
1762+ }{
1763+ {nil, BadArgCount}, // no args
1764+ {[]interface{}{}, BadArgCount}, // still no args
1765+ {[]interface{}{42}, BadArgType}, // bad arg type
1766+ {[]interface{}{1, 2}, BadArgCount}, // too many args
1767+ } {
1768+ reg, err := new(Service).register(s.args, nil)
1769+ c.Check(reg, IsNil, Commentf("iteration #%d", i))
1770+ c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
1771+ }
1772+}
1773+
1774+func (ss *serviceSuite) TestRegistrationWorks(c *C) {
1775+ reg, err := new(Service).register([]interface{}{"this"}, nil)
1776+ c.Assert(reg, HasLen, 1)
1777+ regs, ok := reg[0].(string)
1778+ c.Check(ok, Equals, true)
1779+ c.Check(regs, Not(Equals), "")
1780+ c.Check(err, IsNil)
1781+}
1782+
1783+func (ss *serviceSuite) TestRegistrationOverrideWorks(c *C) {
1784+ os.Setenv("PUSH_REG_stuff", "42")
1785+ defer os.Setenv("PUSH_REG_stuff", "")
1786+
1787+ reg, err := new(Service).register([]interface{}{"stuff"}, nil)
1788+ c.Assert(reg, HasLen, 1)
1789+ regs, ok := reg[0].(string)
1790+ c.Check(ok, Equals, true)
1791+ c.Check(regs, Equals, "42")
1792+ c.Check(err, IsNil)
1793+}
1794+
1795+//
1796+// Injection tests
1797+
1798+func (ss *serviceSuite) TestInjectWorks(c *C) {
1799+ svc := NewService(ss.bus, ss.log)
1800+ rvs, err := svc.inject([]interface{}{"hello", "world"}, nil)
1801+ c.Assert(err, IsNil)
1802+ c.Check(rvs, IsNil)
1803+ rvs, err = svc.inject([]interface{}{"hello", "there"}, nil)
1804+ c.Assert(err, IsNil)
1805+ c.Check(rvs, IsNil)
1806+ c.Assert(svc.mbox, HasLen, 1)
1807+ c.Assert(svc.mbox["hello"], HasLen, 2)
1808+ c.Check(svc.mbox["hello"][0], Equals, "world")
1809+ c.Check(svc.mbox["hello"][1], Equals, "there")
1810+
1811+ // and check it fired the right signal (twice)
1812+ callArgs := testibus.GetCallArgs(ss.bus)
1813+ c.Assert(callArgs, HasLen, 2)
1814+ c.Check(callArgs[0].Member, Equals, "::Signal")
1815+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{"Notification", []interface{}{"hello"}})
1816+ c.Check(callArgs[1], DeepEquals, callArgs[0])
1817+}
1818+
1819+func (ss *serviceSuite) TestInjectFailsIfInjectFails(c *C) {
1820+ bus := testibus.NewTestingEndpoint(condition.Work(true),
1821+ condition.Work(false))
1822+ svc := NewService(bus, ss.log)
1823+ svc.SetMessageHandler(func([]byte) error { return errors.New("fail") })
1824+ _, err := svc.inject([]interface{}{"hello", "xyzzy"}, nil)
1825+ c.Check(err, NotNil)
1826+}
1827+
1828+func (ss *serviceSuite) TestInjectFailsIfBadArgs(c *C) {
1829+ for i, s := range []struct {
1830+ args []interface{}
1831+ errt error
1832+ }{
1833+ {nil, BadArgCount},
1834+ {[]interface{}{}, BadArgCount},
1835+ {[]interface{}{1}, BadArgCount},
1836+ {[]interface{}{1, 2}, BadArgType},
1837+ {[]interface{}{"1", 2}, BadArgType},
1838+ {[]interface{}{1, "2"}, BadArgType},
1839+ {[]interface{}{1, 2, 3}, BadArgCount},
1840+ } {
1841+ reg, err := new(Service).inject(s.args, nil)
1842+ c.Check(reg, IsNil, Commentf("iteration #%d", i))
1843+ c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
1844+ }
1845+}
1846+
1847+//
1848+// Notifications tests
1849+func (ss *serviceSuite) TestNotificationsWorks(c *C) {
1850+ svc := NewService(ss.bus, ss.log)
1851+ nots, err := svc.notifications([]interface{}{"hello"}, nil)
1852+ c.Assert(err, IsNil)
1853+ c.Assert(nots, NotNil)
1854+ c.Assert(nots, HasLen, 1)
1855+ c.Check(nots[0], HasLen, 0)
1856+ if svc.mbox == nil {
1857+ svc.mbox = make(map[string][]string)
1858+ }
1859+ svc.mbox["hello"] = append(svc.mbox["hello"], "this", "thing")
1860+ nots, err = svc.notifications([]interface{}{"hello"}, nil)
1861+ c.Assert(err, IsNil)
1862+ c.Assert(nots, NotNil)
1863+ c.Assert(nots, HasLen, 1)
1864+ c.Check(nots[0], DeepEquals, []string{"this", "thing"})
1865+}
1866+
1867+func (ss *serviceSuite) TestNotificationsFailsIfBadArgs(c *C) {
1868+ for i, s := range []struct {
1869+ args []interface{}
1870+ errt error
1871+ }{
1872+ {nil, BadArgCount}, // no args
1873+ {[]interface{}{}, BadArgCount}, // still no args
1874+ {[]interface{}{42}, BadArgType}, // bad arg type
1875+ {[]interface{}{1, 2}, BadArgCount}, // too many args
1876+ } {
1877+ reg, err := new(Service).notifications(s.args, nil)
1878+ c.Check(reg, IsNil, Commentf("iteration #%d", i))
1879+ c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
1880+ }
1881+}
1882+
1883+func (ss *serviceSuite) TestMessageHandler(c *C) {
1884+ svc := new(Service)
1885+ c.Assert(svc.msgHandler, IsNil)
1886+ var ext = []byte{}
1887+ e := errors.New("Hello")
1888+ f := func(s []byte) error { ext = s; return e }
1889+ c.Check(svc.GetMessageHandler(), IsNil)
1890+ svc.SetMessageHandler(f)
1891+ c.Check(svc.GetMessageHandler(), NotNil)
1892+ c.Check(svc.msgHandler([]byte("37")), Equals, e)
1893+ c.Check(ext, DeepEquals, []byte("37"))
1894+}
1895+
1896+func (ss *serviceSuite) TestInjectCallsMessageHandler(c *C) {
1897+ var ext = []byte{}
1898+ svc := NewService(ss.bus, ss.log)
1899+ f := func(s []byte) error { ext = s; return nil }
1900+ svc.SetMessageHandler(f)
1901+ c.Check(svc.Inject("stuff", "{}"), IsNil)
1902+ c.Check(ext, DeepEquals, []byte("{}"))
1903+ err := errors.New("ouch")
1904+ svc.SetMessageHandler(func([]byte) error { return err })
1905+ c.Check(svc.Inject("stuff", "{}"), Equals, err)
1906+}
1907
1908=== renamed directory 'client/session/levelmap' => 'client/session/seenstate'
1909=== renamed file 'client/session/levelmap/levelmap.go' => 'client/session/seenstate/seenstate.go'
1910--- client/session/levelmap/levelmap.go 2014-02-21 16:17:28 +0000
1911+++ client/session/seenstate/seenstate.go 2014-05-29 12:04:32 +0000
1912@@ -14,31 +14,57 @@
1913 with this program. If not, see <http://www.gnu.org/licenses/>.
1914 */
1915
1916-// Package levelmap holds implementations of the LevelMap that the client
1917+// Package seenstate holds implementations of the SeenState that the client
1918 // session uses to keep track of what messages it has seen.
1919-package levelmap
1920-
1921-type LevelMap interface {
1922+package seenstate
1923+
1924+import (
1925+ "launchpad.net/ubuntu-push/protocol"
1926+)
1927+
1928+type SeenState interface {
1929 // Set() (re)sets the given level to the given value.
1930- Set(level string, top int64) error
1931+ SetLevel(level string, top int64) error
1932 // GetAll() returns a "simple" map of the current levels.
1933- GetAll() (map[string]int64, error)
1934-}
1935-
1936-type mapLevelMap map[string]int64
1937-
1938-func (m *mapLevelMap) Set(level string, top int64) error {
1939- (*m)[level] = top
1940+ GetAllLevels() (map[string]int64, error)
1941+ // FilterBySeen filters notifications already seen, keep track
1942+ // of them as well
1943+ FilterBySeen([]protocol.Notification) ([]protocol.Notification, error)
1944+}
1945+
1946+type memSeenState struct {
1947+ levels map[string]int64
1948+ seenMsgs map[string]bool
1949+}
1950+
1951+func (m *memSeenState) SetLevel(level string, top int64) error {
1952+ m.levels[level] = top
1953 return nil
1954 }
1955-func (m *mapLevelMap) GetAll() (map[string]int64, error) {
1956- return map[string]int64(*m), nil
1957-}
1958-
1959-var _ LevelMap = &mapLevelMap{}
1960-
1961-// NewLevelMap returns an implementation of LevelMap that is memory-based and
1962+func (m *memSeenState) GetAllLevels() (map[string]int64, error) {
1963+ return m.levels, nil
1964+}
1965+
1966+func (m *memSeenState) FilterBySeen(notifs []protocol.Notification) ([]protocol.Notification, error) {
1967+ acc := make([]protocol.Notification, 0, len(notifs))
1968+ for _, notif := range notifs {
1969+ seen := m.seenMsgs[notif.MsgId]
1970+ if seen {
1971+ continue
1972+ }
1973+ m.seenMsgs[notif.MsgId] = true
1974+ acc = append(acc, notif)
1975+ }
1976+ return acc, nil
1977+}
1978+
1979+var _ SeenState = (*memSeenState)(nil)
1980+
1981+// NewSeenState returns an implementation of SeenState that is memory-based and
1982 // does not save state.
1983-func NewLevelMap() (LevelMap, error) {
1984- return &mapLevelMap{}, nil
1985+func NewSeenState() (SeenState, error) {
1986+ return &memSeenState{
1987+ levels: make(map[string]int64),
1988+ seenMsgs: make(map[string]bool),
1989+ }, nil
1990 }
1991
1992=== renamed file 'client/session/levelmap/levelmap_test.go' => 'client/session/seenstate/seenstate_test.go'
1993--- client/session/levelmap/levelmap_test.go 2014-02-08 13:50:58 +0000
1994+++ client/session/seenstate/seenstate_test.go 2014-05-29 12:04:32 +0000
1995@@ -14,42 +14,73 @@
1996 with this program. If not, see <http://www.gnu.org/licenses/>.
1997 */
1998
1999-package levelmap
2000+package seenstate
2001
2002 import (
2003+ "testing"
2004+
2005 . "launchpad.net/gocheck"
2006- "testing"
2007+
2008+ "launchpad.net/ubuntu-push/protocol"
2009 )
2010
2011-func TestLevelMap(t *testing.T) { TestingT(t) }
2012-
2013-type lmSuite struct {
2014- constructor func() (LevelMap, error)
2015-}
2016-
2017-var _ = Suite(&lmSuite{})
2018-
2019-func (s *lmSuite) SetUpSuite(c *C) {
2020- s.constructor = NewLevelMap
2021-}
2022-
2023-func (s *lmSuite) TestAllTheThings(c *C) {
2024+func TestSeenState(t *testing.T) { TestingT(t) }
2025+
2026+type ssSuite struct {
2027+ constructor func() (SeenState, error)
2028+}
2029+
2030+var _ = Suite(&ssSuite{})
2031+
2032+func (s *ssSuite) SetUpSuite(c *C) {
2033+ s.constructor = NewSeenState
2034+}
2035+
2036+func (s *ssSuite) TestAllTheLevelThings(c *C) {
2037 var err error
2038- var lm LevelMap
2039- // checks NewLevelMap returns a LevelMap
2040- lm, err = s.constructor()
2041+ var ss SeenState
2042+ // checks NewSeenState returns a SeenState
2043+ ss, err = s.constructor()
2044 // and that it works
2045 c.Assert(err, IsNil)
2046 // setting a couple of things, sets them
2047- c.Check(lm.Set("this", 12), IsNil)
2048- c.Check(lm.Set("that", 42), IsNil)
2049- all, err := lm.GetAll()
2050+ c.Check(ss.SetLevel("this", 12), IsNil)
2051+ c.Check(ss.SetLevel("that", 42), IsNil)
2052+ all, err := ss.GetAllLevels()
2053 c.Check(err, IsNil)
2054 c.Check(all, DeepEquals, map[string]int64{"this": 12, "that": 42})
2055 // re-setting one of them, resets it
2056- c.Check(lm.Set("this", 999), IsNil)
2057- all, err = lm.GetAll()
2058+ c.Check(ss.SetLevel("this", 999), IsNil)
2059+ all, err = ss.GetAllLevels()
2060 c.Check(err, IsNil)
2061 c.Check(all, DeepEquals, map[string]int64{"this": 999, "that": 42})
2062 // huzzah
2063 }
2064+
2065+func (s *ssSuite) TestFilterBySeen(c *C) {
2066+ var err error
2067+ var ss SeenState
2068+ ss, err = s.constructor()
2069+ // and that it works
2070+ c.Assert(err, IsNil)
2071+ n1 := protocol.Notification{MsgId: "m1"}
2072+ n2 := protocol.Notification{MsgId: "m2"}
2073+ n3 := protocol.Notification{MsgId: "m3"}
2074+ n4 := protocol.Notification{MsgId: "m4"}
2075+ n5 := protocol.Notification{MsgId: "m5"}
2076+
2077+ res, err := ss.FilterBySeen([]protocol.Notification{n1, n2, n3})
2078+ c.Assert(err, IsNil)
2079+ // everything wasn't seen yet
2080+ c.Check(res, DeepEquals, []protocol.Notification{n1, n2, n3})
2081+
2082+ res, err = ss.FilterBySeen([]protocol.Notification{n1, n3, n4, n5})
2083+ c.Assert(err, IsNil)
2084+ // already seen n1-n3 removed
2085+ c.Check(res, DeepEquals, []protocol.Notification{n4, n5})
2086+
2087+ // corner case
2088+ res, err = ss.FilterBySeen([]protocol.Notification{})
2089+ c.Assert(err, IsNil)
2090+ c.Assert(res, HasLen, 0)
2091+}
2092
2093=== renamed file 'client/session/levelmap/sqlevelmap.go' => 'client/session/seenstate/sqlseenstate.go'
2094--- client/session/levelmap/sqlevelmap.go 2014-02-12 13:52:19 +0000
2095+++ client/session/seenstate/sqlseenstate.go 2014-05-29 12:04:32 +0000
2096@@ -14,21 +14,25 @@
2097 with this program. If not, see <http://www.gnu.org/licenses/>.
2098 */
2099
2100-package levelmap
2101+package seenstate
2102
2103 import (
2104- _ "code.google.com/p/gosqlite/sqlite3"
2105 "database/sql"
2106 "fmt"
2107+ "strings"
2108+
2109+ _ "code.google.com/p/gosqlite/sqlite3"
2110+
2111+ "launchpad.net/ubuntu-push/protocol"
2112 )
2113
2114-type sqliteLevelMap struct {
2115+type sqliteSeenState struct {
2116 db *sql.DB
2117 }
2118
2119-// NewSqliteLevelMap returns an implementation of LevelMap that
2120-// persists the map in an sqlite database.
2121-func NewSqliteLevelMap(filename string) (LevelMap, error) {
2122+// NewSqliteSeenState returns an implementation of SeenState that
2123+// keeps and persists the state in an sqlite database.
2124+func NewSqliteSeenState(filename string) (SeenState, error) {
2125 db, err := sql.Open("sqlite3", filename)
2126 if err != nil {
2127 return nil, fmt.Errorf("cannot open sqlite level map %#v: %v", filename, err)
2128@@ -37,18 +41,22 @@
2129 if err != nil {
2130 return nil, fmt.Errorf("cannot (re)create sqlite level map table: %v", err)
2131 }
2132- return &sqliteLevelMap{db}, nil
2133+ _, err = db.Exec("CREATE TABLE IF NOT EXISTS seen_msgs (id text primary key)")
2134+ if err != nil {
2135+ return nil, fmt.Errorf("cannot (re)create sqlite seen msgs table: %v", err)
2136+ }
2137+ return &sqliteSeenState{db}, nil
2138 }
2139
2140-func (pm *sqliteLevelMap) Set(level string, top int64) error {
2141- _, err := pm.db.Exec("REPLACE INTO level_map (level, top) VALUES (?, ?)", level, top)
2142+func (ps *sqliteSeenState) SetLevel(level string, top int64) error {
2143+ _, err := ps.db.Exec("REPLACE INTO level_map (level, top) VALUES (?, ?)", level, top)
2144 if err != nil {
2145 return fmt.Errorf("cannot set %#v to %#v in level map: %v", level, top, err)
2146 }
2147 return nil
2148 }
2149-func (pm *sqliteLevelMap) GetAll() (map[string]int64, error) {
2150- rows, err := pm.db.Query("SELECT * FROM level_map")
2151+func (ps *sqliteSeenState) GetAllLevels() (map[string]int64, error) {
2152+ rows, err := ps.db.Query("SELECT * FROM level_map")
2153 if err != nil {
2154 return nil, fmt.Errorf("cannot retrieve levels from sqlite level map: %v", err)
2155 }
2156@@ -64,3 +72,30 @@
2157 }
2158 return m, nil
2159 }
2160+
2161+func (ps *sqliteSeenState) dropPrevThan(msgId string) error {
2162+ _, err := ps.db.Exec("DELETE FROM seen_msgs WHERE rowid < (SELECT rowid FROM seen_msgs WHERE id = ?)", msgId)
2163+ return err
2164+}
2165+
2166+func (ps *sqliteSeenState) FilterBySeen(notifs []protocol.Notification) ([]protocol.Notification, error) {
2167+ if len(notifs) == 0 {
2168+ return nil, nil
2169+ }
2170+ acc := make([]protocol.Notification, 0, len(notifs))
2171+ for _, notif := range notifs {
2172+ _, err := ps.db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", notif.MsgId)
2173+ if err != nil {
2174+ if strings.HasSuffix(err.Error(), "UNIQUE constraint failed: seen_msgs.id") {
2175+ continue
2176+ }
2177+ return nil, fmt.Errorf("cannot insert %#v in seen msgs: %v", notif.MsgId, err)
2178+ }
2179+ acc = append(acc, notif)
2180+ }
2181+ err := ps.dropPrevThan(notifs[0].MsgId)
2182+ if err != nil {
2183+ return nil, fmt.Errorf("cannot delete obsolete seen msgs: %v", err)
2184+ }
2185+ return acc, nil
2186+}
2187
2188=== renamed file 'client/session/levelmap/sqlevelmap_test.go' => 'client/session/seenstate/sqlseenstate_test.go'
2189--- client/session/levelmap/sqlevelmap_test.go 2014-02-08 18:08:55 +0000
2190+++ client/session/seenstate/sqlseenstate_test.go 2014-05-29 12:04:32 +0000
2191@@ -14,29 +14,32 @@
2192 with this program. If not, see <http://www.gnu.org/licenses/>.
2193 */
2194
2195-package levelmap
2196+package seenstate
2197
2198 import (
2199+ "database/sql"
2200+
2201 _ "code.google.com/p/gosqlite/sqlite3"
2202- "database/sql"
2203 . "launchpad.net/gocheck"
2204+
2205+ "launchpad.net/ubuntu-push/protocol"
2206 )
2207
2208-type sqlmSuite struct{ lmSuite }
2209-
2210-var _ = Suite(&sqlmSuite{})
2211-
2212-func (s *sqlmSuite) SetUpSuite(c *C) {
2213- s.constructor = func() (LevelMap, error) { return NewSqliteLevelMap(":memory:") }
2214+type sqlsSuite struct{ ssSuite }
2215+
2216+var _ = Suite(&sqlsSuite{})
2217+
2218+func (s *sqlsSuite) SetUpSuite(c *C) {
2219+ s.constructor = func() (SeenState, error) { return NewSqliteSeenState(":memory:") }
2220 }
2221
2222-func (s *sqlmSuite) TestNewCanFail(c *C) {
2223- m, err := NewSqliteLevelMap("/does/not/exist")
2224- c.Assert(m, IsNil)
2225+func (s *sqlsSuite) TestNewCanFail(c *C) {
2226+ sqls, err := NewSqliteSeenState("/does/not/exist")
2227+ c.Assert(sqls, IsNil)
2228 c.Check(err, NotNil)
2229 }
2230
2231-func (s *sqlmSuite) TestSetCanFail(c *C) {
2232+func (s *sqlsSuite) TestSetCanFail(c *C) {
2233 dir := c.MkDir()
2234 filename := dir + "test.db"
2235 db, err := sql.Open("sqlite3", filename)
2236@@ -45,14 +48,14 @@
2237 _, err = db.Exec("CREATE TABLE level_map (foo)")
2238 c.Assert(err, IsNil)
2239 // <evil laughter>
2240- m, err := NewSqliteLevelMap(filename)
2241+ sqls, err := NewSqliteSeenState(filename)
2242 c.Check(err, IsNil)
2243- c.Assert(m, NotNil)
2244- err = m.Set("foo", 42)
2245+ c.Assert(sqls, NotNil)
2246+ err = sqls.SetLevel("foo", 42)
2247 c.Check(err, ErrorMatches, "cannot set .*")
2248 }
2249
2250-func (s *sqlmSuite) TestGetAllCanFail(c *C) {
2251+func (s *sqlsSuite) TestGetAllCanFail(c *C) {
2252 dir := c.MkDir()
2253 filename := dir + "test.db"
2254 db, err := sql.Open("sqlite3", filename)
2255@@ -61,15 +64,15 @@
2256 _, err = db.Exec("CREATE TABLE level_map AS SELECT 'what'")
2257 c.Assert(err, IsNil)
2258 // <evil laughter>
2259- m, err := NewSqliteLevelMap(filename)
2260+ sqls, err := NewSqliteSeenState(filename)
2261 c.Check(err, IsNil)
2262- c.Assert(m, NotNil)
2263- all, err := m.GetAll()
2264+ c.Assert(sqls, NotNil)
2265+ all, err := sqls.GetAllLevels()
2266 c.Check(all, IsNil)
2267 c.Check(err, ErrorMatches, "cannot read level .*")
2268 }
2269
2270-func (s *sqlmSuite) TestGetAllCanFailDifferently(c *C) {
2271+func (s *sqlsSuite) TestGetAllCanFailDifferently(c *C) {
2272 dir := c.MkDir()
2273 filename := dir + "test.db"
2274 db, err := sql.Open("sqlite3", filename)
2275@@ -83,10 +86,85 @@
2276 _, err = db.Exec("DROP TABLE foo")
2277 c.Assert(err, IsNil)
2278 // <evil laughter>
2279- m, err := NewSqliteLevelMap(filename)
2280+ sqls, err := NewSqliteSeenState(filename)
2281 c.Check(err, IsNil)
2282- c.Assert(m, NotNil)
2283- all, err := m.GetAll()
2284+ c.Assert(sqls, NotNil)
2285+ all, err := sqls.GetAllLevels()
2286 c.Check(all, IsNil)
2287 c.Check(err, ErrorMatches, "cannot retrieve levels .*")
2288 }
2289+
2290+func (s *sqlsSuite) TestFilterBySeenCanFail(c *C) {
2291+ dir := c.MkDir()
2292+ filename := dir + "test.db"
2293+ db, err := sql.Open("sqlite3", filename)
2294+ c.Assert(err, IsNil)
2295+ // create the wrong kind of table
2296+ _, err = db.Exec("CREATE TABLE seen_msgs AS SELECT 'what'")
2297+ c.Assert(err, IsNil)
2298+ // <evil laughter>
2299+ sqls, err := NewSqliteSeenState(filename)
2300+ c.Check(err, IsNil)
2301+ c.Assert(sqls, NotNil)
2302+ n1 := protocol.Notification{MsgId: "m1"}
2303+ res, err := sqls.FilterBySeen([]protocol.Notification{n1})
2304+ c.Check(res, IsNil)
2305+ c.Check(err, ErrorMatches, "cannot insert .*")
2306+}
2307+
2308+func (s *sqlsSuite) TestDropPrevThan(c *C) {
2309+ dir := c.MkDir()
2310+ filename := dir + "test.db"
2311+ db, err := sql.Open("sqlite3", filename)
2312+ c.Assert(err, IsNil)
2313+ sqls, err := NewSqliteSeenState(filename)
2314+ c.Check(err, IsNil)
2315+ c.Assert(sqls, NotNil)
2316+
2317+ _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m1")
2318+ c.Assert(err, IsNil)
2319+ _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m2")
2320+ c.Assert(err, IsNil)
2321+ _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m3")
2322+ c.Assert(err, IsNil)
2323+ _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m4")
2324+ c.Assert(err, IsNil)
2325+ _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m5")
2326+ c.Assert(err, IsNil)
2327+
2328+ rows, err := db.Query("SELECT COUNT(*) FROM seen_msgs")
2329+ c.Assert(err, IsNil)
2330+ rows.Next()
2331+ var i int
2332+ err = rows.Scan(&i)
2333+ c.Assert(err, IsNil)
2334+ c.Check(i, Equals, 5)
2335+ rows.Close()
2336+
2337+ err = sqls.(*sqliteSeenState).dropPrevThan("m3")
2338+ c.Assert(err, IsNil)
2339+
2340+ rows, err = db.Query("SELECT COUNT(*) FROM seen_msgs")
2341+ c.Assert(err, IsNil)
2342+ rows.Next()
2343+ err = rows.Scan(&i)
2344+ c.Assert(err, IsNil)
2345+ c.Check(i, Equals, 3)
2346+ rows.Close()
2347+
2348+ var msgId string
2349+ rows, err = db.Query("SELECT * FROM seen_msgs")
2350+ rows.Next()
2351+ err = rows.Scan(&msgId)
2352+ c.Assert(err, IsNil)
2353+ c.Check(msgId, Equals, "m3")
2354+ rows.Next()
2355+ err = rows.Scan(&msgId)
2356+ c.Assert(err, IsNil)
2357+ c.Check(msgId, Equals, "m4")
2358+ rows.Next()
2359+ err = rows.Scan(&msgId)
2360+ c.Assert(err, IsNil)
2361+ c.Check(msgId, Equals, "m5")
2362+ rows.Close()
2363+}
2364
2365=== modified file 'client/session/session.go'
2366--- client/session/session.go 2014-04-18 16:37:31 +0000
2367+++ client/session/session.go 2014-05-29 12:04:32 +0000
2368@@ -26,21 +26,24 @@
2369 "fmt"
2370 "math/rand"
2371 "net"
2372+ "os/exec"
2373 "strings"
2374 "sync"
2375 "sync/atomic"
2376 "time"
2377
2378 "launchpad.net/ubuntu-push/client/gethosts"
2379- "launchpad.net/ubuntu-push/client/session/levelmap"
2380+ "launchpad.net/ubuntu-push/client/session/seenstate"
2381 "launchpad.net/ubuntu-push/logger"
2382 "launchpad.net/ubuntu-push/protocol"
2383 "launchpad.net/ubuntu-push/util"
2384 )
2385
2386-var wireVersionBytes = []byte{protocol.ProtocolWireVersion}
2387+var (
2388+ wireVersionBytes = []byte{protocol.ProtocolWireVersion}
2389+)
2390
2391-type Notification struct {
2392+type BroadcastNotification struct {
2393 TopLevel int64
2394 Decoded []map[string]interface{}
2395 }
2396@@ -84,6 +87,7 @@
2397 ExpectAllRepairedTime time.Duration
2398 PEM []byte
2399 Info map[string]interface{}
2400+ AuthHelper []string
2401 }
2402
2403 // ClientSession holds a client<->server session and its configuration.
2404@@ -91,7 +95,7 @@
2405 // configuration
2406 DeviceId string
2407 ClientSessionConfig
2408- Levels levelmap.LevelMap
2409+ SeenState seenstate.SeenState
2410 Protocolator func(net.Conn) protocol.Protocol
2411 // hosts
2412 getHost hostGetter
2413@@ -112,9 +116,12 @@
2414 pingInterval time.Duration
2415 retrier util.AutoRedialer
2416 // status
2417- stateP *uint32
2418- ErrCh chan error
2419- MsgCh chan *Notification
2420+ stateP *uint32
2421+ ErrCh chan error
2422+ BroadcastCh chan *BroadcastNotification
2423+ NotificationsCh chan *protocol.Notification
2424+ // authorization
2425+ auth string
2426 // autoredial knobs
2427 shouldDelayP *uint32
2428 lastAutoRedial time.Time
2429@@ -138,10 +145,10 @@
2430 }
2431
2432 func NewSession(serverAddrSpec string, conf ClientSessionConfig,
2433- deviceId string, levelmapFactory func() (levelmap.LevelMap, error),
2434+ deviceId string, seenStateFactory func() (seenstate.SeenState, error),
2435 log logger.Logger) (*ClientSession, error) {
2436 state := uint32(Disconnected)
2437- levels, err := levelmapFactory()
2438+ seenState, err := seenStateFactory()
2439 if err != nil {
2440 return nil, err
2441 }
2442@@ -159,7 +166,7 @@
2443 DeviceId: deviceId,
2444 Log: log,
2445 Protocolator: protocol.NewProtocol0,
2446- Levels: levels,
2447+ SeenState: seenState,
2448 TLS: &tls.Config{},
2449 stateP: &state,
2450 timeSince: time.Since,
2451@@ -234,6 +241,27 @@
2452 return nil
2453 }
2454
2455+// addAuthorization gets the authorization blob to send to the server
2456+// and adds it to the session.
2457+func (sess *ClientSession) addAuthorization() error {
2458+ sess.Log.Debugf("adding authorization")
2459+ // using a helper, for now at least
2460+ if len(sess.AuthHelper) == 0 {
2461+ // do nothing if helper is unset or empty
2462+ return nil
2463+ }
2464+
2465+ auth, err := exec.Command(sess.AuthHelper[0], sess.AuthHelper[1:]...).Output()
2466+ if err != nil {
2467+ // For now we just log the error, as we don't want to block unauthorized users
2468+ sess.Log.Errorf("unable to get the authorization token from the account: %v", err)
2469+ } else {
2470+ sess.auth = strings.TrimSpace(string(auth))
2471+ }
2472+
2473+ return nil
2474+}
2475+
2476 func (sess *ClientSession) resetHosts() {
2477 sess.deliveryHosts = nil
2478 }
2479@@ -343,7 +371,7 @@
2480 return err
2481 }
2482
2483-func (sess *ClientSession) decodeBroadcast(bcast *serverMsg) *Notification {
2484+func (sess *ClientSession) decodeBroadcast(bcast *serverMsg) *BroadcastNotification {
2485 decoded := make([]map[string]interface{}, 0)
2486 for _, p := range bcast.Payloads {
2487 var v map[string]interface{}
2488@@ -354,7 +382,7 @@
2489 }
2490 decoded = append(decoded, v)
2491 }
2492- return &Notification{
2493+ return &BroadcastNotification{
2494 TopLevel: bcast.TopLevel,
2495 Decoded: decoded,
2496 }
2497@@ -362,7 +390,7 @@
2498
2499 // handle "broadcast" messages
2500 func (sess *ClientSession) handleBroadcast(bcast *serverMsg) error {
2501- err := sess.Levels.Set(bcast.ChanId, bcast.TopLevel)
2502+ err := sess.SeenState.SetLevel(bcast.ChanId, bcast.TopLevel)
2503 if err != nil {
2504 sess.setState(Error)
2505 sess.Log.Errorf("unable to set level: %v", err)
2506@@ -382,15 +410,44 @@
2507 bcast.ChanId, bcast.AppId, bcast.TopLevel, bcast.Payloads)
2508 if bcast.ChanId == protocol.SystemChannelId {
2509 // the system channel id, the only one we care about for now
2510- sess.Log.Debugf("sending it over")
2511- sess.MsgCh <- sess.decodeBroadcast(bcast)
2512- sess.Log.Debugf("sent it over")
2513+ sess.Log.Debugf("sending bcast over")
2514+ sess.BroadcastCh <- sess.decodeBroadcast(bcast)
2515+ sess.Log.Debugf("sent bcast over")
2516 } else {
2517 sess.Log.Debugf("what is this weird channel, %#v?", bcast.ChanId)
2518 }
2519 return nil
2520 }
2521
2522+// handle "notifications" messages
2523+func (sess *ClientSession) handleNotifications(ucast *serverMsg) error {
2524+ notifs, err := sess.SeenState.FilterBySeen(ucast.Notifications)
2525+ if err != nil {
2526+ sess.setState(Error)
2527+ sess.Log.Errorf("unable to record msgs seen: %v", err)
2528+ sess.proto.WriteMessage(protocol.AckMsg{"nak"})
2529+ return err
2530+ }
2531+ // the server assumes if we ack the broadcast, we've updated
2532+ // our state. Hence the order.
2533+ err = sess.proto.WriteMessage(protocol.AckMsg{"ack"})
2534+ if err != nil {
2535+ sess.setState(Error)
2536+ sess.Log.Errorf("unable to ack notifications: %s", err)
2537+ return err
2538+ }
2539+ sess.clearShouldDelay()
2540+ for i := range notifs {
2541+ notif := &notifs[i]
2542+ sess.Log.Debugf("unicast app:%v msg:%s payload:%s",
2543+ notif.AppId, notif.MsgId, notif.Payload)
2544+ sess.Log.Debugf("sending ucast over")
2545+ sess.NotificationsCh <- notif
2546+ sess.Log.Debugf("sent ucast over")
2547+ }
2548+ return nil
2549+}
2550+
2551 // handle "connbroken" messages
2552 func (sess *ClientSession) handleConnBroken(connBroken *serverMsg) error {
2553 sess.setState(Error)
2554@@ -422,8 +479,15 @@
2555 err = sess.handlePing()
2556 case "broadcast":
2557 err = sess.handleBroadcast(&recv)
2558+ case "notifications":
2559+ err = sess.handleNotifications(&recv)
2560 case "connbroken":
2561 err = sess.handleConnBroken(&recv)
2562+ case "warn":
2563+ // XXX: current message "warn" should be "connwarn"
2564+ fallthrough
2565+ case "connwarn":
2566+ sess.Log.Errorf("server sent warning: %s", recv.Reason)
2567 }
2568 if err != nil {
2569 return err
2570@@ -450,17 +514,16 @@
2571 }
2572 proto := sess.Protocolator(conn)
2573 proto.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
2574- levels, err := sess.Levels.GetAll()
2575+ levels, err := sess.SeenState.GetAllLevels()
2576 if err != nil {
2577 sess.setState(Error)
2578 sess.Log.Errorf("unable to start: get levels: %v", err)
2579 return err
2580 }
2581 err = proto.WriteMessage(protocol.ConnectMsg{
2582- Type: "connect",
2583- DeviceId: sess.DeviceId,
2584- // xxx get the SSO Authorization string from the phone
2585- Authorization: "",
2586+ Type: "connect",
2587+ DeviceId: sess.DeviceId,
2588+ Authorization: sess.auth,
2589 Levels: levels,
2590 Info: sess.Info,
2591 })
2592@@ -495,18 +558,21 @@
2593
2594 // run calls connect, and if it works it calls start, and if it works
2595 // it runs loop in a goroutine, and ships its return value over ErrCh.
2596-func (sess *ClientSession) run(closer func(), hostGetter, connecter, starter, looper func() error) error {
2597+func (sess *ClientSession) run(closer func(), authChecker, hostGetter, connecter, starter, looper func() error) error {
2598 closer()
2599- err := hostGetter()
2600- if err != nil {
2601- return err
2602- }
2603- err = connecter()
2604+ if err := authChecker(); err != nil {
2605+ return err
2606+ }
2607+ if err := hostGetter(); err != nil {
2608+ return err
2609+ }
2610+ err := connecter()
2611 if err == nil {
2612 err = starter()
2613 if err == nil {
2614 sess.ErrCh = make(chan error, 1)
2615- sess.MsgCh = make(chan *Notification)
2616+ sess.BroadcastCh = make(chan *BroadcastNotification)
2617+ sess.NotificationsCh = make(chan *protocol.Notification)
2618 go func() { sess.ErrCh <- looper() }()
2619 }
2620 }
2621@@ -531,7 +597,7 @@
2622 // keep on trying.
2623 panic("can't Dial() without a protocol constructor.")
2624 }
2625- return sess.run(sess.doClose, sess.getHosts, sess.connect, sess.start, sess.loop)
2626+ return sess.run(sess.doClose, sess.addAuthorization, sess.getHosts, sess.connect, sess.start, sess.loop)
2627 }
2628
2629 func init() {
2630
2631=== modified file 'client/session/session_test.go'
2632--- client/session/session_test.go 2014-04-18 16:37:31 +0000
2633+++ client/session/session_test.go 2014-05-29 12:04:32 +0000
2634@@ -33,8 +33,7 @@
2635 . "launchpad.net/gocheck"
2636
2637 "launchpad.net/ubuntu-push/client/gethosts"
2638- "launchpad.net/ubuntu-push/client/session/levelmap"
2639- "launchpad.net/ubuntu-push/logger"
2640+ "launchpad.net/ubuntu-push/client/session/seenstate"
2641 "launchpad.net/ubuntu-push/protocol"
2642 helpers "launchpad.net/ubuntu-push/testing"
2643 "launchpad.net/ubuntu-push/testing/condition"
2644@@ -157,17 +156,20 @@
2645 return nil
2646 }
2647
2648-// brokenLevelMap is a LevelMap that always breaks
2649-type brokenLevelMap struct{}
2650+// brokenSeenState is a SeenState that always breaks
2651+type brokenSeenState struct{}
2652
2653-func (*brokenLevelMap) Set(string, int64) error { return errors.New("broken.") }
2654-func (*brokenLevelMap) GetAll() (map[string]int64, error) { return nil, errors.New("broken.") }
2655+func (*brokenSeenState) SetLevel(string, int64) error { return errors.New("broken.") }
2656+func (*brokenSeenState) GetAllLevels() (map[string]int64, error) { return nil, errors.New("broken.") }
2657+func (*brokenSeenState) FilterBySeen([]protocol.Notification) ([]protocol.Notification, error) {
2658+ return nil, errors.New("broken.")
2659+}
2660
2661 /////
2662
2663 type clientSessionSuite struct {
2664- log logger.Logger
2665- lvls func() (levelmap.LevelMap, error)
2666+ log *helpers.TestLogger
2667+ lvls func() (seenstate.SeenState, error)
2668 }
2669
2670 func (cs *clientSessionSuite) SetUpTest(c *C) {
2671@@ -175,7 +177,7 @@
2672 }
2673
2674 // in-memory level map testing
2675-var _ = Suite(&clientSessionSuite{lvls: levelmap.NewLevelMap})
2676+var _ = Suite(&clientSessionSuite{lvls: seenstate.NewSeenState})
2677
2678 // sqlite level map testing
2679 type clientSqlevelsSessionSuite struct{ clientSessionSuite }
2680@@ -183,7 +185,7 @@
2681 var _ = Suite(&clientSqlevelsSessionSuite{})
2682
2683 func (cs *clientSqlevelsSessionSuite) SetUpSuite(c *C) {
2684- cs.lvls = func() (levelmap.LevelMap, error) { return levelmap.NewSqliteLevelMap(":memory:") }
2685+ cs.lvls = func() (seenstate.SeenState, error) { return seenstate.NewSqliteSeenState(":memory:") }
2686 }
2687
2688 /****************************************************************
2689@@ -249,8 +251,8 @@
2690 c.Check(err, NotNil)
2691 }
2692
2693-func (cs *clientSessionSuite) TestNewSessionBadLevelMapFails(c *C) {
2694- ferr := func() (levelmap.LevelMap, error) { return nil, errors.New("Busted.") }
2695+func (cs *clientSessionSuite) TestNewSessionBadSeenStateFails(c *C) {
2696+ ferr := func() (seenstate.SeenState, error) { return nil, errors.New("Busted.") }
2697 sess, err := NewSession("", dummyConf, "wah", ferr, cs.log)
2698 c.Check(sess, IsNil)
2699 c.Assert(err, NotNil)
2700@@ -347,6 +349,43 @@
2701 }
2702
2703 /****************************************************************
2704+ addAuthorization() tests
2705+****************************************************************/
2706+
2707+func (cs *clientSessionSuite) TestAddAuthorizationAddsAuthorization(c *C) {
2708+ sess := &ClientSession{Log: cs.log}
2709+ sess.AuthHelper = []string{"echo", "some auth"}
2710+ c.Assert(sess.auth, Equals, "")
2711+ err := sess.addAuthorization()
2712+ c.Assert(err, IsNil)
2713+ c.Check(sess.auth, Equals, "some auth")
2714+}
2715+
2716+func (cs *clientSessionSuite) TestAddAuthorizationIgnoresErrors(c *C) {
2717+ sess := &ClientSession{Log: cs.log}
2718+ sess.AuthHelper = []string{"sh", "-c", "echo hello; false"}
2719+
2720+ c.Assert(sess.auth, Equals, "")
2721+ err := sess.addAuthorization()
2722+ c.Assert(err, IsNil)
2723+ c.Check(sess.auth, Equals, "")
2724+}
2725+
2726+func (cs *clientSessionSuite) TestAddAuthorizationSkipsIfUnsetOrNil(c *C) {
2727+ sess := &ClientSession{Log: cs.log}
2728+ sess.AuthHelper = nil
2729+ c.Assert(sess.auth, Equals, "")
2730+ err := sess.addAuthorization()
2731+ c.Assert(err, IsNil)
2732+ c.Check(sess.auth, Equals, "")
2733+
2734+ sess.AuthHelper = []string{}
2735+ err = sess.addAuthorization()
2736+ c.Assert(err, IsNil)
2737+ c.Check(sess.auth, Equals, "")
2738+}
2739+
2740+/****************************************************************
2741 startConnectionAttempt()/nextHostToTry()/started tests
2742 ****************************************************************/
2743
2744@@ -601,7 +640,7 @@
2745 conf := ClientSessionConfig{
2746 ExchangeTimeout: time.Millisecond,
2747 }
2748- s.sess, err = NewSession("", conf, "wah", levelmap.NewLevelMap, helpers.NewTestLogger(c, "debug"))
2749+ s.sess, err = NewSession("", conf, "wah", seenstate.NewSeenState, helpers.NewTestLogger(c, "debug"))
2750 c.Assert(err, IsNil)
2751 s.sess.Connection = &testConn{Name: "TestHandle*"}
2752 s.errCh = make(chan error, 1)
2753@@ -609,7 +648,8 @@
2754 s.downCh = make(chan interface{}, 5)
2755 s.sess.proto = &testProtocol{up: s.upCh, down: s.downCh}
2756 // make the message channel buffered
2757- s.sess.MsgCh = make(chan *Notification, 5)
2758+ s.sess.BroadcastCh = make(chan *BroadcastNotification, 5)
2759+ s.sess.NotificationsCh = make(chan *protocol.Notification, 5)
2760 }
2761
2762 func (s *msgSuite) TestHandlePingWorks(c *C) {
2763@@ -668,8 +708,8 @@
2764 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2765 s.upCh <- nil // ack ok
2766 c.Check(<-s.errCh, Equals, nil)
2767- c.Assert(len(s.sess.MsgCh), Equals, 1)
2768- c.Check(<-s.sess.MsgCh, DeepEquals, &Notification{
2769+ c.Assert(len(s.sess.BroadcastCh), Equals, 1)
2770+ c.Check(<-s.sess.BroadcastCh, DeepEquals, &BroadcastNotification{
2771 TopLevel: 2,
2772 Decoded: []map[string]interface{}{
2773 map[string]interface{}{
2774@@ -681,7 +721,7 @@
2775 },
2776 })
2777 // and finally, the session keeps track of the levels
2778- levels, err := s.sess.Levels.GetAll()
2779+ levels, err := s.sess.SeenState.GetAllLevels()
2780 c.Check(err, IsNil)
2781 c.Check(levels, DeepEquals, map[string]int64{"0": 2})
2782 }
2783@@ -716,11 +756,11 @@
2784 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2785 s.upCh <- nil // ack ok
2786 c.Check(<-s.errCh, IsNil)
2787- c.Check(len(s.sess.MsgCh), Equals, 0)
2788+ c.Check(len(s.sess.BroadcastCh), Equals, 0)
2789 }
2790
2791-func (s *msgSuite) TestHandleBroadcastWrongBrokenLevelmap(c *C) {
2792- s.sess.Levels = &brokenLevelMap{}
2793+func (s *msgSuite) TestHandleBroadcastBrokenSeenState(c *C) {
2794+ s.sess.SeenState = &brokenSeenState{}
2795 msg := serverMsg{"broadcast",
2796 protocol.BroadcastMsg{
2797 Type: "broadcast",
2798@@ -733,8 +773,9 @@
2799 s.upCh <- nil // ack ok
2800 // start returns with error
2801 c.Check(<-s.errCh, Not(Equals), nil)
2802+ c.Check(s.sess.State(), Equals, Error)
2803 // no message sent out
2804- c.Check(len(s.sess.MsgCh), Equals, 0)
2805+ c.Check(len(s.sess.BroadcastCh), Equals, 0)
2806 // and nak'ed it
2807 c.Check(len(s.downCh), Equals, 1)
2808 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"})
2809@@ -767,6 +808,118 @@
2810 }
2811
2812 /****************************************************************
2813+ handleNotifications() tests
2814+****************************************************************/
2815+
2816+func (s *msgSuite) TestHandleNotificationsWorks(c *C) {
2817+ s.sess.setShouldDelay()
2818+ n1 := protocol.Notification{
2819+ AppId: "app1",
2820+ MsgId: "a",
2821+ Payload: json.RawMessage(`{"m": 1}`),
2822+ }
2823+ n2 := protocol.Notification{
2824+ AppId: "app2",
2825+ MsgId: "b",
2826+ Payload: json.RawMessage(`{"m": 2}`),
2827+ }
2828+ msg := serverMsg{"notifications",
2829+ protocol.BroadcastMsg{},
2830+ protocol.NotificationsMsg{
2831+ Notifications: []protocol.Notification{n1, n2},
2832+ }, protocol.ConnBrokenMsg{}}
2833+ go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
2834+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2835+ s.upCh <- nil // ack ok
2836+ c.Check(<-s.errCh, Equals, nil)
2837+ c.Check(s.sess.ShouldDelay(), Equals, false)
2838+ c.Assert(len(s.sess.NotificationsCh), Equals, 2)
2839+ c.Check(<-s.sess.NotificationsCh, DeepEquals, &n1)
2840+ c.Check(<-s.sess.NotificationsCh, DeepEquals, &n2)
2841+}
2842+
2843+func (s *msgSuite) TestHandleNotificationsFiltersSeen(c *C) {
2844+ n1 := protocol.Notification{
2845+ AppId: "app1",
2846+ MsgId: "a",
2847+ Payload: json.RawMessage(`{"m": 1}`),
2848+ }
2849+ n2 := protocol.Notification{
2850+ AppId: "app2",
2851+ MsgId: "b",
2852+ Payload: json.RawMessage(`{"m": 2}`),
2853+ }
2854+ msg := serverMsg{"notifications",
2855+ protocol.BroadcastMsg{},
2856+ protocol.NotificationsMsg{
2857+ Notifications: []protocol.Notification{n1, n2},
2858+ }, protocol.ConnBrokenMsg{}}
2859+ go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
2860+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2861+ s.upCh <- nil // ack ok
2862+ c.Check(<-s.errCh, Equals, nil)
2863+ c.Assert(len(s.sess.NotificationsCh), Equals, 2)
2864+ c.Check(<-s.sess.NotificationsCh, DeepEquals, &n1)
2865+ c.Check(<-s.sess.NotificationsCh, DeepEquals, &n2)
2866+
2867+ // second time they get ignored
2868+ go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
2869+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2870+ s.upCh <- nil // ack ok
2871+ c.Check(<-s.errCh, Equals, nil)
2872+ c.Assert(len(s.sess.NotificationsCh), Equals, 0)
2873+}
2874+
2875+func (s *msgSuite) TestHandleNotificationsBadAckWrite(c *C) {
2876+ s.sess.setShouldDelay()
2877+ n1 := protocol.Notification{
2878+ AppId: "app1",
2879+ MsgId: "a",
2880+ Payload: json.RawMessage(`{"m": 1}`),
2881+ }
2882+ msg := serverMsg{"notifications",
2883+ protocol.BroadcastMsg{},
2884+ protocol.NotificationsMsg{
2885+ Notifications: []protocol.Notification{n1},
2886+ }, protocol.ConnBrokenMsg{}}
2887+ go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
2888+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2889+ failure := errors.New("ACK ACK ACK")
2890+ s.upCh <- failure
2891+ c.Assert(<-s.errCh, Equals, failure)
2892+ c.Check(s.sess.State(), Equals, Error)
2893+ // didn't get to clear
2894+ c.Check(s.sess.ShouldDelay(), Equals, true)
2895+}
2896+
2897+func (s *msgSuite) TestHandleNotificationsBrokenSeenState(c *C) {
2898+ s.sess.setShouldDelay()
2899+ s.sess.SeenState = &brokenSeenState{}
2900+ n1 := protocol.Notification{
2901+ AppId: "app1",
2902+ MsgId: "a",
2903+ Payload: json.RawMessage(`{"m": 1}`),
2904+ }
2905+ msg := serverMsg{"notifications",
2906+ protocol.BroadcastMsg{},
2907+ protocol.NotificationsMsg{
2908+ Notifications: []protocol.Notification{n1},
2909+ }, protocol.ConnBrokenMsg{}}
2910+ go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
2911+ s.upCh <- nil // ack ok
2912+ // start returns with error
2913+ c.Check(<-s.errCh, Not(Equals), nil)
2914+ c.Check(s.sess.State(), Equals, Error)
2915+ // no message sent out
2916+ c.Check(len(s.sess.NotificationsCh), Equals, 0)
2917+ // and nak'ed it
2918+ c.Check(len(s.downCh), Equals, 1)
2919+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"})
2920+ // didn't get to clear
2921+ c.Check(s.sess.ShouldDelay(), Equals, true)
2922+}
2923+
2924+/****************************************************************
2925 handleConnBroken() tests
2926 ****************************************************************/
2927
2928@@ -861,6 +1014,26 @@
2929 c.Check(<-s.errCh, Equals, failure)
2930 }
2931
2932+func (s *loopSuite) TestLoopNotifications(c *C) {
2933+ c.Check(s.sess.State(), Equals, Running)
2934+
2935+ n1 := protocol.Notification{
2936+ AppId: "app1",
2937+ MsgId: "a",
2938+ Payload: json.RawMessage(`{"m": 1}`),
2939+ }
2940+ msg := &protocol.NotificationsMsg{
2941+ Type: "notifications",
2942+ Notifications: []protocol.Notification{n1},
2943+ }
2944+ c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
2945+ s.upCh <- msg
2946+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2947+ failure := errors.New("ack")
2948+ s.upCh <- failure
2949+ c.Check(<-s.errCh, Equals, failure)
2950+}
2951+
2952 func (s *loopSuite) TestLoopConnBroken(c *C) {
2953 c.Check(s.sess.State(), Equals, Running)
2954 broken := protocol.ConnBrokenMsg{
2955@@ -872,6 +1045,31 @@
2956 c.Check(<-s.errCh, NotNil)
2957 }
2958
2959+func (s *loopSuite) TestLoopConnWarn(c *C) {
2960+ warn := protocol.ConnWarnMsg{
2961+ Type: "warn",
2962+ Reason: "XXX",
2963+ }
2964+ connwarn := protocol.ConnWarnMsg{
2965+ Type: "connwarn",
2966+ Reason: "REASON",
2967+ }
2968+ failure := errors.New("warn")
2969+ log := s.sess.Log.(*helpers.TestLogger)
2970+
2971+ c.Check(s.sess.State(), Equals, Running)
2972+ c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
2973+ log.ResetCapture()
2974+ s.upCh <- warn
2975+ s.upCh <- connwarn
2976+ s.upCh <- failure
2977+ c.Check(<-s.errCh, Equals, failure)
2978+ c.Check(log.Captured(),
2979+ Matches, `(?ms).* warning: XXX$.*`)
2980+ c.Check(log.Captured(),
2981+ Matches, `(?ms).* warning: REASON$`)
2982+}
2983+
2984 /****************************************************************
2985 start() tests
2986 ****************************************************************/
2987@@ -898,7 +1096,7 @@
2988 func (cs *clientSessionSuite) TestStartFailsIfGetLevelsFails(c *C) {
2989 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
2990 c.Assert(err, IsNil)
2991- sess.Levels = &brokenLevelMap{}
2992+ sess.SeenState = &brokenSeenState{}
2993 sess.Connection = &testConn{Name: "TestStartConnectMessageFails"}
2994 errCh := make(chan error, 1)
2995 upCh := make(chan interface{}, 5)
2996@@ -931,9 +1129,10 @@
2997
2998 c.Check(takeNext(downCh), Equals, "deadline 0")
2999 c.Check(takeNext(downCh), DeepEquals, protocol.ConnectMsg{
3000- Type: "connect",
3001- DeviceId: sess.DeviceId,
3002- Levels: map[string]int64{},
3003+ Type: "connect",
3004+ DeviceId: sess.DeviceId,
3005+ Levels: map[string]int64{},
3006+ Authorization: "",
3007 })
3008 upCh <- errors.New("Overflow error in /dev/null")
3009 err = <-errCh
3010@@ -1038,6 +1237,7 @@
3011 msg, ok := takeNext(downCh).(protocol.ConnectMsg)
3012 c.Check(ok, Equals, true)
3013 c.Check(msg.DeviceId, Equals, "wah")
3014+ c.Check(msg.Authorization, Equals, "")
3015 c.Check(msg.Info, DeepEquals, info)
3016 upCh <- nil // no error
3017 upCh <- protocol.ConnAckMsg{
3018@@ -1054,6 +1254,22 @@
3019 run() tests
3020 ****************************************************************/
3021
3022+func (cs *clientSessionSuite) TestRunBailsIfAuthCheckFails(c *C) {
3023+ sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
3024+ c.Assert(err, IsNil)
3025+ failure := errors.New("TestRunBailsIfAuthCheckFails")
3026+ has_closed := false
3027+ err = sess.run(
3028+ func() { has_closed = true },
3029+ func() error { return failure },
3030+ nil,
3031+ nil,
3032+ nil,
3033+ nil)
3034+ c.Check(err, Equals, failure)
3035+ c.Check(has_closed, Equals, true)
3036+}
3037+
3038 func (cs *clientSessionSuite) TestRunBailsIfHostGetterFails(c *C) {
3039 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
3040 c.Assert(err, IsNil)
3041@@ -1061,6 +1277,7 @@
3042 has_closed := false
3043 err = sess.run(
3044 func() { has_closed = true },
3045+ func() error { return nil },
3046 func() error { return failure },
3047 nil,
3048 nil,
3049@@ -1076,6 +1293,7 @@
3050 err = sess.run(
3051 func() {},
3052 func() error { return nil },
3053+ func() error { return nil },
3054 func() error { return failure },
3055 nil,
3056 nil)
3057@@ -1090,6 +1308,7 @@
3058 func() {},
3059 func() error { return nil },
3060 func() error { return nil },
3061+ func() error { return nil },
3062 func() error { return failure },
3063 nil)
3064 c.Check(err, Equals, failure)
3065@@ -1098,23 +1317,24 @@
3066 func (cs *clientSessionSuite) TestRunRunsEvenIfLoopFails(c *C) {
3067 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
3068 c.Assert(err, IsNil)
3069- // just to make a point: until here we haven't set ErrCh & MsgCh (no
3070+ // just to make a point: until here we haven't set ErrCh & BroadcastCh (no
3071 // biggie if this stops being true)
3072 c.Check(sess.ErrCh, IsNil)
3073- c.Check(sess.MsgCh, IsNil)
3074+ c.Check(sess.BroadcastCh, IsNil)
3075 failureCh := make(chan error) // must be unbuffered
3076- notf := &Notification{}
3077+ notf := &BroadcastNotification{}
3078 err = sess.run(
3079 func() {},
3080 func() error { return nil },
3081 func() error { return nil },
3082 func() error { return nil },
3083- func() error { sess.MsgCh <- notf; return <-failureCh })
3084+ func() error { return nil },
3085+ func() error { sess.BroadcastCh <- notf; return <-failureCh })
3086 c.Check(err, Equals, nil)
3087 // if run doesn't error it sets up the channels
3088 c.Assert(sess.ErrCh, NotNil)
3089- c.Assert(sess.MsgCh, NotNil)
3090- c.Check(<-sess.MsgCh, Equals, notf)
3091+ c.Assert(sess.BroadcastCh, NotNil)
3092+ c.Check(<-sess.BroadcastCh, Equals, notf)
3093 failure := errors.New("TestRunRunsEvenIfLoopFails")
3094 failureCh <- failure
3095 c.Check(<-sess.ErrCh, Equals, failure)
3096@@ -1317,9 +1537,9 @@
3097 c.Check(takeNext(downCh), Equals, protocol.AckMsg{"ack"})
3098 upCh <- nil
3099 // ...get bubbled up,
3100- c.Check(<-sess.MsgCh, NotNil)
3101+ c.Check(<-sess.BroadcastCh, NotNil)
3102 // and their TopLevel remembered
3103- levels, err := sess.Levels.GetAll()
3104+ levels, err := sess.SeenState.GetAllLevels()
3105 c.Check(err, IsNil)
3106 c.Check(levels, DeepEquals, map[string]int64{"0": 2})
3107
3108
3109=== modified file 'config/config.go'
3110--- config/config.go 2014-04-12 08:44:39 +0000
3111+++ config/config.go 2014-05-29 12:04:32 +0000
3112@@ -268,14 +268,16 @@
3113
3114 // used to implement -cfg@=
3115 type readConfigAtVal struct {
3116+ path string
3117 accu map[string]json.RawMessage
3118 }
3119
3120 func (v *readConfigAtVal) String() string {
3121- return "<config.json>"
3122+ return v.path
3123 }
3124
3125 func (v *readConfigAtVal) Set(path string) error {
3126+ v.path = path
3127 return readOneConfig(v.accu, path)
3128 }
3129
3130@@ -292,7 +294,7 @@
3131 help := destField.fld.Tag.Get("help")
3132 flag.Var(&val{destField, accu}, destField.configName(), help)
3133 }
3134- flag.Var(&readConfigAtVal{accu}, "cfg@", "get config values from file")
3135+ flag.Var(&readConfigAtVal{"<config.json>", accu}, "cfg@", "get config values from file")
3136 flag.Parse()
3137 return nil
3138 }
3139@@ -301,17 +303,25 @@
3140 // command line was already parsed.
3141 var IgnoreParsedFlags = false
3142
3143-// ReadFiles reads configuration from a set of files. The string
3144-// "<flags>" can be used as a pseudo file-path, it will consider
3145-// command line flags, invoking flag.Parse(). Among those the flag
3146-// -cfg@=FILE can be used to get further config values from FILE.
3147-func ReadFiles(destConfig interface{}, cfgFpaths ...string) error {
3148+// ReadFilesDefaults reads configuration from a set of files. The
3149+// string "<flags>" can be used as a pseudo file-path, it will
3150+// consider command line flags, invoking flag.Parse(). Among those the
3151+// flag -cfg@=FILE can be used to get further config values from FILE.
3152+// Defaults for fields can be given through a map[string]interface{}.
3153+func ReadFilesDefaults(destConfig interface{}, defls map[string]interface{}, cfgFpaths ...string) error {
3154 destValue, err := checkDestConfig("destConfig", destConfig)
3155 if err != nil {
3156 return err
3157 }
3158 // do the parsing in two phases for better error handling
3159 p1 := make(map[string]json.RawMessage)
3160+ for field, value := range defls {
3161+ b, err := json.Marshal(value)
3162+ if err != nil {
3163+ return err
3164+ }
3165+ p1[field] = json.RawMessage(b)
3166+ }
3167 readOne := false
3168 for _, cfgPath := range cfgFpaths {
3169 if cfgPath == "<flags>" {
3170@@ -336,6 +346,13 @@
3171 return fillDestConfig(destValue, p1)
3172 }
3173
3174+// ReadFiles reads configuration from a set of files exactly like
3175+// ReadFilesDefaults but no defaults can be given making all fields
3176+// mandatory.
3177+func ReadFiles(destConfig interface{}, cfgFpaths ...string) error {
3178+ return ReadFilesDefaults(destConfig, nil, cfgFpaths...)
3179+}
3180+
3181 // CompareConfigs compares the two given configuration structures. It returns a list of differing fields or nil if the config contents are the same.
3182 func CompareConfig(config1, config2 interface{}) ([]string, error) {
3183 v1, err := checkDestConfig("config1", config1)
3184
3185=== modified file 'config/config_test.go'
3186--- config/config_test.go 2014-04-12 08:43:32 +0000
3187+++ config/config_test.go 2014-05-29 12:04:32 +0000
3188@@ -173,6 +173,40 @@
3189 c.Check(err, NotNil)
3190 }
3191
3192+type testConfig2 struct {
3193+ A int
3194+ B string
3195+ C []string `json:"c_list"`
3196+ D ConfigTimeDuration
3197+}
3198+
3199+func (s *configSuite) TestReadFilesDefaults(c *C) {
3200+ var cfg testConfig2
3201+ tmpDir := c.MkDir()
3202+ emptyCfgPath := filepath.Join(tmpDir, "e.json")
3203+ err := ioutil.WriteFile(emptyCfgPath, []byte("{}"), os.ModePerm)
3204+ c.Assert(err, IsNil)
3205+ err = ReadFilesDefaults(&cfg, map[string]interface{}{
3206+ "a": 42,
3207+ "b": "foo",
3208+ "c_list": []string{"bar", "baz"},
3209+ "d": "3s",
3210+ }, emptyCfgPath)
3211+ c.Check(err, IsNil)
3212+ c.Check(cfg.A, Equals, 42)
3213+ c.Check(cfg.B, Equals, "foo")
3214+ c.Check(cfg.C, DeepEquals, []string{"bar", "baz"})
3215+ c.Check(cfg.D.TimeDuration(), Equals, 3*time.Second)
3216+}
3217+
3218+func (s *configSuite) TestReadFilesDefaultsError(c *C) {
3219+ var cfg testConfig2
3220+ err := ReadFilesDefaults(&cfg, map[string]interface{}{
3221+ "a": make(chan int),
3222+ })
3223+ c.Assert(err, NotNil)
3224+}
3225+
3226 type B struct {
3227 BFld int
3228 }
3229@@ -193,13 +227,6 @@
3230 c.Check(a, DeepEquals, A{1, B{2}, 0})
3231 }
3232
3233-type testConfig2 struct {
3234- A int
3235- B string
3236- C []string `json:"c_list"`
3237- D ConfigTimeDuration
3238-}
3239-
3240 func (s *configSuite) TestCompareConfig(c *C) {
3241 var cfg1 = testConfig2{
3242 A: 1,
3243@@ -304,6 +331,7 @@
3244 c.Check(cfg.A, Equals, 42)
3245 c.Check(cfg.B, Equals, "x")
3246 c.Check(cfg.C, DeepEquals, []string{"y", "z"})
3247+ c.Check(flag.Lookup("cfg@").Value.String(), Equals, cfgPath)
3248 }
3249
3250 func (s *configFlagsSuite) TestReadUsingFlagsHelp(c *C) {
3251
3252=== modified file 'debian/changelog'
3253--- debian/changelog 2014-04-23 11:54:00 +0000
3254+++ debian/changelog 2014-05-29 12:04:32 +0000
3255@@ -1,3 +1,9 @@
3256+ubuntu-push (0.21-0.ubuntu1) UNRELEASED; urgency=medium
3257+
3258+ * New upstream release: first auth bits, and Qt dependency.
3259+
3260+ -- John Lenton <john.lenton@canonical.com> Tue, 15 Apr 2014 14:04:35 +0100
3261+
3262 ubuntu-push (0.2.1+14.04.20140423.1-0ubuntu1) trusty; urgency=high
3263
3264 [ Samuele Pedroni ]
3265
3266=== modified file 'debian/control'
3267--- debian/control 2014-03-25 16:26:20 +0000
3268+++ debian/control 2014-05-29 12:04:32 +0000
3269@@ -14,6 +14,8 @@
3270 libgcrypt11-dev,
3271 libglib2.0-dev (>= 2.31.6),
3272 libwhoopsie-dev,
3273+ libubuntuoneauth-2.0-dev,
3274+ cmake,
3275 Standards-Version: 3.9.5
3276 Homepage: http://launchpad.net/ubuntu-push
3277 Vcs-Bzr: lp:ubuntu-push
3278
3279=== modified file 'debian/rules'
3280--- debian/rules 2014-03-24 12:22:55 +0000
3281+++ debian/rules 2014-05-29 12:04:32 +0000
3282@@ -2,9 +2,13 @@
3283 # -*- makefile -*-
3284
3285 export DH_GOPKG := launchpad.net/ubuntu-push
3286-export DEB_BUILD_OPTIONS := nostrip
3287 export UBUNTU_PUSH_TEST_RESOURCES_ROOT := $(CURDIR)
3288
3289+override_dh_auto_build:
3290+ cd $$( find ./ -type d -regex '\./[^/]*/src/launchpad.net' -printf "%h\n" | head -n1)
3291+ dh_auto_build --buildsystem=golang
3292+ (cd signing-helper && cmake . && make)
3293+
3294 override_dh_install:
3295 dh_install -Xusr/bin/cmd -Xusr/bin/dev --fail-missing
3296
3297
3298=== modified file 'debian/ubuntu-push-client.install'
3299--- debian/ubuntu-push-client.install 2014-03-26 16:27:19 +0000
3300+++ debian/ubuntu-push-client.install 2014-05-29 12:04:32 +0000
3301@@ -1,4 +1,5 @@
3302 #!/usr/bin/dh-exec
3303 debian/config.json /etc/xdg/ubuntu-push-client
3304 debian/ubuntu-push-client.conf /usr/share/upstart/sessions
3305+signing-helper/signing-helper /usr/lib/ubuntu-push-client
3306 usr/bin/ubuntu-push => /usr/lib/ubuntu-push-client/ubuntu-push-client
3307
3308=== modified file 'dependencies.tsv'
3309--- dependencies.tsv 2014-03-12 13:23:26 +0000
3310+++ dependencies.tsv 2014-05-29 12:04:32 +0000
3311@@ -2,3 +2,4 @@
3312 launchpad.net/go-dbus/v1 bzr james@jamesh.id.au-20140206110213-pbzcr6ucaz3rqmnw 125
3313 launchpad.net/go-xdg/v0 bzr john.lenton@canonical.com-20140208094800-gubd5md7cro3mtxa 10
3314 launchpad.net/gocheck bzr gustavo@niemeyer.net-20140127131816-zshobk1qqme626xw 86
3315+launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid bzr samuele.pedroni@canonical.com-20140130122455-pm9h8etl4owp90lg 1
3316
3317=== added directory 'nih'
3318=== added directory 'nih/cnih'
3319=== added file 'nih/cnih/cnih.go'
3320--- nih/cnih/cnih.go 1970-01-01 00:00:00 +0000
3321+++ nih/cnih/cnih.go 2014-05-29 12:04:32 +0000
3322@@ -0,0 +1,28 @@
3323+package cnih
3324+
3325+/*
3326+#cgo pkg-config: dbus-1 libnih libnih-dbus
3327+#include <stdlib.h>
3328+#include <nih/alloc.h>
3329+#include <libnih-dbus.h>
3330+
3331+// a small wrapper because cgo doesn't handle varargs
3332+char *cuote (const char *id) {
3333+ return nih_dbus_path (NULL, "", id, NULL);
3334+}
3335+*/
3336+import "C"
3337+
3338+import (
3339+ "unsafe"
3340+)
3341+
3342+func Quote(s []byte) string {
3343+ cs := C.CString(string(s))
3344+ defer C.free(unsafe.Pointer(cs))
3345+
3346+ cq := C.cuote(cs)
3347+ defer C.nih_free(unsafe.Pointer(cq))
3348+
3349+ return C.GoString(cq)[1:]
3350+}
3351
3352=== added file 'nih/nih.go'
3353--- nih/nih.go 1970-01-01 00:00:00 +0000
3354+++ nih/nih.go 2014-05-29 12:04:32 +0000
3355@@ -0,0 +1,68 @@
3356+/*
3357+ Copyright 2013-2014 Canonical Ltd.
3358+
3359+ This program is free software: you can redistribute it and/or modify it
3360+ under the terms of the GNU General Public License version 3, as published
3361+ by the Free Software Foundation.
3362+
3363+ This program is distributed in the hope that it will be useful, but
3364+ WITHOUT ANY WARRANTY; without even the implied warranties of
3365+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3366+ PURPOSE. See the GNU General Public License for more details.
3367+
3368+ You should have received a copy of the GNU General Public License along
3369+ with this program. If not, see <http://www.gnu.org/licenses/>.
3370+*/
3371+
3372+// package nih reimplements libnih-dbus's nih_dbus_path's path element
3373+// quoting.
3374+//
3375+// Reimplementing libnih is a wonderful exercise that everybody should persue
3376+// at least thrice.
3377+package nih
3378+
3379+import "strconv"
3380+
3381+// Quote() takes a byte slice and quotes it á la libnih.
3382+func Quote(s []byte) []byte {
3383+ if len(s) == 0 {
3384+ return []byte{'_'}
3385+ }
3386+ out := make([]byte, 0, 2*len(s))
3387+ for _, c := range s {
3388+ if ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') {
3389+ out = append(out, c)
3390+ } else {
3391+ if c < 16 {
3392+ out = append(out, '_', '0')
3393+ } else {
3394+ out = append(out, '_')
3395+ }
3396+ out = strconv.AppendUint(out, uint64(c), 16)
3397+ }
3398+ }
3399+
3400+ return out
3401+}
3402+
3403+// Quote() takes a byte slice and undoes the damage done to it by the quoting.
3404+func Unquote(s []byte) []byte {
3405+ out := make([]byte, 0, len(s))
3406+
3407+ for i := 0; i < len(s); i++ {
3408+ if s[i] == '_' {
3409+ if len(s) < i+3 {
3410+ break
3411+ }
3412+ num, err := strconv.ParseUint(string(s[i+1:i+3]), 16, 8)
3413+ if err == nil {
3414+ out = append(out, byte(num))
3415+ }
3416+ i += 2
3417+ } else {
3418+ out = append(out, s[i])
3419+ }
3420+ }
3421+
3422+ return out
3423+}
3424
3425=== added file 'nih/nih_test.go'
3426--- nih/nih_test.go 1970-01-01 00:00:00 +0000
3427+++ nih/nih_test.go 2014-05-29 12:04:32 +0000
3428@@ -0,0 +1,57 @@
3429+/*
3430+ Copyright 2013-2014 Canonical Ltd.
3431+
3432+ This program is free software: you can redistribute it and/or modify it
3433+ under the terms of the GNU General Public License version 3, as published
3434+ by the Free Software Foundation.
3435+
3436+ This program is distributed in the hope that it will be useful, but
3437+ WITHOUT ANY WARRANTY; without even the implied warranties of
3438+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3439+ PURPOSE. See the GNU General Public License for more details.
3440+
3441+ You should have received a copy of the GNU General Public License along
3442+ with this program. If not, see <http://www.gnu.org/licenses/>.
3443+*/
3444+
3445+package nih
3446+
3447+import (
3448+ "testing"
3449+
3450+ . "launchpad.net/gocheck"
3451+
3452+ "launchpad.net/ubuntu-push/nih/cnih"
3453+)
3454+
3455+func TestNIH(t *testing.T) { TestingT(t) }
3456+
3457+type nihSuite struct{}
3458+
3459+var _ = Suite(&nihSuite{})
3460+
3461+func (ns *nihSuite) TestQuote(c *C) {
3462+ for i, s := range []struct {
3463+ raw []byte
3464+ quoted []byte
3465+ }{
3466+ {[]byte("test"), []byte("test")},
3467+ {[]byte("foo/bar.baz"), []byte("foo_2fbar_2ebaz")},
3468+ {[]byte("test_thing"), []byte("test_5fthing")},
3469+ {[]byte("\x01\x0f\x10\xff"), []byte("_01_0f_10_ff")},
3470+ {[]byte{}, []byte{'_'}},
3471+ } {
3472+ c.Check(string(s.quoted), Equals, cnih.Quote(s.raw), Commentf("iter %d (%s)", i, string(s.quoted)))
3473+ c.Check(string(Quote(s.raw)), DeepEquals, string(s.quoted), Commentf("iter %d (%s)", i, string(s.quoted)))
3474+ c.Check(Unquote(s.quoted), DeepEquals, s.raw, Commentf("iter %d (%s)", i, string(s.quoted)))
3475+ c.Check(string(Quote(s.raw)), Equals, cnih.Quote(s.raw), Commentf("iter %d (%s)", i, string(s.quoted)))
3476+ }
3477+
3478+ // check one cnih doesn't like
3479+ c.Check(Quote([]byte{0}), DeepEquals, []byte("_00"))
3480+
3481+ // check we don't panic with some weird ones
3482+ for i, s := range []string{"foo_", "foo_a", "foo_zz"} {
3483+ c.Check(Unquote([]byte(s)), DeepEquals, []byte("foo"), Commentf("iter %d (%s)", i, s))
3484+ }
3485+}
3486
3487=== modified file 'protocol/messages.go'
3488--- protocol/messages.go 2014-04-04 13:54:45 +0000
3489+++ protocol/messages.go 2014-05-29 12:04:32 +0000
3490@@ -20,6 +20,7 @@
3491
3492 import (
3493 "encoding/json"
3494+ "fmt"
3495 )
3496
3497 // System channel id using a shortened hex-encoded form for the NIL UUID.
3498@@ -54,6 +55,14 @@
3499 Split() (done bool)
3500 }
3501
3502+// OnewayMsg are messages that are not to be followed by a response,
3503+// after sending them the session either aborts or continues.
3504+type OnewayMsg interface {
3505+ SplittableMsg
3506+ // continue session after the message?
3507+ OnewayContinue() bool
3508+}
3509+
3510 // CONNBROKEN message, server side is breaking the connection for reason.
3511 type ConnBrokenMsg struct {
3512 Type string `json:"T"`
3513@@ -65,11 +74,35 @@
3514 return true
3515 }
3516
3517+func (m *ConnBrokenMsg) OnewayContinue() bool {
3518+ return false
3519+}
3520+
3521 // CONNBROKEN reasons
3522 const (
3523 BrokenHostMismatch = "host-mismatch"
3524 )
3525
3526+// CONNWARN message, server side is warning about partial functionality
3527+// because reason.
3528+type ConnWarnMsg struct {
3529+ Type string `json:"T"`
3530+ // reason
3531+ Reason string
3532+}
3533+
3534+func (m *ConnWarnMsg) Split() bool {
3535+ return true
3536+}
3537+func (m *ConnWarnMsg) OnewayContinue() bool {
3538+ return true
3539+}
3540+
3541+// CONNWARN reasons
3542+const (
3543+ WarnUnauthorized = "unauthorized"
3544+)
3545+
3546 // PING/PONG messages
3547 type PingPongMsg struct {
3548 Type string `json:"T"`
3549@@ -111,8 +144,9 @@
3550 }
3551
3552 // Reset resets the splitting state if the message storage is to be
3553-// reused.
3554+// reused and sets the proper Type.
3555 func (b *BroadcastMsg) Reset() {
3556+ b.Type = "broadcast"
3557 b.splitting = 0
3558 }
3559
3560@@ -120,6 +154,41 @@
3561 type NotificationsMsg struct {
3562 Type string `json:"T"`
3563 Notifications []Notification
3564+ splitting int
3565+}
3566+
3567+// Reset resets the splitting state if the message storage is to be
3568+// reused and sets the proper Type.
3569+func (m *NotificationsMsg) Reset() {
3570+ m.Type = "notifications"
3571+ m.splitting = 0
3572+}
3573+
3574+func (m *NotificationsMsg) Split() bool {
3575+ if m.splitting != 0 {
3576+ m.Notifications = m.Notifications[len(m.Notifications):m.splitting]
3577+ }
3578+ notifs := m.Notifications
3579+ var size int
3580+ for i, notif := range notifs {
3581+ size += len(notif.Payload) + len(notif.AppId) + len(notif.MsgId) + notificationOverhead
3582+ if size > maxPayloadSize {
3583+ m.splitting = len(notifs)
3584+ m.Notifications = notifs[:i]
3585+ return false
3586+ }
3587+ }
3588+ return true
3589+}
3590+
3591+var notificationOverhead int
3592+
3593+func init() {
3594+ buf, err := json.Marshal(Notification{})
3595+ if err != nil {
3596+ panic(fmt.Errorf("failed to compute Notification marshal overhead: %v", err))
3597+ }
3598+ notificationOverhead = len(buf) - 4 // - 4 for the null from P(ayload)
3599 }
3600
3601 // A single unicast notification
3602@@ -130,6 +199,19 @@
3603 Payload json.RawMessage `json:"P"`
3604 }
3605
3606+// ExtractPayloads gets only the payloads out of a slice of notications.
3607+func ExtractPayloads(notifications []Notification) []json.RawMessage {
3608+ n := len(notifications)
3609+ if n == 0 {
3610+ return nil
3611+ }
3612+ payloads := make([]json.RawMessage, n)
3613+ for i := 0; i < n; i++ {
3614+ payloads[i] = notifications[i].Payload
3615+ }
3616+ return payloads
3617+}
3618+
3619 // ACKnowledgement message
3620 type AckMsg struct {
3621 Type string `json:"T"`
3622
3623=== modified file 'protocol/messages_test.go'
3624--- protocol/messages_test.go 2014-04-04 13:19:10 +0000
3625+++ protocol/messages_test.go 2014-05-29 12:04:32 +0000
3626@@ -100,10 +100,100 @@
3627 c.Check(b.TopLevel, Equals, int64(n))
3628 c.Check(n1+n2+n3, Equals, n)
3629 // reset
3630+ b.Type = ""
3631 b.Reset()
3632+ c.Check(b.Type, Equals, "broadcast")
3633 c.Check(b.splitting, Equals, 0)
3634 }
3635
3636-func (s *messagesSuite) TestSplitConnBrokenMsg(c *C) {
3637- c.Check((&ConnBrokenMsg{}).Split(), Equals, true)
3638+func (s *messagesSuite) TestConnBrokenMsg(c *C) {
3639+ m := &ConnBrokenMsg{}
3640+ c.Check(m.Split(), Equals, true)
3641+ c.Check(m.OnewayContinue(), Equals, false)
3642+}
3643+
3644+func (s *messagesSuite) TestConnWarnMsg(c *C) {
3645+ m := &ConnWarnMsg{}
3646+ c.Check(m.Split(), Equals, true)
3647+ c.Check(m.OnewayContinue(), Equals, true)
3648+}
3649+
3650+func (s *messagesSuite) TestExtractPayloads(c *C) {
3651+ c.Check(ExtractPayloads(nil), IsNil)
3652+ p1 := json.RawMessage(`{"a":1}`)
3653+ p2 := json.RawMessage(`{"b":2}`)
3654+ ns := []Notification{Notification{Payload: p1}, Notification{Payload: p2}}
3655+ c.Check(ExtractPayloads(ns), DeepEquals, []json.RawMessage{p1, p2})
3656+}
3657+
3658+func (s *messagesSuite) TestSplitNotificationsMsgNop(c *C) {
3659+ n := &NotificationsMsg{
3660+ Type: "notifications",
3661+ Notifications: []Notification{
3662+ Notification{"app1", "msg1", json.RawMessage(`{m:1}`)},
3663+ Notification{"app1", "msg1", json.RawMessage(`{m:2}`)},
3664+ },
3665+ }
3666+ done := n.Split()
3667+ c.Check(done, Equals, true)
3668+ c.Check(cap(n.Notifications), Equals, 2)
3669+ c.Check(len(n.Notifications), Equals, 2)
3670+}
3671+
3672+var payloadFmt2 = fmt.Sprintf(`{"b":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2-notificationOverhead-4-6)) // 4 = app1 6 = msg%03d
3673+
3674+func manyNotifications(c int) []Notification {
3675+ notifs := make([]Notification, 0, 1)
3676+ for i := 0; i < c; i++ {
3677+ notifs = append(notifs, Notification{
3678+ "app1",
3679+ fmt.Sprintf("msg%03d", i),
3680+ json.RawMessage(fmt.Sprintf(payloadFmt2, i)),
3681+ })
3682+ }
3683+ return notifs
3684+}
3685+
3686+func (s *messagesSuite) TestSplitNotificationsMsgMany(c *C) {
3687+ notifs := manyNotifications(33)
3688+ n := len(notifs)
3689+ // more interesting this way
3690+ c.Assert(cap(notifs), Not(Equals), n)
3691+ nm := &NotificationsMsg{
3692+ Type: "notifications",
3693+ Notifications: notifs,
3694+ }
3695+ done := nm.Split()
3696+ c.Assert(done, Equals, false)
3697+ n1 := len(nm.Notifications)
3698+ buf, err := json.Marshal(nm)
3699+ c.Assert(err, IsNil)
3700+ c.Assert(len(buf) <= 65535, Equals, true)
3701+ c.Check(len(buf)+len(notifs[n1].Payload) > maxPayloadSize, Equals, true)
3702+ done = nm.Split()
3703+ c.Assert(done, Equals, true)
3704+ n2 := len(nm.Notifications)
3705+ c.Check(n1+n2, Equals, n)
3706+
3707+ notifs = manyNotifications(61)
3708+ n = len(notifs)
3709+ nm = &NotificationsMsg{
3710+ Type: "notifications",
3711+ Notifications: notifs,
3712+ }
3713+ done = nm.Split()
3714+ c.Assert(done, Equals, false)
3715+ n1 = len(nm.Notifications)
3716+ done = nm.Split()
3717+ c.Assert(done, Equals, false)
3718+ n2 = len(nm.Notifications)
3719+ done = nm.Split()
3720+ c.Assert(done, Equals, true)
3721+ n3 := len(nm.Notifications)
3722+ c.Check(n1+n2+n3, Equals, n)
3723+ // reset
3724+ nm.Type = ""
3725+ nm.Reset()
3726+ c.Check(nm.Type, Equals, "notifications")
3727+ c.Check(nm.splitting, Equals, 0)
3728 }
3729
3730=== modified file 'protocol/state-diag-client.gv'
3731--- protocol/state-diag-client.gv 2014-01-16 20:07:13 +0000
3732+++ protocol/state-diag-client.gv 2014-05-29 12:04:32 +0000
3733@@ -2,7 +2,7 @@
3734 label = "State diagram for client";
3735 size="12,6";
3736 rankdir=LR;
3737- node [shape = doublecircle]; pingTimeout;
3738+ node [shape = doublecircle]; pingTimeout; connBroken;
3739 node [shape = circle];
3740 start1 -> start2 [ label = "Write wire version" ];
3741 start2 -> start3 [ label = "Write CONNECT" ];
3742@@ -13,4 +13,7 @@
3743 broadcast -> loop [label = "Write ACK"];
3744 loop -> pingTimeout [
3745 label = "Elapsed ping interval + exchange interval"];
3746+ loop -> connBroken [label = "Read CONNBROKEN"];
3747+ loop -> warn [label = "Read CONNWARN"];
3748+ warn -> loop;
3749 }
3750
3751=== modified file 'protocol/state-diag-client.svg'
3752--- protocol/state-diag-client.svg 2014-01-16 19:37:57 +0000
3753+++ protocol/state-diag-client.svg 2014-05-29 12:04:32 +0000
3754@@ -4,95 +4,123 @@
3755 <!-- Generated by graphviz version 2.26.3 (20100126.1600)
3756 -->
3757 <!-- Title: state_diagram_client Pages: 1 -->
3758-<svg width="864pt" height="279pt"
3759- viewBox="0.00 0.00 864.00 278.89" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3760-<g id="graph1" class="graph" transform="scale(0.683544 0.683544) rotate(0) translate(4 404)">
3761+<svg width="822pt" height="432pt"
3762+ viewBox="0.00 0.00 822.36 432.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3763+<g id="graph1" class="graph" transform="scale(0.650602 0.650602) rotate(0) translate(4 660)">
3764 <title>state_diagram_client</title>
3765-<polygon fill="white" stroke="white" points="-4,5 -4,-404 1261,-404 1261,5 -4,5"/>
3766+<polygon fill="white" stroke="white" points="-4,5 -4,-660 1261,-660 1261,5 -4,5"/>
3767 <text text-anchor="middle" x="628" y="-9.4" font-family="Times Roman,serif" font-size="14.00">State diagram for client</text>
3768 <!-- pingTimeout -->
3769 <g id="node1" class="node"><title>pingTimeout</title>
3770-<ellipse fill="none" stroke="black" cx="1180" cy="-324" rx="72.1249" ry="72.1249"/>
3771-<ellipse fill="none" stroke="black" cx="1180" cy="-324" rx="76.1249" ry="76.1249"/>
3772-<text text-anchor="middle" x="1180" y="-320.4" font-family="Times Roman,serif" font-size="14.00">pingTimeout</text>
3773+<ellipse fill="none" stroke="black" cx="1180" cy="-580" rx="72.1249" ry="72.1249"/>
3774+<ellipse fill="none" stroke="black" cx="1180" cy="-580" rx="76.1249" ry="76.1249"/>
3775+<text text-anchor="middle" x="1180" y="-576.4" font-family="Times Roman,serif" font-size="14.00">pingTimeout</text>
3776+</g>
3777+<!-- connBroken -->
3778+<g id="node2" class="node"><title>connBroken</title>
3779+<ellipse fill="none" stroke="black" cx="1180" cy="-413" rx="68.8251" ry="69.2965"/>
3780+<ellipse fill="none" stroke="black" cx="1180" cy="-413" rx="72.7978" ry="73.2965"/>
3781+<text text-anchor="middle" x="1180" y="-409.4" font-family="Times Roman,serif" font-size="14.00">connBroken</text>
3782 </g>
3783 <!-- start1 -->
3784-<g id="node2" class="node"><title>start1</title>
3785-<ellipse fill="none" stroke="black" cx="42" cy="-166" rx="41.2167" ry="41.7193"/>
3786-<text text-anchor="middle" x="42" y="-162.4" font-family="Times Roman,serif" font-size="14.00">start1</text>
3787+<g id="node3" class="node"><title>start1</title>
3788+<ellipse fill="none" stroke="black" cx="42" cy="-231" rx="41.2167" ry="41.7193"/>
3789+<text text-anchor="middle" x="42" y="-227.4" font-family="Times Roman,serif" font-size="14.00">start1</text>
3790 </g>
3791 <!-- start2 -->
3792-<g id="node4" class="node"><title>start2</title>
3793-<ellipse fill="none" stroke="black" cx="292" cy="-166" rx="41.2167" ry="41.7193"/>
3794-<text text-anchor="middle" x="292" y="-162.4" font-family="Times Roman,serif" font-size="14.00">start2</text>
3795+<g id="node5" class="node"><title>start2</title>
3796+<ellipse fill="none" stroke="black" cx="292" cy="-231" rx="41.2167" ry="41.7193"/>
3797+<text text-anchor="middle" x="292" y="-227.4" font-family="Times Roman,serif" font-size="14.00">start2</text>
3798 </g>
3799 <!-- start1&#45;&gt;start2 -->
3800 <g id="edge2" class="edge"><title>start1&#45;&gt;start2</title>
3801-<path fill="none" stroke="black" d="M83.5631,-166C126.547,-166 193.757,-166 240.181,-166"/>
3802-<polygon fill="black" stroke="black" points="240.338,-169.5 250.338,-166 240.338,-162.5 240.338,-169.5"/>
3803-<text text-anchor="middle" x="167" y="-171.4" font-family="Times Roman,serif" font-size="14.00">Write wire version</text>
3804+<path fill="none" stroke="black" d="M83.5631,-231C126.547,-231 193.757,-231 240.181,-231"/>
3805+<polygon fill="black" stroke="black" points="240.338,-234.5 250.338,-231 240.338,-227.5 240.338,-234.5"/>
3806+<text text-anchor="middle" x="167" y="-236.4" font-family="Times Roman,serif" font-size="14.00">Write wire version</text>
3807 </g>
3808 <!-- start3 -->
3809-<g id="node6" class="node"><title>start3</title>
3810-<ellipse fill="none" stroke="black" cx="526" cy="-166" rx="41.2167" ry="41.7193"/>
3811-<text text-anchor="middle" x="526" y="-162.4" font-family="Times Roman,serif" font-size="14.00">start3</text>
3812+<g id="node7" class="node"><title>start3</title>
3813+<ellipse fill="none" stroke="black" cx="526" cy="-231" rx="41.2167" ry="41.7193"/>
3814+<text text-anchor="middle" x="526" y="-227.4" font-family="Times Roman,serif" font-size="14.00">start3</text>
3815 </g>
3816 <!-- start2&#45;&gt;start3 -->
3817 <g id="edge4" class="edge"><title>start2&#45;&gt;start3</title>
3818-<path fill="none" stroke="black" d="M333.565,-166C372.875,-166 431.992,-166 474.321,-166"/>
3819-<polygon fill="black" stroke="black" points="474.429,-169.5 484.429,-166 474.429,-162.5 474.429,-169.5"/>
3820-<text text-anchor="middle" x="409" y="-171.4" font-family="Times Roman,serif" font-size="14.00">Write CONNECT</text>
3821+<path fill="none" stroke="black" d="M333.565,-231C372.875,-231 431.992,-231 474.321,-231"/>
3822+<polygon fill="black" stroke="black" points="474.429,-234.5 484.429,-231 474.429,-227.5 474.429,-234.5"/>
3823+<text text-anchor="middle" x="409" y="-236.4" font-family="Times Roman,serif" font-size="14.00">Write CONNECT</text>
3824 </g>
3825 <!-- loop -->
3826-<g id="node8" class="node"><title>loop</title>
3827-<ellipse fill="none" stroke="black" cx="746" cy="-166" rx="31.8198" ry="31.8198"/>
3828-<text text-anchor="middle" x="746" y="-162.4" font-family="Times Roman,serif" font-size="14.00">loop</text>
3829+<g id="node9" class="node"><title>loop</title>
3830+<ellipse fill="none" stroke="black" cx="746" cy="-231" rx="31.8198" ry="31.8198"/>
3831+<text text-anchor="middle" x="746" y="-227.4" font-family="Times Roman,serif" font-size="14.00">loop</text>
3832 </g>
3833 <!-- start3&#45;&gt;loop -->
3834 <g id="edge6" class="edge"><title>start3&#45;&gt;loop</title>
3835-<path fill="none" stroke="black" d="M567.639,-166C606.633,-166 664.616,-166 703.793,-166"/>
3836-<polygon fill="black" stroke="black" points="703.818,-169.5 713.818,-166 703.818,-162.5 703.818,-169.5"/>
3837-<text text-anchor="middle" x="641" y="-171.4" font-family="Times Roman,serif" font-size="14.00">Read CONNACK</text>
3838+<path fill="none" stroke="black" d="M567.639,-231C606.633,-231 664.616,-231 703.793,-231"/>
3839+<polygon fill="black" stroke="black" points="703.818,-234.5 713.818,-231 703.818,-227.5 703.818,-234.5"/>
3840+<text text-anchor="middle" x="641" y="-236.4" font-family="Times Roman,serif" font-size="14.00">Read CONNACK</text>
3841 </g>
3842 <!-- loop&#45;&gt;pingTimeout -->
3843 <g id="edge16" class="edge"><title>loop&#45;&gt;pingTimeout</title>
3844-<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"/>
3845-<polygon fill="black" stroke="black" points="1093.67,-319.928 1104.02,-317.68 1094.53,-312.981 1093.67,-319.928"/>
3846-<text text-anchor="middle" x="941" y="-319.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval + exchange interval</text>
3847+<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"/>
3848+<polygon fill="black" stroke="black" points="1093.96,-573.992 1104.39,-572.09 1095.05,-567.078 1093.96,-573.992"/>
3849+<text text-anchor="middle" x="941" y="-572.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval + exchange interval</text>
3850+</g>
3851+<!-- loop&#45;&gt;connBroken -->
3852+<g id="edge18" class="edge"><title>loop&#45;&gt;connBroken</title>
3853+<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"/>
3854+<polygon fill="black" stroke="black" points="1096.19,-416.445 1106.33,-413.388 1096.5,-409.452 1096.19,-416.445"/>
3855+<text text-anchor="middle" x="941" y="-417.4" font-family="Times Roman,serif" font-size="14.00">Read CONNBROKEN</text>
3856 </g>
3857 <!-- pong -->
3858-<g id="node10" class="node"><title>pong</title>
3859-<ellipse fill="none" stroke="black" cx="1180" cy="-195" rx="34.8574" ry="35.3553"/>
3860-<text text-anchor="middle" x="1180" y="-191.4" font-family="Times Roman,serif" font-size="14.00">pong</text>
3861+<g id="node11" class="node"><title>pong</title>
3862+<ellipse fill="none" stroke="black" cx="1180" cy="-287" rx="34.8574" ry="35.3553"/>
3863+<text text-anchor="middle" x="1180" y="-283.4" font-family="Times Roman,serif" font-size="14.00">pong</text>
3864 </g>
3865 <!-- loop&#45;&gt;pong -->
3866 <g id="edge8" class="edge"><title>loop&#45;&gt;pong</title>
3867-<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"/>
3868-<polygon fill="black" stroke="black" points="1134.89,-202.186 1144.62,-198.003 1134.4,-195.203 1134.89,-202.186"/>
3869-<text text-anchor="middle" x="941" y="-207.4" font-family="Times Roman,serif" font-size="14.00">Read PING</text>
3870+<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"/>
3871+<polygon fill="black" stroke="black" points="1135.49,-297.371 1144.94,-292.588 1134.57,-290.432 1135.49,-297.371"/>
3872+<text text-anchor="middle" x="941" y="-307.4" font-family="Times Roman,serif" font-size="14.00">Read PING</text>
3873 </g>
3874 <!-- broadcast -->
3875-<g id="node12" class="node"><title>broadcast</title>
3876-<ellipse fill="none" stroke="black" cx="1180" cy="-84" rx="58.1882" ry="58.6899"/>
3877-<text text-anchor="middle" x="1180" y="-80.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>
3878+<g id="node13" class="node"><title>broadcast</title>
3879+<ellipse fill="none" stroke="black" cx="1180" cy="-176" rx="58.1882" ry="58.6899"/>
3880+<text text-anchor="middle" x="1180" y="-172.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>
3881 </g>
3882 <!-- loop&#45;&gt;broadcast -->
3883 <g id="edge10" class="edge"><title>loop&#45;&gt;broadcast</title>
3884-<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"/>
3885-<polygon fill="black" stroke="black" points="1113.34,-101.917 1122.5,-96.5998 1112.02,-95.0419 1113.34,-101.917"/>
3886-<text text-anchor="middle" x="941" y="-136.4" font-family="Times Roman,serif" font-size="14.00">Read BROADCAST</text>
3887+<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"/>
3888+<polygon fill="black" stroke="black" points="1112.44,-189.353 1121.9,-184.574 1111.53,-182.413 1112.44,-189.353"/>
3889+<text text-anchor="middle" x="941" y="-217.4" font-family="Times Roman,serif" font-size="14.00">Read BROADCAST</text>
3890+</g>
3891+<!-- warn -->
3892+<g id="node19" class="node"><title>warn</title>
3893+<ellipse fill="none" stroke="black" cx="1180" cy="-63" rx="36.7696" ry="36.7696"/>
3894+<text text-anchor="middle" x="1180" y="-59.4" font-family="Times Roman,serif" font-size="14.00">warn</text>
3895+</g>
3896+<!-- loop&#45;&gt;warn -->
3897+<g id="edge20" class="edge"><title>loop&#45;&gt;warn</title>
3898+<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"/>
3899+<polygon fill="black" stroke="black" points="1135.26,-79.0068 1144.04,-73.0757 1133.48,-72.2376 1135.26,-79.0068"/>
3900+<text text-anchor="middle" x="941" y="-136.4" font-family="Times Roman,serif" font-size="14.00">Read CONNWARN</text>
3901 </g>
3902 <!-- pong&#45;&gt;loop -->
3903 <g id="edge12" class="edge"><title>pong&#45;&gt;loop</title>
3904-<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"/>
3905-<polygon fill="black" stroke="black" points="787.736,-157.528 778.16,-162.06 788.472,-164.489 787.736,-157.528"/>
3906-<text text-anchor="middle" x="941" y="-168.4" font-family="Times Roman,serif" font-size="14.00">Write PONG</text>
3907+<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"/>
3908+<polygon fill="black" stroke="black" points="788.085,-227.996 778.035,-231.348 787.982,-234.995 788.085,-227.996"/>
3909+<text text-anchor="middle" x="941" y="-254.4" font-family="Times Roman,serif" font-size="14.00">Write PONG</text>
3910 </g>
3911 <!-- broadcast&#45;&gt;loop -->
3912 <g id="edge14" class="edge"><title>broadcast&#45;&gt;loop</title>
3913-<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"/>
3914-<polygon fill="black" stroke="black" points="756.044,-124.528 755.336,-135.099 762.482,-127.277 756.044,-124.528"/>
3915-<text text-anchor="middle" x="941" y="-86.4" font-family="Times Roman,serif" font-size="14.00">Write ACK</text>
3916+<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"/>
3917+<polygon fill="black" stroke="black" points="763.182,-192.043 760.465,-202.284 768.975,-195.973 763.182,-192.043"/>
3918+<text text-anchor="middle" x="941" y="-172.4" font-family="Times Roman,serif" font-size="14.00">Write ACK</text>
3919+</g>
3920+<!-- warn&#45;&gt;loop -->
3921+<g id="edge22" class="edge"><title>warn&#45;&gt;loop</title>
3922+<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"/>
3923+<polygon fill="black" stroke="black" points="751.574,-188.904 752.686,-199.44 758.387,-190.51 751.574,-188.904"/>
3924 </g>
3925 </g>
3926 </svg>
3927
3928=== modified file 'protocol/state-diag-session.gv'
3929--- protocol/state-diag-session.gv 2014-01-16 20:07:13 +0000
3930+++ protocol/state-diag-session.gv 2014-05-29 12:04:32 +0000
3931@@ -2,6 +2,7 @@
3932 label = "State diagram for session";
3933 size="12,6";
3934 rankdir=LR;
3935+ node [shape = doublecircle]; stop;
3936 node [shape = circle];
3937 start1 -> start2 [ label = "Read wire version" ];
3938 start2 -> start3 [ label = "Read CONNECT" ];
3939@@ -17,4 +18,13 @@
3940 split_broadcast -> split_ack_wait [label = "Write split BROADCAST"];
3941 split_ack_wait -> split_broadcast [label = "Read ACK"];
3942 split_broadcast -> loop [label = "All split msgs written"];
3943+ // other
3944+ loop -> conn_broken [label = "Receive connbroken request"];
3945+ loop -> conn_warn [label = "Receive connwarn request"];
3946+ conn_broken -> stop [label = "Write CONNBROKEN"];
3947+ conn_warn -> loop [label = "Write CONNWARN"];
3948+ // timeouts
3949+ ack_wait -> stop [label = "Elapsed exhange timeout"];
3950+ split_ack_wait -> stop [label = "Elapsed exhange timeout"];
3951+ pong_wait -> stop [label = "Elapsed exhange timeout"];
3952 }
3953
3954=== modified file 'protocol/state-diag-session.svg'
3955--- protocol/state-diag-session.svg 2014-01-16 19:37:57 +0000
3956+++ protocol/state-diag-session.svg 2014-05-29 12:04:32 +0000
3957@@ -4,139 +4,197 @@
3958 <!-- Generated by graphviz version 2.26.3 (20100126.1600)
3959 -->
3960 <!-- Title: state_diagram_session Pages: 1 -->
3961-<svg width="864pt" height="208pt"
3962- viewBox="0.00 0.00 864.00 207.94" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3963-<g id="graph1" class="graph" transform="scale(0.435923 0.435923) rotate(0) translate(4 473)">
3964+<svg width="864pt" height="266pt"
3965+ viewBox="0.00 0.00 864.00 265.73" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3966+<g id="graph1" class="graph" transform="scale(0.367035 0.367035) rotate(0) translate(4 720)">
3967 <title>state_diagram_session</title>
3968-<polygon fill="white" stroke="white" points="-4,5 -4,-473 1979,-473 1979,5 -4,5"/>
3969-<text text-anchor="middle" x="987" y="-9.4" font-family="Times Roman,serif" font-size="14.00">State diagram for session</text>
3970+<polygon fill="white" stroke="white" points="-4,5 -4,-720 2351,-720 2351,5 -4,5"/>
3971+<text text-anchor="middle" x="1173" y="-9.4" font-family="Times Roman,serif" font-size="14.00">State diagram for session</text>
3972+<!-- stop -->
3973+<g id="node1" class="node"><title>stop</title>
3974+<ellipse fill="none" stroke="black" cx="2309" cy="-335" rx="32.0813" ry="32.5269"/>
3975+<ellipse fill="none" stroke="black" cx="2309" cy="-335" rx="36.0265" ry="36.5269"/>
3976+<text text-anchor="middle" x="2309" y="-331.4" font-family="Times Roman,serif" font-size="14.00">stop</text>
3977+</g>
3978 <!-- start1 -->
3979-<g id="node1" class="node"><title>start1</title>
3980-<ellipse fill="none" stroke="black" cx="42" cy="-294" rx="41.2167" ry="41.7193"/>
3981-<text text-anchor="middle" x="42" y="-290.4" font-family="Times Roman,serif" font-size="14.00">start1</text>
3982+<g id="node2" class="node"><title>start1</title>
3983+<ellipse fill="none" stroke="black" cx="42" cy="-395" rx="41.2167" ry="41.7193"/>
3984+<text text-anchor="middle" x="42" y="-391.4" font-family="Times Roman,serif" font-size="14.00">start1</text>
3985 </g>
3986 <!-- start2 -->
3987-<g id="node3" class="node"><title>start2</title>
3988-<ellipse fill="none" stroke="black" cx="286" cy="-294" rx="41.2167" ry="41.7193"/>
3989-<text text-anchor="middle" x="286" y="-290.4" font-family="Times Roman,serif" font-size="14.00">start2</text>
3990+<g id="node4" class="node"><title>start2</title>
3991+<ellipse fill="none" stroke="black" cx="286" cy="-395" rx="41.2167" ry="41.7193"/>
3992+<text text-anchor="middle" x="286" y="-391.4" font-family="Times Roman,serif" font-size="14.00">start2</text>
3993 </g>
3994 <!-- start1&#45;&gt;start2 -->
3995 <g id="edge2" class="edge"><title>start1&#45;&gt;start2</title>
3996-<path fill="none" stroke="black" d="M83.6679,-294C125.213,-294 189.13,-294 233.981,-294"/>
3997-<polygon fill="black" stroke="black" points="234.096,-297.5 244.096,-294 234.096,-290.5 234.096,-297.5"/>
3998-<text text-anchor="middle" x="164" y="-299.4" font-family="Times Roman,serif" font-size="14.00">Read wire version</text>
3999+<path fill="none" stroke="black" d="M83.6679,-395C125.213,-395 189.13,-395 233.981,-395"/>
4000+<polygon fill="black" stroke="black" points="234.096,-398.5 244.096,-395 234.096,-391.5 234.096,-398.5"/>
4001+<text text-anchor="middle" x="164" y="-400.4" font-family="Times Roman,serif" font-size="14.00">Read wire version</text>
4002 </g>
4003 <!-- start3 -->
4004-<g id="node5" class="node"><title>start3</title>
4005-<ellipse fill="none" stroke="black" cx="516" cy="-294" rx="41.2167" ry="41.7193"/>
4006-<text text-anchor="middle" x="516" y="-290.4" font-family="Times Roman,serif" font-size="14.00">start3</text>
4007+<g id="node6" class="node"><title>start3</title>
4008+<ellipse fill="none" stroke="black" cx="537" cy="-395" rx="41.2167" ry="41.7193"/>
4009+<text text-anchor="middle" x="537" y="-391.4" font-family="Times Roman,serif" font-size="14.00">start3</text>
4010 </g>
4011 <!-- start2&#45;&gt;start3 -->
4012 <g id="edge4" class="edge"><title>start2&#45;&gt;start3</title>
4013-<path fill="none" stroke="black" d="M327.651,-294C365.959,-294 422.903,-294 464.145,-294"/>
4014-<polygon fill="black" stroke="black" points="464.271,-297.5 474.271,-294 464.271,-290.5 464.271,-297.5"/>
4015-<text text-anchor="middle" x="401" y="-299.4" font-family="Times Roman,serif" font-size="14.00">Read CONNECT</text>
4016+<path fill="none" stroke="black" d="M327.729,-395C370.886,-395 438.364,-395 484.973,-395"/>
4017+<polygon fill="black" stroke="black" points="485.171,-398.5 495.171,-395 485.171,-391.5 485.171,-398.5"/>
4018+<text text-anchor="middle" x="401" y="-400.4" font-family="Times Roman,serif" font-size="14.00">Read CONNECT</text>
4019 </g>
4020 <!-- loop -->
4021-<g id="node7" class="node"><title>loop</title>
4022-<ellipse fill="none" stroke="black" cx="740" cy="-294" rx="31.8198" ry="31.8198"/>
4023-<text text-anchor="middle" x="740" y="-290.4" font-family="Times Roman,serif" font-size="14.00">loop</text>
4024+<g id="node8" class="node"><title>loop</title>
4025+<ellipse fill="none" stroke="black" cx="790" cy="-395" rx="31.8198" ry="31.8198"/>
4026+<text text-anchor="middle" x="790" y="-391.4" font-family="Times Roman,serif" font-size="14.00">loop</text>
4027 </g>
4028 <!-- start3&#45;&gt;loop -->
4029 <g id="edge6" class="edge"><title>start3&#45;&gt;loop</title>
4030-<path fill="none" stroke="black" d="M557.608,-294C597.53,-294 657.517,-294 697.677,-294"/>
4031-<polygon fill="black" stroke="black" points="697.687,-297.5 707.687,-294 697.687,-290.5 697.687,-297.5"/>
4032-<text text-anchor="middle" x="633" y="-299.4" font-family="Times Roman,serif" font-size="14.00">Write CONNACK</text>
4033+<path fill="none" stroke="black" d="M578.778,-395C625.49,-395 700.728,-395 747.665,-395"/>
4034+<polygon fill="black" stroke="black" points="747.805,-398.5 757.805,-395 747.805,-391.5 747.805,-398.5"/>
4035+<text text-anchor="middle" x="675" y="-400.4" font-family="Times Roman,serif" font-size="14.00">Write CONNACK</text>
4036 </g>
4037 <!-- ping -->
4038-<g id="node9" class="node"><title>ping</title>
4039-<ellipse fill="none" stroke="black" cx="1063" cy="-416" rx="32.0265" ry="32.5269"/>
4040-<text text-anchor="middle" x="1063" y="-412.4" font-family="Times Roman,serif" font-size="14.00">ping</text>
4041+<g id="node10" class="node"><title>ping</title>
4042+<ellipse fill="none" stroke="black" cx="1135" cy="-593" rx="32.0265" ry="32.5269"/>
4043+<text text-anchor="middle" x="1135" y="-589.4" font-family="Times Roman,serif" font-size="14.00">ping</text>
4044 </g>
4045 <!-- loop&#45;&gt;ping -->
4046 <g id="edge8" class="edge"><title>loop&#45;&gt;ping</title>
4047-<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"/>
4048-<polygon fill="black" stroke="black" points="1020.35,-420.33 1030.38,-416.906 1020.4,-413.33 1020.35,-420.33"/>
4049-<text text-anchor="middle" x="881" y="-418.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval</text>
4050+<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"/>
4051+<polygon fill="black" stroke="black" points="1092.15,-591.877 1102.53,-589.734 1093.08,-584.939 1092.15,-591.877"/>
4052+<text text-anchor="middle" x="946" y="-583.4" font-family="Times Roman,serif" font-size="14.00">Elapsed ping interval</text>
4053 </g>
4054 <!-- broadcast -->
4055-<g id="node11" class="node"><title>broadcast</title>
4056-<ellipse fill="none" stroke="black" cx="1063" cy="-200" rx="58.1882" ry="58.6899"/>
4057-<text text-anchor="middle" x="1063" y="-196.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>
4058+<g id="node12" class="node"><title>broadcast</title>
4059+<ellipse fill="none" stroke="black" cx="1135" cy="-281" rx="58.1882" ry="58.6899"/>
4060+<text text-anchor="middle" x="1135" y="-277.4" font-family="Times Roman,serif" font-size="14.00">broadcast</text>
4061 </g>
4062 <!-- loop&#45;&gt;broadcast -->
4063 <g id="edge10" class="edge"><title>loop&#45;&gt;broadcast</title>
4064-<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"/>
4065-<polygon fill="black" stroke="black" points="995.396,-212.238 1004.75,-207.269 994.34,-205.318 995.396,-212.238"/>
4066-<text text-anchor="middle" x="881" y="-267.4" font-family="Times Roman,serif" font-size="14.00">Receive broadcast request</text>
4067+<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"/>
4068+<polygon fill="black" stroke="black" points="1066.94,-289.319 1076.53,-284.811 1066.22,-282.355 1066.94,-289.319"/>
4069+<text text-anchor="middle" x="946" y="-348.4" font-family="Times Roman,serif" font-size="14.00">Receive broadcast request</text>
4070+</g>
4071+<!-- conn_broken -->
4072+<g id="node26" class="node"><title>conn_broken</title>
4073+<ellipse fill="none" stroke="black" cx="1361" cy="-99" rx="73.0388" ry="73.5391"/>
4074+<text text-anchor="middle" x="1361" y="-95.4" font-family="Times Roman,serif" font-size="14.00">conn_broken</text>
4075+</g>
4076+<!-- loop&#45;&gt;conn_broken -->
4077+<g id="edge28" class="edge"><title>loop&#45;&gt;conn_broken</title>
4078+<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"/>
4079+<polygon fill="black" stroke="black" points="1278.34,-85.2954 1288.79,-83.4998 1279.5,-78.392 1278.34,-85.2954"/>
4080+<text text-anchor="middle" x="946" y="-160.4" font-family="Times Roman,serif" font-size="14.00">Receive connbroken request</text>
4081+</g>
4082+<!-- conn_warn -->
4083+<g id="node28" class="node"><title>conn_warn</title>
4084+<ellipse fill="none" stroke="black" cx="1135" cy="-477" rx="65.7609" ry="65.7609"/>
4085+<text text-anchor="middle" x="1135" y="-473.4" font-family="Times Roman,serif" font-size="14.00">conn_warn</text>
4086+</g>
4087+<!-- loop&#45;&gt;conn_warn -->
4088+<g id="edge30" class="edge"><title>loop&#45;&gt;conn_warn</title>
4089+<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"/>
4090+<polygon fill="black" stroke="black" points="1058.71,-479.855 1068.85,-476.786 1059.01,-472.861 1058.71,-479.855"/>
4091+<text text-anchor="middle" x="946" y="-480.4" font-family="Times Roman,serif" font-size="14.00">Receive connwarn request</text>
4092 </g>
4093 <!-- pong_wait -->
4094-<g id="node13" class="node"><title>pong_wait</title>
4095-<ellipse fill="none" stroke="black" cx="1526" cy="-406" rx="62.9325" ry="62.9325"/>
4096-<text text-anchor="middle" x="1526" y="-402.4" font-family="Times Roman,serif" font-size="14.00">pong_wait</text>
4097+<g id="node14" class="node"><title>pong_wait</title>
4098+<ellipse fill="none" stroke="black" cx="537" cy="-653" rx="62.9325" ry="62.9325"/>
4099+<text text-anchor="middle" x="537" y="-649.4" font-family="Times Roman,serif" font-size="14.00">pong_wait</text>
4100 </g>
4101 <!-- ping&#45;&gt;pong_wait -->
4102 <g id="edge12" class="edge"><title>ping&#45;&gt;pong_wait</title>
4103-<path fill="none" stroke="black" d="M1095.56,-415.297C1169.19,-413.707 1350.04,-409.8 1452.36,-407.591"/>
4104-<polygon fill="black" stroke="black" points="1452.69,-411.084 1462.61,-407.369 1452.54,-404.086 1452.69,-411.084"/>
4105-<text text-anchor="middle" x="1289" y="-418.4" font-family="Times Roman,serif" font-size="14.00">Write PING</text>
4106+<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"/>
4107+<polygon fill="black" stroke="black" points="609.573,-662.637 599.214,-664.858 608.697,-669.582 609.573,-662.637"/>
4108+<text text-anchor="middle" x="790" y="-670.4" font-family="Times Roman,serif" font-size="14.00">Write PING</text>
4109 </g>
4110 <!-- ack_wait -->
4111-<g id="node15" class="node"><title>ack_wait</title>
4112-<ellipse fill="none" stroke="black" cx="1526" cy="-269" rx="55.1543" ry="55.1543"/>
4113-<text text-anchor="middle" x="1526" y="-265.4" font-family="Times Roman,serif" font-size="14.00">ack_wait</text>
4114+<g id="node16" class="node"><title>ack_wait</title>
4115+<ellipse fill="none" stroke="black" cx="1598" cy="-373" rx="55.1543" ry="55.1543"/>
4116+<text text-anchor="middle" x="1598" y="-369.4" font-family="Times Roman,serif" font-size="14.00">ack_wait</text>
4117 </g>
4118 <!-- broadcast&#45;&gt;ack_wait -->
4119 <g id="edge14" class="edge"><title>broadcast&#45;&gt;ack_wait</title>
4120-<path fill="none" stroke="black" d="M1121.17,-208.669C1207.93,-221.599 1370.7,-245.856 1461.17,-259.339"/>
4121-<polygon fill="black" stroke="black" points="1460.9,-262.837 1471.3,-260.849 1461.93,-255.913 1460.9,-262.837"/>
4122-<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>
4123+<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"/>
4124+<polygon fill="black" stroke="black" points="1534.79,-356.813 1545.37,-356.254 1536.75,-350.093 1534.79,-356.813"/>
4125+<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>
4126 </g>
4127 <!-- split_broadcast -->
4128-<g id="node17" class="node"><title>split_broadcast</title>
4129-<ellipse fill="none" stroke="black" cx="1526" cy="-110" rx="84.1457" ry="84.1457"/>
4130-<text text-anchor="middle" x="1526" y="-106.4" font-family="Times Roman,serif" font-size="14.00">split_broadcast</text>
4131+<g id="node18" class="node"><title>split_broadcast</title>
4132+<ellipse fill="none" stroke="black" cx="1598" cy="-216" rx="84.1457" ry="84.1457"/>
4133+<text text-anchor="middle" x="1598" y="-212.4" font-family="Times Roman,serif" font-size="14.00">split_broadcast</text>
4134 </g>
4135 <!-- broadcast&#45;&gt;split_broadcast -->
4136 <g id="edge16" class="edge"><title>broadcast&#45;&gt;split_broadcast</title>
4137-<path fill="none" stroke="black" d="M1120.7,-188.783C1199.06,-173.553 1340.01,-146.154 1433.29,-128.021"/>
4138-<polygon fill="black" stroke="black" points="1434.15,-131.421 1443.29,-126.077 1432.81,-124.549 1434.15,-131.421"/>
4139-<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>
4140+<path fill="none" stroke="black" d="M1193.17,-272.834C1271.44,-261.846 1411.56,-242.174 1504.66,-229.104"/>
4141+<polygon fill="black" stroke="black" points="1505.23,-232.558 1514.65,-227.702 1504.26,-225.626 1505.23,-232.558"/>
4142+<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>
4143+</g>
4144+<!-- pong_wait&#45;&gt;stop -->
4145+<g id="edge40" class="edge"><title>pong_wait&#45;&gt;stop</title>
4146+<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"/>
4147+<polygon fill="black" stroke="black" points="2292.5,-378.343 2293.84,-367.834 2286.24,-375.212 2292.5,-378.343"/>
4148+<text text-anchor="middle" x="1361" y="-658.4" font-family="Times Roman,serif" font-size="14.00">Elapsed exhange timeout</text>
4149 </g>
4150 <!-- pong_wait&#45;&gt;loop -->
4151 <g id="edge18" class="edge"><title>pong_wait&#45;&gt;loop</title>
4152-<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"/>
4153-<polygon fill="black" stroke="black" points="781.898,-299.011 771.42,-300.582 780.59,-305.888 781.898,-299.011"/>
4154-<text text-anchor="middle" x="1063" y="-359.4" font-family="Times Roman,serif" font-size="14.00">Read PONG</text>
4155+<path fill="none" stroke="black" d="M581.359,-607.765C632.774,-555.333 716.085,-470.376 760.273,-425.314"/>
4156+<polygon fill="black" stroke="black" points="762.774,-427.763 767.277,-418.172 757.776,-422.862 762.774,-427.763"/>
4157+<text text-anchor="middle" x="675" y="-574.4" font-family="Times Roman,serif" font-size="14.00">Read PONG</text>
4158+</g>
4159+<!-- ack_wait&#45;&gt;stop -->
4160+<g id="edge36" class="edge"><title>ack_wait&#45;&gt;stop</title>
4161+<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"/>
4162+<polygon fill="black" stroke="black" points="2263.58,-346.389 2272.99,-341.517 2262.59,-339.459 2263.58,-346.389"/>
4163+<text text-anchor="middle" x="1972" y="-373.4" font-family="Times Roman,serif" font-size="14.00">Elapsed exhange timeout</text>
4164 </g>
4165 <!-- ack_wait&#45;&gt;loop -->
4166 <g id="edge20" class="edge"><title>ack_wait&#45;&gt;loop</title>
4167-<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"/>
4168-<polygon fill="black" stroke="black" points="781.977,-289.56 772.059,-293.288 782.136,-296.558 781.977,-289.56"/>
4169-<text text-anchor="middle" x="1063" y="-292.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>
4170+<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"/>
4171+<polygon fill="black" stroke="black" points="831.485,-386.79 821.871,-391.242 832.163,-393.757 831.485,-386.79"/>
4172+<text text-anchor="middle" x="1135" y="-389.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>
4173 </g>
4174 <!-- split_broadcast&#45;&gt;loop -->
4175 <g id="edge26" class="edge"><title>split_broadcast&#45;&gt;loop</title>
4176-<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"/>
4177-<polygon fill="black" stroke="black" points="753.014,-253.718 751.785,-264.241 759.308,-256.781 753.014,-253.718"/>
4178-<text text-anchor="middle" x="1063" y="-120.4" font-family="Times Roman,serif" font-size="14.00">All split msgs written</text>
4179+<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"/>
4180+<polygon fill="black" stroke="black" points="800.027,-353.699 799.658,-364.287 806.549,-356.24 800.027,-353.699"/>
4181+<text text-anchor="middle" x="1135" y="-201.4" font-family="Times Roman,serif" font-size="14.00">All split msgs written</text>
4182 </g>
4183 <!-- split_ack_wait -->
4184-<g id="node21" class="node"><title>split_ack_wait</title>
4185-<ellipse fill="none" stroke="black" cx="1893" cy="-110" rx="80.1095" ry="80.6102"/>
4186-<text text-anchor="middle" x="1893" y="-106.4" font-family="Times Roman,serif" font-size="14.00">split_ack_wait</text>
4187+<g id="node22" class="node"><title>split_ack_wait</title>
4188+<ellipse fill="none" stroke="black" cx="1972" cy="-257" rx="80.1095" ry="80.6102"/>
4189+<text text-anchor="middle" x="1972" y="-253.4" font-family="Times Roman,serif" font-size="14.00">split_ack_wait</text>
4190 </g>
4191 <!-- split_broadcast&#45;&gt;split_ack_wait -->
4192 <g id="edge22" class="edge"><title>split_broadcast&#45;&gt;split_ack_wait</title>
4193-<path fill="none" stroke="black" d="M1610.2,-110C1667.61,-110 1743.59,-110 1802.33,-110"/>
4194-<polygon fill="black" stroke="black" points="1802.35,-113.5 1812.35,-110 1802.34,-106.5 1802.35,-113.5"/>
4195-<text text-anchor="middle" x="1711" y="-115.4" font-family="Times Roman,serif" font-size="14.00">Write split BROADCAST</text>
4196+<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"/>
4197+<polygon fill="black" stroke="black" points="1881.23,-255.523 1891.43,-252.676 1881.68,-248.537 1881.23,-255.523"/>
4198+<text text-anchor="middle" x="1783" y="-256.4" font-family="Times Roman,serif" font-size="14.00">Write split BROADCAST</text>
4199+</g>
4200+<!-- split_ack_wait&#45;&gt;stop -->
4201+<g id="edge38" class="edge"><title>split_ack_wait&#45;&gt;stop</title>
4202+<path fill="none" stroke="black" d="M2050.59,-275.189C2116.55,-290.456 2208.51,-311.74 2263.08,-324.372"/>
4203+<polygon fill="black" stroke="black" points="2262.62,-327.857 2273.15,-326.702 2264.2,-321.037 2262.62,-327.857"/>
4204+<text text-anchor="middle" x="2166" y="-327.4" font-family="Times Roman,serif" font-size="14.00">Elapsed exhange timeout</text>
4205 </g>
4206 <!-- split_ack_wait&#45;&gt;split_broadcast -->
4207 <g id="edge24" class="edge"><title>split_ack_wait&#45;&gt;split_broadcast</title>
4208-<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"/>
4209-<polygon fill="black" stroke="black" points="1617.23,-85.8043 1607.87,-90.7602 1618.28,-92.7256 1617.23,-85.8043"/>
4210-<text text-anchor="middle" x="1711" y="-93.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>
4211+<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"/>
4212+<polygon fill="black" stroke="black" points="1691.25,-202.053 1681.52,-206.256 1691.74,-209.035 1691.25,-202.053"/>
4213+<text text-anchor="middle" x="1783" y="-218.4" font-family="Times Roman,serif" font-size="14.00">Read ACK</text>
4214+</g>
4215+<!-- conn_broken&#45;&gt;stop -->
4216+<g id="edge32" class="edge"><title>conn_broken&#45;&gt;stop</title>
4217+<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"/>
4218+<polygon fill="black" stroke="black" points="2275.42,-302.808 2284.91,-307.517 2280.43,-297.917 2275.42,-302.808"/>
4219+<text text-anchor="middle" x="1783" y="-127.4" font-family="Times Roman,serif" font-size="14.00">Write CONNBROKEN</text>
4220+</g>
4221+<!-- conn_warn&#45;&gt;loop -->
4222+<g id="edge34" class="edge"><title>conn_warn&#45;&gt;loop</title>
4223+<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"/>
4224+<polygon fill="black" stroke="black" points="831.758,-388.346 821.959,-392.373 832.131,-395.337 831.758,-388.346"/>
4225+<text text-anchor="middle" x="946" y="-420.4" font-family="Times Roman,serif" font-size="14.00">Write CONNWARN</text>
4226 </g>
4227 </g>
4228 </svg>
4229
4230=== added file 'scripts/broadcast'
4231--- scripts/broadcast 1970-01-01 00:00:00 +0000
4232+++ scripts/broadcast 2014-05-29 12:04:32 +0000
4233@@ -0,0 +1,59 @@
4234+#!/usr/bin/python
4235+"""
4236+send broadcast to channel with payload data
4237+"""
4238+import argparse
4239+import json
4240+import requests
4241+import requests.auth
4242+import datetime
4243+import sys
4244+
4245+
4246+def main():
4247+ parser = argparse.ArgumentParser(description=__doc__)
4248+ parser.add_argument('channel', nargs=1)
4249+ parser.add_argument('data', nargs=1)
4250+ parser.add_argument('-H', '--host',
4251+ help="host:port (default: %(default)s)",
4252+ default="localhost:8080")
4253+ parser.add_argument('-e', '--expire',
4254+ help="expire after the given amount of time, "
4255+ "use 'd' suffix for days, 's' for seconds"
4256+ " (default: %(default)s)", default="1d")
4257+ parser.add_argument('--no-https', action='store_true', default=False)
4258+ parser.add_argument('--insecure', action='store_true', default=False,
4259+ help="don't check host/certs with https")
4260+ parser.add_argument('-u', '--user', default="")
4261+ parser.add_argument('-p', '--password', default="")
4262+ args = parser.parse_args()
4263+ expire_on = datetime.datetime.utcnow()
4264+ ex = args.expire
4265+ if ex.endswith('d'):
4266+ delta = datetime.timedelta(days=int(ex[:-1]))
4267+ elif ex.endswith('s'):
4268+ delta = datetime.timedelta(seconds=int(ex[:-1]))
4269+ else:
4270+ print >>sys.stderr, "unknown --expire suffix:", ex
4271+ sys.exit(1)
4272+ expire_on += delta
4273+ scheme = 'https'
4274+ if args.no_https:
4275+ scheme = 'http'
4276+ url = "%s://%s/broadcast" % (scheme, args.host)
4277+ body = {
4278+ 'channel': args.channel[0],
4279+ 'data': json.loads(args.data[0]),
4280+ 'expire_on': expire_on.replace(microsecond=0).isoformat()+"Z"
4281+ }
4282+ xauth = {}
4283+ if args.user and args.password:
4284+ xauth = {'auth': requests.auth.HTTPBasicAuth(args.user, args.password)}
4285+ headers = {'Content-Type': 'application/json'}
4286+ r = requests.post(url, data=json.dumps(body), headers=headers,
4287+ verify=not args.insecure, **xauth)
4288+ print r.status_code
4289+ print r.text
4290+
4291+if __name__ == '__main__':
4292+ main()
4293
4294=== added file 'scripts/deps.sh'
4295--- scripts/deps.sh 1970-01-01 00:00:00 +0000
4296+++ scripts/deps.sh 2014-05-29 12:04:32 +0000
4297@@ -0,0 +1,28 @@
4298+#!/bin/sh
4299+set -eu
4300+
4301+PROJECT=launchpad.net/ubuntu-push
4302+
4303+mktpl () {
4304+ for f in GoFiles CgoFiles; do
4305+ echo '{{join .'$f' "\\n"}}'
4306+ done
4307+}
4308+
4309+directs () {
4310+ go list -f "$(mktpl)" $1 | sed -e "s|^|$1|"
4311+}
4312+
4313+indirects () {
4314+ for i in $(go list -f '{{join .Deps "\n"}}' $1 | grep ^$PROJECT ); do
4315+ directs $i/
4316+ done
4317+ wait
4318+}
4319+
4320+norm () {
4321+ tr "\n" " " | sed -r -e "s|$PROJECT/?||g" -e 's/ *$//'
4322+}
4323+
4324+out=".$1.deps"
4325+( echo -n "${1%.go} ${out}: "; indirects $(echo $1 | norm) | norm ) > "$out"
4326
4327=== added file 'scripts/unicast'
4328--- scripts/unicast 1970-01-01 00:00:00 +0000
4329+++ scripts/unicast 2014-05-29 12:04:32 +0000
4330@@ -0,0 +1,65 @@
4331+#!/usr/bin/python
4332+"""
4333+send broadcast to channel with payload data
4334+"""
4335+import argparse
4336+import json
4337+import requests
4338+import requests.auth
4339+import datetime
4340+import sys
4341+
4342+
4343+def main():
4344+ parser = argparse.ArgumentParser(description=__doc__)
4345+ parser.add_argument('reg', nargs=1) # userid:deviceid or reg
4346+ parser.add_argument('appid', nargs=1)
4347+ parser.add_argument('data', nargs=1)
4348+ parser.add_argument('-H', '--host',
4349+ help="host:port (default: %(default)s)",
4350+ default="localhost:8080")
4351+ parser.add_argument('-e', '--expire',
4352+ help="expire after the given amount of time, "
4353+ "use 'd' suffix for days, 's' for seconds"
4354+ " (default: %(default)s)", default="1d")
4355+ parser.add_argument('--no-https', action='store_true', default=False)
4356+ parser.add_argument('--insecure', action='store_true', default=False,
4357+ help="don't check host/certs with https")
4358+ parser.add_argument('-u', '--user', default="")
4359+ parser.add_argument('-p', '--password', default="")
4360+ args = parser.parse_args()
4361+ expire_on = datetime.datetime.utcnow()
4362+ ex = args.expire
4363+ if ex.endswith('d'):
4364+ delta = datetime.timedelta(days=int(ex[:-1]))
4365+ elif ex.endswith('s'):
4366+ delta = datetime.timedelta(seconds=int(ex[:-1]))
4367+ else:
4368+ print >>sys.stderr, "unknown --expire suffix:", ex
4369+ sys.exit(1)
4370+ expire_on += delta
4371+ scheme = 'https'
4372+ if args.no_https:
4373+ scheme = 'http'
4374+ url = "%s://%s/notify" % (scheme, args.host)
4375+ body = {
4376+ 'appid': args.appid[0],
4377+ 'data': json.loads(args.data[0]),
4378+ 'expire_on': expire_on.replace(microsecond=0).isoformat()+"Z"
4379+ }
4380+ reg = args.reg[0]
4381+ if ':' in reg:
4382+ userid, devid = reg.split(':', 1)
4383+ body['userid'] = userid
4384+ body['deviceid'] = devid
4385+ xauth = {}
4386+ if args.user and args.password:
4387+ xauth = {'auth': requests.auth.HTTPBasicAuth(args.user, args.password)}
4388+ headers = {'Content-Type': 'application/json'}
4389+ r = requests.post(url, data=json.dumps(body), headers=headers,
4390+ verify=not args.insecure, **xauth)
4391+ print r.status_code
4392+ print r.text
4393+
4394+if __name__ == '__main__':
4395+ main()
4396
4397=== modified file 'server/acceptance/acceptance_test.go'
4398--- server/acceptance/acceptance_test.go 2014-04-07 19:39:19 +0000
4399+++ server/acceptance/acceptance_test.go 2014-05-29 12:04:32 +0000
4400@@ -59,3 +59,6 @@
4401
4402 // broadcast
4403 var _ = Suite(&suites.BroadcastAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}})
4404+
4405+// unicast
4406+var _ = Suite(&suites.UnicastAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}, nil})
4407
4408=== modified file 'server/acceptance/acceptanceclient.go'
4409--- server/acceptance/acceptanceclient.go 2014-04-09 19:30:53 +0000
4410+++ server/acceptance/acceptanceclient.go 2014-05-29 12:04:32 +0000
4411@@ -24,6 +24,7 @@
4412 "errors"
4413 "fmt"
4414 "net"
4415+ "strings"
4416 "time"
4417
4418 "launchpad.net/ubuntu-push/protocol"
4419@@ -44,6 +45,7 @@
4420 Levels map[string]int64
4421 Insecure bool // don't verify certs
4422 Prefix string // prefix for events
4423+ Auth string
4424 // connection
4425 Connection net.Conn
4426 }
4427@@ -73,6 +75,7 @@
4428 Type string `json:"T"`
4429 protocol.BroadcastMsg
4430 protocol.NotificationsMsg
4431+ protocol.ConnWarnMsg
4432 }
4433
4434 // Run the session with the server, emits a stream of events.
4435@@ -93,6 +96,7 @@
4436 "device": sess.Model,
4437 "channel": sess.ImageChannel,
4438 },
4439+ Authorization: sess.Auth,
4440 })
4441 if err != nil {
4442 return err
4443@@ -125,9 +129,24 @@
4444 if sess.ReportPings {
4445 events <- sess.Prefix + "ping"
4446 }
4447+ case "notifications":
4448+ conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
4449+ err := proto.WriteMessage(protocol.AckMsg{Type: "ack"})
4450+ if err != nil {
4451+ return err
4452+ }
4453+ parts := make([]string, len(recv.Notifications))
4454+ for i, notif := range recv.Notifications {
4455+ pack, err := json.Marshal(&notif.Payload)
4456+ if err != nil {
4457+ return err
4458+ }
4459+ parts[i] = fmt.Sprintf("app:%v payload:%s;", notif.AppId, pack)
4460+ }
4461+ events <- fmt.Sprintf("%sunicast %s", sess.Prefix, strings.Join(parts, " "))
4462 case "broadcast":
4463 conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
4464- err := proto.WriteMessage(protocol.PingPongMsg{Type: "ack"})
4465+ err := proto.WriteMessage(protocol.AckMsg{Type: "ack"})
4466 if err != nil {
4467 return err
4468 }
4469@@ -136,6 +155,8 @@
4470 return err
4471 }
4472 events <- fmt.Sprintf("%sbroadcast chan:%v app:%v topLevel:%d payloads:%s", sess.Prefix, recv.ChanId, recv.AppId, recv.TopLevel, pack)
4473+ case "connwarn":
4474+ events <- fmt.Sprintf("%sconnwarn %s", sess.Prefix, recv.Reason)
4475 }
4476 }
4477 return nil
4478
4479=== modified file 'server/acceptance/cmd/acceptanceclient.go'
4480--- server/acceptance/cmd/acceptanceclient.go 2014-04-14 14:54:14 +0000
4481+++ server/acceptance/cmd/acceptanceclient.go 2014-05-29 12:04:32 +0000
4482@@ -22,7 +22,9 @@
4483 "fmt"
4484 "log"
4485 "os"
4486+ "os/exec"
4487 "path/filepath"
4488+ "strings"
4489
4490 "launchpad.net/ubuntu-push/config"
4491 "launchpad.net/ubuntu-push/server/acceptance"
4492@@ -40,12 +42,13 @@
4493 ExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"`
4494 // server connection config
4495 Addr config.ConfigHostPort
4496- CertPEMFile string `json:"cert_pem_file"`
4497+ CertPEMFile string `json:"cert_pem_file"`
4498+ AuthHelper []string `json:"auth_helper"`
4499 }
4500
4501 func main() {
4502 flag.Usage = func() {
4503- fmt.Fprintf(os.Stderr, "Usage: acceptancclient [options] <config.json> <device id>\n")
4504+ fmt.Fprintf(os.Stderr, "Usage: acceptancclient [options] <device id>\n")
4505 flag.PrintDefaults()
4506 }
4507 missingArg := func(what string) {
4508@@ -53,28 +56,24 @@
4509 flag.Usage()
4510 os.Exit(2)
4511 }
4512- flag.Parse()
4513+ cfg := &configuration{}
4514+ err := config.ReadFilesDefaults(cfg, map[string]interface{}{
4515+ "exchange_timeout": "5s",
4516+ "cert_pem_file": "",
4517+ "auth_helper": []string{},
4518+ }, "<flags>")
4519+ if err != nil {
4520+ log.Fatalf("reading config: %v", err)
4521+ }
4522 narg := flag.NArg()
4523 switch {
4524 case narg < 1:
4525- missingArg("config file")
4526- case narg < 2:
4527 missingArg("device-id")
4528 }
4529- configFName := flag.Arg(0)
4530- f, err := os.Open(configFName)
4531- if err != nil {
4532- log.Fatalf("reading config: %v", err)
4533- }
4534- cfg := &configuration{}
4535- err = config.ReadConfig(f, cfg)
4536- if err != nil {
4537- log.Fatalf("reading config: %v", err)
4538- }
4539 session := &acceptance.ClientSession{
4540 ExchangeTimeout: cfg.ExchangeTimeout.TimeDuration(),
4541 ServerAddr: cfg.Addr.HostPort(),
4542- DeviceId: flag.Arg(1),
4543+ DeviceId: flag.Arg(0),
4544 // flags
4545 Model: *deviceModel,
4546 ImageChannel: *imageChannel,
4547@@ -82,12 +81,21 @@
4548 Insecure: *insecureFlag,
4549 }
4550 log.Printf("with: %#v", session)
4551- if !*insecureFlag {
4552- session.CertPEMBlock, err = config.LoadFile(cfg.CertPEMFile, filepath.Dir(configFName))
4553+ if !*insecureFlag && cfg.CertPEMFile != "" {
4554+ cfgDir := filepath.Dir(flag.Lookup("cfg@").Value.String())
4555+ log.Printf("cert: %v relToDir: %v", cfg.CertPEMFile, cfgDir)
4556+ session.CertPEMBlock, err = config.LoadFile(cfg.CertPEMFile, cfgDir)
4557 if err != nil {
4558 log.Fatalf("reading CertPEMFile: %v", err)
4559 }
4560 }
4561+ if len(cfg.AuthHelper) != 0 {
4562+ auth, err := exec.Command(cfg.AuthHelper[0], cfg.AuthHelper[1:]...).Output()
4563+ if err != nil {
4564+ log.Fatalf("auth helper: %v", err)
4565+ }
4566+ session.Auth = strings.TrimSpace(string(auth))
4567+ }
4568 err = session.Dial()
4569 if err != nil {
4570 log.Fatalln(err)
4571
4572=== modified file 'server/acceptance/suites/broadcast.go'
4573--- server/acceptance/suites/broadcast.go 2014-04-07 19:39:19 +0000
4574+++ server/acceptance/suites/broadcast.go 2014-05-29 12:04:32 +0000
4575@@ -29,14 +29,11 @@
4576 "launchpad.net/ubuntu-push/server/api"
4577 )
4578
4579-// BroadCastAcceptanceSuite has tests about broadcast.
4580+// BroadcastAcceptanceSuite has tests about broadcast.
4581 type BroadcastAcceptanceSuite struct {
4582 AcceptanceSuite
4583 }
4584
4585-// Long after the end of the tests.
4586-var future = time.Now().Add(9 * time.Hour).Format(time.RFC3339)
4587-
4588 func (s *BroadcastAcceptanceSuite) TestBroadcastToConnected(c *C) {
4589 events, errCh, stop := s.StartClient(c, "DEVB", nil)
4590 got, err := s.PostRequest("/broadcast", &api.Broadcast{
4591@@ -91,7 +88,7 @@
4592 c.Check(len(errCh), Equals, 0)
4593 }
4594
4595-func (s *BroadcastAcceptanceSuite) TestBroadcasLargeNeedsSplitting(c *C) {
4596+func (s *BroadcastAcceptanceSuite) TestBroadcastLargeNeedsSplitting(c *C) {
4597 // send bunch of broadcasts that will be pending
4598 payloadFmt := fmt.Sprintf(`{"img1/m1":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))
4599 for i := 0; i < 32; i++ {
4600@@ -106,8 +103,17 @@
4601
4602 events, errCh, stop := s.StartClient(c, "DEVC", nil)
4603 // gettting pending on connect
4604- c.Check(NextEvent(events, errCh), Matches, `broadcast chan:0 app: topLevel:30 payloads:\[{"img1/m1":0,.*`)
4605- c.Check(NextEvent(events, errCh), Matches, `broadcast chan:0 app: topLevel:32 payloads:\[.*`)
4606+ n := 0
4607+ for {
4608+ evt := NextEvent(events, errCh)
4609+ c.Check(evt, Matches, "broadcast chan:0 .*")
4610+ n += 1
4611+ if strings.Contains(evt, "topLevel:32") {
4612+ break
4613+ }
4614+ }
4615+ // was split
4616+ c.Check(n > 1, Equals, true)
4617 stop()
4618 c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
4619 c.Check(len(errCh), Equals, 0)
4620@@ -265,7 +271,11 @@
4621
4622 func (s *BroadcastAcceptanceSuite) TestGetHosts(c *C) {
4623 gh := gethosts.New("", s.ServerAPIURL+"/delivery-hosts", 2*time.Second)
4624- hosts, err := gh.Get()
4625+ host, err := gh.Get()
4626 c.Assert(err, IsNil)
4627- c.Check(hosts, DeepEquals, []string{s.ServerAddr})
4628+ expected := &gethosts.Host{
4629+ Domain: "localhost",
4630+ Hosts: []string{s.ServerAddr},
4631+ }
4632+ c.Check(host, DeepEquals, expected)
4633 }
4634
4635=== modified file 'server/acceptance/suites/suite.go'
4636--- server/acceptance/suites/suite.go 2014-04-03 16:47:47 +0000
4637+++ server/acceptance/suites/suite.go 2014-05-29 12:04:32 +0000
4638@@ -44,10 +44,19 @@
4639
4640 // Start a client.
4641 func (h *ServerHandle) StartClient(c *C, devId string, levels map[string]int64) (events <-chan string, errorCh <-chan error, stop func()) {
4642+ return h.StartClientAuth(c, devId, levels, "")
4643+}
4644+
4645+// Start a client with auth.
4646+func (h *ServerHandle) StartClientAuth(c *C, devId string, levels map[string]int64, auth string) (events <-chan string, errorCh <-chan error, stop func()) {
4647 errCh := make(chan error, 1)
4648 cliEvents := make(chan string, 10)
4649 sess := testClientSession(h.ServerAddr, devId, "m1", "img1", false)
4650 sess.Levels = levels
4651+ sess.Auth = auth
4652+ if auth != "" {
4653+ sess.ExchangeTimeout = 5 * time.Second
4654+ }
4655 err := sess.Dial()
4656 c.Assert(err, IsNil)
4657 clientShutdown := make(chan bool, 1) // abused as an atomic flag
4658@@ -186,3 +195,6 @@
4659 }
4660 return
4661 }
4662+
4663+// Long after the end of the tests.
4664+var future = time.Now().Add(9 * time.Hour).Format(time.RFC3339)
4665
4666=== added file 'server/acceptance/suites/unicast.go'
4667--- server/acceptance/suites/unicast.go 1970-01-01 00:00:00 +0000
4668+++ server/acceptance/suites/unicast.go 2014-05-29 12:04:32 +0000
4669@@ -0,0 +1,149 @@
4670+/*
4671+ Copyright 2013-2014 Canonical Ltd.
4672+
4673+ This program is free software: you can redistribute it and/or modify it
4674+ under the terms of the GNU General Public License version 3, as published
4675+ by the Free Software Foundation.
4676+
4677+ This program is distributed in the hope that it will be useful, but
4678+ WITHOUT ANY WARRANTY; without even the implied warranties of
4679+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4680+ PURPOSE. See the GNU General Public License for more details.
4681+
4682+ You should have received a copy of the GNU General Public License along
4683+ with this program. If not, see <http://www.gnu.org/licenses/>.
4684+*/
4685+
4686+package suites
4687+
4688+import (
4689+ "encoding/json"
4690+ "fmt"
4691+ "strings"
4692+
4693+ . "launchpad.net/gocheck"
4694+
4695+ "launchpad.net/ubuntu-push/server/api"
4696+)
4697+
4698+// UnicastAcceptanceSuite has tests about unicast.
4699+type UnicastAcceptanceSuite struct {
4700+ AcceptanceSuite
4701+ AssociatedAuth func(string) (string, string)
4702+}
4703+
4704+func (s *UnicastAcceptanceSuite) associatedAuth(deviceId string) (userId string, auth string) {
4705+ if s.AssociatedAuth != nil {
4706+ return s.AssociatedAuth(deviceId)
4707+ }
4708+ return deviceId, ""
4709+}
4710+
4711+func (s *UnicastAcceptanceSuite) TestUnicastToConnected(c *C) {
4712+ userId, auth := s.associatedAuth("DEV1")
4713+ events, errCh, stop := s.StartClientAuth(c, "DEV1", nil, auth)
4714+ got, err := s.PostRequest("/notify", &api.Unicast{
4715+ UserId: userId,
4716+ DeviceId: "DEV1",
4717+ AppId: "app1",
4718+ ExpireOn: future,
4719+ Data: json.RawMessage(`{"a": 42}`),
4720+ })
4721+ c.Assert(err, IsNil)
4722+ c.Assert(got, Matches, ".*ok.*")
4723+ c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"a":42};`)
4724+ stop()
4725+ c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
4726+ c.Check(len(errCh), Equals, 0)
4727+}
4728+
4729+func (s *UnicastAcceptanceSuite) TestUnicastCorrectDistribution(c *C) {
4730+ userId1, auth1 := s.associatedAuth("DEV1")
4731+ userId2, auth2 := s.associatedAuth("DEV2")
4732+ // start 1st client
4733+ events1, errCh1, stop1 := s.StartClientAuth(c, "DEV1", nil, auth1)
4734+ // start 2nd client
4735+ events2, errCh2, stop2 := s.StartClientAuth(c, "DEV2", nil, auth2)
4736+ // unicast to one and the other
4737+ got, err := s.PostRequest("/notify", &api.Unicast{
4738+ UserId: userId1,
4739+ DeviceId: "DEV1",
4740+ AppId: "app1",
4741+ ExpireOn: future,
4742+ Data: json.RawMessage(`{"to": 1}`),
4743+ })
4744+ c.Assert(err, IsNil)
4745+ c.Assert(got, Matches, ".*ok.*")
4746+ got, err = s.PostRequest("/notify", &api.Unicast{
4747+ UserId: userId2,
4748+ DeviceId: "DEV2",
4749+ AppId: "app1",
4750+ ExpireOn: future,
4751+ Data: json.RawMessage(`{"to": 2}`),
4752+ })
4753+ c.Assert(err, IsNil)
4754+ c.Assert(got, Matches, ".*ok.*")
4755+ c.Check(NextEvent(events1, errCh1), Equals, `unicast app:app1 payload:{"to":1};`)
4756+ c.Check(NextEvent(events2, errCh2), Equals, `unicast app:app1 payload:{"to":2};`)
4757+ stop1()
4758+ stop2()
4759+ c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
4760+ c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
4761+ c.Check(len(errCh1), Equals, 0)
4762+ c.Check(len(errCh2), Equals, 0)
4763+}
4764+
4765+func (s *UnicastAcceptanceSuite) TestUnicastPending(c *C) {
4766+ // send unicast that will be pending
4767+ userId, auth := s.associatedAuth("DEV1")
4768+ got, err := s.PostRequest("/notify", &api.Unicast{
4769+ UserId: userId,
4770+ DeviceId: "DEV1",
4771+ AppId: "app1",
4772+ ExpireOn: future,
4773+ Data: json.RawMessage(`{"a": 42}`),
4774+ })
4775+ c.Assert(err, IsNil)
4776+ c.Assert(got, Matches, ".*ok.*")
4777+
4778+ // get pending on connect
4779+ events, errCh, stop := s.StartClientAuth(c, "DEV1", nil, auth)
4780+ c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"a":42};`)
4781+ stop()
4782+ c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
4783+ c.Check(len(errCh), Equals, 0)
4784+}
4785+
4786+func (s *UnicastAcceptanceSuite) TestUnicastLargeNeedsSplitting(c *C) {
4787+ userId, auth := s.associatedAuth("DEV2")
4788+ // send bunch of unicasts that will be pending
4789+ payloadFmt := fmt.Sprintf(`{"serial":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))
4790+ for i := 0; i < 32; i++ {
4791+ got, err := s.PostRequest("/notify", &api.Unicast{
4792+ UserId: userId,
4793+ DeviceId: "DEV2",
4794+ AppId: "app1",
4795+ ExpireOn: future,
4796+ Data: json.RawMessage(fmt.Sprintf(payloadFmt, i)),
4797+ })
4798+ c.Assert(err, IsNil)
4799+ c.Assert(got, Matches, ".*ok.*")
4800+ }
4801+
4802+ events, errCh, stop := s.StartClientAuth(c, "DEV2", nil, auth)
4803+ // gettting pending on connect
4804+ n := 0
4805+ for {
4806+ evt := NextEvent(events, errCh)
4807+ c.Check(evt, Matches, "unicast app:app1 .*")
4808+ n += 1
4809+ if strings.Contains(evt, `"serial":31`) {
4810+ break
4811+ }
4812+ }
4813+ // was split
4814+ c.Check(n > 1, Equals, true)
4815+ stop()
4816+ c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
4817+ c.Check(len(errCh), Equals, 0)
4818+}
4819
4820=== modified file 'server/api/handlers.go'
4821--- server/api/handlers.go 2014-02-20 17:09:03 +0000
4822+++ server/api/handlers.go 2014-05-29 12:04:32 +0000
4823@@ -19,12 +19,15 @@
4824 package api
4825
4826 import (
4827+ "encoding/base64"
4828 "encoding/json"
4829 "fmt"
4830 "io"
4831 "net/http"
4832 "time"
4833
4834+ "launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid/uuid"
4835+
4836 "launchpad.net/ubuntu-push/logger"
4837 "launchpad.net/ubuntu-push/server/broker"
4838 "launchpad.net/ubuntu-push/server/store"
4839@@ -93,6 +96,11 @@
4840 ioError,
4841 "Could not read request body",
4842 }
4843+ ErrMissingIdField = &APIError{
4844+ http.StatusBadRequest,
4845+ invalidRequest,
4846+ "Missing id field",
4847+ }
4848 ErrMissingData = &APIError{
4849 http.StatusBadRequest,
4850 invalidRequest,
4851@@ -130,10 +138,17 @@
4852 }
4853 )
4854
4855-type Message struct {
4856- Registration string `json:"registration"`
4857- CoalesceTag string `json:"coalesce_tag"`
4858- Data json.RawMessage `json:"data"`
4859+type castCommon struct {
4860+}
4861+
4862+type Unicast struct {
4863+ UserId string `json:"userid"`
4864+ DeviceId string `json:"deviceid"`
4865+ AppId string `json:"appid"`
4866+ //Registration string `json:"registration"`
4867+ //CoalesceTag string `json:"coalesce_tag"`
4868+ ExpireOn string `json:"expire_on"`
4869+ Data json.RawMessage `json:"data"`
4870 }
4871
4872 // Broadcast request JSON object.
4873@@ -198,11 +213,11 @@
4874
4875 var zeroTime = time.Time{}
4876
4877-func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {
4878- if len(bcast.Data) == 0 {
4879+func checkCastCommon(data json.RawMessage, expireOn string) (time.Time, *APIError) {
4880+ if len(data) == 0 {
4881 return zeroTime, ErrMissingData
4882 }
4883- expire, err := time.Parse(time.RFC3339, bcast.ExpireOn)
4884+ expire, err := time.Parse(time.RFC3339, expireOn)
4885 if err != nil {
4886 return zeroTime, ErrInvalidExpiration
4887 }
4888@@ -212,6 +227,10 @@
4889 return expire, nil
4890 }
4891
4892+func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {
4893+ return checkCastCommon(bcast.Data, bcast.ExpireOn)
4894+}
4895+
4896 type StoreForRequest func(w http.ResponseWriter, request *http.Request) (store.PendingStore, error)
4897
4898 // context holds the interfaces to delegate to serving requests
4899@@ -234,6 +253,20 @@
4900 return sto, nil
4901 }
4902
4903+func (ctx *context) prepare(w http.ResponseWriter, request *http.Request, reqObj interface{}) (store.PendingStore, *APIError) {
4904+ body, apiErr := ReadBody(request, MaxRequestBodyBytes)
4905+ if apiErr != nil {
4906+ return nil, apiErr
4907+ }
4908+
4909+ err := json.Unmarshal(body, reqObj)
4910+ if err != nil {
4911+ return nil, ErrMalformedJSONObject
4912+ }
4913+
4914+ return ctx.getStore(w, request)
4915+}
4916+
4917 type BroadcastHandler struct {
4918 *context
4919 }
4920@@ -270,23 +303,13 @@
4921 }
4922 }()
4923
4924- body, apiErr := ReadBody(request, MaxRequestBodyBytes)
4925- if apiErr != nil {
4926- return
4927- }
4928-
4929- sto, apiErr := h.getStore(writer, request)
4930- if apiErr != nil {
4931- return
4932- }
4933- defer sto.Close()
4934-
4935 broadcast := &Broadcast{}
4936- err := json.Unmarshal(body, broadcast)
4937- if err != nil {
4938- apiErr = ErrMalformedJSONObject
4939+
4940+ sto, apiErr := h.prepare(writer, request, broadcast)
4941+ if apiErr != nil {
4942 return
4943 }
4944+ defer sto.Close()
4945
4946 apiErr = h.doBroadcast(sto, broadcast)
4947 if apiErr != nil {
4948@@ -297,6 +320,64 @@
4949 fmt.Fprintf(writer, `{"ok":true}`)
4950 }
4951
4952+type UnicastHandler struct {
4953+ *context
4954+}
4955+
4956+func checkUnicast(ucast *Unicast) (time.Time, *APIError) {
4957+ if ucast.UserId == "" || ucast.DeviceId == "" || ucast.AppId == "" {
4958+ return zeroTime, ErrMissingIdField
4959+ }
4960+ return checkCastCommon(ucast.Data, ucast.ExpireOn)
4961+}
4962+
4963+// use a base64 encoded TimeUUID
4964+var generateMsgId = func() string {
4965+ return base64.StdEncoding.EncodeToString(uuid.NewUUID())
4966+}
4967+
4968+func (h *UnicastHandler) doUnicast(sto store.PendingStore, ucast *Unicast) *APIError {
4969+ expire, apiErr := checkUnicast(ucast)
4970+ if apiErr != nil {
4971+ return apiErr
4972+ }
4973+ chanId := store.UnicastInternalChannelId(ucast.UserId, ucast.DeviceId)
4974+ msgId := generateMsgId()
4975+ err := sto.AppendToUnicastChannel(chanId, ucast.AppId, ucast.Data, msgId, expire)
4976+ if err != nil {
4977+ h.logger.Errorf("could not store notification: %v", err)
4978+ return ErrCouldNotStoreNotification
4979+ }
4980+
4981+ h.broker.Unicast(chanId)
4982+ return nil
4983+}
4984+
4985+func (h *UnicastHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
4986+ var apiErr *APIError
4987+ defer func() {
4988+ if apiErr != nil {
4989+ RespondError(writer, apiErr)
4990+ }
4991+ }()
4992+
4993+ unicast := &Unicast{}
4994+
4995+ sto, apiErr := h.prepare(writer, request, unicast)
4996+ if apiErr != nil {
4997+ return
4998+ }
4999+ defer sto.Close()
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches