2014/7/28

erlang - rebar

本文接續上一篇對 rebar 的簡介,說明有關取得相依套件, 建構 release package 以及程式不關機直接升級的問題。

Templates

如果要使用自己的 template,就把 mytemplate.template 放到 templates 目錄中,並執行以下指令

rebar create template=mytemplate

rebar 提供了幾個內建的 templates

Template Variables Command Alias Command
simplesrv srvid rebar create template=simplesrv X
simplenode nodeid rebar create template=simplenode rebar create-node
simplemod modid rebar create template=simplemod X
simplelib libid rebar create template=simplelib rebar create-lib
simplefsm fsmid rebar create template=simplefsm X
simpleapp appid rebar create template=simpleapp rebar create-app
ctsuite testmod rebar create template=ctsuite X
basicnif module rebar create template=basicnif X

管理發行版本

rebar 利用 reltool.config 建立執行的節點。

如果一個專案中,包含了多個 OTP applicaitons,我們可以建立一個 app 目錄,並把 otp application 移到 app 目錄中。

mkdir apps
cd apps

mkdir myapp
cd myapp

rebar create-app appid=myapp

cd ../../

編輯檔案 rebar.config

{sub_dirs, ["apps/myapp", "rel"]}.

然後就可以編譯專案

rebar compile

建立 release 目錄與檔案

mkdir rel
cd rel
rebar create-node

修改 reltool.config

line 4
        {lib_dirs, ["../apps"]},

line 12 mynode 改為 myapp
        [
         kernel,
         stdlib,
         sasl,
         myapp
        ]},

line 27 改為
        {app, myapp, [{mod_cond, app}, {incl_cond, include}, {lib_dir, ".."}]}

產生 build release

cd ..
rebar -v generate

使用 rel/mynode/bin/mynode 啟動與停止節點

> rel/mynode/bin/mynode
Usage: mynode {start|start_boot <file>|foreground|stop|restart|reboot|ping|console|getpid|console_clean|console_boot <file>|attach|remote_console|upgrade}

啟動 app 後,進入 console 互動
> rel/mynode/bin/mynode console
(mynode@127.0.0.1)1> application:which_applications().
[{sasl,"SASL  CXC 138 11","2.3.4"},
 {myapp,[],"1"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"},
 {kernel,"ERTS  CXC 138 10","2.16.4"}]


在背景啟動或停止 app
> rel/mynode/bin/mynode start
> rel/mynode/bin/mynode stop

增加 deps

專案通常會使用一些第三方的專業套件,我們必須要先修改 rebar.config。
必須增加 deps tuple 如下
{deps, [Dependency1, dependency2, ...]}

每一個 Dependency1 內容如下
{App, VsnRegex, Source}

Source 指定該 library 的來源,有以下幾種選擇。

  1. {hg, Url, Rev}
    mercury repository
  2. {git, Url}
  3. {git, Url, {branch, Branch}}
  4. {git, Url, ""} 等同於 {git, Url, {branch, "HEAD"}}
  5. {git, Url, {tag, Tag}}
  6. {git, Url, Rev}
  7. {bzr, Url, Rev}
    bazaar repository

以下這個例子,使用了 cowboy。

{deps, [
    {cowboy, "",
        {git, "git://github.com/extend/cowboy.git",{branch, "master"}}}
        ]}.
{sub_dirs, ["apps/myapp", "rel"]}.

我們可以在 command line 用以下指令,從 git 取得 cowboy 相關的 libraries,包含了 cowlib, ranch與cowboy,程式碼都會 clone 到 deps。

rebar get-deps
rebar update-deps

如果專案 app 裡的程式碼使用了 cowboy,編譯時,也必須把 deps 相關的 libs 包含進去。

修改 reltool.config

line 4 增加 ../deps
        {lib_dirs, ["../apps", "../deps"]},

line 12 mynode 改為 myapp
        [
         kernel,
         stdlib,
         sasl,
         myapp
        ]},

line 27
        {app, mynode, [{incl_cond, include}]},
        {app, cowboy, [{incl_cond, include}]}

Makefile

可以做個簡單的 Makefile,簡化重複輸入指令的麻煩。

all: compile

