class HookingMacroWin32: def __init__(self, log_path="c:\\development\\keymouselog.txt"): ##### # initialize classes self.append_queue = queue.Queue() self.log_path = log_path self.text_thread = TextThread() self.hooking_thread = HookingThread(log_path=log_path, send_queue=self.append_queue) self.replay_thread = ReplayThread(log_path=log_path, send_queue=self.append_queue) ##### # set append thread self.ender = False self.append_thread = threading.Thread(target=self.append_manager, daemon=False, name='AppendThread') # set keyinput thread self.user32 = WinDLL('user32', use_last_error=True) # user32 = windll.user32 self.kernel32 = windll.kernel32 self.keychk_thread = threading.Thread(target=self._keychk_runner, daemon=False, name="KeychkThread") ##### # class flag setting self._ismain = False self.hookstate = {'key': True, 'mouse': False} def start_main(self): if not self.text_thread.is_alive(): # text_window 실행 및 설정 # 창 실행 self.text_thread.start_window( ) # start로 하면 _txwin에 대한 접근오류 날 수 있다. start_window로 실행할 것. # 창 설정 self.text_thread.update_window_attribute( title="Keyboard and Mouse Hooker - python 3.6") # 창 title self.text_thread.update_window_attribute( btn2="Hook") # button2 setting # 버튼 실행 self.text_thread.set_button_action( btn0=lambda: self._push_open_button()) # 버튼0 실행 self.text_thread.set_button_action( btn1=lambda: self._start_replay_thread()) # 버튼1 실행 self.text_thread.set_button_action( btn2=lambda: self._start_hooking_thread()) # 버튼2 실행 self.text_thread.set_button_action( btn3=lambda: self._stop_sub()) # 버튼3 실행 self.text_thread.set_button_action( btn4=lambda: self.stop_main()) # 버튼4 실행 self.text_thread.set_button_action( xbtn=lambda: self.stop_main()) # x 버튼 실행 설정 """ if not self.append_hook_thread.is_alive(): # text_window에 값을 전달하는 append thread self.append_hook_thread.start() if not self.append_replay_thread.is_alive(): # text_window에 값을 전달하는 append thread self.append_replay_thread.start() """ if not self.append_thread.is_alive(): self.append_thread.start() if not self.keychk_thread.is_alive(): # 입력된 키값을 모니터링하는 key_thread 시작 self.keychk_thread.start() self._ismain = True # 시작 메시지 welcome message self.text_thread.append( "--------------------------------------------------------------------\n", " Hello! This is hooking and Replaying Program.\n", " - If you want to REPLAY HOOKED ACTION, press REPLAY button or 'F2'\n", " - If you want to HOOK, press HOOK button or 'F3'\n", " - When you wanna STOP YOUR ACTION, press STOP button or 'ESC'\n", " - If you want open ANOTHER MACRO FILE, press OPEN button or 'F5'\n", " - Change Keyboard and Mouse Hooking State, press 'F6'\n", " - If you need to DISABLE SHOW HOOKED KEY, press 'F7'\n" " (it is usefull to speed up hooking process)\n", " - If you want to press key in DIRECTX GAME,\n" " switch from keyboard event to directX input by press 'F8'\n" "--------------------------------------------------------------------\n", ) self.text_thread.append("default log path : ", self.log_path) self.text_thread.append( " keypress option : ", self.replay_thread.key_input_type.replace('_', ' ')) self.text_thread.append( " show hooked key option : ", 'able' if self.hooking_thread.action_visible else 'disable') def stop_main(self): self.text_thread.destroy() # 창 정지 self.hooking_thread.stop_hookThread() # 후커 정지 self.replay_thread.stop_replay() # 리플레이 정지 self.ender = True # append 정지 self._uninstall_keychk() # keychk uninstall 및 정지 self._ismain = False # main 종료 표시 import os os._exit(1) # append thread를 정상적으로 종료할 수 없어서 사용하는 임시방편 def is_main(self): return self._ismain def _stop_sub(self): if self.hooking_thread.ishookThread(): self.hooking_thread.stop_hookThread() if self.replay_thread.is_replay(): self.replay_thread.stop_replay() return def _start_hooking_thread(self): if self.hooking_thread.ishookThread(): return elif self.replay_thread.is_replay(): self.text_thread.append("Relplayer is alive") else: self.hooking_thread.start_hookThread(log_path=log_path) def _start_replay_thread(self): if self.replay_thread.is_replay(): return elif self.hooking_thread.ishookThread(): self.text_thread.append("Hook is alive") else: self.replay_thread.start_replay(log_path=log_path) def _push_open_button(self): filename: str = self.text_thread.open_file() filename = filename.replace("/", u"\\") if filename == "": pass else: self.log_path = filename self.text_thread.append("set new log_path : ", self.log_path) ############################### def append_manager(self): while True: try: get_queue = self.append_queue.get(block=True, timeout=5) args, kwargs = get_queue # print(*args, **kwargs) if self.text_thread.is_alive(): self.text_thread.append(*args, *kwargs) except queue.Empty as e: print(e.args) ############################### def _keychk_proc(self, nCode, wParam, lParam): # replay중 runner를 종료시키기 위한 프로시져 if wParam is win32con.WM_KEYDOWN: # keydown 처리 if VK_CODE['esc'] == int(lParam[0]): self.text_thread.append("ESC inputed") self._stop_sub() elif VK_CODE['F2'] == int(lParam[0]): # self.text_thread.append("F2 inputed") self._start_replay_thread() elif VK_CODE['F3'] == int(lParam[0]): # self.text_thread.append("F3 inputed") # time.sleep(0.5) # to not press 'f3' keyup. self._start_hooking_thread() elif VK_CODE['F5'] == int(lParam[0]): # self.text_thread.append('F5 inputed') self._push_open_button() elif VK_CODE['F6'] == int(lParam[0]): if not self.hookstate['key']: self.hookstate['key'] = True self.hookstate['mouse'] = True self.text_thread.append('keyboard hooking only') elif not self.hookstate['mouse']: self.hookstate['key'] = False self.hookstate['mouse'] = True self.text_thread.append('mouse hooking only') else: self.hookstate['key'] = True self.hookstate['mouse'] = False self.text_thread.append('both keyboard and mouse hooking') elif VK_CODE['F7'] == int(lParam[0]): # self.text_thread.append("F7 inputed") if self.hooking_thread.action_visible: self.hooking_thread.action_visible = False self.text_thread.append( '"hooked command view" is disabled') else: self.hooking_thread.action_visible = True self.text_thread.append('"hooked command view" is abled') elif VK_CODE['F8'] == int(lParam[0]): if self.replay_thread.key_input_type == 'event_input': self.replay_thread.key_input_type = "directx_input" self.text_thread.append('switch to "DirectX Event" input') else: self.replay_thread.key_input_type = 'event_input' self.text_thread.append('switch to "Keyboard Event" input') # elif wParam is win32con.WM_KEYUP: # keydup 처리 ##### return self.user32.CallNextHookEx(None, nCode, wParam, lParam) def _keychk_runner(self): cmp_func = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p)) pointer = cmp_func(self._keychk_proc) self.hooked = self.user32.SetWindowsHookExA( # 키보드 후커 win32con.WH_KEYBOARD_LL, # 후킹 타입. 13 0xD pointer, # 후킹 프로시저 self.kernel32.GetModuleHandleA(None), # 앞서 지정한 후킹 프로시저가 있는 핸들 0 # thread id. 0일 경우 글로벌로 전체를 후킹한다. ) # hook 인스톨 msg = MSG() b = self.user32.GetMessageW(byref(msg), None, 0, 0) # 메시지 받기 시작. byref가 있어야 에러 발생 안함. def _uninstall_keychk(self): if self.hooked is not None: self.user32.UnhookWindowsHookEx(self.hooked) self.hooked = None if self.keychk_thread.is_alive(): win32gui.PostThreadMessage(self.keychk_thread.ident, win32con.WM_CLOSE, 0, 0) # WM_QUIT 말고 WM_CLOSE로.
class HookingThread: __slots__ = ( 'hooked', 'hooked2', 'user32', 'kernel32', 'hookThread', 'hookThread_name', 'starttime', 'endtime', 'stateget', 'log_path', 'keymouselog', 'send_queue', 'action_visible', ) # 속도상승을 위한 slots 설정 def __init__(self, log_path="C:/development\\KeyMouseLog.txt", send_queue: queue.Queue = None): # 후킹준비하기 self.hooked, self.hooked2 = None, None # 키보드 후킹 핸들러 전달용, 마우스 후킹 핸들러 전달용 self.user32 = WinDLL('user32', use_last_error=True) # user32 = windll.user32 self.kernel32 = windll.kernel32 # 쓰레드 준비하기 self.hookThread, self.hookThread_name = None, None # 로그파일 경로 self.log_path = log_path self.keymouselog = [] self.stateget = [] self.starttime = 0 # 창에 append 전달용 queue self.send_queue = send_queue self.action_visible = True def print1(self, *args, **kwargs): # print("append queue") # print(*args, **kwargs) self.send_queue.put((args, kwargs)) def _getFPTR(self, fn): # 포인터로 만들어줌. CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p)) # CMPFUNC 가 포인터로 만들어주는 함수. return CMPFUNC(fn) # 입력받은 fn을 가리키는 포인터 생성 def _hookProc(self, nCode, wParam, lParam): # 키보드 프로시저, 콜백함수로 후킹에 사용되는 부분 # self.print1("[wParam:", wParam, " lParam[0]:", lParam[0], ",", type(lParam[0]), " : ", nCode, "]") # keyup 이벤트 처리 hooked_key, key_info = "", "" if wParam == win32con.WM_KEYUP: # 257 key_up hooked_key = VK_REVERSE_CODE[lParam[0]] key_info = "keyup" # keydown, syskeydown 이벤트 처리 elif wParam == win32con.WM_KEYDOWN or wParam == win32con.WM_SYSKEYDOWN: # 256 key_up # 260 syskey_down if VK_CODE['esc'] == int(lParam[0]): # esc는 기록하지 않는다. return self.user32.CallNextHookEx(None, nCode, wParam, lParam) hooked_key = VK_REVERSE_CODE[lParam[0]] key_info = "keydown" # keylog 기록 dd = [ 'keyboard', hooked_key, key_info, str(time.time() - self.starttime) ] self.keymouselog.append(dd) if self.action_visible: self.print1(self.keymouselog[-1]) # 창에 가장 최신의 keymouselog 입력정보 출력 return self.user32.CallNextHookEx(self.hooked, nCode, wParam, lParam) def _hookProc2(self, nCode, wParam, lParam): # 마우스 프로시져 # self.print1("nCode:", nCode, " wParam:", wParam, "lParam[0]:", lParam[0]) # MouseButtonDown 처리 및 mouselog 기록 if wParam == win32con.WM_LBUTTONDOWN or \ wParam == win32con.WM_RBUTTONDOWN: # win32con을 따르지 않는다. 작동안함 -> 아님, is와 ==의 차이. dd = [ 'mouse', "LBUTTONDOWN" if wParam == win32con.WM_LBUTTONDOWN else "RBUTTONDOWN", str(lParam[0]) + "|" + str(lParam[1]), str(time.time() - self.starttime) ] # lParam[0], lParam[1] 은 각각 x좌표, y좌표이다. 그 이상은 없음. self.keymouselog.append(dd) if self.action_visible: self.print1( self.keymouselog[-1]) # 창에 가장 최신의 keymouselog 입력정보 출력 # MouseButtonUp 처리 및 mouselog 기록 elif wParam == win32con.WM_LBUTTONUP or \ wParam == win32con.WM_RBUTTONUP: dd = [ 'mouse', "LBUTTONUP" if wParam == win32con.WM_LBUTTONUP else "RBUTTONUP", str(lParam[0]) + "|" + str(lParam[1]), str(time.time() - self.starttime) ] # lParam[0], lParam[1] 은 각각 x좌표, y좌표이다. 그 이상은 없음. self.keymouselog.append(dd) if self.action_visible: self.print1( self.keymouselog[-1]) # 창에 가장 최신의 keymouselog 입력정보 출력 return self.user32.CallNextHookEx(self.hooked2, nCode, wParam, lParam) def _msg_loop(self, keyboard=True, mouse=True): # 쓰레드 대상 if keyboard == True: pointer1 = self._getFPTR(self._hookProc) self.hooked = self.user32.SetWindowsHookExA( # 키보드 후커 win32con.WH_KEYBOARD_LL, # 후킹 타입. 13 0xD pointer1, # 후킹 프로시저 self.kernel32.GetModuleHandleA(None), # 앞서 지정한 후킹 프로시저가 있는 핸들 0 # thread id. 0일 경우 글로벌로 전체를 후킹한다. ) # hook 인스톨 if mouse == True: pointer2 = self._getFPTR(self._hookProc2) self.hooked2 = self.user32.SetWindowsHookExA( # 키보드 후커 win32con.WH_MOUSE_LL, # 후킹 타입. 13 0xD pointer2, # 후킹 프로시저 self.kernel32.GetModuleHandleA(None), # 앞서 지정한 후킹 프로시저가 있는 핸들 0 # thread id. 0일 경우 글로벌로 전체를 후킹한다. ) # hook2 if keyboard == False and mouse == False: self.print1("no hooker installed") return self.print1("hooker installed") msg = MSG() while True: bRet = self.user32.GetMessageW( byref(msg), None, 0, 0) # 메시지 받기 시작. byref가 있어야 에러 발생 안함. if bRet == 1: break if bRet == -1: # print(bRet, " raise WinError(get_last_error())") # raise WinError(get_last_error()) break self.user32.TranslateMessage(byref(msg)) self.user32.DispatchMessageW(byref(msg)) def saveKeyMouseLog(self): with open(self.log_path, 'w', encoding='UTF-8', newline='') as f: keys = ('type', 'input', 'info', 'time') cwrite = csv.DictWriter(f, fieldnames=keys) # DictWriter 사용 # print(stateget) # 마지막 네번째 kor/en_key 값이 이상함. cwrite.writeheader() cwrite.writerow({ 'type': 'start', 'input': 'state_keys', # start log 시작 'info': ''.join(self.stateget), 'time': str(self.starttime - self.starttime) }) for log in self.keymouselog: kk = list(keys) tmpdic = {kk.pop(0): x for x in log} cwrite.writerow(tmpdic) cwrite.writerow({ 'type': 'end', 'input': '', # end log 끝 'info': '', 'time': str(self.endtime - self.starttime) }) self.keymouselog = [] # log 초기화 f.close() # Logging 기록 def start_hookThread(self, log_path=None, keyboard=True, mouse=True): # 훅쓰레드 실행. state_keys = ['caps_lock', 'num_lock', 'scroll_lock', 'kor/en_key'] # state_key 설정, 시작설정 self.stateget = [ str(win32api.GetKeyState(VK_CODE[key])) for key in state_keys ] # 키값 받아 0/1로 저장 self.starttime = time.time() if log_path is not None: self.log_path = log_path if self.hookThread_name is None: # 생성했으면 참, 이미 있으면 거짓 self.hookThread = threading.Thread( target=self._msg_loop, args=(keyboard, mouse), daemon=True, name="HookingKeyboardMouseThread") # 키보드/마우스 동시후킹 self.hookThread.start() # 쓰레드 시작 self.hookThread_name = self.hookThread.ident # 쓰레드 살아있는지 여부 저장 / is_alive는 느릴 수 있음 return True return False def _uninstallHooker(self): self.endtime = time.time() if self.hooked is not None: self.user32.UnhookWindowsHookEx(self.hooked) if self.hooked2 is not None: # 이걸 체크하지 않아서 속도가 느려졌던것. 당연히 체크했어야 했다. self.user32.UnhookWindowsHookEx(self.hooked2) self.hooked, self.hooked2 = None, None if self.ishookThread(): win32gui.PostThreadMessage(self.hookThread.ident, win32con.WM_CLOSE, 0, 0) # WM_QUIT 말고 WM_CLOSE로. self.print1("hooker uninstalled") self.hookThread_name = None # 쓰레드 죽은걸로 갱신 / is_alive보다 빠르게 정보 접근 def stop_hookThread(self): # 클래스내의 메인쓰레드 파괴 if self.ishookThread(): self._uninstallHooker( ) # 언인스톨 -> WM_CLOSE 메시지 전달 -> GetMessage에서 멈춘 thread 진행 및 종료 self.saveKeyMouseLog() # log에 저장 / log는 다 날라가버림 # self.print1("hooker stopped") return True return False def ishookThread(self): # 쓰레드 살아있으면 True/False, 쓰레드 생성 자체가 안됐으면 None if self.hookThread_name is not None: return True return False def get_log_path(self): # 키보드, 마우스 입력 로그 저장 경로 반환 return self.log_path def set_log_path( self, set_log_path): # 키보드, 마우스 입력 로그 저장 경로 설정 / replay에서 읽는 로그경로도 같이 변경 self.log_path = set_log_path def get_stop_hookThread(self): return self.stop_hookThread