~andrewtrusty/context-agent/trunk

1 by Andrew Trusty
Initial import
1
#!/usr/bin/python
2
#
3
# pyxhook -- an extension to emulate some of the PyHook library on linux.
4
#
5
#    Copyright (C) 2008 Tim Alexander <dragonfyre13@gmail.com>
6
#
7
#    This program is free software; you can redistribute it and/or modify
8
#    it under the terms of the GNU General Public License as published by
9
#    the Free Software Foundation; either version 2 of the License, or
10
#    (at your option) any later version.
11
#
12
#    This program is distributed in the hope that it will be useful,
13
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
#    GNU General Public License for more details.
16
#
17
#    You should have received a copy of the GNU General Public License
18
#    along with this program; if not, write to the Free Software
19
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
#
21
#    Thanks to Alex Badea <vamposdecampos@gmail.com> for writing the Record
22
#    demo for the xlib libraries. It helped me immensely working with these
23
#    in this library.
24
#
25
#    Thanks to the python-xlib team. This wouldn't have been possible without
26
#    your code.
27
#
28
#    This requires:
29
#    at least python-xlib 1.4
30
#    xwindows must have the "record" extension present, and active.
31
#
32
#    This file has now been somewhat extensively modified by
33
#    Daniel Folkinshteyn <nanotube@users.sf.net>
34
#    So if there are any bugs, they are probably my fault. :)
35
36
import sys
37
import os
38
import re
39
import time
40
import threading
41
42
import Image
43
44
from Xlib import X, XK, display, error
45
from Xlib.ext import record
46
from Xlib.protocol import rq
47
48
#######################################################################
49
########################START CLASS DEF################################
50
#######################################################################
51
52
class HookManager(threading.Thread):
53
    """This is the main class. Instantiate it, and you can hand it KeyDown and KeyUp (functions in your own code) which execute to parse the pyxhookkeyevent class that is returned.
54
55
    This simply takes these two values for now:
56
    KeyDown = The function to execute when a key is pressed, if it returns anything. It hands the function an argument that is the pyxhookkeyevent class.
57
    KeyUp = The function to execute when a key is released, if it returns anything. It hands the function an argument that is the pyxhookkeyevent class.
58
    """
59
60
    def __init__(self):
61
        threading.Thread.__init__(self)
62
        self.finished = threading.Event()
63
64
        # Give these some initial values
65
        self.mouse_position_x = 0
66
        self.mouse_position_y = 0
67
        self.ison = {"shift":False, "caps":False}
68
69
        # Compile our regex statements.
70
        self.isshift = re.compile('^Shift')
71
        self.iscaps = re.compile('^Caps_Lock')
72
        self.shiftablechar = re.compile('^[a-z0-9]$|^minus$|^equal$|^bracketleft$|^bracketright$|^semicolon$|^backslash$|^apostrophe$|^comma$|^period$|^slash$|^grave$')
73
        self.logrelease = re.compile('.*')
74
        self.isspace = re.compile('^space$')
75
76
        # Assign default function actions (do nothing).
77
        self.KeyDown = lambda x: True
78
        self.KeyUp = lambda x: True
79
        self.MouseAllButtonsDown = lambda x: True
80
        self.MouseAllButtonsUp = lambda x: True
81
82
        self.contextEventMask = set([])#0,2])
83
        # can only have 2 things in it?
84
        # but it fails if only hooking one thing i think..
85
        # its a mask..
86
87
        # Hook to our display.
88
        self.local_dpy = display.Display()
89
        self.record_dpy = display.Display()
90
91
    def run(self):
92
        # Check if the extension is present
93
        if not self.record_dpy.has_extension("RECORD"):
94
            print "RECORD extension not found"
95
            sys.exit(1)
96
        r = self.record_dpy.record_get_version(0, 0)
97
        print "RECORD extension version %d.%d" % (r.major_version, r.minor_version)
98
99
        # Create a recording context; we only want key and mouse events
100
        self.ctx = self.record_dpy.record_create_context(
101
                0,
102
                [record.AllClients],
103
                [{
104
                        'core_requests': (0, 0),
105
                        'core_replies': (0, 0),
106
                        'ext_requests': (0, 0, 0, 0),
107
                        'ext_replies': (0, 0, 0, 0),
108
                        'delivered_events': (0, 0),
109
                        'device_events': tuple(self.contextEventMask), #(X.KeyPress, X.ButtonPress),
110
                        'errors': (0, 0),
111
                        'client_started': False,
112
                        'client_died': False,
113
                }])
