diff --git a/README.md b/README.md index eb539ae..20ab413 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ Uinput [![Build Status](https://travis-ci.org/bendahl/uinput.svg?branch=master)](https://travis-ci.org/bendahl/uinput) ====== -This package provides pure go wrapper functions for the LINUX uinput device and therefore allows the creation of virtual input devices in userland. As it stands right now, only virtual keyboards are supported. Support for relative and absolute input devices will be added later on. +This package provides pure go wrapper functions for the LINUX uinput device, which allows to create virtual input devices in userspace. At the moment this package offers a virtual keyboard implementation as well as a virtual mouse device. The keyboard can be used to either send single key presses or hold down a specified key and release it later (useful for building game controllers). The mouse device issues relative positional change events to the x and y asix of the mouse pointer and may also fire click events (left and right click). More functionality will be added in future version. -Please note that you will need to make sure to have the necessary rights to write to uinput. You can either chmod your uinput device, or add a rule in /etc/udev/rules.d to allow your user's group or a dedicated group to write to the device. An example file could be named "99-user.rules" and the line you would need to add for "user", belonging to the group "utest" would be
KERNEL=="uinput", GROUP="utest", MODE:="0660"
Also, make sure to restart in order for these settings to work. Which approach you'll take is up to you, although I would encourage the creation of a udev rule, as it is the clean approach. +Please note that you will need to make sure to have the necessary rights to write to uinput. You can either chmod your uinput device, or add a rule in /etc/udev/rules.d to allow your user's group or a dedicated group to write to the device. +You may use the following two commands to add the necessary rights for you current user to a file called 99-$USER.rules (where $USER is your current user's name): +

+echo KERNEL==\"uinput\", GROUP=\"$USER\", MODE:=\"0660\" | sudo tee /etc/udev/rules.d/99-$USER.rules
+sudo udevadm trigger
+
Installation ------------- @@ -13,7 +18,7 @@ License -------- The package falls under the MIT license. Please see the "LICENSE" file for details. -ToDos/ Open Issues +ToDos ------------------ -The package is currently a work in progress and some more testing will need to be done. Also, as mentioned before, a few features will still need to be implemented as well. To get an idea of the things that are on the current todo list, check out the file "TODO.md". As always, helpful comments and ideas are always welcome. Feel free to do some testing on your own if you're up to it. +Besides mouse and keyboard events it would be great to introduce an input device that allows absolute mouse pointer movements (similar to a touchpad). This still needs to be done. Also, all testing has been done on Ubunu 14.04 and 16.04 x86\_64. Testing for other platforms will need to be done. To get an idea of the things that are on the current todo list, check out the file "TODO.md". As always, helpful comments and ideas are always welcome. Feel free to do some testing on your own if you're up to it. diff --git a/TODO.md b/TODO.md index d03e1dc..4d634c4 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,8 @@ TODO 1. ~~Create Tests for the uinput package~~ 2. ~~Migrate code from C to GO~~ -3. Implement relative input +3. ~~Implement relative input~~ 4. Implement absolute input -5. Test on different platforms (besides x86_64) \ No newline at end of file +5. Test on different platforms (besides x86_64) +6. Implement functions to allow mouse button up and down events (for region selects) +7. Extend test cases \ No newline at end of file diff --git a/uinput.go b/uinput.go index 6446558..fe0417d 100644 --- a/uinput.go +++ b/uinput.go @@ -1,8 +1,8 @@ /* Package uinput provides access to the userland input device driver uinput on linux systems. -For now, only the creation of a virtual keyboard is supported. The keycodes, that are available -and can be used to trigger key press events, are part of this package ("Key1" for number 1, for -example). +Virtual keyboard devices as well as virtual mouse input devices may be created using this package. +The keycodes and other event definitions, that are available and can be used to trigger input events, +are part of this package ("Key1" for number 1, for example). In order to use the virtual keyboard, you will need to follow these three steps: @@ -10,31 +10,66 @@ In order to use the virtual keyboard, you will need to follow these three steps: Example: vk, err := CreateKeyboard("/dev/uinput", "Virtual Keyboard") 2. Send Button events to the device - Example: err = vk.SendKeyPress(uinput.KeyD) - err = vk.SendKeyRelease(uinput.KeyD) + Example (print a single D): + err = vk.KeyPress(uinput.KeyD) + + Example (keep moving right by holding down right arrow key): + err = vk.KeyDown(uinput.KeyRight) + + Example (stop moving right by releasing the right arrow key): + err = vk.KeyUp(uinput.KeyRight) 3. Close the device Example: err = vk.Close() + +A virtual mouse input device is just as easy to create and use: + + 1. Initialize the device: + Example: vm, err := CreateMouse("/dev/uinput", "DangerMouse") + + 2. Move the cursor around and issue click events + Example (move mouse right): + err = vm.MoveRight(42) + + Example (move mouse left): + err = vm.MoveLeft(42) + + Example (move mouse up): + err = vm.MoveUp(42) + + Example (move mouse down): + err = vm.MoveDown(42) + + Example (trigger a left click): + err = vm.LeftClick() + + Example (trigger a right click): + err = vm.RightClick() + */ package uinput import ( - "io" - "os" "errors" "fmt" + "io" + "os" ) // A Keyboard is an key event output device. It is used to // enable a program to simulate HID keyboard input events. type Keyboard interface { - // SendKeyPress will send a keypress event to an existing keyboard device. + // KeyPress will cause the key to be pressed and immediately released. + KeyPress(key int) error + + // KeyDown will send a keypress event to an existing keyboard device. // The key can be any of the predefined keycodes from uinputdefs. - SendKeyPress(key int) error + // Note that the key will be "held down" until "KeyUp" is called. + KeyDown(key int) error - // SendKeyRelease will send a keyrelease event to an existing keyboard device. + // KeyUp will send a keyrelease event to an existing keyboard device. // The key can be any of the predefined keycodes from uinputdefs. - SendKeyRelease(key int) error + KeyUp(key int) error io.Closer } @@ -44,10 +79,26 @@ type vKeyboard struct { deviceFile *os.File } -// An Mouse is a device that will trigger an absolute change event. +// A Mouse is a device that will trigger an absolute change event. // For details see: https://www.kernel.org/doc/Documentation/input/event-codes.txt type Mouse interface { - MoveCursor(x, y int32) error + // MoveLeft will move the mouse cursor left by the given number of pixel. + MoveLeft(pixel int32) error + + // MoveRight will move the mouse cursor right by the given number of pixel. + MoveRight(pixel int32) error + + // MoveUp will move the mouse cursor up by the given number of pixel. + MoveUp(pixel int32) error + + // MoveDown will move the mouse cursor down by the given number of pixel. + MoveDown(pixel int32) error + + // LeftClick will issue a single left click. + LeftClick() error + + // RightClick will issue a right click. + RightClick() error io.Closer } @@ -57,6 +108,8 @@ type vMouse struct { deviceFile *os.File } +// CreateMouse will create a new mouse input device. A mouse is a device that allows relative input. +// Relative input means that all changes to the x and y coordinates of the mouse pointer will be func CreateMouse(path string, name []byte) (Mouse, error) { if path == "" { return nil, errors.New("device path must not be empty") @@ -73,15 +126,67 @@ func CreateMouse(path string, name []byte) (Mouse, error) { return vMouse{name: name, deviceFile: fd}, nil } -// MoveCursor sets the absolute position of the device. Values will have to be within the boundaries specified during -// intialization -func (vAbs vMouse) MoveCursor(x, y int32) error { - return sendRelEvent(vAbs.deviceFile, x, y) +// MoveLeft will move the cursor left by the number of pixel specified. +func (vRel vMouse) MoveLeft(pixel int32) error { + return sendRelEvent(vRel.deviceFile, relX, -pixel) } -// Close closes the device and releases the -func (vAbs vMouse) Close() error { - return closeDevice(vAbs.deviceFile) +// MoveRight will move the cursor right by the number of pixel specified. +func (vRel vMouse) MoveRight(pixel int32) error { + return sendRelEvent(vRel.deviceFile, relX, pixel) +} + +// MoveUp will move the cursor up by the number of pixel specified. +func (vRel vMouse) MoveUp(pixel int32) error { + return sendRelEvent(vRel.deviceFile, relY, -pixel) +} + +// MoveDown will move the cursor down by the number of pixel specified. +func (vRel vMouse) MoveDown(pixel int32) error { + return sendRelEvent(vRel.deviceFile, relY, pixel) +} + +// LeftClick will issue a LeftClick. +func (vRel vMouse) LeftClick() error { + err := sendBtnEvent(vRel.deviceFile, evBtnLeft, btnStatePressed) + if err != nil { + return fmt.Errorf("Failed to issue the LeftClick event: %v", err) + } + + err = sendBtnEvent(vRel.deviceFile, evBtnLeft, btnStateReleased) + if err != nil { + return fmt.Errorf("Failed to issue the KeyUp event: %v", err) + } + + err = syncEvents(vRel.deviceFile) + if err != nil { + return fmt.Errorf("sync to device file failed: %v", err) + } + return nil +} + +// RightClick will issue a RightClick +func (vRel vMouse) RightClick() error { + err := sendBtnEvent(vRel.deviceFile, evBtnRight, btnStatePressed) + if err != nil { + return fmt.Errorf("Failed to issue the RightClick event: %v", err) + } + + err = sendBtnEvent(vRel.deviceFile, evBtnRight, btnStateReleased) + if err != nil { + return fmt.Errorf("Failed to issue the KeyUp event: %v", err) + } + + err = syncEvents(vRel.deviceFile) + if err != nil { + return fmt.Errorf("sync to device file failed: %v", err) + } + return nil +} + +// Close closes the device and releases the device. +func (vRel vMouse) Close() error { + return closeDevice(vRel.deviceFile) } // CreateKeyboard will create a new keyboard using the given uinput @@ -102,18 +207,55 @@ func CreateKeyboard(path string, name []byte) (Keyboard, error) { return vKeyboard{name: name, deviceFile: fd}, nil } -// SendKeyPress will send the key code passed (see uinputdefs.go for available keycodes). Note that unless a key release +// KeyPress will issue a single key press (push down a key and then immediately release it). +func (vk vKeyboard) KeyPress(key int) error { + err := sendBtnEvent(vk.deviceFile, key, btnStatePressed) + if err != nil { + return fmt.Errorf("Failed to issue the KeyDown event: %v", err) + } + + err = sendBtnEvent(vk.deviceFile, key, btnStateReleased) + if err != nil { + return fmt.Errorf("Failed to issue the KeyUp event: %v", err) + } + + err = syncEvents(vk.deviceFile) + if err != nil { + return fmt.Errorf("sync to device file failed: %v", err) + } + return nil +} + +// KeyDown will send the key code passed (see uinputdefs.go for available keycodes). Note that unless a key release // event is sent to the device, the key will remain pressed and therefore input will continuously be generated. Therefore, -// do not forget to call "SendKeyRelease" afterwards. -func (vk vKeyboard) SendKeyPress(key int) error { - return sendBtnEvent(vk.deviceFile, key, btnStatePressed) +// do not forget to call "KeyUp" afterwards. +func (vk vKeyboard) KeyDown(key int) error { + err := sendBtnEvent(vk.deviceFile, key, btnStatePressed) + if err != nil { + return fmt.Errorf("Failed to issue the KeyDown event: %v", err) + } + + err = syncEvents(vk.deviceFile) + if err != nil { + return fmt.Errorf("sync to device file failed: %v", err) + } + return nil } -// SendKeyRelease will release the given key passed as a parameter (see uinputdefs.go for available keycodes). In most -// cases it is recommended to call this function immediately after the "SendKeyPress" function in order to only issue a -// singel key press. -func (vk vKeyboard) SendKeyRelease(key int) error { - return sendBtnEvent(vk.deviceFile, key, btnStateReleased) +// KeyUp will release the given key passed as a parameter (see uinputdefs.go for available keycodes). In most +// cases it is recommended to call this function immediately after the "KeyDown" function in order to only issue a +// single key press. +func (vk vKeyboard) KeyUp(key int) error { + err := sendBtnEvent(vk.deviceFile, key, btnStatePressed) + if err != nil { + return fmt.Errorf("Failed to issue the KeyUp event: %v", err) + } + + err = syncEvents(vk.deviceFile) + if err != nil { + return fmt.Errorf("sync to device file failed: %v", err) + } + return nil } // Close will close the device and free resources. diff --git a/uinput_test.go b/uinput_test.go index 9d1ae15..16ca874 100644 --- a/uinput_test.go +++ b/uinput_test.go @@ -4,23 +4,26 @@ import ( "testing" ) -// This test will create a basic VKeyboard, send a key command and then close the keyboard device +// This test will confirm that all basic key events are working func TestBasicKeyboard(t *testing.T) { vk, err := CreateKeyboard("/dev/uinput", []byte("Test Basic Keyboard")) if err != nil { t.Fatalf("Failed to create the virtual keyboard. Last error was: %s\n", err) } - err = vk.SendKeyPress(Key1) - + err = vk.KeyPress(Key1) if err != nil { - t.Fatalf("Failed to send key event. Last error was: %s\n", err) + t.Fatalf("Failed to send key press. Last error was: %s\n", err) } - err = vk.SendKeyRelease(Key1) + err = vk.KeyDown(Key1) + if err != nil { + t.Fatalf("Failed to send key down event. Last error was: %s\n", err) + } + err = vk.KeyUp(Key1) if err != nil { - t.Fatalf("Failed to send key event. Last error was: %s\n", err) + t.Fatalf("Failed to send key up event. Last error was: %s\n", err) } err = vk.Close() @@ -41,37 +44,44 @@ func TestInvalidDevicePath(t *testing.T) { } } -// This test will confirm that a proper error code is returned if an invalid keycode is -// passed to the library -func TestInvalidKeycode(t *testing.T) { - vk, err := CreateKeyboard("/dev/uinput", []byte("Test Keyboard")) +// This test confirms that all basic mouse events are working as expected. +func TestBasicMouseMoves(t *testing.T) { + relDev, err := CreateMouse("/dev/uinput", []byte("Test Basic Mouse")) if err != nil { - t.Fatalf("Failed to create the virtual keyboard. Last error was: %s\n", err) + t.Fatalf("Failed to create the virtual mouse. Last error was: %s\n", err) } - err = vk.SendKeyPress(4711) - if err == nil { - t.Fatalf("Sending an invalid keycode did not trigger an error.\n") + err = relDev.MoveLeft(100) + if err != nil { + t.Fatalf("Failed to move mouse left. Last error was: %s\n", err) } - vk.Close() -} + err = relDev.MoveRight(150) + if err != nil { + t.Fatalf("Failed to move mouse right. Last error was: %s\n", err) + } -// This test will create a basic Mouse, send an absolute axis event and close the device -func TestBasicMouse(t *testing.T) { - relDev, err := CreateMouse("/dev/uinput", []byte("Test Basic Mouse")) + err = relDev.MoveUp(50) if err != nil { - t.Fatalf("Failed to create the virtual mouse. Last error was: %s\n", err) + t.Fatalf("Failed to move mouse up. Last error was: %s\n", err) + } + + err = relDev.MoveDown(100) + if err != nil { + t.Fatalf("Failed to move mouse down. Last error was: %s\n", err) } - err = relDev.MoveCursor(1000, 100) + err = relDev.RightClick() + if err != nil { + t.Fatalf("Failed to perform right click. Last error was: %s\n", err) + } + err = relDev.LeftClick() if err != nil { - t.Fatalf("Failed to send key event. Last error was: %s\n", err) + t.Fatalf("Failed to perform right click. Last error was: %s\n", err) } err = relDev.Close() - if err != nil { t.Fatalf("Failed to close device. Last error was: %s\n", err) } diff --git a/uinputwrapper.go b/uinputwrapper.go index 7a58178..68d2e41 100644 --- a/uinputwrapper.go +++ b/uinputwrapper.go @@ -1,12 +1,12 @@ package uinput import ( - "os" - "fmt" - "syscall" - "errors" "bytes" "encoding/binary" + "errors" + "fmt" + "os" + "syscall" "time" ) @@ -24,15 +24,17 @@ const ( // input event codes as specified in input-event-codes.h const ( - evSyn = 0x00 - evKey = 0x01 - evRel = 0x02 - evAbs = 0x03 - relX = 0x0 - relY = 0x1 - absX = 0x0 - absY = 0x1 - synReport = 0 + evSyn = 0x00 + evKey = 0x01 + evRel = 0x02 + evAbs = 0x03 + relX = 0x0 + relY = 0x1 + absX = 0x0 + absY = 0x1 + synReport = 0 + evBtnLeft = 0x110 + evBtnRight = 0x111 ) const ( @@ -80,14 +82,14 @@ func releaseDevice(deviceFile *os.File) (err error) { } func createDeviceFile(path string) (fd *os.File, err error) { - deviceFile, err := os.OpenFile(path, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NDELAY, 0666) + deviceFile, err := os.OpenFile(path, syscall.O_WRONLY|syscall.O_NONBLOCK, 0660) if err != nil { return nil, errors.New("could not open device file") } return deviceFile, err } -func registerDevice(deviceFile *os.File, evType uintptr) (error) { +func registerDevice(deviceFile *os.File, evType uintptr) error { err := ioctl(deviceFile, uiSetEvBit, evType) if err != nil { err = releaseDevice(deviceFile) @@ -138,36 +140,54 @@ func toUinputName(name []byte) (uinputName [uinputMaxNameSize]byte) { return fixedSizeName } -func createMouse(path string, name [] byte) (fd *os.File, err error) { +func createMouse(path string, name []byte) (fd *os.File, err error) { deviceFile, err := createDeviceFile(path) if err != nil { - return nil, fmt.Errorf("could not create absolute axis input device: %v", err) + return nil, fmt.Errorf("could not create relative axis input device: %v", err) + } + + err = registerDevice(deviceFile, uintptr(evKey)) + if err != nil { + deviceFile.Close() + return nil, fmt.Errorf("failed to register virtual mouse device: %v", err) + } + // register button events (in order to enable left and right click) + err = ioctl(deviceFile, uiSetKeyBit, uintptr(evBtnLeft)) + if err != nil { + deviceFile.Close() + return nil, fmt.Errorf("failed to register left click event: %v", err) + } + err = ioctl(deviceFile, uiSetKeyBit, uintptr(evBtnRight)) + if err != nil { + deviceFile.Close() + return nil, fmt.Errorf("failed to register right click event: %v", err) } err = registerDevice(deviceFile, uintptr(evRel)) if err != nil { deviceFile.Close() - return nil, fmt.Errorf("failed to register absolute axis input device: %v", err) + return nil, fmt.Errorf("failed to register relative axis input device: %v", err) } // register x and y axis events err = ioctl(deviceFile, uiSetRelBit, uintptr(relX)) if err != nil { deviceFile.Close() - return nil, fmt.Errorf("failed to register absolute x axis events: %v", err) + return nil, fmt.Errorf("failed to register relative x axis events: %v", err) } err = ioctl(deviceFile, uiSetRelBit, uintptr(relY)) if err != nil { deviceFile.Close() - return nil, fmt.Errorf("failed to register absolute y axis events: %v", err) + return nil, fmt.Errorf("failed to register relative y axis events: %v", err) } + return createUsbDevice(deviceFile, uinputUserDev{ - Name: toUinputName(name), + Name: toUinputName(name), ID: inputID{ Bustype: busUsb, Vendor: 0x4711, - Product: 0x0815, + Product: 0x0816, Version: 1}}) } @@ -196,9 +216,6 @@ func createUsbDevice(deviceFile *os.File, dev uinputUserDev) (fd *os.File, err e } func sendBtnEvent(deviceFile *os.File, key int, btnState int) (err error) { - if key < 1 || key > keyMax { - return fmt.Errorf("could not send key event: invalid keycode '%d'", key); - } buf, err := inputEventToBuffer(inputEvent{ Time: syscall.Timeval{0, 0}, Type: evKey, @@ -211,28 +228,26 @@ func sendBtnEvent(deviceFile *os.File, key int, btnState int) (err error) { if err != nil { return fmt.Errorf("writing btnEvent structure to the device file failed: %v", err) } - err = syncEvents(deviceFile) - if err != nil && err != syscall.EINVAL { - return fmt.Errorf("sync to device file failed: %v", err) - } - return nil + return err } -func sendRelEvent(deviceFile *os.File, x, y int32) error { - ievX := inputEvent{ +func sendRelEvent(deviceFile *os.File, eventCode uint16, pixel int32) error { + iev := inputEvent{ Time: syscall.Timeval{0, 0}, Type: evRel, - Code: relX, - Value: x} + Code: eventCode, + Value: pixel} - buf, err := inputEventToBuffer(ievX) + buf, err := inputEventToBuffer(iev) if err != nil { return fmt.Errorf("writing abs event failed: %v", err) } + _, err = deviceFile.Write(buf) if err != nil { return fmt.Errorf("failed to write abs event to device file: %v", err) } + return syncEvents(deviceFile) } @@ -249,7 +264,7 @@ func syncEvents(deviceFile *os.File) (err error) { return err } -func inputEventToBuffer(iev interface{}) (buffer []byte, err error) { +func inputEventToBuffer(iev inputEvent) (buffer []byte, err error) { buf := new(bytes.Buffer) err = binary.Write(buf, binary.LittleEndian, iev) if err != nil {