[Python][Twisted] twistd

環境

この記事の内容は、Ubuntu Linux 6.10, Python 2.4.4c1, Twisted 2.4.0で確認しました。

はじめに

id:SumiTomohiko:20070712:1184244636でTwistedのリアクタの話をした際、yasusiiさん (http://lowlife.jp/yasusii/) から、

どのリアクタを使うかハードコーディングせずに、実行時に切り替えられるようにするため、若干トリッキーな仕組みになっています。Twisted アプリケーションの場合、twistd コマンドの -r オプションでリアクタを変更できます。twisted/application/reactors.pyあたりを読んでみてください。

というコメントをいただきました。そこで、twistdを始め、リアクタについてもう少し調べてみました。

まとめ

  1. twistdは、サーバの動作のうち、デーモン化やリアクターの設定といった、どのサーバでも同じ機能を受け持つプログラムです。
  2. twistdで動かすサーバを作るには、決まりごとを守る必要があります。
  3. twistdの-rオプションでリアクタの種類を選択できます。
  4. リアクタのクラスがあるモジュールのinstall関数を呼び出すと、twisted.internet.reactorモジュールが、選択したリアクタのオブジェクトに置き換わります。

twistd

Twistedでデーモンを書くと、アプリケーションをデーモン化するコードとか、リアクタを動かすコードとか、同じようなコードをアプリケーションを書く度に何度も記述することになります。twistdは、その同じコードを引き受けて、開発者はデーモンの中核のみを作成すればよいようにするツールです。

twistdで動作するサーバを記述する際、守らなければならない決まりごとがあります。それは、以下の2点です。

  1. twisted.application.service.Application関数(クラスのように見える名前ですが、これは関数です)の戻り値を、"application"という名前の変数に代入します。
  2. サーバの親に、上のapplication変数を設定します。

実際に、twistdで動作するサーバのコードを、以下に記します。このサーバは、ユーザからのリクエストを表示するだけのウェブサーバで、

Twisted Network Programming Essentials

Twisted Network Programming Essentials

に掲載されているコードを元にして作成しました。このコードのうち、一番下の3行が、twistdのためのコードです。

# -*- coding: utf-8 -*-

from twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.protocols import basic

class HttpEchoProtocol(basic.LineReceiver):

    def __init__(self):
        self.lines = []
        self.gotRequest = False

    def lineReceived(self, line):
        self.lines.append(line)
        if not line and not self.gotRequest:
            self.sendResponse()
            self.gotRequest = True

    def sendResponse(self):
        responseBody = "You said:\r\n\r\n" + "\r\n".join(self.lines)
        self.sendLine("HTTP/1.0 200 OK")
        self.sendLine("Content-Type: text/plain")
        self.sendLine("Content-Length: %i" % (len(responseBody)))
        self.sendLine("")
        self.transport.write(responseBody)
        self.transport.loseConnection()

f = protocol.ServerFactory()
f.protocol = HttpEchoProtocol

# リアクターを直接動かす場合
#reactor.listenTCP(8000, f)
#reactor.run()   

server = internet.TCPServer(8000, f)
application = service.Application("my_httpd")
server.setServiceParent(application)

このコードをwebecho.pyとして保存し、

$ twistd -y webecho.py

と実行すると、サーバがデーモンとして動作します。

リアクタ

twistdのヘルプを読むと、以下の記述があります。

  -r, --reactor=         Which reactor to use out of: kqueue, poll, qt, default,
                         win, cfreactor, gtk, cf, glade, win32, gtk2, iocp,
                         glib2, threadedselect, wx.

すなわち、-rオプションでリアクタの種類を設定できるとのことです。この、リアクタを設定するまでの流れを追ってみたいと思います。

まず、twistdのソースコードを読むと、最後が以下のようになっています。

from twisted.python.runtime import platformType
if platformType == "win32":
    from twisted.scripts._twistw import run
else:
    from twisted.scripts.twistd import run

run()

そこで、/usr/lib/python2.4/site-packages/twisted/scripts/twistd.pyのrun関数を読んでみます。

def run():
    app.run(runApp, ServerOptions)

ここで、appは、

from twisted.application import app, service

でインポートされています。また、runAppは、以下の関数です。

def runApp(config):
    checkPID(config['pidfile'])
    passphrase = app.getPassphrase(config['encrypted'])
    app.installReactor(config['reactor'])
    config['nodaemon'] = config['nodaemon'] or config['debug']
    oldstdout = sys.stdout
    oldstderr = sys.stderr
    startLogging(config['logfile'], config['syslog'], config['prefix'],
                 config['nodaemon'])
    app.initialLog()
    application = app.getApplication(config, passphrase)
    startApplication(config, application)
    app.runReactorWithLogging(config, oldstdout, oldstderr)
    removePID(config['pidfile'])
    app.reportProfile(config['report-profile'],
                      service.IProcess(application).processName)
    log.msg("Server Shut Down.")

なんとなく、これがtwistdの中核であり、あとからコールバックされるような感じがします。ServerOptionsはクラスで、名前から判断するにコマンドラインなどからのオプションを保持する役割を持っていそうです。runApp関数がコールバックされる際、引数として与えられそうです。

次に、/usr/lib/python2.4/site-packages/twisted/application/app.pyを読んでみます。twisted.application.app.run関数は、以下のようになっていました。

def run(runApp, ServerOptions):
    config = ServerOptions()
    try:
        config.parseOptions()
    except usage.error, ue:
        print config
        print "%s: %s" % (sys.argv[0], ue)
    else:
        runApp(config)

案の定、twisted.scripts.twistd.ServerOptionsオブジェクトを引数にして、twisted.scripts.twistd.runApp関数をコールバックしています。

        config.parseOptions()

という箇所では、コマンドライン引数のパースをやっていそうですが、今回の目的からは外れるため、無視することにします。そうすると、話は先ほどのtwisted.scripts.twistd.runApp関数に戻ります。

twisted.scripts.twistd.runApp関数の中で、リアクターに関する行として、

    app.installReactor(config['reactor'])

があります。何やらリアクターを設定していそうな名前です。config['reactor']の中身が何かは別として、twisted.application.app.installReactor関数を読んでみます。inistallReactor関数は、以下の辞書とともに見つかりました。

reactorTypes = {
    'wx': 'twisted.internet.wxreactor',
    'gtk': 'twisted.internet.gtkreactor',
    'gtk2': 'twisted.internet.gtk2reactor',
    'glib2': 'twisted.internet.glib2reactor',
    'glade': 'twisted.manhole.gladereactor',
    'default': 'twisted.internet.selectreactor',
    'win32': 'twisted.internet.win32eventreactor',
    'win': 'twisted.internet.win32eventreactor',
    'poll': 'twisted.internet.pollreactor',
    'qt': 'twisted.internet.qtreactor',
    'cf' : 'twisted.internet.cfreactor',
    'kqueue': 'twisted.internet.kqreactor',
    'iocp': 'twisted.internet.iocpreactor',
    'cfreactor': 'twisted.internet.cfreactor',
    'threadedselect': 'twisted.internet.threadedselectreactor',
    }

def installReactor(reactor):
    if reactor:
        reflect.namedModule(reactorTypes[reactor]).install()

reactorTypesという辞書は、与えられた名前に対して、リアクタのモジュールを対応させるもののようです。reflectは、

from twisted.python import runtime, log, usage, reflect, failure, util, logfile

としてインポートされているモジュールで、twisted.python.reflect.namedModule関数は、以下の通りです。

def namedModule(name):
    """Return a module given its name."""
    topLevel = __import__(name)
    packages = name.split(".")[1:]
    m = topLevel
    for p in packages:
        m = getattr(m, p)
    return m

これは、Pythonのドキュメント (http://www.python.jp/doc/2.4/lib/built-in-funcs.html) にも記載されているコードで、与えられた名前のモジュールを返すという関数です。よって、上のtwisted.application.app.installReactor関数というのは、指定されたモジュールのinstall関数を呼び出す、ということになります。

ここで例として、twisted.internet.threadedselectreactorモジュールのinstall関数を見てみます。

def install():
    """Configure the twisted mainloop to be run using the select() reactor.
    """
    reactor = ThreadedSelectReactor()
    from twisted.internet.main import installReactor
    installReactor(reactor)
    return reactor

この中では、twisted.internet.threadedselectreactor.ThreadedSelectReactorオブジェクトを、twisted.internet.main.installReactor関数に与えています。ここまでくると、話はid:SumiTomohiko:20070712:1184244636のときと同じです。生成したオブジェクトで、twisted.internet.reactorモジュールを置き換えているわけです。

def installReactor(reactor):
    # this stuff should be common to all reactors.
    import twisted.internet
    import sys
    assert not sys.modules.has_key('twisted.internet.reactor'), \
           "reactor already installed"
    twisted.internet.reactor = reactor
    sys.modules['twisted.internet.reactor'] = reactor

参考文献

  1. Python と Twisted できみにも書ける Web サーバ(1) (http://lowlife.jp/yasusii/stories/24.html)