Blog


Global Hot Keys in Python for Windows

I used to think I had a basic understanding of Python. I’m not so sure anymore…

globalhotkeys.py

import ctypes
import ctypes.wintypes
import win32con

class GlobalHotKeys(object):
    """
    Register a key using the register() method, or using the @register decorator
    Use listen() to start the message pump
    Example:
    from globalhotkeys import GlobalHotKeys
    @GlobalHotKeys.register(GlobalHotKeys.VK_F1)
    def hello_world():
        print 'Hello World'
    GlobalHotKeys.listen()
    """

    key_mapping = []
    user32 = ctypes.windll.user32

    MOD_ALT = win32con.MOD_ALT
    MOD_CTRL = win32con.MOD_CONTROL
    MOD_CONTROL = win32con.MOD_CONTROL
    MOD_SHIFT = win32con.MOD_SHIFT
    MOD_WIN = win32con.MOD_WIN

    @classmethod
    def register(cls, vk, modifier=0, func=None):
        """
        vk is a windows virtual key code
         - can use ord('X') for A-Z, and 0-1 (note uppercase letter only)
         - or win32con.VK_* constants
         - for full list of VKs see: http://msdn.microsoft.com/en-us/library/dd375731.aspx
        modifier is a win32con.MOD_* constant
        func is the function to run.  If False then break out of the message loop
        """

        # Called as a decorator?
        if func is None:
            def register_decorator(f):
                cls.register(vk, modifier, f)
                return f
            return register_decorator
        else:
            cls.key_mapping.append((vk, modifier, func))

    @classmethod
    def listen(cls):
        """
        Start the message pump
        """

        for index, (vk, modifiers, func) in enumerate(cls.key_mapping):
            if not cls.user32.RegisterHotKey(None, index, modifiers, vk):
                raise Exception('Unable to register hot key: ' + str(vk))

        try:
            msg = ctypes.wintypes.MSG()
            while cls.user32.GetMessageA(ctypes.byref(msg), None, 0, 0) != 0:
                if msg.message == win32con.WM_HOTKEY:
                    (vk, modifiers, func) = cls.key_mapping[msg.wParam]
                    if not func:
                        break
                    func()

                cls.user32.TranslateMessage(ctypes.byref(msg))
                cls.user32.DispatchMessageA(ctypes.byref(msg))

        finally:
            for index, (vk, modifiers, func) in enumerate(cls.key_mapping):
                cls.user32.UnregisterHotKey(None, index)

    @classmethod
    def _include_defined_vks(cls):
        for item in win32con.__dict__:
            item = str(item)
            if item[:3] == 'VK_':
                setattr(cls, item, win32con.__dict__[item])

    @classmethod
    def _include_alpha_numeric_vks(cls):
        for key_code in (range(ord('A'), ord('Z')) + range(ord('0'), ord('9'))):
            setattr(cls, 'VK_' + chr(key_code), key_code)

# Not sure if this is really a good idea or not?
#
# It makes decorators look a little nicer, and the user doesn't have to explicitly use win32con (and we add missing VKs
# for A-Z, 0-9
#
# But there no auto-complete (as it's done at run time), and lint'ers hate it
GlobalHotKeys._include_defined_vks()
GlobalHotKeys._include_alpha_numeric_vks()

globalhotkeys_test.py

from globalhotkeys import GlobalHotKeys

@GlobalHotKeys.register(GlobalHotKeys.VK_F1, GlobalHotKeys.MOD_SHIFT)
def hello_world():
    print "Hello World!"

@GlobalHotKeys.register(GlobalHotKeys.VK_F2)
def hello_world_2():
    print "Hello World again?"

# Q and ctrl will stop message loop
GlobalHotKeys.register(GlobalHotKeys.VK_Q, 0, False)
GlobalHotKeys.register(GlobalHotKeys.VK_C, GlobalHotKeys.MOD_CTRL, False)

