Python を daemon 化してくれる関数が、標準ライブラリで見つからなかった。
探せばいくらでも有るんだろうけれど、面倒なので Ruby の Process.daemon を真似て作ってみた。

といっても、Ruby 1.9 の lib ディレクトリを見ても、Process.rb というファイルは見つからない。
おそらく C で書かれてコンパイルしていると想像される。
(本当かどうかは知らない)

さすがに Ruby インタープリタのソースを読むのは手間がかかるので、
引数の仕様だけを真似てやってみた。

ちなみに、Python ならではの hack として、
「ライブラリの中で exit したい時は、"sys.exit" ではなく "os._exit" を使う」
という事を最近しったので、ついでにメモしておく。

実は、sys.exit は、実際には例外を投げているだけ。
Python のインタープリタがその例外をキャッチして、各種 buffer の処理などを
よろしくやった上で終了してくれる。
要は、面倒な後処理を Python に任せて安全に(?)終了してくれるのだ。

しかし、ライブラリでこの関数を使用する事は危険だ。
何も知らないプログラマが、上流でこの例外をキャッチしてしまうかもしれない。
sys.exit では本当にプログラムが終了する事は保証できないし、
その他どんな副作用が発生するか予想できない。

そのため、上流行程に関係なく問答無用でプログラムを終了したい場合は
os._exit を使用するらしい。

まあ、ライブラリの中から exit する事なんて多く無いと思うが。

そんな訳で、作成した関数が以下。
いい加減な作りだが、一応動作する。

def daemon(nochdir=False, noclose=False):
    r''' Fork twice and become a daemon.
         @param nodir
             The daemon process does not change the root directory to '/' if True.
             The default is False.
         @param noclose
             Does not close the stdin, stdout, stderr if True.
             The default is True.
    '''

    ## 1st fork
    pid = os.fork()
    if pid != 0:
        os.waitpid(pid, os.P_WAIT)
        os._exit(0)

    ## 2nd fork
    os.setsid()
    pid = os.fork()
    if pid != 0:
        os._exit(0)

    ## daemon process
    # ch '/'
    if not nochdir:
        os.chdir('/')

    # close stdin, stdout, stderr
    if not noclose:
        try:
            os.close(0)
        except OSError:
            pass
        else:
            os.open(os.devnull, os.O_WRONLY)

        for fd in [1, 2]:
            try:
                os.close(fd)
            except OSError:
                pass
            else:
                os.open(os.devnull, os.O_RDONLY)

    return



2011/1/2 Typo 発見。直す
shell スクリプト書いた事のある人は、old-style backquote を使用した事があると思う。
コマンドを ` でくくる、あれだ。

例えば、1から 10までの数字を表示したければ、以下のようにすればよい
$ for i in `seq 10`; do
> echo $i
done


では、文字列をエスケープする時はどうだろう。

最初に、'a^a$a' という文字列から '^' と '$' をエスケープした文字列を表示させる場合を考えよう
(ここでは、` は使う必要がない)
echo 'a^a$a' | sed 's/\^/\\^/g; s/\$/\\$/g'


では、先ほどと同じ文字列 'a^a$a' から "^" と '$' をエスケープした文字列を escaped という変数に代入してみる
` でくくるため、エスケープ後の文字列に \ を余分に入れる必要がある点に注意して、
escaped=`echo 'a^a$a' | sed 's/\^/\\\\^/g; s/\$/\\\\$/g'`
echo $escaped

とかやってみる。
すると、期待に反して以下のように表示される
a\^a$a\$

ご理解いただけただろうか?
'^' は正常にエスケープされたが、'$' はされていない。
さらに、文字列の最後にエスケープされた '$' が加わっている

つまり、` の中の sed では '\^' は '^' という文字にマッチし、'\$' は文字列の末尾にマッチしているのだ。

これは、shell の仕様である。
man を見ると、
$ man sh
:
When the old-style backquote form of substitution is used,
backslash retains its literal meaning except when followed by $, `, or \.
:

と書いてある。
つまり、` の中で、$, `, \ の文字列は特別なのだ。
私が調べた man には記載していなかったが、手元の環境では '*' も同様の挙動を示した。

上記のややこしい仕様を回避するためには、Arithmetic Expansion を使用すれば良い。
つまり、コマンドを` でくくった中に記載するのではなく、'$(' と ')' でくくった中に記載すれば良い。


先ほどの例を Arithmetic Expansion を使用すると、次のようになる。

記のややこしい仕様を回避するためには、コマンドを` でくくった中に記載するのではなく
'$(' と ')' でくくった中に記載すれば良い。
escaped=$(echo 'a^a$a' | sed 's/\^/\\\\^/g; s/\$/\\\\$/g')
echo $escaped

今度は、期待通りに以下のように表示された。
a\^a\$a


shell って、難しい。

twitter にて、_enami さんのご指摘を受けて修正
Arithmetic Expansion は $(...) という形式の事ではない
http://twitter.com/#!/_enami
Python のコードレビュー中に、関数の引数チェックでちょっと指摘をうけた。
「ここ、int である事を確認しているけれど、別に long でもいいんじゃない?」

「たしかにそうだな~」と思いつつ、一方で「long でも収まらない場合はどうするんだろう」とか
考えていた。

そんなわけで python で long より大きい整数を扱うような関数を作ってみようと思い、
とりあえず m 進数の整数を n 進数に直す関数を書いてみた。

やっている事は、m 進数で表記された整数を 0 になるまで順次 n で割り、
その余りを結合しているだけ。

例えば、10進数の 29 を 2進数にする場合の流れは以下
29 割 2 = 14 あまり 1
14 割 2 = 7 あまり 0
7 割 2 = 3 あまり 1
3 割 2 = 1 あまり 1
1 割 2 = 0 あまり 1

よって、10進数の 29 を 2進数にすると 11101 になる

long より大きい整数なので、引数も戻り値も整数を表す文字列になる。
(もっとも、内部では各桁の整数を保存した list で計算している)

表示部分を簡単にするため、今回は 2進数から 10進数までしか扱えないことにした。

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

def change_base(num_str, old_base, new_base):
    ''' Change number base.

    ex) change_base('300', 10, 2) changes decimal 300 to binary-code form'''

    if not isinstance(num_str, str):
        raise TypeError('change_base() called with non-str num_str.')
    if not isinstance(old_base, int):
        raise TypeError('change_base() called with non-int old_base.')
    if not isinstance(new_base, int):
        raise TypeError('change_base() called with non-intr new_base.')

    import re
    if not re.match('^?d+$' num_str):
        raise ValueError('change_base() called with non-positive-number num_str.')
    if not (1 < old_base < 11)
        raise ValueError('change_base() called with out-range old_base.')
    if not (1 < new_base < 11)
        raise ValueError('change_base() called with out-range new_base.')

    num_list = map(lambda(x): int(x), num_str)

    def _divide():
        quotient = []
        remainder = 0

        for n in num_list:
            tmp = remainder * old_base + n
            quotient.append(tmp / new_base)
            remainder = tmp % new_base

        # omit beginning 0 from quotient
        while(True):
            if quotient and quotient[0] == 0:
                del quotient[0]
            else:
                break

        return quotient, remainder

    ret = ''
    while(True):
        num_list, tmp = _divide()
        ret = str(tmp) + ret

        if not num_list:
            break

    return ret

if __name__ == '__main__':
    pass


どうでもいいネタだが、最近更新頻度があまりにも落ちているので。