Created
May 18, 2021 16:46
-
-
Save dgacitua/b25180727500a4e11ecfeb3edc777e12 to your computer and use it in GitHub Desktop.
KeyUp/Keydown logger in Python3 for Linux (writes to CSV file)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# KeyUp/Keydown logger in Python3 (writes to CSV file) | |
# Based on https://blog.robertelder.org/detect-keyup-event-linux-terminal/ | |
import signal | |
import re | |
import struct | |
import select | |
import os | |
import subprocess | |
import csv | |
class MyKeyEventClass3(object): | |
def on_fd_read(self, fd): | |
recv_return = bytearray(os.read(fd, struct.calcsize(self.event_bin_format))) | |
seconds, microseconds, e_type, code, value = struct.unpack(self.event_bin_format, recv_return) | |
full_time = (seconds * 1000) + round(microseconds / 1000) | |
s = self.get_keymap_as_string() | |
with open('keylog.csv', mode='a') as keylog_file: | |
keylog_writer = csv.writer(keylog_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) | |
if e_type == 0x1: # 0x1 == EV_KEY means key press or release. | |
if s: | |
d = ("KeyUp" if value == 0 else "KeyDown") # value == 0 release, value == 1 press | |
keymap = self.parse_keymap_file(s) | |
if keymap is not None: | |
m_action = d | |
m_fd = str(fd) | |
m_time = str(full_time) | |
m_type = str(e_type) | |
m_code = str(code) | |
m_key = str(keymap[code]) | |
keylog_writer.writerow([m_action, m_fd, m_time, m_type, m_code, m_key]) | |
msg = 'ACTION: {action} - FD: {fd} - TIME: {time} - TYPE: {type} - CODE: {code} - KEY: {key}'.format(action = m_action, fd = m_fd, time = m_time, type = m_type, code = m_code, key = m_key) | |
print(msg) | |
else: | |
print("Error while decoding keycode map.") | |
else: | |
print("Unable to obtain keycode map, perhaps you need to use 'sudo'?") | |
def __init__(self): | |
self.event_bin_format = 'llHHI' # See kernel documentation for 'struct input_event' | |
self.done = False | |
signal.signal(signal.SIGINT, self.cleanup) | |
self.poller = select.poll() | |
initial_event_mask = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR | |
with open('/proc/bus/input/devices') as f: | |
devices_file_contents = f.read() | |
files = {} | |
for handlers in re.findall(r"""H: Handlers=([^\n]+)""", devices_file_contents, re.DOTALL): | |
dev_event_file = '/dev/input/event' + re.search(r'event(\d+)', handlers).group(1) | |
if 'kbd' in handlers: | |
try: | |
files[dev_event_file] = open(dev_event_file, 'rb') | |
# Listen for events on this socket: | |
self.poller.register(files[dev_event_file].fileno(), initial_event_mask) | |
print("Listening to " + str(dev_event_file) + " on fd " + str(files[dev_event_file].fileno())) | |
except IOError as e: | |
if e.strerror == 'Permission denied': | |
print("You don't have read permission on ({}). Are you root?".format(dev_event_file)) | |
return | |
while not self.done: | |
try: | |
events = self.poller.poll(None) | |
for fd, flag in events: | |
if flag & (select.POLLIN | select.POLLPRI): | |
self.on_fd_read(fd) | |
if flag & (select.POLLHUP | select.POLLERR): | |
return # Lost the file descriptor | |
except Exception as e: | |
return # Probably interrupted system call | |
def cleanup(self, signum, frame): | |
self.done = True | |
def get_keymap_as_string(self): | |
try: | |
# External call to 'dumpkeys' executable. | |
child = subprocess.Popen('dumpkeys', stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
stdout, stderr = child.communicate() | |
except Exception as e: | |
print("An exception happend when trying to get the keymap data: " + str(e) + ". Note that you need to be root in order for dumpkeys to work.?") | |
return None | |
if child.returncode > 0: | |
print("Return code was " + str(child.returncode) + " when getting keymap data. stderr was " + str(stderr) + "") | |
return None | |
else: | |
return stdout.decode("utf-8") | |
def parse_keymap_file(self, s): | |
m = {} | |
try: | |
for line in s.splitlines(): | |
trimmed_line = line.strip() | |
if re.match(r"^keycode.*", trimmed_line): | |
# Example format of the split format of each line we expect: | |
# ['keycode', '30', '=', '+a'] | |
# Some keymappings will have multiple things they map to, but we just take the first one: | |
# ['keycode', '86', '=', 'less', 'greater', 'bar'] | |
parts = line.split() | |
if(len(parts) >= 4): | |
m[int(parts[1])] = parts[3] | |
return m | |
except Exception as e: | |
print("An exception happend when trying to parse the keymap data: " + str(e) + ".") | |
return None | |
a = MyKeyEventClass3() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment