윈도우 환경에서 Python을 사용하여 키보드 후킹을 하는 방법의 이해를 돕기 위한 예제 코드이다.



소스

# -*- coding: utf-8 -*-
#!/usr/bin/python

import sys
from ctypes import *
from ctypes.wintypes import MSG
from ctypes.wintypes import DWORD

user32 = windll.user32
kernel32 = windll.kernel32

WH_KEYBOARD_LL = 13
WM_KEYDOWN = 0x0100
CTRL_CODE = 162

class KeyRogue:
    def __init__(self):
        self.lUser32 = user32
        self.hooked = None

    def installHookProc(self, pointer):
        
        self.hooked = self.lUser32.SetWindowsHookExA(
            WH_KEYBOARD_LL,
            pointer,
            kernel32.GetModuleHandleW(None),
            0
        )
        if not self.hooked:
            return False
        return True

    def uninstallHookProc(self):
        if self.hooked is None:
            return
        self.lUser32.UnhookWindowsHookEx(self.hooked)
        self.hooked = None

    def bytes(self, integer):
        return divmod(integer, 0x10000)
            
    def getFPTR(self, fn):
        CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p))
        return CMPFUNC(fn)

    def startKeyLog(self):
        msg = MSG()
        user32.GetMessageA(byref(msg), 0, 0, 0)

    def getKeyCode(self, lParam):
        high, low = self.bytes(lParam[0])
        return low
   
def hookProc(nCode, wParam, lParam):
    if wParam is not WM_KEYDOWN:
        return user32.CallNextHookEx(keyRogue.hooked, nCode, wParam, lParam)
    keyCode = keyRogue.getKeyCode(lParam)
    hookedKey = chr(keyCode)
    print(hookedKey)
    if (CTRL_CODE == keyCode):
        print("ctrl pressed, call uninstallHook()")
        keyRogue.uninstallHookProc()
        sys.exit(-1)
    return user32.CallNextHookEx(keyRogue.hooked, nCode, wParam, lParam)

keyRogue = KeyRogue()
pointer = keyRogue.getFPTR(hookProc)
if keyRogue.installHookProc(pointer):
    print("install success")
    keyRogue.startKeyLog()
else:
    print("install failed")


문제 해결

  • Python 3.7.5 64비트 버전에서 SetWindowsHookExA 호출 시에 0값이 리턴되면서 Hook 설치가 실패되는 일이 있었는데, 3.7.5 32비트 버전을 설치하니까 해결이 되었다. 그런데 3.6.5 64비트에서는 또 문제가 없고.. 잘 안될 경우는, 버전 혹은 비트 수를 변경해서 시도해볼 필요가 있다.


참고사항

<테스트 환경> - OS : Windows 10 - Python 버전 : 3.6.5(32/64bit), 3.7.5(32bit)


,

.INI 파일에 설정을 읽고 쓰는 것을 간편하게 하기 위한 boilerplate(부뚜막?) 코드



개요


다음 의식의 흐름에 의해 처리된다.

  1. 소스 상의 디폴트 설정값을 적용한다.
  2. .INI 파일이 존재할 경우 .INI 파일 상의 설정값을 적용한다.
  3. 최종 설정값을 .INI 파일에 저장한다.


소스


easyconfig.py
# -*- coding: utf-8 -*-
#!/usr/bin/python

import configparser

class EasyConfig:

    SECTION_DEFAULT = 'DEFAULT'
    configFileName = None

    # 설정 파일명을 세팅함
    def setFileName(self, fileName):
        self.configFileName = fileName

    # 파일로부터 설정을 세팅함
    def loadConfig(self, configParam):
        if not self.configFileName:
            return
        
        configFile = configparser.ConfigParser()
        configFile.read(self.configFileName)
        
        for key in configParam.keys():
            if key in configFile[self.SECTION_DEFAULT]:
                if type(configParam[key]) == int:
                    try:
                        val = int(configFile[self.SECTION_DEFAULT][key])
                        configParam[key] = val
                    except:
                        pass
                elif type(configParam[key]) == float:
                    try:
                        val = float(configFile[self.SECTION_DEFAULT][key])
                        configParam[key] = val
                    except:
                        pass
                elif type(configParam[key]) == bool:
                    if configFile[self.SECTION_DEFAULT][key] == 'True':
                        configParam[key] = True
                    else:
                        configParam[key] = False
                else:
                    configParam[key] = configFile[self.SECTION_DEFAULT][key]

    # 현재 설정을 파일로 저장함
    def saveConfig(self, configParam):
        if not self.configFileName:
            return
        
        configFile = configparser.ConfigParser()
        
        for key in configParam:
            val = None
            if type(configParam[key]) == int:
                val = "%d" % configParam[key]
            elif type(configParam[key]) == float:
                val = "%f" % configParam[key]
            elif type(configParam[key]) == bool:
                if configParam[key]:
                    val = 'True'
                else:
                    val = 'False'
            elif type(configParam[key]) == str:
                val = configParam[key]

            if val:
                configFile[self.SECTION_DEFAULT][key] = val
        
        with open(self.configFileName, 'w') as writeFile:
            configFile.write(writeFile)

    # 현재 설정 내용을 출력함
    def dumpConfig(self, configParam):
        for key in configParam:
            if type(configParam[key]) == int:
                print("%s = %d[int]" % (key, configParam[key]))
            elif type(configParam[key]) == float:
                print("%s = %f[float]" % (key, configParam[key]))
            elif type(configParam[key]) == bool:
                if configParam[key]:
                    print("%s = True[bool]" % key)
                else:
                    print("%s = False[bool]" % key)
            elif type(configParam[key]) == str:
                print("%s = %s[str]" % (key, configParam[key]))

    def process(self, configParam):
        self.loadConfig(configParam)
        self.saveConfig(configParam)

  • DEFAULT 섹션만을 사용한다. 그 외의 섹션을 사용하려면 섹션 존재 여부를 확인하는 추가적인 처리가 필요하다.


