3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LinuxでHTTPサーバー lighttpdを動かす。プラグインサンプルを作ろう!

Last updated at Posted at 2018-05-27

プラグインを作ってみよう!

プラグインの導入方法の続き、サンプルを作ってみよう!
概要に興味がある方はこちらを参照ください。

環境は変わらず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種類の方法でプラグインを実装してみました。

  1. handle_uri_clean内でそのまま処理を定周期処理を実現する。
  2. 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はクエリ情報なので省略。

mod_loopcgi/mod_loopcgi.c
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

メイン処理はこんな感じ。各関数の詳細は省略します。

  1. mod_loopcgi_parse_queriesで頑張ってクエリをパース
  2. mod_loopcgi_set_commandで利用するコマンドを取得
  3. mod_loopcgi_con_info_newで接続情報を作成、mod_loopcgi_connection_pushでリストに詰めて
  4. mod_loopcgi_con_info_startでコマンド実行開始!
mod_loopcgi/mod_loopcgi.c
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はこんな感じです。

  1. mod_loopcgi_con_info_call_onceでコマンド実行してHTTPレスポンスボディを更新
  2. 指定された回数に満たなかったらsleep
    の繰り返しで、終われば応答を返す形にしています。
mod_loopcgi/mod_loopcgi.c
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です。

mod_loopcgi/mod_loopcgi.c
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_statuscon->file_finished = 1にしてHANDLER_FINISHEDを返せばOK。

mod_loopcgi/mod_loopcgi.c
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レスポンスデータはちゃんと出来ていそうですが、どうでしょうか。
試しにこのようなリクエストを送信してみましょう。

test.sh
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点となります。

  1. fdeventを利用したタイマー制御にする。
  2. 応答の出し方をTransfer-Encodinf:chunkedにする(任意)

Transfer-Encodinf:chunked

今回のような、**HTTPレスポンスサイズが決まっていない、かつ定期的にレスポンスデータは送信したい!**というケースで利用されるHTTP 1/1のレスポンス方式です。
ルールは3点。

  1. レスポンスヘッダーにTransfer-Encoding:chunkedとつけ、Content-Lengthは設定しない。
  2. 送信するデータの先頭にデータ長\r\nを張り付ける。
  3. 送信するデータの最後に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を挟んで繰り返しやっていた処理をここに詰めました。
ポイントは以下です。

  1. 戻り値でHTTPレスポンスを送信するかコントロールする。
  2. eventのコールバックにはconが含まれないので、登録したポインタに持たせる必要がある。
  3. joblist_append(srv, this->con);を実行しないと次の何かしらのイベントまで応答を待たされるので、とりあえず呼ぶ。(呼びすぎても問題はなかったはず)
  4. 受信データはちゃんとreadする。(lighttpdがまたイベントを投げてきました笑)

受信が完了したらmod_loopcgi_append_end_of_chunkmod_loopcgi_finishedで200 OKを返すようにします。これでHTTPレスポンスが送信されるようになります。
mod_loopcgi_append_end_of_chunkはlighttpdが設定してくれないchunked形式の最後"0\r\n\r\n"を自分で設定しています。

mod_loopcgi_fd/mod_loopcgi.c
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を呼んでいます。

  1. returnで**HANDLER_WAIT_FOR_EVENTを返す**
  2. con->file_started=1にする。⇒chunked形式にはなるけどクライアントへの送信が最後まで行われない。
  3. con->file_finished=0にしない⇒これがlighttpd内部でのデータ送信フラグになっているので、データ送信終了扱いされてしまいます。
mod_loopcgi_fd/mod_loopcgi.c
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でタイマー送信を開始しています。

mod_loopcgi_fd/mod_loopcgi.c
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レスポンスデータがそろわないと送信してくれません。

lighttpd.conf
## 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を実行するようにしてます。

mod_loopcgi.c
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でパスを取り出すようにします。

mod_loopcgi.c
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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?