attribute: Phillie Casablanca

サーバへ自動転送とsubprocessのタイムアウト



この間いくつかの問題を出会った。

一つは、リモートサーバでファイルの編集したくて、今使っているEclipseでリモートアクセスPluginとかを探していたが、見つかったPluginのインストールがうまく行かなくて、あきらめて、自分でファイルを転送するスクリプトを作ってみた。
(今までは、サーバ側、VIで編集していたが、得意じゃないので、開発が遅い)

Eclipseで保存したら、ファイルが自動にサーバへ転送されるのが要望。
複数ファイルを編集している可能性があるので、DIR内全てのファイルをターゲットしたい。

まず、ファイルが変更されたかどうかの確認方法は、os.path.getmtime()。

つまり、mtime(最後に変更(Modify)された時間)を記録して、変わったら、そのファイルを転送する。

それでファイルのパスとそのmtimeの記録をDICTにまとめて返す関数を作った:


def _get_local_files(local_dir):

return [
{
          'path':os.path.join(local_dir,file),
'mtime':os.path.getmtime(os.path.join(local_dir,file))
         }
for file in os.listdir(local_dir)
if os.path.isfile(os.path.join(local_dir,file))
]

ここでリストComprehensionを使ってみた。リストComprehensionは他人を読むとわかりずらくて、あまり、使うのは好きじゃないけど、たまにはたのしい。

それで変更を監視して、転送を実行する部分:


last_files_list = _get_local_files(local_dir)

while True:
try:
time.sleep(SLEEP_SECONDS)

latest_files_list = _get_local_files(local_dir)

files_to_update = []

for idx in xrange(len(latest_files_list)):
if idx < len(last_files_list):
if latest_files_list[idx]['mtime'] > last_files_list[idx]['mtime']:
files_to_update.append(latest_files_list[idx])

else:
files_to_update.append(latest_files_list[idx])

if files_to_update:

print '変更あり、転送実行~'

is_success = upload_all(server,username,password,local_dir, remote_dir, files_to_update, encrypt)

if not is_success:
break

else:
print '.',

last_files_list = latest_files_list[:] # copy the list to hold
except KeyboardInterrupt:
print
print 'Exiting.'
break

これで失敗しない限り、監視がCtrl-Cを押すまで続く。

じゃ、upload_all()の中身、まず、ftplibを使ったんですが、やっぱり、sftpを使いたい。
軽く調べてみたら、Pythonのいいsftpモジュールが見つからなくて、結局、Putty (www.putty.org) についているpsftpをラップして、ftplib.FTP()と同じようなインタフェースを作った。

要は、ラッパクラスを作って、このメッソドを加えた:

class SftpHandler:

def connect()
def login()
def cwd()
def storbinary()
def quit()

subprocess.Popenでpsftpを起動させて、PIPEでコマンドのやり取りをやればと思った。
クラス Popen( args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
http://www.python.jp/doc/release/lib/node229.html

そこで、問題発生。

この感じで読んでいた:


proc_args = ('C:\\Putty\\psftp.exe', '<サーバ>')
self.proc = subprocess.Popen(proc_args,
      bufsize=0,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

ログインのところでユーザ名が完全に送られていないみたい。

self.proc.stdin.write('myusername\n')
self.proc.stdin.flush()

まず、bufsizeを1(行ごとにバッファされること)にしてみた。
多少よくなったが、最後の文字がとられていた。よく考えてみたら、WINDOWSの改行コードが'\r\n'、それに変更したら、うまく行った。

つまり:

proc_args = ('C:\\Putty\\psftp.exe', '<サーバ>')
self.proc = subprocess.Popen(proc_args,
      bufsize=1,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

self.proc.stdin.write('username\r\n')
self.proc.stdin.flush()

ここまでは、psftpからの出力を読まなくても、済んだが、パスワードのプロンプトやコマンドプロンプトがちゃっとでているかどうかは知りたい。そこでプロセスのstdoutを読めば、できるが、proc.stdout.read()はなんかくるまでずっと待つ。

stdout.read()のタイムアウトがほしいよね。

もし、タイムアウトがあったら、そこで、プロンプトがないと判断できる。

まず、ほしいプロンプトがあるかどうかの判断はこんな感じでやった:


def _wait_for_text(self, text = 'login as'):
resp = ''
while text not in resp:
resp += self.proc.stdout.read(1)

self.proc.stdout.flush()
return True

じゃ、このread()が待ち状態にとまったら、タイムアウトして、Falseを返したい。
timeoutをさがたら、いくつかの答えはでるけど、これが一番きれいで簡単かなと思った:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878

これをread()のタイムアウトとして使いたいので、下記のように編集した:


def timeout(func, args=(), kwargs={}, timeout_duration=1, default=None):
'''This function will spwan a thread and
return the given default value if the timeout_duration is exceeded or the function all raises an exception.
'''
import threading
class InterruptableThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.result = default
def run(self):
try:
self.result = func(*args, **kwargs)
except:
self.result = default
it = InterruptableThread()
it.start()
it.join(timeout_duration)
if it.isAlive():
return it.result
else:
return it.result

このtimeoutを使って、_wait_for_text()をラップするような感じで、呼ぶ。
そうすると、時間が切れたら、Falseを返して、切る前ならTrueを返す。

ラップするとこうな呼びかた:


prompt_found = timeout(self._wait_for_text,
args=(password_prompt,),
timeout_duration=PROCESS_TIMEOUT_SEC,
default=False )

FalseならプロセスをKILLしたいなら、ウインドウズでこの方法がある:
(win32apiを使う方法)


import win32api
PROCESS_TERMINATE = 1
handle = win32api.OpenProcess(PROCESS_TERMINATE, False, self.proc.pid)
win32api.TerminateProcess(handle, -1)
win32api.CloseHandle(handle)

subprocessを使って、初めてプロセスのPIPEでやりとりが出来た。
かなり勉強になった!

monkut // June 30, 2008 // 8:58 a.m.