プラグインを作ってみよう!
プラグインの導入方法の続き、サンプルを作ってみよう!
概要に興味がある方はこちらを参照ください。
環境は変わらずUbuntu 18.04 デスクトップ、lighttpdのバージョンは1.4.48です。
lighttpdプラグインサンプル紹介
コードはこちらに置いてありますので興味がある方は参照ください。
https://github.com/developer-kikikaikai/lighttpd_setting/tree/master/plugin
mod_loopcgi, mod_loopcgi_fdというフォルダ内にプラグインのコードが格納してあります。
動かしてみたい場合は、lightttpd_settingファイルのLIGHTTPD_PATHを利用されるのパスに変えてご利用ください。
cgiも同リポジトリに格納してあります。
仕様
以前に作ったcgi名を指定すると、そのcgiが利用しているLinuxコマンドを定周期実行する感じにします。シングルスレッドという特徴がわかるよう、良くない例と通常の例の2種類作っています。
定義
- /loopcgi?cgi=cginame[&interval=sec][&count=count_number]
URLパス: /loopcgi
クエリ概要
クエリ | 説明 | 備考 |
---|---|---|
cgi | 実行するcgi名を指定します。 | 必須パラメータ |
interval | 定周期取得するインターバル(秒)を指定します。 | デフォルト1秒 |
count | 何回取得するかを指定します。 | デフォルトはセッションクローズまで永久に取得 |
サンプル紹介
上記に仕様に対して、以下2種類の方法でプラグインを実装してみました。
- handle_uri_clean内でそのまま処理を定周期処理を実現する。
- handle_uri_clean内でイベント登録、イベントトリガーで処理を実現する。
今回のloopcgi仕様だと、インターバルや取得回数が多ければ多いほどHTTPレスポンスを返しきるまで終わらない為、lighttpdのプラグイン仕様としては1つめが良くない例、2が通常の例となります。(どちらでも実装できれば作る方は楽なんですけどね)
handle_uri_clean内でそのまま定周期処理を実現する場合
plugin_data
plugin_configはcgiパス確認で利用。mod_loopcgi_request_info_t
は自分あての全HTTPリクエストを管理するための構造体で、LOOPCGI_CON_INFOという1 HTTPリクエスト用の情報を詰めたリストになっています。
mod_loopcgi_req_setting_tはクエリ情報なので省略。
typedef struct mod_loopcgi_con_info_t *LOOPCGI_CON_INFO;
struct mod_loopcgi_con_info_t {
LOOPCGI_CON_INFO next;
LOOPCGI_CON_INFO prev;
connection *con;//set con pointer only for checking connection is same or not
int current_count;//current count
mod_loopcgi_req_setting_t setting;
};
typedef struct mod_loopcgi_request_info_t {
LOOPCGI_CON_INFO head;
LOOPCGI_CON_INFO tail;
} mod_loopcgi_request_info_t;
typedef struct {
buffer *cgidir;
} plugin_config;
/*plugin data*/
typedef struct {
PLUGIN_DATA;
plugin_config conf;
mod_loopcgi_request_info_t request_info;
} plugin_data;
handle_uri_clean
メイン処理はこんな感じ。各関数の詳細は省略します。
-
mod_loopcgi_parse_queries
で頑張ってクエリをパース -
mod_loopcgi_set_command
で利用するコマンドを取得 -
mod_loopcgi_con_info_new
で接続情報を作成、mod_loopcgi_connection_push
でリストに詰めて -
mod_loopcgi_con_info_start
でコマンド実行開始!
URIHANDLER_FUNC(mod_loopcgi_uri_handler) {
UNUSED(srv);
plugin_data *p = p_d;
//check request url, if request uri is different to accept_uri, skip
if (!mod_loopcgi_is_ownreq(con->request.uri, LOOPCGI_ACCEPT_URL)) return HANDLER_GO_ON;
//parse query
parse_query_t queries[] = {
{support_queries[INDEX_CGI], NULL},//cgi
{support_queries[INDEX_INTERVAL], NULL},//interval
{support_queries[INDEX_COUNT], NULL},//count
{NULL, NULL},//count
};
mod_loopcgi_parse_queries(con, queries);
//check cgi
if(!queries[INDEX_CGI].value) {
DEBUG_ERRPRINT("error, There is no cgi query\n");
log_error_write(srv, __FILE__, __LINE__, "s", "There is no cgi query");
return mod_loopcgi_failed(con, 400);
}
mod_loopcgi_req_setting_t setting;
//set cgi command
if(mod_loopcgi_set_command(&p->conf, queries[INDEX_CGI].value, &setting)) {
DEBUG_ERRPRINT("error, Filed to set command\n");
log_error_write(srv, __FILE__, __LINE__, "s", "Filed to set command");
return mod_loopcgi_failed(con, 400);
}
//set interval
setting.interval=LOOPCGI_DEFALUT_INTERVAL;
if(queries[INDEX_INTERVAL].value) {
int tmp_interval=atoi(queries[INDEX_INTERVAL].value);
//only accept positive interval
if(0 < tmp_interval) setting.interval = tmp_interval;
}
//set count
setting.count_max= LOOPCGI_DEFAULT_LOOPCOUNT;
if(queries[INDEX_COUNT].value) setting.count_max=atoi(queries[INDEX_COUNT].value);
LOOPCGI_CON_INFO instance = mod_loopcgi_con_info_new(con, &setting);
mod_loopcgi_connection_push(&p->request_info, instance);
return mod_loopcgi_con_info_start(instance, con);
}
肝心のmod_loopcgi_con_info_start
はこんな感じです。
-
mod_loopcgi_con_info_call_once
でコマンド実行してHTTPレスポンスボディを更新 -
指定された回数に満たなかったらsleep
の繰り返しで、終われば応答を返す形にしています。
static handler_t mod_loopcgi_con_info_start(LOOPCGI_CON_INFO this, connection *con) {
//loop calling command
while(1) {
if(mod_loopcgi_con_info_call_once(this, con)) {
//failed to call
return mod_loopcgi_failed(con, 500);
}
if(mod_loopcgi_con_info_is_finished(this)) break;
sleep(this->setting.interval);
}
return mod_loopcgi_finished(con, 200);
}
また、HTTPレスポンスボディの更新はこのような感じです。
buffer
を作って、chunkqueue_append_buffer
等でHTTPレスポンスボディcon->write_queue
を更新していきます。
buffer_XXX
, chunkqueue_append_buffer
は公式のAPIです。
static int mod_loopcgi_con_info_call_once(LOOPCGI_CON_INFO this, connection *con) {
FILE * fp = popen(this->setting.command, "r");
int ret=-1;
if(!fp) return -1;
//バッファー作成。
buffer *b = buffer_init();
char result_buf[256]={0};
char * result;
while(fgets(result_buf, sizeof(result_buf), fp) != NULL) {
//データをbuffer_append_string_lenなど、buffer_XXX系のAPIで詰める。
buffer_append_string_len(b, result_buf, strlen(result_buf));
}
//if there is a response, add information to chunk
if(!buffer_is_empty(b)) {
chunkqueue_append_buffer(con->write_queue, b);
ret = 0;
}
buffer_free(b);
pclose(fp);
this->current_count++;
return ret;
}
最後はcon->http_status
、con->file_finished = 1
にしてHANDLER_FINISHEDを返せばOK。
static inline handler_t mod_loopcgi_finished(connection *con, int http_status) {
con->http_status = http_status;
con->file_finished = 1;
return HANDLER_FINISHED;
}
アクセス結果
HTTPレスポンスデータはちゃんと出来ていそうですが、どうでしょうか。
試しにこのようなリクエストを送信してみましょう。
curl -g "http://192.168.100.102/loopcgi?cgi=cpu.py&interval=1&count=3" &
curl -g "http://192.168.100.102/cgi-bin/loadavarage.py" &
cpu.py, loadavarage.pyそれぞれをcgiとして単独実行した結果はこうなります。
$ curl -g "http://192.168.100.102/cgi-bin/cpu.py"
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 5182680 143144 1151500 0 0 3 3 15 24 0 0 99 0 0
curl -g "http://192.168.100.102/cgi-bin/loadavarage.py"
15:39:16 up 23:21, 3 users, load average: 0.00, 0.00, 0.00
test.shの実行結果はこうなりました。
$./test.sh
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 5180580 143168 1151516 0 0 3 3 15 24 0 0 99 0 0
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 5185052 143168 1151516 0 0 3 3 15 24 0 0 99 0 0
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 5185392 143168 1151528 0 0 3 3 15 24 0 0 99 0 0
15:40:22 up 23:23, 3 users, load average: 0.00, 0.00, 0.00
HTTPリクエスト自体は同時に送信しているけど、loopcgiの処理が終わってからloadavarage.pyが実行されていますね。
次のHTTPリクエスト処理が待たされているような感じです。これはまずいですね(-_-;)
何が問題だったのか?
理由は明白ですね。handle_uri_clean
内でsleepしながらwhileループしているため、別のHTTPリクエスト処理がブロックされてしまっています。
lighttpdはシングルスレッドなので、handle_uri_clean内で時間がかかる処理をするとこうなります。
handle_uri_clean内でイベント登録、イベントトリガーで処理を実現する。
lighttpd側で用意されているfdeventの機能を使います。これは自分で用意したファイルディスクリプタのイベントをlighttpd側に拾ってもらうようにする機能になります。
Linuxにもtimerfd_createというファイルディスクリプタに定周期でイベントを発行する機能があるのでこれを利用しましょう!
変更点概要
ざっくりと以下2点となります。
- fdeventを利用したタイマー制御にする。
- 応答の出し方をTransfer-Encodinf:chunkedにする(任意)
Transfer-Encodinf:chunked
今回のような、**HTTPレスポンスサイズが決まっていない、かつ定期的にレスポンスデータは送信したい!**というケースで利用されるHTTP 1/1のレスポンス方式です。
ルールは3点。
- レスポンスヘッダーにTransfer-Encoding:chunkedとつけ、Content-Lengthは設定しない。
- 送信するデータの先頭にデータ長\r\nを張り付ける。
- 送信するデータの最後に0\r\n\r\nを設定する。
こちらの方の説明がわかりやすかったです。
今回のケースで最適です。lighttpdではヘッダーの設定と設定ファイルの設定追加し、chunkqueue_append_buffer(con->write_queue, b);
ではなくhttp_chunk_append_buffer(srv, con, b);
を利用すれば、1, 2はやってくれます。(3はやってくれなかった。)
今回はTransfer-Encoding:chunked形式(以下chunked形式)を採用します。
変更点詳細
plugin_data
まずはtimer用のtimerfdと、fdeventで利用されるidとしてfdevent_idの定義を追加。
LOOPCGI_CON_INFO prev;
connection *con;//set con pointer only for checking connection is same or not
int current_count;//current count
+ int timerfd;//timer fd for using timerfd API
+ int fdevent_id;//for set/unset event
mod_loopcgi_req_setting_t setting;
};
その後、LOOPCGI_CON_INFO生成時にtimerfd_create
でファイルディスクリプタtimerfdを生成。
fdevent_registerでfd, イベント用関数fdevent_handler, 関数に渡されるポインタを登録
⇒fdevent_event_setでイベントを設定
これで、timerfdに書き込みがあるとfdevent_handlerとして設定したmod_loopcgi_con_info_calleventが呼ばれるようになります。
※注意: info->fdevent_id=-1;を入れてからfdevent_event_setを呼んだらlighttpdが落ちたのでご注意ください。
! static LOOPCGI_CON_INFO mod_loopcgi_con_info_new(server *srv, connection *con, mod_loopcgi_req_setting_t *req_setting) {
LOOPCGI_CON_INFO info = calloc(1, sizeof(*info));
info->con = con;
memcpy(&info->setting, req_setting, sizeof(info->setting));
+
+ //add timer for using fdevent_register
+ info->timerfd=timerfd_create(CLOCK_REALTIME, 0);
+
+ //register and set event
+ fdevent_register(srv->ev, info->timerfd, mod_loopcgi_con_info_callevent, info);
+ info->fdevent_id=-1;
+ fdevent_event_set(srv->ev, &info->fdevent_id, info->timerfd, FDEVENT_IN);
+
return info;
}
fdevent_handler
fdevent_handlerとして登録したmod_loopcgi_con_info_callevent
はこんな感じです。sleepを挟んで繰り返しやっていた処理をここに詰めました。
ポイントは以下です。
- 戻り値でHTTPレスポンスを送信するかコントロールする。
- eventのコールバックにはconが含まれないので、登録したポインタに持たせる必要がある。
-
joblist_append(srv, this->con);
を実行しないと次の何かしらのイベントまで応答を待たされるので、とりあえず呼ぶ。(呼びすぎても問題はなかったはず) - 受信データはちゃんとreadする。(lighttpdがまたイベントを投げてきました笑)
受信が完了したらmod_loopcgi_append_end_of_chunk
⇒mod_loopcgi_finished
で200 OKを返すようにします。これでHTTPレスポンスが送信されるようになります。
mod_loopcgi_append_end_of_chunk
はlighttpdが設定してくれないchunked形式の最後"0\r\n\r\n"を自分で設定しています。
static handler_t mod_loopcgi_con_info_callevent(struct server *srv, void *ctx, int revents) {
LOOPCGI_CON_INFO this = (LOOPCGI_CON_INFO) ctx;
//read buffer
if(revents&FDEVENT_IN) {
//read buffer to finish this event
uint64_t buffer;
read(this->timerfd, &buffer, sizeof(buffer));
}
//To go next loop, I have to add joblist_append
joblist_append(srv, this->con);
if(mod_loopcgi_con_info_call_once(this, srv, this->con)) {
//failed to call
return mod_loopcgi_failed(this->con, 500);
}
if(mod_loopcgi_con_info_is_finished(this)) {
mod_loopcgi_con_info_stoptimer(this);
//add 0 buffer to end of chunk
mod_loopcgi_append_end_of_chunk(this->con);
return mod_loopcgi_finished(this->con, 200);
} else {
return mod_loopcgi_wait_event(this->con);
}
}
まだ定期収集を続ける場合はmod_loopcgi_wait_event
を呼んでいます。
- returnで**
HANDLER_WAIT_FOR_EVENT
を返す** - con->file_started=1にする。⇒chunked形式にはなるけどクライアントへの送信が最後まで行われない。
- con->file_finished=0にしない⇒これがlighttpd内部でのデータ送信フラグになっているので、データ送信終了扱いされてしまいます。
static inline handler_t mod_loopcgi_wait_event(connection *con) {
con->file_started=1;
con->file_finished = 0;
return HANDLER_WAIT_FOR_EVENT;
}
handle_uri_clean
handle_uri_cleanのメインで呼ぶmod_loopcgi_con_info_start
をイベントに合わせて変更。
chunked形式の設定とcon->file_started=1;を設定し、mod_loopcgi_con_info_callevent
実行。
必要ならmod_loopcgi_con_info_starttimerでタイマー送信を開始しています。
static handler_t mod_loopcgi_con_info_start(LOOPCGI_CON_INFO this, server *srv, connection *con) {
//set transfer_encoding:chunked
response_header_append(srv, con, CONST_STR_LEN("Transfer-Encoding"), CONST_STR_LEN("chunked"));
con->file_started=1;
handler_t response = mod_loopcgi_con_info_callevent(srv, this, FDEVENT_OUT);
if(response == HANDLER_WAIT_FOR_EVENT) mod_loopcgi_con_info_starttimer(this);
return response;
}
後はfail safe的なチェック処理や引数の微修正をしたくらいで大筋の方針は変えず。
その他
他にはchunked形式送信の為にはlighttpdの設定ファイル変更も必要です。これが無いと結局最後までHTTPレスポンスデータがそろわないと送信してくれません。
## Option to use chunk
server.stream-response-body = 1
アクセス結果
同じスクリプトを実施してみました。
1度目のcpu.py結果の受信後にloadavarage.pyの結果であるload averageの文字が見えます。
ちゃんとブロックもされず、chunked形式で定期通知も出来ていることがわかります。
$./test.sh
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 5181748 144668 1153044 0 0 3 2 15 24 0 0 100 0 0
16:43:52 up 1 day, 26 min, 3 users, load average: 0.00, 0.00, 0.00
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 5182384 144668 1153048 0 0 3 2 15 24 0 0 100 0 0
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 5182376 144668 1153052 0 0 3 2 15 24 0 0 100 0 0
プラグインサンプル その他
設定値の読み込み (set_defaults) 処理について
スケルトンコードにあるようなconfig_storage
を作ってパースっての実装、面倒なので最初から必要なものだけ取り出すための関数mod_loopcgi_patch_connection
を用意しました。
config_values_t cv
には詰め込みたいデータを詰めておきます。スケルトンコードのようにconfig_contextのループの度に用意する必要ななし。
該当する分だけcv_tmp
の方に詰めてから、config_insert_values_globalを実行するようにしてます。
static int mod_loopcgi_patch_connection(server *srv, data_config const* dc, config_values_t *cv, config_values_t *cv_tmp, config_scope_type_t scope) {
int i=0, cv_index=0, cv_tmp_index=0;
for (i = 0; i < dc->value->used; i++) {
data_unset *du = dc->value->data[i];
//この辺で該当する設定なのかを
for(cv_index=0, cv_tmp_index=0; cv[cv_index].key; cv_index++) {
if (buffer_is_equal_string(du->key, cv[cv_index].key, strlen(cv[cv_index].key))) {
//add to tmpcv
memcpy(&cv_tmp[cv_tmp_index++], &cv[cv_index], sizeof(config_values_t));
}
}
//NULL追加
cv_tmp[cv_tmp_index]=cv[cv_index];
//load settings
if (0 != config_insert_values_global(srv, dc->value, cv_tmp, scope)) {
return -1;
}
}
return 0;
}
set_defaults
本体はこんな感じ。今後もp->confを参照すれば設定値をそのまま見ることが出来る状態になります。
今回のケースだと"alias.url"の設定値をarray * alias
に詰めてもらいます。
また、"alias.url"もcgiの実行パスを検索するために取得しただけなので、mod_loopcgi_get_cgipath
でパスを取り出すようにします。
SETDEFAULTS_FUNC(mod_loopcgi_set_defaults) {
plugin_data *p = p_d;
if (!p) return HANDLER_ERROR;
config_values_t cv[] = {
{ "alias.url", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION },/* 0, to load cgi dir */
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
};
//set data to load conf
array * alias = array_init();
cv[0].destination = alias;
//create tmp cv
config_values_t *cv_tmp = calloc(sizeof(cv)/sizeof(cv[0]), sizeof(config_values_t));
//initalize memory
mod_loopcgi_conf_init(&p->conf);
size_t i = 0;
for (i = 0; i < srv->config_context->used; i++) {
data_config const* config = (data_config const*)srv->config_context->data[i];
//patch connection first
if(mod_loopcgi_patch_connection(srv, config, cv, cv_tmp, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
log_error_write(srv, __FILE__, __LINE__, "s",
"unexpected value for mod_loopcgi config;");
}
}
//aliasもパースしてcgidirだけ取り出す
mod_loopcgi_get_cgipath(alias, p->conf.cgidir);
//tempで作ったものは削除
array_free(alias);
free(cv_tmp);
return HANDLER_GO_ON;
}
後はサンプルのhandle_uri_cleanで実装した、con->request.uriのチェックとかqueryのパースなんかも公式のAPIを用意してほしいところですね。
構造体の中身をこちらがいじってコントロールするのは仕様変更の影響をもろに受けるので。気付いてないだけで公式にもあるのかな。
参考
Transfer-Encoding: chunked について
lighttpdのchunked設定
公式wiki, Server stream-response-bodyDetails