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 }