Skip to content

Commit

Permalink
mobile: Go based replacement for MKAsyncTask (#347)
Browse files Browse the repository at this point in the history
See #347
  • Loading branch information
bassosimone committed Feb 18, 2020
1 parent 3fc6d3e commit 357cb1c
Show file tree
Hide file tree
Showing 14 changed files with 1,446 additions and 8 deletions.
132 changes: 132 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# OONI Measurement Engine

| Author | Simone Basso |
|--------------|--------------|
| Last-Updated | 2020-02-18 |
| Status | under review |

## Introduction

We want to write experiments in Go. This reduces our burden
compared to writing them using C/C++ code.

Go consumers of probe-engine shall directly use its Go API. We
will discuss the Go API in a future revision of this spec.

For mobile apps, we want to replace these MK APIs:

- [measurement-kit/android-libs](https://github.com/measurement-kit/android-libs)

- [measurement-kit/mkall-ios](https://github.com/measurement-kit/mkall-ios)

We also want consumers of [measurement-kit's FFI API](https://git.io/Jv4Rv)
to be able to replace measurement-kit with probe-engine.

## APIs to replace

### Mobile APIs

We define a Go API that `gomobile` binds to a Java/ObjectiveC
API that is close enough to the MK's mobile APIs.

### FFI API

We define a CGO API such that `go build -buildmode=c-shared`
yields an API reasonably close to MK's FFI API.

## Running experiments

It seems the generic API for enabling running experiments both on
mobile devices and for FFI consumers is like:

```Go
type Task struct{ ... }
func StartTask(input string) (*Task, error)
func (t *Task) Interrupt()
func (t *Task) IsDone() bool
func (t *Task) WaitForNextEvent() string
```

This should be enough to generate a suitable mobile API when
using the `gomobile` Go subcommand.

We can likewise generate a FFI API as follows:

```Go
package main

import (
"C"
"sync"

"github.com/ooni/probe-engine/oonimkall"
)

var (
idx int64 = 1
m = make(map[int64]*oonimkall.Task)
mu sync.Mutex
)

//export ooni_task_start
func ooni_task_start(settings string) int64 {
tp, err := oonimkall.StartTask(settings)
if err != nil {
return 0
}
mu.Lock()
handle := idx
idx++
m[handle] = tp
mu.Unlock()
return handle
}

//export ooni_task_interrupt
func ooni_task_interrupt(handle int64) {
mu.Lock()
if tp := m[handle]; tp != nil {
tp.Interrupt()
}
mu.Unlock()
}

//export ooni_task_is_done
func ooni_task_is_done(handle int64) bool {
isdone := true
mu.Lock()
if tp := m[handle]; tp != nil {
isdone = tp.IsDone()
}
mu.Unlock()
return isdone
}

//export ooni_task_wait_for_next_event
func ooni_task_wait_for_next_event(handle int64) (event string) {
mu.Lock()
tp := m[handle]
mu.Unlock()
if tp != nil {
event = tp.WaitForNextEvent()
}
return
}

func main() {}
```

This is close enough to [measurement-kit's FFI API](https://git.io/Jv4Rv) that
a few lines of C allow to implement an ABI compatible replacement.

## Other APIs of interest

We currently don't have plans for replacing other APIs.

## History

[The initial version of this design document](
https://github.com/measurement-kit/engine/blob/master/DESIGN.md)
lived in the measurement-kit namespace at GitHub. It discussed
a bunch of broad, extra topics, e.g., code bloat that are not
discussed in this document.
38 changes: 37 additions & 1 deletion experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,10 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{
"example": func(session *Session) *ExperimentBuilder {
return &ExperimentBuilder{
build: func(config interface{}) *experiment.Experiment {
return example.NewExperiment(session.session, *config.(*example.Config))
return example.NewExperiment(
session.session, *config.(*example.Config),
"example",
)
},
config: &example.Config{
Message: "Good day from the example experiment!",
Expand All @@ -310,6 +313,39 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{
}
},

"example_with_input": func(session *Session) *ExperimentBuilder {
return &ExperimentBuilder{
build: func(config interface{}) *experiment.Experiment {
return example.NewExperiment(
session.session, *config.(*example.Config),
"example_with_input",
)
},
config: &example.Config{
Message: "Good day from the example with input experiment!",
SleepTime: int64(5 * time.Second),
},
needsInput: true,
}
},

"example_with_failure": func(session *Session) *ExperimentBuilder {
return &ExperimentBuilder{
build: func(config interface{}) *experiment.Experiment {
return example.NewExperiment(
session.session, *config.(*example.Config),
"example_with_failure",
)
},
config: &example.Config{
Message: "Good day from the example with failure experiment!",
ReturnError: true,
SleepTime: int64(5 * time.Second),
},
needsInput: false,
}
},

"facebook_messenger": func(session *Session) *ExperimentBuilder {
return &ExperimentBuilder{
build: func(config interface{}) *experiment.Experiment {
Expand Down
13 changes: 6 additions & 7 deletions experiment/example/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
)

const (
testName = "example"
testVersion = "0.0.1"
)

Expand All @@ -22,8 +21,8 @@ const (
// This contains all the settings that user can set to modify the behaviour
// of this experiment.
type Config struct {
ReturnError bool `ooni:"Toogle to return a mocked error"`
Message string `ooni:"Message to emit at test completion"`
ReturnError bool `ooni:"Toogle to return a mocked error"`
SleepTime int64 `ooni:"Amount of time to sleep for"`
}

Expand All @@ -49,10 +48,10 @@ func measure(
}
testkeys := &TestKeys{Success: err == nil}
measurement.TestKeys = testkeys
select {
case <-time.After(time.Duration(config.SleepTime)):
case <-ctx.Done():
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(config.SleepTime))
defer cancel()
<-ctx.Done()
sess.Logger.Warnf("example: remember to drink: %s", "water is key to survival")
callbacks.OnProgress(1.0, config.Message)
callbacks.OnDataUsage(0, 0)
return err
Expand All @@ -64,7 +63,7 @@ func measure(
// Once you have created an instance, you can use directly the
// generic experiment API.
func NewExperiment(
sess *session.Session, config Config,
sess *session.Session, config Config, testName string,
) *experiment.Experiment {
return experiment.New(
sess, testName, testVersion,
Expand Down
2 changes: 2 additions & 0 deletions experiment/example/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestIntegration(t *testing.T) {

experiment := example.NewExperiment(
sess, example.Config{SleepTime: int64(2 * time.Second)},
"example",
)
if err := experiment.OpenReport(ctx); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -60,6 +61,7 @@ func TestIntegrationFailure(t *testing.T) {
SleepTime: int64(2 * time.Second),
ReturnError: true,
},
"example",
)
if _, err := experiment.Measure(ctx, ""); err == nil {
t.Fatal("expected an error here")
Expand Down
91 changes: 91 additions & 0 deletions oonimkall/chanlogger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package oonimkall

import (
"fmt"
)

// chanLogger is a logger targeting a channel
type chanLogger struct {
emitter *eventEmitter
hasdebug bool
hasinfo bool
haswarning bool
out chan<- *eventRecord
settings *settingsRecord
}

// Debug implements Logger.Debug
func (cl *chanLogger) Debug(msg string) {
if cl.hasdebug {
cl.emitter.Emit("log", eventValue{
LogLevel: "DEBUG",
Message: msg,
})
}
}

// Debugf implements Logger.Debugf
func (cl *chanLogger) Debugf(format string, v ...interface{}) {
if cl.hasdebug {
cl.Debug(fmt.Sprintf(format, v...))
}
}

// Info implements Logger.Info
func (cl *chanLogger) Info(msg string) {
if cl.hasinfo {
cl.emitter.Emit("log", eventValue{
LogLevel: "INFO",
Message: msg,
})
}
}

// Infof implements Logger.Infof
func (cl *chanLogger) Infof(format string, v ...interface{}) {
if cl.hasinfo {
cl.Info(fmt.Sprintf(format, v...))
}
}

// Warn implements Logger.Warn
func (cl *chanLogger) Warn(msg string) {
if cl.haswarning {
cl.emitter.Emit("log", eventValue{
LogLevel: "WARNING",
Message: msg,
})
}
}

// Warnf implements Logger.Warnf
func (cl *chanLogger) Warnf(format string, v ...interface{}) {
if cl.haswarning {
cl.Warn(fmt.Sprintf(format, v...))
}
}

// newChanLogger creates a new ChanLogger instance.
func newChanLogger(
emitter *eventEmitter, settings *settingsRecord,
out chan<- *eventRecord,
) *chanLogger {
cl := &chanLogger{
emitter: emitter,
out: out,
settings: settings,
}
switch settings.LogLevel {
case "DEBUG", "DEBUG2":
cl.hasdebug = true
fallthrough
case "INFO":
cl.hasinfo = true
fallthrough
case "ERR", "WARNING":
fallthrough
default:
cl.haswarning = true
}
return cl
}
Loading

0 comments on commit 357cb1c

Please sign in to comment.