ooni-probe-cli/pkg/oonimkall/session.go

554 lines
18 KiB
Go
Raw Permalink Normal View History

package oonimkall
import (
"context"
"encoding/json"
"errors"
"net/url"
"runtime"
"sync"
refactor: flatten and separate (#353) * refactor(atomicx): move outside the engine package After merging probe-engine into probe-cli, my impression is that we have too much unnecessary nesting of packages in this repository. The idea of this commit and of a bunch of following commits will instead be to reduce the nesting and simplify the structure. While there, improve the documentation. * fix: always use the atomicx package For consistency, never use sync/atomic and always use ./internal/atomicx so we can just grep and make sure we're not risking to crash if we make a subtle mistake on a 32 bit platform. While there, mention in the contributing guidelines that we want to always prefer the ./internal/atomicx package over sync/atomic. * fix(atomicx): remove unnecessary constructor We don't need a constructor here. The default constructed `&Int64{}` instance is already usable and the constructor does not add anything to what we are doing, rather it just creates extra confusion. * cleanup(atomicx): we are not using Float64 Because atomicx.Float64 is unused, we can safely zap it. * cleanup(atomicx): simplify impl and improve tests We can simplify the implementation by using defer and by letting the Load() method call Add(0). We can improve tests by making many goroutines updated the atomic int64 value concurrently. * refactor(fsx): can live in the ./internal pkg Let us reduce the amount of nesting. While there, ensure that the package only exports the bare minimum, and improve the documentation of the tests, to ease reading the code. * refactor: move runtimex to ./internal * refactor: move shellx into the ./internal package While there, remove unnecessary dependency between packages. While there, specify in the contributing guidelines that one should use x/sys/execabs instead of os/exec. * refactor: move ooapi into the ./internal pkg * refactor(humanize): move to ./internal and better docs * refactor: move platform to ./internal * refactor(randx): move to ./internal * refactor(multierror): move into the ./internal pkg * refactor(kvstore): all kvstores in ./internal Rather than having part of the kvstore inside ./internal/engine/kvstore and part in ./internal/engine/kvstore.go, let us put every piece of code that is kvstore related into the ./internal/kvstore package. * fix(kvstore): always return ErrNoSuchKey on Get() error It should help to use the kvstore everywhere removing all the copies that are lingering around the tree. * sessionresolver: make KVStore mandatory Simplifies implementation. While there, use the ./internal/kvstore package rather than having our private implementation. * fix(ooapi): use the ./internal/kvstore package * fix(platform): better documentation
2021-06-04 10:34:18 +02:00
"github.com/ooni/probe-cli/v3/internal/atomicx"
"github.com/ooni/probe-cli/v3/internal/engine"
"github.com/ooni/probe-cli/v3/internal/engine/probeservices"
refactor: flatten and separate (#353) * refactor(atomicx): move outside the engine package After merging probe-engine into probe-cli, my impression is that we have too much unnecessary nesting of packages in this repository. The idea of this commit and of a bunch of following commits will instead be to reduce the nesting and simplify the structure. While there, improve the documentation. * fix: always use the atomicx package For consistency, never use sync/atomic and always use ./internal/atomicx so we can just grep and make sure we're not risking to crash if we make a subtle mistake on a 32 bit platform. While there, mention in the contributing guidelines that we want to always prefer the ./internal/atomicx package over sync/atomic. * fix(atomicx): remove unnecessary constructor We don't need a constructor here. The default constructed `&Int64{}` instance is already usable and the constructor does not add anything to what we are doing, rather it just creates extra confusion. * cleanup(atomicx): we are not using Float64 Because atomicx.Float64 is unused, we can safely zap it. * cleanup(atomicx): simplify impl and improve tests We can simplify the implementation by using defer and by letting the Load() method call Add(0). We can improve tests by making many goroutines updated the atomic int64 value concurrently. * refactor(fsx): can live in the ./internal pkg Let us reduce the amount of nesting. While there, ensure that the package only exports the bare minimum, and improve the documentation of the tests, to ease reading the code. * refactor: move runtimex to ./internal * refactor: move shellx into the ./internal package While there, remove unnecessary dependency between packages. While there, specify in the contributing guidelines that one should use x/sys/execabs instead of os/exec. * refactor: move ooapi into the ./internal pkg * refactor(humanize): move to ./internal and better docs * refactor: move platform to ./internal * refactor(randx): move to ./internal * refactor(multierror): move into the ./internal pkg * refactor(kvstore): all kvstores in ./internal Rather than having part of the kvstore inside ./internal/engine/kvstore and part in ./internal/engine/kvstore.go, let us put every piece of code that is kvstore related into the ./internal/kvstore package. * fix(kvstore): always return ErrNoSuchKey on Get() error It should help to use the kvstore everywhere removing all the copies that are lingering around the tree. * sessionresolver: make KVStore mandatory Simplifies implementation. While there, use the ./internal/kvstore package rather than having our private implementation. * fix(ooapi): use the ./internal/kvstore package * fix(platform): better documentation
2021-06-04 10:34:18 +02:00
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/legacy/assetsdir"
"github.com/ooni/probe-cli/v3/internal/model"
refactor: flatten and separate (#353) * refactor(atomicx): move outside the engine package After merging probe-engine into probe-cli, my impression is that we have too much unnecessary nesting of packages in this repository. The idea of this commit and of a bunch of following commits will instead be to reduce the nesting and simplify the structure. While there, improve the documentation. * fix: always use the atomicx package For consistency, never use sync/atomic and always use ./internal/atomicx so we can just grep and make sure we're not risking to crash if we make a subtle mistake on a 32 bit platform. While there, mention in the contributing guidelines that we want to always prefer the ./internal/atomicx package over sync/atomic. * fix(atomicx): remove unnecessary constructor We don't need a constructor here. The default constructed `&Int64{}` instance is already usable and the constructor does not add anything to what we are doing, rather it just creates extra confusion. * cleanup(atomicx): we are not using Float64 Because atomicx.Float64 is unused, we can safely zap it. * cleanup(atomicx): simplify impl and improve tests We can simplify the implementation by using defer and by letting the Load() method call Add(0). We can improve tests by making many goroutines updated the atomic int64 value concurrently. * refactor(fsx): can live in the ./internal pkg Let us reduce the amount of nesting. While there, ensure that the package only exports the bare minimum, and improve the documentation of the tests, to ease reading the code. * refactor: move runtimex to ./internal * refactor: move shellx into the ./internal package While there, remove unnecessary dependency between packages. While there, specify in the contributing guidelines that one should use x/sys/execabs instead of os/exec. * refactor: move ooapi into the ./internal pkg * refactor(humanize): move to ./internal and better docs * refactor: move platform to ./internal * refactor(randx): move to ./internal * refactor(multierror): move into the ./internal pkg * refactor(kvstore): all kvstores in ./internal Rather than having part of the kvstore inside ./internal/engine/kvstore and part in ./internal/engine/kvstore.go, let us put every piece of code that is kvstore related into the ./internal/kvstore package. * fix(kvstore): always return ErrNoSuchKey on Get() error It should help to use the kvstore everywhere removing all the copies that are lingering around the tree. * sessionresolver: make KVStore mandatory Simplifies implementation. While there, use the ./internal/kvstore package rather than having our private implementation. * fix(ooapi): use the ./internal/kvstore package * fix(platform): better documentation
2021-06-04 10:34:18 +02:00
"github.com/ooni/probe-cli/v3/internal/runtimex"
)
// AtomicInt64 allows us to export atomicx.Int64 variables to
// mobile libraries so we can use them in testing.
type AtomicInt64 struct {
*atomicx.Int64
}
// These two variables contain metrics pertaining to the number
// of Sessions and Contexts that are currently being used.
var (
refactor: flatten and separate (#353) * refactor(atomicx): move outside the engine package After merging probe-engine into probe-cli, my impression is that we have too much unnecessary nesting of packages in this repository. The idea of this commit and of a bunch of following commits will instead be to reduce the nesting and simplify the structure. While there, improve the documentation. * fix: always use the atomicx package For consistency, never use sync/atomic and always use ./internal/atomicx so we can just grep and make sure we're not risking to crash if we make a subtle mistake on a 32 bit platform. While there, mention in the contributing guidelines that we want to always prefer the ./internal/atomicx package over sync/atomic. * fix(atomicx): remove unnecessary constructor We don't need a constructor here. The default constructed `&Int64{}` instance is already usable and the constructor does not add anything to what we are doing, rather it just creates extra confusion. * cleanup(atomicx): we are not using Float64 Because atomicx.Float64 is unused, we can safely zap it. * cleanup(atomicx): simplify impl and improve tests We can simplify the implementation by using defer and by letting the Load() method call Add(0). We can improve tests by making many goroutines updated the atomic int64 value concurrently. * refactor(fsx): can live in the ./internal pkg Let us reduce the amount of nesting. While there, ensure that the package only exports the bare minimum, and improve the documentation of the tests, to ease reading the code. * refactor: move runtimex to ./internal * refactor: move shellx into the ./internal package While there, remove unnecessary dependency between packages. While there, specify in the contributing guidelines that one should use x/sys/execabs instead of os/exec. * refactor: move ooapi into the ./internal pkg * refactor(humanize): move to ./internal and better docs * refactor: move platform to ./internal * refactor(randx): move to ./internal * refactor(multierror): move into the ./internal pkg * refactor(kvstore): all kvstores in ./internal Rather than having part of the kvstore inside ./internal/engine/kvstore and part in ./internal/engine/kvstore.go, let us put every piece of code that is kvstore related into the ./internal/kvstore package. * fix(kvstore): always return ErrNoSuchKey on Get() error It should help to use the kvstore everywhere removing all the copies that are lingering around the tree. * sessionresolver: make KVStore mandatory Simplifies implementation. While there, use the ./internal/kvstore package rather than having our private implementation. * fix(ooapi): use the ./internal/kvstore package * fix(platform): better documentation
2021-06-04 10:34:18 +02:00
ActiveSessions = &AtomicInt64{&atomicx.Int64{}}
ActiveContexts = &AtomicInt64{&atomicx.Int64{}}
)
// Logger is the logger used by a Session. You should implement a class
// compatible with this interface in Java/ObjC and then save a reference
// to this instance in the SessionConfig object. All log messages that
// the Session will generate will be routed to this Logger.
type Logger interface {
// Debug handles debug messages.
Debug(msg string)
// Info handles informational messages.
Info(msg string)
// Warn handles warning messages.
Warn(msg string)
}
// ExperimentCallbacks contains experiment callbacks.
type ExperimentCallbacks interface {
// OnProgress provides information about an experiment progress.
OnProgress(percentage float64, message string)
}
// SessionConfig contains configuration for a Session. You should
// fill all the mandatory fields and could also optionally fill some of
// the optional fields. Then pass this struct to NewSession.
type SessionConfig struct {
// AssetsDir is the mandatory directory where to store assets
// required by a Session, e.g. MaxMind DB files.
//
// This field is currently deprecated and unused. We will
// remove it when we'll bump the major number.
AssetsDir string
// Logger is the optional logger that will receive all the
// log messages generated by a Session. If this field is nil
// then the session will not emit any log message.
Logger Logger
// Proxy allows you to optionally force a specific proxy
// rather than using no proxy (the default).
//
// Use `psiphon:///` to force using Psiphon with the
// embedded configuration file. Not all builds have
// an embedded configuration file, but OONI builds have
// such a file, so they can use this functionality.
//
// Use `socks5://10.0.0.1:9050/` to connect to a SOCKS5
// proxy running on 10.0.0.1:9050. This could be, for
// example, a suitably configured `tor` instance.
Proxy string
// ProbeServicesURL allows you to optionally force the
// usage of an alternative probe service instance. This setting
// should only be used for implementing integration tests.
ProbeServicesURL string
// SoftwareName is the mandatory name of the application
// that will be using the new Session.
SoftwareName string
// SoftwareVersion is the mandatory version of the application
// that will be using the new Session.
SoftwareVersion string
// StateDir is the mandatory directory where to store state
// information required by a Session.
StateDir string
// TempDir is the mandatory directory where the Session shall
// store temporary files. Among other tasks, Session.Close will
// remove any temporary file created within this Session.
TempDir string
// TunnelDir is the directory where the Session shall store
// persistent data regarding circumvention tunnels. This directory
// is mandatory if you want to use tunnels.
TunnelDir string
// Verbose is optional. If there is a non-null Logger and this
// field is true, then the Logger will also receive Debug messages,
// otherwise it will not receive such messages.
Verbose bool
}
// Session contains shared state for running experiments and/or other
// OONI related task (e.g. geolocation). Note that the Session isn't
// mean to be a long living object. The workflow is to create a Session,
// do the operations you need to do with it now, then make sure it is
// not referenced by other variables, so the Go GC can finalize it. This
// is what you would normally done with Java/ObjC.
type Session struct {
// Hooks for testing (should not appear in Java/ObjC, because they
// cannot be automatically transformed to Java/ObjC code.)
TestingCheckInBeforeNewProbeServicesClient func(ctx *Context)
TestingCheckInBeforeCheckIn func(ctx *Context)
cl []context.CancelFunc
mtx sync.Mutex
submitter *probeservices.Submitter
sessp *engine.Session
}
// NewSession is like NewSessionWithContext but without context. This
// factory is deprecated and will be removed when we bump the major
// version number of ooni/probe-cli.
func NewSession(config *SessionConfig) (*Session, error) {
return newSessionWithContext(context.Background(), config)
}
// NewSessionWithContext creates a new session. You should use a session for running
// a set of operations in a relatively short time frame. You SHOULD NOT create
// a single session and keep it all alive for the whole app lifecyle, since
// the Session code is not specifically designed for this use case.
func NewSessionWithContext(ctx *Context, config *SessionConfig) (*Session, error) {
return newSessionWithContext(ctx.ctx, config)
}
// newSessionWithContext implements NewSessionWithContext.
func newSessionWithContext(ctx context.Context, config *SessionConfig) (*Session, error) {
refactor: flatten and separate (#353) * refactor(atomicx): move outside the engine package After merging probe-engine into probe-cli, my impression is that we have too much unnecessary nesting of packages in this repository. The idea of this commit and of a bunch of following commits will instead be to reduce the nesting and simplify the structure. While there, improve the documentation. * fix: always use the atomicx package For consistency, never use sync/atomic and always use ./internal/atomicx so we can just grep and make sure we're not risking to crash if we make a subtle mistake on a 32 bit platform. While there, mention in the contributing guidelines that we want to always prefer the ./internal/atomicx package over sync/atomic. * fix(atomicx): remove unnecessary constructor We don't need a constructor here. The default constructed `&Int64{}` instance is already usable and the constructor does not add anything to what we are doing, rather it just creates extra confusion. * cleanup(atomicx): we are not using Float64 Because atomicx.Float64 is unused, we can safely zap it. * cleanup(atomicx): simplify impl and improve tests We can simplify the implementation by using defer and by letting the Load() method call Add(0). We can improve tests by making many goroutines updated the atomic int64 value concurrently. * refactor(fsx): can live in the ./internal pkg Let us reduce the amount of nesting. While there, ensure that the package only exports the bare minimum, and improve the documentation of the tests, to ease reading the code. * refactor: move runtimex to ./internal * refactor: move shellx into the ./internal package While there, remove unnecessary dependency between packages. While there, specify in the contributing guidelines that one should use x/sys/execabs instead of os/exec. * refactor: move ooapi into the ./internal pkg * refactor(humanize): move to ./internal and better docs * refactor: move platform to ./internal * refactor(randx): move to ./internal * refactor(multierror): move into the ./internal pkg * refactor(kvstore): all kvstores in ./internal Rather than having part of the kvstore inside ./internal/engine/kvstore and part in ./internal/engine/kvstore.go, let us put every piece of code that is kvstore related into the ./internal/kvstore package. * fix(kvstore): always return ErrNoSuchKey on Get() error It should help to use the kvstore everywhere removing all the copies that are lingering around the tree. * sessionresolver: make KVStore mandatory Simplifies implementation. While there, use the ./internal/kvstore package rather than having our private implementation. * fix(ooapi): use the ./internal/kvstore package * fix(platform): better documentation
2021-06-04 10:34:18 +02:00
kvstore, err := kvstore.NewFS(config.StateDir)
if err != nil {
return nil, err
}
// We cleanup the assets files used by versions of ooniprobe
// older than v3.9.0, where we started embedding the assets
// into the binary and use that directly. This cleanup doesn't
// remove the whole directory but only known files inside it
// and then the directory itself, if empty. We explicitly discard
// the return value as it does not matter to us here.
_, _ = assetsdir.Cleanup(config.AssetsDir)
var availableps []model.OOAPIService
if config.ProbeServicesURL != "" {
availableps = append(availableps, model.OOAPIService{
Address: config.ProbeServicesURL,
Type: "https",
})
}
// TODO(bassosimone): write tests for this functionality.
// See https://github.com/ooni/probe/issues/1465.
var proxyURL *url.URL
if config.Proxy != "" {
var err error
proxyURL, err = url.Parse(config.Proxy)
if err != nil {
return nil, err
}
}
engineConfig := engine.SessionConfig{
AvailableProbeServices: availableps,
KVStore: kvstore,
Logger: newLogger(config.Logger, config.Verbose),
ProxyURL: proxyURL,
SoftwareName: config.SoftwareName,
SoftwareVersion: config.SoftwareVersion,
TempDir: config.TempDir,
TunnelDir: config.TunnelDir,
}
sessp, err := engine.NewSession(ctx, engineConfig)
if err != nil {
return nil, err
}
sess := &Session{sessp: sessp}
// We use finalizers to reduce the burden of managing the
// session from languages with a garbage collector.
runtime.SetFinalizer(sess, sessionFinalizer)
ActiveSessions.Add(1)
return sess, nil
}
// sessionFinalizer finalizes a Session. While in general in Go code using a
// finalizer is probably unclean, it seems that using a finalizer when binding
// with Java/ObjC code is actually useful to simplify the apps.
func sessionFinalizer(sess *Session) {
for _, fn := range sess.cl {
fn()
}
sess.sessp.Close() // ignore return value
ActiveSessions.Add(-1)
}
// Context is the context of an operation. You use this context
// to cancel a long running operation by calling Cancel(). Because
// you create a Context from a Session and because the Session is
// keeping track of the Context instances it owns, you do don't
// need to call the Cancel method when you're done.
type Context struct {
cancel context.CancelFunc
ctx context.Context
}
// Cancel cancels pending operations using this context. This method
// is idempotent. Calling it more than once is fine. The first invocation
// cancels the context. Subsequent invocations are no-operations.
func (ctx *Context) Cancel() {
ctx.cancel()
}
// NewContext creates an new interruptible Context.
func (sess *Session) NewContext() *Context {
return sess.NewContextWithTimeout(-1)
}
// NewContextWithTimeout creates an new interruptible Context that will automatically
// cancel itself after the given timeout. Setting a zero or negative timeout implies
// there is no actual timeout configured for the Context, making this invocation
// equivalent to calling NewContext().
func (sess *Session) NewContextWithTimeout(timeout int64) *Context {
sess.mtx.Lock()
defer sess.mtx.Unlock()
ctx, origcancel := newContext(timeout)
ActiveContexts.Add(1)
var once sync.Once
cancel := func() {
once.Do(func() {
ActiveContexts.Add(-1)
origcancel()
})
}
sess.cl = append(sess.cl, cancel)
return &Context{cancel: cancel, ctx: ctx}
}
// GeolocateResults contains the results of session.Geolocate.
type GeolocateResults struct {
// ASN is the autonomous system number.
ASN string
// Country is the country code.
Country string
// IP is the IP address.
IP string
// Org is the commercial name of the ASN.
Org string
}
// MaybeUpdateResources is a legacy stub. It does nothing. We will
// remove it when we're ready to bump the major number.
func (sess *Session) MaybeUpdateResources(ctx *Context) error {
return nil
}
// Geolocate performs a geolocate operation and returns the results.
//
// This function locks the session until it's done. That is, no other operation
// can be performed as long as this function is pending.
func (sess *Session) Geolocate(ctx *Context) (*GeolocateResults, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
info, err := sess.sessp.LookupLocationContext(ctx.ctx)
if err != nil {
return nil, err
}
return &GeolocateResults{
ASN: info.ASNString(),
Country: info.CountryCode,
IP: info.ProbeIP,
Org: info.NetworkName,
}, nil
}
// SubmitMeasurementResults contains the results of a single measurement submission
// to the OONI backends using the OONI collector API.
type SubmitMeasurementResults struct {
// UpdateMeasurement is the measurement with updated report ID.
UpdatedMeasurement string
// UpdatedReportID is the report ID used for the measurement.
UpdatedReportID string
}
// Submit submits the given measurement and returns the results.
//
// This function locks the session until it's done. That is, no other operation
// can be performed as long as this function is pending.
func (sess *Session) Submit(ctx *Context, measurement string) (*SubmitMeasurementResults, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
if sess.submitter == nil {
psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx)
if err != nil {
return nil, err
}
sess.submitter = probeservices.NewSubmitter(psc, sess.sessp.Logger())
}
var mm model.Measurement
if err := json.Unmarshal([]byte(measurement), &mm); err != nil {
return nil, err
}
if err := sess.submitter.Submit(ctx.ctx, &mm); err != nil {
return nil, err
}
data, err := json.Marshal(mm)
runtimex.PanicOnError(err, "json.Marshal should not fail here")
return &SubmitMeasurementResults{
UpdatedMeasurement: string(data),
UpdatedReportID: mm.ReportID,
}, nil
}
// CheckInConfigWebConnectivity contains WebConnectivity
// configuration for the check-in API.
type CheckInConfigWebConnectivity struct {
// CategoryCodes contains zero or more category codes (e.g. "HUMR").
CategoryCodes []string
}
// Add adds a category code to ckw.CategoryCode. This method allows you to
// edit ckw.CategoryCodes, which is inaccessible from Java/ObjC.
func (ckw *CheckInConfigWebConnectivity) Add(cat string) {
ckw.CategoryCodes = append(ckw.CategoryCodes, cat)
}
func (ckw *CheckInConfigWebConnectivity) toModel() model.OOAPICheckInConfigWebConnectivity {
return model.OOAPICheckInConfigWebConnectivity{
CategoryCodes: ckw.CategoryCodes,
}
}
// CheckInConfig contains configuration for the check-in API.
type CheckInConfig struct {
// Charging indicates whether the phone is charging.
Charging bool
// OnWiFi indicates whether the phone is using the Wi-Fi.
OnWiFi bool
// Platform is the mobile platform (e.g. "android")
Platform string
// RunType indicates whether this is an automated (model.RunTypeTimed) run
// or otherwise a manual run initiated by the user.
RunType string
// SoftwareName is the name of the application.
SoftwareName string
// SoftwareVersion is the version of the application.
SoftwareVersion string
// WebConnectivity contains configuration items specific of
// the WebConnectivity experiment.
WebConnectivity *CheckInConfigWebConnectivity
}
// CheckInInfoWebConnectivity contains the WebConnectivity
// specific results of the check-in API call.
type CheckInInfoWebConnectivity struct {
// ReportID is the report ID we should be using.
ReportID string
// URLs contains the list of URLs to measure.
URLs []model.OOAPIURLInfo
}
// URLInfo contains info on a specific URL to measure.
type URLInfo struct {
// CategoryCode is the URL's category code (e.g. "HUMR").
CategoryCode string
// CountryCode is the test list from which this URL
// comes from (e.g. "IT", "FR").
CountryCode string
// URL is the URL itself.
URL string
}
// Size returns the number of URLs included into the result.
func (ckw *CheckInInfoWebConnectivity) Size() int64 {
return int64(len(ckw.URLs))
}
// At returns the URLInfo at index idx. Note that this function will
// return nil/null if the index is out of bounds.
func (ckw *CheckInInfoWebConnectivity) At(idx int64) *URLInfo {
if idx < 0 || int(idx) >= len(ckw.URLs) {
return nil
}
w := ckw.URLs[idx]
return &URLInfo{
CategoryCode: w.CategoryCode,
CountryCode: w.CountryCode,
URL: w.URL,
}
}
func newCheckInInfoWebConnectivity(ckw *model.OOAPICheckInInfoWebConnectivity) *CheckInInfoWebConnectivity {
if ckw == nil {
return nil
}
return &CheckInInfoWebConnectivity{
ReportID: ckw.ReportID,
URLs: ckw.URLs,
}
}
// CheckInInfo contains the result of the check-in API.
type CheckInInfo struct {
// WebConnectivity contains results that are specific to
// the WebConnectivity experiment. This field MAY be null
// if the server's response did not contain any info.
WebConnectivity *CheckInInfoWebConnectivity
}
// CheckIn calls the check-in API. Both ctx and config MUST NOT be nil. This
// function will fail if config is missing required settings. The return value
// is either an error or a valid CheckInInfo instance. Beware that the returned
// object MAY still contain nil fields depending on the server's response.
//
// This function locks the session until it's done. That is, no other operation
// can be performed as long as this function is pending.
func (sess *Session) CheckIn(ctx *Context, config *CheckInConfig) (*CheckInInfo, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
if config.WebConnectivity == nil {
return nil, errors.New("oonimkall: missing webconnectivity config")
}
info, err := sess.sessp.LookupLocationContext(ctx.ctx)
if err != nil {
return nil, err
}
if sess.TestingCheckInBeforeNewProbeServicesClient != nil {
sess.TestingCheckInBeforeNewProbeServicesClient(ctx) // for testing
}
psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx)
if err != nil {
return nil, err
}
if sess.TestingCheckInBeforeCheckIn != nil {
sess.TestingCheckInBeforeCheckIn(ctx) // for testing
}
cfg := model.OOAPICheckInConfig{
Charging: config.Charging,
OnWiFi: config.OnWiFi,
Platform: config.Platform,
ProbeASN: info.ASNString(),
ProbeCC: info.CountryCode,
RunType: model.RunType(config.RunType),
SoftwareVersion: config.SoftwareVersion,
WebConnectivity: config.WebConnectivity.toModel(),
}
result, err := psc.CheckIn(ctx.ctx, cfg)
if err != nil {
return nil, err
}
return &CheckInInfo{
WebConnectivity: newCheckInInfoWebConnectivity(result.WebConnectivity),
}, nil
}
// URLListConfig contains configuration for fetching the URL list.
type URLListConfig struct {
Categories []string // Categories to query for (empty means all)
CountryCode string // CountryCode is the optional country code
Limit int64 // Max number of URLs (<= 0 means no limit)
}
// URLListResult contains the URLs returned from the FetchURL API
type URLListResult struct {
Results []model.OOAPIURLInfo
}
// AddCategory adds category code to the array in URLListConfig
func (ckw *URLListConfig) AddCategory(cat string) {
ckw.Categories = append(ckw.Categories, cat)
}
// At gets the URLInfo at position idx from CheckInInfoWebConnectivity.URLs. It returns
// nil if you are using an outs of bound index.
func (ckw *URLListResult) At(idx int64) *URLInfo {
if idx < 0 || int(idx) >= len(ckw.Results) {
return nil
}
w := ckw.Results[idx]
return &URLInfo{
CategoryCode: w.CategoryCode,
CountryCode: w.CountryCode,
URL: w.URL,
}
}
// Size returns the number of URLs.
func (ckw *URLListResult) Size() int64 {
return int64(len(ckw.Results))
}
// FetchURLList fetches the list of URLs to test
func (sess *Session) FetchURLList(ctx *Context, config *URLListConfig) (*URLListResult, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx)
if err != nil {
return nil, err
}
if config.CountryCode == "" {
config.CountryCode = "XX"
info, err := sess.sessp.LookupLocationContext(ctx.ctx)
// TODO(bassosimone): this piece of code feels wrong to me. We don't
// want to continue if we cannot discover the country.
if err == nil && info != nil {
config.CountryCode = info.CountryCode
}
}
cfg := model.OOAPIURLListConfig{
Categories: config.Categories,
CountryCode: config.CountryCode,
Limit: config.Limit,
}
result, err := psc.FetchURLList(ctx.ctx, cfg)
if err != nil {
return nil, err
}
return &URLListResult{
Results: result,
}, nil
}