blob: c8dd5418a1d3d7f1b109cff5a3ed4f4e78863e37 [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package wificell
import (
"context"
"time"
"github.com/golang/protobuf/ptypes/empty"
"chromiumos/tast/common/network/protoutil"
"chromiumos/tast/common/shillconst"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/services/cros/wifi"
"chromiumos/tast/testing"
"chromiumos/tast/timing"
)
// WifiClient is a wrapper of ShillServiceClient to simplify gRPC calls
// e.g. handle complex streaming gRPCs, hide gRPC request/response, etc.
// Users can still access the raw gRPC with WifiClient.ShillServiceClient.
type WifiClient struct {
wifi.ShillServiceClient
}
// DiscoverBSSID discovers a service with the given properties.
func (cli *WifiClient) DiscoverBSSID(ctx context.Context, bssid, iface string, ssid []byte) error {
ctx, st := timing.Start(ctx, "DiscoverBSSID")
defer st.End()
request := &wifi.DiscoverBSSIDRequest{
Bssid: bssid,
Interface: iface,
Ssid: ssid,
}
if _, err := cli.ShillServiceClient.DiscoverBSSID(ctx, request); err != nil {
return err
}
return nil
}
// RequestRoam requests DUT to roam to the specified BSSID and waits until the DUT has roamed.
func (cli *WifiClient) RequestRoam(ctx context.Context, iface, bssid string, timeout time.Duration) error {
request := &wifi.RequestRoamRequest{
InterfaceName: iface,
Bssid: bssid,
Timeout: timeout.Nanoseconds(),
}
if _, err := cli.ShillServiceClient.RequestRoam(ctx, request); err != nil {
return err
}
return nil
}
// Reassociate triggers reassociation with the current AP and waits until it has reconnected or the timeout expires.
func (cli *WifiClient) Reassociate(ctx context.Context, iface string, timeout time.Duration) error {
_, err := cli.ShillServiceClient.Reassociate(ctx, &wifi.ReassociateRequest{
InterfaceName: iface,
Timeout: timeout.Nanoseconds(),
})
return err
}
// FlushBSS flushes BSS entries over the specified age from wpa_supplicant's cache.
func (cli *WifiClient) FlushBSS(ctx context.Context, iface string, age time.Duration) error {
req := &wifi.FlushBSSRequest{
InterfaceName: iface,
Age: age.Nanoseconds(),
}
_, err := cli.ShillServiceClient.FlushBSS(ctx, req)
return err
}
// AssureDisconnect assures that the WiFi service has disconnected within timeout.
func (cli *WifiClient) AssureDisconnect(ctx context.Context, servicePath string, timeout time.Duration) error {
req := &wifi.AssureDisconnectRequest{
ServicePath: servicePath,
Timeout: timeout.Nanoseconds(),
}
if _, err := cli.ShillServiceClient.AssureDisconnect(ctx, req); err != nil {
return err
}
return nil
}
// QueryService queries shill information of selected service.
func (cli *WifiClient) QueryService(ctx context.Context) (*wifi.QueryServiceResponse, error) {
selectedSvcResp, err := cli.ShillServiceClient.SelectedService(ctx, &empty.Empty{})
if err != nil {
return nil, errors.Wrap(err, "failed to get selected service")
}
req := &wifi.QueryServiceRequest{
Path: selectedSvcResp.ServicePath,
}
resp, err := cli.ShillServiceClient.QueryService(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "failed to get the service information")
}
return resp, nil
}
// Interface returns the WiFi interface name of the DUT.
func (cli *WifiClient) Interface(ctx context.Context) (string, error) {
netIf, err := cli.ShillServiceClient.GetInterface(ctx, &empty.Empty{})
if err != nil {
return "", errors.Wrap(err, "failed to get the WiFi interface name")
}
return netIf.Name, nil
}
// CurrentTime returns the current time on DUT.
func (cli *WifiClient) CurrentTime(ctx context.Context) (time.Time, error) {
res, err := cli.ShillServiceClient.GetCurrentTime(ctx, &empty.Empty{})
if err != nil {
return time.Time{}, errors.Wrap(err, "failed to get the current DUT time")
}
currentTime := time.Unix(res.NowSecond, res.NowNanosecond)
return currentTime, nil
}
// ShillProperty holds a shill service property with it's expected and unexpected values.
type ShillProperty struct {
Property string
ExpectedValues []interface{}
UnexpectedValues []interface{}
Method wifi.ExpectShillPropertyRequest_CheckMethod
}
// ExpectShillProperty is a wrapper for the streaming gRPC call ExpectShillProperty.
// It takes an array of ShillProperty, an array of shill properties to monitor, and
// a shill service path. It returns a function that waites for the expected property
// changes and returns the monitor results.
func (cli *WifiClient) ExpectShillProperty(ctx context.Context, objectPath string, props []*ShillProperty, monitorProps []string) (func() ([]protoutil.ShillPropertyHolder, error), error) {
var expectedProps []*wifi.ExpectShillPropertyRequest_Criterion
for _, prop := range props {
var anyOfVals []*wifi.ShillVal
for _, shillState := range prop.ExpectedValues {
state, err := protoutil.ToShillVal(shillState)
if err != nil {
return nil, errors.Wrap(err, "failed to convert property name to ShillVal")
}
anyOfVals = append(anyOfVals, state)
}
var noneOfVals []*wifi.ShillVal
for _, shillState := range prop.UnexpectedValues {
state, err := protoutil.ToShillVal(shillState)
if err != nil {
return nil, errors.Wrap(err, "failed to convert property name to ShillVal")
}
noneOfVals = append(noneOfVals, state)
}
shillPropReqCriterion := &wifi.ExpectShillPropertyRequest_Criterion{
Key: prop.Property,
AnyOf: anyOfVals,
NoneOf: noneOfVals,
Method: prop.Method,
}
expectedProps = append(expectedProps, shillPropReqCriterion)
}
req := &wifi.ExpectShillPropertyRequest{
ObjectPath: objectPath,
Props: expectedProps,
MonitorProps: monitorProps,
}
stream, err := cli.ShillServiceClient.ExpectShillProperty(ctx, req)
if err != nil {
return nil, err
}
ready, err := stream.Recv()
if err != nil || ready.Key != "" {
// Error due to expecting an empty response as ready signal.
return nil, errors.New("failed to get the ready signal")
}
// Get the expected properties and values.
waitForProperties := func() ([]protoutil.ShillPropertyHolder, error) {
for {
resp, err := stream.Recv()
if err != nil {
return nil, errors.Wrap(err, "failed to get the expected properties")
}
if resp.MonitorDone {
return protoutil.DecodeFromShillPropertyChangedSignalList(resp.Props)
}
// Now we get the matched state change in resp.
stateVal, err := protoutil.FromShillVal(resp.Val)
if err != nil {
return nil, errors.Wrap(err, "failed to convert property name to ShillVal")
}
testing.ContextLogf(ctx, "The current WiFi service %s: %v", resp.Key, stateVal)
}
}
return waitForProperties, nil
}
// EAPAuthSkipped is a wrapper for the streaming gRPC call EAPAuthSkipped.
// It returns a function that waits and verifies the EAP authentication is skipped or not in the next connection.
func (cli *WifiClient) EAPAuthSkipped(ctx context.Context) (func() (bool, error), error) {
recv, err := cli.ShillServiceClient.EAPAuthSkipped(ctx, &empty.Empty{})
if err != nil {
return nil, err
}
s, err := recv.Recv()
if err != nil {
return nil, errors.Wrap(err, "failed to receive ready signal from EAPAuthSkipped")
}
if s.Skipped {
return nil, errors.New("unexpected ready signal: got true, want false")
}
return func() (bool, error) {
resp, err := recv.Recv()
if err != nil {
return false, errors.Wrap(err, "failed to receive from EAPAuthSkipped")
}
return resp.Skipped, nil
}, nil
}
// DisconnectReason is a wrapper for the streaming gRPC call DisconnectReason.
// It returns a function that waits for the wpa_supplicant DisconnectReason
// property change, and returns the disconnection reason code.
func (cli *WifiClient) DisconnectReason(ctx context.Context) (func() (int32, error), error) {
recv, err := cli.ShillServiceClient.DisconnectReason(ctx, &empty.Empty{})
if err != nil {
return nil, err
}
ready, err := recv.Recv()
if err != nil || ready.Reason != 0 {
// Error due to expecting an empty response as ready signal.
return nil, errors.New("failed to get the ready signal")
}
return func() (int32, error) {
resp, err := recv.Recv()
if err != nil {
return 0, errors.Wrap(err, "failed to receive from DisconnectReason")
}
return resp.Reason, nil
}, nil
}
// SuspendAssertConnect suspends the DUT for wakeUpTimeout seconds through gRPC and returns the duration from resume to connect.
func (cli *WifiClient) SuspendAssertConnect(ctx context.Context, wakeUpTimeout time.Duration) (time.Duration, error) {
service, err := cli.ShillServiceClient.SelectedService(ctx, &empty.Empty{})
if err != nil {
return 0, errors.Wrap(err, "failed to get selected service")
}
resp, err := cli.ShillServiceClient.SuspendAssertConnect(ctx, &wifi.SuspendAssertConnectRequest{
WakeUpTimeout: wakeUpTimeout.Nanoseconds(),
ServicePath: service.ServicePath,
})
if err != nil {
return 0, errors.Wrap(err, "failed to suspend and assert connection")
}
return time.Duration(resp.ReconnectTime), nil
}
// Suspend suspends the DUT for wakeUpTimeout seconds through gRPC.
// This call will fail when the DUT wake up early. If the caller expects the DUT to
// wake up early, please use the Suspend gRPC to specify the detailed options.
func (cli *WifiClient) Suspend(ctx context.Context, wakeUpTimeout time.Duration) error {
req := &wifi.SuspendRequest{
WakeUpTimeout: wakeUpTimeout.Nanoseconds(),
CheckEarlyWake: true,
}
_, err := cli.ShillServiceClient.Suspend(ctx, req)
if err != nil {
return errors.Wrap(err, "failed to suspend")
}
return nil
}
// DisableMACRandomize disables MAC randomization on DUT if supported, this
// is useful for tests verifying probe requests from DUT.
// On success, a shortened context and cleanup function is returned.
func (cli *WifiClient) DisableMACRandomize(ctx context.Context) (shortenCtx context.Context, cleanupFunc func() error, retErr error) {
// If MAC randomization setting is supported, disable MAC randomization
// as we're filtering the packets with MAC address.
if supResp, err := cli.ShillServiceClient.MACRandomizeSupport(ctx, &empty.Empty{}); err != nil {
return ctx, nil, errors.Wrap(err, "failed to get if MAC randomization is supported")
} else if supResp.Supported {
resp, err := cli.ShillServiceClient.GetMACRandomize(ctx, &empty.Empty{})
if err != nil {
return ctx, nil, errors.Wrap(err, "failed to get MAC randomization setting")
}
if resp.Enabled {
ctxRestore := ctx
ctx, cancel := ctxutil.Shorten(ctx, time.Second)
_, err := cli.ShillServiceClient.SetMACRandomize(ctx, &wifi.SetMACRandomizeRequest{Enable: false})
if err != nil {
return ctx, nil, errors.Wrap(err, "failed to disable MAC randomization")
}
// Restore the setting when exiting.
restore := func() error {
cancel()
if _, err := cli.ShillServiceClient.SetMACRandomize(ctxRestore, &wifi.SetMACRandomizeRequest{Enable: true}); err != nil {
return errors.Wrap(err, "failed to re-enable MAC randomization")
}
return nil
}
return ctx, restore, nil
}
}
// Not supported or not enabled. No-op for these cases.
return ctx, func() error { return nil }, nil
}
// SetWifiEnabled persistently enables/disables Wifi via shill.
func (cli *WifiClient) SetWifiEnabled(ctx context.Context, enabled bool) error {
req := &wifi.SetWifiEnabledRequest{Enabled: enabled}
_, err := cli.ShillServiceClient.SetWifiEnabled(ctx, req)
return err
}
// TurnOffBgscan turns off the DUT's background scan, and returns a shortened ctx and a restoring function.
func (cli *WifiClient) TurnOffBgscan(ctx context.Context) (context.Context, func() error, error) {
ctxForRestoreBgConfig := ctx
ctx, cancel := ctxutil.Shorten(ctxForRestoreBgConfig, 2*time.Second)
testing.ContextLog(ctx, "Disable the DUT's background scan")
bgscanResp, err := cli.ShillServiceClient.GetBgscanConfig(ctx, &empty.Empty{})
if err != nil {
return ctxForRestoreBgConfig, nil, err
}
oldBgConfig := bgscanResp.Config
turnOffBgConfig := wifi.BgscanConfig{
Method: shillconst.DeviceBgscanMethodNone,
LongInterval: oldBgConfig.LongInterval,
ShortInterval: oldBgConfig.ShortInterval,
}
if _, err := cli.ShillServiceClient.SetBgscanConfig(ctx, &wifi.SetBgscanConfigRequest{Config: &turnOffBgConfig}); err != nil {
return ctxForRestoreBgConfig, nil, err
}
return ctx, func() error {
cancel()
testing.ContextLog(ctxForRestoreBgConfig, "Restore the DUT's background scan config: ", oldBgConfig)
_, err := cli.ShillServiceClient.SetBgscanConfig(ctxForRestoreBgConfig, &wifi.SetBgscanConfigRequest{Config: oldBgConfig})
return err
}, nil
}
// SetWakeOnWifiOption is the type of options of SetWakeOnWifi method of TestFixture.
type SetWakeOnWifiOption func(*wifi.WakeOnWifiConfig)
// WakeOnWifiFeatures returns a option for SetWakeOnWifi to modify the
// WakeOnWiFiFeaturesEnabled property.
func WakeOnWifiFeatures(features string) SetWakeOnWifiOption {
return func(config *wifi.WakeOnWifiConfig) {
config.Features = features
}
}
// WakeOnWifiNetDetectScanPeriod returns an option for SetWakeOnWifi to modify
// the NetDetectScanPeriodSeconds property.
func WakeOnWifiNetDetectScanPeriod(seconds uint32) SetWakeOnWifiOption {
return func(config *wifi.WakeOnWifiConfig) {
config.NetDetectScanPeriod = seconds
}
}
// SetWakeOnWifi sets properties related to wake on WiFi.
func (cli *WifiClient) SetWakeOnWifi(ctx context.Context, ops ...SetWakeOnWifiOption) (shortenCtx context.Context, cleanupFunc func() error, retErr error) {
resp, err := cli.ShillServiceClient.GetWakeOnWifi(ctx, &empty.Empty{})
if err != nil {
return ctx, nil, errors.Wrap(err, "failed to get WoWiFi setting")
}
origConfig := resp.Config
// Allow WakeOnWiFi.
newConfig := wifi.WakeOnWifiConfig{
Allowed: true,
Features: origConfig.Features,
NetDetectScanPeriod: origConfig.NetDetectScanPeriod,
}
for _, op := range ops {
op(&newConfig)
}
ctxRestore := ctx
ctx, cancel := ctxutil.Shorten(ctx, 3*time.Second)
req := &wifi.SetWakeOnWifiRequest{
Config: &newConfig,
}
if _, err := cli.ShillServiceClient.SetWakeOnWifi(ctx, req); err != nil {
return ctx, nil, errors.Wrap(err, "failed to set WoWiFi features")
}
restore := func() error {
cancel()
req := &wifi.SetWakeOnWifiRequest{
Config: origConfig,
}
if _, err := cli.ShillServiceClient.SetWakeOnWifi(ctxRestore, req); err != nil {
return errors.Wrapf(err, "failed to restore WoWiFi features to %v", origConfig)
}
return nil
}
return ctx, restore, nil
}
// WatchDarkResume is a wrapper for the streaming gRPC call WatchDarkResume, which
// watches dark resumes before next full resume.
// It returns a function that waits for the response of the gRPC call.
func (cli *WifiClient) WatchDarkResume(ctx context.Context) (func() (*wifi.WatchDarkResumeResponse, error), error) {
stream, err := cli.ShillServiceClient.WatchDarkResume(ctx, &empty.Empty{})
if err != nil {
return nil, err
}
s, err := stream.Recv()
if err != nil {
return nil, errors.New("failed to get the ready signal from WatchDarkResume")
}
if s.Count != 0 {
return nil, errors.Errorf("unexpected ready signal=%v", s)
}
return func() (*wifi.WatchDarkResumeResponse, error) {
resp, err := stream.Recv()
if err != nil {
return nil, errors.Wrap(err, "failed to receive from WatchDarkResume")
}
return resp, nil
}, nil
}