ooni-probe-cli/ooni.go

222 lines
5.7 KiB
Go
Raw Normal View History

package ooni
import (
"io/ioutil"
"os"
Optionally treat EOF on stdin just like SIGTERM (#111) * Optionally treat EOF on stdin just like SIGTERM On Unix, Node.js allows us to gracefully kill a process. On Windows this is more compex. You certainly cannot rely on the default `kill()` function, which calls `TerminateProcess`. There is a bunch of C/C++ extensions that in principle allow you to attempt to gracefully shutdown a Windows process. But, hey, here's a reality check. Node.js controls our stdin. Node.js does IPC easy. Controlling uv_spawn flags and using the right not well maintained C/C++ Node.js extension to kill a process is fragile. So, treat EOF and any other error on stdin as equivalent to SIGTERM. However, systemd. The sane thing to do with systemd is `StandardInput=null`. With such configuration, stdin immediately returns EOF. Then, introduce the `OONI_STDIN_EOF_IMPLIES_SIGTERM` environment variable. When it is `true`, this behaviour is enabled, e.g.: ```bash export OONI_STDIN_EOF_IMPLIES_SIGTERM=true # behaviour enabled ooniprobe run ``` I want the default to be disabled because: 1. in the future we may find a better way to solve this problem and I don't want the _default behaviour_ to change in such case 2. we know we need this knob for ooniprobe-desktop, and we will not fail to provide it, so it won't suprise/damage us 3. a person trying to write a systemd unit for ooniprobe would be very surprised to find out they need to disable this behaviour, if it was enabled by default by this PR Hence, I believe this design is consistent with designing for the future and for trying to minimize surprises. Also, why an environment variable and not a command line flag? Because: 1. we don't want such hypothetical flag to be available where it does not make sense, e.g., for all subcommands but `run` 2. we don't want the ooni/probe-desktop app to write conditional code because it needs to check the command we're using and then decide whether to add such hypothetical flag Also, why not enabling this only on Windows? Because again we don't want the ooni/probe-desktop app to write conditional code. To summarize: we want ooni/probe-desktop app to see the same behaviour everywhere and we want others to be the least surprised. Related to https://github.com/ooni/probe/issues/1005 * Update ooni.go
2020-02-13 14:53:06 +01:00
"os/signal"
"sync/atomic"
Optionally treat EOF on stdin just like SIGTERM (#111) * Optionally treat EOF on stdin just like SIGTERM On Unix, Node.js allows us to gracefully kill a process. On Windows this is more compex. You certainly cannot rely on the default `kill()` function, which calls `TerminateProcess`. There is a bunch of C/C++ extensions that in principle allow you to attempt to gracefully shutdown a Windows process. But, hey, here's a reality check. Node.js controls our stdin. Node.js does IPC easy. Controlling uv_spawn flags and using the right not well maintained C/C++ Node.js extension to kill a process is fragile. So, treat EOF and any other error on stdin as equivalent to SIGTERM. However, systemd. The sane thing to do with systemd is `StandardInput=null`. With such configuration, stdin immediately returns EOF. Then, introduce the `OONI_STDIN_EOF_IMPLIES_SIGTERM` environment variable. When it is `true`, this behaviour is enabled, e.g.: ```bash export OONI_STDIN_EOF_IMPLIES_SIGTERM=true # behaviour enabled ooniprobe run ``` I want the default to be disabled because: 1. in the future we may find a better way to solve this problem and I don't want the _default behaviour_ to change in such case 2. we know we need this knob for ooniprobe-desktop, and we will not fail to provide it, so it won't suprise/damage us 3. a person trying to write a systemd unit for ooniprobe would be very surprised to find out they need to disable this behaviour, if it was enabled by default by this PR Hence, I believe this design is consistent with designing for the future and for trying to minimize surprises. Also, why an environment variable and not a command line flag? Because: 1. we don't want such hypothetical flag to be available where it does not make sense, e.g., for all subcommands but `run` 2. we don't want the ooni/probe-desktop app to write conditional code because it needs to check the command we're using and then decide whether to add such hypothetical flag Also, why not enabling this only on Windows? Because again we don't want the ooni/probe-desktop app to write conditional code. To summarize: we want ooni/probe-desktop app to see the same behaviour everywhere and we want others to be the least surprised. Related to https://github.com/ooni/probe/issues/1005 * Update ooni.go
2020-02-13 14:53:06 +01:00
"syscall"
2018-02-12 16:45:13 +01:00
"github.com/apex/log"
"github.com/ooni/probe-cli/config"
2018-06-25 17:49:17 +02:00
"github.com/ooni/probe-cli/internal/bindata"
"github.com/ooni/probe-cli/internal/database"
Integrate further with ooni/probe-engine: episode two (#46) * utils/geoip.go: use github.com/ooni/probe-engine Let's start using the engine by rewriting utils/geoip.go to be just a thin wrapper around the engine functionality. * Ready for review * Checkpoint: the im tests are converted Still have some doubts with respect to the variables that are passed to MK via probe-engine. Will double check. * fix(i/c/r/run.go): write the correct logic * nettests: one more comment and also fix a format string * Tweak previous * progress * Fix doofus * better comment * XXX => actionable comment * Add glue to simplify test keys management Making the concept of measurement more abstract in the engine is not feasible because, when submitting a measurement, we need to modify it to update the report ID and the measurement ID. Therefore, returning a serialized measurement is not a good idea. We will keep using a model.Measurement in the engine. Changing model.Measurement.TestKeys's type from a `interface{}` pointing to a well defined data structure to `map[string]interface{}` is a regression because means that we are moving from code that has a clear and defined structure to code that is more complicated to parse and validate. Since we're already suffering havily from the lack of a good schema, I'm not going to make the situation worst by worsening the engine. At least for ndt7 and psiphon, we now have a good schema and I don't want to lose that. However, the current code in this repository is expecting the test keys to be a `map[string]interface{}`. This choice was dictated by the fact that we receive a JSON from Measurement Kit and by the fact that there's not a clear schema. To solve this tension, in this commit I am going to write glue adapter code that makes sure that the TestKeys of a Measurement are converted to `map[string]interface{}`. This will be done using a type cast where possible and JSON serialization and parsing otherwise. In a perfect world, glue is not a good idea, but in a real world it may actually be useful. When all tests in the engine will have a clear Go data structure, we'll then remove the glue and just cast to the proper data structure from `interface{}` where required. * nettests/performance: use probe-engine * go.{mod,sum}: upgrade to latest probe-engine * nettests/middlebox: use ooni/probe-engine * Update to the latest probe-engine * web_connectivity: rewrite to use probe-engine * Cosmetic change suggested by @hellais * nettests/nettests.go: remove unused code * nettests/nettests.go: fix progress * nettests/nettests.go: remove go-measurement-kit code * We don't depend on go-measurement-kit anymore * Improve non-verbose output where possible See also: https://github.com/measurement-kit/measurement-kit/issues/1856 * Make web_connectivity output pleasant * Update to the latest probe-engine * nettests/nettests.go: honour sharing settings * Update to the latest probe-engine * Use log.WithFields for probe-engine * Update go.mod go.sum * Revert "Update go.mod go.sum" This reverts commit 5ecd38d8236f4a4e9b77ddb8e8a0d1e3cdd4b818. * Revert "Revert "Update go.mod go.sum"" This reverts commit 6114b31eca98826112032776bd0feff02d763ecd. * Upgrade ooni/probe-engine * Unset GOPATH before running go build commands * Dockefile: fix linux build by using latest * Update to the latest ooni/probe-engine ``` go get -u github.com/ooni/probe-engine go mod tidy ``` * Repair build
2019-08-15 18:08:43 +02:00
"github.com/ooni/probe-cli/internal/enginex"
"github.com/ooni/probe-cli/utils"
engine "github.com/ooni/probe-engine"
"github.com/ooni/probe-engine/model"
"github.com/pkg/errors"
"upper.io/db.v3/lib/sqlbuilder"
)
// Context for OONI Probe
type Context struct {
Integrate further with ooni/probe-engine: episode two (#46) * utils/geoip.go: use github.com/ooni/probe-engine Let's start using the engine by rewriting utils/geoip.go to be just a thin wrapper around the engine functionality. * Ready for review * Checkpoint: the im tests are converted Still have some doubts with respect to the variables that are passed to MK via probe-engine. Will double check. * fix(i/c/r/run.go): write the correct logic * nettests: one more comment and also fix a format string * Tweak previous * progress * Fix doofus * better comment * XXX => actionable comment * Add glue to simplify test keys management Making the concept of measurement more abstract in the engine is not feasible because, when submitting a measurement, we need to modify it to update the report ID and the measurement ID. Therefore, returning a serialized measurement is not a good idea. We will keep using a model.Measurement in the engine. Changing model.Measurement.TestKeys's type from a `interface{}` pointing to a well defined data structure to `map[string]interface{}` is a regression because means that we are moving from code that has a clear and defined structure to code that is more complicated to parse and validate. Since we're already suffering havily from the lack of a good schema, I'm not going to make the situation worst by worsening the engine. At least for ndt7 and psiphon, we now have a good schema and I don't want to lose that. However, the current code in this repository is expecting the test keys to be a `map[string]interface{}`. This choice was dictated by the fact that we receive a JSON from Measurement Kit and by the fact that there's not a clear schema. To solve this tension, in this commit I am going to write glue adapter code that makes sure that the TestKeys of a Measurement are converted to `map[string]interface{}`. This will be done using a type cast where possible and JSON serialization and parsing otherwise. In a perfect world, glue is not a good idea, but in a real world it may actually be useful. When all tests in the engine will have a clear Go data structure, we'll then remove the glue and just cast to the proper data structure from `interface{}` where required. * nettests/performance: use probe-engine * go.{mod,sum}: upgrade to latest probe-engine * nettests/middlebox: use ooni/probe-engine * Update to the latest probe-engine * web_connectivity: rewrite to use probe-engine * Cosmetic change suggested by @hellais * nettests/nettests.go: remove unused code * nettests/nettests.go: fix progress * nettests/nettests.go: remove go-measurement-kit code * We don't depend on go-measurement-kit anymore * Improve non-verbose output where possible See also: https://github.com/measurement-kit/measurement-kit/issues/1856 * Make web_connectivity output pleasant * Update to the latest probe-engine * nettests/nettests.go: honour sharing settings * Update to the latest probe-engine * Use log.WithFields for probe-engine * Update go.mod go.sum * Revert "Update go.mod go.sum" This reverts commit 5ecd38d8236f4a4e9b77ddb8e8a0d1e3cdd4b818. * Revert "Revert "Update go.mod go.sum"" This reverts commit 6114b31eca98826112032776bd0feff02d763ecd. * Upgrade ooni/probe-engine * Unset GOPATH before running go build commands * Dockefile: fix linux build by using latest * Update to the latest ooni/probe-engine ``` go get -u github.com/ooni/probe-engine go mod tidy ``` * Repair build
2019-08-15 18:08:43 +02:00
Config *config.Config
DB sqlbuilder.Database
IsBatch bool
Home string
TempDir string
2018-03-23 12:10:14 +01:00
dbPath string
configPath string
// We need to use a int32 in order to use the atomic.AddInt32/LoadInt32
// operations to ensure consistent reads of the variables. We do not use
// a 64 bit integer here because that may lead to crashes with 32 bit
// OSes as documented in https://golang.org/pkg/sync/atomic/#pkg-note-BUG.
isTerminatedAtomicInt int32
2018-02-12 16:45:13 +01:00
softwareName string
softwareVersion string
}
// IsTerminated checks to see if the isTerminatedAtomicInt is set to a non zero
// value and therefore we have received the signal to shutdown the running test
func (c *Context) IsTerminated() bool {
i := atomic.LoadInt32(&c.isTerminatedAtomicInt)
return i != 0
}
// Terminate interrupts the running context
func (c *Context) Terminate() {
atomic.AddInt32(&c.isTerminatedAtomicInt, 1)
}
Optionally treat EOF on stdin just like SIGTERM (#111) * Optionally treat EOF on stdin just like SIGTERM On Unix, Node.js allows us to gracefully kill a process. On Windows this is more compex. You certainly cannot rely on the default `kill()` function, which calls `TerminateProcess`. There is a bunch of C/C++ extensions that in principle allow you to attempt to gracefully shutdown a Windows process. But, hey, here's a reality check. Node.js controls our stdin. Node.js does IPC easy. Controlling uv_spawn flags and using the right not well maintained C/C++ Node.js extension to kill a process is fragile. So, treat EOF and any other error on stdin as equivalent to SIGTERM. However, systemd. The sane thing to do with systemd is `StandardInput=null`. With such configuration, stdin immediately returns EOF. Then, introduce the `OONI_STDIN_EOF_IMPLIES_SIGTERM` environment variable. When it is `true`, this behaviour is enabled, e.g.: ```bash export OONI_STDIN_EOF_IMPLIES_SIGTERM=true # behaviour enabled ooniprobe run ``` I want the default to be disabled because: 1. in the future we may find a better way to solve this problem and I don't want the _default behaviour_ to change in such case 2. we know we need this knob for ooniprobe-desktop, and we will not fail to provide it, so it won't suprise/damage us 3. a person trying to write a systemd unit for ooniprobe would be very surprised to find out they need to disable this behaviour, if it was enabled by default by this PR Hence, I believe this design is consistent with designing for the future and for trying to minimize surprises. Also, why an environment variable and not a command line flag? Because: 1. we don't want such hypothetical flag to be available where it does not make sense, e.g., for all subcommands but `run` 2. we don't want the ooni/probe-desktop app to write conditional code because it needs to check the command we're using and then decide whether to add such hypothetical flag Also, why not enabling this only on Windows? Because again we don't want the ooni/probe-desktop app to write conditional code. To summarize: we want ooni/probe-desktop app to see the same behaviour everywhere and we want others to be the least surprised. Related to https://github.com/ooni/probe/issues/1005 * Update ooni.go
2020-02-13 14:53:06 +01:00
// ListenForSignals will listen for SIGINT and SIGTERM. When it receives those
// signals it will set isTerminatedAtomicInt to non-zero, which will cleanly
// shutdown the test logic.
//
// TODO refactor this to use a cancellable context.Context instead of a bool
// flag, probably as part of: https://github.com/ooni/probe-cli/issues/45
func (c *Context) ListenForSignals() {
s := make(chan os.Signal, 1)
signal.Notify(s, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-s
log.Info("caught a stop signal, shutting down cleanly")
c.Terminate()
}()
}
// MaybeListenForStdinClosed will treat any error on stdin just
// like SIGTERM if and only if
//
// os.Getenv("OONI_STDIN_EOF_IMPLIES_SIGTERM") == "true"
//
// When this feature is enabled, a collateral effect is that we swallow
// whatever is passed to us on the standard input.
//
// See https://github.com/ooni/probe-cli/pull/111 for more info
// regarding the design of this functionality.
//
// TODO refactor this to use a cancellable context.Context instead of a bool
// flag, probably as part of: https://github.com/ooni/probe-cli/issues/45
func (c *Context) MaybeListenForStdinClosed() {
if os.Getenv("OONI_STDIN_EOF_IMPLIES_SIGTERM") != "true" {
return
}
go func() {
defer c.Terminate()
defer log.Info("stdin closed, shutting down cleanly")
b := make([]byte, 1<<10)
for {
if _, err := os.Stdin.Read(b); err != nil {
return
}
}
}()
}
2018-02-12 16:45:13 +01:00
// Init the OONI manager
func (c *Context) Init(softwareName, softwareVersion string) error {
2018-03-23 12:10:14 +01:00
var err error
if err = MaybeInitializeHome(c.Home); err != nil {
2018-03-23 12:10:14 +01:00
return err
}
if c.configPath != "" {
log.Debugf("Reading config file from %s", c.configPath)
c.Config, err = config.ReadConfig(c.configPath)
2018-03-23 12:10:14 +01:00
} else {
log.Debug("Reading default config file")
2018-06-25 17:49:17 +02:00
c.Config, err = InitDefaultConfig(c.Home)
2018-03-23 12:10:14 +01:00
}
if err != nil {
return err
}
if err = c.Config.MaybeMigrate(); err != nil {
return errors.Wrap(err, "migrating config")
}
2018-03-23 12:10:14 +01:00
c.dbPath = utils.DBDir(c.Home, "main")
2018-03-23 12:10:14 +01:00
log.Debugf("Connecting to database sqlite3://%s", c.dbPath)
db, err := database.Connect(c.dbPath)
if err != nil {
return err
}
c.DB = db
tempDir, err := ioutil.TempDir("", "ooni")
if err != nil {
return errors.Wrap(err, "creating TempDir")
}
c.TempDir = tempDir
c.softwareName = softwareName
c.softwareVersion = softwareVersion
return nil
}
// NewSession creates a new ooni/probe-engine session using the
// current configuration inside the context. The caller must close
// the session when done using it, by calling sess.Close().
func (c *Context) NewSession() (*engine.Session, error) {
kvstore, err := engine.NewFileSystemKVStore(
utils.EngineDir(c.Home),
)
if err != nil {
return nil, errors.Wrap(err, "creating engine's kvstore")
}
return engine.NewSession(engine.SessionConfig{
AssetsDir: utils.AssetsDir(c.Home),
KVStore: kvstore,
Logger: enginex.Logger,
PrivacySettings: model.PrivacySettings{
IncludeASN: c.Config.Sharing.IncludeASN,
IncludeCountry: true,
IncludeIP: c.Config.Sharing.IncludeIP,
},
SoftwareName: c.softwareName,
SoftwareVersion: c.softwareVersion,
TempDir: c.TempDir,
})
2018-02-12 16:45:13 +01:00
}
2019-05-23 16:38:46 +02:00
// NewContext creates a new context instance.
2018-03-23 12:10:14 +01:00
func NewContext(configPath string, homePath string) *Context {
return &Context{
Home: homePath,
Config: &config.Config{},
configPath: configPath,
isTerminatedAtomicInt: 0,
2018-02-12 16:45:13 +01:00
}
}
// MaybeInitializeHome does the setup for a new OONI Home
func MaybeInitializeHome(home string) error {
for _, d := range utils.RequiredDirs(home) {
if _, e := os.Stat(d); e != nil {
if err := os.MkdirAll(d, 0700); err != nil {
2018-03-23 12:10:14 +01:00
return err
}
}
}
2018-03-23 12:10:14 +01:00
return nil
}
2018-06-25 17:49:17 +02:00
// InitDefaultConfig reads the config from common locations or creates it if
// missing.
func InitDefaultConfig(home string) (*config.Config, error) {
var (
err error
c *config.Config
configPath = utils.ConfigPath(home)
)
c, err = config.ReadConfig(configPath)
if err != nil {
if os.IsNotExist(err) {
log.Debugf("writing default config to %s", configPath)
var data []byte
data, err = bindata.Asset("data/default-config.json")
if err != nil {
return nil, err
}
err = ioutil.WriteFile(
configPath,
data,
0644,
)
2018-02-12 16:45:13 +01:00
if err != nil {
return nil, err
}
2018-06-25 17:49:17 +02:00
return InitDefaultConfig(home)
2018-02-12 16:45:13 +01:00
}
2018-06-25 17:49:17 +02:00
return nil, err
2018-02-12 16:45:13 +01:00
}
2018-06-25 17:49:17 +02:00
return c, nil
}