# start main loop
GlobalHotKeys.listen()
Link


Phone Fun (Snom300)

"""
Finds all Snom brand phones in a /24 and tells them to key in 6405 turn on
the hands free speaker.  (6405 is an extension that plays the stock monkies
sound from Asterisk)
"""

import urllib2
import socket
import sys

def enumerate_snom_ips(base):
    original_timeout = socket.getdefaulttimeout()
    socket.setdefaulttimeout(0.2)

    ips_to_check = [base + '.' + str(i) for i in range(2, 255)]
    snom_ips = []

    for ip in ips_to_check:
        try:
            data = urllib2.urlopen('http://' + ip + '/index.htm').read()
            if '<TITLE>snom 300</TITLE>' in data:
                snom_ips.append(ip)
        except KeyboardInterrupt:
            sys.exit()
        except:
            pass

    socket.setdefaulttimeout(original_timeout)

    return snom_ips

def send_command(ip, key):
    urllib2.urlopen('http://' + ip + '/command.htm?key=' + key).read()

def monkies(ip):
    for key in ['6', '4', '0', '5', 'SPEAKER']:
        send_command(ip, key)

print 'monkies.py - Finding all Snom phones...'
for ip in enumerate_snom_ips('10.10.12'):
    print ip
    monkies(ip)
Link


Python + matplotlib

After two hours solid of banging my head against a wall trying to get matplotlib do what I wanted, I was able to go from this LibreOffice graph:

To something a little nicer:

It’s still ugly, and confusing, but it’s a lot closer to what I wanted. The IPython environment is a interesting way of working with data too. I will have to play around with it more in the future.

Awful, awful Python code

(Really. Everything below is probably wrong)

%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib.dates import strpdate2num
import matplotlib.dates as mdates
import numpy
import csv

filename = 'c:\\Users\Matthew\\adsl.csv'

rows = ("\t".join(i) for i in csv.reader(open(filename, 'r'), quotechar='"'))
converters = {0: strpdate2num('%Y/%m/%d %H:%M:%S')}
(x, sync_up, sync_down, snr_up, snr_down, attune_up, attune_down) = numpy.genfromtxt(rows, delimiter="\t", skip_header=1, converters=converters, unpack=True)

fig = plt.figure()
fig.set_size_inches(16,8)

ax = fig.add_subplot(111)

# Configure x-ticks
# ax.set_xticks(x) # Tickmark + label at every plotted point
ax.xaxis.set_major_formatter(mdates.DateFormatter('%d/%m/%Y %H:%M'))

ax.plot_date(x, sync_up, '--', c='r', label='Sync(up)') # ls='-', marker='o')
ax.plot_date(x, sync_down, '--', c='g', label='Sync(down)') # ls='-', marker='o')
ax.set_title('ADSL2+ Line Quality')
ax.set_ylabel('Speed (kbps)')
ax.grid(True)

ax.legend(bbox_to_anchor=(1.05, 1), loc=2)

highlight_start = x[212]
highlight_end = x[220]

ax.axvspan(highlight_start, highlight_end, facecolor='red', alpha=0.1)

ax2 = ax.twinx()
ax2.plot_date(x, snr_up,'-', c='r', label='SNR(up)')
ax2.plot_date(x, snr_down, '-', c='g', label='SNR(down)')
ax2.plot_date(x, attune_up, '-', c='c', label='Attenuation(up)')
ax2.plot_date(x, attune_down, '-', c='m', label='Attenuation(down)')
ax2.set_ylabel('Decibel (db)')

ax2.legend(bbox_to_anchor=(1.05, 0.80), loc=2)

fig.autofmt_xdate(rotation=45)

fig.show()
Link


Tyrian Music

Tyrian is a vertical shooter by World Tree Games released in 1995 and published by Epic games. I have fond memories of the music (and game) but was having trouble finding a copy of the soundtrack as MP3s (etc).

