はじめに
cgiとは
Common Gateway Interface(コモン・ゲートウェイ・インタフェース、CGI)は、ウェブサーバ上でユーザプログラムを動作させるための仕組み
wikipediaより。
例えばLinuxコマンドのようなそのOS上で実行可能なプログラム・コマンドをHTTPを介して利用しよう!というものです。
私の知っている範囲では、cgiとして、標準出力にHTTPレスポンスを出力すれば、出力内容をクライアントに送信してくれるという仕組みが多いです。
lighttpdのcgiもそんな仕組みで、STDOUTをがっつり読み込んで結果を送信します。
シングルスレッドなのでこれでいいですが、マルチスレッドでcgiを実現するにはどうしよう。STDOUTは1つなのでバッティングしちゃうんですよね。
せっかくFDイベント待ち受けの仕組みがあるのでそれは活かしたいし、どうせならC言語以外も使いたいな
スクリプト言語でよくあるsocketの仕組みを使って設計してみるかってのが今回の主題になります。
案1:コマンドサーバーを作って実行してもらおう!
ruby, python等のスクリプト言語で作るソケット通信例で、よく標準出力をソケットにリダイレクトする仕組みを見かけます。
そうか、HTTPサーバーはSTDOUTの代わりに別途作成したUnix Socketを待ち受けて、コマンド実行は別のサーバーに任せればいいか。
ということで設計。コマンドサーバーに実際のコマンドはお任せします。

サンプルもサクッと実装。備えの充実した言語、素晴らしいです。
CommandService
というコマンドサーバー用クラスを作成。
HTTPサーバー⇒send_to_server
を使ってsend
コマンドサーバー⇒run_server
でrecv, 受信後コマンド実行, 応答送信という仕組みにしました。
ちなみにCommandService
クラスにsend/recv両方導入したのは、コマンドサーバーが待ち受けるunix socketのファイルを隠ぺいしたかったから。
もちろんUnix socketの代わりにポートをあけて通信してもOKです。
2018/07/08 デリミタが","だとコマンド内で利用される可能性があったので、self._delimiter
定義を追加しました。
# for socket
import os, sys, socket
# for exec command
import subprocess
class CommandService:
#constructor, set socket path
def __init__(self):
self._socket_path="/var/run/command_service.sock"
self._delimiter="__ComSvc__"
#for server
def run_server(self):
print("Start server")
waitsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
try:
waitsock.bind(self._socket_path)
while True:
req_cmd_byte = waitsock.recv(1024)
if not req_cmd_byte: break
self._exec_request(req_cmd_byte.decode('utf-8').split(self._delimiter))
except socket.error as msg:
print(msg)
except:
print("Execute")
finally:
waitsock.close()
os.unlink(self._socket_path)
print("Exit server")
#parse request, call command and send response
def _exec_request(self, req_cmd):
command=req_cmd[0]
sockpath=req_cmd[1]
print("comamnd:" + command)
print("socket:" + sockpath)
callresp = self._call_cmd(command.split(" "))
if len(callresp) != 0 :
self._send(callresp.encode('utf-8'), sockpath)
#call system command
def _call_cmd(self, args):
try:
res_byte = subprocess.check_output(args)
res=res_byte.decode('utf-8')
except:
print('Failed, command:', args )
res=''
return res
#for client
def send_to_server(self, command, response_sockpath):
request=command + self._delimiter + response_sockpath
self._send(request.encode('utf-8'), self._socket_path)
def _send(self, request, sockpath):
sendsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
try:
sendsock.sendto(request, sockpath)
#sendsock.sendto(b"test, command", self._socket_path)
except socket.error as msg:
print(msg)
finally:
sendsock.close()
軽く解説を入れると、サーバー側run_server
はこんな流れで動作しています。
-
socket.socket
でソケット作成 -
bind
で隠ぺいしているunix socketと紐づけ recv
- 受信データを
_exec_request
でコマンド実行。実際にコマンド実行している箇所は_call_cmd
内subprocess.check_output
で。["echo", "test"]みたいなコマンド引数の配列を入力としてコマンド実行します。 - 結果を送信
注意点は以下です。
・sendto
はbyteデータを扱うのでencode('utf-8')
やb"文字列"
でbyte列に変換してあげること
・recv
, subprocess.check_output
は逆にbyte列で応答が返るのでdecodeしてあげること
後は以下のようにコマンドサーバーでrun_server
を実行する処理を書けばOK
from CommandService import CommandService
def main():
command=CommandService()
command.run_server()
if __name__ == '__main__':
main()
試しにechoコマンドを投げる以下のようなCommandClient
と、HTTPサーバー側の代わりに/tmp/resp_sockを待ち受けるサーバーをコピペで作成(省略)してテスト。環境内のpythonのバージョンはpython3.6です。
from CommandService import CommandService
def main():
command=CommandService()
command.send_to_server("echo -n test", "/tmp/resp_sock")
if __name__ == '__main__':
main()
サーバーを起動してCommandClient.py
を実行すると、こんな感じに出力が出ます。
無事/tmp/resp_sock側にはecho -n test
の出力結果が渡りました。
# python3.6 CommandServer.py
Start server
comamnd:echo -n test
socket:/tmp/resp_sock
# python3.6 TestService.py
Start server
test
2018/07/07 抜け漏れがあったので続きを記載
python 環境変数, stdinを制御してコマンドを実行する。
案2:forkしてコマンド実行すればよくね?
記事を書きつつ仕様を整理しながら思いました。これサーバーまで作る必要なくね?と。
…popenなりで直接_exec_request
の処理を呼べるようにすりゃいいじゃん。
まあ自分はfork
があまり好きじゃないので案1で行きますが。
いずれにせよ実現性が見えたのでC側は明日にしよう。満足したので寝ます
参考
socket参考:
PythonでUnixドメインソケットを使って通信する
リファレンス
https://docs.python.org/ja/3.6/library/socket.html#socket.socket.sendto
注意点の参考
対処方法 TypeError: a bytes-like object is required, not 'str'