mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
245 lines
6.3 KiB
Go
245 lines
6.3 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2020 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package triggerwatch
|
|
|
|
import (
|
|
"fmt"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
// TODO:UC20: not packaged, reimplement the minimal things we need?
|
|
evdev "github.com/gvalkov/golang-evdev"
|
|
|
|
"github.com/snapcore/snapd/logger"
|
|
)
|
|
|
|
type keyEvent struct {
|
|
Dev triggerDevice
|
|
Err error
|
|
}
|
|
|
|
type triggerEventFilter struct {
|
|
Key string
|
|
}
|
|
|
|
var (
|
|
strToKey = map[string]int{
|
|
"KEY_ESC": evdev.KEY_ESC,
|
|
"KEY_1": evdev.KEY_1,
|
|
"KEY_2": evdev.KEY_2,
|
|
"KEY_3": evdev.KEY_3,
|
|
"KEY_4": evdev.KEY_4,
|
|
"KEY_5": evdev.KEY_5,
|
|
"KEY_6": evdev.KEY_6,
|
|
"KEY_7": evdev.KEY_7,
|
|
"KEY_8": evdev.KEY_8,
|
|
"KEY_9": evdev.KEY_9,
|
|
"KEY_0": evdev.KEY_0,
|
|
}
|
|
evKeyCapability = evdev.CapabilityType{Type: evdev.EV_KEY, Name: "EV_KEY"}
|
|
|
|
// hold time needed to trigger the event
|
|
holdToTrigger = 2 * time.Second
|
|
)
|
|
|
|
func init() {
|
|
trigger = &evdevInput{}
|
|
}
|
|
|
|
type evdevKeyboardInputDevice struct {
|
|
keyCode uint16
|
|
dev *evdev.InputDevice
|
|
}
|
|
|
|
func (e *evdevKeyboardInputDevice) probeKeyState() (bool, error) {
|
|
// XXX: evdev defines EVIOCGKEY using MAX_NAME_SIZE which is larger than
|
|
// what is needed to store the key bitmap with KEY_MAX bits, but we need
|
|
// to play along since the value is already encoded
|
|
keyBitmap := new([evdev.MAX_NAME_SIZE]byte)
|
|
|
|
// obtain the large bitmap with all key states
|
|
// https://elixir.bootlin.com/linux/v5.5.5/source/drivers/input/evdev.c#L1163
|
|
_, _, err := syscall.RawSyscall(syscall.SYS_IOCTL, e.dev.File.Fd(), uintptr(evdev.EVIOCGKEY), uintptr(unsafe.Pointer(keyBitmap)))
|
|
if err != 0 {
|
|
return false, err
|
|
}
|
|
byteIdx := e.keyCode / 8
|
|
keyMask := byte(1 << (e.keyCode % 8))
|
|
isDown := keyBitmap[byteIdx]&keyMask != 0
|
|
return isDown, nil
|
|
}
|
|
|
|
func (e *evdevKeyboardInputDevice) WaitForTrigger(ch chan keyEvent) {
|
|
logger.Noticef("%s: starting wait, hold %s to trigger", e, holdToTrigger)
|
|
|
|
// XXX: do not mess with setting the key repeat rate, as it's cumbersome
|
|
// and golang-evdev SetRepeatRate() parameter order is actually reversed
|
|
// wrt. what the kernel does. The evdev interprets EVIOCSREP arguments
|
|
// as (delay, repeat)
|
|
// https://elixir.bootlin.com/linux/latest/source/drivers/input/evdev.c#L1072
|
|
// but the wrapper is passing is passing (repeat, delay)
|
|
// https://github.com/gvalkov/golang-evdev/blob/287e62b94bcb850ab42e711bd74b2875da83af2c/device.go#L226-L230
|
|
|
|
keyDown, err := e.probeKeyState()
|
|
if err != nil {
|
|
ch <- keyEvent{Err: fmt.Errorf("cannot obtain initial key state: %v", err), Dev: e}
|
|
}
|
|
if keyDown {
|
|
// looks like the key is pressed initially, we don't know when
|
|
// that happened, but pretend it happened just now
|
|
logger.Noticef("%s: key is already down", e)
|
|
}
|
|
|
|
type evdevEvent struct {
|
|
kev *evdev.KeyEvent
|
|
err error
|
|
}
|
|
|
|
// buffer large enough to collect some events
|
|
evChan := make(chan evdevEvent, 10)
|
|
|
|
monitorKey := func() {
|
|
for {
|
|
ies, err := e.dev.Read()
|
|
if err != nil {
|
|
evChan <- evdevEvent{err: err}
|
|
break
|
|
}
|
|
for _, ie := range ies {
|
|
if ie.Type != evdev.EV_KEY || ie.Code != e.keyCode {
|
|
continue
|
|
}
|
|
kev := evdev.NewKeyEvent(&ie)
|
|
evChan <- evdevEvent{kev: kev}
|
|
}
|
|
}
|
|
close(evChan)
|
|
}
|
|
|
|
go monitorKey()
|
|
|
|
holdTimer := time.NewTimer(holdToTrigger)
|
|
// no sense to keep it running later either
|
|
defer holdTimer.Stop()
|
|
|
|
if !keyDown {
|
|
// key isn't held yet, stop the timer
|
|
holdTimer.Stop()
|
|
}
|
|
|
|
// invariant: tholdTimer is running iff keyDown is true, otherwise is stopped
|
|
Loop:
|
|
for {
|
|
select {
|
|
case ev := <-evChan:
|
|
if ev.err != nil {
|
|
holdTimer.Stop()
|
|
ch <- keyEvent{Err: ev.err, Dev: e}
|
|
break Loop
|
|
}
|
|
kev := ev.kev
|
|
switch kev.State {
|
|
case evdev.KeyDown:
|
|
if keyDown {
|
|
// unexpected, but possible if we missed
|
|
// a key up event right after checking
|
|
// the initial keyboard state when the
|
|
// key was still down
|
|
if !holdTimer.Stop() {
|
|
// drain the channel before the
|
|
// timer gets reset
|
|
<-holdTimer.C
|
|
}
|
|
}
|
|
keyDown = true
|
|
// timer is stopped at this point
|
|
holdTimer.Reset(holdToTrigger)
|
|
logger.Noticef("%s: trigger key down", e)
|
|
case evdev.KeyHold:
|
|
if !keyDown {
|
|
keyDown = true
|
|
// timer is not running yet at this point
|
|
holdTimer.Reset(holdToTrigger)
|
|
logger.Noticef("%s: unexpected hold without down", e)
|
|
}
|
|
case evdev.KeyUp:
|
|
// no need to drain the channel, if it expired,
|
|
// we'll handle it in next iteration
|
|
holdTimer.Stop()
|
|
keyDown = false
|
|
logger.Noticef("%s: trigger key up", e)
|
|
}
|
|
case <-holdTimer.C:
|
|
logger.Noticef("%s: hold complete", e)
|
|
ch <- keyEvent{Dev: e}
|
|
break Loop
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *evdevKeyboardInputDevice) String() string {
|
|
return fmt.Sprintf("%s: %s", e.dev.Phys, e.dev.Name)
|
|
}
|
|
|
|
func (e *evdevKeyboardInputDevice) Close() {
|
|
e.dev.File.Close()
|
|
}
|
|
|
|
type evdevInput struct{}
|
|
|
|
func (e *evdevInput) FindMatchingDevices(filter triggerEventFilter) ([]triggerDevice, error) {
|
|
devices, err := evdev.ListInputDevices()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot list input devices: %v", err)
|
|
}
|
|
|
|
// NOTE: this supports so far only key input devices
|
|
|
|
kc, ok := strToKey[filter.Key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot find a key matching the filter %q", filter.Key)
|
|
}
|
|
cap := evdev.CapabilityCode{Code: kc, Name: filter.Key}
|
|
|
|
match := func(dev *evdev.InputDevice) triggerDevice {
|
|
for _, cc := range dev.Capabilities[evKeyCapability] {
|
|
if cc == cap {
|
|
return &evdevKeyboardInputDevice{
|
|
dev: dev,
|
|
keyCode: uint16(cap.Code),
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// collect all input devices that can emit the trigger key
|
|
var devs []triggerDevice
|
|
for _, dev := range devices {
|
|
idev := match(dev)
|
|
if idev != nil {
|
|
devs = append(devs, idev)
|
|
} else {
|
|
defer dev.File.Close()
|
|
}
|
|
}
|
|
return devs, nil
|
|
}
|