Luckily this site has a copy of the original LDS (Loudness Sound System) files and using Foobar2k + AdPlug input component I was able to get them to play (note: the component expects the extension to be be ldsa). While I was at it, I also made a copy of them as MP3 files.

You can listen to the music below, or download a zip file of all the MP3s: tyrianlds-mp3.zip (40MB)

Link


Website downtime

My website went down for around 1:30 to 2 hours today, which was a little odd. After it came back up I noticed the 15min load was > 30. Turns out someone tried to log into WordPress which isn’t that odd. It happens all the time, so much so that I run Limit Login Attempts plugin to apply a 20min timeout a IP address after 4 incorrect attempts (and then a 24 hour timeout after 16 attempts). This time though, it was a kind of a large attack.

In 118 minutes, 2337 different IP addresses tried to log into WordPress. Most were timed-out after 4 attempts, but due to oom-killer sometimes the timeout details weren’t added to the database and more than 4 attempts were made (Two ips made > 26 attempts each, though only 8 IPs made 7 or more attempts). Using a Geo IP database we can quickly get a breakdown of where the IPs came from:

Country Attempts
Russia 1786
Ukraine 1225
Vietnam 909
Thailand 817
Taiwan 613
Romania 380
Turkey 379
Bulgaria 342
Iran 278
Belarus 276
India 241
Poland 164
Serbia 159
Egypt 146
Hungary 137
Brazil 122
United States of America 121
Canada 115
South Africa 115
Other (73 other unique countries) 1762

The user agent for every attempt was “Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/20100101 Firefox/19.0″ which is a valid user agent for FF19 running Windows 7. I’m guessing it was someone’s small botnet. Though the distribution of the countries seems a bit off? Maybe it’s a compromised FF addon? If it continues I may need to switch to a whitelist for logins, or move the admin login page elsewhere as current login limit plugin isn’t really suited to an attack from such a large number of IP addresses.

Code

__author__ = 'Matthew'

import pygeoip

ACCESS_FILENAME = 'access.log'

GEOIP = pygeoip.Database('GeoIP.dat')

ip_breakdown = {}
country_breakdown = {}

for line in open(ACCESS_FILENAME):
    if 'wp-login' not in line:
        continue

    ip = line[0:line.find(' ')]

    if not ip_breakdown.has_key(ip):
        ip_breakdown[ip] = 0

    ip_breakdown[ip] += 1

    ########

    info = GEOIP.lookup(ip)
    if not info.country:
        country = 'unknown'
    else:
        country = info.country

    if not country_breakdown.has_key(country):
        country_breakdown[country] = 0

    country_breakdown[country] += 1

print 'Number ips:', len(ip_breakdown.keys())
print 'Number countries:', len(country_breakdown.keys())

for country in sorted(country_breakdown, key=country_breakdown.get, reverse=True):
    print country, country_breakdown[country]

Output

Number ips: 2337
Number countries: 92
RU 1786
UA 1225
VN 909
TH 817
TW 613
RO 380
TR 379
BG 342
IR 278
BY 276
IN 241
PL 164
RS 159
EG 146
HU 137
BR 122
US 121
CN 115
SA 115
PK 93
HK 91
CZ 90
ID 82
KZ 78
GE 74
GB 73
SK 63
MD 59
DE 57
AE 56
GR 53
AZ 50
IQ 45
ES 44
BD 43
IL 43
CA 39
AR 36
NL 34
CO 34
CL 32
LV 32
MY 30
AT 29
IT 28
KG 22
BE 21
DK 19
JP 18
QA 17
ZA 15
LK 15
AU 15
LT 14
MX 13
MO 12
KH 12
PH 11
NO 11
PS 10
SE 10
MA 9
MN 9
CY 9
AM 9
BA 8
PY 8
FR 8
GH 7
EE 7
YE 7
DZ 7
PT 6
A2 6
OM 4
HR 4
EC 4
NZ 4
LB 4
LA 3
JO 2
PE 2
MK 2
MZ 2
BH 1
AO 1
ET 1
UZ 1
NP 1
CD 1
SY 1
SD 1
Link