114
        from datetime import datetime
115
        fp = open('hooker.log','a')
116
        try:
117
          start = datetime.now()
118
          fp.write("\nRUNNING " + str(datetime.now()))
119
          fp.flush()
120
          # Enable the context; this only returns after a call to record_disable_context,
121
          # while calling the callback function in the meantime
122
          self.record_dpy.record_enable_context(self.ctx, self.processevents)
123
          fp.write("\nRUNNING 2 " + str(datetime.now()))
124
          fp.flush()
125
          # Finally free the context
126
          self.record_dpy.record_free_context(self.ctx)
127
          fp.write("\nI RAN " + str(datetime.now()))
128
          fp.flush()
129
          end = datetime.now()
130
          fp.write("\nI RAN FOR " + str(end - start))
131
          fp.flush()
132
        except Exception, e:
133
          fp.write("\nexception " + str(e) + " at " + str(datetime.now()))
134
          import traceback
135
          fp.write("\n" + traceback.format_exc(e))
136
          fp.flush()
137
        fp.close()
138
139
140
    def cancel(self):
141
        from datetime import datetime
142
        fp = open('hooker.log','a')
143
        fp.write("CANCELING " + str(datetime.now()))
144
        fp.close()
145
        
146
        self.finished.set()
147
        self.local_dpy.record_disable_context(self.ctx)
148
        self.local_dpy.flush()
149
150
    def printevent(self, event):
151
        print event
152
153
    def HookKeyboard(self):
154
        #self.contextEventMask[0] = X.KeyPress
155
        self.contextEventMask.add(X.KeyPress)
156
        self.contextEventMask.add(X.KeyRelease)
157
158
159
    def HookMouse(self):
160
        # need mouse motion to track pointer position, since ButtonPress events
161
        # don't carry that info.
162
        #self.contextEventMask[1] = X.MotionNotify
163
        self.contextEventMask.add(X.MotionNotify)
164
165
    def processevents(self, reply):
166
        if reply.category != record.FromServer:
167
            return
168
        if reply.client_swapped:
169
            print "* received swapped protocol data, cowardly ignored"
170
            return
171
        if not len(reply.data) or ord(reply.data[0]) < 2:
172
            # not an event
173
            return
174
        data = reply.data
175
        while len(data):
176
            event, data = rq.EventField(None).parse_binary_value(data, self.record_dpy.display, None, None)
177
            if event.type == X.KeyPress:
178
                hookevent = self.keypressevent(event)
179
                self.KeyDown(hookevent)
180
            elif event.type == X.KeyRelease:
181
                hookevent = self.keyreleaseevent(event)
182
                self.KeyUp(hookevent)
183
            elif event.type == X.ButtonPress:
184
                hookevent = self.buttonpressevent(event)
185
                self.MouseAllButtonsDown(hookevent)
186
            elif event.type == X.ButtonRelease:
187
                hookevent = self.buttonreleaseevent(event)
188
                self.MouseAllButtonsUp(hookevent)
189
            elif event.type == X.MotionNotify:
190
                # use mouse moves to record mouse position, since press and release events
191
                # do not give mouse position info (event.root_x and event.root_y have
192
                # bogus info).
193
                self.mousemoveevent(event)
194
195
        #print "processing events...", event.type
196
197
    def keypressevent(self, event):
198
        matchto = self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0))
199
        if self.shiftablechar.match(self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0))): ## This is a character that can be typed.
200
            if self.ison["shift"] == False:
201
                keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
202
                return self.makekeyhookevent(keysym, event)
203
            else:
204
                keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
205
                return self.makekeyhookevent(keysym, event)
206
        else: ## Not a typable character.
207
            keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
208
            if self.isshift.match(matchto):
209
                self.ison["shift"] = self.ison["shift"] + 1
210
            elif self.iscaps.match(matchto):
211
                if self.ison["caps"] == False:
212
                    self.ison["shift"] = self.ison["shift"] + 1
213
                    self.ison["caps"] = True
