ooni-probe-cli/internal/tutorial/experiment/torsf/chapter01/README.md

287 lines
7.6 KiB
Markdown
Raw Normal View History

# 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.