Friday, October 17, 2008

Simple example of Threads in Python

The first time it was immediately obvious to me that there would be a significant gain from 'threading' a program I had written - was in the context of screen scraping. I had a handful of HTTP GET requests from almost 20 pages that were being processed one... after... the... other. I realized of course that if I would just start the next request before waiting on the last one to finish - the entire process would be over much more quickly.

In this example the screen scraping 'worker' function is replaced with a simpler 'random wait' function:

#! /usr/bin/env python

import sys
import threading
import time
import random

# The worker function does the processing
def worker(arg):
arg = random.randint(2,10)
time.sleep(arg)
return arg

# The myThreadObj wraps the worker function in a thread
class myThreadObj(threading.Thread):
def __init__(self, arg):
threading.Thread.__init__(self)
self.arg = arg
self.value = 0
def run(self):
self.value = worker(self.arg)
print 'Thread %d Ended.' % self.arg

# my array of arguments to be processed by the worker function
myArgs = range(5)

# create a myThreadObj to process each argument
myThreadList = []
for i in myArgs:
myThreadList.append(myThreadObj(i))
# and start it immediately
myThreadList[i].start()

# wait for all threads to finish
for each in myThreadList:
each.join()

print 'All threads have completed.'

for i in myArgs:
print "myThreadList[%d] = %d" % (i, myThreadList[i].value)

The myThreadObj wrapper should accept whatever arguments you normally pass to the worker, and when the worker is completed - it will store the returned value in 'self.value'

The .join() function blocks until the .isAlive() method would return false. I process each thread iteratively to verify that all have completed. It doesn't matter if .join() blocks for 8 seconds while it's waiting on the first thread, or if it gets to a thread that's already been completed for 6 seconds cause an earlier .join was waiting on a previous thread that took longer. The point is that, by the time all of the .join() statements complete - ALL THREADS HAVE FINISHED.

Once the threads are done we expect myThreadObj.value to contain the return value of the worker function.

If your 'worker' function is something like an API call, or database query - anything with some built in lag from a system that's designed to serve multiple simultaneous requests - as long as you can queue them up - threading will provide a significant improvement.

e.g.

clayg@m-net:~$ cat nonthread.py
#! /usr/bin/env python

import sys
import threading
import time
import random

# The worker function does the processing
def worker(arg):
arg = random.randint(2,10)
time.sleep(arg)
return arg

myArgs = range(5)

for i in myArgs:
print "myThreadList[%d] = %d" % (i, worker(i))
clayg@m-net:~$ time ./nonthread.py ; echo ; time ./simplethread.py
myThreadList[0] = 5
myThreadList[1] = 5
myThreadList[2] = 10
myThreadList[3] = 2
myThreadList[4] = 9

real 0m31.073s
user 0m0.045s
sys 0m0.024s

Thread 4 Ended.
Thread 0 Ended.
Thread 1 Ended.
Thread 2 Ended.
Thread 3 Ended.
All threads have completed.
myThreadList[0] = 5
myThreadList[1] = 10
myThreadList[2] = 10
myThreadList[3] = 10
myThreadList[4] = 2

real 0m10.078s
user 0m0.045s
sys 0m0.030s

Wednesday, October 1, 2008

Parsing a list of numbers in Python

I find that I often need to get a selection of numbers in a range as input. I'm using Python more and more these days it seems, so I needed to port this classic function over. I must have done this 4 weeks ago - I'd been meaning to put it up here.

The valid input will be a comma separated list of integers, which could possibly contain a 'range' defined as "x-y" - where x and y are both integers.

I tried not to make any special stipulation for the order of these integers, or even that the input string would not contain bad characters.

Here it is:
#! /usr/local/bin/python
import sys
import os

# return a set of selected values when a string in the form:
# 1-4,6
# would return:
# 1,2,3,4,6
# as expected...

def parseIntSet(nputstr=""):
selection = set()
invalid = set()
# tokens are comma seperated values
tokens = [x.strip() for x in nputstr.split(',')]
for i in tokens:
try:
# typically tokens are plain old integers
selection.add(int(i))
except:
# if not, then it might be a range
try:
token = [int(k.strip()) for k in i.split('-')]
if len(token) > 1:
token.sort()
# we have items seperated by a dash
# try to build a valid range
first = token[0]
last = token[len(token)-1]
for x in range(first, last+1):
selection.add(x)
except:
# not an int and not a range...
invalid.add(i)
# Report invalid tokens before returning valid selection
print "Invalid set: " + str(invalid)
return selection
# end parseIntSet

print 'Generate a list of selected items!'
nputstr = raw_input('Enter a list of items: ')

