When it’s ready.

出来るまで出来ない

タリーサーバーを作る その3 (Reactorの書き方を変えてみた)

先日のエントリーにliris_ppさんから、「reactor.runはブロックするので・・・」というコメントをもらった。ありがとうございます。参考のURLも教えてもらった。ありがとうです。

しかし、そもそもreactorの理解をしていないのでちょっと調べる。

Reactorとは?
 イベントループ
 イベントに応じてコールバックを実行
 スケジュール管理
 select/pollだけじゃなく、Win32, GTK1,2, QTなどのevent loopなども使用可能→クライアントのGUIプログラミングと親和性が良い
 Twistedを使ってクライアントを作る場合、WindowsだとWin32のイベントループをReactorに置き換えることで既存のコードをあまりいじらずに利用できる(http://talpa.kahei.org/blog/156)

という事らしい。未だに良く理解して居ないが、GUIとの親和性が高いという事らしい。しかも、コードをあまり弄らなくていいという事で、これはすばらしい事に違いない。

改めて、liris_ppさんの教えてくれたURLに行ってみる。以下の様なコードが書いてあった。

from Tkinter import *
from twisted.internet import tksupport

root = Tk()

# Install the Reactor support
tksupport.install(root)

# at this point build Tk app as usual using the root object,
# and start the program with "reactor.run()", and stop it
# with "reactor.stop()".

ふむふむ、tksupportのインストールメソッドに、Tk()クラスのインスタンスを渡しているが、それ以上はよく分かんない。tksupportのソースをみてみる事にする。以下ソース

# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
# See LICENSE for details.


"""This module integrates Tkinter with twisted.internet's mainloop.

API Stability: semi-stable

Maintainer: U{Itamar Shtull-Trauring<mailto:twisted@itamarst.org>}

To use, do::

    | tksupport.install(rootWidget)

and then run your reactor as usual - do *not* call Tk's mainloop(),
use Twisted's regular mechanism for running the event loop.

Likewise, to stop your program you will need to stop Twisted's
event loop. For example, if you want closing your root widget to
stop Twisted::

    | root.protocol('WM_DELETE_WINDOW', reactor.stop)

"""

# system imports
import Tkinter, tkSimpleDialog, tkMessageBox

# twisted imports
from twisted.python import log
from twisted.internet import task


_task = None

def install(widget, ms=10, reactor=None):
    """Install a Tkinter.Tk() object into the reactor."""
    installTkFunctions()
    global _task
    _task = task.LoopingCall(widget.update)
    _task.start(ms / 1000.0, False)

def uninstall():
    """Remove the root Tk widget from the reactor.

    Call this before destroy()ing the root widget.
    """
    global _task
    _task.stop()
    _task = None


def installTkFunctions():
    import twisted.python.util
    twisted.python.util.getPassword = getPassword


def getPassword(prompt = '', confirm = 0):
    while 1:
        try1 = tkSimpleDialog.askstring('Password Dialog', prompt, show='*')
        if not confirm:
            return try1
        try2 = tkSimpleDialog.askstring('Password Dialog', 'Confirm Password', show='*')
        if try1 == try2:
            return try1
        else:
            tkMessageBox.showerror('Password Mismatch', 'Passwords did not match, starting over')

__all__ = ["install", "uninstall"]

ソースにパスワードが何やら有るが全く使ってないのに必ず通るようになっている。これはいったいいつ使われるんだろう?とかそんな妄想に入り時間をロスってしまった。ソースは、相変わらずよく分からないが、冒頭のコメントを信じる事にする。

and then run your reactor as usual - do *not* call Tk's mainloop(),
use Twisted's regular mechanism for running the event loop.

そして、いつものように貴方のリアクターを作ったください。Tk'sのメインループは使わないで!
Twisted'sの普通のイベントループを使ってください。

これを信じてコーディングしてみる事にする。本当にいつも通りにやればいいんだな?と信じて・・・

先日のソースの先頭に

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

root = Tk()

# Install the Reactor support
tksupport.install(root)

これだけ、追加してみた。

実行・・・

おぉ!! ボタンがきくじぇーー!コレは凄い。
色々下べた時間:実装して試す時間  = 99 : 1
くらいだった。とっとと試せば良かった。しかし、ソースを追っかける事に少しずつ抵抗を感じなくなってきている自分がいる。良い事なのかどうかは分からないが・・・

liris_ppさんありがとうです。

ただし、終了時に大量のエラーが出るようになった。

Unhandled error in Deferred:
Traceback (most recent call last):
  File "/Library/Python/2.5/site-packages/twisted/internet/posixbase.py", line 220, in run
    self.mainLoop()
  File "/Library/Python/2.5/site-packages/twisted/internet/posixbase.py", line 228, in mainLoop
    self.runUntilCurrent()
  File "/Library/Python/2.5/site-packages/twisted/internet/base.py", line 561, in runUntilCurrent
    call.func(*call.args, **call.kw)
  File "/Library/Python/2.5/site-packages/twisted/internet/task.py", line 108, in __call__
    d = defer.maybeDeferred(self.f, *self.a, **self.kw)
--- <exception caught here> ---
  File "/Library/Python/2.5/site-packages/twisted/internet/defer.py", line 107, in maybeDeferred
    result = f(*args, **kw)
  File "/System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-tk/Tkinter.py", line 917, in update
    self.tk.call('update')
_tkinter.TclError: can't invoke "update" command:  application has been destroyed

リアクターを止めるボタンを追加してみると、終了時にもエラーが出なくなった。スバラスィ!!
これで、普通に受け止める事が出来るようになったので、明日から、ファンクションを追加して行こうっと

# coding:utf8
"""
tallyCpt.py

Created by atusi on 2008-03-20.
Copyright (c) 2008 __atusi.com__. All rights reserved.
"""
from Tkinter import *
from ScrolledText import ScrolledText
from datetime import datetime
from twisted.internet import tksupport

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

root = Tk()

# Install the Reactor support
tksupport.install(root)


class EchoServer(basic.LineReceiver):
    def connectionMade(self):
        self.factory.clients.append(self)

    def connectionLost(self):
        self.factory.clients.remove(self)

    def lineReceived(self, line):
        for client in self.factory.clients:
            client.message(line)

    def message(self, message):
        #self.transport.write(message + '\n')
        data = eval(message)
        typeList = []
        capt_massage(message)
        if (type(data)==type(typeList)):
            for i in data:
                print i
        else:
            print data

class MainForm(Frame):
  font = ('Courier', 10, 'normal')
  portNo = None
  
  def init(self):
    self.master.title("TallyServer")
    f = Frame(self, relief=SUNKEN, bd=1)
    
    #label_ip = Label(f, text="IP : ", font=self.font)
    #self.entry_ip = Entry(f, width=18, font=self.font)
    #label_ip.pack(side=LEFT)
    #self.entry_ip.pack(side=LEFT,padx=5)

    label_port = Label(f,text="port : ", font=self.font)
    self.entry_port = Entry(f, width=`4`, font=self.font)
    label_port.pack(side=LEFT)
    self.entry_port.pack(side=LEFT,padx=5)

    b0 = Button(f, text="set ", command=self.setPort, font=self.font)
    b1 = Button(f, text="startServer ", command=self.startServer, font=self.font)
    b2 = Button(f, text="stopServer ", command=self.stopServer, font=self.font)
    b3 = Button(f, text="Exit", command=self.cmd_quit, font=self.font)
    
    self.text = ScrolledText(self, width=40,height=14,font=self.font)

    for e in [b0, b1, b2, b3]: e.pack(side=LEFT, padx=5)
    for e in [f, self.text]:  e.pack(side=TOP, expand=YES, fill=BOTH,padx=1, pady=1)

  def setPort(self):
    if (app.entry_port.get()==""):
      capt_massage("Port がセットされてないお\n")
    else:
      self.portNo = app.entry_port.get()
      main_routine()

  def cmd_quit(self):
    self.master.destroy()

  def __init__(self, master=None):
    Frame.__init__(self, master)
    self.pack()
    self.init()

  def addline(self, content):
    self.text.config(state=NORMAL)
    self.text.insert(END, content)
    self.text.yview(END)
    self.text.config(state=DISABLED)
    self.update()
    
  def startServer(self):
    self.factory = protocol.ServerFactory()
    self.factory.protocol = EchoServer
    self.factory.clients = []
    self.setPort()
    if self.portNo == None:
      capt_massage("Port がセットされてないお\n")
    else:
      reactor.listenTCP(int(self.portNo), self.factory)
      capt_massage('サーバー始めました\n')
      reactor.run()

  def stopServer(self):
    """docstring for stopServer"""
    reactor.stop()
    capt_massage('サーバー止めたお\n')
    
#///////////////////////////////////////////////////////////////////////////////

def main_routine():
    port = app.entry_port.get()
    capt_massage("port [ %s ]\n" %( port ))

def capt_massage(str):
    oline = "massage : \n%s\n%s\n\n" % (str, datetime.now())
    app.addline(oline)
   
#///////////////////////////////////////////////////////////////////////////////

if __name__ == '__main__':
    app = MainForm()
    app.mainloop()