github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  
    25  	"gopkg.in/tomb.v2"
    26  
    27  	"github.com/snapcore/snapd/interfaces/hotplug"
    28  	"github.com/snapcore/snapd/logger"
    29  	"github.com/snapcore/snapd/osutil/udev/netlink"
    30  )
    31  
    32  type Interface interface {
    33  	Connect() error
    34  	Disconnect() 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   chan struct{}
    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": "tty"}},
    99  			{Env: map[string]string{"SUBSYSTEM": "net"}},
   100  			{Env: map[string]string{"SUBSYSTEM": "usb"}}}}
   101  
   102  	m.monitorStop = m.netlinkConn.Monitor(m.netlinkEvents, m.netlinkErrors, filter)
   103  
   104  	return nil
   105  }
   106  
   107  func (m *Monitor) Disconnect() error {
   108  	select {
   109  	case m.monitorStop <- struct{}{}:
   110  	default:
   111  	}
   112  	return m.netlinkConn.Close()
   113  }
   114  
   115  // Run enumerates existing USB devices and starts a new goroutine that
   116  // handles hotplug events (devices added or removed). It returns immediately.
   117  // The goroutine must be stopped by calling Stop() method.
   118  func (m *Monitor) Run() error {
   119  	// Gather devices from udevadm info output (enumeration on startup).
   120  	devices, parseErrors, err := hotplug.EnumerateExistingDevices()
   121  	if err != nil {
   122  		return fmt.Errorf("cannot enumerate existing devices: %s", err)
   123  	}
   124  	m.tomb.Go(func() error {
   125  		for _, perr := range parseErrors {
   126  			logger.Noticef("udev enumeration error: %s", perr)
   127  		}
   128  		for _, dev := range devices {
   129  			devPath := dev.DevicePath()
   130  			if m.seen[devPath] {
   131  				continue
   132  			}
   133  			m.seen[devPath] = true
   134  			if m.deviceAdded != nil {
   135  				m.deviceAdded(dev)
   136  			}
   137  		}
   138  		if m.enumerationDone != nil {
   139  			m.enumerationDone()
   140  		}
   141  
   142  		// Process hotplug events reported by udev monitor.
   143  		for {
   144  			select {
   145  			case err := <-m.netlinkErrors:
   146  				logger.Noticef("udev event error: %s", err)
   147  			case ev := <-m.netlinkEvents:
   148  				m.udevEvent(&ev)
   149  			case <-m.tomb.Dying():
   150  				return m.Disconnect()
   151  			}
   152  		}
   153  	})
   154  
   155  	return nil
   156  }
   157  
   158  func (m *Monitor) Stop() error {
   159  	m.tomb.Kill(nil)
   160  	err := m.tomb.Wait()
   161  	m.netlinkConn = nil
   162  	return err
   163  }
   164  
   165  func (m *Monitor) udevEvent(ev *netlink.UEvent) {
   166  	switch ev.Action {
   167  	case netlink.ADD:
   168  		m.addDevice(ev.KObj, ev.Env)
   169  	case netlink.REMOVE:
   170  		m.removeDevice(ev.KObj, ev.Env)
   171  	default:
   172  	}
   173  }
   174  
   175  func (m *Monitor) addDevice(kobj string, env map[string]string) {
   176  	dev, err := hotplug.NewHotplugDeviceInfo(env)
   177  	if err != nil {
   178  		return
   179  	}
   180  	devPath := dev.DevicePath()
   181  	if m.seen[devPath] {
   182  		return
   183  	}
   184  	m.seen[devPath] = true
   185  	if m.deviceAdded != nil {
   186  		m.deviceAdded(dev)
   187  	}
   188  }
   189  
   190  func (m *Monitor) removeDevice(kobj string, env map[string]string) {
   191  	dev, err := hotplug.NewHotplugDeviceInfo(env)
   192  	if err != nil {
   193  		return
   194  	}
   195  	devPath := dev.DevicePath()
   196  	if !m.seen[devPath] {
   197  		logger.Debugf("udev monitor observed remove event for unknown device %s", dev)
   198  		return
   199  	}
   200  	delete(m.seen, devPath)
   201  	if m.deviceRemoved != nil {
   202  		m.deviceRemoved(dev)
   203  	}
   204  }