feat(miniooni): make CLI much more user friendly (#913)

Part of https://github.com/ooni/probe/issues/2184, because I wanted
to allow swapping commands and options more freely.

As a side effect, this PR closes https://github.com/ooni/probe/issues/2248.

AFAICT, every usage that was legal before is still legal. What has
changed seems the freedom to swap commands and options and a much
better help that lists the available options.
This commit is contained in:
Simone Basso 2022-08-31 12:44:46 +02:00 committed by GitHub
parent 7daa686c68
commit 0bc6aae601
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 280 additions and 139 deletions

6
go.mod
View File

@ -43,6 +43,11 @@ require (
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 golang.org/x/sys v0.0.0-20220818161305-2296e01440c6
) )
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
require ( require (
filippo.io/edwards25519 v1.0.0 // indirect filippo.io/edwards25519 v1.0.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
@ -109,6 +114,7 @@ require (
github.com/refraction-networking/utls v1.0.0 // indirect github.com/refraction-networking/utls v1.0.0 // indirect
github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 // indirect github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cobra v1.5.0
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/templexxx/cpu v0.0.9 // indirect github.com/templexxx/cpu v0.0.9 // indirect
github.com/templexxx/xorsimd v0.4.1 // indirect github.com/templexxx/xorsimd v0.4.1 // indirect

6
go.sum
View File

@ -156,6 +156,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@ -398,6 +399,7 @@ github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
@ -778,6 +780,7 @@ github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36t
github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735 h1:7YvPJVmEeFHR1Tj9sZEYsmarJEQfMVYpd/Vyy/A8dqE= github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735 h1:7YvPJVmEeFHR1Tj9sZEYsmarJEQfMVYpd/Vyy/A8dqE=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
@ -839,10 +842,13 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=

View File

@ -13,7 +13,7 @@ import (
// acquireUserConsent ensures the user is okay with using miniooni. This function // acquireUserConsent ensures the user is okay with using miniooni. This function
// panics if we do not have acquired the user consent. // panics if we do not have acquired the user consent.
func acquireUserConsent(miniooniDir string, currentOptions Options) { func acquireUserConsent(miniooniDir string, currentOptions *Options) {
consentFile := path.Join(miniooniDir, "informed") consentFile := path.Join(miniooniDir, "informed")
err := maybeWriteConsentFile(currentOptions.Yes, consentFile) err := maybeWriteConsentFile(currentOptions.Yes, consentFile)
runtimex.PanicOnError(err, "cannot write informed consent file") runtimex.PanicOnError(err, "cannot write informed consent file")

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strings"
"time" "time"
"github.com/apex/log" "github.com/apex/log"
@ -14,9 +15,10 @@ import (
"github.com/ooni/probe-cli/v3/internal/humanize" "github.com/ooni/probe-cli/v3/internal/humanize"
"github.com/ooni/probe-cli/v3/internal/legacy/assetsdir" "github.com/ooni/probe-cli/v3/internal/legacy/assetsdir"
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/registry"
"github.com/ooni/probe-cli/v3/internal/runtimex" "github.com/ooni/probe-cli/v3/internal/runtimex"
"github.com/ooni/probe-cli/v3/internal/version" "github.com/ooni/probe-cli/v3/internal/version"
"github.com/pborman/getopt/v2" "github.com/spf13/cobra"
) )
// Options contains the options you can set from the CLI. // Options contains the options you can set from the CLI.
@ -38,118 +40,227 @@ type Options struct {
TorBinary string TorBinary string
Tunnel string Tunnel string
Verbose bool Verbose bool
Version bool
Yes bool Yes bool
} }
var globalOptions Options // main is the main function of miniooni.
func init() {
getopt.FlagLong(
&globalOptions.Annotations, "annotation", 'A', "Add annotaton", "KEY=VALUE",
)
getopt.FlagLong(
&globalOptions.ExtraOptions, "option", 'O',
"Pass an option to the experiment", "KEY=VALUE",
)
getopt.FlagLong(
&globalOptions.InputFilePaths, "input-file", 'f',
"Path to input file to supply test-dependent input. File must contain one input per line.", "PATH",
)
getopt.FlagLong(
&globalOptions.HomeDir, "home", 0,
"Force specific home directory", "PATH",
)
getopt.FlagLong(
&globalOptions.Inputs, "input", 'i',
"Add test-dependent input to the test input", "INPUT",
)
getopt.FlagLong(
&globalOptions.MaxRuntime, "max-runtime", 0,
"Maximum runtime in seconds when looping over a list of inputs (zero means infinite)", "N",
)
getopt.FlagLong(
&globalOptions.NoJSON, "no-json", 'N', "Disable writing to disk",
)
getopt.FlagLong(
&globalOptions.NoCollector, "no-collector", 'n', "Don't use a collector",
)
getopt.FlagLong(
&globalOptions.ProbeServicesURL, "probe-services", 0,
"Set the URL of the probe-services instance you want to use", "URL",
)
getopt.FlagLong(
&globalOptions.Proxy, "proxy", 0, "Set the proxy URL", "URL",
)
getopt.FlagLong(
&globalOptions.Random, "random", 0, "Randomize inputs",
)
getopt.FlagLong(
&globalOptions.RepeatEvery, "repeat-every", 0,
"Repeat the measurement every INTERVAL number of seconds", "INTERVAL",
)
getopt.FlagLong(
&globalOptions.ReportFile, "reportfile", 'o',
"Set the report file path", "PATH",
)
getopt.FlagLong(
&globalOptions.TorArgs, "tor-args", 0,
"Extra args for tor binary (may be specified multiple times)",
)
getopt.FlagLong(
&globalOptions.TorBinary, "tor-binary", 0,
"Specify path to a specific tor binary",
)
getopt.FlagLong(
&globalOptions.Tunnel, "tunnel", 0,
"Name of the tunnel to use (one of `tor`, `psiphon`)",
)
getopt.FlagLong(
&globalOptions.Verbose, "verbose", 'v', "Increase verbosity",
)
getopt.FlagLong(
&globalOptions.Version, "version", 0, "Print version and exit",
)
getopt.FlagLong(
&globalOptions.Yes, "yes", 'y',
"Assume yes as the answer to all questions",
)
}
// main is the main function of miniooni. This function parses the command line
// options and uses a global state. Use MainWithConfiguration if you want to avoid
// using any global state and relying on command line options.
//
// This function will panic in case of a fatal error. It is up to you that
// integrate this function to either handle the panic of ignore it.
func main() { func main() {
getopt.Parse() var globalOptions Options
if globalOptions.Version { rootCmd := &cobra.Command{
fmt.Printf("%s\n", version.Version) Use: "miniooni",
os.Exit(0) Short: "miniooni is OONI's research client",
Args: cobra.NoArgs,
Version: version.Version,
}
rootCmd.SetVersionTemplate("{{ .Version }}\n")
flags := rootCmd.PersistentFlags()
flags.StringSliceVarP(
&globalOptions.Annotations,
"annotation",
"A",
[]string{},
"add KEY=VALUE annotation to the report (can be repeated multiple times)",
)
flags.StringVar(
&globalOptions.HomeDir,
"home",
"",
"force specific home directory",
)
flags.BoolVarP(
&globalOptions.NoJSON,
"no-json",
"N",
false,
"disable writing to disk",
)
flags.BoolVarP(
&globalOptions.NoCollector,
"no-collector",
"n",
false,
"do not submit measurements to the OONI collector",
)
flags.StringVar(
&globalOptions.ProbeServicesURL,
"probe-services",
"",
"URL of the OONI backend instance you want to use",
)
flags.StringVar(
&globalOptions.Proxy,
"proxy",
"",
"set proxy URL to communicate with the OONI backend (mutually exclusive with --tunnel)",
)
flags.Int64Var(
&globalOptions.RepeatEvery,
"repeat-every",
0,
"wait the given number of seconds and then repeat the same measurement",
)
flags.StringVarP(
&globalOptions.ReportFile,
"reportfile",
"o",
"",
"set the output report file path (default: \"report.jsonl\")",
)
flags.StringSliceVar(
&globalOptions.TorArgs,
"tor-args",
[]string{},
"extra arguments for the tor binary (may be specified multiple times)",
)
flags.StringVar(
&globalOptions.TorBinary,
"tor-binary",
"",
"execute a specific tor binary",
)
flags.StringVar(
&globalOptions.Tunnel,
"tunnel",
"",
"tunnel to use to communicate with the OONI backend (one of: tor, psiphon)",
)
flags.BoolVarP(
&globalOptions.Verbose,
"verbose",
"v",
false,
"increase verbosity level",
)
flags.BoolVarP(
&globalOptions.Yes,
"yes",
"y",
false,
"assume yes as the answer to all questions",
)
rootCmd.MarkFlagsMutuallyExclusive("proxy", "tunnel")
registerAllExperiments(rootCmd, &globalOptions)
registerOONIRun(rootCmd, &globalOptions)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
} }
runtimex.PanicIfFalse(len(getopt.Args()) == 1, "Missing experiment name")
runtimex.PanicOnError(engine.CheckEmbeddedPsiphonConfig(), "Invalid embedded psiphon config")
MainWithConfiguration(getopt.Arg(0), globalOptions)
} }
// tunnelAndProxy is the text printed when the user specifies // TODO(bassosimone): the current implementation is basically a cobra application
// both the --tunnel and the --proxy options // where we hammered the previous miniooni code to make it work. We should
const tunnelAndProxy = `USAGE ERROR: The --tunnel option and the --proxy // obviously strive for more correctness. For example, it's a bit disgusting
option cannot be specified at the same time. The --tunnel option is actually // that MainWithConfiguration is invoked for both oonirun and random experiments.
just syntactic sugar for --proxy. Setting --tunnel=psiphon is currently the
equivalent of setting --proxy=psiphon:///. This MAY change in a future version // registerOONIRun registers the oonirun subcommand
of miniooni, when we will allow a tunnel to use a proxy. func registerOONIRun(rootCmd *cobra.Command, globalOptions *Options) {
` subCmd := &cobra.Command{
Use: "oonirun",
Short: "Runs a given OONI Run v2 link",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
MainWithConfiguration(cmd.Use, globalOptions)
},
}
rootCmd.AddCommand(subCmd)
flags := subCmd.Flags()
flags.StringSliceVarP(
&globalOptions.Inputs,
"input",
"i",
[]string{},
"URL of the OONI Run v2 descriptor to run (may be specified multiple times)",
)
}
// registerAllExperiments registers a subcommand for each experiment
func registerAllExperiments(rootCmd *cobra.Command, globalOptions *Options) {
for name, factory := range registry.AllExperiments {
subCmd := &cobra.Command{
Use: name,
Short: fmt.Sprintf("Runs the %s experiment", name),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
MainWithConfiguration(cmd.Use, globalOptions)
},
}
rootCmd.AddCommand(subCmd)
flags := subCmd.Flags()
switch factory.InputPolicy() {
case model.InputOrQueryBackend,
model.InputStrictlyRequired,
model.InputOptional,
model.InputOrStaticDefault:
flags.StringSliceVarP(
&globalOptions.InputFilePaths,
"input-file",
"f",
[]string{},
"path to file to supply test dependent input (may be specified multiple times)",
)
flags.StringSliceVarP(
&globalOptions.Inputs,
"input",
"i",
[]string{},
"add test-dependent input (may be specified multiple times)",
)
flags.Int64Var(
&globalOptions.MaxRuntime,
"max-runtime",
0,
"maximum runtime in seconds for the experiment (zero means infinite)",
)
flags.BoolVar(
&globalOptions.Random,
"random",
false,
"randomize the inputs list",
)
default:
// nothing
}
if doc := documentationForOptions(name, factory); doc != "" {
flags.StringSliceVarP(
&globalOptions.ExtraOptions,
"option",
"O",
[]string{},
doc,
)
}
}
}
// MainWithConfiguration is the miniooni main with a specific configuration // MainWithConfiguration is the miniooni main with a specific configuration
// represented by the experiment name and the current options. // represented by the experiment name and the current options.
// //
// This function will panic in case of a fatal error. It is up to you that // This function will panic in case of a fatal error. It is up to you that
// integrate this function to either handle the panic of ignore it. // integrate this function to either handle the panic of ignore it.
func MainWithConfiguration(experimentName string, currentOptions Options) { func MainWithConfiguration(experimentName string, currentOptions *Options) {
runtimex.PanicIfTrue(currentOptions.Proxy != "" && currentOptions.Tunnel != "", runtimex.PanicOnError(engine.CheckEmbeddedPsiphonConfig(), "Invalid embedded psiphon config")
tunnelAndProxy)
if currentOptions.Tunnel != "" { if currentOptions.Tunnel != "" {
currentOptions.Proxy = fmt.Sprintf("%s:///", currentOptions.Tunnel) currentOptions.Proxy = fmt.Sprintf("%s:///", currentOptions.Tunnel)
} }
@ -175,7 +286,7 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
// mainSingleIteration runs a single iteration. There may be multiple iterations // mainSingleIteration runs a single iteration. There may be multiple iterations
// when the user specifies the --repeat-every command line flag. // when the user specifies the --repeat-every command line flag.
func mainSingleIteration(logger model.Logger, experimentName string, currentOptions Options) { func mainSingleIteration(logger model.Logger, experimentName string, currentOptions *Options) {
extraOptions := mustMakeMapStringAny(currentOptions.ExtraOptions) extraOptions := mustMakeMapStringAny(currentOptions.ExtraOptions)
annotations := mustMakeMapStringString(currentOptions.Annotations) annotations := mustMakeMapStringString(currentOptions.Annotations)
@ -225,3 +336,21 @@ func mainSingleIteration(logger model.Logger, experimentName string, currentOpti
// Otherwise just run OONI experiments as we normally do. // Otherwise just run OONI experiments as we normally do.
runx(ctx, sess, experimentName, annotations, extraOptions, currentOptions) runx(ctx, sess, experimentName, annotations, extraOptions, currentOptions)
} }
func documentationForOptions(name string, factory *registry.Factory) string {
var sb strings.Builder
options, err := factory.Options()
if err != nil || len(options) < 1 {
return ""
}
fmt.Fprint(&sb, "Pass KEY=VALUE options to the experiment. Available options:\n")
for name, info := range options {
if info.Doc == "" {
continue
}
fmt.Fprintf(&sb, "\n")
fmt.Fprintf(&sb, " -O, --option %s=<%s>\n", name, info.Type)
fmt.Fprintf(&sb, " %s\n", info.Doc)
}
return sb.String()
}

View File

@ -6,7 +6,7 @@ func TestSimple(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
MainWithConfiguration("example", Options{ MainWithConfiguration("example", &Options{
Yes: true, Yes: true,
}) })
} }

View File

@ -16,7 +16,7 @@ import (
// ooniRunMain runs the experiments described by the given OONI Run URLs. This // ooniRunMain runs the experiments described by the given OONI Run URLs. This
// function works with both v1 and v2 OONI Run URLs. // function works with both v1 and v2 OONI Run URLs.
func ooniRunMain(ctx context.Context, func ooniRunMain(ctx context.Context,
sess *engine.Session, currentOptions Options, annotations map[string]string) { sess *engine.Session, currentOptions *Options, annotations map[string]string) {
runtimex.PanicIfTrue( runtimex.PanicIfTrue(
len(currentOptions.Inputs) <= 0, len(currentOptions.Inputs) <= 0,
"in oonirun mode you need to specify at least one URL using `-i URL`", "in oonirun mode you need to specify at least one URL using `-i URL`",

View File

@ -13,7 +13,7 @@ import (
// runx runs the given experiment by name // runx runs the given experiment by name
func runx(ctx context.Context, sess oonirun.Session, experimentName string, func runx(ctx context.Context, sess oonirun.Session, experimentName string,
annotations map[string]string, extraOptions map[string]any, currentOptions Options) { annotations map[string]string, extraOptions map[string]any, currentOptions *Options) {
desc := &oonirun.Experiment{ desc := &oonirun.Experiment{
Annotations: annotations, Annotations: annotations,
ExtraOptions: extraOptions, ExtraOptions: extraOptions,

View File

@ -20,7 +20,7 @@ const (
) )
// newSessionOrPanic creates and starts a new session or panics on failure // newSessionOrPanic creates and starts a new session or panics on failure
func newSessionOrPanic(ctx context.Context, currentOptions Options, func newSessionOrPanic(ctx context.Context, currentOptions *Options,
miniooniDir string, logger model.Logger) *engine.Session { miniooniDir string, logger model.Logger) *engine.Session {
var proxyURL *url.URL var proxyURL *url.URL
if currentOptions.Proxy != "" { if currentOptions.Proxy != "" {

View File

@ -1,11 +1,11 @@
package registry package registry
// Where we register all the available experiments. // Where we register all the available experiments.
var allexperiments = map[string]*Factory{} var AllExperiments = map[string]*Factory{}
// ExperimentNames returns the name of all experiments // ExperimentNames returns the name of all experiments
func ExperimentNames() (names []string) { func ExperimentNames() (names []string) {
for key := range allexperiments { for key := range AllExperiments {
names = append(names, key) names = append(names, key)
} }
return return

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["dash"] = &Factory{ AllExperiments["dash"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return dash.NewExperimentMeasurer( return dash.NewExperimentMeasurer(
*config.(*dash.Config), *config.(*dash.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["dnscheck"] = &Factory{ AllExperiments["dnscheck"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return dnscheck.NewExperimentMeasurer( return dnscheck.NewExperimentMeasurer(
*config.(*dnscheck.Config), *config.(*dnscheck.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["dnsping"] = &Factory{ AllExperiments["dnsping"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return dnsping.NewExperimentMeasurer( return dnsping.NewExperimentMeasurer(
*config.(*dnsping.Config), *config.(*dnsping.Config),

View File

@ -12,7 +12,7 @@ import (
) )
func init() { func init() {
allexperiments["example"] = &Factory{ AllExperiments["example"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return example.NewExperimentMeasurer( return example.NewExperimentMeasurer(
*config.(*example.Config), "example", *config.(*example.Config), "example",

View File

@ -208,6 +208,8 @@ func CanonicalizeExperimentName(name string) string {
name = "dnscheck" name = "dnscheck"
case "stun_reachability": case "stun_reachability":
name = "stunreachability" name = "stunreachability"
case "web_connectivity@v_0_5":
name = "web_connectivity@v0.5"
default: default:
} }
return name return name
@ -216,7 +218,7 @@ func CanonicalizeExperimentName(name string) string {
// NewFactory creates a new Factory instance. // NewFactory creates a new Factory instance.
func NewFactory(name string) (*Factory, error) { func NewFactory(name string) (*Factory, error) {
name = CanonicalizeExperimentName(name) name = CanonicalizeExperimentName(name)
factory := allexperiments[name] factory := AllExperiments[name]
if factory == nil { if factory == nil {
return nil, fmt.Errorf("no such experiment: %s", name) return nil, fmt.Errorf("no such experiment: %s", name)
} }

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["facebook_messenger"] = &Factory{ AllExperiments["facebook_messenger"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return fbmessenger.NewExperimentMeasurer( return fbmessenger.NewExperimentMeasurer(
*config.(*fbmessenger.Config), *config.(*fbmessenger.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["http_header_field_manipulation"] = &Factory{ AllExperiments["http_header_field_manipulation"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return hhfm.NewExperimentMeasurer( return hhfm.NewExperimentMeasurer(
*config.(*hhfm.Config), *config.(*hhfm.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["http_invalid_request_line"] = &Factory{ AllExperiments["http_invalid_request_line"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return hirl.NewExperimentMeasurer( return hirl.NewExperimentMeasurer(
*config.(*hirl.Config), *config.(*hirl.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["http_host_header"] = &Factory{ AllExperiments["http_host_header"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return httphostheader.NewExperimentMeasurer( return httphostheader.NewExperimentMeasurer(
*config.(*httphostheader.Config), *config.(*httphostheader.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["ndt"] = &Factory{ AllExperiments["ndt"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return ndt7.NewExperimentMeasurer( return ndt7.NewExperimentMeasurer(
*config.(*ndt7.Config), *config.(*ndt7.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["psiphon"] = &Factory{ AllExperiments["psiphon"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return psiphon.NewExperimentMeasurer( return psiphon.NewExperimentMeasurer(
*config.(*psiphon.Config), *config.(*psiphon.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["quicping"] = &Factory{ AllExperiments["quicping"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return quicping.NewExperimentMeasurer( return quicping.NewExperimentMeasurer(
*config.(*quicping.Config), *config.(*quicping.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["riseupvpn"] = &Factory{ AllExperiments["riseupvpn"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return riseupvpn.NewExperimentMeasurer( return riseupvpn.NewExperimentMeasurer(
*config.(*riseupvpn.Config), *config.(*riseupvpn.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["run"] = &Factory{ AllExperiments["run"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return run.NewExperimentMeasurer( return run.NewExperimentMeasurer(
*config.(*run.Config), *config.(*run.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["signal"] = &Factory{ AllExperiments["signal"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return signal.NewExperimentMeasurer( return signal.NewExperimentMeasurer(
*config.(*signal.Config), *config.(*signal.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["simplequicping"] = &Factory{ AllExperiments["simplequicping"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return simplequicping.NewExperimentMeasurer( return simplequicping.NewExperimentMeasurer(
*config.(*simplequicping.Config), *config.(*simplequicping.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["sni_blocking"] = &Factory{ AllExperiments["sni_blocking"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return sniblocking.NewExperimentMeasurer( return sniblocking.NewExperimentMeasurer(
*config.(*sniblocking.Config), *config.(*sniblocking.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["stunreachability"] = &Factory{ AllExperiments["stunreachability"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return stunreachability.NewExperimentMeasurer( return stunreachability.NewExperimentMeasurer(
*config.(*stunreachability.Config), *config.(*stunreachability.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["tcpping"] = &Factory{ AllExperiments["tcpping"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return tcpping.NewExperimentMeasurer( return tcpping.NewExperimentMeasurer(
*config.(*tcpping.Config), *config.(*tcpping.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["telegram"] = &Factory{ AllExperiments["telegram"] = &Factory{
build: func(config any) model.ExperimentMeasurer { build: func(config any) model.ExperimentMeasurer {
return telegram.NewExperimentMeasurer( return telegram.NewExperimentMeasurer(
config.(telegram.Config), config.(telegram.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["tlsping"] = &Factory{ AllExperiments["tlsping"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return tlsping.NewExperimentMeasurer( return tlsping.NewExperimentMeasurer(
*config.(*tlsping.Config), *config.(*tlsping.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["tlstool"] = &Factory{ AllExperiments["tlstool"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return tlstool.NewExperimentMeasurer( return tlstool.NewExperimentMeasurer(
*config.(*tlstool.Config), *config.(*tlstool.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["tor"] = &Factory{ AllExperiments["tor"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return tor.NewExperimentMeasurer( return tor.NewExperimentMeasurer(
*config.(*tor.Config), *config.(*tor.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["torsf"] = &Factory{ AllExperiments["torsf"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return torsf.NewExperimentMeasurer( return torsf.NewExperimentMeasurer(
*config.(*torsf.Config), *config.(*torsf.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["urlgetter"] = &Factory{ AllExperiments["urlgetter"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return urlgetter.NewExperimentMeasurer( return urlgetter.NewExperimentMeasurer(
*config.(*urlgetter.Config), *config.(*urlgetter.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["vanilla_tor"] = &Factory{ AllExperiments["vanilla_tor"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return vanillator.NewExperimentMeasurer( return vanillator.NewExperimentMeasurer(
*config.(*vanillator.Config), *config.(*vanillator.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["web_connectivity"] = &Factory{ AllExperiments["web_connectivity"] = &Factory{
build: func(config any) model.ExperimentMeasurer { build: func(config any) model.ExperimentMeasurer {
return webconnectivity.NewExperimentMeasurer( return webconnectivity.NewExperimentMeasurer(
config.(webconnectivity.Config), config.(webconnectivity.Config),

View File

@ -12,9 +12,7 @@ import (
) )
func init() { func init() {
// Note: the name inserted into the table is the canonicalized experiment AllExperiments["web_connectivity@v0.5"] = &Factory{
// name though we advertise using `web_connectivity@v0.5`.
allexperiments["web_connectivity@v_0_5"] = &Factory{
build: func(config any) model.ExperimentMeasurer { build: func(config any) model.ExperimentMeasurer {
return webconnectivity.NewExperimentMeasurer( return webconnectivity.NewExperimentMeasurer(
config.(*webconnectivity.Config), config.(*webconnectivity.Config),

View File

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
allexperiments["whatsapp"] = &Factory{ AllExperiments["whatsapp"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer { build: func(config interface{}) model.ExperimentMeasurer {
return whatsapp.NewExperimentMeasurer( return whatsapp.NewExperimentMeasurer(
*config.(*whatsapp.Config), *config.(*whatsapp.Config),