사용 방법


client.py
from easyconfig import *

# 설정 디폴트 값을 맵으로 정의한다
configDef = {}
configDef["A"] = "B"
configDef["C"] = "D"
configDef["E"] = 12
configDef["F"] = 3.14
configDef["G"] = True

easyConfig = EasyConfig()
easyConfig.setFileName('ec_config.ini') # 설정 파일명을 정의한다
easyConfig.process(configDef) # 설정 파일을 처리한다

  • int, float, bool, 문자열 타입만 지원한다. 설정 맵 상에 그 외의 타입이 존재할 경우 저장은 되지 않는다.


참고사항

<테스트 환경>
- OS : Windows 10
- Python 버전 : 3.7.5
,
1) ->를 사용하지 않은 코드
(reduce (map (map xs bar) foo) baz)
2) ->를 사용한 코드
(-> xs (map bar) (map foo) (reduce baz))
  • 1번과 같은 중첩된 호출 대신에 2번과 같은 순차적인 호출로 표현할 수 있게 해준다.

  • 앞쪽의 함수의 호출 결과가 그 다음 호출의 첫번째 파라미터로 들어간다. (xs는 변수)


<테스트 환경>
 - OS : Windows 10
 - Leiningen 버전 : 1.0.0
,

표준 라이브러리인 configparser를 사용하면 .INI 파일을 읽고 쓸 수 있다.



.INI 파일 쓰기

import configparser

config = configparser.ConfigParser()
config['AAA'] = {}                # 섹션을 생성한다
config['AAA']['BBB'] = 'CCC'      # 섹션 아래 실제 값을 생성한다
config['DEFAULT']['DDD'] = 'EEE'      # DEFAULT 섹션은 기본적으로 생성되어 있어 생성없이 쓸 수 있다

with open('config.ini', 'w') as configfile:
    config.write(configfile)

  • 1 Depth에 반드시 섹션을 생성한 후에 그 아래 값을 생성할 수 있다. (1 Depth에는 값을 생성할 수 없다)
  • Depth는 최대 2 Depth까지 가능하다. (2 Depth에는 섹션을 생성할 수 없다)


.INI 파일 읽기 (기본)

import configparser

config = configparser.ConfigParser()
config.read('config.ini')

print("config['AAA']['BBB'] : " + config['AAA']['BBB'])


.INI 파일 읽기 (고급)

import configparser

config = configparser.ConfigParser()
config.read('config.ini')

if not 'AAA' in config:           # 섹션이 존재하는지 체크
    print("config['AAA'] not exist")
   
if 'BBB' in config['AAA']:        # 섹션 아래 값이 존재하는지 체크
    print("config['AAA']['BBB'] : " + config['AAA']['BBB'])
else:
    print("config['AAA']['BBB'] not exist")
    
if 'DDD' in config['AAA']:        # 섹션 아래 값이 존재하는지 체크
    print("config['AAA']['DDD'] : " + config['AAA']['DDD'])
else:
    print("config['AAA']['DDD'] not exist")

  • Config 파일이 존재하지 않을 경우는, 에러가 나지 않고 단지 빈 맵으로 반환된다.(DEFAULT 섹션은 존재한다)


참고사항

<테스트 환경>
 - OS : Windows 10
 - Python 버전 : 3.7.5
,

인스턴스 메소드

  • 인스턴스에 대한 참조를 첫번째 파라미터로 받는다.
  • 정의 방법
  • class ClassName1:
      def method1(self, a, b, c):
        ...
    


정적(static) 메소드

  • 필수 파라미터가 없다.
  • 정의 방법
  • class ClassName2:
      @staticmethod
      def method2(a, b, c):
        ...
    


클래스 메소드

  • 클래스 정보 객체에 대한 참조를 첫번째 파라미터로 받는다.
  • 정의 방법
  • class ClassName3:
      @classmethod
      def method3(cls, a, b, c):
        ...
    
<테스트 환경> 
- OS : Windows 10 
- Python 버전 : 3.7.5
,

