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() |