214
                if self.ison["caps"] == True:
215
                    self.ison["shift"] = self.ison["shift"] - 1
216
                    self.ison["caps"] = False
217
            return self.makekeyhookevent(keysym, event)
218
219
    def keyreleaseevent(self, event):
220
        if self.shiftablechar.match(self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0))):
221
            if self.ison["shift"] == False:
222
                keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
223
            else:
224
                keysym = self.local_dpy.keycode_to_keysym(event.detail, 1)
225
        else:
226
            keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
227
        matchto = self.lookup_keysym(keysym)
228
        if self.isshift.match(matchto):
229
            self.ison["shift"] = self.ison["shift"] - 1
230
        return self.makekeyhookevent(keysym, event)
231
232
    def buttonpressevent(self, event):
233
        #self.clickx = self.rootx
234
        #self.clicky = self.rooty
235
        return self.makemousehookevent(event)
236
237
    def buttonreleaseevent(self, event):
238
        #if (self.clickx == self.rootx) and (self.clicky == self.rooty):
239
            ##print "ButtonClick " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty)
240
            #if (event.detail == 1) or (event.detail == 2) or (event.detail == 3):
241
                #self.captureclick()
242
        #else:
243
            #pass
244
245
        return self.makemousehookevent(event)
246
247
        #    sys.stdout.write("ButtonDown " + str(event.detail) + " x=" + str(self.clickx) + " y=" + str(self.clicky) + "\n")
248
        #    sys.stdout.write("ButtonUp " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty) + "\n")
249
        #sys.stdout.flush()
250
251
    def mousemoveevent(self, event):
252
        self.mouse_position_x = event.root_x
253
        self.mouse_position_y = event.root_y
254
255
    # need the following because XK.keysym_to_string() only does printable chars
256
    # rather than being the correct inverse of XK.string_to_keysym()
257
    def lookup_keysym(self, keysym):
258
        for name in dir(XK):
259
            if name.startswith("XK_") and getattr(XK, name) == keysym:
260
                return name.lstrip("XK_")
261
        return "[%d]" % keysym
262
263
    def asciivalue(self, keysym):
264
        asciinum = XK.string_to_keysym(self.lookup_keysym(keysym))
265
        if asciinum < 256:
266
            return asciinum
267
        else:
268
            return 0
269
270
    def makekeyhookevent(self, keysym, event):
271
        storewm = self.xwindowinfo()
272
        if event.type == X.KeyPress:
273
            MessageName = "key down"
274
        elif event.type == X.KeyRelease:
275
            MessageName = "key up"
276
        return pyxhookkeyevent(storewm["handle"], storewm["name"], storewm["class"],
277
                               self.lookup_keysym(keysym), self.asciivalue(keysym),
278
                               False, event.detail, MessageName, storewm["pid"])
279
280
    def makemousehookevent(self, event):
281
        storewm = self.xwindowinfo()
282
        if event.detail == 1:
283
            MessageName = "mouse left "
284
        elif event.detail == 3:
285
            MessageName = "mouse right "
286
        elif event.detail == 2:
287
            MessageName = "mouse middle "
288
        elif event.detail == 5:
289
            MessageName = "mouse wheel down "
290
        elif event.detail == 4:
291
            MessageName = "mouse wheel up "
292
        else:
293
            MessageName = "mouse " + str(event.detail) + " "
294
295
        if event.type == X.ButtonPress:
296
            MessageName = MessageName + "down"
297
        elif event.type == X.ButtonRelease:
298
            MessageName = MessageName + "up"
299
        return pyxhookmouseevent(storewm["handle"], storewm["name"], storewm["class"],
300
                                 (self.mouse_position_x, self.mouse_position_y),
301
                                 MessageName, storewm["pid"])
302
303
    def xwindowinfo(self):
304
        try:
305
            windowvar = self.local_dpy.get_input_focus().focus
306
            wmname = windowvar.get_wm_name()
307
            wmclass = windowvar.get_wm_class()
308
            wmhandle = str(windowvar)[20:30]
309
            try:
310
                wmpid = windowvar.get_full_property(253, 6).value[0] # ADDED by me!!! ----
311
            except:
312
                wmpid = -1
313
        except:
314
            ## This is to keep things running smoothly. It almost never happens, but still...
