| // Copyright 2020 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 wifi |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/hex" |
| "fmt" |
| "io/ioutil" |
| "net" |
| "os" |
| "reflect" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/godbus/dbus" |
| "github.com/golang/protobuf/ptypes/empty" |
| "google.golang.org/grpc" |
| |
| "chromiumos/tast/common/network/ping" |
| "chromiumos/tast/common/network/protoutil" |
| "chromiumos/tast/common/network/wpacli" |
| "chromiumos/tast/common/shillconst" |
| "chromiumos/tast/common/testexec" |
| "chromiumos/tast/ctxutil" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/network" |
| "chromiumos/tast/local/network/cmd" |
| network_iface "chromiumos/tast/local/network/iface" |
| local_ping "chromiumos/tast/local/network/ping" |
| "chromiumos/tast/local/power" |
| "chromiumos/tast/local/shill" |
| "chromiumos/tast/local/upstart" |
| "chromiumos/tast/local/wpasupplicant" |
| "chromiumos/tast/services/cros/wifi" |
| "chromiumos/tast/testing" |
| "chromiumos/tast/timing" |
| ) |
| |
| // reserveForReturn reserves a second in order to let the gRPC to be able to return the details of the errors (mainly for timeout errors). |
| func reserveForReturn(ctx context.Context) (context.Context, func()) { |
| return ctxutil.Shorten(ctx, time.Second) |
| } |
| |
| func init() { |
| testing.AddService(&testing.Service{ |
| Register: func(srv *grpc.Server, s *testing.ServiceState) { |
| wifi.RegisterShillServiceServer(srv, &ShillService{s: s}) |
| }, |
| }) |
| } |
| |
| // wifiTestProfileName is the profile we create and use for WiFi tests. |
| const wifiTestProfileName = "test" |
| |
| // ShillService implements tast.cros.wifi.Shill gRPC service. |
| type ShillService struct { |
| s *testing.ServiceState |
| } |
| |
| // cleanUpUsbmon cleans up usbmon files and processes, if any. |
| // BT tests might left those after exiting and caused some problems. As a |
| // workaround, let's force cleanup here as well in case the previous tasks may |
| // be killed before the cleanup is finished. See: b/194536867#comment15. |
| func (s *ShillService) cleanUpUsbmon(ctx context.Context) { |
| // Ignore the error as the process might not exist. |
| testexec.CommandContext(ctx, "pkill", "tcpdump").Run() |
| |
| // USBMON_DIR_LOG_PATH in Tauto (bluetooth_adapter_tests.py). |
| const usbmonDirLogPath = "/var/log/usbmon" |
| if err := os.RemoveAll(usbmonDirLogPath); err != nil { |
| // Only log here because this is just something nice to be done and |
| // does not really block WiFi tests. |
| testing.ContextLogf(ctx, "Error removing %s: %v", usbmonDirLogPath, err) |
| } |
| } |
| |
| // InitDUT properly initializes the DUT for WiFi tests. |
| func (s *ShillService) InitDUT(ctx context.Context, req *wifi.InitDUTRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| // Clean up usbmon before any setup in case it can cause any misbehaviors. |
| s.cleanUpUsbmon(ctx) |
| |
| if !req.WithUi { |
| // Stop UI to avoid interference from UI (e.g. request scan). |
| if err := upstart.StopJob(ctx, "ui"); err != nil { |
| return nil, errors.Wrap(err, "failed to stop ui") |
| } |
| } else { |
| if err := upstart.EnsureJobRunning(ctx, "ui"); err != nil { |
| return nil, errors.Wrap(err, "failed to start ui") |
| } |
| } |
| |
| m, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := dev.Enable(ctx); err != nil { |
| return nil, errors.Wrap(err, "failed to enable WiFi device") |
| } |
| if err := s.reinitTestState(ctx, m); err != nil { |
| return nil, err |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // reinitTestState prepare the environment for WiFi testcase. |
| func (s *ShillService) reinitTestState(ctx context.Context, m *shill.Manager) error { |
| // Clean old profiles. |
| if err := s.cleanProfiles(ctx, m); err != nil { |
| return errors.Wrap(err, "cleanProfiles failed") |
| } |
| if err := s.removeWifiEntries(ctx, m); err != nil { |
| return errors.Wrap(err, "removeWifiEntries failed") |
| } |
| // Try to create the test profile. |
| if _, err := m.CreateProfile(ctx, wifiTestProfileName); err != nil { |
| return errors.Wrap(err, "failed to create the test profile") |
| } |
| // Push the test profile. |
| if _, err := m.PushProfile(ctx, wifiTestProfileName); err != nil { |
| return errors.Wrap(err, "failed to push the test profile") |
| } |
| // Clean up wpa_supplicant BSSID_IGNORE in case some BSSID cannot be scanned. |
| // See https://crrev.com/c/219844. |
| if err := wpacli.NewRunner(&cmd.LocalCmdRunner{}).ClearBSSIDIgnore(ctx); err != nil { |
| return errors.Wrap(err, "failed to clear wpa_supplicant BSSID_IGNORE") |
| } |
| if err := m.SetProperty(ctx, shillconst.ManagerPropertyScanAllowRoam, true); err != nil { |
| return errors.Wrap(err, "failed to set WiFi.ScanAllowRoam property") |
| } |
| return nil |
| } |
| |
| // ReinitTestState cleans and sets up the environment for a single WiFi testcase. |
| func (s *ShillService) ReinitTestState(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create Manager object") |
| } |
| if err := s.reinitTestState(ctx, m); err != nil { |
| return nil, err |
| } |
| |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| servicePath, err := s.selectedService(ctx) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| if servicePath != "/" { |
| return errors.Errorf("unexpected service path, got %q, want \"/\"", servicePath) |
| } |
| return nil |
| }, &testing.PollOptions{ |
| Timeout: time.Second * 5, |
| Interval: time.Millisecond * 200, |
| }); err != nil { |
| return nil, err |
| } |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // TearDown reverts the settings made by InitDUT and InitTestState. |
| func (s *ShillService) TearDown(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create Manager object") |
| } |
| |
| var retErr error |
| if err := s.cleanProfiles(ctx, m); err != nil { |
| retErr = errors.Wrapf(retErr, "cleanProfiles failed: %s", err) |
| } |
| if err := s.removeWifiEntries(ctx, m); err != nil { |
| retErr = errors.Wrapf(retErr, "removeWifiEntries failed: %s", err) |
| } |
| if err := upstart.EnsureJobRunning(ctx, "ui"); err != nil { |
| testing.ContextLog(ctx, "Failed to start ui: ", err) |
| } |
| if retErr != nil { |
| return nil, retErr |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| func (s *ShillService) discoverService(ctx context.Context, m *shill.Manager, props map[string]interface{}) (*shill.Service, error) { |
| ctx, st := timing.Start(ctx, "discoverService") |
| defer st.End() |
| testing.ContextLog(ctx, "Discovering a WiFi service with properties: ", props) |
| |
| visibleProps := make(map[string]interface{}) |
| for k, v := range props { |
| visibleProps[k] = v |
| } |
| visibleProps[shillconst.ServicePropertyVisible] = true |
| |
| var service *shill.Service |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| var err error |
| service, err = m.FindMatchingService(ctx, visibleProps) |
| if err == nil { |
| return nil |
| } |
| // Scan WiFi AP again if the expected AP is not found. |
| if err2 := m.RequestScan(ctx, shill.TechnologyWifi); err2 != nil { |
| return testing.PollBreak(errors.Wrap(err2, "failed to request active scan")) |
| } |
| return err |
| }, &testing.PollOptions{ |
| Timeout: 15 * time.Second, |
| Interval: 200 * time.Millisecond, // RequestScan is spammy, but shill handles that for us. |
| }); err != nil { |
| return nil, err |
| } |
| return service, nil |
| } |
| |
| // connectService connects to a WiFi service and wait until conntected state. |
| // The time used for association and configuration is returned when success. |
| func (s *ShillService) connectService(ctx context.Context, service *shill.Service) (assocTime, configTime time.Duration, retErr error) { |
| ctx, st := timing.Start(ctx, "connectService") |
| defer st.End() |
| testing.ContextLog(ctx, "Connecting to the service: ", service) |
| |
| start := time.Now() |
| |
| // Spawn watcher before connect. |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return 0, 0, errors.Wrap(err, "failed to create watcher") |
| } |
| defer pw.Close(ctx) |
| |
| if err := service.Connect(ctx); err != nil { |
| return 0, 0, errors.Wrap(err, "failed to connect to service") |
| } |
| |
| // Wait until connection established. |
| // For debug and profile purpose, it is separated into association |
| // and configuration stages. |
| |
| // Prepare the state list for ExpectIn. |
| var connectedStates []interface{} |
| for _, s := range shillconst.ServiceConnectedStates { |
| connectedStates = append(connectedStates, s) |
| } |
| associatedStates := append(connectedStates, shillconst.ServiceStateConfiguration) |
| |
| testing.ContextLog(ctx, "Associating with ", service) |
| assocCtx, cancel := context.WithTimeout(ctx, 15*time.Second) |
| defer cancel() |
| state, err := pw.ExpectIn(assocCtx, shillconst.ServicePropertyState, associatedStates) |
| if err != nil { |
| return 0, 0, errors.Wrap(err, "failed to associate") |
| } |
| assocTime = time.Since(start) |
| start = time.Now() |
| |
| testing.ContextLog(ctx, "Configuring ", service) |
| if state == shillconst.ServiceStateConfiguration { |
| // We're not yet in connectedStates, wait until connected. |
| configCtx, cancel := context.WithTimeout(ctx, 15*time.Second) |
| defer cancel() |
| if _, err := pw.ExpectIn(configCtx, shillconst.ServicePropertyState, connectedStates); err != nil { |
| return 0, 0, errors.Wrap(err, "failed to configure") |
| } |
| } |
| configTime = time.Since(start) |
| |
| return assocTime, configTime, nil |
| } |
| |
| // waitForBSSID waits for a BSS with specific SSID and BSSID on the |
| // given iface. Returns error if it fails to wait for the BSS before |
| // ctx.Done. |
| func (s *ShillService) waitForBSSID(ctx context.Context, iface *wpasupplicant.Interface, targetSSID, targetBSSID []byte) error { |
| // Create a watcher for BSSAdded signal. |
| sw, err := iface.DBusObject().CreateWatcher(ctx, wpasupplicant.DBusInterfaceSignalBSSAdded) |
| if err != nil { |
| return errors.Wrap(err, "failed to create a signal watcher") |
| } |
| defer sw.Close(ctx) |
| |
| // Check if the BSS is already in the table. |
| bsses, err := iface.BSSs(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to get BSSs") |
| } |
| |
| for _, bss := range bsses { |
| ssid, err := bss.SSID(ctx) |
| if err != nil { |
| testing.ContextLog(ctx, "Failed to get SSID for bss: ", err) |
| } else if bytes.Equal(ssid, targetSSID) { |
| bssid, err := bss.BSSID(ctx) |
| if err != nil { |
| testing.ContextLog(ctx, "Failed to get BSSID for bss: ", err) |
| } else if bytes.Equal(bssid, targetBSSID) { |
| return nil |
| } |
| } |
| } |
| |
| checkSig := func(sig *dbus.Signal) (bool, error) { |
| bss, err := iface.ParseBSSAddedSignal(ctx, sig) |
| if err != nil { |
| return false, errors.Wrap(err, "failed to parse the BSSAdded signal") |
| } |
| if !bytes.Equal(bss.SSID, targetSSID) { |
| return false, nil |
| } |
| if !bytes.Equal(bss.BSSID, targetBSSID) { |
| return false, nil |
| } |
| return true, nil |
| } |
| |
| for { |
| select { |
| case <-ctx.Done(): |
| return errors.Wrapf(ctx.Err(), "failed to wait for BSSID %q", targetBSSID) |
| case sig := <-sw.Signals: |
| match, err := checkSig(sig) |
| if err != nil { |
| return err |
| } else if match { |
| return nil |
| } |
| } |
| } |
| } |
| |
| // DiscoverBSSID discovers the specified BSSID by running a scan. |
| // This is the implementation of wifi.ShillService/DiscoverBSSID gRPC. |
| // Note that WiFi.ScanAllowRoam is disabled so that we ensure that |
| // the device doesn't roam while attempting to discover a BSSID. |
| func (s *ShillService) DiscoverBSSID(ctx context.Context, request *wifi.DiscoverBSSIDRequest) (*wifi.DiscoverBSSIDResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| supplicant, err := wpasupplicant.NewSupplicant(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to connect to wpa_supplicant") |
| } |
| iface, err := supplicant.GetInterface(ctx, request.Interface) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get interface object paths") |
| } |
| |
| // Convert BSSID from human readable string to hardware address in bytes. |
| requestBSSID, err := net.ParseMAC(request.Bssid) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to parse the MAC address from %s", request.Bssid) |
| } |
| |
| start := time.Now() |
| |
| // Start background routine to wait for the expected BSS. |
| done := make(chan error, 1) |
| // Wait for bg routine to end. |
| defer func() { <-done }() |
| // Notify the bg routine to end if we return early. |
| bgCtx, cancel := context.WithCancel(ctx) |
| defer cancel() |
| go func(ctx context.Context) { |
| defer close(done) |
| done <- s.waitForBSSID(ctx, iface, request.Ssid, requestBSSID) |
| }(bgCtx) |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager") |
| } |
| props, err := m.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the shill manager properties") |
| } |
| allow, err := props.GetBool(shillconst.ManagerPropertyScanAllowRoam) |
| if err != nil { |
| return nil, err |
| } |
| if allow { |
| if err := m.SetProperty(ctx, shillconst.ManagerPropertyScanAllowRoam, false); err != nil { |
| return nil, errors.Wrap(err, "failed to set WiFi.ScanAllowRoam property") |
| } |
| defer func(ctx context.Context) { |
| if err := m.SetProperty(ctx, shillconst.ManagerPropertyScanAllowRoam, true); err != nil { |
| testing.ContextLog(ctx, "Failed to restore WiFi.ScanAllowRoam property") |
| } |
| }(ctx) |
| } |
| // Trigger request scan every 200ms if the expected BSS is not found. |
| // It might be spammy, but shill handles it for us. |
| for { |
| if err := m.RequestScan(ctx, shill.TechnologyWifi); err != nil { |
| return nil, err |
| } |
| select { |
| case err := <-done: |
| if err != nil { |
| return nil, err |
| } |
| discoveryTime := time.Since(start) |
| return &wifi.DiscoverBSSIDResponse{ |
| DiscoveryTime: discoveryTime.Nanoseconds(), |
| }, nil |
| case <-ctx.Done(): |
| return nil, ctx.Err() |
| case <-time.After(200 * time.Millisecond): |
| } |
| } |
| } |
| |
| // Connect connects to a WiFi service with specific config. |
| // This is the implementation of wifi.ShillService/Connect gRPC. |
| func (s *ShillService) Connect(ctx context.Context, request *wifi.ConnectRequest) (*wifi.ConnectResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| ctx, st := timing.Start(ctx, "wifi_service.Connect") |
| defer st.End() |
| testing.ContextLog(ctx, "Attempting to connect with config: ", request) |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a manager object") |
| } |
| |
| hexSSID := s.hexSSID(request.Ssid) |
| start := time.Now() |
| |
| // Configure a service for the hidden SSID as a result of manual input SSID. |
| if request.Hidden { |
| props := map[string]interface{}{ |
| shillconst.ServicePropertyType: shillconst.TypeWifi, |
| shillconst.ServicePropertyWiFiHexSSID: hexSSID, |
| shillconst.ServicePropertyWiFiHiddenSSID: request.Hidden, |
| shillconst.ServicePropertySecurityClass: request.Security, |
| } |
| if _, err := m.ConfigureService(ctx, props); err != nil { |
| return nil, errors.Wrap(err, "failed to configure a hidden SSID") |
| } |
| } |
| props := map[string]interface{}{ |
| shillconst.ServicePropertyType: shillconst.TypeWifi, |
| shillconst.ServicePropertyWiFiHexSSID: hexSSID, |
| shillconst.ServicePropertySecurityClass: request.Security, |
| } |
| |
| service, err := s.discoverService(ctx, m, props) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to discover service") |
| } |
| discoveryTime := time.Since(start) |
| |
| shillProps, err := protoutil.DecodeFromShillValMap(request.Shillprops) |
| if err != nil { |
| return nil, err |
| } |
| for k, v := range shillProps { |
| if err = service.SetProperty(ctx, k, v); err != nil { |
| return nil, errors.Wrapf(err, "failed to set properties %s to %v", k, v) |
| } |
| } |
| |
| assocTime, configTime, err := s.connectService(ctx, service) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &wifi.ConnectResponse{ |
| ServicePath: string(service.ObjectPath()), |
| DiscoveryTime: discoveryTime.Nanoseconds(), |
| AssociationTime: assocTime.Nanoseconds(), |
| ConfigurationTime: configTime.Nanoseconds(), |
| }, nil |
| } |
| |
| func (s *ShillService) selectedService(ctx context.Context) (dbus.ObjectPath, error) { |
| ctx, st := timing.Start(ctx, "wifi_service.selectedService") |
| defer st.End() |
| |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return "", err |
| } |
| prop, err := dev.GetProperties(ctx) |
| if err != nil { |
| return "", errors.Wrap(err, "failed to get WiFi device properties") |
| } |
| servicePath, err := prop.GetObjectPath(shillconst.DevicePropertySelectedService) |
| if err != nil { |
| return "", errors.Wrap(err, "failed to get SelectedService") |
| } |
| |
| return servicePath, nil |
| } |
| |
| // SelectedService returns the object path of selected service of WiFi service. |
| func (s *ShillService) SelectedService(ctx context.Context, _ *empty.Empty) (*wifi.SelectedServiceResponse, error) { |
| servicePath, err := s.selectedService(ctx) |
| if err != nil { |
| return nil, err |
| } |
| // Handle a special case of no selected service. |
| // See: https://chromium.googlesource.com/chromiumos/platform2/+/HEAD/shill/doc/device-api.txt |
| if servicePath == "/" { |
| return nil, errors.New("no selected service") |
| } |
| |
| return &wifi.SelectedServiceResponse{ |
| ServicePath: string(servicePath), |
| }, nil |
| } |
| |
| // Disconnect disconnects from a WiFi service. |
| // This is the implementation of wifi.ShillService/Disconnect gRPC. |
| func (s *ShillService) Disconnect(ctx context.Context, request *wifi.DisconnectRequest) (ret *empty.Empty, retErr error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| ctx, st := timing.Start(ctx, "wifi_service.Disconnect") |
| defer st.End() |
| |
| service, err := shill.NewService(ctx, dbus.ObjectPath(request.ServicePath)) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create service object") |
| } |
| defer func() { |
| // Try to remove profile even if Disconnect failed. |
| if !request.RemoveProfile { |
| return |
| } |
| if err := service.Remove(ctx); err != nil { |
| if retErr != nil { |
| testing.ContextLogf(ctx, "Failed to remove service profile of %v: %v", service, err) |
| } else { |
| ret = nil |
| retErr = errors.Wrapf(err, "failed to remove service profile of %v", service) |
| } |
| } |
| }() |
| |
| // Spawn watcher before disconnect. |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create watcher") |
| } |
| defer pw.Close(ctx) |
| if err := service.Disconnect(ctx); err != nil { |
| return nil, errors.Wrap(err, "failed to disconnect") |
| } |
| testing.ContextLog(ctx, "Wait for the service to be idle") |
| timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) |
| defer cancel() |
| if err := pw.Expect(timeoutCtx, shillconst.ServicePropertyState, shillconst.ServiceStateIdle); err != nil { |
| return nil, err |
| } |
| testing.ContextLog(ctx, "Disconnected") |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // AssureDisconnect assures that the WiFi service has disconnected within request.Timeout. |
| // It waits for the service state to be idle. |
| // This is the implementation of wifi.ShillService/AssureDisconnect gRPC. |
| func (s *ShillService) AssureDisconnect(ctx context.Context, request *wifi.AssureDisconnectRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| ctx, st := timing.Start(ctx, "wifi_service.AssureDisconnect") |
| defer st.End() |
| |
| service, err := shill.NewService(ctx, dbus.ObjectPath(request.ServicePath)) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create service object") |
| } |
| |
| // Spawn watcher before disconnect. |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create watcher") |
| } |
| defer pw.Close(ctx) |
| |
| props, err := service.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get service properties") |
| } |
| |
| state, err := props.GetString(shillconst.ServicePropertyState) |
| if err != nil { |
| return nil, err |
| } |
| |
| if state != shillconst.ServiceStateIdle { |
| testing.ContextLog(ctx, "Wait for the service to be idle") |
| timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(request.Timeout)) |
| defer cancel() |
| |
| if err := pw.Expect(timeoutCtx, shillconst.ServicePropertyState, shillconst.ServiceStateIdle); err != nil { |
| return nil, err |
| } |
| } |
| |
| testing.ContextLog(ctx, "Disconnected") |
| return &empty.Empty{}, nil |
| } |
| |
| // uint16sToUint32s converts []uint16 to []uint32. |
| func uint16sToUint32s(s []uint16) []uint32 { |
| ret := make([]uint32, len(s)) |
| for i, v := range s { |
| ret[i] = uint32(v) |
| } |
| return ret |
| } |
| |
| // QueryService queries shill service information. |
| // This is the implementation of wifi.ShillService/QueryService gRPC. |
| func (s *ShillService) QueryService(ctx context.Context, req *wifi.QueryServiceRequest) (*wifi.QueryServiceResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| ctx, st := timing.Start(ctx, "wifi_service.QueryService") |
| defer st.End() |
| |
| service, err := shill.NewService(ctx, dbus.ObjectPath(req.Path)) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create service object") |
| } |
| props, err := service.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get service properties") |
| } |
| |
| name, err := props.GetString(shillconst.ServicePropertyName) |
| if err != nil { |
| return nil, err |
| } |
| device, err := props.GetObjectPath(shillconst.ServicePropertyDevice) |
| if err != nil { |
| return nil, err |
| } |
| serviceType, err := props.GetString(shillconst.ServicePropertyType) |
| if err != nil { |
| return nil, err |
| } |
| mode, err := props.GetString(shillconst.ServicePropertyMode) |
| if err != nil { |
| return nil, err |
| } |
| state, err := props.GetString(shillconst.ServicePropertyState) |
| if err != nil { |
| return nil, err |
| } |
| visible, err := props.GetBool(shillconst.ServicePropertyVisible) |
| if err != nil { |
| return nil, err |
| } |
| isConnected, err := props.GetBool(shillconst.ServicePropertyIsConnected) |
| if err != nil { |
| return nil, err |
| } |
| |
| bssid, err := props.GetString(shillconst.ServicePropertyWiFiBSSID) |
| if err != nil { |
| return nil, err |
| } |
| |
| frequency, err := props.GetUint16(shillconst.ServicePropertyWiFiFrequency) |
| if err != nil { |
| return nil, err |
| } |
| frequencyList, err := props.GetUint16s(shillconst.ServicePropertyWiFiFrequencyList) |
| if err != nil { |
| return nil, err |
| } |
| hexSSID, err := props.GetString(shillconst.ServicePropertyWiFiHexSSID) |
| if err != nil { |
| return nil, err |
| } |
| hiddenSSID, err := props.GetBool(shillconst.ServicePropertyWiFiHiddenSSID) |
| if err != nil { |
| return nil, err |
| } |
| phyMode, err := props.GetUint16(shillconst.ServicePropertyWiFiPhyMode) |
| if err != nil { |
| return nil, err |
| } |
| guid, err := props.GetString(shillconst.ServicePropertyGUID) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &wifi.QueryServiceResponse{ |
| Name: name, |
| Device: string(device), |
| Type: serviceType, |
| Mode: mode, |
| State: state, |
| Visible: visible, |
| IsConnected: isConnected, |
| Guid: guid, |
| Wifi: &wifi.QueryServiceResponse_Wifi{ |
| Bssid: bssid, |
| Frequency: uint32(frequency), |
| FrequencyList: uint16sToUint32s(frequencyList), |
| HexSsid: hexSSID, |
| HiddenSsid: hiddenSSID, |
| PhyMode: uint32(phyMode), |
| }, |
| }, nil |
| } |
| |
| // DeleteEntriesForSSID deletes all WiFi profile entries for a given SSID. |
| func (s *ShillService) DeleteEntriesForSSID(ctx context.Context, request *wifi.DeleteEntriesForSSIDRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| ctx, st := timing.Start(ctx, "wifi_service.DeleteEntriesForSSID") |
| defer st.End() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create Manager object") |
| } |
| filter := map[string]interface{}{ |
| shillconst.ServicePropertyWiFiHexSSID: s.hexSSID(request.Ssid), |
| shillconst.ProfileEntryPropertyType: shillconst.TypeWifi, |
| } |
| if err := s.removeMatchedEntries(ctx, m, filter); err != nil { |
| return nil, err |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // cleanProfiles pops and removes all active profiles until default profile and |
| // then removes the WiFi test profile if still exists. |
| func (s *ShillService) cleanProfiles(ctx context.Context, m *shill.Manager) error { |
| for { |
| profile, err := m.ActiveProfile(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to get active profile") |
| } |
| props, err := profile.GetProperties(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to get properties from profile object") |
| } |
| name, err := props.GetString(shillconst.ProfilePropertyName) |
| if name == shillconst.DefaultProfileName { |
| break |
| } |
| if err != nil { |
| return errors.Wrap(err, "failed to get profile name") |
| } |
| if err := m.PopProfile(ctx, name); err != nil { |
| return errors.Wrap(err, "failed to pop profile") |
| } |
| if err := m.RemoveProfile(ctx, name); err != nil { |
| return errors.Wrap(err, "failed to delete profile") |
| } |
| } |
| // Try to remove the test profile. |
| m.RemoveProfile(ctx, wifiTestProfileName) |
| return nil |
| } |
| |
| // removeWifiEntries removes all the entries with type=wifi in all profiles. |
| func (s *ShillService) removeWifiEntries(ctx context.Context, m *shill.Manager) error { |
| filter := map[string]interface{}{ |
| shillconst.ProfileEntryPropertyType: shillconst.TypeWifi, |
| } |
| return s.removeMatchedEntries(ctx, m, filter) |
| } |
| |
| // removeMatchedEntries traverses all profiles and removes all entries matching the properties in propFilter. |
| func (s *ShillService) removeMatchedEntries(ctx context.Context, m *shill.Manager, propFilter map[string]interface{}) error { |
| profiles, err := m.Profiles(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to get profiles") |
| } |
| for _, p := range profiles { |
| props, err := p.GetProperties(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to get properties from profile object") |
| } |
| entryIDs, err := props.GetStrings(shillconst.ProfilePropertyEntries) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get entryIDs from profile %s", p.String()) |
| } |
| entryLoop: |
| for _, entryID := range entryIDs { |
| entry, err := p.GetEntry(ctx, entryID) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get entry %s", entryID) |
| } |
| |
| for k, expect := range propFilter { |
| v, ok := entry[k] |
| if !ok || !reflect.DeepEqual(expect, v) { |
| // not matched, try new entry. |
| continue entryLoop |
| } |
| } |
| if err := p.DeleteEntry(ctx, entryID); err != nil { |
| return errors.Wrapf(err, "failed to delete entry %s", entryID) |
| } |
| } |
| } |
| return nil |
| } |
| |
| // GetInterface returns the WiFi device interface name (e.g., wlan0). |
| func (s *ShillService) GetInterface(ctx context.Context, e *empty.Empty) (*wifi.GetInterfaceResponse, error) { |
| manager, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager proxy") |
| } |
| netIf, err := shill.WifiInterface(ctx, manager, 5*time.Second) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the WiFi interface") |
| } |
| return &wifi.GetInterfaceResponse{ |
| Name: netIf, |
| }, nil |
| } |
| |
| // GetIPv4Addrs returns the IPv4 addresses for the network interface. |
| func (s *ShillService) GetIPv4Addrs(ctx context.Context, iface *wifi.GetIPv4AddrsRequest) (*wifi.GetIPv4AddrsResponse, error) { |
| ifaceObj, err := net.InterfaceByName(iface.InterfaceName) |
| if err != nil { |
| return nil, err |
| } |
| |
| addrs, err := ifaceObj.Addrs() |
| if err != nil { |
| return nil, err |
| } |
| |
| var ret wifi.GetIPv4AddrsResponse |
| |
| for _, a := range addrs { |
| if ipnet, ok := a.(*net.IPNet); ok && ipnet.IP.To4() != nil { |
| ret.Ipv4 = append(ret.Ipv4, ipnet.String()) |
| } |
| } |
| |
| return &ret, nil |
| } |
| |
| // GetHardwareAddr returns the HardwareAddr for the network interface. |
| func (s *ShillService) GetHardwareAddr(ctx context.Context, iface *wifi.GetHardwareAddrRequest) (*wifi.GetHardwareAddrResponse, error) { |
| ifaceObj, err := net.InterfaceByName(iface.InterfaceName) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &wifi.GetHardwareAddrResponse{HwAddr: ifaceObj.HardwareAddr.String()}, nil |
| } |
| |
| // RequestScans requests shill to trigger active scans on WiFi devices, |
| // and waits until at least req.Count scans are done. |
| func (s *ShillService) RequestScans(ctx context.Context, req *wifi.RequestScansRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| // Create watcher for ScanDone signal. |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager") |
| } |
| ifaceName, err := shill.WifiInterface(ctx, m, 10*time.Second) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get WiFi interface") |
| } |
| supplicant, err := wpasupplicant.NewSupplicant(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to connect to wpa_supplicant") |
| } |
| iface, err := supplicant.GetInterface(ctx, ifaceName) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get interface object paths") |
| } |
| sw, err := iface.DBusObject().CreateWatcher(ctx, wpasupplicant.DBusInterfaceSignalScanDone) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create signal watcher") |
| } |
| defer sw.Close(ctx) |
| |
| // Start background routine to wait for ScanDone signals. |
| done := make(chan error, 1) |
| // Wait for bg routine to end. |
| defer func() { <-done }() |
| // Notify the bg routine to end if we return early. |
| bgCtx, cancel := context.WithCancel(ctx) |
| defer cancel() |
| go func(ctx context.Context) { |
| defer close(done) |
| done <- func() error { |
| count := int32(0) |
| for count < req.Count { |
| select { |
| case <-ctx.Done(): |
| return ctx.Err() |
| case sig := <-sw.Signals: |
| if success, err := iface.ParseScanDoneSignal(ctx, sig); err != nil { |
| testing.ContextLogf(ctx, "Unexpected ScanDone signal %v: %v", sig, err) |
| } else if !success { |
| testing.ContextLog(ctx, "Unexpected ScanDone signal with failed scan") |
| } else { |
| count++ |
| } |
| } |
| } |
| return nil |
| }() |
| }(bgCtx) |
| |
| // Trigger request scan every 200ms if the expected number of ScanDone is |
| // not yet captured by the background routine and context deadline is not |
| // yet reached. |
| // It might be spammy, but shill handles it for us. |
| for { |
| if err := m.RequestScan(ctx, shill.TechnologyWifi); err != nil { |
| return nil, err |
| } |
| select { |
| case err := <-done: |
| if err != nil { |
| return nil, err |
| } |
| return &empty.Empty{}, nil |
| case <-ctx.Done(): |
| return nil, ctx.Err() |
| case <-time.After(200 * time.Millisecond): |
| } |
| } |
| } |
| |
| // RequestRoam requests shill to roam to another BSSID and waits until the DUT has roamed. |
| // This is the implementation of wifi.ShillService/RequestRoam gRPC. |
| func (s *ShillService) RequestRoam(ctx context.Context, req *wifi.RequestRoamRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| dev, err := m.WaitForDeviceByName(ctx, req.InterfaceName, 5*time.Second) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to find the device for the interface %s", req.InterfaceName) |
| } |
| |
| if err := dev.RequestRoam(ctx, req.Bssid); err != nil { |
| return nil, err |
| } |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // Reassociate triggers reassociation with the current AP and waits until it has reconnected or the timeout expires. |
| // This is the implementation of wifi.WiFi/Reassociate gRPC. |
| func (s *ShillService) Reassociate(ctx context.Context, req *wifi.ReassociateRequest) (*empty.Empty, error) { |
| ctx, cancelRet := reserveForReturn(ctx) |
| defer cancelRet() |
| |
| supplicant, err := wpasupplicant.NewSupplicant(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to connect to wpa_supplicant") |
| } |
| iface, err := supplicant.GetInterface(ctx, req.InterfaceName) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get interface object paths") |
| } |
| |
| // Create watcher for PropertiesChanged signal. |
| sw, err := iface.DBusObject().CreateWatcher(ctx, wpasupplicant.DBusInterfaceSignalPropertiesChanged) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create signal watcher") |
| } |
| defer sw.Close(ctx) |
| |
| // Trigger reassociation back to the current BSS. |
| if err := iface.Reattach(ctx); err != nil { |
| return nil, errors.Wrap(err, "failed to call Reattach method") |
| } |
| |
| // Watch the PropertiesChanged signals looking for state transitions which |
| // indicate reassociation taking place. Most of the time on most platforms |
| // this looks like: |
| // |
| // State="associating" |
| // State="associated" or State="completed" |
| // |
| // But sometimes on specific platforms this can instead look like: |
| // |
| // Scanning=true |
| // Scanning=false |
| // State="completed" |
| // |
| // So the detection logic here will accept either Scanning=true or |
| // (State!="associated" && State!="completed") as indicating that we have |
| // begun reassociation. |
| associating := false |
| ctx, cancel := context.WithTimeout(ctx, time.Duration(req.Timeout)) |
| defer cancel() |
| for { |
| select { |
| case <-ctx.Done(): |
| return nil, errors.Wrap(ctx.Err(), "did not reassociate in time") |
| case sig := <-sw.Signals: |
| props := sig.Body[0].(map[string]dbus.Variant) |
| testing.ContextLog(ctx, "PropertiesChanged: ", props) |
| if val, ok := props["Scanning"]; ok { |
| scanning := val.Value().(bool) |
| if !associating && scanning { |
| associating = true |
| } |
| } |
| if val, ok := props["State"]; ok { |
| state := val.Value().(string) |
| associated := state == wpasupplicant.DBusInterfaceStateAssociated || state == wpasupplicant.DBusInterfaceStateCompleted |
| if !associating && !associated { |
| associating = true |
| } |
| if associating && associated { |
| return &empty.Empty{}, nil |
| } |
| } |
| } |
| } |
| } |
| |
| // MACRandomizeSupport tells if MAC randomization is supported for the WiFi device. |
| func (s *ShillService) MACRandomizeSupport(ctx context.Context, _ *empty.Empty) (*wifi.MACRandomizeSupportResponse, error) { |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| prop, err := dev.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get WiFi device properties") |
| } |
| |
| supported, err := prop.GetBool(shillconst.DevicePropertyMACAddrRandomSupported) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to get WiFi device boolean prop %q", |
| shillconst.DevicePropertyMACAddrRandomSupported) |
| } |
| return &wifi.MACRandomizeSupportResponse{Supported: supported}, nil |
| } |
| |
| // GetMACRandomize tells if MAC randomization is enabled for the WiFi device. |
| func (s *ShillService) GetMACRandomize(ctx context.Context, _ *empty.Empty) (*wifi.GetMACRandomizeResponse, error) { |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| prop, err := dev.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get WiFi device properties") |
| } |
| |
| enabled, err := prop.GetBool(shillconst.DevicePropertyMACAddrRandomEnabled) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to get WiFi device boolean prop %q", |
| shillconst.DevicePropertyMACAddrRandomEnabled) |
| } |
| |
| return &wifi.GetMACRandomizeResponse{Enabled: enabled}, nil |
| } |
| |
| // SetMACRandomize sets the MAC randomization setting on the WiFi device. |
| // The original setting is returned for ease of restoring. |
| func (s *ShillService) SetMACRandomize(ctx context.Context, req *wifi.SetMACRandomizeRequest) (*wifi.SetMACRandomizeResponse, error) { |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| prop, err := dev.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get WiFi device properties") |
| } |
| |
| // Check if it is supported. |
| support, err := prop.GetBool(shillconst.DevicePropertyMACAddrRandomSupported) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to get WiFi device boolean prop %q", |
| shillconst.DevicePropertyMACAddrRandomSupported) |
| } |
| if !support { |
| return nil, errors.New("MAC randomization not supported") |
| } |
| // Get old setting. |
| old, err := prop.GetBool(shillconst.DevicePropertyMACAddrRandomEnabled) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to get WiFi device boolean prop %q", |
| shillconst.DevicePropertyMACAddrRandomEnabled) |
| } |
| |
| // NOP if the setting is already as requested. |
| if req.Enable != old { |
| if err := dev.SetProperty(ctx, shillconst.DevicePropertyMACAddrRandomEnabled, req.Enable); err != nil { |
| return nil, errors.Wrapf(err, "failed to set WiFi device property %q", |
| shillconst.DevicePropertyMACAddrRandomEnabled) |
| } |
| } |
| |
| return &wifi.SetMACRandomizeResponse{OldSetting: old}, nil |
| } |
| |
| // WaitScanIdle waits for not scanning state. If there's a running scan, it |
| // waits for the scan to be done with timeout 10 seconds. |
| // This is useful when the test sets some parameters regarding scans and wants |
| // to avoid noises due to in-progress scans. |
| func (s *ShillService) WaitScanIdle(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| pw, err := dev.CreateWatcher(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create watcher") |
| } |
| defer pw.Close(ctx) |
| |
| // Check initial state. |
| prop, err := dev.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get properties of WiFi device") |
| } |
| scanning, err := prop.GetBool(shillconst.DevicePropertyScanning) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get WiFi scanning state") |
| } |
| if !scanning { |
| // Already in the expected state, return immediately. |
| return &empty.Empty{}, nil |
| } |
| |
| // Wait scanning to become false. |
| timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second) |
| defer cancel() |
| if err := pw.Expect(timeoutCtx, shillconst.DevicePropertyScanning, false); err != nil { |
| return nil, errors.Wrap(err, "failed to wait for not scanning state") |
| } |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // hexSSID converts a SSID into the format of WiFi.HexSSID in shill. |
| // As in our tests, the SSID might contain non-ASCII characters, use WiFi.HexSSID |
| // field for better compatibility. |
| // Note: shill has the hex in upper case. |
| func (s *ShillService) hexSSID(ssid []byte) string { |
| return strings.ToUpper(hex.EncodeToString(ssid)) |
| } |
| |
| // uint16sEqualUint32s returns true if a is equal to b. |
| func uint16sEqualUint32s(a []uint16, b []uint32) bool { |
| if len(a) != len(b) { |
| return false |
| } |
| |
| sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) |
| sort.Slice(b, func(i, j int) bool { return b[i] < b[j] }) |
| |
| for i, v := range b { |
| if v != uint32(a[i]) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // ExpectWifiFrequencies checks if the device discovers the given SSID on the specific frequencies. |
| func (s *ShillService) ExpectWifiFrequencies(ctx context.Context, req *wifi.ExpectWifiFrequenciesRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| ctx, st := timing.Start(ctx, "wifi_service.ExpectWifiFrequencies") |
| defer st.End() |
| testing.ContextLog(ctx, "ExpectWifiFrequencies: ", req) |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a manager object") |
| } |
| |
| hexSSID := s.hexSSID(req.Ssid) |
| query := map[string]interface{}{ |
| shillconst.ServicePropertyType: shillconst.TypeWifi, |
| shillconst.ServicePropertyWiFiHexSSID: hexSSID, |
| } |
| |
| service, err := s.discoverService(ctx, m, query) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Spawn watcher for checking property change. |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create watcher") |
| } |
| defer pw.Close(ctx) |
| |
| shortCtx, cancel := context.WithTimeout(ctx, 20*time.Second) |
| defer cancel() |
| |
| for { |
| props, err := service.GetProperties(shortCtx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get service properties") |
| } |
| freqs, err := props.GetUint16s(shillconst.ServicePropertyWiFiFrequencyList) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to get property %s", shillconst.ServicePropertyWiFiFrequencyList) |
| } |
| if uint16sEqualUint32s(freqs, req.Frequencies) { |
| testing.ContextLogf(shortCtx, "Got wanted frequencies %v for service with SSID: %s", req.Frequencies, req.Ssid) |
| return &empty.Empty{}, nil |
| } |
| |
| testing.ContextLogf(shortCtx, "Got frequencies %v for service with SSID: %s; want %v; waiting for update", freqs, req.Ssid, req.Frequencies) |
| if _, err := pw.WaitAll(shortCtx, shillconst.ServicePropertyWiFiFrequencyList); err != nil { |
| return nil, errors.Wrap(err, "failed to wait for the service property change") |
| } |
| } |
| } |
| |
| func (s *ShillService) wifiDev(ctx context.Context) (*shill.Manager, *shill.Device, error) { |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, nil, errors.Wrap(err, "failed to create Manager object") |
| } |
| iface, err := shill.WifiInterface(ctx, m, 5*time.Second) |
| if err != nil { |
| return m, nil, errors.Wrap(err, "failed to get a WiFi device") |
| } |
| dev, err := m.DeviceByName(ctx, iface) |
| if err != nil { |
| return m, nil, errors.Wrapf(err, "failed to find the device for interface %s", iface) |
| } |
| return m, dev, nil |
| } |
| |
| func (s *ShillService) GetBgscanConfig(ctx context.Context, e *empty.Empty) (*wifi.GetBgscanConfigResponse, error) { |
| ctx, st := timing.Start(ctx, "wifi_service.GetBgscan") |
| defer st.End() |
| |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| props, err := dev.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the WiFi device properties") |
| } |
| method, err := props.GetString(shillconst.DevicePropertyWiFiBgscanMethod) |
| if err != nil { |
| return nil, err |
| } |
| interval, err := props.GetUint16(shillconst.DevicePropertyWiFiScanInterval) |
| if err != nil { |
| return nil, err |
| } |
| shortInterval, err := props.GetUint16(shillconst.DevicePropertyWiFiBgscanShortInterval) |
| if err != nil { |
| return nil, err |
| } |
| return &wifi.GetBgscanConfigResponse{ |
| Config: &wifi.BgscanConfig{ |
| Method: method, |
| LongInterval: uint32(interval), |
| ShortInterval: uint32(shortInterval), |
| }, |
| }, nil |
| } |
| |
| func (s *ShillService) SetBgscanConfig(ctx context.Context, req *wifi.SetBgscanConfigRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| ctx, st := timing.Start(ctx, "wifi_service.SetBgscan") |
| defer st.End() |
| |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| setProp := func(ctx context.Context, key string, val interface{}) error { |
| if err := dev.SetProperty(ctx, key, val); err != nil { |
| return errors.Wrapf(err, "failed to set the WiFi device property %s with value %v", key, val) |
| } |
| return nil |
| } |
| |
| if err := setProp(ctx, shillconst.DevicePropertyWiFiBgscanMethod, req.Config.Method); err != nil { |
| return nil, err |
| } |
| if err := setProp(ctx, shillconst.DevicePropertyWiFiScanInterval, uint16(req.Config.LongInterval)); err != nil { |
| return nil, err |
| } |
| if err := setProp(ctx, shillconst.DevicePropertyWiFiBgscanShortInterval, uint16(req.Config.ShortInterval)); err != nil { |
| return nil, err |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // DisableEnableTest disables and then enables the WiFi interface. This is the main body of the DisableEnable test. |
| // It first disables the WiFi interface and waits for the idle state; then waits for the IsConnected property after enable. |
| // The reason we place most of the logic here is that, we need to spawn a shill properties watcher before disabling/enabling |
| // the WiFi interface, so we won't lose the state change events between the gRPC commands of disabling/enabling interface |
| // and checking state. |
| func (s *ShillService) DisableEnableTest(ctx context.Context, request *wifi.DisableEnableTestRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create Manager object") |
| } |
| dev, err := m.DeviceByName(ctx, request.InterfaceName) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to find the device for interface %s", request.InterfaceName) |
| } |
| |
| // Spawn watcher before disabling and enabling. |
| service, err := shill.NewService(ctx, dbus.ObjectPath(request.ServicePath)) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create service object") |
| } |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create watcher") |
| } |
| defer pw.Close(ctx) |
| |
| // Form a closure here so we can ensure that interface will be re-enabled even if something failed. |
| if err := func() (retErr error) { |
| // Disable WiFi interface. |
| testing.ContextLog(ctx, "Disabling WiFi interface: ", request.InterfaceName) |
| if err := dev.Disable(ctx); err != nil { |
| return errors.Wrap(err, "failed to disable WiFi device") |
| } |
| |
| defer func() { |
| // Re-enable WiFi interface. |
| testing.ContextLog(ctx, "Enabling WiFi interface: ", request.InterfaceName) |
| if err := dev.Enable(ctx); err != nil { |
| if retErr == nil { |
| retErr = errors.Wrap(err, "failed to enable WiFi device") |
| } else { |
| testing.ContextLog(ctx, "Failed to enable WiFi device and the test is already failed: ", err) |
| } |
| } |
| }() |
| |
| // Wait for WiFi service becomes idle state. |
| testing.ContextLog(ctx, "Waiting for idle state") |
| timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second) |
| defer cancel() |
| if err := pw.Expect(timeoutCtx, shillconst.ServicePropertyState, shillconst.ServiceStateIdle); err != nil { |
| return errors.Wrap(err, "failed to wait for idle state after disabling") |
| } |
| return nil |
| }(); err != nil { |
| return nil, err |
| } |
| |
| // The interface has been re-enabled as a defer statement in the anonymous function above, |
| // now just need to wait for WiFi service becomes connected. |
| testing.ContextLog(ctx, "Waiting for IsConnected property") |
| timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| defer cancel() |
| if err := pw.Expect(timeoutCtx, shillconst.ServicePropertyIsConnected, true); err != nil { |
| return nil, errors.Wrap(err, "failed to wait for IsConnected property after enabling") |
| } |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // ConfigureAndAssertAutoConnect configures the matched shill service and then waits for the IsConnected property becomes true. |
| // Note that this function does not attempt to connect; it waits for auto connect instead. |
| func (s *ShillService) ConfigureAndAssertAutoConnect(ctx context.Context, |
| request *wifi.ConfigureAndAssertAutoConnectRequest) (*wifi.ConfigureAndAssertAutoConnectResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a manager object") |
| } |
| |
| props, err := protoutil.DecodeFromShillValMap(request.Props) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to decode shill properties") |
| } |
| servicePath, err := m.ConfigureService(ctx, props) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to configure service") |
| } |
| testing.ContextLog(ctx, "Configured service; start scanning") |
| |
| service, err := shill.NewService(ctx, servicePath) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create service object") |
| } |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create property watcher") |
| } |
| defer pw.Close(ctx) |
| |
| // Service may become connected between ConfigureService and CreateWatcher, we would lose the property changing event of IsConnected then. |
| // Checking once after creating watcher should be good enough since we expect that the connection should not disconnect without our attempt. |
| p, err := service.GetProperties(ctx) |
| if err != nil { |
| return nil, err |
| } |
| isConnected, err := p.GetBool(shillconst.ServicePropertyIsConnected) |
| if err != nil { |
| return nil, err |
| } |
| if isConnected { |
| return &wifi.ConfigureAndAssertAutoConnectResponse{Path: string(servicePath)}, nil |
| } |
| |
| done := make(chan error, 1) |
| // Wait for bg routine to end. |
| defer func() { <-done }() |
| // Notify the bg routine to end if we return early. |
| bgCtx, cancel := context.WithCancel(ctx) |
| defer cancel() |
| go func(ctx context.Context) { |
| defer close(done) |
| done <- pw.Expect(ctx, shillconst.ServicePropertyIsConnected, true) |
| }(bgCtx) |
| |
| // Request a scan every 200ms until the background routine catches the IsConnected signal. |
| for { |
| if err := m.RequestScan(ctx, shill.TechnologyWifi); err != nil { |
| return nil, errors.Wrap(err, "failed to request scan") |
| } |
| select { |
| case err := <-done: |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to wait for IsConnected property becoming true") |
| } |
| return &wifi.ConfigureAndAssertAutoConnectResponse{Path: string(servicePath)}, nil |
| case <-time.After(200 * time.Millisecond): |
| } |
| } |
| } |
| |
| // GetCurrentTime returns the current local time in the given format. |
| func (s *ShillService) GetCurrentTime(ctx context.Context, _ *empty.Empty) (*wifi.GetCurrentTimeResponse, error) { |
| now := time.Now() |
| return &wifi.GetCurrentTimeResponse{NowSecond: now.Unix(), NowNanosecond: int64(now.Nanosecond())}, nil |
| } |
| |
| // ExpectShillProperty is a streaming gRPC, takes a shill service path, expects a list of property |
| // criteria in order, and takes a list of shill properties to monitor. When a property's value is |
| // expected, it responds the property's (key, value) pair. The method sends an empty response as the |
| // property watcher is set. A property matching criterion consists of a property name, a list of |
| // expected values, a list of excluded values, and a "CheckType". We say a criterion is met iff the |
| // property value is in one of the expected values and not in any of the excluded values. If the |
| // property value is one of the excluded values, the method fails immediately. The call monitors the |
| // specified shill properties and returns the monitor results as a chronological list of pairs |
| // (changed property, changed value) at the end. |
| // For CheckMethod, it has three methods: |
| // 1. CHECK_ONLY: checks if the criterion is met. |
| // 2. ON_CHANGE: waits for the property changes to the expected values. |
| // 3. CHECK_WAIT: checks if the criterion is met; if not, waits until the property's value is met. |
| // This is the implementation of wifi.ShillService/ExpectShillProperty gRPC. |
| func (s *ShillService) ExpectShillProperty(req *wifi.ExpectShillPropertyRequest, sender wifi.ShillService_ExpectShillPropertyServer) error { |
| ctx, cancel := reserveForReturn(sender.Context()) |
| defer cancel() |
| |
| service, err := shill.NewService(ctx, dbus.ObjectPath(req.ObjectPath)) |
| if err != nil { |
| return errors.Wrap(err, "failed to create a service object") |
| } |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to create a watcher") |
| } |
| defer pw.Close(ctx) |
| |
| if err := sender.Send(&wifi.ExpectShillPropertyResponse{}); err != nil { |
| return errors.Wrap(err, "failed to send a response") |
| } |
| |
| // foundIn returns true if the property value v is found in vs; false otherwise. |
| foundIn := func(v interface{}, vs []interface{}) bool { |
| for _, ev := range vs { |
| // Protoutil does not support uint16 in the meantime. |
| // Change the type of v to uint32, if its type is uint16. |
| if x, ok := v.(uint16); ok { |
| v = uint32(x) |
| } |
| if reflect.DeepEqual(ev, v) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| var monitorResult []protoutil.ShillPropertyHolder |
| for _, p := range req.Props { |
| var expectedVals []interface{} |
| for _, sv := range p.AnyOf { |
| v, err := protoutil.FromShillVal(sv) |
| if err != nil { |
| return err |
| } |
| expectedVals = append(expectedVals, v) |
| } |
| |
| var excludedVals []interface{} |
| for _, sv := range p.NoneOf { |
| v, err := protoutil.FromShillVal(sv) |
| if err != nil { |
| return err |
| } |
| excludedVals = append(excludedVals, v) |
| } |
| |
| // Check the current value of the property. |
| if p.Method != wifi.ExpectShillPropertyRequest_ON_CHANGE { |
| props, err := service.GetProperties(ctx) |
| if err != nil { |
| return err |
| } |
| |
| val, err := props.Get(p.Key) |
| if err != nil { |
| return err |
| } |
| |
| if foundIn(val, excludedVals) { |
| return errors.Errorf("unexpected property [ %q ] value: got %s, want any of %v", p.Key, val, expectedVals) |
| } |
| |
| if foundIn(val, expectedVals) { |
| shillVal, err := protoutil.ToShillVal(val) |
| if err != nil { |
| return err |
| } |
| if err := sender.Send(&wifi.ExpectShillPropertyResponse{Key: p.Key, Val: shillVal}); err != nil { |
| return errors.Wrap(err, "failed to send response") |
| } |
| |
| // Skip waiting for the property change and move to the next criterion. |
| continue |
| } |
| |
| // Return an error if the method is CHECK_ONLY and the property does not meet the criterion. |
| if p.Method == wifi.ExpectShillPropertyRequest_CHECK_ONLY { |
| return errors.Errorf("unexpected property [ %q ] value: got %s, want any of %v", p.Key, val, expectedVals) |
| } |
| } |
| |
| // Wait for the property to change to an expected or unexpected value. Record the property changes we are monitoring. |
| var propVal interface{} |
| for { |
| prop, val, _, err := pw.Wait(ctx) |
| if err != nil { |
| return errors.Wrapf(err, "failed to wait for the property %s", prop) |
| } |
| for _, mp := range req.MonitorProps { |
| if mp == prop { |
| monitorResult = append(monitorResult, protoutil.ShillPropertyHolder{Name: prop, Value: val}) |
| break |
| } |
| } |
| if prop == p.Key { |
| if foundIn(val, expectedVals) { |
| propVal = val |
| break |
| } |
| if foundIn(val, excludedVals) { |
| return errors.Errorf("unexpected property %q: got %s, want any of %v", prop, val, expectedVals) |
| } |
| } |
| } |
| |
| shillVal, err := protoutil.ToShillVal(propVal) |
| if err != nil { |
| return err |
| } |
| |
| if err := sender.Send(&wifi.ExpectShillPropertyResponse{Key: p.Key, Val: shillVal}); err != nil { |
| return errors.Wrap(err, "failed to send response") |
| } |
| } |
| |
| rslt, err := protoutil.EncodeToShillPropertyChangedSignalList(monitorResult) |
| if err != nil { |
| return err |
| } |
| |
| if err := sender.Send(&wifi.ExpectShillPropertyResponse{Props: rslt, MonitorDone: true}); err != nil { |
| return errors.Wrap(err, "failed to send response") |
| } |
| |
| return nil |
| } |
| |
| // expectServiceProperty sets up a properties watcher before calling f, and waits for the given property/value. |
| func (*ShillService) expectServiceProperty(ctx context.Context, service *shill.Service, prop string, val interface{}, f func() error) (dbus.Sequence, error) { |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to create a service property watcher") |
| } |
| defer pw.Close(ctx) |
| if err := f(); err != nil { |
| return 0, err |
| } |
| for { |
| p, v, s, err := pw.Wait(ctx) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to wait for property") |
| } |
| if p == prop && reflect.DeepEqual(v, val) { |
| return s, nil |
| } |
| } |
| } |
| |
| // ProfileBasicTest is the main body of the ProfileBasic test, which creates, pushes, and pops the profiles and asserts the connection states between those operations. |
| // This is the implementation of wifi.ShillService/ProfileBasicTest gRPC. |
| func (s *ShillService) ProfileBasicTest(ctx context.Context, req *wifi.ProfileBasicTestRequest) (_ *empty.Empty, retErr error) { |
| const ( |
| profileBottomName = "bottom" |
| profileTopName = "top" |
| |
| pingLossThreshold = 20.0 |
| ) |
| |
| expectIdle := func(ctx context.Context, service *shill.Service, f func() error) (dbus.Sequence, error) { |
| ctx, cancel := context.WithTimeout(ctx, 5*time.Second) |
| defer cancel() |
| return s.expectServiceProperty(ctx, service, shillconst.ServicePropertyState, shillconst.ServiceStateIdle, f) |
| } |
| expectIsConnected := func(ctx context.Context, service *shill.Service, f func() error) (dbus.Sequence, error) { |
| ctx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| defer cancel() |
| return s.expectServiceProperty(ctx, service, shillconst.ServicePropertyIsConnected, true, f) |
| } |
| connectAndPing := func(ctx context.Context, service *shill.Service, ap *wifi.ProfileBasicTestRequest_Config) error { |
| shillProps, err := protoutil.DecodeFromShillValMap(ap.ShillProps) |
| if err != nil { |
| return err |
| } |
| for k, v := range shillProps { |
| if err = service.SetProperty(ctx, k, v); err != nil { |
| return errors.Wrapf(err, "failed to set property %s to %v", k, v) |
| } |
| } |
| if _, _, err := s.connectService(ctx, service); err != nil { |
| return err |
| } |
| |
| res, err := local_ping.NewLocalRunner().Ping(ctx, ap.Ip) |
| if err != nil { |
| return err |
| } |
| if res.Loss > pingLossThreshold { |
| return errors.Errorf("unexpected packet loss percentage: got %g%%, want <= %g%%", res.Loss, pingLossThreshold) |
| } |
| |
| return nil |
| } |
| |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a shill manager") |
| } |
| |
| service0, err := s.discoverService(ctx, m, map[string]interface{}{ |
| shillconst.ServicePropertyType: shillconst.TypeWifi, |
| shillconst.ServicePropertyWiFiHexSSID: s.hexSSID(req.Ap0.Ssid), |
| shillconst.ServicePropertySecurityClass: req.Ap0.Security, |
| }) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to discover AP0") |
| } |
| service1, err := s.discoverService(ctx, m, map[string]interface{}{ |
| shillconst.ServicePropertyType: shillconst.TypeWifi, |
| shillconst.ServicePropertyWiFiHexSSID: s.hexSSID(req.Ap1.Ssid), |
| shillconst.ServicePropertySecurityClass: req.Ap1.Security, |
| }) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to discover AP1") |
| } |
| |
| if _, err := m.CreateProfile(ctx, profileBottomName); err != nil { |
| return nil, errors.Wrapf(err, "failed to create profile %q", profileBottomName) |
| } |
| defer func(ctx context.Context) { |
| // Ignore the error of popping profile since it may be popped during test. |
| m.PopProfile(ctx, profileBottomName) |
| if err := m.RemoveProfile(ctx, profileBottomName); err != nil { |
| if retErr != nil { |
| testing.ContextLogf(ctx, "Failed to remove profile %q and the test has already failed: %v", profileBottomName, err) |
| } else { |
| retErr = errors.Wrapf(err, "failed to remove profile %q", profileBottomName) |
| } |
| } |
| }(ctx) |
| if _, err := m.PushProfile(ctx, profileBottomName); err != nil { |
| return nil, errors.Wrapf(err, "failed to push profile %q", profileBottomName) |
| } |
| |
| if err := connectAndPing(ctx, service0, req.Ap0); err != nil { |
| return nil, err |
| } |
| |
| // We should lose the credentials if we pop the profile. |
| if _, err := expectIdle(ctx, service0, func() error { |
| return m.PopProfile(ctx, profileBottomName) |
| }); err != nil { |
| return nil, errors.Wrapf(err, "failed to pop profile %q and wait for idle", profileBottomName) |
| } |
| // We should retrieve the credentials if we push the profile back. |
| if _, err := expectIsConnected(ctx, service0, func() error { |
| _, err := m.PushProfile(ctx, profileBottomName) |
| return err |
| }); err != nil { |
| return nil, errors.Wrapf(err, "failed to push profile %q and wait for isConnected", profileBottomName) |
| } |
| |
| // Explicitly disconnect from AP0. |
| if _, err := expectIdle(ctx, service0, func() error { |
| return service0.Disconnect(ctx) |
| }); err != nil { |
| return nil, errors.Wrap(err, "failed to disconnect from AP0") |
| } |
| |
| if _, err := m.CreateProfile(ctx, profileTopName); err != nil { |
| return nil, errors.Wrapf(err, "failed to create profile %q", profileTopName) |
| } |
| defer func(ctx context.Context) { |
| // Ignore the error of popping profile since it may be popped during test. |
| m.PopProfile(ctx, profileTopName) |
| if err := m.RemoveProfile(ctx, profileTopName); err != nil { |
| if retErr != nil { |
| testing.ContextLogf(ctx, "Failed to remove profile %q and the test has already failed: %v", profileTopName, err) |
| } else { |
| retErr = errors.Wrapf(err, "failed to remove profile %q", profileTopName) |
| } |
| } |
| }(ctx) |
| |
| // The modification of the profile stack should clear the "explicitly disconnected" |
| // flag on all services and leads to a re-connecting. |
| if _, err := expectIsConnected(ctx, service0, func() error { |
| _, err := m.PushProfile(ctx, profileTopName) |
| return err |
| }); err != nil { |
| return nil, errors.Wrapf(err, "failed to push profile %q and wait for isConnected", profileTopName) |
| } |
| |
| if err := connectAndPing(ctx, service1, req.Ap1); err != nil { |
| return nil, err |
| } |
| |
| // Removing the entries of AP1 should cause a disconnecting to AP1 and then a reconnecting to AP0. |
| // Recording the sequence code of the Idle/IsConnected events helps us to determine the order. |
| var idleSeq, isConnectedSeq dbus.Sequence |
| if isConnectedSeq, err = expectIsConnected(ctx, service0, func() error { |
| var innerErr error |
| if idleSeq, innerErr = expectIdle(ctx, service1, func() error { |
| return s.removeMatchedEntries(ctx, m, map[string]interface{}{ |
| shillconst.ServicePropertyWiFiHexSSID: s.hexSSID(req.Ap1.Ssid), |
| shillconst.ProfileEntryPropertyType: shillconst.TypeWifi, |
| }) |
| }); innerErr != nil { |
| return innerErr |
| } |
| return nil |
| }); err != nil { |
| return nil, errors.Wrap(err, "failed to remove entries of AP0 and wait for reconnect") |
| } |
| if idleSeq > isConnectedSeq { |
| return nil, errors.New("expected to get the Idle signal of AP0 before the IsConnected signal of AP1 but got an inverse order") |
| } |
| |
| if err := connectAndPing(ctx, service1, req.Ap1); err != nil { |
| return nil, err |
| } |
| |
| // Popping the current profile should be similar to the case above. |
| if isConnectedSeq, err = expectIsConnected(ctx, service0, func() error { |
| var innerErr error |
| if idleSeq, innerErr = expectIdle(ctx, service1, func() error { |
| return m.PopProfile(ctx, profileTopName) |
| }); innerErr != nil { |
| return innerErr |
| } |
| return nil |
| }); err != nil { |
| return nil, errors.Wrapf(err, "failed to pop profile %q and wait for reconnect", profileTopName) |
| } |
| if idleSeq > isConnectedSeq { |
| return nil, errors.New("expected to get the Idle signal of AP0 before the IsConnected signal of AP1 but got an inverse order") |
| } |
| |
| if _, err := m.PushProfile(ctx, profileTopName); err != nil { |
| return nil, errors.Wrapf(err, "failed to push profile %q", profileTopName) |
| } |
| |
| // Explicitly disconnect from AP0. |
| if _, err := expectIdle(ctx, service0, func() error { |
| return service0.Disconnect(ctx) |
| }); err != nil { |
| return nil, errors.Wrap(err, "failed to disconnect from AP0") |
| } |
| |
| // Verify that popping a profile which does not affect the service should also clear the service's "explicitly disconnected" flag. |
| if _, err := expectIsConnected(ctx, service0, func() error { |
| return m.PopProfile(ctx, profileTopName) |
| }); err != nil { |
| return nil, errors.Wrapf(err, "failed to pop profile %q and wait for isConnected", profileTopName) |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // waitForWifiAvailable waits for WiFi to be available in shill. |
| func (s *ShillService) waitForWifiAvailable(ctx context.Context, m *shill.Manager) error { |
| pw, err := m.CreateWatcher(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to create watcher") |
| } |
| defer pw.Close(ctx) |
| |
| prop, err := m.GetProperties(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to get Manager's properties from shill") |
| } |
| |
| findWifi := func(list []string) bool { |
| for _, s := range list { |
| if s == shillconst.TypeWifi { |
| return true |
| } |
| } |
| return false |
| } |
| |
| techs, err := prop.GetStrings(shillconst.ManagerPropertyAvailableTechnologies) |
| if err != nil { |
| return errors.Wrap(err, "failed to get availabe technologies property") |
| } |
| if findWifi(techs) { |
| return nil |
| } |
| for { |
| val, err := pw.WaitAll(ctx, shillconst.ManagerPropertyAvailableTechnologies) |
| if err != nil { |
| return errors.Wrap(err, "failed to wait available technologies changes") |
| } |
| techs, ok := val[0].([]string) |
| if !ok { |
| return errors.Errorf("unexpected available technologies value: %v", val) |
| } |
| if findWifi(techs) { |
| return nil |
| } |
| } |
| } |
| |
| // GetWifiEnabled checks to see if Wifi is an enabled technology on shill. |
| // This call will wait for WiFi to appear in available technologies so we |
| // can get correct enabled setting. |
| func (s *ShillService) GetWifiEnabled(ctx context.Context, _ *empty.Empty) (*wifi.GetWifiEnabledResponse, error) { |
| manager, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create Manager object") |
| } |
| |
| if err := s.waitForWifiAvailable(ctx, manager); err != nil { |
| return nil, err |
| } |
| |
| prop, err := manager.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get Manager' properties from shill") |
| } |
| |
| technologies, err := prop.GetStrings(shillconst.ManagerPropertyEnabledTechnologies) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed go get enabled technologies property") |
| } |
| |
| for _, t := range technologies { |
| if t == string(shill.TechnologyWifi) { |
| return &wifi.GetWifiEnabledResponse{Enabled: true}, nil |
| } |
| } |
| return &wifi.GetWifiEnabledResponse{Enabled: false}, nil |
| } |
| |
| // SetWifiEnabled persistently enables/disables Wifi via shill. |
| func (s *ShillService) SetWifiEnabled(ctx context.Context, request *wifi.SetWifiEnabledRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| manager, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create Manager object") |
| } |
| _, err = shill.WifiInterface(ctx, manager, 5*time.Second) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the WiFi interface") |
| } |
| if request.Enabled { |
| if err := manager.EnableTechnology(ctx, shill.TechnologyWifi); err != nil { |
| return nil, errors.Wrap(err, "could not enable wifi via shill") |
| } |
| return &empty.Empty{}, nil |
| } |
| if err := manager.DisableTechnology(ctx, shill.TechnologyWifi); err != nil { |
| return nil, errors.Wrap(err, "could not disable wifi via shill") |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // SetDHCPProperties sets DHCP properties in shill and returns the original values. |
| func (s *ShillService) SetDHCPProperties(ctx context.Context, req *wifi.SetDHCPPropertiesRequest) (ret *wifi.SetDHCPPropertiesResponse, retErr error) { |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a Manager object") |
| } |
| prop, err := m.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get Manager's properties") |
| } |
| // We expect that the two properties below could be not there yet. |
| // If that's the case, the default value is set as an empty string. |
| oldHostname, err := prop.GetString(shillconst.DHCPPropertyHostname) |
| if err != nil { |
| oldHostname = "" |
| } |
| oldVendor, err := prop.GetString(shillconst.DHCPPropertyVendorClass) |
| if err != nil { |
| oldVendor = "" |
| } |
| |
| // Revert the DHCP properties if something goes wrong. |
| defer func(ctx context.Context) { |
| if retErr == nil { |
| // No need of restore. |
| return |
| } |
| if err := m.SetProperty(ctx, shillconst.DHCPPropertyHostname, oldHostname); err != nil { |
| testing.ContextLogf(ctx, "Failed to restore DHCP hostname to %q: %v", oldHostname, err) |
| } |
| if err := m.SetProperty(ctx, shillconst.DHCPPropertyVendorClass, oldVendor); err != nil { |
| testing.ContextLogf(ctx, "Failed to restore DHCP vendor class to %q: %v", oldVendor, err) |
| } |
| }(ctx) |
| ctx, cancel := ctxutil.Shorten(ctx, time.Second) |
| defer cancel() |
| |
| hostname := req.Props.Hostname |
| if oldHostname != hostname { |
| if err := m.SetProperty(ctx, shillconst.DHCPPropertyHostname, hostname); err != nil { |
| return nil, errors.Wrapf(err, "failed to set DHCP hostname to %q", hostname) |
| } |
| } |
| vendor := req.Props.VendorClass |
| if oldVendor != vendor { |
| if err := m.SetProperty(ctx, shillconst.DHCPPropertyVendorClass, vendor); err != nil { |
| return nil, errors.Wrapf(err, "failed to set DHCP vendor class to %q", vendor) |
| } |
| } |
| |
| return &wifi.SetDHCPPropertiesResponse{ |
| Props: &wifi.DHCPProperties{ |
| Hostname: oldHostname, |
| VendorClass: oldVendor, |
| }, |
| }, nil |
| } |
| |
| // WaitForBSSID waits for a specific BSSID to be found. |
| func (s *ShillService) WaitForBSSID(ctx context.Context, request *wifi.WaitForBSSIDRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager") |
| } |
| ifaceName, err := shill.WifiInterface(ctx, m, 10*time.Second) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get WiFi interface") |
| } |
| supplicant, err := wpasupplicant.NewSupplicant(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to connect to wpa_supplicant") |
| } |
| iface, err := supplicant.GetInterface(ctx, ifaceName) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the wpa_supplicant's interface's object path") |
| } |
| bssid, err := net.ParseMAC(request.Bssid) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to parse BSSID") |
| } |
| if err := s.waitForBSSID(ctx, iface, request.Ssid, bssid); err != nil { |
| return nil, errors.Wrap(err, "failed to wait for BSSID") |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // EAPAuthSkipped is a streaming gRPC, who watches wpa_supplicant's D-Bus signals until the next connection |
| // completes, and tells that the EAP authentication is skipped (i.e., PMKSA is cached and used) or not. |
| // Note that the method sends an empty response after the signal watcher is initialized. |
| func (s *ShillService) EAPAuthSkipped(_ *empty.Empty, sender wifi.ShillService_EAPAuthSkippedServer) error { |
| ctx, cancel := reserveForReturn(sender.Context()) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to create shill manager") |
| } |
| ifaceName, err := shill.WifiInterface(ctx, m, 10*time.Second) |
| if err != nil { |
| return errors.Wrap(err, "failed to get WiFi interface") |
| } |
| supplicant, err := wpasupplicant.NewSupplicant(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to connect to wpa_supplicant") |
| } |
| iface, err := supplicant.GetInterface(ctx, ifaceName) |
| if err != nil { |
| return errors.Wrap(err, "failed to get interface object paths") |
| } |
| |
| sw, err := iface.DBusObject().CreateWatcher(ctx, wpasupplicant.DBusInterfaceSignalPropertiesChanged, wpasupplicant.DBusInterfaceSignalEAP) |
| if err != nil { |
| return errors.Wrap(err, "failed to create signal watcher") |
| } |
| defer sw.Close(ctx) |
| |
| // Send an empty response to notify that the watcher is ready. |
| if err := sender.Send(&wifi.EAPAuthSkippedResponse{}); err != nil { |
| return errors.Wrap(err, "failed to send a ready signal") |
| } |
| |
| // Watch if there is any EAP signal until the connection completes. |
| skipped := true |
| for { |
| var s *dbus.Signal |
| select { |
| case s = <-sw.Signals: |
| case <-ctx.Done(): |
| return errors.Wrap(ctx.Err(), "failed to wait for signal") |
| } |
| |
| switch name := wpasupplicant.SignalName(s); name { |
| case wpasupplicant.DBusInterfaceSignalEAP: |
| // Any of the EAP signals indicate that wpa_supplicant has started an EAP authentication state machine. |
| skipped = false |
| case wpasupplicant.DBusInterfaceSignalPropertiesChanged: |
| props := s.Body[0].(map[string]dbus.Variant) |
| if val, ok := props["State"]; ok { |
| if state := val.Value().(string); state == wpasupplicant.DBusInterfaceStateCompleted { |
| return sender.Send(&wifi.EAPAuthSkippedResponse{Skipped: skipped}) |
| } |
| } |
| default: |
| return errors.Errorf("unexpected name type: %s", name) |
| } |
| } |
| } |
| |
| // DisconnectReason is a streaming gRPC, who waits for the wpa_supplicant's |
| // DisconnectReason property change, and returns the code to the client. |
| // To notify the caller that it is ready, it sends an empty response after |
| // the signal watcher is initialized. |
| // This is the implementation of wifi.ShillService/DisconnectReason gRPC. |
| func (s *ShillService) DisconnectReason(_ *empty.Empty, sender wifi.ShillService_DisconnectReasonServer) error { |
| ctx, cancel := reserveForReturn(sender.Context()) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to create shill manager") |
| } |
| ifaceName, err := shill.WifiInterface(ctx, m, 10*time.Second) |
| if err != nil { |
| return errors.Wrap(err, "failed to get WiFi interface") |
| } |
| supplicant, err := wpasupplicant.NewSupplicant(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to connect to wpa_supplicant") |
| } |
| iface, err := supplicant.GetInterface(ctx, ifaceName) |
| if err != nil { |
| return errors.Wrap(err, "failed to get interface object paths") |
| } |
| |
| sw, err := iface.DBusObject().CreateWatcher(ctx, wpasupplicant.DBusInterfaceSignalPropertiesChanged) |
| if err != nil { |
| return errors.Wrap(err, "failed to create signal watcher") |
| } |
| defer sw.Close(ctx) |
| |
| // Send an empty response to notify that the watcher is ready. |
| if err := sender.Send(&wifi.DisconnectReasonResponse{}); err != nil { |
| return errors.Wrap(err, "failed to send a ready signal") |
| } |
| |
| for { |
| select { |
| case s := <-sw.Signals: |
| name := wpasupplicant.SignalName(s) |
| if name != wpasupplicant.DBusInterfaceSignalPropertiesChanged { |
| return errors.Errorf("unexpected name type: %s", name) |
| } |
| props := s.Body[0].(map[string]dbus.Variant) |
| if val, ok := props[wpasupplicant.DBusInterfacePropDisconnectReason]; ok { |
| reason := val.Value().(int32) |
| return sender.Send(&wifi.DisconnectReasonResponse{Reason: reason}) |
| } |
| case <-ctx.Done(): |
| return errors.Wrap(ctx.Err(), "failed to wait for signal") |
| } |
| } |
| } |
| |
| // suspend suspends the DUT for wakeUpTimeout. On success, the time of this |
| // suspend is returned. |
| // If checkEarlyWake is true, the call will fail when the suspend time is |
| // shorter than wakeUpTimeout. |
| // TODO(b/171280216): Extract these logics from network component. |
| func suspend(ctx context.Context, wakeUpTimeout time.Duration, checkEarlyWake bool) (time.Duration, error) { |
| const ( |
| powerdDBusSuspendPath = "/usr/bin/powerd_dbus_suspend" |
| rtcPath = "/sys/class/rtc/rtc0/since_epoch" |
| pauseEthernetHookPath = "/run/autotest_pause_ethernet_hook" |
| ) |
| |
| unlock, err := network.LockCheckNetworkHook(ctx) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to lock the check network hook") |
| } |
| defer unlock() |
| |
| rtcTimeSeconds := func() (int, error) { |
| b, err := ioutil.ReadFile(rtcPath) |
| if err != nil { |
| return 0, errors.Wrapf(err, "failed to read the %s", rtcPath) |
| } |
| return strconv.Atoi(strings.TrimSpace(string(b))) |
| } |
| |
| if wakeUpTimeout < 2*time.Second { |
| // May cause DUT not wake from sleep if the suspend time is 1 second. |
| // It happens when the current clock (floating point) is close to the |
| // next integer, as the RTC sysfs interface only accepts integers. |
| // Make sure it is larger than or equal to 2. |
| return 0, errors.Errorf("unexpected wake up timeout: got %s, want >= 2 seconds", wakeUpTimeout) |
| } |
| |
| startRTC, err := rtcTimeSeconds() |
| if err != nil { |
| return 0, err |
| } |
| |
| wakeUpTimeoutSecond := int(wakeUpTimeout.Seconds()) |
| if err := testexec.CommandContext(ctx, powerdDBusSuspendPath, |
| "--delay=0", // By default it delays the start of suspending by a second. |
| fmt.Sprintf("--wakeup_timeout=%d", wakeUpTimeoutSecond), // Ask the powerd_dbus_suspend to spawn a RTC alarm to wake the DUT up after wakeUpTimeoutSecond. |
| fmt.Sprintf("--suspend_for_sec=%d", wakeUpTimeoutSecond), // Request powerd daemon to suspend for wakeUpTimeoutSecond. |
| ).Run(); err != nil { |
| return 0, err |
| } |
| |
| finishRTC, err := rtcTimeSeconds() |
| if err != nil { |
| return 0, err |
| } |
| |
| suspendedInterval := finishRTC - startRTC |
| testing.ContextLogf(ctx, "RTC suspend time: %d", suspendedInterval) |
| |
| if checkEarlyWake && suspendedInterval < wakeUpTimeoutSecond { |
| return 0, errors.Errorf("the DUT wakes up too early, got %d, want %d", suspendedInterval, wakeUpTimeoutSecond) |
| } |
| |
| return time.Duration(suspendedInterval) * time.Second, nil |
| } |
| |
| // SuspendAssertConnect suspends the DUT and waits for connection after resuming. |
| func (s *ShillService) SuspendAssertConnect(ctx context.Context, req *wifi.SuspendAssertConnectRequest) (*wifi.SuspendAssertConnectResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| service, err := shill.NewService(ctx, dbus.ObjectPath(req.ServicePath)) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a new shill service") |
| } |
| pw, err := service.CreateWatcher(ctx) |
| defer pw.Close(ctx) |
| |
| if _, err := suspend(ctx, time.Duration(req.WakeUpTimeout), true /* checkEarlyWake */); err != nil { |
| return nil, errors.Wrap(err, "failed to suspend") |
| } |
| |
| resumeStartTime := time.Now() |
| |
| wCtx, cancel := context.WithTimeout(ctx, time.Second*30) |
| defer cancel() |
| if err := pw.Expect(wCtx, shillconst.ServicePropertyState, shillconst.ServiceStateIdle); err != nil { |
| return nil, errors.Wrap(err, "failed to wait for service to enter idle state") |
| } |
| if err := pw.Expect(wCtx, shillconst.ServicePropertyIsConnected, true); err != nil { |
| return nil, errors.Wrap(err, "failed to wait for connection") |
| } |
| |
| return &wifi.SuspendAssertConnectResponse{ReconnectTime: time.Since(resumeStartTime).Nanoseconds()}, nil |
| } |
| |
| // Suspend suspends the DUT. |
| func (s *ShillService) Suspend(ctx context.Context, req *wifi.SuspendRequest) (*wifi.SuspendResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| interval, err := suspend(ctx, time.Duration(req.WakeUpTimeout), req.CheckEarlyWake) |
| if err != nil { |
| |
| return nil, err |
| } |
| return &wifi.SuspendResponse{ |
| SuspendTime: interval.Nanoseconds(), |
| }, nil |
| } |
| |
| // GetGlobalFTProperty returns the WiFi.GlobalFTEnabled manager property value. |
| func (s *ShillService) GetGlobalFTProperty(ctx context.Context, _ *empty.Empty) (*wifi.GetGlobalFTPropertyResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a shill manager") |
| } |
| |
| props, err := m.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the shill manager properties") |
| } |
| enabled, err := props.GetBool(shillconst.ManagerPropertyGlobalFTEnabled) |
| if err != nil { |
| return nil, err |
| } |
| return &wifi.GetGlobalFTPropertyResponse{Enabled: enabled}, nil |
| } |
| |
| // SetGlobalFTProperty set the WiFi.GlobalFTEnabled manager property value. |
| func (s *ShillService) SetGlobalFTProperty(ctx context.Context, req *wifi.SetGlobalFTPropertyRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a shill manager") |
| } |
| if err := m.SetProperty(ctx, shillconst.ManagerPropertyGlobalFTEnabled, req.Enabled); err != nil { |
| return nil, errors.Wrapf(err, "failed to set the shill manager property %s with value %v", shillconst.ManagerPropertyGlobalFTEnabled, req.Enabled) |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // GetScanAllowRoamProperty returns the WiFi.ScanAllowRoam manager property value. |
| func (s *ShillService) GetScanAllowRoamProperty(ctx context.Context, _ *empty.Empty) (*wifi.GetScanAllowRoamPropertyResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a shill manager") |
| } |
| |
| props, err := m.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the shill manager properties") |
| } |
| allow, err := props.GetBool(shillconst.ManagerPropertyScanAllowRoam) |
| if err != nil { |
| return nil, err |
| } |
| return &wifi.GetScanAllowRoamPropertyResponse{Allow: allow}, nil |
| } |
| |
| // SetScanAllowRoamProperty set the WiFi.ScanAllowRoam manager property value. |
| func (s *ShillService) SetScanAllowRoamProperty(ctx context.Context, req *wifi.SetScanAllowRoamPropertyRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| m, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create a shill manager") |
| } |
| if err := m.SetProperty(ctx, shillconst.ManagerPropertyScanAllowRoam, req.Allow); err != nil { |
| return nil, errors.Wrapf(err, "failed to set the shill manager property %s with value %v", shillconst.ManagerPropertyScanAllowRoam, req.Allow) |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // FlushBSS flushes BSS entries over the specified age from wpa_supplicant's cache. |
| func (s *ShillService) FlushBSS(ctx context.Context, req *wifi.FlushBSSRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| supplicant, err := wpasupplicant.NewSupplicant(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to connect to wpa_supplicant") |
| } |
| iface, err := supplicant.GetInterface(ctx, req.InterfaceName) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get interface object paths") |
| } |
| ageThreshold := uint32(time.Duration(req.Age).Seconds()) |
| if err := iface.FlushBSS(ctx, ageThreshold); err != nil { |
| return nil, errors.Wrap(err, "failed to call FlushBSS method") |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // ResetTest is the main body of the Reset test, which resets/suspends and verifies the connection for several times. |
| func (s *ShillService) ResetTest(ctx context.Context, req *wifi.ResetTestRequest) (*empty.Empty, error) { |
| const ( |
| resetNum = 15 |
| suspendNum = 5 |
| suspendDuration = time.Second * 10 |
| // suspend() may fail to wake up after the specific duration. We give suspend() a new |
| // ctx with timeout suspendDuration + suspendToleranceDuration so that for those |
| // DUTs fail to wake up in time, the test can fail early with correct error message. |
| suspendToleranceDuration = time.Second * 5 |
| idleConnectTimeout = time.Second * 20 |
| pingCount = 10 |
| pingInterval = 1 // In seconds. |
| pingLossThreshold = 20.0 |
| |
| mwifiexFormat = "/sys/kernel/debug/mwifiex/%s/reset" |
| ath10kFormat = "/sys/kernel/debug/ieee80211/%s/ath10k/simulate_fw_crash" |
| // Possible reset paths for Intel wireless NICs are: |
| // 1. /sys/kernel/debug/iwlwifi/{iface}/iwlmvm/fw_restart |
| // Logs look like: iwlwifi 0000:00:0c.0: 0x00000038 | BAD_COMMAND |
| // This also triggers a register dump after the restart. |
| // 2. /sys/kernel/debug/iwlwifi/{iface}/iwlmvm/fw_nmi |
| // Logs look like: iwlwifi 0000:00:0c.0: 0x00000084 | NMI_INTERRUPT_UNKNOWN |
| // This triggers a "hardware restart" once the NMI is processed |
| // 3. /sys/kernel/debug/iwlwifi/{iface}/iwlmvm/fw_dbg_collect |
| // The third one is a mechanism to collect firmware debug dumps, that |
| // effectively causes a restart, but we'll leave it aside for now. |
| iwlwifiFormat = "/sys/kernel/debug/iwlwifi/%s/iwlmvm/fw_restart" |
| mt76Format = "/sys/kernel/debug/ieee80211/%s/mt76/chip_reset" |
| rtw88Format = "/sys/kernel/debug/ieee80211/%s/rtw88/fw_crash" |
| |
| // The path is used to check for wcn399x device. |
| ath10kDeviceFormat = "/sys/class/net/%s/device/of_node/compatible" |
| wcn3990Signature = "qcom,wcn3990-wifi" |
| |
| mwifiexTimeout = time.Second * 20 |
| mwifiexInterval = time.Millisecond * 500 |
| |
| rtw88Timeout = time.Second * 20 |
| rtw88Interval = time.Second |
| ) |
| |
| fileExists := func(file string) bool { |
| _, err := os.Stat(file) |
| return !os.IsNotExist(err) |
| } |
| writeStringToFile := func(file, content string) error { |
| return ioutil.WriteFile(file, []byte(content), 0444) |
| } |
| pingOnce := func(ctx context.Context) error { |
| pingOps := []ping.Option{ |
| ping.Count(pingCount), |
| ping.Interval(pingInterval), |
| } |
| res, err := local_ping.NewLocalRunner().Ping(ctx, req.ServerIp, pingOps...) |
| if err != nil { |
| return errors.Wrap(err, "failed to ping from the DUT") |
| } |
| testing.ContextLogf(ctx, "ping result: %+v", res) |
| if res.Loss > pingLossThreshold { |
| return errors.Errorf("unexpected packet loss percentage: got %g%%, want <= %g%%", res.Loss, pingLossThreshold) |
| } |
| return nil |
| } |
| // Asserts that after f() is called, shill sees the service changes state to Idle then to IsConnected. |
| // The reason to pass f() is because we need to set up a shill property watcher before the f() is called. |
| assertIdleAndConnect := func(ctx context.Context, f func(ctx context.Context) error) error { |
| service, err := shill.NewService(ctx, dbus.ObjectPath(req.ServicePath)) |
| if err != nil { |
| return errors.Wrap(err, "failed to create a shill service") |
| } |
| pw, err := service.CreateWatcher(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to create shill property watcher") |
| } |
| defer pw.Close(ctx) |
| ctx, cancel := ctxutil.Shorten(ctx, time.Second) |
| defer cancel() |
| |
| if err := f(ctx); err != nil { |
| return err |
| } |
| |
| if err := pw.Expect(ctx, shillconst.ServicePropertyState, shillconst.ServiceStateIdle); err != nil { |
| return errors.Wrap(err, "failed to wait for the service enters Idle state") |
| } |
| if err := pw.Expect(ctx, shillconst.ServicePropertyIsConnected, true); err != nil { |
| return errors.Wrap(err, "failed to wait for the service becomes IsConnected") |
| } |
| |
| return nil |
| } |
| |
| // Utils for mwifiex, ath10k, and iwlwifi drivers. *ResetPath return the reset paths in sysfs if they exist. *Reset do the reset once. |
| mwifiexResetPath := func(_ context.Context, iface string) (string, error) { |
| resetPath := fmt.Sprintf(mwifiexFormat, iface) |
| if !fileExists(resetPath) { |
| return "", errors.Errorf("mwifiex reset path %q does not exist", resetPath) |
| } |
| return resetPath, nil |
| } |
| mwifiexReset := func(ctx context.Context, resetPath string) error { |
| ctx, cancel := context.WithTimeout(ctx, mwifiexTimeout) |
| defer cancel() |
| |
| // We aren't guaranteed to receive a disconnect event, but shill will at least notice the adapter went away. |
| if err := assertIdleAndConnect(ctx, func(_ context.Context) error { |
| if err := writeStringToFile(resetPath, "1"); err != nil { |
| return errors.Wrapf(err, "failed to write to the reset path %q", resetPath) |
| } |
| return nil |
| }); err != nil { |
| return err |
| } |
| |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if !fileExists(resetPath) { |
| return errors.Errorf("failed to wait for reset interface file %q to come back", resetPath) |
| } |
| return nil |
| }, &testing.PollOptions{ |
| // Not setting Timeout here as we have shortened the ctx in the beginning of the function. |
| Interval: mwifiexInterval, |
| }) |
| } |
| ath10kResetPath := func(ctx context.Context, iface string) (string, error) { |
| phy, err := network_iface.NewInterface(iface).PhyName(ctx) |
| if err != nil { |
| return "", errors.Wrapf(err, "failed to get the phy name of the WiFi interface (%s)", iface) |
| } |
| resetPath := fmt.Sprintf(ath10kFormat, phy) |
| if !fileExists(resetPath) { |
| return "", errors.Errorf("ath10k reset path %q does not exist", resetPath) |
| } |
| return resetPath, nil |
| } |
| ath10kReset := func(_ context.Context, resetPath string) error { |
| // Simulate ath10k firmware crash. mac80211 handles firmware crashes transparently, so we don't expect a full disconnect/reconnet event. |
| // From ath10k debugfs: |
| // To simulate firmware crash write one of the keywords to this file: |
| // `soft` - This will send WMI_FORCE_FW_HANG_ASSERT to firmware if FW supports that command. |
| // `hard` - This will send to firmware command with illegal parameters causing firmware crash. |
| // `assert` - This will send special illegal parameter to firmware to cause assert failure and crash. |
| // `hw-restart` - This will simply queue hw restart without fw/hw actually crashing. |
| if err := writeStringToFile(resetPath, "soft"); err != nil { |
| return errors.Wrapf(err, "failed to write to the reset path %q", resetPath) |
| } |
| return nil |
| } |
| ath10kWCN3990ResetPath := func(ctx context.Context, iface string) (string, error) { |
| rp, err := ath10kResetPath(ctx, iface) |
| if err != nil { |
| return "", err |
| } |
| b, err := ioutil.ReadFile(fmt.Sprintf(ath10kDeviceFormat, iface)) |
| if err != nil { |
| return "", err |
| } |
| for _, d := range strings.Split(string(b), "\000") { |
| if d == wcn3990Signature { |
| return rp, nil |
| } |
| } |
| return "", errors.New("not a wcn3990 device") |
| } |
| ath10kWCN3990Reset := func(ctx context.Context, resetPath string) error { |
| return assertIdleAndConnect(ctx, func(ctx context.Context) error { |
| return ath10kReset(ctx, resetPath) |
| }) |
| } |
| iwlwifiResetPath := func(ctx context.Context, iface string) (string, error) { |
| par, err := network_iface.NewInterface(iface).ParentDeviceName(ctx) |
| if err != nil { |
| return "", errors.Wrapf(err, "failed to get the parent device name of the WiFi interface (%s)", iface) |
| } |
| resetPath := fmt.Sprintf(iwlwifiFormat, par) |
| if !fileExists(resetPath) { |
| return "", errors.Errorf("iwlwifi reset path %q does not exist", resetPath) |
| } |
| return resetPath, nil |
| } |
| iwlwifiReset := func(_ context.Context, resetPath string) error { |
| // Simulate iwlwifi firmware crash. mac80211 handles firmware crashes transparently, so we don't expect a full disconnect/reconnet event. |
| if err := writeStringToFile(resetPath, "1"); err != nil { |
| return errors.Wrapf(err, "failed to write to the reset path %q", resetPath) |
| } |
| return nil |
| } |
| mt76ResetPath := func(ctx context.Context, iface string) (string, error) { |
| phy, err := network_iface.NewInterface(iface).PhyName(ctx) |
| if err != nil { |
| return "", errors.Wrapf(err, "failed to get the phy name of the WiFi interface (%s)", iface) |
| } |
| resetPath := fmt.Sprintf(mt76Format, phy) |
| if !fileExists(resetPath) { |
| return "", errors.Errorf("mt76 reset path %q does not exist", resetPath) |
| } |
| return resetPath, nil |
| } |
| mt76Reset := func(ctx context.Context, resetPath string) error { |
| return assertIdleAndConnect(ctx, func(ctx context.Context) error { |
| if err := writeStringToFile(resetPath, "1"); err != nil { |
| return errors.Wrapf(err, "failed to write to the reset path %q", resetPath) |
| } |
| return nil |
| }) |
| } |
| rtw88ResetPath := func(ctx context.Context, iface string) (string, error) { |
| phy, err := network_iface.NewInterface(iface).PhyName(ctx) |
| if err != nil { |
| return "", errors.Wrapf(err, "failed to get the phy name of the WiFi interface (%s)", iface) |
| } |
| resetPath := fmt.Sprintf(rtw88Format, phy) |
| if !fileExists(resetPath) { |
| return "", errors.Errorf("mt76 reset path %q does not exist", resetPath) |
| } |
| return resetPath, nil |
| } |
| rtw88Reset := func(ctx context.Context, resetPath string) error { |
| if err := writeStringToFile(resetPath, "1"); err != nil { |
| return errors.Wrapf(err, "failed to write to the reset path %q", resetPath) |
| } |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| raw, err := ioutil.ReadFile(resetPath) |
| if err != nil { |
| return errors.Wrap(err, "failed to read reset status") |
| } |
| resetting := strings.TrimSpace(string(raw)) |
| if resetting != "0" { |
| return errors.New("reset not yet done") |
| } |
| return nil |
| }, &testing.PollOptions{ |
| Interval: rtw88Interval, |
| Timeout: rtw88Timeout, |
| }) |
| } |
| |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| manager, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager proxy") |
| } |
| iface, err := shill.WifiInterface(ctx, manager, 5*time.Second) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the WiFi interface") |
| } |
| |
| // Find the first workable reset path and function by checking the presence of a reset path from the three WiFi module families. |
| var reset func(context.Context, string) error |
| var resetPath string |
| var resetPathErrs []string |
| for _, v := range []struct { |
| reset func(context.Context, string) error |
| resetPath func(context.Context, string) (string, error) |
| }{ |
| {mwifiexReset, mwifiexResetPath}, |
| // WCN3990 belongs to ath10k Wi-Fi family. Evaluate the specific Wi-Fi module detectors first. |
| {ath10kWCN3990Reset, ath10kWCN3990ResetPath}, |
| {ath10kReset, ath10kResetPath}, |
| {iwlwifiReset, iwlwifiResetPath}, |
| {mt76Reset, mt76ResetPath}, |
| {rtw88Reset, rtw88ResetPath}, |
| } { |
| rp, err := v.resetPath(ctx, iface) |
| if err == nil { |
| reset = v.reset |
| resetPath = rp |
| break |
| } |
| // Collect the errors from *ResetPath functions in case we can't find any available reset path. |
| resetPathErrs = append(resetPathErrs, err.Error()) |
| } |
| if reset == nil { |
| return nil, errors.Errorf("no valid reset path, err=%s", strings.Join(resetPathErrs, ", err=")) |
| } |
| |
| testing.ContextLogf(ctx, "Reset WiFi device for %d times then perform suspend/resume test; totally %d rounds; reset path=%s", resetNum, suspendNum, resetPath) |
| |
| for i := 0; i < suspendNum; i++ { |
| testing.ContextLogf(ctx, "Start reseting for %d times", resetNum) |
| for j := 0; j < resetNum; j++ { |
| testing.ContextLogf(ctx, "Reset %d", j+1) |
| if err := reset(ctx, resetPath); err != nil { |
| return nil, errors.Wrap(err, "failed to reset the WiFi interface") |
| } |
| if err := pingOnce(ctx); err != nil { |
| return nil, errors.Wrap(err, "failed to verify connection after reset") |
| } |
| } |
| testing.ContextLogf(ctx, "Finished %d resetings; Start suspending for %s", resetNum, suspendDuration) |
| // Suspend for suspendDuration; resume; then wait for the service enters Idle state and IsConnected in order. |
| if err := func(ctx context.Context) error { |
| ctx, cancel := context.WithTimeout(ctx, suspendDuration+suspendToleranceDuration+idleConnectTimeout) |
| defer cancel() |
| |
| return assertIdleAndConnect(ctx, func(ctx context.Context) error { |
| ctx, cancel := context.WithTimeout(ctx, suspendDuration+suspendToleranceDuration) |
| defer cancel() |
| if _, err := suspend(ctx, suspendDuration, true /* checkEarlyWake */); err != nil { |
| return errors.Wrap(err, "failed to suspend/resume") |
| } |
| return nil |
| }) |
| }(ctx); err != nil { |
| return nil, err |
| } |
| if err := pingOnce(ctx); err != nil { |
| return nil, errors.Wrap(err, "failed to verify connection after suspend") |
| } |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // HealthCheck checks if the DUT has a WiFi device. If not, we may need to reboot the DUT. |
| func (s *ShillService) HealthCheck(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { |
| manager, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager") |
| } |
| _, err = shill.WifiInterface(ctx, manager, 5*time.Second) |
| if err != nil { |
| return nil, errors.Wrap(err, "could not get a WiFi interface") |
| } |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // GetLoggingConfig returns the logging configuration the device currently uses. |
| func (s *ShillService) GetLoggingConfig(ctx context.Context, e *empty.Empty) (*wifi.GetLoggingConfigResponse, error) { |
| manager, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager proxy") |
| } |
| |
| level, err := manager.GetDebugLevel(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the debug level") |
| } |
| |
| tags, err := manager.GetDebugTags(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the debug tags") |
| } |
| |
| return &wifi.GetLoggingConfigResponse{ |
| DebugLevel: int32(level), |
| DebugTags: tags, |
| }, nil |
| } |
| |
| // SetLoggingConfig sets the device logging configuration. |
| func (s *ShillService) SetLoggingConfig(ctx context.Context, req *wifi.SetLoggingConfigRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| manager, err := shill.NewManager(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create shill manager proxy") |
| } |
| |
| if err := manager.SetDebugLevel(ctx, int(req.DebugLevel)); err != nil { |
| return nil, errors.Wrap(err, "failed to set the debug level") |
| } |
| |
| if err := manager.SetDebugTags(ctx, req.DebugTags); err != nil { |
| return nil, errors.Wrap(err, "failed to set the debug tags") |
| } |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // GetWakeOnWifi returns the wake on WiFi related properties of WiFi device. |
| func (s *ShillService) GetWakeOnWifi(ctx context.Context, _ *empty.Empty) (*wifi.GetWakeOnWifiResponse, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| props, err := dev.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the WiFi device properties") |
| } |
| allowed, err := props.GetBool(shillconst.DevicePropertyWakeOnWiFiAllowed) |
| if err != nil { |
| return nil, err |
| } |
| features, err := props.GetString(shillconst.DevicePropertyWakeOnWiFiFeaturesEnabled) |
| if err != nil { |
| return nil, err |
| } |
| netDetectScanPeriod, err := props.GetUint32(shillconst.DevicePropertyNetDetectScanPeriodSeconds) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &wifi.GetWakeOnWifiResponse{ |
| Config: &wifi.WakeOnWifiConfig{ |
| Allowed: allowed, |
| Features: features, |
| NetDetectScanPeriod: netDetectScanPeriod, |
| }, |
| }, nil |
| } |
| |
| // SetWakeOnWifi sets wake on WiFi related property of WiFi device. |
| func (s *ShillService) SetWakeOnWifi(ctx context.Context, req *wifi.SetWakeOnWifiRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| config := req.Config |
| |
| // Currently, we block WoWiFi enablement behind the "allowed" flag. |
| // Check if the setting is valid before further action. |
| if !config.Allowed && config.Features != shillconst.WakeOnWiFiFeaturesNone { |
| return nil, errors.Errorf("WoWiFi not allowed but expected features=%q to be enabled", config.Features) |
| } |
| |
| // Set allowed first. |
| if err := dev.SetProperty(ctx, shillconst.DevicePropertyWakeOnWiFiAllowed, config.Allowed); err != nil { |
| return nil, errors.Wrapf(err, "failed to set WakeOnWiFiAllowed to %t", config.Allowed) |
| } |
| // Only set features when allowed as it should be always "none" when not allowed. |
| if config.Allowed { |
| if err := dev.SetProperty(ctx, shillconst.DevicePropertyWakeOnWiFiFeaturesEnabled, config.Features); err != nil { |
| return nil, errors.Wrapf(err, "failed to set the WakeOnWiFiFeaturesEnabled property to %s", config.Features) |
| } |
| } |
| if err := dev.SetProperty(ctx, shillconst.DevicePropertyNetDetectScanPeriodSeconds, config.NetDetectScanPeriod); err != nil { |
| return nil, errors.Wrapf(err, "failed to set NetDetectScanPeriod to %d seconds", config.NetDetectScanPeriod) |
| } |
| |
| return &empty.Empty{}, nil |
| } |
| |
| // CheckLastWakeReason checks if the last wake reason of WiFi device is as expected. |
| func (s *ShillService) CheckLastWakeReason(ctx context.Context, req *wifi.CheckLastWakeReasonRequest) (*empty.Empty, error) { |
| ctx, cancel := reserveForReturn(ctx) |
| defer cancel() |
| |
| _, dev, err := s.wifiDev(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| props, err := dev.GetProperties(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the WiFi device properties") |
| } |
| reason, err := props.GetString(shillconst.DevicePropertyLastWakeReason) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get LastWakeReason property") |
| } |
| // TODO(b/187362093): The check can race with NL80211 message to shill. Could improve |
| // robustness by waiting for PropertyChanged. |
| if reason != req.Reason { |
| return nil, errors.Wrapf(err, "unexpected LastWakeReason, got %s, want %s", reason, req.Reason) |
| } |
| return &empty.Empty{}, nil |
| } |
| |
| // WatchDarkResume is a streaming gRPC which watchers power manager's D-Bus |
| // signals until next resume (SuspendDone), and returns the count of dark |
| // resumes. |
| // Note that it sends back an empty response first to notify the caller that |
| // the D-Bus watcher is ready. |
| func (s *ShillService) WatchDarkResume(_ *empty.Empty, sender wifi.ShillService_WatchDarkResumeServer) error { |
| ctx, cancel := reserveForReturn(sender.Context()) |
| defer cancel() |
| |
| watcher, err := power.NewSignalWatcher(ctx, power.SignalDarkSuspendImminent, power.SignalSuspendDone) |
| if err != nil { |
| return errors.Wrap(err, "failed to create power manager signal watcher") |
| } |
| defer watcher.Close(ctx) |
| |
| // Send an empty response to notify that the watcher is ready. |
| if err := sender.Send(&wifi.WatchDarkResumeResponse{}); err != nil { |
| return errors.Wrap(err, "failed to send a ready signal") |
| } |
| darkResumeCount := uint32(0) |
| for { |
| var s *dbus.Signal |
| select { |
| case s = <-watcher.Signals: |
| case <-ctx.Done(): |
| return errors.Wrap(ctx.Err(), "failed to wait for signal") |
| } |
| switch signalName := power.SignalName(s); signalName { |
| case power.SignalDarkSuspendImminent: |
| darkResumeCount++ |
| case power.SignalSuspendDone: |
| // DUT resumed, return result. |
| return sender.Send(&wifi.WatchDarkResumeResponse{Count: darkResumeCount}) |
| default: |
| return errors.Errorf("unexpected signal name: %s", signalName) |
| } |
| } |
| } |