Tuesday, May 24, 2011

Converting Errno to an Exception

The way Linux and associated libs do error handling is very passive in that if a function call fails, or otherwise does not succeed, it returns a non zero integer. Often it returns -1 and sets the errno integer. It is than the programs responsibility to check the value of errno to figure out what failed and how to recover. This is my understanding; not being a Linux developer.

Python on the other hand is more active in that if something fails or faults, it raises an exception that typically must be handled or the program will exit, dumping a traceback to stderr.

The TeeOS solution is to turn each errno into an exception class of its own:

ErrnoExcepts = {}

# errno 1
class EPERM(Exception):
    def __init__(self):
        self.desc = 'Operation not permitted'
        self.errno = 1
    def __str__(self):
        return repr(self.desc)
ErrnoExcepts[1] = EPERM

# errno 2
class ENOENT(Exception):
    def __init__(self):
        self.desc = 'No such file or directory'
        self.errno = 2
    def __str__(self):
        return repr(self.desc)
ErrnoExcepts[2] = ENOENT
def RaiseErrnoException(errno):
    '''Given an errno; will raise the matching Python exception
    '''
    raise ErrnoExcepts[errno]

In a function or method that will be calling functions from the C libs:

def _mnt(device, target, fstype, flags, data, libcfile = 'libc.so.0'):

    from ctypes import CDLL, get_errno
    from Teeos.SysUtils.ErrnoExceptions import RaiseErrnoException

    # We can't use find_library() because TeeOS does not have
    # 'gcc', 'ldconfig' or 'objdump'.
    # http://docs.python.org/library/ctypes.html#finding-shared-libraries
    libc = CDLL(libcfile, use_errno = True)
    if libc.mount(device, target, fstype, flags, data):
        RaiseErrnoException(get_errno())
        return False

    return True

Now when using the _mnt() function (used in the Mounter class) if there is a non-zero return from the C libs mount() function, a descriptive Python exception will be raised.

>>> from Teeos.SysUtils.Mount import Mounter
>>> procMount = Mounter(None, '/proc', 'proc', 0, None)
>>> procMount.Mount()
True
>>> procMount.Mount()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/teeos/system-packages/Teeos/SysUtils/Mount.py", line 45, in Mount
    self.fstype, self.mflags, self.data)
  File "/teeos/system-packages/Teeos/SysUtils/Mount.py", line 11, in _mnt
    RaiseErrnoException(get_errno())
  File "/teeos/system-packages/Teeos/SysUtils/ErrnoExceptions.py", line 1181, in RaiseErrnoException
    raise ErrnoExcepts[errno]
Teeos.SysUtils.ErrnoExceptions.EBUSY: 'Device or resource busy'
>>>

The second call to the _mnt() function raised the exception EBUSY, as proc was already mounted.

As long as all 'user facing' classes rely on the underlying system functions like _mnt(), error handling can be in a very 'Pythonic' way with Pythons own exception handling methods.

Sunday, May 15, 2011

Kernel Boot Options

I'd initially thought that the first process started by the kernel would be the Python init script, but I've decided to add another step to the TeeOS boot sequence. I'm calling the first process run 'pinit' and it is a statically compiled C binary.

The kernel sets any unrecognized 'key=value' strings at the boot prompt as environment variables. pinit will take advantage of this and it run the interpreter specified on the boot prompt. This allows the specification of which Python to use on the system, should there be more than one version of Python installed. This also makes it possible to test other versions of the interpreter; PyPy for example.

When executed by the kernel, pinit will have PID 1 so it will exec() the Python interpreter passing the specified Python init script as an argument, making the Python script PID 1- the init process.

The currently designed additional boot options are:
PYBIN=/path/to/python
PYINIT=/path/to/python/init

Specifying the interpreter and system init at boot allows testing of of different python versions and different init scripts.

Sunday, May 1, 2011

Python based Init

Update: 
05/05/2011 - The issue does not reside in Pythons implementation of signal, but it was rather an issue with Python being compiled against uClibc version 0.9.31.  Recompiling with version 0.9.32-rc3 of uClibc and enabling NPTL seems to have corrected the issue.  I will leave the original, incorrect post as a example of ctypes callback feature- Just ignore the other ignorant rambling ;)



I've stubbled upon the first major issue with re-writing the userland of Linux in Python. Linux seems to filter all signals to PID 1 or the Init process; only passing the signals Init has installed handlers for.  This makes sense and explains why you can't kill Init with a SIGKILL (I've tried, haven't you?).

The issue is, for some reason, the signal module in Python doesn't seem to install the signal handler in a way Linux expects from Init.  Here is a post on SO explaining the issue.  This was looking like a show stopper.  Just to ensure I wasn't crazy, I compiled a quick C program that would spawn a child process and the child would send a signal to PID 1- it worked so the issue is with Python's signal handling.

It was looking like the first piece of the TeeOS system was going to have to be a compiled binary until I thought about using the ctypes module.  I'd already used ctypes to use the mount() function in libc to mount /proc (Python doesn't offer an os.mount() as I would have expected).

Using ctypes and loading the systems C Library I could call the binary signal() function in the libc shared object.  It took awhile to figure out the callback function (the signal handler) as I wanted to keep that a Python function if I could.  After reading the callback example on the ctypes callback more than a few times I ended up with this test Init script:

#!/usr/bin/python

import os
import sys
import time

from ctypes import *

def SigHUP():
    print "Caught SIGHUP"
    return 0

def SigCHLD():
    print "Caught SIGCHLD"
    return 0

SIGFUNC = CFUNCTYPE(c_int)
SigHUPFunc = SIGFUNC(SigHUP)
SigCHLDFunc = SIGFUNC(SigCHLD)

libc = cdll.LoadLibrary('libc.so.0')
libc.signal(1, SigHUPFunc)
libc.signal(17, SigCHLDFunc)

print "\n\n\n\n\n\n\n\n\33[31mWelcome to TeeOS\33[0m"

print "Mounting Proc: %s" % libc.mount(None, "/proc", "proc", 0, None)

print "forking for ash"
cpid = os.fork()
if cpid == 0:
    os.closerange(0, 4)
    sys.stdin = open('/dev/tty2', 'r')
    sys.stdout = open('/dev/tty2', 'w')
    sys.stderr = open('/dev/tty2', 'w')
    os.execv('/bin/ash', ('ash',))

print "ash started on tty2"

print "sleeping"
while True:
    libc.pause()

Using the signal handling functions directly in libc has allowed me to not use the the Python signal module at all.  Using the command `kill -l` I can find the needed signal numbers and hard code those in the program.  Since portablity isn't at all an issue, this works.

Testing the above code worked perfectly.  I'm not sure if this is a known issue with Python or if I'm just trying to color to far outside the lines.  Now its time to write the first major piece of the TeeOS system.