deps:
    rebar get-deps
    rebar update-deps

compile:
    rebar compile

clean:
    rebar clean

test: compile
    rebar eunit skip_deps=true

release: compile
    rebar -v generate
    mkdir -p ./rel/mynode/priv
    cp -r ./apps/myapp/priv/ ./rel/mynode/priv/

.PHONY: all deps compile

Upgrades

把 apps/myapp/src/myapp.app.src and rel/reltool.conf 兩個檔案的版本號碼 vsn 都從 1 改為 2

在 rebar 有個測試的 dummy project 可用來驗證 OTP 程式線上直接升級的程序。

在取得 dummy project source code 時,一開始是設定為 0.1 版,所以先編譯然後就啟動 dummy server。

rebar compile
rebar generate
mv rel/dummy rel/dummy_0.1
rebar clean

啟動 dummy server

cd rel/dummy_0.1
bin/dummy console

(dummy@127.0.0.1)1> dummy_server:get_state().
0
(dummy@127.0.0.1)2> dummy_server:set_state(123).
{ok,123}
(dummy@127.0.0.1)3> dummy_server:get_state().
123

在另一個 terminal,進行 0.2 版的編譯

先將版本號碼從 0.1 版改為 0.2

vi apps/dummy/src/dummy.app.src
vi rel/reltool.config

編譯並封裝 0.2 版

rebar compile
rebar generate

rebar generate-appups previous_release=dummy_0.1
rebar generate-upgrade previous_release=dummy_0.1

這一行是一個測試 dummy_0.2.tar.gz 的壓縮檔
tar -zvtf rel/dummy_0.2.tar.gz

將 0.1 版 升級到 0.2 版

mv rel/dummy_0.2.tar.gz rel/dummy_0.1/releases/

回到剛剛 0.1 版的 console
(dummy@127.0.0.1)6> release_handler:unpack_release("dummy_0.2").
{ok,"0.2"}
(dummy@127.0.0.1)7> release_handler:install_release("0.2").
{ok,"0.1",[]}
(dummy@127.0.0.1)8> release_handler:make_permanent("0.2").
ok
(dummy@127.0.0.1)9> release_handler:which_releases().
[{"dummy","0.2",
  ["kernel-2.16.4","stdlib-1.19.4","sasl-2.3.4","dummy-0.2",
   "asn1-2.0.4","compiler-4.9.4","crypto-3.2","et-1.4.4.5",
   "gs-1.5.15.2","inets-5.9.8","mnesia-4.11",
   "observer-1.3.1.2","public_key-0.21","runtime_tools-1.8.13",
   "ssl-5.3.3","tools-2.6.13","webtool-0.8.9.2","wx-1.1.2"],
  permanent},
 {"dummy","0.1",
  ["kernel-2.16.4","stdlib-1.19.4","sasl-2.3.4","dummy-0.1",
   "asn1-2.0.4","compiler-4.9.4","crypto-3.2","et-1.4.4.5",
   "gs-1.5.15.2","inets-5.9.8","mnesia-4.11",
   "observer-1.3.1.2","public_key-0.21","runtime_tools-1.8.13",
   "ssl-5.3.3","tools-2.6.13","webtool-0.8.9.2","wx-1.1.2"],
  old}]
(dummy@127.0.0.1)10> dummy_server:get_state().
123

References

[How To]使用rebar構建erlang 項目
Rebar:Erlang構建工具

erlang - otp application release tool

OTP application 最後發布時,需要一個工具幫我們分析 application 的相關 dependency libraries,發布正確的 OTP application。

目前有看到三個 release 工具: reltool, rebar, relx,雖然 reltool 是 OTP 官方提供的封裝工具,前面的部份先簡述 reltool 與 relx,relx 是因為 survey 了 cowboy 的關係,而去了解,功能跟 rebar 類似,但提供了更高階的封裝簡化工具,這個工具是 erlware 這個組織開發提供的。rebar 提供了比 reltool 高階的封裝方式,我們就以 rebar 為主,因為 rebar 的文件說明比 relx 還清楚。

reltool

reltool提供 GUI 與 CLI 兩種界面

reltool user guide
reltool doc
reltool使用指南
reltool使用入門

