287 lines
7.6 KiB
Markdown
287 lines
7.6 KiB
Markdown
|
|
||
|
# 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.
|