ブログトップ 記事一覧 ログイン 無料ブログ開設

saito’s blog RSSフィード

2011-09-25

Emacs LispとRubyを使ってGoogle Chromeを操作する

私は以前までブラウザはFirefoxを使っていましたが、Firefox4でMacでFlashが見れなくなったり、Firefox5でタブを切り替えても表示が切り替わらない(この現象は検索してもヒットしないので自分の環境だけの可能性有り)といった不具合があってから、メインのブラウザをGoogle Chromeに乗り換えています。

FirefoxからGoogle Chromeに乗り換えるのをためらった理由の1つが、自作したEmacsとの連携機能Chromeに移行すると使えなくなるというものでしたが、google-chrome-clientという別プログラムからChromeにアクセスするためのRubyライブラリの存在を知ったので、Emacs連携機能のGoogle Chrome版を作成しました。

この機能を使うためには、Google Chromeを次のオプションで起動します。

--remote-shell-port=9222

Macの場合、Google Chromeの実行形式へのパスは、/Applications/Google Chrome.app/Contents/MacOS/Google Chromeです。

このオプション付きで起動すると、「ChromeDevTools Protocol」というプロトコルが使えるようになります。

Ruby側のプログラムは、google-chrome-clientというライブラリを使用しているので、まずそちらをインストールします。

gem install google-chrome-client

続いて、以下のスクリプトを適当なファイル名で保存します。

require 'rubygems'
require 'google/chrome/client'

module Google
  module Chrome

    class Client
      def ping
        header, resp = self.request({ 'Tool' => 'DevToolsService' },
                                    { 'command' => 'ping' })
        resp['data']
      end
      def list_tabs
        header, resp = self.request({ 'Tool' => 'DevToolsService' },
                                    { 'command' => 'list_tabs' })
        resp['data']
      end
    end

    class Tab
      def evaluate_javascript(script)
        body = {'command' => 'evaluate_javascript', 
                'data' => script}
        number = @number
        @client.instance_eval do
          write_request({ 'Tool' => 'V8Debugger', 'Destination' => number },
                        body)
        end
      end
    end

  end
end

if $0 == __FILE__
  url = ARGV[0]
  client = Google::Chrome::Client.new('localhost', 9222)
  client.tabs[0].evaluate_javascript("window.open('#{url}');")
end

Emacs側のプログラムは先ほどのRubyプログラムのラッパーになっています。コードそのものはFirefox版のコピペです。chromerepl-open-uri-pathに先ほどのRubyプログラムのパスを指定します。

