github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/overlord/ifacestate/udevmonitor/udevmon.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package udevmonitor
    21  
    22  import (
    23  	"fmt"
    24  	"time"
    25  
    26  	"gopkg.in/tomb.v2"
    27  
    28  	"github.com/snapcore/snapd/interfaces/hotplug"
    29  	"github.com/snapcore/snapd/logger"
    30  	"github.com/snapcore/snapd/osutil/udev/netlink"
    31  )
    32  
    33  type Interface interface {
    34  	Connect() error
    35  	Run() error
    36  	Stop() error
    37  }
    38  
    39  type DeviceAddedFunc func(device *hotplug.HotplugDeviceInfo)
    40  type DeviceRemovedFunc func(device *hotplug.HotplugDeviceInfo)
    41  type EnumerationDoneFunc func()
    42  
    43  // Monitor monitors kernel uevents making it possible to find hotpluggable devices.
    44  type Monitor struct {
    45  	tomb            tomb.Tomb
    46  	deviceAdded     DeviceAddedFunc
    47  	deviceRemoved   DeviceRemovedFunc
    48  	enumerationDone func()
    49  	netlinkConn     *netlink.UEventConn
    50  	// channels used by netlink connection and monitor
    51  	monitorStop   func(stopTimeout time.Duration) bool
    52  	netlinkErrors chan error
    53  	netlinkEvents chan netlink.UEvent
    54  
    55  	// seen keeps track of all observed devices to know when to
    56  	// ignore a spurious event (in case it happens, e.g. when
    57  	// device gets reported by both the enumeration and monitor on
    58  	// startup).  the keys are based on device paths which are
    59  	// guaranteed to be unique and stable till device gets
    60  	// removed.  the lookup is not persisted and gets populated
    61  	// and updated in response to enumeration and hotplug events.
    62  	seen map[string]bool
    63  }
    64  
    65  func New(added DeviceAddedFunc, removed DeviceRemovedFunc, enumerationDone EnumerationDoneFunc) Interface {
    66  	m := &Monitor{
    67  		deviceAdded:     added,
    68  		deviceRemoved:   removed,
    69  		enumerationDone: enumerationDone,
    70  		netlinkConn:     &netlink.UEventConn{},
    71  		seen:            make(map[string]bool),
    72  	}
    73  
    74  	m.netlinkEvents = make(chan netlink.UEvent)
    75  	m.netlinkErrors = make(chan error)
    76  
    77  	return m
    78  }
    79  
    80  func (m *Monitor) EventsChannel() chan netlink.UEvent {
    81  	return m.netlinkEvents
    82  }
    83  
    84  func (m *Monitor) Connect() error {
    85  	if m.netlinkConn == nil || m.netlinkConn.Fd != 0 {
    86  		// this cannot happen in real code but may happen in tests
    87  		return fmt.Errorf("cannot connect: already connected")
    88  	}
    89  
    90  	if err := m.netlinkConn.Connect(netlink.UdevEvent); err != nil {
    91  		return fmt.Errorf("cannot start udev monitor: %s", err)
    92  	}
    93  
    94  	var filter netlink.Matcher
    95  	// TODO: extend with other criteria based on the hotplug interfaces
    96  	filter = &netlink.RuleDefinitions{
    97  		Rules: []netlink.RuleDefinition{
    98  			{Env: map[string]string{"SUBSYSTEM": "net"}},
    99  			{Env: map[string]string{"SUBSYSTEM": "tty"}},
   100  			{Env: map[string]string{"SUBSYSTEM": "usb"}},
   101  		}}
   102  
   103  	m.monitorStop = m.netlinkConn.Monitor(m.netlinkEvents, m.netlinkErrors, filter)
   104  
   105  	return nil
   106  }
   107  
   108  func (m *Monitor) disconnect() error {
   109  	if m.monitorStop != nil {
   110  		if ok := m.monitorStop(5 * time.Second); !ok {
   111  			logger.Noticef("udev monitor stopping timed out")
   112  		}
   113  	}
   114  	return m.netlinkConn.Close()
   115  }
   116  
   117  // Run enumerates existing USB devices and starts a new goroutine that
   118  // handles hotplug events (devices added or removed). It returns immediately.
   119  // The goroutine must be stopped by calling Stop() method.
   120  func (m *Monitor) Run() error {
   121  	// Gather devices from udevadm info output (enumeration on startup).
   122  	devices, parseErrors, err := hotplug.EnumerateExistingDevices()
   123  	if err != nil {
   124  		m.disconnect()
   125  		return fmt.Errorf("cannot enumerate existing devices: %s", err)
   126  	}
   127  	m.tomb.Go(func() error {
   128  		for _, perr := range parseErrors {
   129  			logger.Noticef("udev enumeration error: %s", perr)
   130  		}
   131  		for _, dev := range devices {
   132  			devPath := dev.DevicePath()
   133  			if m.seen[devPath] {
   134  				continue
   135  			}
   136  			m.seen[devPath] = true
   137  			if m.deviceAdded != nil {
   138  				m.deviceAdded(dev)
   139  			}
   140  		}
   141  		if m.enumerationDone != nil {
   142  			m.enumerationDone()
   143  		}
   144  
   145  		// Process hotplug events reported by udev monitor.
   146  		for {
   147  			select {
   148  			case err := <-m.netlinkErrors:
   149  				logger.Noticef("udev event error: %s", err)
   150  			case ev := <-m.netlinkEvents:
   151  				m.udevEvent(&ev)
   152  			case <-m.tomb.Dying():
   153  				return m.disconnect()
   154  			}
   155  		}
   156  	})
   157  
   158  	return nil
   159  }
   160  
   161  func (m *Monitor) Stop() error {
   162  	m.tomb.Kill(nil)
   163  	err := m.tomb.Wait()
   164  	m.netlinkConn = nil
   165  	return err
   166  }
   167  
   168  func (m *Monitor) udevEvent(ev *netlink.UEvent) {
   169  	switch ev.Action {
   170  	case netlink.ADD:
   171  		m.addDevice(ev.KObj, ev.Env)
   172  	case netlink.REMOVE:
   173  		m.removeDevice(ev.KObj, ev.Env)
   174  	default:
   175  	}
   176  }
   177  
   178  func (m *Monitor) addDevice(kobj string, env map[string]string) {
   179  	dev, err := hotplug.NewHotplugDeviceInfo(env)
   180  	if err != nil {
   181  		return
   182  	}
   183  	devPath := dev.DevicePath()
   184  	if m.seen[devPath] {
   185  		return
   186  	}
   187  	m.seen[devPath] = true
   188  	if m.deviceAdded != nil {
   189  		m.deviceAdded(dev)
   190  	}
   191  }
   192  
   193  func (m *Monitor) removeDevice(kobj string, env map[string]string) {
   194  	dev, err := hotplug.NewHotplugDeviceInfo(env)
   195  	if err != nil {
   196  		return
   197  	}
   198  	devPath := dev.DevicePath()
   199  	if !m.seen[devPath] {
   200  		logger.Debugf("udev monitor observed remove event for unknown device %s", dev)
   201  		return
   202  	}
   203  	delete(m.seen, devPath)
   204  	if m.deviceRemoved != nil {
   205  		m.deviceRemoved(dev)
   206  	}
   207  }