Merge lp:~pedronis/ubuntu-push/register-for-unicast into lp:ubuntu-push

Proposed by Samuele Pedroni
Status: Superseded
Proposed branch: lp:~pedronis/ubuntu-push/register-for-unicast
Merge into: lp:ubuntu-push
Diff against target: 8332 lines (+4686/-769)
74 files modified
.bzrignore (+9/-0)
Makefile (+57/-12)
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 (+7/-0)
debian/config.json (+1/-0)
debian/control (+6/-1)
debian/rules (+5/-1)
debian/ubuntu-push-client.conf (+1/-0)
debian/ubuntu-push-client.install (+1/-0)
dependencies.tsv (+3/-2)
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/register (+43/-0)
scripts/unicast (+67/-0)
server/acceptance/acceptance_test.go (+3/-0)
server/acceptance/acceptanceclient.go (+22/-1)
server/acceptance/cmd/acceptanceclient.go (+28/-18)
server/acceptance/suites/broadcast.go (+19/-9)
server/acceptance/suites/suite.go (+14/-2)
server/acceptance/suites/unicast.go (+157/-0)
server/api/handlers.go (+202/-24)
server/api/handlers_test.go (+406/-13)
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 (+78/-18)
server/store/inmemory_test.go (+116/-3)
server/store/store.go (+82/-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:~pedronis/ubuntu-push/register-for-unicast
Reviewer Review Type Date Requested Status
Ubuntu Push Hackers Pending
Review via email: mp+222178@code.launchpad.net

Commit message

support registering tokens and sending notifications with a token; register script and scripts unicast support

Description of the change

support registering tokens and sending notifications with a token

register script and scripts unicast support

other small tweaks

To post a comment you must log in.
182. By Samuele Pedroni

move register to py3

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

Subscribers

People subscribed via source and target branches