315
            return {"name":None, "class":None, "handle":None, "pid":None}
316
        if (wmname == None) and (wmclass == None):
317
            try:
318
                windowvar = windowvar.query_tree().parent
319
                wmname = windowvar.get_wm_name()
320
                wmclass = windowvar.get_wm_class()
321
                wmhandle = str(windowvar)[20:30]
322
                wmpid = windowvar.get_full_property(253, 6).value[0] # ADDED by me!!! ----
323
            except:
324
                ## This is to keep things running smoothly. It almost never happens, but still...
325
                return {"name":None, "class":None, "handle":None, "pid":None}
326
        if wmclass == None:
327
            return {"name":wmname, "class":wmclass, "handle":wmhandle, "pid":wmpid}
328
        else:
329
            return {"name":wmname, "class":wmclass[0], "handle":wmhandle, "pid":wmpid}
330
331
class pyxhookkeyevent:
332
    """This is the class that is returned with each key event.f
333
    It simply creates the variables below in the class.
334
335
    Window = The handle of the window.
336
    WindowName = The name of the window.
337
    WindowProcName = The backend process for the window.
338
    Key = The key pressed, shifted to the correct caps value.
339
    Ascii = An ascii representation of the key. It returns 0 if the ascii value is not between 31 and 256.
340
    KeyID = This is just False for now. Under windows, it is the Virtual Key Code, but that's a windows-only thing.
341
    ScanCode = Please don't use this. It differs for pretty much every type of keyboard. X11 abstracts this information anyway.
342
    MessageName = "key down", "key up".
343
    """
344
345
    def __init__(self, Window, WindowName, WindowProcName, Key, Ascii, KeyID, ScanCode, MessageName, pid):
346
        self.Window = Window
347
        self.WindowName = WindowName
348
        self.WindowProcName = WindowProcName
349
        self.Key = Key
350
        self.Ascii = Ascii
351
        self.KeyID = KeyID
352
        self.ScanCode = ScanCode
353
        self.MessageName = MessageName
354
        self.pid = pid
355
356
    def __str__(self):
357
        return "Window Handle: " + str(self.Window) + \
358
            "\nWindow Name: " + str(self.WindowName) + \
359
            "\nWindow's Process Name: " + str(self.WindowProcName) + \
360
            "\nKey Pressed: " + str(self.Key) + \
361
            "\nAscii Value: " + str(self.Ascii) + \
362
            "\nKeyID: " + str(self.KeyID) + \
363
            "\nScanCode: " + str(self.ScanCode) + \
364
            "\nMessageName: " + str(self.MessageName) + \
365
            "\nProcessID: %s" % pid + "\n"
366
367
class pyxhookmouseevent:
368
    """This is the class that is returned with each key event.f
369
    It simply creates the variables below in the class.
370
371
    Window = The handle of the window.
372
    WindowName = The name of the window.
373
    WindowProcName = The backend process for the window.
374
    Position = 2-tuple (x,y) coordinates of the mouse click
375
    MessageName = "mouse left|right|middle down", "mouse left|right|middle up".
376
    """
377
378
    def __init__(self, Window, WindowName, WindowProcName, Position, MessageName, pid):
379
        self.Window = Window
380
        self.WindowName = WindowName
381
        self.WindowProcName = WindowProcName
382
        self.Position = Position
383
        self.MessageName = MessageName
384
        self.pid = pid
385
386
    # TODO: add pid to print out
387
    def __str__(self):
388
        return "Window Handle: " + str(self.Window) + "\nWindow Name: " + str(self.WindowName) + "\nWindow's Process Name: " + str(self.WindowProcName) + "\nPosition: " + str(self.Position) + "\nMessageName: " + str(self.MessageName) + "\n"
389
390
#######################################################################
391
#########################END CLASS DEF#################################
392
#######################################################################
393
394
if __name__ == '__main__':
395
    hm = HookManager()
396
    hm.HookKeyboard()
397
    hm.HookMouse()
398
    hm.KeyDown = hm.printevent
399
    hm.KeyUp = hm.printevent
400
    hm.MouseAllButtonsDown = hm.printevent
401
    hm.MouseAllButtonsUp = hm.printevent
402
    hm.start()
403
    time.sleep(10)
404
    hm.cancel()