erlang.mk and relx

建構 OTP releases 一直以來都是個很麻煩的工作,除了 reltool, rebar 之外 cowboy 作者提供了另一個建立 application 的方式。

建立 OTP release 需要兩個步驟:

  1. compile source
    erlang.mk 用來處理第一個步驟,erlang.mk 是 GNU Makefile 的一個 include file,它可以用來 build project, 取得 building dependencies,產生文件,做 dialyzer 靜態分析。
  2. create a release
    relx 是個 release creation tool,它是一個執行檔。用來封裝OTP application,裡面包含了Erlang runtime, a boot script to start the node and all its applications, configuration files

最簡單的 Makefile,就只需要一個 PORJECT name 並 include erlang.mk

PROJECT = my_project

include erlang.mk

DEPS 定義本專案相關的 dependencies,條列出來後,在下面以 dep_cowboy 的方式定義該 library 的 repository URL 以及 commit number, tag 或 branch。 .PHONY 表示有兩個 build target,預設是 release。 release 最後會使用 relx 將 project 建構在 rel 目錄中。

PROJECT = ninenines

DEPS = cowboy erlydtl
dep_cowboy = https://github.com/extend/cowboy.git 0.8.5
dep_erlydtl = https://github.com/evanmiller/erlydtl.git 4d0dc8fb

.PHONY: release clean-release

release: clean-release all projects
    relx -o rel/$(PROJECT)

clean-release: clean-projects
    rm -rf rel/$(PROJECT)

include erlang.mk

這是 relx.config 檔,第一行定義 release name: ninenines,版本號碼1,裡面包含一個 application: ninenines,extended_start_script 告訴 relx 要建立一個可啟動 application 的 script,下一行 sys.config 代表可指定 erlang vm 的起始參數,

{release, {ninenines, "1"}, [ninenines]}.

{extended_start_script, true}.
{sys_config, "rel/sys.config"}.

{overlay, [
    {mkdir, "log"},
    {copy, "rel/vm.args",
        "releases/\{\{release_name\}\}-\{\{release_version\}\}/vm.args"}
]}.

參考文件 Build Erlang releases with erlang.mk and relx

rebar

編譯與安裝

  1. 下載 rebar source code
    git clone https://github.com/basho/rebar.git

  2. 編譯
    cd rebar
    ./bootstrap

    產生出 rebar 執行檔後,可以將 rebar 複製到任何地方,都可以使用。我們可以將 rebar 複製到 /usr/local/bin 或是 ~/bin 目錄,放在隨時可使用到的 PATH 裡面,就完成 rebar 的安裝了。

  3. 直接下載 rebar
    因為 rebar 是完全由 erlang 撰寫的,而且有整合成一個獨立的 escript,我們也可以直接下載編譯好的 rebar http://cloud.github.com/downloads/basho/rebar/rebar ,然後就能使用了。

  4. windows
    把在 linux 環境編譯出來的 rebar 複製到 windows 環境,把 rebar.cmd 與 rebar 放在 windows 的 PATH 路徑中可以存取到的地方,就可以直接使用 rebar 了。

測試專案

  1. mkdir myapp
    建立專案目錄

  2. rebar create-app appid=myapp
    產生專案的檔案骨架
    完成後會看到一個 src 目錄,裡面有三個檔案:myapp.app.src、myapp_app.erl、myapp_sup.erl

  3. rebar compile
    編譯 myall project,完成後會產生 ebin 目錄,也會得到一個 OTP 專案檔 myapp.app

  4. rebar clean
    清除編譯結果

命令列的參數

在開發過程中,最常用的功能有

  1. 編譯
  2. 單元測試和覆蓋分析
  3. 靜態分析(通過Dialyzer和Xref)
  4. 生成文檔
  5. 依賴管理

Rebar commands 提供了指令與參數列表

