以前、ロックファイルだけを使ったバージョンを書いたのですが、不評だった(?)ので、pidファイルを使ったバージョンを作りました。デコレータとして定義したので使うのは簡単。以下のようにするだけです。
@singleprocess
def main();
pass
動作的には、以下のようになっています。
- メインの処理を始める前に、pidファイルにpidを書き込む。処理が終わったらpidファイルを消す。
- すでに、このpidファイルに書き込まれたpidのプロセスが、自プロセスと同じシグネチャを持っている(同じプログラムであると認められる)ときは、すでに同じプログラムが動作していると判断して起動しない。
- プロセスの同一性を判断するためのシグネチャとしては、コマンドラインの第1パラメータ(python)と第2パラメータ(pythonファイル名)からディレクトリ名を除いた部分を使う。
- これだけだと、pidファイルの読み書きのタイミングが重なったときに整合性に問題が起きるため、pidファイルの読み書き時にはロッキング機構を入れている。これは通常のflockを使っている。
- このため、pidファイルの読み書き中に異常終了した場合は、手動でpidファイルを削除しないと起動しなくなる可能性があるが、メイン処理の異常終了の影響は受けない。pidファイルの読み書き中の異常終了はそうそう起きないので、実用上はこれで問題ない。
pidファイルは/tmp以下に置かれる仕様で、セキュリティは考慮されていません。実運用で使うためには、用途に応じて、getSignatureのアルゴリズムとpidファイルの置き場所を変更する必要があるかもしれません。
def singleprocess(func):
def basenameWithoutExt(path):
return os.path.splitext(os.path.basename(path))[0]
def getSignature(pid):
processDirectory = '/proc/%d/' % (pid,)
if os.path.isdir(processDirectory):
args = open(processDirectory + 'cmdline').read().split("\x00")
return [os.path.basename(args[0]), os.path.basename(args[1])]
return None
@contextmanager
def filelockingcontext(path):
open(path, "a").close() #ensure file to exist for locking
with open(path) as lockFd:
fcntl.flock(lockFd, fcntl.LOCK_EX)
yield
fcntl.flock(lockFd, fcntl.LOCK_UN)
def wrapper(*args, **kwargs):
pidFilePath = tempfile.gettempdir() + '/' + basenameWithoutExt(sys.argv[0]) + '.pid'
with filelockingcontext(pidFilePath):
runningPid = ""
with open(pidFilePath) as readFd:
runningPid = readFd.read()
if runningPid != "" and getSignature(int(runningPid)) == getSignature(os.getpid()) is not None:
print("process already exists", file=sys.stderr)
exit()
with open(pidFilePath, "w") as writeFd:
writeFd.write(str(os.getpid()))
try:
func(*args, **kwargs)
finally:
with filelockingcontext(pidFilePath):
os.remove(pidFilePath)
return wrapper