ooni-probe-cli/internal/tutorial/experiment/torsf/chapter01/main.go

288 lines
8.4 KiB
Go
Raw Normal View History

// -=-=- 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/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.