PATH

  • Emacs에서 M-x를 누른 후, shell을 입력하면, Emacs 내부에서 운영체제 Shell을 이용할 수 있는데, 여기에 적용되는 경로를 설정하는 것이다. (Shell에서 path를 입력하면 나오는 path 환경변수를 변경)
  • 최근의 윈도우 환경에서는 역슬래시/정슬래시 모두 지원하므로 둘다 사용가능하다.
  • path 환경변수에 덧붙여지는 것이므로 ;까지 포함하여 설정하여야 한다.
  • 설정 방법
    # 하나만 설정
    (setenv "PATH" (concat (getenv "PATH") ";/path/to/path1"))
    
    # 여러개를 설정
    (setenv "PATH" (concat (getenv "PATH") ";/path/to/path1;/path/to/path2"))
    


exe-path

  • Emacs에서 어떤 기능을 제공하기 위해 외부 프로그램을 찾는 경로이다. (예를 들면, 철자 검사, 파일 압축, 컴파일, grep, diff 등등)
  • 윈도우 환경에서도 정슬래시를 사용한다.
  • 설정 방법
    # 하나씩 원하는 만큼 추가하는 방식
    (add-to-list 'exec-path "/path/to/exe-path1")
    (add-to-list 'exec-path "/path/to/exe-path1")
    
    # 한꺼번에 추가하는 방식
    (setq exec-path (append '("/path/to/exe-path1" "/path/to/exe-path2") exec-path))
    
<테스트 환경>
- OS : Windows 10
- Emacs 버전 : Emacs 24.5.1 윈도우
,

클래스 변수

  • C++/Java에서 static 멤버 변수와 같은 역할
  • 정의 방법
    class ClassName1:
      var1 = None
      ...
    
  • 사용 방법 : [클래스명].[변수명], [인스턴스명].[변수명] 모두 가능


인스턴스 변수

  • C++/Java에서 비 static 멤버 변수와 같은 역할
  • 정의 방법
    class ClassName2:
      def __init__(self):
        self.var2 = None
        ...
    
  • 사용 방법 : [인스턴스명].[변수명] 만 가능
  • 같은 이름일 경우, 인스턴스 변수에서 먼저 찾고 클래스 변수에서 찾는다


<테스트 환경> 
- OS : Windows 10 
- Python 버전 : 3.7.5
,

Prettier는 javascript(+ 약간의 기타언어)의 소스 코드를 일정한 형식에 맞게 정리해주는 툴로 단독으로도 사용될 수 있지만 여기서는 Emacs에 연동하여 사용하는 방법에 대해 정리해보겠다. 물론 Emacs 외 다른 편집기들도 지원한다. (자세한 사항은 홈페이지 참조)


설치 방법

  1. DiffUtils for Windows를 설치한다. (Emacs에서만 사용시 Path 환경변수는 굳이 잡아줄 필요없다)
  2. Prettier-js for Emacs 홈페이지에서 prettier-js.el 파일을 다운받아 Emacs의 site-lisp 폴더에 복사한다.
  3. .emacs 파일에서 exe-path에 Diff Util의 bin폴더에 대한 경로를 추가한다. (정슬래시 사용)

  4. (setq exec-path (append exec-path '("/path/to/diffutils")))
  5. .emacs 파일에서 Javascript Hook으로 prettier mode를 설정한다.

  6. (require 'prettier-js)
    (dolist (hook '(js2-mode-hook js-mode-hook json-mode-hook))
      (add-hook hook 'prettier-js-mode))
  7. [옵션] 들여쓰기에 탭 문자가 아닌 스페이스를 사용하기 위해서는 아래와 같이 prettier-js-arg를 설정한다. 혹은 추가로 필요한 옵션이 있으면 설정한다. 아래 옵션 페이지 중 CLI용 옵션을 사용하면된다.
    (setq prettier-js-args '(
      "--use-tabs" "false"
      "--tab-width" "4"
    ))
    • Prettier 옵션 페이지 : https://prettier.io/docs/en/options.html
    • 들여쓰기로 탭 문자 사용으로 설정할 경우, 여기서 설정한 탭 사이즈가 아닌 Emacs 기본 탭 사이즈가 적용된다.


동작 방식

  • 일반 다른 모드들과 같이 Tab키를 누르거나 Enter키로 줄바꿈을 하였을 때 조정되는 방식이 아니라, 1) 저장시와 그리고 2) M-x를 누른후 명시적으로 prettier-js를 입력하였을 때 정리가 일어난다. 단순이 들여쓰기만 정리되는 것이 아닌 코딩 스타일 전반에 걸쳐 정리가 된다.
  • 저장시에 적용되는 것은 빼고 명시적으로 커맨드를 입력하였을 때만 적용하려면 .emacs 파일 설정시 아래와 같이 hook 부분은 빼고 설정해도 된다.
    (require 'prettier-js)
    


참고사항


<테스트 환경>
- OS : Windows 10
- Emacs 버전 : Emacs 24.5.1 윈도우
,