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