(defun uri-encode (str)
  (mapconcat
   (lambda (s)
     (mapconcat
      (lambda (x) (format "%%%x" x))
      (vconcat (encode-coding-string s 'utf-8))
      ""))
   (split-string str)
   "+"
   ))

(defvar chromerepl-open-uri-path "~/.emacs.d/bin/chromerepl-open-uri.rb")

(defun chromerepl-open-uri (uri)
  (interactive "suri: ")
  (let ((cmd (format "ruby %s '%s'" chromerepl-open-uri-path uri)))
  (shell-command cmd)))

(defun chromerepl-open-uri-region (begin end)
  (interactive "r")
  (let (str)
    (setq str (buffer-substring-no-properties begin end))
    (chromerepl-open-uri str)))

(defun chromerepl-alc-search (word)
  (interactive "sword: ")
  (chromerepl-open-uri (format "http://eow.alc.co.jp/%s/UTF-8/" (uri-encode word))))

(defun chromerepl-alc-search-region (begin end)
  (interactive "r")
  (let (str)
    (setq str (buffer-substring-no-properties begin end))
    (chromerepl-alc-search str)))

(defun chromerepl-google-search (keywords)
  (interactive "skeywords: ")
  (chromerepl-open-uri (format "http://www.google.co.jp/search?hl=ja&q=%s" (uri-encode keywords))))

(defun chromerepl-google-feeling-lucky (keywords)
  (interactive "skeywords: ")
  (chromerepl-open-uri (format "http://www.google.co.jp/search?hl=ja&btnI=&q=%s" (uri-encode keywords))))

(defun chromerepl-google-search-region (begin end)
  (interactive "r")
  (let (str)
    (setq str (buffer-substring-no-properties begin end))
    (chromerepl-google-search str)))

(defun chromerepl-google-feeling-lucky-region (begin end)
  (interactive "r")
  (let (str)
    (setq str (buffer-substring-no-properties begin end))
    (chromerepl-google-feeling-lucky str)))

(defun chromerepl-google-translate-region (begin end)
  (interactive "r")
  (let (str uri)
    (setq str (buffer-substring-no-properties begin end))
    (setq uri (format "http://translate.google.co.jp/translate_t?hl=ja&sl=en&tl=ja#%s%s"
		      (if (equal (find-charset-region begin end) '(ascii)) "en|ja|" "ja|en|")
		      (uri-encode str)))
    (chromerepl-open-uri uri)
    ))

(defmacro define-chromerepl-x-search (name site)
  (let ((sym-i (intern (concat "chromerepl-" (symbol-name name) "-search")))
	(sym-g (intern (concat "chromerepl-" (symbol-name name) "-search-result")))
	(url-i (format "http://www.google.co.jp/search?hl=ja&btnI=&as_sitesearch=%s&q=%%s" site))
	(url-g (format "http://www.google.co.jp/search?hl=ja&as_sitesearch=%s&q=%%s" site)))
    `(progn
       (defun ,sym-i (keywords)
	 (interactive "skeywords: ")
	 (chromerepl-open-uri (format ,url-i (uri-encode keywords))))
       (defun ,sym-g (keywords)
	 (interactive "skeywords: ")
	 (chromerepl-open-uri (format ,url-g (uri-encode keywords))))
       )
    ))
(defmacro chromerepl-x-search-expand ()
  (let ((chromerepl-x-search-list
	 '((ruby        . "doc.ruby-lang.org")
	   (python      . "docs.python.org"  )
	   (lisp        . "www.lispworks.com")
	   (gauche      . "practical-scheme.net/gauche/man")
	   (django      . "docs.djangoproject.com/en/1.2")
	   )))
    `(progn
       ,@(loop for elt in chromerepl-x-search-list
	       collect `(define-chromerepl-x-search ,(car elt) ,(cdr elt))))    
    ))
(chromerepl-x-search-expand)

参考URL

関連エントリ

Google App Engineを辞めた

約1年前にGAEを使って立ち上げたこのサイトですが、今回のGAEの料金体系の変更によって無料クオータの限界を超える日が存在することが分かったため、GAEを辞めて既に契約しているVPSに移動することにしました。

最初に無料クオータの限界に達していたリソースは、Frontend Instance HoursとDatastore Readsの2つでした。

この2つのリソースの多く消費していたのが、全ポケモンのJSONデータを生成していた箇所だったのですが、生成したJSONをmemcacheだけでなくデータストアにもキャッシュするようにコードを修正したところ、多数のデータストアの読み込みが1回に集約されたためDatastore Readsが減り、かつ遅延も小さくなったため余計なインスタンスが立ち上がることが少なくなりFrontend Instance Hoursも無料クオータに収めることができました。

ところが、今度はデータストア上のキャッシュが原因でSmall Datastore Operationsが無料クオータ限界付近まで増えてしまい、もはやJSONデータを静的ファイルとして予め用意しておいてデータストアの読み込み自体を無くす以外に、無料クオータ内に収める方法がなくなってしまいました。

幸い、GAEを辞めることを見越して別のデプロイ方法を考えていたので、移行はそれなりにスムーズに完了しました。

GAEは当初月間500万PV相当まで無料という売り文句でしたが、CPU時間の方は速いレスポンスを返す工夫とMin Pending Latency等の設定次第で新料金体系でもそれなりになんとかなりそうですが、データストアの方はかなり工夫して作っても、新料金体系下では500万よりも遥かに少ないPVで無料クオータの限界に達してしまうという印象です。

GAEの魅力はGoogleのインフラを使ってアプリケーション開発ができることでしたが、無料で運用されていたアプリにはもともと自動スケールは必要なく、無料であることを選択の理由にしていたアプリ・開発者にとって、GAEはもはや魅力的なプラットフォームでは無いと思われます。

(まあ、無料であることが持続可能であるわけがないので、当然ですが。)

関連エントリ

2011-09-12

DevQuiz スライドパズル 解答

本日、Google Developer Day 2011の参加資格をかけたDevQuizが終了しました。

というわけで、今年一番の難問だったスライドパズルの回答を掲載します。

結果から先に言うと、5000問中4562問正解でした。

が、特別なアルゴリズムを使っているわけでもなく、計算時間もとてつもなくかかるので、はっきりいってあまり参考にはならないと思います。

(MacBook Pro Core2 Duo 2.4GHzを使って6x6の問題を解くのに、1日で200問以下という遅さです。実際にはAmazon EC2を使って計算時間の遅さをカバーしました。)

ちなみに、使用言語はPythonです。

基本アルゴリズム

基本アルゴリズムは幅優先探索です。

最初に、初期状態と空の入力列からなるタプル1つを要素にもつリストを用意します(変数名histories)。

次に、historiesの各要素から1手先の状態と入力列を計算し、それを再びリスト(histories)に格納します。

これを繰り返すことによって、解を探索していきます。

しかし、これだけではすぐにリストの長さが発散しメモリを使い尽くしてしまうので、重複の削除と枝刈りによってこれを回避します。

重複の削除

「右左右左」といった単純なループや同じ所をぐるぐる回るといったループを回避するために、過去の状態と入力列を全てデータベースに保存し、新しい解を得る度に過去の状態と比較を行います。

過去に同じ状態に辿りついたことがある場合は、そこで探索を終了します。

枝刈り

重複の削除を行ってもすぐに探索空間は発散してしまうので、枝刈りを行う必要があります。

histories中の各状態が最終状態にどれくらい近いかを評価し、より評価の良いものだけを残していきます。

この評価関数をどのように設計するかによって問題を解けるか解けないかが決定する訳ですが、評価関数をΣ距離×重みとすると、問題を解ける確率が飛躍的に高まりました。

ここで、「距離」はそのパネルの本来の位置までのユークリッド距離の二乗とし、「重み」は本来の位置が左上にあるパネルほど高い値をもつ数字です。

最終状態では動かせるマスが一番右下であるため、左上のパネルは早い段階で完成させる必要があると考え、このような評価関数を設定しました。

この評価関数は「壁」を考慮していないため、場合によっては局所最適に陥ってしまいうまく解けない問題もありましたが、最終的に約9割の問題が解けたことから、そこそこ良い評価関数ではないかと思っています。

高速化

高速化の工夫はほとんどありませんが、強いて挙げるとすれば、multiprocessingを使ってCPUのコアを使いきるような設計にしたことです。

multiprocessingは本当に便利なライブラリだと思います。

反省点

中途半端に問題が解けることを良い事に、遅いプログラムを最後まで使い続けたことが反省点です。

きちんとしたアルゴリズムを用いれば、同じ数の問題を数時間で解くことができることを知り、専門家の皆さんとの実力の違いを思い知りました。

ソース

import sys
import os
import re
import sqlite3
import multiprocessing
from contextlib import nested

WORK_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'work')
if not os.path.exists(WORK_DIR):
  os.mkdir(WORK_DIR)
DEBUG = False

def vadd(va, vb):
  return tuple((a + b for a, b in zip(va, vb)))

def vsub(va, vb):
  return tuple((a - b for a, b in zip(va, vb)))

def vabs(v):
  return sum([x*x for x in v])

class InputSequence(object):
  def __init__(self, l=0, r=0, u=0, d=0, seq=''):
    self.l   = l
    self.r   = r
    self.u   = u
    self.d   = d
    self.counts = (self.l, self.r, self.u, self.d)
    self.seq = seq

  def __str__(self):
    return self.seq

  def append(self, item):
    if item == 'L':
      return InputSequence(self.l+1, self.r, self.u, self.d, self.seq + 'L')
    elif item == 'R':
      return InputSequence(self.l, self.r+1, self.u, self.d, self.seq + 'R')
    elif item == 'U':
      return InputSequence(self.l, self.r, self.u+1, self.d, self.seq + 'U')
    elif item == 'D':
      return InputSequence(self.l, self.r, self.u, self.d+1, self.seq + 'D')
    raise RuntimeError

  def is_superior(self, other):
    return all([s <= o for s, o in zip(self.counts, other.counts)]) and (self.counts != other.counts)

  def is_inferior(self, other):
    return all([s >= o for s, o in zip(self.counts, other.counts)])

class State(object):
  def __init__(self, w, h, blank, state, final_state):
    self.width  = w
    self.height = h
    self.blank = blank
    self.state = state
    self.final_state = final_state

  def __str__(self):
    state_str = ''.join([''.join(row) for row in self.state])
    return ','.join([str(self.width), str(self.height), state_str])

  def norm(self):
    if hasattr(self, '_norm'):
      return self._norm

    char2coord = dict()
    for x in xrange(self.width):
      for y in xrange(self.height):
        if self.final_state[y][x] != '=':
          char2coord[self.final_state[y][x]] = (x, y)
    self._norm = 0
    for x in xrange(self.width):
      for y in xrange(self.height):
        if self.state[y][x] != '=':
          distance = vabs(vsub((x, y), char2coord[self.state[y][x]]))
          weight = sum(vsub((self.width-1, self.height-1), char2coord[self.state[y][x]]))
          self._norm += distance * weight
    return self._norm

  @staticmethod
  def parse_string(string):
    width, height, state_str = string.rstrip().split(',')
    width  = int(width)
    height = int(height)
    assert(len(state_str) == width * height)

    def pos2coord(pos):
      return (pos % width, pos / width)    
    blank = pos2coord(state_str.find('0'))
    walls = [pos2coord(m.start()) for m in re.compile('=').finditer(state_str)]
    final_state = State.get_final_state(width, height, walls)
    
    state = []
    state_list = list(state_str)
    for i in xrange(height):
      state.append(state_list[i*width:(i+1)*width])
        
    return State(width, height, blank, state, final_state)

  def is_finished(self):
    return self.state == self.final_state

  @staticmethod
  def get_final_state(width, height, walls):
    chars = [chr(ord('1') + i) for i in xrange(9)] + [chr(ord('A') + i) for i in xrange(26)]
    chars = chars[0:width*height-1] + ['0']
    result = []
    for i in xrange(height):
      result.append(chars[i*width:(i+1)*width])
    for x, y in walls:
      result[y][x] = '='
    return result

  def next_states(self):
    result = []
    directions = [('L', (-1, 0)),
                  ('R', (1,  0)),
                  ('U', (0, -1)),
                  ('D', (0,  1))]
    for direction_str,  direction in directions:
      next = self.move(direction)
      if next:
        result.append((next, direction_str))
    return result
  
  def move(self, direction):
    next_x, next_y = vadd(self.blank, direction)
    if (next_x >= 0 and
        next_y >= 0 and 
        next_x < self.width and
        next_y < self.height and
        self.state[next_y][next_x] != '='):
      return self.swap(next_x, next_y)
    else:
      return False

  def swap(self, next_x, next_y):
    x, y = self.blank
    state = [list(row) for row in self.state] # deep copy
    state[y][x], state[next_y][next_x] = self.state[next_y][next_x], self.state[y][x]
    return State(self.width, self.height, (next_x, next_y), state, self.final_state)

class Player(object):
  def __init__(self, problem, max_depth, max_histories):
    self.problem = problem
    self.max_depth = max_depth
    self.max_histories = max_histories
    self.finished = False
    
    db_path = os.path.join(WORK_DIR, problem + '.sqlite')
    db_exists = os.path.exists(db_path)
    self.db = sqlite3.connect(db_path)
    if not db_exists:
      self.initialize_db(self.db)
      
  def initialize_db(self, db):
    sql_table_create = """CREATE TABLE history(
                            state    TEXT,
                            finished INTEGER,
                            left     INTEGER,
                            right    INTEGER,
                            up       INTEGER,
                            down     INTEGER,
                            seq      TEXT);"""
    db.execute(sql_table_create)
    db.execute('CREATE INDEX history_state_index ON history(state);')
    db.execute('CREATE INDEX history_seq_index ON history(seq);')

  def __del__(self):
    if self.finished == True:
      self.db.execute('DELETE FROM history WHERE finished=0;')
      self.db.commit()
      self.db.execute('VACUUM;')
    self.db.close()

  def insert(self, state, input_sequence, finished):
    sql = 'INSERT INTO history values (?,?,?,?,?,?,?);'
    if finished:      
      finished_flag = 1
    else:
      finished_flag = 0
    self.db.execute(sql, (str(state),
                          finished_flag,
                          input_sequence.l,
                          input_sequence.r,
                          input_sequence.u,
                          input_sequence.d,
                          str(input_sequence))
                    )
    
  def delete(self, state, input_sequence):
    sql = 'DELETE FROM history WHERE state=? AND seq=?;'
    self.db.execute(sql, ((str(state), str(input_sequence))))

  def start(self):
    def norm_cmp(h1, h2):
      s1 = h1[0]
      s2 = h2[0]
      norm1 = s1.norm()
      norm2 = s2.norm()
      return norm1 - norm2
    
    state = State.parse_string(self.problem)
    input_sequence = InputSequence()
    if state.is_finished() == True:
      self.insert(state, input_sequence, True)
    else:
      self.insert(state, input_sequence, False)
      histories = [(state, input_sequence)]

      for i in xrange(self.max_depth):
        if self.finished:
          break
        if len(histories) > self.max_histories:
          histories.sort(norm_cmp)
          histories = histories[0:self.max_histories]            
        if DEBUG == True:
          print ('i = %d, len(histories) = %d' %
                 (i, len(histories)))
        histories = list(self.get_next_histories(histories))
        if i % 10 == 0:
          self.db.commit()

  def get_next_histories(self, histories):
    for state, input_sequence in histories:
      for next_state, direction in state.next_states():
        next_input_sequence = input_sequence.append(direction)
        if not self.check_duplication(next_state, next_input_sequence):
          if next_state.is_finished() == True:
            self.insert(next_state, next_input_sequence, True)
            self.finished = True
          else:
            self.insert(next_state, next_input_sequence, False)
            yield (next_state, next_input_sequence)

  def check_duplication(self, next_state, next_input_sequence):
    for duplication in self.get_duplications(next_state):
      if next_input_sequence.is_inferior(duplication):
        return True
      elif next_input_sequence.is_superior(duplication):
        self.delete(next_state, duplication)
    return False

  def get_duplications(self, state):
    cursor = self.db.cursor()
    cursor.execute('SELECT left,right,up,down,seq FROM history WHERE state=?', (str(state), ))
    for row in cursor:
      yield InputSequence(int(row[0]), int(row[1]),int(row[2]),int(row[3]), row[4])

  def get_finished(self, limit):
    cursor = self.db.cursor()
    cursor.execute('SELECT left,right,up,down,seq FROM history WHERE finished=1 LIMIT ? OFFSET 0;', (limit,))
    for row in cursor:
      yield InputSequence(int(row[0]), int(row[1]),int(row[2]),int(row[3]), row[4])

  def get_result(self):
    finished = list(self.get_finished(1))
    if len(finished) == 0:
      self.start()
      finished = list(self.get_finished(1))
      if len(finished) == 0:
        print 'Fail: %s' % self.problem
        return ''
    return str(finished[0])

def slide_puzzle(problem):
  return Player(problem, max_depth=100, max_histories=10000).get_result()

def main():
  if len(sys.argv) < 3:
    print 'usage: python %s [problems_file] [output_file]' % __file__
    return 0
  with nested(open(sys.argv[1], 'r'), open(sys.argv[2], 'w')) as (fin, fout):
    limits = [int(x) for x in fin.readline().rstrip().split()]
    num_problems = int(fin.readline().rstrip())
    problems = [line.rstrip() for line in fin.readlines()]
    assert(len(problems) == num_problems)
    processes = multiprocessing.cpu_count() + 1
    pool = multiprocessing.Pool(processes=processes)
    fout.write('\n'.join(pool.map(slide_puzzle, problems)))
    fout.write('\n')
  return 0

if __name__ == '__main__':
  sys.exit(main())

2011-08-11

nginxとgunicornとsupervisorを連携させる

私はVPSと自宅LAN内でDjangoで作った個人的なwebサービスをいくつか運用しています。

現在の運用環境はapache+mod_wsgiですが、ネットで色々調べていると、nginxとgunicornとsupervisorを組み合わせるのが旬(?)のようなので、その方法について色々調べたことを書きたいと思います。

具体的には、nginxがリバースプロキシ兼Webサーバとして、キャッシュと静的ファイルの配信を担当し、gunicornがバックエンドとして動的なページ生成を担当し、supervisorがサーバプロセスの監視を行う、という構成です。

環境としては、CentOS 5.X を想定しています。

nginxのインストール

CentOSにnginxをイントールする方法はこちらのページに詳しく書かれているので、それを参考にしました。configureの引数のうち、追加した方が良さそうなものがあったので、それだけ掲載しておきます。

./configure \
--prefix=/usr/local \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--user=nginx \
--group=nginx \
--with-http_stub_status_module \
--with-http_ssl_module \
--with-http_gzip_static_module \
--http-log-path=/var/log/nginx/access.log \
--http-client-body-temp-path=/var/tmp/nginx/client/ \
--http-proxy-temp-path=/var/tmp/nginx/proxy/ \
--http-fastcgi-temp-path=/var/tmp/nginx/fcgi/ \
--http-uwsgi-temp-path=/var/tmp/nginx/uwsgi/ \
--http-scgi-temp-path=/var/tmp/nginx/scgi/

gunicornのインストール

gunicornはeasy_installを使って一発でインストールできます。また、setproctitleというライブラリをインストールしておけば、psコマンドやtopコマンドで見えるプロセス名を任意の名前に設定できるようになります。

sudo easy_install gunicorn
sudo easy_install setproctitle

supervisorのインストール

supervisorもeasy_installを使ってインストールすることができます。

sudo easy_install supervisor

supervisorをインストールするとecho_supervisord_confというコマンドが使用できるようになります。

このコマンドを実行すると、supervisorの設定ファイルの雛形が標準出力に出力されるので、リダイレクトして保存し、編集した後、/etc/supervisord.confに保存します。

echo_supervisord_conf > supervisord_conf
vim supervisord_conf
sudo cp supervisord_conf /etc/supervisord_conf

ついでに、supervisord自体の起動スクリプトを見よう見まねで作成したので、掲載しておきます。

#!/bin/sh
#
# supervisord - this script starts and stops the supervisord daemon
#
# chkconfig:   - 90 10
# description:  Supervisor is a client/server system that allows \
#               its users to monitor and control a number of \
#               processes on UNIX-like operating systems.
# processname:  supervisord
# config:       /etc/supervisord.conf
# pidfile:      /tmp/supervisord.pid

# Source function library.
. /etc/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0

RETVAL=0
supervisord="/usr/local/bin/supervisord"
prog=$(basename $supervisord)
pidfile=/tmp/supervisord.pid
lockfile=/var/lock/subsys/supervisord

start () {
    echo -n $"Starting $prog: "
    daemon $supervisord --pidfile $pidfile
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && touch $lockfile
    return $RETVAL
}
stop () {
    echo -n $"Stopping $prog: "
    killproc -p $pidfile $supervisord -QUIT
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && rm -f $lockfile
    return $RETVAL
}
restart () {
    stop
    sleep 1
    start
}
reload () {
    echo -n $"Reloading $prog: "
    killproc -p $pidfile $supervisord -HUP
    RETVAL=$?
    echo
}

case "$1" in
    start)
	start
	;;
    stop)
	stop
	;;
    reload)
	reload
	;;
    restart)
	restart
	;;    
    status)
	status -p ${pidfile} supervisord
	RETVAL=$?
	;;
    *)
	echo $"Usage: $0 {start|stop|status|restart|reload}"
        RETVAL=2
	;;
esac

exit $RETVAL

このファイルを/etc/rc.d/init.d/supervisordに保存して、以下のコマンドを実行すると、マシン起動時にsupervisordが自動的に起動されます。

sudo chmod 755 /etc/rc.d/init.d/supervisord
sudo /sbin/chkconfig --add supervisord
sudo /sbin/chkconfig supervisord on

gunicornの設定

gunicornの設定例を以下に示します。(memoはアプリケーション名)

bind = 'unix:/tmp/gunicorn_memo.sock'

backlog = 2048
workers = 1
worker_class = 'sync'
worker_connections = 1000
max_requests = 0
timeout = 30
keepalive = 2

debug = False
spew  = False

preload_app = True
daemon = False
pidfile = '/var/run/gunicorn/memo.pid' # /var/run/gunicornを作成しておく
user  = 'memo_app'
group = 'nginx'
umask = 0002
# tmp_upload_dir = None

logfile = '/var/log/gunicorn/memo.log' # /var/log/gunicornを作成しておく
loglevel = 'info'
logconfig = None

proc_name = gunicorn_memo'

ワーカーの数は個人的なサービスを考えているので1としていますが、経験的にはCPUコア数×2+1が良いようです。nginxとsupervisorとの連携を考える上では、以下の点がポイントです。

  • nginxとはUNIXドメインソケットを使ってやり取りを行うことを考えているので、bindオプションの値をunix:〜にする。
  • ソケットはnginxとgunicornの両方のプロセスから読み書き可能でなければいけない。nginxとgunicornを別々のユーザで走らせたい場合は、gunicornのgroupをnginxとし、umask=0002(ユーザ及びグループが読み書き可能)とする。
  • supervisorでプロセスを監視するためにはデーモン化してはいけないので、daemon=Falseとする。

supervisorの設定

supervisorはHTTPサーバ機能を持っており、ウェブブラウザを介してプロセスの状態確認や起動/停止等を行うためのインターフェースが用意されています。

これをnginxを介して利用できるようにしたいと思います。

supervisorのHTTPサーバにははUNIXドメインソケットを使ったサーバとINETドメインソケットを使ったサーバの二種類ありますが、UNIXドメインソケットを使う方はnginxとの連携がうまくいかなかったので、INETドメインソケットを使ったサーバを使用します。

unix_http_serverのセクションをコメントアウトして、inet_http_serverを有効化します(パスワードは適宜設定します)。

;[unix_http_server]
;file=/tmp/supervisor.sock ; (the path to the socket file)
;chmod=0700                ; sockef file mode (default 0700)
;chown=nobody:nobody       ; socket file uid:gid owner
;username=user             ; (default is no username (open server))
;password=123              ; (default is no password (open server))

[inet_http_server]         ; inet (TCP) server disabled by default
port=127.0.0.1:9001        ; (ip_address:port specifier, *:port for all iface)
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

supervisorctlのセクションも合わせて変更します。

[supervisorctl]
;serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket

supervisorを使ってgunicornプロセスを起動するための設定例

[program:gunicorn_memo]
command=/usr/local/bin/gunicorn_django --config /opt/www/memo/gunicorn_conf.py /opt/www/memo/settings.py
directory=/opt/www/memo
user=root
autostart=true
autorestart=true
redirect_stderr=true
environment=PYTHON_EGG_CACHE=/opt/www/memo/.python-eggs

nginxの設定

supervisorのバーチャルホストの設定

upstream supervisor-backend {
  server 127.0.0.1:9001 fail_timeout=0;
}

server {
  listen       80;
  server_name  supervisor.local.saitodev.com;

  access_log  /var/log/nginx/supervisor.local.saitodev.com-access.log main;
  error_log   /var/log/nginx/supervisor.local.saitodev.com-error.log  info;

  location / {
    allow 127.0.0.1;
    allow 192.168.0.0/24;
    deny  all;
    proxy_set_header Host $host;
    proxy_pass http://supervisor-backend;
    break;
  }
}

proxy_set_headerの行が無いと、リダイレクト処理で失敗します。

gunicornのバーチャルホストの設定

upstream memo-backend {
  server unix:/tmp/gunicorn_memo.sock fail_timeout=0;
}

server {
    listen       80;
    server_name  memo.local.saitodev.com;

    access_log  /var/log/nginx/memo.local.saitodev.com-access.log main;
    error_log   /var/log/nginx/memo.local.saitodev.com-error.log  info;

    # djangoのadminの静的ファイルを配信する場合に必要
    location ~ /media/(.*)$ {
      alias /usr/local/lib/python2.7/site-packages/django/contrib/admin/media/$1;
      break;
    }

    location / {
      proxy_set_header Host $host;
      proxy_pass http://memo-backend;
      break;
    }
}

最後にコメント

今回の例では、nginxとgunicornを別ユーザで動かそうとしたため、結果的にsupervisordとgunicornのmasterがrootで動いてしまっています。

実際の運用では、nginxとgunicornとsupervisordを全て同一の非rootユーザで動かした方がセキュリティ的にましな気がしてきました。

2011-05-29

Djangoのモデル継承でポリモーフィズムを実現する

Djangoのモデルクラスは継承が可能ですが、基底クラスのオブジェクトを派生クラスのオブジェクトに変換する手段がデフォルトで存在しないため、普通のオブジェクト指向プログラミングのように、派生クラスでメソッドをオーバーライドしてオブジェクトの振る舞いを変えるといったことができません。

具体例として、次のサンプルプログラムを実行してみます。

  • without_polymorphic/models.py
from django.db import models

class Base(models.Model):
  pass

class Derived1(Base):
  pass

class Derived2(Base):
  pass
  • sample1.py
import settings
from django.core import management
management.setup_environ(settings)
from django.db.transaction import commit_on_success

from without_polymorphic import models

@commit_on_success
def delete_instances():
  for base in models.Base.objects.all():
    base.delete()

@commit_on_success
def create_instances():
  print '='*40
  print 'create_instances'
  print '='*40
  objs = (models.Derived1(), models.Derived1(), models.Derived2(), models.Derived2())
  for obj in objs:
    print obj
    obj.save()

def show_instances():
  print '='*40
  print 'show_instances'
  print '='*40
  for obj in models.Base.objects.all():
    print obj

def main():
  delete_instances()
  create_instances()
  show_instances()

if __name__ == '__main__':
  main()
  • 実行結果
========================================
create_instances
========================================
Derived1 object
Derived1 object
Derived2 object
Derived2 object
========================================
show_instances
========================================
Base object
Base object
Base object
Base object

オブジェクト作成時はオブジェクトのクラスは派生後のクラスですが、データベースから取り出したオブジェクトのクラスは基底クラスとなっていることが分かります。これでは、派生クラスでいくらメソッドをオーバーライドしても、そのメソッドを使うことができません。

(もちろん、オブジェクト取得の部分でmodels.Base.objects.all()の代わりにmodels.Derived1.objects.all()と書けば派生クラスのオブジェクトを得ることが出来ますが、その方法で全てのオブジェクトを得るには、派生クラスを全て知っている必要があります。)


基底クラスのオブジェクトから派生クラスのオブジェクトに変換するには、django-polymorphic-modelsというライブラリを使用します。このライブラリをDjangoのアプリに追加し、次のプログラムを実行してみます。

  • with_polymorphic/models.py
from django.db import models
from polymorphic.models import PolymorphicMetaclass

class Base(models.Model):
  __metaclass__ = PolymorphicMetaclass

class Derived1(Base):
  pass

class Derived2(Base):
  pass
  • sample2.py
import settings
from django.core import management
management.setup_environ(settings)
from django.db.transaction import commit_on_success

from with_polymorphic import models

@commit_on_success
def delete_instances():
  for base in models.Base.objects.all():
    base.delete()

@commit_on_success
def create_instances():
  print '='*40
  print 'create_instances'
  print '='*40
  objs = (models.Derived1(), models.Derived1(), models.Derived2(), models.Derived2())
  for obj in objs:
    print obj
    obj.save()

def show_instances():
  print '='*40
  print 'show_instances'
  print '='*40
  for obj in models.Base.objects.all():
    obj = obj.downcast()
    print obj

def main():
  delete_instances()
  create_instances()
  show_instances()

if __name__ == '__main__':
  main()

最初の例との違いは、Baseクラスのメタクラスをpolymorphic.models.PolymorphicMetaclassにした点と、基底クラスのオブジェクトを派生クラスのオブジェクトに変換するために、downcasetメソッドを実行している点です。

このプログラムの実行結果は次のようになります。

========================================
create_instances
========================================
Derived1 object
Derived1 object
Derived2 object
Derived2 object
========================================
show_instances
========================================
Derived1 object
Derived1 object
Derived2 object
Derived2 object

この例では、派生クラスのオブジェクトを得るためにdowncastメソッドを実行していますが、基底クラスのメタクラスをPolymorphicMetaclassではなくDowncastMetaclassとすると、自動的にdowncastメソッドを実行してくれるようです。

2011-05-15

Qt4のQFileSystemWatcherでファイルの更新検知を行う

プログラマの方々なら、ファイルを更新したときに自動的にあるアクションを実行したいと思うことが、一度はあると思います。

そのようなタスクを実行するプログラムを書くには、ファイルシステムを監視して、ファイルの更新を検知する必要があります。


ファイルの更新を検知する方法として、2つの方法が考えられます。

1つ目の方法は、一定時間間隔でファイルの更新時間をポーリングする方法、2つ目の方法は、OSのAPI(Linuxのinotify等)を使う方法です。

1つ目の方法の場合、監視したいファイルが少数の場合は問題ありませんが、監視したいファイルが増えた場合にパフォーマンスが悪化すると思われます。

2つの方法の場合、OSのAPIを使うためにC言語を書く必要があるため、実装が容易ではありません。

また、基本的にファイル監視のAPIはOS固有のAPIであるため、複数OSで動作しないという問題点もあります。


OSのAPIを直接利用すると、特定OSに依存したプログラムになってしまうことが問題だったわけですが、Qt4のQFileSystemWatcherというライブラリがOSの違いを吸収したインターフェースを提供してくれているので、これを利用することによりファイルの更新検知をOS非依存で行うことができます。

また、Qt4自体はC++のライブラリですが、Python等の他の言語からQt4を利用するためのライブラリも存在も存在するため、それらを利用することで、C/C++といった低水準のプログラミング言語を書くことなく、ファイルシステム監視のAPIを利用することが出来ます。


Qt4のPythonバインディングであるPyQt4を使って、特定のファイルを更新したときに、任意のコマンドを実行するプログラムを書いてみたので、ここで紹介したいと思います。

GUIバージョン

スナップショットはこんな感じです。

f:id:saitodevel01:20110515235928p:image

監視するファイルと実行するコマンドを入力し、監視開始を押すと、ファイルの更新検知を開始します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import subprocess
from PyQt4.QtCore import *
from PyQt4.QtGui  import *

class FileWatchDialog(QWidget):
  def __init__(self, parent=None):
    super(FileWatchDialog, self).__init__(parent)
    self.watcher = QFileSystemWatcher(self)
    self.filename = None
    self.mtime = None
    self.command = None
    self.file_edit = QLineEdit(self)
    self.command_edit = QLineEdit(self)
    self.start_button = QPushButton(u"開始", self)
    self.start_button.setCheckable(True)
    self.browse_button = QPushButton(u"参照", self)

    top_layout = QHBoxLayout()
    top_layout.addWidget(QLabel(u"監視するファイル:", self))
    top_layout.addWidget(self.file_edit)
    top_layout.addWidget(self.browse_button)
    middle_layout = QHBoxLayout()
    middle_layout.addWidget(QLabel(u"コマンド:", self))
    middle_layout.addWidget(self.command_edit)
    bottom_layout = QHBoxLayout()
    bottom_layout.addStretch()
    bottom_layout.addWidget(self.start_button)
    bottom_layout.addStretch()
    main_layout = QVBoxLayout()
    main_layout.addLayout(top_layout)
    main_layout.addLayout(middle_layout)
    main_layout.addLayout(bottom_layout)
    self.setLayout(main_layout)

    self.start_button.toggled.connect(self._on_start_button_toggled)
    self.browse_button.clicked.connect(self._on_browse_button_clicked)
    self.watcher.fileChanged.connect(self._on_file_changed)

  def _on_start_button_toggled(self, checked):
    if checked == True:
      self._start_watching()
    else:
      self._end_watching()

  def _start_watching(self):
    self.filename = unicode(self.file_edit.text())
    if not os.path.exists(self.filename):
      QMessageBox.information(self,
                              u"指定したファイルは存在しません",
                              u"指定したファイルは存在しません")
      return
    self.mtime = os.path.getmtime(self.filename)
    self.command = unicode(self.command_edit.text())
      
    self.start_button.setText(u"終了")
    self.file_edit.setEnabled(False)
    self.browse_button.setEnabled(False)
    self.command_edit.setEnabled(False)
    self.watcher.addPath(self.filename)

  def _end_watching(self):
    self.start_button.setText(u"開始")
    self.file_edit.setEnabled(True)
    self.browse_button.setEnabled(True)
    self.command_edit.setEnabled(True)
    self.watcher.removePath(self.filename)

  def _on_browse_button_clicked(self):
    filename = QFileDialog.getOpenFileName(self)
    if not filename.isNull():
      self.file_edit.setText(filename)

  def _on_file_changed(self, _):
    mtime = os.path.getmtime(self.filename)
    if self.mtime < mtime:
      self.mtime = mtime
      print self.command
      subprocess.call(self.command, shell=True)

if __name__ == '__main__':
  import sys
  app = QApplication(sys.argv)
  win = FileWatchDialog()
  win.show()
  sys.exit( app.exec_() )  

CUIバージョン

第一引数に監視するファイル名を、第二引数にファイル更新時に実行するコマンドを指定します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import os
import subprocess
from PyQt4.QtCore import *
from PyQt4.QtGui  import *

global filename, command, mtime
filename = None
command  = None
mtime = None

def do_command(_):
  global filename, command, mtime
  new_mtime = os.path.getmtime(filename)
  if mtime < new_mtime:
    mtime = new_mtime
    print command
    subprocess.call(command, shell=True)

def excepthook(type, value, traceback):
  if type is KeyboardInterrupt:
    qApp.quit()
  else:
    sys.__excepthook__(type, value, traceback)
sys.excepthook = excepthook

def usage():
  print "usage: python %s <filename> <command>" % __file__

if __name__ == '__main__':
  if len(sys.argv) < 3:
    usage()
    sys.exit(0)

  filename = sys.argv[1]
  command  = sys.argv[2]
  new_mtime = os.path.getmtime(filename)
  
  app = QApplication(sys.argv)
  watcher = QFileSystemWatcher()
  watcher.addPath(filename)
  watcher.fileChanged.connect(do_command)
  timer = QTimer()
  timer.timeout.connect(lambda : 0)
  timer.setInterval(100)
  timer.setSingleShot(False)
  timer.start()
  sys.exit( app.exec_() )

CUIバージョンでは、Ctrl-Cでプログラムが終了するようにsys.excepthookを書き換えるというハックを行っています。

また、PyQt4はC++のQt4の単なるラッパーであるため、イベントループ中はC++のコードが動いています。

そのためか、Ctrl-Cを押しても、その瞬間にはKeyboardInterruptは発生せず、イベントが発生したときにKeyboardInterruptが発生します。

これは、おそらく、Pythonが非同期シグナルを同期的に処理しているため、Pythonのコードが動いていないとKeyboardInterruptが発生しないためだと考えられます、そのため、タイマーを使って定期的にPythonコードを動かし、Ctrl-Cが押された時にKeyboardInterruptがきちんと発生するようにしています。