selection = parseIntSet(nputstr)
print 'Your selection is: '
print str(selection)

When trying to copy this from someone else I came across a similar function written in Ruby, in case you needed that instead.

Friday, September 26, 2008

olpc default no password

I received an OLPC in the mail today. It's on loan from a family member who got it as a gift for her daughter. I was told "It's not a REAL computer! - can you fix it?"

Yes, send it to me.

Whatever it takes right? I couldn't wait to tear into this thing.

SSH is up and running by default out the gate. Just go to the "Neighborhood View" and connect to your in home wireless AP and you can hit ssh right away:

[olpc@xo-14-7F-A0 ~]$ telnet 192.168.1.118 22
Trying 192.168.1.118...

Connected to 192.168.1.118.

Escape character is '^]'.

SSH-2.0-OpenSSH_4.5

Which is cool, but there's a catch - you can't login.

The default user is named 'olpc' - this user, as well as root DO NOT HAVE A DEFAULT PASSWORD.

The password file is shadow'd:
[olpc@xo-14-7F-A0 ~]$ egrep '^(root|olpc)' /etc/passwd
root:x:0:0:root:/root:/bin/bash
olpc:x:500:500::/home/olpc:/bin/bash

But if you look at the hash in /etc/shadow you see olpc's "current password hash" is "!!" - which is not valid:
[olpc@xo-14-7F-A0 ~]$ egrep '^(root|olpc)' /etc/shadow
root::13880:0:99999:7:::
olpc:!!:14148:0:99999:7:::

root's password is just plain empty
!

Which is handy on the local system:
[olpc@xo-14-7F-A0 ~]$ whoami
olpc

[olpc@xo-14-7F-A0 ~]$ su

bash-3.2# whoami

root


But kind of awkward when you try to login via ssh:
login as: olpc
olpc@192.168.1.118's password:

Access denied


The tricky part about that invalid "!!" hash is that it's neither a blank password nor a valid password. The olpc user can't even change it's OWN password because there is NO CURRENT VALID PASSWORD:
[olpc@xo-14-7F-A0 ~]$ passwd
Changing password for user olpc.
Changing password for olpc.
(current) UNIX password:

passwd: Authentication token manipulation error


So we decide move on to root.

Root has an 'empty' password. Which is valid - but it had me raise en eyebrow as well.

login as: root
root@192.168.1.118's password:
Access denied


I was slightly forgiving after my investigation into the default configuration of OpenSSH.
bash-3.2# egrep '#Permit(Root|Empty)' /etc/ssh/sshd_config
#PermitRootLogin yes
#PermitEmptyPasswords no

Because these configuration lines are commented out, the ssh server goes with the standard OpenSSH settings. Which will NEVER allow the super user to to authenticate directly over ssh, and furthermore would not allow ANY user with an empty password to authenticate anyway.

To login to your OLPC XO Laptop via ssh you will need to:
1) open a local terminal
2) su to root
3) set a valid password for the 'olpc' user
4) connect to the laptop
as the 'olpc' user via ssh using the new password you just created

e.g.

[olpc@xo-14-7F-A0 ~]$ whoami
olpc
[olpc@xo-14-7F-A0 ~]$ su
bash-3.2# passwd olpc
Changing password for user olpc.
New UNIX password:
BAD PASSWORD: it is too short
Retype new UNIX password:
passwd: all authentication tokens updated successfully.
bash-3.2# exit
exit
[olpc@xo-14-7F-A0 ~]$ ssh olpc@192.168.1.118
The authenticity of host '192.168.1.118 (192.168.1.118)' can't be established.
RSA key fingerprint is d4:5a:b0:30:0d:eb:61:9c:df:c9:32:0e:ab:ff:51:9e.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.1.118' (RSA) to the list of known hosts.
olpc@192.168.1.118's password:
[olpc@xo-14-7F-A0 ~]$

Stay tuned - More to come!

Saturday, September 13, 2008

Display free memory in Linux / Ubuntu

Short Answer:
free -m | awk 'NR==3 {print $4 " MB"}'

The command 'free' displays some formatted information from /proc/meminfo.
The throw '-m' displays these numbers in rounded megabytes.


# free -m
         total      used     free   shared     buffers     cached
Mem:      4049      3982       67        0          16       3530
-/+ buffers/cache:   435     3614
Swap:     6142        53     6088

It's strange, cause I always figured the "first row, third column" - right under 'FREE' - would be the amount of free memory on a Linux system. But that number was always low after I'd been running awhile.


And it turns out that it is NOT really what I was looking for. A more accurate representation of the memory being used by your applications and available for new processes is displayed in the SECOND line.