常見指令 description
compile 編譯
eunit 執行 eunit
doc 使用 edoc 產生文件
clean 清除所有生成的資料
一般指令 description
check_deps 檢查 rebar.config 的 dep libs
create 建立專案,必須要給予 template 的設定
create-app 根據 simpleapp.template 建立 app 專案
create-lib 根據 simplelib.template 建立 OTP library
create-node 根據 simplenode.template 建立一個 prototypical OTP embedded system
ct 執行 common_test suites
delete-deps 刪除依賴的專案原始碼
escriptize 以 ebin 目錄的 beam files 產生 escript 的執行檔
generate 使用 reltool 建立一個embedded system
generate-upgrade 產生 upgrade package
get-deps 根據 rebar.config 取得依賴的專案原始碼
list-deps 依賴的專案列表
update-deps 更新依賴的專案原始碼
xref 使用 xref 分析依賴

compile 支援的 source code 格式

source file destination file description
src/*.erl ebin/*.beam erlang source
src/*.app.src ebin/*.app otp application
c_src/*.c priv/.so port driver的c語言源代碼或者NIF共享鏈接庫
mibs/*.mib priv/mibs/*.bin SNMP 的 MIB 檔案
src/*.xrl src/*.erl leex 產生的檔案
src/*.yrl src/*.erl Yecc 產生的檔案
asn1/*.asn1 src/*.erl ASN-1文件
templates/*.dtl ebin/*_dtl.beam ErlyDTL模板文件 (需要額外安裝 ErlyDTL)
src/*.lfe ebin/*.beam LFE source code (需要額外安裝LFE)
src/*.peg ebin/*.beam Neotoma PEG 語法 source code (需要額外安裝Neotoma)
src/*.proto ebin/_pb.beam, include/_pb.hrl Protocol Buffers 參數(需要額外安裝protobuffs)

rebar.config 設定的選項

命令 設定的選項 description
compile erl_first_files 需要提前編譯的erlang源文件(例如behavior模塊)
compile erl_opts 編譯器支援的其他設定,請參閱文件
compile mib_first_files 需要提前編譯的mib文件列表 (例如, mib 文件中import部分的引用的RFC文件
compile src_dirs 列出其他包含erlang源文件的目錄
compile erlydtl_opts erlydtl 更多的支援的設定 ErlyDTL Templates
clean clean_files 需要在clean步驟刪除的文件列表,列出那些需要clean指令刪除的其他模塊的文件
doc edoc_opts edoc 支援的指令
eunit eunit_opts eunit支援的指令
eunit cover_enabled 開啟erlang的覆蓋率分析
eunit eunit_compile_opts Eunit編譯時用到的其他的選項
analyze dialyzer_opts 指定Dialyzer PLT 文件
build_plt dialyzer_opts 指定Dialyzer PLT 文件
check_plt dialyzer_opts 指定 Dialyzer PLT 文件
get-deps, delete-deps base_dir 為deps_dir 指定一個候選的目錄
get-deps, delete-deps deps_dir 設定存儲依賴檔的資料夾
get-deps, delete-deps deps 依賴的列表
generate target_dir 目標資料夾
generate overlay_vars Overlay variables file
xref xref_warnings 打開xref的警告
xref xref_checks Xref模塊中analyze/3支持的選項

目錄結構

rebar 遵循 OTP 的建議,包含了下列的資料夾:

  1. src
  2. ebin
  3. priv
  4. include
  5. test: eunit test source code
  6. c_src: C 語言寫的 Port Driver

Testing

rebar 支援 eunitcommon test 兩種 testing frameworks。

首先撰寫一些 eunit 測試的程式碼,在 apps/myapp/src/myapp_app.erl 的 -export 跟 程式的最後面,增加兩段 -ifdef(TEST) 到 -endif 的測試區塊。

-module(myapp_app).
-behaviour(application).

%% Application callbacks
-export([start/2, stop/1]).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

%% ===================================================================
%% Application callbacks
%% ===================================================================

start(_StartType, _StartArgs) ->
    myapp_sup:start_link().

stop(_State) ->
    ok.

-ifdef(TEST).

simple_test() ->
    ok = application:start(myapp),
    ?assertNot(undefined == whereis(myapp_sup)).

-endif.

因為直接執行 rebar eunit,連帶會對 deps 進行 eunit 的測試,所以增加 skip_deps 參數,只針對我們自己寫的程式進行測試。

rebar compile eunit skip_deps=true

如果要檢查 code coverage 的統計資料,我們必須在 rebar.config 增加以下設定

{cover_enabled, true}.

再執行一次 rebar compile eunit skip_deps=true ,就可以看到 code coverage analysis 結果寫在 apps/myapp/.eunit/index.html 檔案裡面。

其它

有關 deps, 建構 release package 以及升級的問題,就放在下一篇文章裡面說明。

2014/7/7

erlang - cowboy - file upload

因為 cowboy 在目前 git master 版本跟最新的 0.9 release 兩個版本,在實作 http multi-part request 的處理上,給了兩個不相容的處理方式,而且 cowboy 又在 1.0 開發的過程中,作者也沒有時間給出很多範例以及參考的資料,再加上網路上搜尋到的解決方案,幾乎都是舊版的 cowboy 支援的方式,因此,我們試著研究把 multi-part http request 的範例程式寫出來。

我們參考了 cowboy multipart 這一份唯一的官方文件,另外在 cowboy source code 裡面,有一個 http_multipart 測試程式,由這兩個資料,我們可以組合出一個可以運作的 multi-part http request 的範例程式。

project

這個範例專案已經放在 github 了,可以直接由clone 這個 project cowboy_fileupload source code in github

project settings

這個範例遵循 cowboy 範例的作法,使用了 erlang.mk 以及 relx 這兩個工具。

專案要寫 Makefile 與 relx.config 兩個設定檔,Makefile 要將 cowboy master branch 設定為 dependent library,接下來在 make 時,才會自動下載這些 libraries。

  1. Makefile

     PROJECT = upload
    
     DEPS = cowboy
     dep_cowboy = pkg://cowboy master
    
     include erlang.mk
  2. relx.config

     {release, {upload_example, "1"}, [upload]}.
     {extended_start_script, true}.

static html index.html

靜態網頁要放在 priv 的目錄中,index.html 裡面寫了三個 html form,以下只列出最多的第三個 form,這些 form 的 action 都設定為 /upload 這個網址,cowboy 的 routing 要設定 /upload 的網址,由 multipart 的程式碼處理。

這個 form 有兩個 text 欄位,另外還有兩個 file 的欄位,而且是一個 text 與一個 file 間隔的順序,接下來我們作的 cowboy mutipart handler 必須要能根據欄位資料的型態,自動判斷是不是 text 或是檔案,而有不同的對應處理方式。

<form id="uploadForm3" action="/upload" method="POST" enctype="multipart/form-data">
    <h1>Upload Form 3</h1>
    description 3.1: <input type="text" id="desc3_1" name="desc3_1" /><br/>
    file 3.1: <input type="file" name="file3_1" /><br/>
    description 3.2: <input type="text" id="desc3_2" name="desc3_2"><br>
    file 3.2: <input type="file" name="file3_2" /><br/>
    <button type="submit">Submit</button>
</form>

source codes

  1. upload.app.src
    relx 工具會自動根據這個檔案,產生 OTP upload.app 設定檔,兩個檔案的差異只有 modules 欄位。upload.app.src填寫為 [] 空的 list,而 upload.app 自動由 relx 把相關的 modules 填寫上去了。

     {modules, [upload_app, upload_sup, upload_handler]},
  2. upload_sup.erl
    這是 OTP 的 supervisor 程式,基本上內容就跟其他 cowboy samples 一樣。

  3. upload_app.erl
    重點是 cowboy 的 routing 部份,/ 指定為靜態檔案,路徑在 upload 的 priv 路徑裡面的 index.html。

    而 /upload 路徑指派由 upload_handler 處理。

     Dispatch = cowboy_router:compile([
             {'_', [
                 {"/", cowboy_static, {priv_file, upload, "index.html"}},
                 {"/upload", upload_handler, []}
             ]}
         ]),
         {ok, _} = cowboy:start_http(http, 100, [{port, 8000}], [
             {env, [{dispatch, Dispatch}]}
         ]),
  4. uplaod_handler.erl

    這個檔案是 multipart 程式的處理重點,首先我們定義這個 handler 的 behaviour。

     -behaviour(cowboy_http_handler).

    接下來是實作三個 callback functions

     init/3, handle/2, terminate/3

    重點是 handle,acc_multipart 是處理 multipart的遞迴程式,最後取得的結果 Result,是所有 multipart 資料的 header 與 body 的 list,但因為有些欄位是檔案,我們沒有必要把檔案內容放到 body 裡面傳回來這裡,所以在 acc_multipart 裡面有特別把檔案的 body 改寫為固定的文字內容 filecontent。

     handle(Req, State) ->
    
         {Result, Req2} = acc_multipart(Req, []),
         io:format( "Result= ~p~n", [Result] ),
         {ok, Req3} = cowboy_req:reply(200, [
             {<<"content-type">>, <<"text/plain; charset=UTF-8">>}
         ], <<"OK">>, Req2),
         %%writeToFile(term_to_binary(Result)),
         {ok, Req3, State}.

    這裡把測試時取得的 Result 資料記錄下來。

     %% Result= [{[{<<"content-disposition">>,<<"form-data; name=\"desc3_1\"">>}],
     %%          <<"desc1">>},
     %%         {[{<<"content-type">>,<<"text/plain">>},
     %%           {<<"content-disposition">>,
     %%            <<"form-data; name=\"file3_1\"; filename=\"userlist1.txt\"">>}],
     %%         <<"filecontent\r\n">>},
     %%         {[{<<"content-disposition">>,<<"form-data; name=\"desc3_2\"">>}],
     %%          <<"desc2">>},
     %%         {[{<<"content-type">>,<<"text/plain">>},
     %%           {<<"content-disposition">>,
     %%            <<"form-data; name=\"file3_2\"; filename=\"userlist2.txt\"">>}],
     %%          <<"filecontent\r\n">>}]

    參考 cowboy multipart 裡面 Reading a multipart message 這一段的內容,我們可以用 cow_multipart:form_data 回傳的資料內容的不同,直接將 text 與 file 兩個區分開來。file 的部份可直接取得 content type: CType 與檔名 Filename。

    因為要把檔案寫入磁碟中,但當檔案超過 8MB 的時候,cowboy 不能一次把所有資料都傳給 stream_file 處理,因此搭配 stream_file 的檔案寫入的處理,我們把檔案開啟 file:open 跟關閉檔案 file:close 分別寫在 stream_file 的前面與後面。

     acc_multipart(Req, Acc) ->
         case cowboy_req:part(Req) of
             {ok, Headers, Req2} ->
                 [Req4, Body] = case cow_multipart:form_data(Headers) of
                     {data, _FieldName} ->
                         {ok, MyBody, Req3} = cowboy_req:part_body(Req2),
                         [Req3, MyBody];
                     {file, _FieldName, Filename, CType, _CTransferEncoding} ->
                         io:format("stream_file filename=~p content_type=~p~n", [Filename, CType]),
                         {ok, IoDevice} = file:open( Filename, [raw, write, binary]),
                         Req5=stream_file(Req2, IoDevice),
                         file:close(IoDevice),
                         [Req5, <<"skip printing file content">>]
                     end,
                 acc_multipart(Req4, [{Headers, Body}|Acc]);
             {done, Req2} ->
                 {lists:reverse(Acc), Req2}
         end.

    參考 cowboy multipart 裡面 Skipping unwanted parts 這一段的內容,我們知道 cowboy 在還沒取得所有上傳檔案的資料時,cowboy_req:part_body 就會先回傳給呼叫端,並用 more 為信號告訴 client 還需要再呼叫一次,取得檔案後面的資料。

     stream_file(Req, IoDevice) ->
         case cowboy_req:part_body(Req) of
             {ok, Body, Req2} ->
                 io:format("part_body ok~n", []),
                 file:write(IoDevice, Body),
                 Req2;
             {more, Body, Req2} ->
                 io:format("part_body more~n", []),
                 file:write(IoDevice, Body),
                 stream_file(Req2, IoDevice)
         end.

編譯與測試

  1. 編譯

     > make

    它會自動取得所有需要的 libraries 包含了 cowboy, cowlib, ranch 還有封裝工具 relx。

  2. 啟動

     > _rel/bin/upload_example console
  3. 測試

    瀏覽網頁 http://localhost:8000/