diff options
| author | Thibaut Horel <thibaut.horel@gmail.com> | 2014-07-06 17:56:02 -0400 |
|---|---|---|
| committer | Thibaut Horel <thibaut.horel@gmail.com> | 2014-07-06 17:56:02 -0400 |
| commit | d1a77fac18e25df1093172f15fb8925c4545a7b5 (patch) | |
| tree | a5914472db4073d79444e86c05cdf56cfaeb1a31 | |
| download | tracker-d1a77fac18e25df1093172f15fb8925c4545a7b5.tar.gz | |
Initial commit, X events tracking
| -rw-r--r-- | main.py | 36 | ||||
| -rw-r--r-- | models.py | 94 | ||||
| -rw-r--r-- | utils.py | 19 | ||||
| -rw-r--r-- | xlogger.py | 137 |
4 files changed, 286 insertions, 0 deletions
@@ -0,0 +1,36 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +import logging +from time import sleep +from xlogger import XLogger +from utils import SqlHandler +import sys + +engine = create_engine("sqlite:///test.db") +Session = sessionmaker(bind=engine) + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +session = Session() +sql_handler = SqlHandler(session) +file_handler = logging.FileHandler("test.log") +logger.addHandler(file_handler) +logger.addHandler(sql_handler) + + +xlogger = XLogger(logger) +xlogger.start() + +while True: + try: + sleep(1) + except KeyboardInterrupt: + break + +logger.disabled = True +xlogger.stop() +xlogger.join() +sql_handler.close() +file_handler.close() +session.close() +sys.exit(0) diff --git a/models.py b/models.py new file mode 100644 index 0000000..68b3e53 --- /dev/null +++ b/models.py @@ -0,0 +1,94 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, DateTime, String, Boolean +from sqlalchemy.types import TypeDecorator +from datetime import datetime +from Xlib import X +import json + +Base = declarative_base() + +MODIFIERS = { + X.ShiftMask: "Shift", + X.LockMask: "Lock", + X.ControlMask: "Ctrl", + X.Mod1Mask: "Alt", + X.Mod2Mask: "Mod2", + X.Mod3Mask: "Mod3", + X.Mod4Mask: "Super", + X.Mod5Mask: "AltGr" +} + + +class Event: + + id = Column(Integer, primary_key=True) + time = Column(DateTime) + + def __init__(self, **args): + self.time = datetime.utcnow() + for key, value in args.iteritems(): + setattr(self, key, value) + + def to_dict(self): + return {col: getattr(self, col) + for col in self.__table__.columns.keys()} + + def __str__(self): + r = self.to_dict() + del r["id"] + r["time"] = r["time"].isoformat("T") + "Z" + r["type"] = self.__class__.__name__ + return json.dumps(r, ensure_ascii=False) + + +class Modifiers(TypeDecorator): + """ Custom type for lists of key modifiers. + + Lists are serialized/deserialized into/from integers using bitfields. + """ + + impl = Integer + + def process_bind_param(self, value, dialect): + return sum(key for key, v in MODIFIERS.iteritems() if v in value) + + def process_result_value(self, value, dialect): + return [v for key, v in MODIFIERS.iteritems() + if value & key] + + +class KeyEvent(Event, Base): + + __tablename__ = "key_events" + + key = Column(Integer) + key_name = Column(String) + modifiers = Column(Modifiers) + repeat = Column(Boolean) + + +class ClickEvent(Event, Base): + + __tablename__ = "click_events" + + button = Column(Integer) + x = Column(Integer) + y = Column(Integer) + + +class WindowEvent(Event, Base): + + __tablename__ = "window_events" + + window_id = Column(Integer) + name = Column(String) + class1 = Column(String) + class2 = Column(String) + +if __name__ == "__main__": + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + engine = create_engine("sqlite:///test.db") + Session = sessionmaker(bind=engine) + Base.metadata.create_all(engine) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..9fa3476 --- /dev/null +++ b/utils.py @@ -0,0 +1,19 @@ +from logging import Handler + + +class SqlHandler(Handler): + + def __init__(self, session): + Handler.__init__(self) + self.session = session + self.count = 0 + + def emit(self, record): + self.session.add(record.msg) + self.count += 1 + if self.count >= 100: + self.session.commit() + self.count = 0 + + def close(self): + self.session.commit() diff --git a/xlogger.py b/xlogger.py new file mode 100644 index 0000000..64f6237 --- /dev/null +++ b/xlogger.py @@ -0,0 +1,137 @@ +from threading import Thread + +from Xlib import X, XK, display +from Xlib.ext import record +from Xlib.error import BadWindow +from Xlib.protocol import rq + +from models import KeyEvent, ClickEvent, WindowEvent, MODIFIERS + +XK.load_keysym_group("latin2") +XK.load_keysym_group("xkb") +XK.load_keysym_group("xf86") + + +def state_to_idx(state): + s = 0 + if state & X.Mod5Mask: + s += 4 + if state & X.ShiftMask: + s += 1 + return s + + +class XLogger(Thread): + + def __init__(self, logger): + Thread.__init__(self) + self.keysymdict = {getattr(XK, name): name[3:] for name in dir(XK) + if name.startswith("XK_")} + self.display = display.Display() + self.record_dpy = display.Display() + self.keymap = self.display._keymap_codes + self.logger = logger + self.last_id = -1 + self.root = self.display.screen().root + + def run(self): + if not self.record_dpy.has_extension("RECORD"): + print "RECORD extension not found" + return + else: + print "RECORD extension present" + + self.ctx = self.record_dpy.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (X.FocusIn, X.FocusIn), + 'device_events': (X.KeyPress, X.ButtonPress), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + + self.record_dpy.record_enable_context(self.ctx, self.process) + self.record_dpy.record_free_context(self.ctx) + + def stop(self): + self.display.record_disable_context(self.ctx) + self.display.flush() + + def process(self, reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + print "* received swapped protocol data, cowardly ignored" + return + if not len(reply.data) or ord(reply.data[0]) < 2: + return + + data = reply.data + while len(data): + ef = rq.EventField(None) + event, data = ef.parse_binary_value(data, self.record_dpy.display, + None, None) + self.log_event(event) + + def log_event(self, event): + if event.type == X.FocusIn: + p_event = self.window_event(event) + if p_event: + self.logger.info(p_event) + if event.type == X.KeyPress: + self.logger.info(self.key_event(event)) + elif event.type == X.ButtonPress: + self.logger.info(self.button_event(event)) + + def get_key_name(self, keycode, state): + state_idx = state_to_idx(state) + cn = self.keymap[keycode][state_idx] + if cn < 256: + return chr(cn).decode('latin1') + else: + return self.lookup_keysym(cn) + + def key_event(self, event): + modifiers = [v for key, v in MODIFIERS.iteritems() + if key & event.state] + return KeyEvent(key=event.detail, modifiers=modifiers, + key_name=self.get_key_name(event.detail, event.state), + repeat=event.sequence_number == 1) + + def window_event(self, event): + window = self.display.create_resource_object("window", + event.window.id) + try: + class_name = window.get_wm_class() + window_name = window.get_wm_name() + except BadWindow: + return None + if not class_name and not window_name: + if window.id != self.root.id: + window = window.query_tree().parent + class_name = window.get_wm_class() or ('', '') + window_name = window.get_wm_name() or '' + else: + class_name = ('root', 'Root') + window_name = "root" + if window.id != self.last_id: + self.last_id = window.id + window_name = window_name.decode('latin1') + class1, class2 = map(lambda x: x.decode('latin1'), class_name) + return WindowEvent(window_id=window.id, name=window_name, + class1=class1, class2=class2) + + def button_event(self, event): + return ClickEvent(button=event.detail, x=event.root_x, y=event.root_y) + + def lookup_keysym(self, keysym): + if keysym in self.keysymdict: + return self.keysymdict[keysym] + else: + return "[%d]" % keysym |
