blob: dd24cdb84fc40da4ffad7f4cccc8818d4477f123 [file] [log] [blame]
// Copyright 2019 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.
// WiFi functions using shill.
package shill
import (
"context"
"encoding/hex"
"strings"
"time"
"chromiumos/tast/common/shillconst"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
const wifiDefaultTimeout = 30 * time.Second
// WifiInterface polls the WiFi interface name with timeout.
// It returns "" with error if no (or more than one) WiFi interface is found.
func WifiInterface(ctx context.Context, m *Manager, timeout time.Duration) (string, error) {
wm, err := NewWifiManager(ctx, m)
if err != nil {
return "", errors.Wrap(err, "failed to create new WifiManager")
}
wm.SetTimeout(timeout)
return wm.Interface(ctx)
}
// WifiManager manages WiFi services, profiles, and devices through shill Manager.
type WifiManager struct {
m *Manager // shill manager
timeout time.Duration
}
// NewWifiManager makes sure WiFi interface is available and returns WiFi Manager.
func NewWifiManager(ctx context.Context, m *Manager) (*WifiManager, error) {
if m == nil {
var err error
m, err = NewManager(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to create shill Manager object")
}
}
return &WifiManager{m: m, timeout: wifiDefaultTimeout}, nil
}
// SetTimeout sets the WiFi manager timeout value for all WiFi operations.
func (wifi *WifiManager) SetTimeout(t time.Duration) {
wifi.timeout = t
}
// Interface returns the WiFi interface name.
func (wifi *WifiManager) Interface(ctx context.Context) (string, error) {
watcher, err := wifi.m.CreateWatcher(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to create a shill manager watcher")
}
defer watcher.Close(ctx)
// Check interface within the default timeout interval.
wCtx, cancel := context.WithTimeout(ctx, wifi.timeout)
defer cancel()
for {
_, props, err := wifi.m.DevicesByTechnology(wCtx, TechnologyWifi)
if err != nil {
return "", errors.Wrap(err, "failed to get WiFi devices")
}
var ifaces []string
for _, p := range props {
if iface, err := p.GetString(shillconst.DevicePropertyInterface); err == nil {
ifaces = append(ifaces, iface)
}
}
// If more than one WiFi interface is found, an error is raised.
// If there's no WiFi interface, probe again when manager's "Devices" property is changed.
if len(ifaces) > 1 {
return "", errors.Errorf("more than one WiFi interface found: %q", ifaces)
} else if len(ifaces) == 1 {
return ifaces[0], nil
}
if _, err := watcher.WaitAll(wCtx, shillconst.ManagerPropertyDevices); err != nil {
return "", errors.Wrap(err, "failed waiting for shill devices to change")
}
}
}
// Enable enables or disables the WiFi network according to the given enable flag.
func (wifi *WifiManager) Enable(ctx context.Context, enable bool) error {
enabled, err := wifi.m.IsEnabled(ctx, TechnologyWifi)
if err != nil {
return errors.Wrap(err, "failed to get WiFi enabled state")
}
if enabled == enable {
return nil
}
watcher, err := wifi.m.CreateWatcher(ctx)
if err != nil {
return errors.Wrap(err, "failed to create a shill manager watcher")
}
defer watcher.Close(ctx)
if enable {
if err := wifi.m.EnableTechnology(ctx, TechnologyWifi); err != nil {
return errors.Wrap(err, "could not enable wifi via shill")
}
} else if err := wifi.m.DisableTechnology(ctx, TechnologyWifi); err != nil {
return errors.Wrap(err, "could not disable wifi via shill")
}
// Check enabled status change within the default timeout interval.
wCtx, cancel := context.WithTimeout(ctx, wifi.timeout)
defer cancel()
for {
enabled, err = wifi.m.IsEnabled(wCtx, TechnologyWifi)
if err != nil {
return errors.Wrap(err, "failed to get WiFi enabled state")
}
if enabled == enable {
return nil
}
if _, err := watcher.WaitAll(wCtx, shillconst.ManagerPropertyEnabledTechnologies); err != nil {
return errors.Wrap(err, "failed waiting for enabled status to change")
}
}
}
// Connected returns true if any WiFi AP is connected.
func (wifi *WifiManager) Connected(ctx context.Context) (bool, error) {
props := map[string]interface{}{
shillconst.ServicePropertyType: shillconst.TypeWifi,
shillconst.ServicePropertyIsConnected: true,
}
if _, err := wifi.m.FindMatchingService(ctx, props); err != nil {
if err.Error() == shillconst.ErrorMatchingServiceNotFound {
return false, nil
}
return false, errors.Wrap(err, "failed to find the WiFi AP")
}
return true, nil
}
// APConnected returns true if the given WiFi AP is connected.
func (wifi *WifiManager) APConnected(ctx context.Context, ssid string) (bool, error) {
props := map[string]interface{}{
shillconst.ServicePropertyType: shillconst.TypeWifi,
shillconst.ServicePropertyWiFiHexSSID: strings.ToUpper(hex.EncodeToString([]byte(ssid))),
shillconst.ServicePropertyVisible: true, // only find visible APs.
}
service, err := wifi.m.FindMatchingService(ctx, props)
if err != nil {
if err.Error() == shillconst.ErrorMatchingServiceNotFound {
return false, nil
}
return false, errors.Wrap(err, "failed to find the WiFi AP")
}
connected, err := service.IsConnected(ctx)
if err != nil {
return false, errors.Wrap(err, "failed to get the WiFi service connected status")
}
return connected, nil
}
// ConnectAP connects to a given WiFi AP identified by SSID.
func (wifi *WifiManager) ConnectAP(ctx context.Context, ssid, passphrase string) error {
props := map[string]interface{}{
shillconst.ServicePropertyType: shillconst.TypeWifi,
shillconst.ServicePropertyWiFiHexSSID: strings.ToUpper(hex.EncodeToString([]byte(ssid))),
shillconst.ServicePropertyVisible: true, // only find visible APs.
}
var service *Service
if err := testing.Poll(ctx, func(ctx context.Context) error {
var err error
service, err = wifi.m.FindMatchingService(ctx, props)
if err == nil {
return nil
}
// Scan WiFi AP again if the expected AP is not found.
if err := wifi.m.RequestScan(ctx, TechnologyWifi); err != nil {
return testing.PollBreak(errors.Wrap(err, "failed to request wifi active scan"))
}
return err
}, &testing.PollOptions{
Timeout: wifi.timeout,
Interval: 200 * time.Millisecond,
}); err != nil {
return errors.Wrap(err, "failed to find the WiFi AP")
}
connected, err := service.IsConnected(ctx)
if err != nil {
return errors.Wrap(err, "failed to get the WiFi service connected status")
}
if connected {
return nil
}
if err := service.SetProperty(ctx, shillconst.ServicePropertyPassphrase, passphrase); err != nil {
return errors.Wrap(err, "failed to set service passphrase")
}
watcher, err := service.CreateWatcher(ctx)
if err != nil {
return errors.Wrap(err, "failed to create service watcher")
}
defer watcher.Close(ctx)
if err := service.Connect(ctx); err != nil {
return errors.Wrap(err, "failed to connect to service")
}
// Check connected state change within the default timeout interval.
wCtx, cancel := context.WithTimeout(ctx, wifi.timeout)
defer cancel()
for {
connected, err = service.IsConnected(wCtx)
if err != nil {
return errors.Wrap(err, "failed to get WiFi connected state")
}
if connected {
return nil
}
if _, err := watcher.WaitAll(wCtx, shillconst.ServicePropertyState); err != nil {
return errors.Wrap(err, "failed waiting for service state to change")
}
}
}
// ForgetAP removes the WiFi AP from user profile.
func (wifi *WifiManager) ForgetAP(ctx context.Context, ssid string) error {
props := map[string]interface{}{
shillconst.ServicePropertyType: shillconst.TypeWifi,
shillconst.ServicePropertyWiFiHexSSID: strings.ToUpper(hex.EncodeToString([]byte(ssid))),
}
service, err := wifi.m.FindMatchingService(ctx, props)
if err != nil {
return errors.Wrap(err, "cannot find the given WiFi AP service")
}
if err := service.Remove(ctx); err != nil {
return errors.Wrap(err, "failed to remove the service")
}
return nil
}