MetaBright PHP Challenge

MetaBright is a quiz/community-challenges website focusing on programming and development. I recently found out about a new general PHP section and gave it a go. Overall it was a fun diversion, though a handful of questions were a little odd (like the hash size for RIPEMD vs Whirlpool vs GOST vs MD5). I managed to get to second place on the leader-board, and with a itsy-little bug managed to take first place (of course this is a new section so there isn’t much competition).

Link



Diablo3 Farming + Python

I haven’t played that much Diablo 3 recently, but since the last patch I’ve done a little bit of Demonic Essence farming in Warrior’s Rest. To get an idea of how efficient it is, I wrote a really hack’ish Python script to track some stats and used Tim Golden’s example code for catching global key presses in Windows. The script allowed me to press Shift+F1 or Shift+F2 while inside Diablo 3 to record a time stamp and whether or not I got an essence in the previous run. Note that the code is pretty horrible, but it was a 20 minute project and I only needed something quick and dirty.

Code

import os
import sys
import ctypes
from ctypes import wintypes
import win32con
import time
import datetime
import winsound

byref = ctypes.byref
user32 = ctypes.windll.user32

HOTKEYS = {
    1: (win32con.VK_F3, win32con.MOD_WIN),
    2: (win32con.VK_F4, win32con.MOD_WIN),
    3: (win32con.VK_F1, win32con.MOD_SHIFT),
    4: (win32con.VK_F2, win32con.MOD_SHIFT)
}

def handle_win_f3():
    os.startfile(os.environ['TEMP'])

def handle_win_f4():
    user32.PostQuitMessage(0)

################

LAST_TIMESTAMP = False
RUNS = 0
ESSENCES = 0

def handle_shift_f1():
    record_run(True)

def handle_shift_f2():
    record_run(False)

def record_run(got_essence):
    global LAST_TIMESTAMP
    global RUNS
    global ESSENCES

    RUNS += 1
    if got_essence:
        ESSENCES += 1

    timestamp = int(time.time())
    date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")

    drop_rate = 0
    if ESSENCES > 0:
        drop_rate = (ESSENCES / float(RUNS)) * 100

    run_length = 0
    if LAST_TIMESTAMP is not False:
        run_length = timestamp - LAST_TIMESTAMP

    if got_essence:
        print 'DROP - %d - %s - %d/%d - %0.2f%% - %d' % (timestamp, date_str, ESSENCES, RUNS, drop_rate, run_length)
    else:
        print 'NULL - %d - %s - %d/%d - %0.2f%% - %d' % (timestamp, date_str, ESSENCES, RUNS, drop_rate, run_length)

    winsound.PlaySound('CallWaiting.wav', winsound.SND_FILENAME)

    LAST_TIMESTAMP = timestamp

################

HOTKEY_ACTIONS = {
    1: handle_win_f3,
    2: handle_win_f4,
    3: handle_shift_f1,
    4: handle_shift_f2
}

# RegisterHotKey takes:
#  Window handle for WM_HOTKEY messages (None = this thread)
#  arbitrary id unique within the thread
#  modifiers (MOD_SHIFT, MOD_ALT, MOD_CONTROL, MOD_WIN)
#  VK code (either ord ('x') or one of win32con.VK_*)
for id, (vk, modifiers) in HOTKEYS.items ():
    print "Registering id", id, "for key", vk
    if not user32.RegisterHotKey (None, id, modifiers, vk):
        print "Unable to register id", id

