feat: tutorial on how to write the torsf experiment (#390)
Original tracking issue for Sprint 41: https://github.com/ooni/probe/issues/1507 Follow-up work in Sprint 42 tracked by: https://github.com/ooni/probe/issues/1689
This commit is contained in:
parent
a9990ac9da
commit
520398dd8e
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/montanaflynn/stats"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/trace"
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/example"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
engine "github.com/ooni/probe-cli/v3/internal/engine"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/fbmessenger"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hirl"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dnscheck"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/run"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/signal"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/stunreachability"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
"github.com/pion/stun"
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/telegram"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/legacy/oonidatamodel"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/legacy/oonitemplates"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
)
|
||||
|
||||
func TestGetterHTTPSWithTunnelCannotCreateTempDir(t *testing.T) {
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
)
|
||||
|
||||
func TestFillASNsEmpty(t *testing.T) {
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpfailure"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/probeservices"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/probeservices/testorchestra"
|
||||
|
|
18
internal/tutorial/README.md
Normal file
18
internal/tutorial/README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Tutorials: writing OONI nettests
|
||||
|
||||
This package contains a living tutorial explaining how to write OONI
|
||||
nettests. The code in here is based on existing nettests.
|
||||
|
||||
Because it's committed to the probe-cli repository and depends on
|
||||
real OONI code, it should always be up to date.
|
||||
|
||||
## Index
|
||||
|
||||
- [Rewriting the torsf experiment](experiment/torsf/)
|
||||
|
||||
|
||||
## Regenerating the tutorials
|
||||
|
||||
```
|
||||
(cd ./internal/tutorial && go run ./generator)
|
||||
```
|
20
internal/tutorial/experiment/torsf/README.md
Normal file
20
internal/tutorial/experiment/torsf/README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Tutorial: rewriting the torsf experiment
|
||||
|
||||
This tutorial teaches you how to write a minimal implementation of the
|
||||
[torsf](https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md)
|
||||
experiment. We will do that in four steps.
|
||||
|
||||
In the [first step](chapter01/) we will write a `main.go`
|
||||
function that runs the existing `torsf` implementation.
|
||||
|
||||
In the [second step](chapter02/) we will modify the existing
|
||||
code to launch an empty experiment instead.
|
||||
|
||||
In the [third step](chapter03/) we will start to fill in
|
||||
the empty experiment to more closely simulate a real implementation
|
||||
of the `torsf` experiment.
|
||||
|
||||
In the [fourth step](chapter04/) we will replace the code
|
||||
simulating a real `torsf` experiment with a minimal implementation
|
||||
of such an experiment that uses other code in `ooni/probe-cli` to
|
||||
attempt to bootstrap `tor` over Snowflake.
|
286
internal/tutorial/experiment/torsf/chapter01/README.md
Normal file
286
internal/tutorial/experiment/torsf/chapter01/README.md
Normal file
|
@ -0,0 +1,286 @@
|
|||
|
||||
# Chapter I: main.go using the real torsf implementation
|
||||
|
||||
In this chapter we will write together a `main.go` file that
|
||||
uses the real `torsf` implementation to run the experiment.
|
||||
|
||||
(This file is auto-generated from the corresponding source file,
|
||||
so make sure you don't edit it manually.)
|
||||
|
||||
## The torsf experiment
|
||||
|
||||
This experiment attempts to bootstrap the `tor` binary using
|
||||
Snowflake as the pluggable transport.
|
||||
|
||||
You can read the [specification](https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md)
|
||||
of the `torsf` experiment in the [ooni/spec](https://github.com/ooni/spec)
|
||||
repository. (The `ooni/spec` repository is the repository
|
||||
containing the specification of all OONI nettests, as well
|
||||
as of the data formats used by OONI.)
|
||||
|
||||
## The main.go file
|
||||
|
||||
We define `main.go` file using `package main`.
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
Then we add the required imports.
|
||||
|
||||
```Go
|
||||
import (
|
||||
```
|
||||
|
||||
These are standard library imports.
|
||||
|
||||
```Go
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
```
|
||||
|
||||
The apex/log library is the logging library used by OONI Probe.
|
||||
|
||||
```Go
|
||||
"github.com/apex/log"
|
||||
|
||||
```
|
||||
|
||||
The torsf package contains the implementation of the torsf experiment.
|
||||
|
||||
```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
|
||||
|
||||
```
|
||||
|
||||
The mockable package contains widely used mocks.
|
||||
|
||||
```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
|
||||
```
|
||||
|
||||
The model package contains the data model used by OONI experiments.
|
||||
|
||||
```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
|
||||
```
|
||||
|
||||
We will need the execabs library to check whether there is
|
||||
a binary called `tor` in the `PATH`.
|
||||
|
||||
```Go
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### Main function
|
||||
|
||||
Finally, here's the code of the `main function`.
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
```
|
||||
|
||||
We start by checking whether there is an executable named `"tor"` in
|
||||
the `PATH`. If there is no such executable, we fail with an error.
|
||||
|
||||
```Go
|
||||
if _, err := execabs.LookPath("tor"); err != nil {
|
||||
log.Fatal("cannot find the tor executable in path")
|
||||
}
|
||||
```
|
||||
|
||||
Then, we create a temporary directory to hold any state that may be
|
||||
required either by the `tor` or by the Snowflake pluggable transport.
|
||||
|
||||
```Go
|
||||
tempdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("cannot create temporary directory")
|
||||
}
|
||||
```
|
||||
|
||||
### Creating the experiment measurer
|
||||
|
||||
All OONI experiments implement a function called
|
||||
`NewExprimentMeasurer` that allows you to make
|
||||
an `ExperimentMeasurer` instance. The `ExperimentMeasurer`
|
||||
is an `interface` defined by the `model` package we
|
||||
imported above. Because we don't want to configure
|
||||
any setting (and the experiment does not support any
|
||||
setting anyway), here we're passing to the
|
||||
`NewExperimentMeasurer` factory an empty `Config`.
|
||||
|
||||
```Go
|
||||
m := torsf.NewExperimentMeasurer(torsf.Config{})
|
||||
```
|
||||
|
||||
### Creating the measurement
|
||||
|
||||
Next, we create an empty `Measurement`. OONI measurements
|
||||
are JSON data structures that contain generic fields common
|
||||
to all OONI experiments and experiment-specific data. The
|
||||
experiment-specific data is contained by a the `test_keys`
|
||||
field of the `Measurement`.
|
||||
|
||||
In the *real* OONI implementation, there is common code
|
||||
that fills the several fields of a `Measurement`. For
|
||||
example, it will fill the country code and the autonomous
|
||||
system number of the network in which the OONI Probe is
|
||||
running. Because this is just an example to illustrate
|
||||
how to write experiments, we will not bother with doing
|
||||
that. Instead, we will pass to the experiment just an
|
||||
emtpy measurement where no field has been set.
|
||||
|
||||
```Go
|
||||
measurement := &model.Measurement{}
|
||||
```
|
||||
|
||||
### Creating the callbacks
|
||||
|
||||
Then, we create an instance of the experiment callbacks. The
|
||||
experiment callbacks historically groups a set of callbacks
|
||||
called when the measurer is running. At the moment of writing
|
||||
this note, the `model.ExperimentCallbacks` contains just a
|
||||
single method called `OnDataUsage`, which is used to tell the
|
||||
caller which is the amount of data used by the experiment.
|
||||
|
||||
Because this is an example for illustrative purposes, here
|
||||
we construct an implementation of `ExperimentCallbacks` that
|
||||
just prints the data usage using the `log.Log` logger.
|
||||
|
||||
```Go
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
```
|
||||
|
||||
### Creating a session
|
||||
|
||||
The `ExperimentMeasurer` also wants a `Session`. In normal
|
||||
OONI code, the `Session` is a data structure containing
|
||||
information regarding the current measurement session. Since
|
||||
this is just an illustrative example, rather than creating
|
||||
a real `Session` instance, we use much-simpler mock.
|
||||
|
||||
The interface required by a `Session` is called
|
||||
`ExperimentSession` and is part of the `model` package.
|
||||
|
||||
Here we configure this mockable session to use `log.Log`
|
||||
as a logger and the previously computed temp dir.
|
||||
|
||||
```Go
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTempDir: tempdir,
|
||||
}
|
||||
```
|
||||
|
||||
# Running the experiment
|
||||
|
||||
At last, it's time to run the experiment using all the
|
||||
previously constructed data structures. The `Run` function
|
||||
is the main function you need to implement when you are
|
||||
defining a new OONI experiment.
|
||||
|
||||
By convention, the `Run` function only returns an error
|
||||
when some precondition required by the experiment is
|
||||
not met. Say that, for example, the experiment needs a
|
||||
port listening on the local host. If we cannot create
|
||||
such a port, we will return an error to the caller.
|
||||
|
||||
For network errors, instead, we return nil. Consider the
|
||||
case where we connect to a remote host and the connection
|
||||
fails. This is not really an error, rather it's a result
|
||||
that we will include into the measurement.
|
||||
|
||||
Apart from the other arguments that we discussed previously,
|
||||
the `Run` function also wants a `context.Context` as its
|
||||
first argument. The context is used to interrupt long running
|
||||
functions early, and our code (mostly) honours contexts.
|
||||
|
||||
Since here we are just writing a simple example, we don't
|
||||
need any fancy context and we pass a `context.Background` to `Run`.
|
||||
|
||||
```Go
|
||||
ctx := context.Background()
|
||||
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
log.WithError(err).Fatal("torsf experiment failed")
|
||||
}
|
||||
```
|
||||
|
||||
### Printing the measurement result
|
||||
|
||||
The `Run` function modifies the `TestKeys` (`test_keys` in JSON)
|
||||
field of the measurement. The real OONI implementation would
|
||||
now submit this measurement. Because this is an illustrative example,
|
||||
we will just pretty-print the measurement on the `stdout`.
|
||||
|
||||
```Go
|
||||
data, err := json.Marshal(measurement)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("json.Marshal failed")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Running the code
|
||||
|
||||
You can now run this code as follows:
|
||||
|
||||
```
|
||||
$ go run ./experiment/torsf/chapter01 | jq
|
||||
[snip]
|
||||
{
|
||||
"data_format_version": "",
|
||||
"input": null,
|
||||
"measurement_start_time": "",
|
||||
"probe_asn": "",
|
||||
"probe_cc": "",
|
||||
"probe_network_name": "",
|
||||
"report_id": "",
|
||||
"resolver_asn": "",
|
||||
"resolver_ip": "",
|
||||
"resolver_network_name": "",
|
||||
"software_name": "",
|
||||
"software_version": "",
|
||||
"test_keys": {
|
||||
"bootstrap_time": 68.909067459,
|
||||
"failure": null
|
||||
},
|
||||
"test_name": "",
|
||||
"test_runtime": 0,
|
||||
"test_start_time": "",
|
||||
"test_version": ""
|
||||
}
|
||||
```
|
||||
|
||||
We have snipped through logs and we have used `jq` to
|
||||
pretty print the measurement. You see that all the fields
|
||||
except the `test_keys` are empty.
|
||||
|
||||
Let us now analyze the content of the `test_keys`:
|
||||
|
||||
- the `bootstrap_time` field contains the time (in seconds) to
|
||||
bootstrap `tor` using the Snowflake transport;
|
||||
|
||||
- the `failure` field contains the error that occurred, if
|
||||
any, or `null` if no error occurred.
|
||||
|
||||
## Concluding remarks
|
||||
|
||||
This is all you need to know in terms of minimal code for
|
||||
running an OONI experiment. In the remainder of this tutorial,
|
||||
we will show how to reimplement the `torsf` experiment.
|
||||
|
||||
Apart from minor changes, the `main.go` file would basically
|
||||
not change for the remainder of this tutorial.
|
287
internal/tutorial/experiment/torsf/chapter01/main.go
Normal file
287
internal/tutorial/experiment/torsf/chapter01/main.go
Normal file
|
@ -0,0 +1,287 @@
|
|||
// -=-=- StartHere -=-=-
|
||||
//
|
||||
// # Chapter I: main.go using the real torsf implementation
|
||||
//
|
||||
// In this chapter we will write together a `main.go` file that
|
||||
// uses the real `torsf` implementation to run the experiment.
|
||||
//
|
||||
// (This file is auto-generated from the corresponding source file,
|
||||
// so make sure you don't edit it manually.)
|
||||
//
|
||||
// ## The torsf experiment
|
||||
//
|
||||
// This experiment attempts to bootstrap the `tor` binary using
|
||||
// Snowflake as the pluggable transport.
|
||||
//
|
||||
// You can read the [specification](https://github.com/ooni/spec/blob/master/nettests/ts-030-torsf.md)
|
||||
// of the `torsf` experiment in the [ooni/spec](https://github.com/ooni/spec)
|
||||
// repository. (The `ooni/spec` repository is the repository
|
||||
// containing the specification of all OONI nettests, as well
|
||||
// as of the data formats used by OONI.)
|
||||
//
|
||||
// ## The main.go file
|
||||
//
|
||||
// We define `main.go` file using `package main`.
|
||||
//
|
||||
// ```Go
|
||||
package main
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### Imports
|
||||
//
|
||||
// Then we add the required imports.
|
||||
//
|
||||
// ```Go
|
||||
import (
|
||||
// ```
|
||||
//
|
||||
// These are standard library imports.
|
||||
//
|
||||
// ```Go
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
// ```
|
||||
//
|
||||
// The apex/log library is the logging library used by OONI Probe.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/apex/log"
|
||||
|
||||
// ```
|
||||
//
|
||||
// The torsf package contains the implementation of the torsf experiment.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
|
||||
|
||||
// ```
|
||||
//
|
||||
// The mockable package contains widely used mocks.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
|
||||
// ```
|
||||
//
|
||||
// The model package contains the data model used by OONI experiments.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
|
||||
// ```
|
||||
//
|
||||
// We will need the execabs library to check whether there is
|
||||
// a binary called `tor` in the `PATH`.
|
||||
//
|
||||
// ```Go
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### Main function
|
||||
//
|
||||
// Finally, here's the code of the `main function`.
|
||||
//
|
||||
// ```Go
|
||||
func main() {
|
||||
// ```
|
||||
//
|
||||
// We start by checking whether there is an executable named `"tor"` in
|
||||
// the `PATH`. If there is no such executable, we fail with an error.
|
||||
//
|
||||
// ```Go
|
||||
if _, err := execabs.LookPath("tor"); err != nil {
|
||||
log.Fatal("cannot find the tor executable in path")
|
||||
}
|
||||
// ```
|
||||
//
|
||||
// Then, we create a temporary directory to hold any state that may be
|
||||
// required either by the `tor` or by the Snowflake pluggable transport.
|
||||
//
|
||||
// ```Go
|
||||
tempdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("cannot create temporary directory")
|
||||
}
|
||||
// ```
|
||||
//
|
||||
// ### Creating the experiment measurer
|
||||
//
|
||||
// All OONI experiments implement a function called
|
||||
// `NewExprimentMeasurer` that allows you to make
|
||||
// an `ExperimentMeasurer` instance. The `ExperimentMeasurer`
|
||||
// is an `interface` defined by the `model` package we
|
||||
// imported above. Because we don't want to configure
|
||||
// any setting (and the experiment does not support any
|
||||
// setting anyway), here we're passing to the
|
||||
// `NewExperimentMeasurer` factory an empty `Config`.
|
||||
//
|
||||
// ```Go
|
||||
m := torsf.NewExperimentMeasurer(torsf.Config{})
|
||||
// ```
|
||||
//
|
||||
// ### Creating the measurement
|
||||
//
|
||||
// Next, we create an empty `Measurement`. OONI measurements
|
||||
// are JSON data structures that contain generic fields common
|
||||
// to all OONI experiments and experiment-specific data. The
|
||||
// experiment-specific data is contained by a the `test_keys`
|
||||
// field of the `Measurement`.
|
||||
//
|
||||
// In the *real* OONI implementation, there is common code
|
||||
// that fills the several fields of a `Measurement`. For
|
||||
// example, it will fill the country code and the autonomous
|
||||
// system number of the network in which the OONI Probe is
|
||||
// running. Because this is just an example to illustrate
|
||||
// how to write experiments, we will not bother with doing
|
||||
// that. Instead, we will pass to the experiment just an
|
||||
// emtpy measurement where no field has been set.
|
||||
//
|
||||
// ```Go
|
||||
measurement := &model.Measurement{}
|
||||
// ```
|
||||
//
|
||||
// ### Creating the callbacks
|
||||
//
|
||||
// Then, we create an instance of the experiment callbacks. The
|
||||
// experiment callbacks historically groups a set of callbacks
|
||||
// called when the measurer is running. At the moment of writing
|
||||
// this note, the `model.ExperimentCallbacks` contains just a
|
||||
// single method called `OnDataUsage`, which is used to tell the
|
||||
// caller which is the amount of data used by the experiment.
|
||||
//
|
||||
// Because this is an example for illustrative purposes, here
|
||||
// we construct an implementation of `ExperimentCallbacks` that
|
||||
// just prints the data usage using the `log.Log` logger.
|
||||
//
|
||||
// ```Go
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
// ```
|
||||
//
|
||||
// ### Creating a session
|
||||
//
|
||||
// The `ExperimentMeasurer` also wants a `Session`. In normal
|
||||
// OONI code, the `Session` is a data structure containing
|
||||
// information regarding the current measurement session. Since
|
||||
// this is just an illustrative example, rather than creating
|
||||
// a real `Session` instance, we use much-simpler mock.
|
||||
//
|
||||
// The interface required by a `Session` is called
|
||||
// `ExperimentSession` and is part of the `model` package.
|
||||
//
|
||||
// Here we configure this mockable session to use `log.Log`
|
||||
// as a logger and the previously computed temp dir.
|
||||
//
|
||||
// ```Go
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTempDir: tempdir,
|
||||
}
|
||||
// ```
|
||||
//
|
||||
// # Running the experiment
|
||||
//
|
||||
// At last, it's time to run the experiment using all the
|
||||
// previously constructed data structures. The `Run` function
|
||||
// is the main function you need to implement when you are
|
||||
// defining a new OONI experiment.
|
||||
//
|
||||
// By convention, the `Run` function only returns an error
|
||||
// when some precondition required by the experiment is
|
||||
// not met. Say that, for example, the experiment needs a
|
||||
// port listening on the local host. If we cannot create
|
||||
// such a port, we will return an error to the caller.
|
||||
//
|
||||
// For network errors, instead, we return nil. Consider the
|
||||
// case where we connect to a remote host and the connection
|
||||
// fails. This is not really an error, rather it's a result
|
||||
// that we will include into the measurement.
|
||||
//
|
||||
// Apart from the other arguments that we discussed previously,
|
||||
// the `Run` function also wants a `context.Context` as its
|
||||
// first argument. The context is used to interrupt long running
|
||||
// functions early, and our code (mostly) honours contexts.
|
||||
//
|
||||
// Since here we are just writing a simple example, we don't
|
||||
// need any fancy context and we pass a `context.Background` to `Run`.
|
||||
//
|
||||
// ```Go
|
||||
ctx := context.Background()
|
||||
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
log.WithError(err).Fatal("torsf experiment failed")
|
||||
}
|
||||
// ```
|
||||
//
|
||||
// ### Printing the measurement result
|
||||
//
|
||||
// The `Run` function modifies the `TestKeys` (`test_keys` in JSON)
|
||||
// field of the measurement. The real OONI implementation would
|
||||
// now submit this measurement. Because this is an illustrative example,
|
||||
// we will just pretty-print the measurement on the `stdout`.
|
||||
//
|
||||
// ```Go
|
||||
data, err := json.Marshal(measurement)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("json.Marshal failed")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ## Running the code
|
||||
//
|
||||
// You can now run this code as follows:
|
||||
//
|
||||
// ```
|
||||
// $ go run ./experiment/torsf/chapter01 | jq
|
||||
// [snip]
|
||||
// {
|
||||
// "data_format_version": "",
|
||||
// "input": null,
|
||||
// "measurement_start_time": "",
|
||||
// "probe_asn": "",
|
||||
// "probe_cc": "",
|
||||
// "probe_network_name": "",
|
||||
// "report_id": "",
|
||||
// "resolver_asn": "",
|
||||
// "resolver_ip": "",
|
||||
// "resolver_network_name": "",
|
||||
// "software_name": "",
|
||||
// "software_version": "",
|
||||
// "test_keys": {
|
||||
// "bootstrap_time": 68.909067459,
|
||||
// "failure": null
|
||||
// },
|
||||
// "test_name": "",
|
||||
// "test_runtime": 0,
|
||||
// "test_start_time": "",
|
||||
// "test_version": ""
|
||||
//}
|
||||
// ```
|
||||
//
|
||||
// We have snipped through logs and we have used `jq` to
|
||||
// pretty print the measurement. You see that all the fields
|
||||
// except the `test_keys` are empty.
|
||||
//
|
||||
// Let us now analyze the content of the `test_keys`:
|
||||
//
|
||||
// - the `bootstrap_time` field contains the time (in seconds) to
|
||||
// bootstrap `tor` using the Snowflake transport;
|
||||
//
|
||||
// - the `failure` field contains the error that occurred, if
|
||||
// any, or `null` if no error occurred.
|
||||
//
|
||||
// ## Concluding remarks
|
||||
//
|
||||
// This is all you need to know in terms of minimal code for
|
||||
// running an OONI experiment. In the remainder of this tutorial,
|
||||
// we will show how to reimplement the `torsf` experiment.
|
||||
//
|
||||
// Apart from minor changes, the `main.go` file would basically
|
||||
// not change for the remainder of this tutorial.
|
204
internal/tutorial/experiment/torsf/chapter02/README.md
Normal file
204
internal/tutorial/experiment/torsf/chapter02/README.md
Normal file
|
@ -0,0 +1,204 @@
|
|||
|
||||
# Chapter II: creating an empty experiment
|
||||
|
||||
In this chapter we will create an empty experiment and replace
|
||||
the code calling the real `torsf` experiment in `main.go` to
|
||||
call our empty experiment instead.
|
||||
|
||||
(This file is auto-generated from the corresponding source file,
|
||||
so make sure you don't edit it manually.)
|
||||
|
||||
## Changes in main.go
|
||||
|
||||
In `main.go` we will simply replace the call to the
|
||||
`torsf.NewExperimentMeasurer` function with a call to
|
||||
a `NewExperimentMeasurer` function that we are going
|
||||
to implement as part of this chapter.
|
||||
|
||||
After you do this, you also need to remove the now-unneded
|
||||
import of the `torsf` package.
|
||||
|
||||
There are no additional changes to `main.go`.
|
||||
|
||||
```Go
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
```
|
||||
|
||||
## The torsf.go file
|
||||
|
||||
This file will contain the implementation of the
|
||||
`NewExperimentMeasurer` function.
|
||||
|
||||
As usual we start with the `package` declaration and
|
||||
with the few imports we need to add.
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### Data structures
|
||||
|
||||
Next, we define data structures.
|
||||
|
||||
Config contains config for the torsf experiment. As for the real
|
||||
`torsf` experiment, we don't have any specific config, so we keep
|
||||
the structure empty. We still need to define a `Config` struct
|
||||
here, because, by convention, all OONI experiments have a `Config`.
|
||||
|
||||
```Go
|
||||
type Config struct{}
|
||||
|
||||
```
|
||||
|
||||
Measurer is the torsf measurer. This structure implements the
|
||||
`model.ExperimentMeasurer` interface, as we will see below.
|
||||
|
||||
Most OONI experiments have a measurer that contains as its unique
|
||||
field the specific configuration. Here we do the same.
|
||||
|
||||
```Go
|
||||
type Measurer struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
```
|
||||
NewExperimentMeasurer creates a new model.ExperimentMeasurer
|
||||
instance for performing `torsf` measurements. This function
|
||||
will just assemble a new instance of `Measurer` with the `config`
|
||||
that was passed as an argument.
|
||||
|
||||
```Go
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return &Measurer{config: config}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Implementing the model.ExperimentMeasurer.
|
||||
|
||||
Now it's time to implement the methods required by the `model`'s
|
||||
`ExperimentMeasurer` interface.
|
||||
|
||||
ExperimentName implements ExperimentMeasurer.ExperimentName. This function
|
||||
returns the name of the experiment. This code is used by generic code
|
||||
manipulating the experiment to print the experiment name.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) ExperimentName() string {
|
||||
return "torsf"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. This
|
||||
function returns the version of the experiment. This code is also used by
|
||||
generic code manipulating the experiment to print the experiment version.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) ExperimentVersion() string {
|
||||
return "0.1.0"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Run implements ExperimentMeasurer.Run. This is the most interesting
|
||||
function, where we run the experiment proper. In the previous chapter
|
||||
we learned how to call this function from a `main.go` file. Here,
|
||||
instead, we're going to create a minimal stub. In the subsequent
|
||||
chapters, finally, we will modify this function until it is a
|
||||
minimal implementation of the `torsf` experiment.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
```
|
||||
As you can see, this is just a stub implementation that sleeps
|
||||
for one second and prints a logging message.
|
||||
|
||||
```Go
|
||||
time.Sleep(time.Second)
|
||||
sess.Logger().Info("hello from the torsf experiment!")
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
### Summary keys
|
||||
|
||||
Before concluding this chapter, we also need to create the `SummaryKeys`
|
||||
for this experiment. For historical reasons, the `TestKeys` of each
|
||||
experiment is an `interface{}`. Every experiment also defines a `SummaryKeys`
|
||||
data structure and a `GetSummaryKeys` method to convert the opaque
|
||||
result of a measurement to the summary for such an experiment.
|
||||
|
||||
The experiment summary is *only* used by the OONI Probe CLI.
|
||||
|
||||
SummaryKeys contains summary keys for this experiment. Because this is
|
||||
just an illustrative tutorial, we will just include a single key, named
|
||||
`IsAnomaly`. This key is not exported as JSON and is used by the OONI
|
||||
Probe CLI to inform the user of whether this measurement is ordinary or
|
||||
anomalous. All OONI experiments' `SummaryKeys` contain such a field.
|
||||
|
||||
```Go
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. This
|
||||
method just converts the `TestKeys` inside `measurement` to an instance of
|
||||
the `SummaryKeys` structure. For now, we'll just implement a stub returning
|
||||
fake `SummaryKeys` declaring there was no anomaly.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
return &SummaryKeys{IsAnomaly: false}, nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Running the code
|
||||
|
||||
We can run the code written in this chapter as follows:
|
||||
|
||||
```
|
||||
$ go run ./experiment/torsf/chapter02
|
||||
2021/06/21 20:48:32 info hello from the torsf experiment!
|
||||
{
|
||||
"data_format_version": "",
|
||||
"input": null,
|
||||
"measurement_start_time": "",
|
||||
"probe_asn": "",
|
||||
"probe_cc": "",
|
||||
"probe_network_name": "",
|
||||
"report_id": "",
|
||||
"resolver_asn": "",
|
||||
"resolver_ip": "",
|
||||
"resolver_network_name": "",
|
||||
"software_name": "",
|
||||
"software_version": "",
|
||||
"test_keys": null,
|
||||
"test_name": "",
|
||||
"test_runtime": 0,
|
||||
"test_start_time": "",
|
||||
"test_version": ""
|
||||
}
|
||||
```
|
||||
|
||||
Here you see that we're printing the log message and
|
||||
that the `test_keys` are `null`.
|
||||
|
||||
The OONI data processing popeline will not be so happy
|
||||
if we pass it a `null` settings, because there is not
|
||||
much interesting data in there. We will thus start filling
|
||||
it in the next chapter.
|
65
internal/tutorial/experiment/torsf/chapter02/main.go
Normal file
65
internal/tutorial/experiment/torsf/chapter02/main.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := execabs.LookPath("tor"); err != nil {
|
||||
log.Fatal("cannot find the tor executable in path")
|
||||
}
|
||||
tempdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("cannot create temporary directory")
|
||||
}
|
||||
// -=-=- StartHere -=-=-
|
||||
//
|
||||
// # Chapter II: creating an empty experiment
|
||||
//
|
||||
// In this chapter we will create an empty experiment and replace
|
||||
// the code calling the real `torsf` experiment in `main.go` to
|
||||
// call our empty experiment instead.
|
||||
//
|
||||
// (This file is auto-generated from the corresponding source file,
|
||||
// so make sure you don't edit it manually.)
|
||||
//
|
||||
// ## Changes in main.go
|
||||
//
|
||||
// In `main.go` we will simply replace the call to the
|
||||
// `torsf.NewExperimentMeasurer` function with a call to
|
||||
// a `NewExperimentMeasurer` function that we are going
|
||||
// to implement as part of this chapter.
|
||||
//
|
||||
// After you do this, you also need to remove the now-unneded
|
||||
// import of the `torsf` package.
|
||||
//
|
||||
// There are no additional changes to `main.go`.
|
||||
//
|
||||
// ```Go
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
// ```
|
||||
// -=-=- StopHere -=-=-
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTempDir: tempdir,
|
||||
}
|
||||
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
log.WithError(err).Fatal("torsf experiment failed")
|
||||
}
|
||||
data, err := json.Marshal(measurement)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("json.Marshal failed")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
180
internal/tutorial/experiment/torsf/chapter02/torsf.go
Normal file
180
internal/tutorial/experiment/torsf/chapter02/torsf.go
Normal file
|
@ -0,0 +1,180 @@
|
|||
// -=-=- StartHere -=-=-
|
||||
//
|
||||
// ## The torsf.go file
|
||||
//
|
||||
// This file will contain the implementation of the
|
||||
// `NewExperimentMeasurer` function.
|
||||
//
|
||||
// As usual we start with the `package` declaration and
|
||||
// with the few imports we need to add.
|
||||
//
|
||||
// ```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### Data structures
|
||||
//
|
||||
// Next, we define data structures.
|
||||
|
||||
// Config contains config for the torsf experiment. As for the real
|
||||
// `torsf` experiment, we don't have any specific config, so we keep
|
||||
// the structure empty. We still need to define a `Config` struct
|
||||
// here, because, by convention, all OONI experiments have a `Config`.
|
||||
//
|
||||
// ```Go
|
||||
type Config struct{}
|
||||
|
||||
// ```
|
||||
//
|
||||
// Measurer is the torsf measurer. This structure implements the
|
||||
// `model.ExperimentMeasurer` interface, as we will see below.
|
||||
//
|
||||
// Most OONI experiments have a measurer that contains as its unique
|
||||
// field the specific configuration. Here we do the same.
|
||||
//
|
||||
// ```Go
|
||||
type Measurer struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// ```
|
||||
// NewExperimentMeasurer creates a new model.ExperimentMeasurer
|
||||
// instance for performing `torsf` measurements. This function
|
||||
// will just assemble a new instance of `Measurer` with the `config`
|
||||
// that was passed as an argument.
|
||||
//
|
||||
// ```Go
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return &Measurer{config: config}
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### Implementing the model.ExperimentMeasurer.
|
||||
//
|
||||
// Now it's time to implement the methods required by the `model`'s
|
||||
// `ExperimentMeasurer` interface.
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperimentName. This function
|
||||
// returns the name of the experiment. This code is used by generic code
|
||||
// manipulating the experiment to print the experiment name.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) ExperimentName() string {
|
||||
return "torsf"
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. This
|
||||
// function returns the version of the experiment. This code is also used by
|
||||
// generic code manipulating the experiment to print the experiment version.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) ExperimentVersion() string {
|
||||
return "0.1.0"
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// Run implements ExperimentMeasurer.Run. This is the most interesting
|
||||
// function, where we run the experiment proper. In the previous chapter
|
||||
// we learned how to call this function from a `main.go` file. Here,
|
||||
// instead, we're going to create a minimal stub. In the subsequent
|
||||
// chapters, finally, we will modify this function until it is a
|
||||
// minimal implementation of the `torsf` experiment.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
// ```
|
||||
// As you can see, this is just a stub implementation that sleeps
|
||||
// for one second and prints a logging message.
|
||||
//
|
||||
// ```Go
|
||||
time.Sleep(time.Second)
|
||||
sess.Logger().Info("hello from the torsf experiment!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ```
|
||||
// ### Summary keys
|
||||
//
|
||||
// Before concluding this chapter, we also need to create the `SummaryKeys`
|
||||
// for this experiment. For historical reasons, the `TestKeys` of each
|
||||
// experiment is an `interface{}`. Every experiment also defines a `SummaryKeys`
|
||||
// data structure and a `GetSummaryKeys` method to convert the opaque
|
||||
// result of a measurement to the summary for such an experiment.
|
||||
//
|
||||
// The experiment summary is *only* used by the OONI Probe CLI.
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment. Because this is
|
||||
// just an illustrative tutorial, we will just include a single key, named
|
||||
// `IsAnomaly`. This key is not exported as JSON and is used by the OONI
|
||||
// Probe CLI to inform the user of whether this measurement is ordinary or
|
||||
// anomalous. All OONI experiments' `SummaryKeys` contain such a field.
|
||||
//
|
||||
// ```Go
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. This
|
||||
// method just converts the `TestKeys` inside `measurement` to an instance of
|
||||
// the `SummaryKeys` structure. For now, we'll just implement a stub returning
|
||||
// fake `SummaryKeys` declaring there was no anomaly.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
return &SummaryKeys{IsAnomaly: false}, nil
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ## Running the code
|
||||
//
|
||||
// We can run the code written in this chapter as follows:
|
||||
//
|
||||
// ```
|
||||
// $ go run ./experiment/torsf/chapter02
|
||||
// 2021/06/21 20:48:32 info hello from the torsf experiment!
|
||||
// {
|
||||
// "data_format_version": "",
|
||||
// "input": null,
|
||||
// "measurement_start_time": "",
|
||||
// "probe_asn": "",
|
||||
// "probe_cc": "",
|
||||
// "probe_network_name": "",
|
||||
// "report_id": "",
|
||||
// "resolver_asn": "",
|
||||
// "resolver_ip": "",
|
||||
// "resolver_network_name": "",
|
||||
// "software_name": "",
|
||||
// "software_version": "",
|
||||
// "test_keys": null,
|
||||
// "test_name": "",
|
||||
// "test_runtime": 0,
|
||||
// "test_start_time": "",
|
||||
// "test_version": ""
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Here you see that we're printing the log message and
|
||||
// that the `test_keys` are `null`.
|
||||
//
|
||||
// The OONI data processing popeline will not be so happy
|
||||
// if we pass it a `null` settings, because there is not
|
||||
// much interesting data in there. We will thus start filling
|
||||
// it in the next chapter.
|
166
internal/tutorial/experiment/torsf/chapter03/README.md
Normal file
166
internal/tutorial/experiment/torsf/chapter03/README.md
Normal file
|
@ -0,0 +1,166 @@
|
|||
|
||||
# Chapter III: starting to simulate a real torsf experiment
|
||||
|
||||
In this chapter we will improve upon what we did in the previous
|
||||
chapter by creating runner code for the `torsf` experiment. We will
|
||||
not, yet, run the real experiment, but we will instead write
|
||||
simple code that pretends to run a `tor` bootstrap using snowflake.
|
||||
|
||||
(This file is auto-generated from the corresponding source file,
|
||||
so make sure you don't edit it manually.)
|
||||
|
||||
### The TestKeys structure
|
||||
|
||||
Let us start by defining the `TestKeys` structure that contains
|
||||
the experiment specific results. As we have already seen in
|
||||
Chapter I, this structure must contain two fields. The bootstrap
|
||||
time for the experiment and the failure.
|
||||
|
||||
```Go
|
||||
type TestKeys struct {
|
||||
BootstrapTime float64 `json:"bootstrap_time"`
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Rewriting the Run method
|
||||
|
||||
Next we will rewrite the Run method. We will arrange for this
|
||||
method to fill the `measurement`, to setup the timeout, and to
|
||||
print periodic updates via the `callbacks`. We will defer the
|
||||
real work to a private function called `run`.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
```
|
||||
|
||||
Let's create an instance of `TestKeys` and let's modify
|
||||
the `measurement` to refer to such an instance.
|
||||
|
||||
```Go
|
||||
testkeys := &TestKeys{}
|
||||
measurement.TestKeys = testkeys
|
||||
```
|
||||
|
||||
Next, we record the current time and we modify the
|
||||
context to have a timeout after 300 seconds. Because
|
||||
Snowflake *may* take a long time to bootstrap, we
|
||||
need to specify a generous timeout here.
|
||||
|
||||
```Go
|
||||
start := time.Now()
|
||||
const maxRuntime = 300 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
||||
defer cancel()
|
||||
```
|
||||
|
||||
Okay, now we are ready to defer the real work to
|
||||
the internal `run` function. We first create a
|
||||
channel to receive the result of `run`. Then, we
|
||||
create a ticker to emit periodic updates. We
|
||||
emit an update every 250 milliseconds, which is
|
||||
a reasonably smooth way of increasing a progress
|
||||
bar (progress is indeed used to move progress bars
|
||||
both in OONI Probe Desktop and mobile.)
|
||||
|
||||
```Go
|
||||
errch := make(chan error)
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
```
|
||||
|
||||
Now we defer the real work to `run`, which will
|
||||
run in a background goroutine.
|
||||
|
||||
```Go
|
||||
go m.run(ctx, sess, testkeys, errch)
|
||||
```
|
||||
|
||||
While `run` is running, we loop and check which
|
||||
channel has become ready.
|
||||
|
||||
If the `errch` channel is ready, it means that `run` is
|
||||
terminated, so we return to the caller.
|
||||
|
||||
Instead, if `ticker.C` is ready, we emit a progress
|
||||
update using the `callbacks`.
|
||||
|
||||
```Go
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
callbacks.OnProgress(1.0, "torsf experiment is finished")
|
||||
return err
|
||||
case <-ticker.C:
|
||||
progress := time.Since(start).Seconds() / maxRuntime.Seconds()
|
||||
callbacks.OnProgress(progress, "torsf experiment is running")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### The run function
|
||||
|
||||
We will now implement the `run` function. For now, this function
|
||||
will not do any real work, but it will just pretend to do work.
|
||||
|
||||
Note how we sleep for some time, set the `BootstrapTime` field
|
||||
of the `TestKeys`, and then return using `errch`.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) run(ctx context.Context,
|
||||
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
|
||||
fakeBootstrapTime := 10 * time.Second
|
||||
time.Sleep(fakeBootstrapTime)
|
||||
testkeys.BootstrapTime = fakeBootstrapTime.Seconds()
|
||||
errch <- nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Running the code
|
||||
|
||||
It's now time to run the new code we've written:
|
||||
|
||||
```
|
||||
$ go run ./experiment/torsf/chapter03 | jq
|
||||
2021/06/21 21:21:18 info [ 0.1%] torsf experiment is running
|
||||
2021/06/21 21:21:19 info [ 0.2%] torsf experiment is running
|
||||
[...]
|
||||
2021/06/21 21:21:28 info [ 3.3%] torsf experiment is running
|
||||
2021/06/21 21:21:28 info [100.0%] torsf experiment is finished
|
||||
{
|
||||
"data_format_version": "",
|
||||
"input": null,
|
||||
"measurement_start_time": "",
|
||||
"probe_asn": "",
|
||||
"probe_cc": "",
|
||||
"probe_network_name": "",
|
||||
"report_id": "",
|
||||
"resolver_asn": "",
|
||||
"resolver_ip": "",
|
||||
"resolver_network_name": "",
|
||||
"software_name": "",
|
||||
"software_version": "",
|
||||
"test_keys": {
|
||||
"bootstrap_time": 10,
|
||||
"failure": null
|
||||
},
|
||||
"test_name": "",
|
||||
"test_runtime": 0,
|
||||
"test_start_time": "",
|
||||
"test_version": ""
|
||||
}
|
||||
```
|
||||
|
||||
You see that now we're filling the bootstrap time and we're
|
||||
also printing progress using `callbacks`.
|
||||
|
||||
In the next chapter, we'll replace the stub `run` implementation
|
||||
with a real implementation using Snowflake.
|
||||
|
39
internal/tutorial/experiment/torsf/chapter03/main.go
Normal file
39
internal/tutorial/experiment/torsf/chapter03/main.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := execabs.LookPath("tor"); err != nil {
|
||||
log.Fatal("cannot find the tor executable in path")
|
||||
}
|
||||
tempdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("cannot create temporary directory")
|
||||
}
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTempDir: tempdir,
|
||||
}
|
||||
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
log.WithError(err).Fatal("torsf experiment failed")
|
||||
}
|
||||
data, err := json.Marshal(measurement)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("json.Marshal failed")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
210
internal/tutorial/experiment/torsf/chapter03/torsf.go
Normal file
210
internal/tutorial/experiment/torsf/chapter03/torsf.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
// Config contains config for the torsf experiment.
|
||||
type Config struct{}
|
||||
|
||||
// Measurer is the torsf measurer.
|
||||
type Measurer struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// newExperimentMeasurer creates a new ExperimentMeasurer for torsf.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return &Measurer{config: config}
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperimentName.
|
||||
func (m *Measurer) ExperimentName() string {
|
||||
return "torsf"
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||
func (m *Measurer) ExperimentVersion() string {
|
||||
return "0.1.0"
|
||||
}
|
||||
|
||||
// -=-=- StartHere -=-=-
|
||||
//
|
||||
// # Chapter III: starting to simulate a real torsf experiment
|
||||
//
|
||||
// In this chapter we will improve upon what we did in the previous
|
||||
// chapter by creating runner code for the `torsf` experiment. We will
|
||||
// not, yet, run the real experiment, but we will instead write
|
||||
// simple code that pretends to run a `tor` bootstrap using snowflake.
|
||||
//
|
||||
// (This file is auto-generated from the corresponding source file,
|
||||
// so make sure you don't edit it manually.)
|
||||
//
|
||||
// ### The TestKeys structure
|
||||
//
|
||||
// Let us start by defining the `TestKeys` structure that contains
|
||||
// the experiment specific results. As we have already seen in
|
||||
// Chapter I, this structure must contain two fields. The bootstrap
|
||||
// time for the experiment and the failure.
|
||||
//
|
||||
// ```Go
|
||||
type TestKeys struct {
|
||||
BootstrapTime float64 `json:"bootstrap_time"`
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### Rewriting the Run method
|
||||
//
|
||||
// Next we will rewrite the Run method. We will arrange for this
|
||||
// method to fill the `measurement`, to setup the timeout, and to
|
||||
// print periodic updates via the `callbacks`. We will defer the
|
||||
// real work to a private function called `run`.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
// ```
|
||||
//
|
||||
// Let's create an instance of `TestKeys` and let's modify
|
||||
// the `measurement` to refer to such an instance.
|
||||
//
|
||||
// ```Go
|
||||
testkeys := &TestKeys{}
|
||||
measurement.TestKeys = testkeys
|
||||
// ```
|
||||
//
|
||||
// Next, we record the current time and we modify the
|
||||
// context to have a timeout after 300 seconds. Because
|
||||
// Snowflake *may* take a long time to bootstrap, we
|
||||
// need to specify a generous timeout here.
|
||||
//
|
||||
// ```Go
|
||||
start := time.Now()
|
||||
const maxRuntime = 300 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
||||
defer cancel()
|
||||
// ```
|
||||
//
|
||||
// Okay, now we are ready to defer the real work to
|
||||
// the internal `run` function. We first create a
|
||||
// channel to receive the result of `run`. Then, we
|
||||
// create a ticker to emit periodic updates. We
|
||||
// emit an update every 250 milliseconds, which is
|
||||
// a reasonably smooth way of increasing a progress
|
||||
// bar (progress is indeed used to move progress bars
|
||||
// both in OONI Probe Desktop and mobile.)
|
||||
//
|
||||
// ```Go
|
||||
errch := make(chan error)
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
// ```
|
||||
//
|
||||
// Now we defer the real work to `run`, which will
|
||||
// run in a background goroutine.
|
||||
//
|
||||
// ```Go
|
||||
go m.run(ctx, sess, testkeys, errch)
|
||||
// ```
|
||||
//
|
||||
// While `run` is running, we loop and check which
|
||||
// channel has become ready.
|
||||
//
|
||||
// If the `errch` channel is ready, it means that `run` is
|
||||
// terminated, so we return to the caller.
|
||||
//
|
||||
// Instead, if `ticker.C` is ready, we emit a progress
|
||||
// update using the `callbacks`.
|
||||
//
|
||||
// ```Go
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
callbacks.OnProgress(1.0, "torsf experiment is finished")
|
||||
return err
|
||||
case <-ticker.C:
|
||||
progress := time.Since(start).Seconds() / maxRuntime.Seconds()
|
||||
callbacks.OnProgress(progress, "torsf experiment is running")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### The run function
|
||||
//
|
||||
// We will now implement the `run` function. For now, this function
|
||||
// will not do any real work, but it will just pretend to do work.
|
||||
//
|
||||
// Note how we sleep for some time, set the `BootstrapTime` field
|
||||
// of the `TestKeys`, and then return using `errch`.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) run(ctx context.Context,
|
||||
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
|
||||
fakeBootstrapTime := 10 * time.Second
|
||||
time.Sleep(fakeBootstrapTime)
|
||||
testkeys.BootstrapTime = fakeBootstrapTime.Seconds()
|
||||
errch <- nil
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ## Running the code
|
||||
//
|
||||
// It's now time to run the new code we've written:
|
||||
//
|
||||
// ```
|
||||
// $ go run ./experiment/torsf/chapter03 | jq
|
||||
// 2021/06/21 21:21:18 info [ 0.1%] torsf experiment is running
|
||||
// 2021/06/21 21:21:19 info [ 0.2%] torsf experiment is running
|
||||
// [...]
|
||||
// 2021/06/21 21:21:28 info [ 3.3%] torsf experiment is running
|
||||
// 2021/06/21 21:21:28 info [100.0%] torsf experiment is finished
|
||||
// {
|
||||
// "data_format_version": "",
|
||||
// "input": null,
|
||||
// "measurement_start_time": "",
|
||||
// "probe_asn": "",
|
||||
// "probe_cc": "",
|
||||
// "probe_network_name": "",
|
||||
// "report_id": "",
|
||||
// "resolver_asn": "",
|
||||
// "resolver_ip": "",
|
||||
// "resolver_network_name": "",
|
||||
// "software_name": "",
|
||||
// "software_version": "",
|
||||
// "test_keys": {
|
||||
// "bootstrap_time": 10,
|
||||
// "failure": null
|
||||
// },
|
||||
// "test_name": "",
|
||||
// "test_runtime": 0,
|
||||
// "test_start_time": "",
|
||||
// "test_version": ""
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// You see that now we're filling the bootstrap time and we're
|
||||
// also printing progress using `callbacks`.
|
||||
//
|
||||
// In the next chapter, we'll replace the stub `run` implementation
|
||||
// with a real implementation using Snowflake.
|
||||
//
|
||||
// -=-=- StopHere -=-=-
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment.
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
return &SummaryKeys{IsAnomaly: false}, nil
|
||||
}
|
201
internal/tutorial/experiment/torsf/chapter04/README.md
Normal file
201
internal/tutorial/experiment/torsf/chapter04/README.md
Normal file
|
@ -0,0 +1,201 @@
|
|||
|
||||
# Chapter IV: writing minimal torsf experiment
|
||||
|
||||
In this chapter we will replace the code written in the previous
|
||||
chapter that simulates running the torsf experiment with code that
|
||||
uses the `ooni/probe-cli` library to run the real experiment.
|
||||
|
||||
(This file is auto-generated from the corresponding source file,
|
||||
so make sure you don't edit it manually.)
|
||||
|
||||
## Updating the imports
|
||||
|
||||
We need to update the imports of `torsf.go` first to look like this:
|
||||
|
||||
```Go
|
||||
|
||||
import (
|
||||
```
|
||||
|
||||
These are standard library imports.
|
||||
|
||||
```Go
|
||||
"context"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
```
|
||||
|
||||
As we have already seen, the `model` package defines the
|
||||
generic data model used by all experiments.
|
||||
|
||||
```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
|
||||
```
|
||||
|
||||
The `archival` package contains code used to format internal
|
||||
measurements representations to the OONI data format.
|
||||
|
||||
```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
|
||||
```
|
||||
|
||||
The `ptx` package contains pluggable transport code. It includes
|
||||
code to dial with obfs4 and snowflake and code to create a
|
||||
pluggable transport listener.
|
||||
|
||||
```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/ptx"
|
||||
|
||||
```
|
||||
|
||||
The `tunnel` package contains code to create a tunnel. We will
|
||||
use this package to start a `tor` tunnel, which executes the `tor`
|
||||
binary using specified command line arguments.
|
||||
|
||||
```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Rewriting the run method
|
||||
|
||||
Let us now rewrite the `run` method to run a real `torsf`
|
||||
test rather than just pretending to do it.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) run(ctx context.Context,
|
||||
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
|
||||
```
|
||||
|
||||
As a first step, we create a dialer for snowflake using the
|
||||
`ptx` package. This dialer will allow us to create a `net.Conn`-like
|
||||
network connection where traffic is sent using the Snowflake
|
||||
pluggable transport. There are several optional fields in
|
||||
`SnowflakeDialer`, but we don't need to override the default
|
||||
values, so we can just use a default-initialized struct.
|
||||
|
||||
```Go
|
||||
sfdialer := &ptx.SnowflakeDialer{}
|
||||
```
|
||||
|
||||
Let us now create a listener. The `ptx.Listener` is a listener
|
||||
that listens on a local port and speaks the SOCKS5 protocol. When
|
||||
tor connect to this port, the listener will forward the traffic
|
||||
to the Snowflake dialer we previously created. We will also
|
||||
use the session's logger to emit logging messages.
|
||||
|
||||
```Go
|
||||
ptl := &ptx.Listener{
|
||||
PTDialer: sfdialer,
|
||||
Logger: sess.Logger(),
|
||||
}
|
||||
```
|
||||
|
||||
Now we start the listener. This entails opening a port on the
|
||||
local host. If this operation fails, we return an error. In fact,
|
||||
a failure here means a hard error that prevented us from even
|
||||
starting the experiment. Therefore, it's consistent with the
|
||||
`Run`'s expectations to return an error here.
|
||||
|
||||
```Go
|
||||
if err := ptl.Start(); err != nil {
|
||||
testkeys.Failure = archival.NewFailure(err)
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
defer ptl.Stop()
|
||||
```
|
||||
|
||||
Next, we start `tor` using the `tunnel` package. Note how we
|
||||
pass specific `TorArgs` that cause `tor` to know about the
|
||||
pluggable transport created by `ptl` and `sfdialer`.
|
||||
|
||||
```Go
|
||||
tun, err := tunnel.Start(ctx, &tunnel.Config{
|
||||
Name: "tor",
|
||||
Session: sess,
|
||||
TunnelDir: path.Join(sess.TempDir(), "torsf"),
|
||||
Logger: sess.Logger(),
|
||||
TorArgs: []string{
|
||||
"UseBridges", "1",
|
||||
"ClientTransportPlugin", ptl.AsClientTransportPluginArgument(),
|
||||
"Bridge", sfdialer.AsBridgeArgument(),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In case of error, we convert `err` to a OONI failure using
|
||||
the `NewFailure` function of `archival`. This function reduces
|
||||
Go error strings to the error strings used by OONI. You can
|
||||
read the [errors spec](https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md)
|
||||
at the [github.com/ooni/spec repo](https://github.com/ooni/spec).
|
||||
|
||||
Note that, in this case, we return `nil` to the caller, because
|
||||
a failure here is not a fundamental failure in running the
|
||||
experiment, but rather a possibly interesting anomaly.
|
||||
|
||||
```Go
|
||||
if err != nil {
|
||||
testkeys.Failure = archival.NewFailure(err)
|
||||
errch <- nil
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
Otherwise, we successfully created a tor tunnel using Snowflake,
|
||||
so we just close the tunnel and record the bootstrap time.
|
||||
|
||||
```Go
|
||||
defer tun.Stop()
|
||||
testkeys.BootstrapTime = tun.BootstrapTime().Seconds()
|
||||
errch <- nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Running the code
|
||||
|
||||
We can now run the code as follows to obtain:
|
||||
|
||||
```
|
||||
$ go run ./experiment/torsf/chapter04 | jq
|
||||
[...]
|
||||
Jun 21 23:40:50.000 [notice] Bootstrapped 100% (done): Done
|
||||
2021/06/21 23:40:50 info [100.0%] torsf experiment is finished
|
||||
Jun 21 23:40:50.000 [notice] Catching signal TERM, exiting cleanly
|
||||
{
|
||||
"data_format_version": "",
|
||||
"input": null,
|
||||
"measurement_start_time": "",
|
||||
"probe_asn": "",
|
||||
"probe_cc": "",
|
||||
"probe_network_name": "",
|
||||
"report_id": "",
|
||||
"resolver_asn": "",
|
||||
"resolver_ip": "",
|
||||
"resolver_network_name": "",
|
||||
"software_name": "",
|
||||
"software_version": "",
|
||||
"test_keys": {
|
||||
"bootstrap_time": 48.122813,
|
||||
"failure": null
|
||||
},
|
||||
"test_name": "",
|
||||
"test_runtime": 0,
|
||||
"test_start_time": "",
|
||||
"test_version": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Concluding remarks
|
||||
|
||||
Congratulations, we have now rewritten together (a simplified version of)
|
||||
the `torsf` experiment! In this journey, we have learned how experiments
|
||||
interact with the rest of OONI Probe, how they are typically organized,
|
||||
and how to use lower-level libraries to implement them.
|
||||
|
39
internal/tutorial/experiment/torsf/chapter04/main.go
Normal file
39
internal/tutorial/experiment/torsf/chapter04/main.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := execabs.LookPath("tor"); err != nil {
|
||||
log.Fatal("cannot find the tor executable in path")
|
||||
}
|
||||
tempdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("cannot create temporary directory")
|
||||
}
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTempDir: tempdir,
|
||||
}
|
||||
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
log.WithError(err).Fatal("torsf experiment failed")
|
||||
}
|
||||
data, err := json.Marshal(measurement)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("json.Marshal failed")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
277
internal/tutorial/experiment/torsf/chapter04/torsf.go
Normal file
277
internal/tutorial/experiment/torsf/chapter04/torsf.go
Normal file
|
@ -0,0 +1,277 @@
|
|||
package main
|
||||
|
||||
// -=-=- StartHere -=-=-
|
||||
//
|
||||
// # Chapter IV: writing minimal torsf experiment
|
||||
//
|
||||
// In this chapter we will replace the code written in the previous
|
||||
// chapter that simulates running the torsf experiment with code that
|
||||
// uses the `ooni/probe-cli` library to run the real experiment.
|
||||
//
|
||||
// (This file is auto-generated from the corresponding source file,
|
||||
// so make sure you don't edit it manually.)
|
||||
//
|
||||
// ## Updating the imports
|
||||
//
|
||||
// We need to update the imports of `torsf.go` first to look like this:
|
||||
//
|
||||
// ```Go
|
||||
|
||||
import (
|
||||
// ```
|
||||
//
|
||||
// These are standard library imports.
|
||||
//
|
||||
// ```Go
|
||||
"context"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
// ```
|
||||
//
|
||||
// As we have already seen, the `model` package defines the
|
||||
// generic data model used by all experiments.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
|
||||
// ```
|
||||
//
|
||||
// The `archival` package contains code used to format internal
|
||||
// measurements representations to the OONI data format.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
|
||||
// ```
|
||||
//
|
||||
// The `ptx` package contains pluggable transport code. It includes
|
||||
// code to dial with obfs4 and snowflake and code to create a
|
||||
// pluggable transport listener.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/ptx"
|
||||
|
||||
// ```
|
||||
//
|
||||
// The `tunnel` package contains code to create a tunnel. We will
|
||||
// use this package to start a `tor` tunnel, which executes the `tor`
|
||||
// binary using specified command line arguments.
|
||||
//
|
||||
// ```Go
|
||||
"github.com/ooni/probe-cli/v3/internal/tunnel"
|
||||
)
|
||||
|
||||
// ```
|
||||
//
|
||||
// -=-=- StopHere -=-=-
|
||||
|
||||
// Config contains config for the torsf experiment.
|
||||
type Config struct{}
|
||||
|
||||
// Measurer is the torsf measurer.
|
||||
type Measurer struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// newExperimentMeasurer creates a new ExperimentMeasurer for torsf.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return &Measurer{config: config}
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperimentName.
|
||||
func (m *Measurer) ExperimentName() string {
|
||||
return "torsf"
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||
func (m *Measurer) ExperimentVersion() string {
|
||||
return "0.1.0"
|
||||
}
|
||||
|
||||
// TestKeys contains the experiment results.
|
||||
type TestKeys struct {
|
||||
// BootstrapTime is the time required to bootstrap.
|
||||
BootstrapTime float64 `json:"bootstrap_time"`
|
||||
|
||||
// Failure is the failure that occurred, or nil.
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
// Run implements ExperimentMeasurer.Run.
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
testkeys := &TestKeys{}
|
||||
measurement.TestKeys = testkeys
|
||||
start := time.Now()
|
||||
const maxRuntime = 300 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
||||
defer cancel()
|
||||
errch := make(chan error)
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
go m.run(ctx, sess, testkeys, errch)
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
callbacks.OnProgress(1.0, "torsf experiment is finished")
|
||||
return err
|
||||
case <-ticker.C:
|
||||
progress := time.Since(start).Seconds() / maxRuntime.Seconds()
|
||||
callbacks.OnProgress(progress, "torsf experiment is running")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -=-=- StartHere -=-=-
|
||||
//
|
||||
// ## Rewriting the run method
|
||||
//
|
||||
// Let us now rewrite the `run` method to run a real `torsf`
|
||||
// test rather than just pretending to do it.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) run(ctx context.Context,
|
||||
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
|
||||
// ```
|
||||
//
|
||||
// As a first step, we create a dialer for snowflake using the
|
||||
// `ptx` package. This dialer will allow us to create a `net.Conn`-like
|
||||
// network connection where traffic is sent using the Snowflake
|
||||
// pluggable transport. There are several optional fields in
|
||||
// `SnowflakeDialer`, but we don't need to override the default
|
||||
// values, so we can just use a default-initialized struct.
|
||||
//
|
||||
// ```Go
|
||||
sfdialer := &ptx.SnowflakeDialer{}
|
||||
// ```
|
||||
//
|
||||
// Let us now create a listener. The `ptx.Listener` is a listener
|
||||
// that listens on a local port and speaks the SOCKS5 protocol. When
|
||||
// tor connect to this port, the listener will forward the traffic
|
||||
// to the Snowflake dialer we previously created. We will also
|
||||
// use the session's logger to emit logging messages.
|
||||
//
|
||||
// ```Go
|
||||
ptl := &ptx.Listener{
|
||||
PTDialer: sfdialer,
|
||||
Logger: sess.Logger(),
|
||||
}
|
||||
// ```
|
||||
//
|
||||
// Now we start the listener. This entails opening a port on the
|
||||
// local host. If this operation fails, we return an error. In fact,
|
||||
// a failure here means a hard error that prevented us from even
|
||||
// starting the experiment. Therefore, it's consistent with the
|
||||
// `Run`'s expectations to return an error here.
|
||||
//
|
||||
// ```Go
|
||||
if err := ptl.Start(); err != nil {
|
||||
testkeys.Failure = archival.NewFailure(err)
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
defer ptl.Stop()
|
||||
// ```
|
||||
//
|
||||
// Next, we start `tor` using the `tunnel` package. Note how we
|
||||
// pass specific `TorArgs` that cause `tor` to know about the
|
||||
// pluggable transport created by `ptl` and `sfdialer`.
|
||||
//
|
||||
// ```Go
|
||||
tun, err := tunnel.Start(ctx, &tunnel.Config{
|
||||
Name: "tor",
|
||||
Session: sess,
|
||||
TunnelDir: path.Join(sess.TempDir(), "torsf"),
|
||||
Logger: sess.Logger(),
|
||||
TorArgs: []string{
|
||||
"UseBridges", "1",
|
||||
"ClientTransportPlugin", ptl.AsClientTransportPluginArgument(),
|
||||
"Bridge", sfdialer.AsBridgeArgument(),
|
||||
},
|
||||
})
|
||||
// ```
|
||||
//
|
||||
// In case of error, we convert `err` to a OONI failure using
|
||||
// the `NewFailure` function of `archival`. This function reduces
|
||||
// Go error strings to the error strings used by OONI. You can
|
||||
// read the [errors spec](https://github.com/ooni/spec/blob/master/data-formats/df-007-errors.md)
|
||||
// at the [github.com/ooni/spec repo](https://github.com/ooni/spec).
|
||||
//
|
||||
// Note that, in this case, we return `nil` to the caller, because
|
||||
// a failure here is not a fundamental failure in running the
|
||||
// experiment, but rather a possibly interesting anomaly.
|
||||
//
|
||||
// ```Go
|
||||
if err != nil {
|
||||
testkeys.Failure = archival.NewFailure(err)
|
||||
errch <- nil
|
||||
return
|
||||
}
|
||||
// ```
|
||||
//
|
||||
// Otherwise, we successfully created a tor tunnel using Snowflake,
|
||||
// so we just close the tunnel and record the bootstrap time.
|
||||
//
|
||||
// ```Go
|
||||
defer tun.Stop()
|
||||
testkeys.BootstrapTime = tun.BootstrapTime().Seconds()
|
||||
errch <- nil
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ## Running the code
|
||||
//
|
||||
// We can now run the code as follows to obtain:
|
||||
//
|
||||
// ```
|
||||
// $ go run ./experiment/torsf/chapter04 | jq
|
||||
// [...]
|
||||
// Jun 21 23:40:50.000 [notice] Bootstrapped 100% (done): Done
|
||||
// 2021/06/21 23:40:50 info [100.0%] torsf experiment is finished
|
||||
// Jun 21 23:40:50.000 [notice] Catching signal TERM, exiting cleanly
|
||||
// {
|
||||
// "data_format_version": "",
|
||||
// "input": null,
|
||||
// "measurement_start_time": "",
|
||||
// "probe_asn": "",
|
||||
// "probe_cc": "",
|
||||
// "probe_network_name": "",
|
||||
// "report_id": "",
|
||||
// "resolver_asn": "",
|
||||
// "resolver_ip": "",
|
||||
// "resolver_network_name": "",
|
||||
// "software_name": "",
|
||||
// "software_version": "",
|
||||
// "test_keys": {
|
||||
// "bootstrap_time": 48.122813,
|
||||
// "failure": null
|
||||
// },
|
||||
// "test_name": "",
|
||||
// "test_runtime": 0,
|
||||
// "test_start_time": "",
|
||||
// "test_version": ""
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// ## Concluding remarks
|
||||
//
|
||||
// Congratulations, we have now rewritten together (a simplified version of)
|
||||
// the `torsf` experiment! In this journey, we have learned how experiments
|
||||
// interact with the rest of OONI Probe, how they are typically organized,
|
||||
// and how to use lower-level libraries to implement them.
|
||||
//
|
||||
// -=-=- StopHere -=-=-
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment.
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
return &SummaryKeys{IsAnomaly: false}, nil
|
||||
}
|
95
internal/tutorial/generator/main.go
Normal file
95
internal/tutorial/generator/main.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Command generator generates or re-generates the tutorial chapters. You
|
||||
// should run this command like `go run ./generator`.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// writeString writes a string on the given writer. If there
|
||||
// is a write error, this function will call log.Fatal.
|
||||
func writeString(w io.Writer, s string) {
|
||||
if _, err := io.WriteString(w, s); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// gen1 generates a single file within a chapter.
|
||||
func gen1(destfile io.Writer, filepath string) {
|
||||
srcfile, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer srcfile.Close()
|
||||
scanner := bufio.NewScanner(srcfile)
|
||||
var started bool
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.Trim(line, " \t\r\n")
|
||||
if trimmed == "// -=-=- StopHere -=-=-" {
|
||||
started = false
|
||||
continue
|
||||
}
|
||||
if trimmed == "// -=-=- StartHere -=-=-" {
|
||||
started = true
|
||||
continue
|
||||
}
|
||||
if !started {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "//") {
|
||||
if strings.HasPrefix(trimmed, "// ") {
|
||||
trimmed = trimmed[3:]
|
||||
} else {
|
||||
trimmed = trimmed[2:]
|
||||
}
|
||||
writeString(destfile, trimmed+"\n")
|
||||
continue
|
||||
}
|
||||
writeString(destfile, line+"\n")
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// gen generates or re-generates a chapter. The dirpath argument
|
||||
// is the path to the directory that contains a chapter. The files
|
||||
// arguments contains the source file names to process. We will process
|
||||
// files using the specified order. Note that files names are not
|
||||
// paths, just file names, e.g.,
|
||||
//
|
||||
// gen("./experiment/torsf/chapter01", "main.go")
|
||||
func gen(dirpath string, files ...string) {
|
||||
readme := path.Join(dirpath, "README.md")
|
||||
destfile, err := os.Create(path.Join(readme))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := destfile.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
for _, file := range files {
|
||||
gen1(destfile, path.Join(dirpath, file))
|
||||
}
|
||||
}
|
||||
|
||||
// gentorsf generates the torsf chapters.
|
||||
func gentorsf() {
|
||||
prefix := path.Join(".", "experiment", "torsf")
|
||||
gen(path.Join(prefix, "chapter01"), "main.go")
|
||||
gen(path.Join(prefix, "chapter02"), "main.go", "torsf.go")
|
||||
gen(path.Join(prefix, "chapter03"), "torsf.go")
|
||||
gen(path.Join(prefix, "chapter04"), "torsf.go")
|
||||
}
|
||||
|
||||
func main() {
|
||||
gentorsf()
|
||||
}
|
Loading…
Reference in New Issue
Block a user