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