# Home-grown Windows message loop: does
#  just enough to handle the WM_HOTKEY
#  messages and pass everything else along.
try:
    msg = wintypes.MSG()
    while user32.GetMessageA(byref(msg), None, 0, 0) != 0:
        if msg.message == win32con.WM_HOTKEY:
            action_to_take = HOTKEY_ACTIONS.get(msg.wParam)
            if action_to_take:
                action_to_take()

        user32.TranslateMessage(byref(msg))
        user32.DispatchMessageA(byref(msg))

finally:
    for id in HOTKEYS.keys():
        user32.UnregisterHotKey(None, id)

Runs

C:\Python27\python.exe D:/Dropbox/Code/py-farm-companion/companion.py
Registering id 1 for key 114
Registering id 2 for key 115
Registering id 3 for key 112
Registering id 4 for key 113
DROP - 1363409462 - 2013-03-16 15:51 - 1/1 - 100.00% - 0
NULL - 1363409526 - 2013-03-16 15:52 - 1/2 - 50.00% - 64
DROP - 1363409594 - 2013-03-16 15:53 - 2/3 - 66.67% - 68
NULL - 1363409660 - 2013-03-16 15:54 - 2/4 - 50.00% - 66
NULL - 1363409742 - 2013-03-16 15:55 - 2/5 - 40.00% - 82
NULL - 1363409808 - 2013-03-16 15:56 - 2/6 - 33.33% - 66
DROP - 1363409870 - 2013-03-16 15:57 - 3/7 - 42.86% - 62
NULL - 1363409923 - 2013-03-16 15:58 - 3/8 - 37.50% - 53
NULL - 1363409980 - 2013-03-16 15:59 - 3/9 - 33.33% - 57
NULL - 1363410035 - 2013-03-16 16:00 - 3/10 - 30.00% - 55
DROP - 1363410098 - 2013-03-16 16:01 - 4/11 - 36.36% - 63
DROP - 1363410158 - 2013-03-16 16:02 - 5/12 - 41.67% - 60
DROP - 1363410216 - 2013-03-16 16:03 - 6/13 - 46.15% - 58
NULL - 1363410277 - 2013-03-16 16:04 - 6/14 - 42.86% - 61
DROP - 1363410340 - 2013-03-16 16:05 - 7/15 - 46.67% - 63
NULL - 1363410456 - 2013-03-16 16:07 - 7/16 - 43.75% - 116
NULL - 1363410515 - 2013-03-16 16:08 - 7/17 - 41.18% - 59
NULL - 1363410578 - 2013-03-16 16:09 - 7/18 - 38.89% - 63
DROP - 1363410656 - 2013-03-16 16:10 - 8/19 - 42.11% - 78
DROP - 1363410768 - 2013-03-16 16:12 - 9/20 - 45.00% - 112
DROP - 1363410837 - 2013-03-16 16:13 - 10/21 - 47.62% - 69
DROP - 1363410899 - 2013-03-16 16:14 - 11/22 - 50.00% - 62
NULL - 1363410953 - 2013-03-16 16:15 - 11/23 - 47.83% - 54
NULL - 1363411016 - 2013-03-16 16:16 - 11/24 - 45.83% - 63
DROP - 1363411076 - 2013-03-16 16:17 - 12/25 - 48.00% - 60
NULL - 1363411168 - 2013-03-16 16:19 - 12/26 - 46.15% - 92
NULL - 1363411244 - 2013-03-16 16:20 - 12/27 - 44.44% - 76
NULL - 1363411306 - 2013-03-16 16:21 - 12/28 - 42.86% - 62
DROP - 1363411375 - 2013-03-16 16:22 - 13/29 - 44.83% - 69
NULL - 1363411454 - 2013-03-16 16:24 - 13/30 - 43.33% - 79
NULL - 1363411523 - 2013-03-16 16:25 - 13/31 - 41.94% - 69
NULL - 1363411582 - 2013-03-16 16:26 - 13/32 - 40.62% - 59
DROP - 1363411645 - 2013-03-16 16:27 - 14/33 - 42.42% - 63
NULL - 1363411699 - 2013-03-16 16:28 - 14/34 - 41.18% - 54
NULL - 1363411764 - 2013-03-16 16:29 - 14/35 - 40.00% - 65
NULL - 1363411821 - 2013-03-16 16:30 - 14/36 - 38.89% - 57
DROP - 1363411879 - 2013-03-16 16:31 - 15/37 - 40.54% - 58
DROP - 1363411942 - 2013-03-16 16:32 - 16/38 - 42.11% - 63
DROP - 1363412001 - 2013-03-16 16:33 - 17/39 - 43.59% - 59
NULL - 1363412067 - 2013-03-16 16:34 - 17/40 - 42.50% - 66
DROP - 1363412145 - 2013-03-16 16:35 - 18/41 - 43.90% - 78
DROP - 1363412223 - 2013-03-16 16:37 - 19/42 - 45.24% - 78
NULL - 1363412293 - 2013-03-16 16:38 - 19/43 - 44.19% - 70
NULL - 1363412360 - 2013-03-16 16:39 - 19/44 - 43.18% - 67
DROP - 1363412422 - 2013-03-16 16:40 - 20/45 - 44.44% - 62
NULL - 1363412485 - 2013-03-16 16:41 - 20/46 - 43.48% - 63
DROP - 1363412548 - 2013-03-16 16:42 - 21/47 - 44.68% - 63
DROP - 1363412611 - 2013-03-16 16:43 - 22/48 - 45.83% - 63
NULL - 1363412681 - 2013-03-16 16:44 - 22/49 - 44.90% - 70
DROP - 1363412752 - 2013-03-16 16:45 - 23/50 - 46.00% - 71
NULL - 1363412815 - 2013-03-16 16:46 - 23/51 - 45.10% - 63
NULL - 1363412880 - 2013-03-16 16:48 - 23/52 - 44.23% - 65
DROP - 1363412953 - 2013-03-16 16:49 - 24/53 - 45.28% - 73
DROP - 1363413020 - 2013-03-16 16:50 - 25/54 - 46.30% - 67
DROP - 1363413090 - 2013-03-16 16:51 - 26/55 - 47.27% - 70
NULL - 1363413156 - 2013-03-16 16:52 - 26/56 - 46.43% - 66
NULL - 1363413209 - 2013-03-16 16:53 - 26/57 - 45.61% - 53
DROP - 1363413274 - 2013-03-16 16:54 - 27/58 - 46.55% - 65
NULL - 1363413349 - 2013-03-16 16:55 - 27/59 - 45.76% - 75
NULL - 1363413417 - 2013-03-16 16:56 - 27/60 - 45.00% - 68
DROP - 1363413479 - 2013-03-16 16:57 - 28/61 - 45.90% - 62
DROP - 1363413548 - 2013-03-16 16:59 - 29/62 - 46.77% - 69
NULL - 1363413608 - 2013-03-16 17:00 - 29/63 - 46.03% - 60
NULL - 1363413669 - 2013-03-16 17:01 - 29/64 - 45.31% - 61
NULL - 1363413735 - 2013-03-16 17:02 - 29/65 - 44.62% - 66
DROP - 1363413801 - 2013-03-16 17:03 - 30/66 - 45.45% - 66

Summary

In the end I ran Warrior’s Rest on MP9 66 times with my

Barbarian. The average run time was 67 seconds (timed from pressing resume to existing back to the lobby screen). I got 30 essences (45%) which is quite a bit higher than the expected drop rate (35%). Overall I got 1 essences every 2:24, which doesn’t really sound that great. I think I need to tweak my build and items (I really want Earthquake, but I don’t want to give up the healing from Rend). Maybe I’ll time some Vault of the Assassin runs next time.

Link



Mouth of the Powlett River

From Christmas 2012.

{{< gallery dir="static/images/20121226_mouth_of_the_powlett_river/" baseurl="/images/20121226_mouth_of_the_powlett_river/" >}}

Link