In addition to the memory that is actually being USED by the kernel and processes resident in memory - Linux also reserves memory to allocate to processes as 'buffers' AND uses pretty much any left over memory to hold "cached" files.

Looking only at the top line...
total = all memory in the system (4GB on this server)
used = all memory currently in use/reserved by running processes and the OS
free = total - used
shared = memory being shared by multiple processes (deprecated?)
buffers = memory reserved by the OS to alloc as buffers when process need them (aka the 'heap')
cached = recently used files being stored in ram (THANK YOU LINUX!)

Here's a simple example I found to show off the power of 'caching':
for i in 1 2 ; do free -o; time grep -r foo /usr/bin >/dev/null 2>/dev/null; done
So really the buffers would be allocated to a running process if it asked for them anyway, and the memory being used to cache copies of recently used files would be released immediately if it makes sense to allocate the RAM elsewhere. So all that memory is 'available'.

Using these definitions:

When thinking about 'how much memory is really being used' - I want to calculate:
'used' - ('buffers' + 'cached')

When thinking about 'how much memory is really free' - I want to calculate:
'free' + ('buffers' + 'cached')

With this in mind, the meaning of the second row header form the output of the Linux command "free" (-/+ buffers/cache:) makes more sense...

Free is doing some light lifting for us, using the formula's above to display:
"minus buffers and cache" for the used column
and
"plus buffers and cache" for the free colum

So when you run free on Linux - the amount of free memory is always displayed right there in the second row, third column. Hence the 'Short Answer'...

- Much respect to Mike Griffin who got me thinking about this.

Thursday, September 4, 2008

DD-WRT Xbox LIVE open NAT

When I first setup my DD-WRT router I had problems with Xbox LIVE reporting strict NAT and then moderate NAT.

To achive "open" NAT status on Xbox LIVE your best bet is ALWAYS UPnP.

Not DMZ.
Not even setting up manual LIVE port forwarding.

Both of these methods fall apart if you have two Xboxs. UPnP allows the Xbox(s) to decide the best ports to use, when they should be open and when to close them.

By default the DD-WRT v24 firmware disables UPnP.

To Enable UPnP on DD-WRT v24:
NAT / QoS -> UPnP ->
UPnP Service: Enabled

Click Apply. You may need to reboot your xbox for changes to take effect.

In order for UPnP to work as intended the client device (the xbox) must be using DHCP to obtain an IP address from the UPnP enabled router. To verify your Xbox is using DHCP, as opposed to assigning itself a static ip - open the dashboard and navigate to:
System -> Network Settings -> Edit Settings ->
Select "IP Settings"
Verify "Automatic"

To see what your Xbox thinks about your NAT try the "Live Connection Test"
System -> Network Settings -> "Test Xbox LIVE Connection"

Wednesday, September 3, 2008

Easy DD-WRT wireless bridge

So this is me connecting to a dd-wrt on the other side of the room - no wires


First you have to be able to connect to both routers on different ip's on the same subnet via wired connection.

primary gateway - 192.168.1.1
remote client bridge - 192.168.1.2

If you're all set up and rocking it with your primary gateway on 192.168.1.1 - but you need to add this new device which defaults to the same ip into the mix....
Just log into your current device and change it's ip to .3 - then plug in the new router, navigate to it on .1 - change it .2 - go back to the original on .3 and put it back on .1

You *may* want to go ahead and disable DHCP on the remote client bridge while your in there, but only if you're not going to immediately configure wireless.

Remote client bridge (192.168.1.2) wireless setup:

Step 1:
Wireless -> Basic Settings

Wireless Mode: Client Bridge
Wireless Network Name (SSID): "SSID of primary gateway / wireless AP"

click Apply

Step 2:
Wireless -> Wireless Security
configure wireless settings to match your primary gateway's wireless config

Apply changes

That's it, unplug your router and test the connection!

I *highly* recommend you START with 64bit/10 digit hex WEP. Go ahead and change your settings on the primary gateway / wireless AP if you have to. Once you have that working by all means upgrade to a higher security state. But, do your self a favor and start simple to make sure you have it working.

DD-WRT applyuser.cgi hangs

What to do if you're running into the issue with the DD-WRT admin page hanging on the applyuser.cgi script when you try to save changes.

SYMPTOM:
when you click 'apply' you get directed to the address -
http://192.168.1.1/applyuser.cgi
Which is followed by a blank white screen. Navigating back just proves your changes were not affected.

SOLUTION:
restart the browser - it's probably just a cookie with a hung up user session or something, but I couldn't find it. Other suggestions would be very welcome...