檢視原始碼 建立及升級目標系統
當使用 Erlang/OTP 建立系統時,最簡單的方法是在某處安裝 Erlang/OTP,在其他地方安裝特定應用程式的程式碼,然後啟動 Erlang 運行時系統,確保程式碼路徑包含特定應用程式的程式碼。
通常不希望直接使用 Erlang/OTP 系統。開發人員可以為特定目的建立新的符合 Erlang/OTP 標準的應用程式,而幾個原始 Erlang/OTP 應用程式可能與所考慮的目的無關。因此,需要能夠根據給定的 Erlang/OTP 系統建立新的系統,其中移除不必要的應用程式並包含新的應用程式。文件和原始碼無關,因此不包含在新系統中。
本章節是關於建立這樣的系統,稱為目標系統。
以下章節將介紹對功能有不同要求的目標系統
- 一個基本目標系統,可以透過呼叫普通的
erl
腳本啟動。 - 一個簡單目標系統,也支援執行時的程式碼替換。
- 一個嵌入式目標系統,也支援在開機時自動啟動,並記錄系統檔案的輸出以供稍後檢查。
這裡僅考慮 Erlang/OTP 在 UNIX 系統上執行的情況。
sasl
應用程式包含範例 Erlang 模組 target_system.erl
,其中包含用於建立和安裝目標系統的函數。此模組將在以下範例中使用。該模組的原始碼列在 target_system.erl 的程式碼清單
建立目標系統
假設您有一個依照 OTP 設計原則建構的正常運作的 Erlang/OTP 系統。
步驟 1. 建立一個 .rel
檔案(請參閱 SASL 中的 rel(4) 手冊頁面),其中指定 ERTS 版本並列出要包含在新基本目標系統中的所有應用程式。以下是一個 mysystem.rel
檔案的範例
%% mysystem.rel
{release,
{"MYSYSTEM", "FIRST"},
{erts, "5.10.4"},
[{kernel, "2.16.4"},
{stdlib, "1.19.4"},
{sasl, "2.3.4"},
{pea, "1.0"}]}.
列出的應用程式不僅是原始的 Erlang/OTP 應用程式,也可能是您編寫的新應用程式(這裡以應用程式 Pea (pea
) 為例)。
步驟 2. 從 mysystem.rel
檔案所在的目錄啟動 Erlang/OTP
% erl -pa /home/user/target_system/myapps/pea-1.0/ebin
-pa
引數將 Pea 應用程式的 ebin
目錄的路徑加到程式碼路徑的前面。
步驟 3. 建立目標系統
1> target_system:create("mysystem").
函數 target_system:create/1
執行以下操作
讀取檔案
mysystem.rel
並建立一個新的檔案plain.rel
。新檔案與原始檔案相同,只是它只列出 Kernel 和 STDLIB 應用程式。透過呼叫
systools:make_script/2
,從檔案mysystem.rel
和plain.rel
建立檔案mysystem.script
、mysystem.boot
、plain.script
和plain.boot
。透過呼叫
systools:make_tar/2
建立檔案mysystem.tar.gz
。該檔案包含以下內容
erts-5.10.4/bin/
releases/FIRST/start.boot
releases/FIRST/mysystem.rel
releases/mysystem.rel
lib/kernel-2.16.4/
lib/stdlib-1.19.4/
lib/sasl-2.3.4/
lib/pea-1.0/
檔案 releases/FIRST/start.boot
是我們 mysystem.boot
的副本
發布資源檔案 mysystem.rel
在 tar 檔案中重複。最初,此檔案僅儲存在 releases
目錄中,以使 release_handler
能夠單獨提取此檔案。解壓縮 tar 檔案後,release_handler
會自動將檔案複製到 releases/FIRST
。但是,有時會在不涉及 release_handler
的情況下解壓縮 tar 檔案(例如,解壓縮第一個目標系統時)。因此,該檔案現在在 tar 封存檔中重複,從而無需手動複製。
- 建立臨時目錄
tmp
,並將 tar 檔案mysystem.tar.gz
解壓縮到該目錄中。 - 從
tmp/erts-5.10.4/bin
中刪除檔案erl
和start
。這些檔案將在安裝發布版本時從原始碼重新建立。 - 建立目錄
tmp/bin
。 - 將先前建立的檔案
plain.boot
複製到tmp/bin/start.boot
。 - 將檔案
epmd
、run_erl
和to_erl
從目錄tmp/erts-5.10.4/bin
複製到目錄tmp/bin
。 - 建立目錄
tmp/log
,如果系統使用bin/start
腳本以嵌入式方式啟動,則會使用此目錄。 - 建立檔案
tmp/releases/start_erl.data
,其中包含 "5.10.4 FIRST" 的內容。此檔案將作為資料檔案傳遞給start_erl
腳本。 - 從目錄
tmp
中的目錄重新建立檔案mysystem.tar.gz
,並刪除tmp
。
安裝目標系統
步驟 4. 將建立的目標系統安裝到合適的目錄中。
2> target_system:install("mysystem", "/usr/local/erl-target").
函數 target_system:install/2
執行以下操作
- 將 tar 檔案
mysystem.tar.gz
解壓縮到目標目錄/usr/local/erl-target
。 - 在目標目錄中讀取檔案
releases/start_erl.data
以查找 Erlang 執行時系統版本 ("5.10.4")。 - 將目標
erts-5.10.4/bin
目錄中的檔案erl.src
、start.src
和start_erl.src
中的%FINAL_ROOTDIR%
和%EMU%
分別替換為/usr/local/erl-target
和beam
,並將結果檔案erl
、start
和run_erl
放置在目標bin
目錄中。 - 最後,從檔案
releases/mysystem.rel
中的資料建立目標releases/RELEASES
檔案。
啟動目標系統
現在我們有一個可以透過各種方式啟動的目標系統。我們透過調用以下命令,將其作為基本目標系統啟動
% /usr/local/erl-target/bin/erl
這裡只啟動 Kernel 和 STDLIB 應用程式,也就是說,該系統是作為普通的開發系統啟動的。要使所有這些都正常運作,只需要兩個檔案
bin/erl
(從erts-5.10.4/bin/erl.src
取得)bin/start.boot
(plain.boot
的副本)
我們也可以啟動分散式系統(需要 bin/epmd
)。
若要啟動原始 mysystem.rel
檔案中指定的所有應用程式,請使用以下 -boot
旗標
% /usr/local/erl-target/bin/erl -boot /usr/local/erl-target/releases/FIRST/start
我們如上所述啟動簡單目標系統。唯一的區別是,releases/RELEASES
檔案也存在,以便在執行時進行程式碼替換。
若要啟動嵌入式目標系統,請使用 shell 腳本 bin/start
。該腳本呼叫 bin/run_erl
,而 bin/run_erl
又呼叫 bin/start_erl
(粗略來說,start_erl
是 erl
的嵌入式變體)。
在安裝期間從 erts-5.10.4/bin/start.src
產生的 shell 腳本 start
只是一個範例。請根據您的需要編輯它。通常,它會在 UNIX 系統啟動時執行。
run_erl
是一個包裝函式,它提供將執行時系統的輸出記錄到檔案中。它還提供一種簡單的機制來附加到 Erlang shell (to_erl
)。
start_erl
需要
- 根目錄 (
"/usr/local/erl-target"
) - 發布版本目錄 (
"/usr/local/erl-target/releases"
) start_erl.data
檔案的位置
它執行以下操作
- 從檔案
start_erl.data
中讀取執行時系統版本 ("5.10.4"
) 和發布版本 ("FIRST"
)。 - 啟動找到的版本的執行時系統。
- 提供旗標
-boot
,指定找到的發布版本的啟動檔案 ("releases/FIRST/start.boot"
)。
start_erl
還假設發布版本目錄中存在 sys.config
("releases/FIRST/sys.config"
)。這是下一節的主題。
start_erl
shell 腳本通常不應由使用者更改。
系統組態參數
如上一節所述,start_erl
需要發布版本目錄中存在 sys.config
("releases/FIRST/sys.config"
)。如果沒有此檔案,系統啟動會失敗。因此,也必須加入此類檔案。
如果您的系統組態資料既不依賴於檔案位置也不依賴於站點,則盡早建立 sys.config
會很方便,這樣它就會成為 target_system:create/1
建立的目標系統 tar 檔案的一部分。事實上,如果您在目前目錄中不僅建立檔案 mysystem.rel
,還建立檔案 sys.config
,則後者檔案會被悄悄地放入適當的目錄中。
但是,在解壓縮後但在執行發布版本之前,在目標上的 sys.config
中替換變數也可能會很方便。如果您有 sys.config.src
,它將被包含,並且不需要像 sys.config
那樣成為有效的 Erlang 術語檔案。在執行發布版本之前,您必須在同一個目錄中有一個有效的 sys.config
,因此使用 sys.config.src
需要有一些工具來填入所需的內容,並在啟動發布版本之前將 sys.config
寫入磁碟。
與安裝腳本的差異
先前的 install/2
程序與普通的 Install
shell 腳本的程序略有不同。事實上,create/1
會使發布版本套件盡可能完整,並留給 install/2
程序來完成,方法是僅考慮與位置相關的檔案。
建立下一個版本
在此範例中,Pea 應用程式已變更,ERTS、Kernel、STDLIB 和 SASL 應用程式也是如此。
步驟 1. 建立檔案 .rel
%% mysystem2.rel
{release,
{"MYSYSTEM", "SECOND"},
{erts, "6.0"},
[{kernel, "3.0"},
{stdlib, "2.0"},
{sasl, "2.4"},
{pea, "2.0"}]}.
步驟 2. 為 Pea 建立應用程式升級檔案(請參閱 SASL 中的 appup),例如
%% pea.appup
{"2.0",
[{"1.0",[{load_module,pea_lib}]}],
[{"1.0",[{load_module,pea_lib}]}]}.
步驟 3. 從檔案 mysystem2.rel
所在的目錄啟動 Erlang/OTP 系統,並提供新版本 Pea 的路徑
% erl -pa /home/user/target_system/myapps/pea-2.0/ebin
步驟 4. 建立發布版本升級檔案(請參閱 SASL 中的 relup)
1> systools:make_relup("mysystem2",["mysystem"],["mysystem"],
[{path,["/home/user/target_system/myapps/pea-1.0/ebin",
"/my/old/erlang/lib/*/ebin"]}]).
這裡,"mysystem"
是基本發布版本,"mysystem2"
是要升級到的發布版本。
path
選項用於指出所有應用程式的舊版本。(新版本已在程式碼路徑中 - 當然前提是執行此操作的 Erlang 節點正在執行正確版本的 Erlang/OTP。)
步驟 5. 建立新的發布版本
2> target_system:create("mysystem2").
假設步驟 4 中產生的檔案 relup
現在位於目前目錄,它將會自動包含在發布套件中。
升級目標系統
這個部分在目標節點上完成。在這個範例中,我們希望節點以嵌入式系統的方式執行,並使用 -heart
選項,以便自動重新啟動節點。有關更多資訊,請參閱啟動目標系統。
我們將 -heart
加入到 bin/start
中。
#!/bin/sh
ROOTDIR=/usr/local/erl-target/
if [ -z "$RELDIR" ]
then
RELDIR=$ROOTDIR/releases
fi
START_ERL_DATA=${1:-$RELDIR/start_erl.data}
$ROOTDIR/bin/run_erl -daemon /tmp/ $ROOTDIR/log "exec $ROOTDIR/bin/start_erl $ROOTDIR\
$RELDIR $START_ERL_DATA -heart"
我們使用最簡單的 sys.config
,並將其儲存在 releases/FIRST
中。
%% sys.config
[].
最後,為了準備升級,我們必須將新的發布套件放入第一個目標系統的 releases
目錄中。
% cp mysystem2.tar.gz /usr/local/erl-target/releases
假設節點已按照以下方式啟動
% /usr/local/erl-target/bin/start
它可以使用以下方式存取
% /usr/local/erl-target/bin/to_erl /tmp/erlang.pipe.1
日誌可以在 /usr/local/erl-target/log
中找到。此目錄是在啟動腳本中,作為 run_erl
的參數指定。
步驟 1. 解壓縮發布套件
1> {ok,Vsn} = release_handler:unpack_release("mysystem2").
步驟 2. 安裝發布套件
2> release_handler:install_release(Vsn).
{continue_after_restart,"FIRST",[]}
heart: Tue Apr 1 12:15:10 2014: Erlang has closed.
heart: Tue Apr 1 12:15:11 2014: Executed "/usr/local/erl-target/bin/start /usr/local/erl-target/releases/new_start_erl.data" -> 0. Terminating.
[End]
在呼叫 release_handler:install_release/1
之後,上述回傳值和輸出表示 release_handler
已使用 heart
重新啟動節點。當升級涉及應用程式 ERTS、Kernel、STDLIB 或 SASL 的變更時,始終會執行此操作。有關更多資訊,請參閱當 Erlang/OTP 變更時進行升級。
現在可以透過新的管道存取節點
% /usr/local/erl-target/bin/to_erl /tmp/erlang.pipe.2
列出系統中可用的發布套件
1> release_handler:which_releases().
[{"MYSYSTEM","SECOND",
["kernel-3.0","stdlib-2.0","sasl-2.4","pea-2.0"],
current},
{"MYSYSTEM","FIRST",
["kernel-2.16.4","stdlib-1.19.4","sasl-2.3.4","pea-1.0"],
permanent}]
我們的新發布套件 "SECOND" 現在是目前的發布套件,但我們也可以看到我們的 "FIRST" 發布套件仍然是永久的。這表示如果現在重新啟動節點,它將會再次執行 "FIRST" 發布套件。
步驟 3. 將新的發布套件設為永久
2> release_handler:make_permanent("SECOND").
再次檢查發布套件
3> release_handler:which_releases().
[{"MYSYSTEM","SECOND",
["kernel-3.0","stdlib-2.0","sasl-2.4","pea-2.0"],
permanent},
{"MYSYSTEM","FIRST",
["kernel-2.16.4","stdlib-1.19.4","sasl-2.3.4","pea-1.0"],
old}]
我們看到新的發布套件版本是 permanent
,因此重新啟動節點會是安全的。
target_system.erl 的列表
這個模組也可以在 SASL 應用程式的 examples
目錄中找到。
-module(target_system).
-export([create/1, create/2, install/2]).
%% Note: RelFileName below is the *stem* without trailing .rel,
%% .script etc.
%%
%% create(RelFileName)
%%
create(RelFileName) ->
create(RelFileName,[]).
create(RelFileName,SystoolsOpts) ->
RelFile = RelFileName ++ ".rel",
Dir = filename:dirname(RelFileName),
PlainRelFileName = filename:join(Dir,"plain"),
PlainRelFile = PlainRelFileName ++ ".rel",
io:fwrite("Reading file: ~ts ...~n", [RelFile]),
{ok, [RelSpec]} = file:consult(RelFile),
io:fwrite("Creating file: ~ts from ~ts ...~n",
[PlainRelFile, RelFile]),
{release,
{RelName, RelVsn},
{erts, ErtsVsn},
AppVsns} = RelSpec,
PlainRelSpec = {release,
{RelName, RelVsn},
{erts, ErtsVsn},
lists:filter(fun({kernel, _}) ->
true;
({stdlib, _}) ->
true;
(_) ->
false
end, AppVsns)
},
{ok, Fd} = file:open(PlainRelFile, [write]),
io:fwrite(Fd, "~p.~n", [PlainRelSpec]),
file:close(Fd),
io:fwrite("Making \"~ts.script\" and \"~ts.boot\" files ...~n",
[PlainRelFileName,PlainRelFileName]),
make_script(PlainRelFileName,SystoolsOpts),
io:fwrite("Making \"~ts.script\" and \"~ts.boot\" files ...~n",
[RelFileName, RelFileName]),
make_script(RelFileName,SystoolsOpts),
TarFileName = RelFileName ++ ".tar.gz",
io:fwrite("Creating tar file ~ts ...~n", [TarFileName]),
make_tar(RelFileName,SystoolsOpts),
TmpDir = filename:join(Dir,"tmp"),
io:fwrite("Creating directory ~tp ...~n",[TmpDir]),
file:make_dir(TmpDir),
io:fwrite("Extracting ~ts into directory ~ts ...~n", [TarFileName,TmpDir]),
extract_tar(TarFileName, TmpDir),
TmpBinDir = filename:join([TmpDir, "bin"]),
ErtsBinDir = filename:join([TmpDir, "erts-" ++ ErtsVsn, "bin"]),
io:fwrite("Deleting \"erl\" and \"start\" in directory ~ts ...~n",
[ErtsBinDir]),
file:delete(filename:join([ErtsBinDir, "erl"])),
file:delete(filename:join([ErtsBinDir, "start"])),
io:fwrite("Creating temporary directory ~ts ...~n", [TmpBinDir]),
file:make_dir(TmpBinDir),
io:fwrite("Copying file \"~ts.boot\" to ~ts ...~n",
[PlainRelFileName, filename:join([TmpBinDir, "start.boot"])]),
copy_file(PlainRelFileName++".boot",filename:join([TmpBinDir, "start.boot"])),
io:fwrite("Copying files \"epmd\", \"run_erl\" and \"to_erl\" from \n"
"~ts to ~ts ...~n",
[ErtsBinDir, TmpBinDir]),
copy_file(filename:join([ErtsBinDir, "epmd"]),
filename:join([TmpBinDir, "epmd"]), [preserve]),
copy_file(filename:join([ErtsBinDir, "run_erl"]),
filename:join([TmpBinDir, "run_erl"]), [preserve]),
copy_file(filename:join([ErtsBinDir, "to_erl"]),
filename:join([TmpBinDir, "to_erl"]), [preserve]),
%% This is needed if 'start' script created from 'start.src' shall
%% be used as it points out this directory as log dir for 'run_erl'
TmpLogDir = filename:join([TmpDir, "log"]),
io:fwrite("Creating temporary directory ~ts ...~n", [TmpLogDir]),
ok = file:make_dir(TmpLogDir),
StartErlDataFile = filename:join([TmpDir, "releases", "start_erl.data"]),
io:fwrite("Creating ~ts ...~n", [StartErlDataFile]),
StartErlData = io_lib:fwrite("~s ~s~n", [ErtsVsn, RelVsn]),
write_file(StartErlDataFile, StartErlData),
io:fwrite("Recreating tar file ~ts from contents in directory ~ts ...~n",
[TarFileName,TmpDir]),
{ok, Tar} = erl_tar:open(TarFileName, [write, compressed]),
%% {ok, Cwd} = file:get_cwd(),
%% file:set_cwd("tmp"),
ErtsDir = "erts-"++ErtsVsn,
erl_tar:add(Tar, filename:join(TmpDir,"bin"), "bin", []),
erl_tar:add(Tar, filename:join(TmpDir,ErtsDir), ErtsDir, []),
erl_tar:add(Tar, filename:join(TmpDir,"releases"), "releases", []),
erl_tar:add(Tar, filename:join(TmpDir,"lib"), "lib", []),
erl_tar:add(Tar, filename:join(TmpDir,"log"), "log", []),
erl_tar:close(Tar),
%% file:set_cwd(Cwd),
io:fwrite("Removing directory ~ts ...~n",[TmpDir]),
remove_dir_tree(TmpDir),
ok.
install(RelFileName, RootDir) ->
TarFile = RelFileName ++ ".tar.gz",
io:fwrite("Extracting ~ts ...~n", [TarFile]),
extract_tar(TarFile, RootDir),
StartErlDataFile = filename:join([RootDir, "releases", "start_erl.data"]),
{ok, StartErlData} = read_txt_file(StartErlDataFile),
[ErlVsn, _RelVsn| _] = string:tokens(StartErlData, " \n"),
ErtsBinDir = filename:join([RootDir, "erts-" ++ ErlVsn, "bin"]),
BinDir = filename:join([RootDir, "bin"]),
io:fwrite("Substituting in erl.src, start.src and start_erl.src to "
"form erl, start and start_erl ...\n"),
subst_src_scripts(["erl", "start", "start_erl"], ErtsBinDir, BinDir,
[{"FINAL_ROOTDIR", RootDir}, {"EMU", "beam"}],
[preserve]),
%%! Workaround for pre OTP 17.0: start.src and start_erl.src did
%%! not have correct permissions, so the above 'preserve' option did not help
ok = file:change_mode(filename:join(BinDir,"start"),8#0755),
ok = file:change_mode(filename:join(BinDir,"start_erl"),8#0755),
io:fwrite("Creating the RELEASES file ...\n"),
create_RELEASES(RootDir, filename:join([RootDir, "releases",
filename:basename(RelFileName)])).
%% LOCALS
%% make_script(RelFileName,Opts)
%%
make_script(RelFileName,Opts) ->
systools:make_script(RelFileName, [no_module_tests,
{outdir,filename:dirname(RelFileName)}
|Opts]).
%% make_tar(RelFileName,Opts)
%%
make_tar(RelFileName,Opts) ->
RootDir = code:root_dir(),
systools:make_tar(RelFileName, [{erts, RootDir},
{outdir,filename:dirname(RelFileName)}
|Opts]).
%% extract_tar(TarFile, DestDir)
%%
extract_tar(TarFile, DestDir) ->
erl_tar:extract(TarFile, [{cwd, DestDir}, compressed]).
create_RELEASES(DestDir, RelFileName) ->
release_handler:create_RELEASES(DestDir, RelFileName ++ ".rel").
subst_src_scripts(Scripts, SrcDir, DestDir, Vars, Opts) ->
lists:foreach(fun(Script) ->
subst_src_script(Script, SrcDir, DestDir,
Vars, Opts)
end, Scripts).
subst_src_script(Script, SrcDir, DestDir, Vars, Opts) ->
subst_file(filename:join([SrcDir, Script ++ ".src"]),
filename:join([DestDir, Script]),
Vars, Opts).
subst_file(Src, Dest, Vars, Opts) ->
{ok, Conts} = read_txt_file(Src),
NConts = subst(Conts, Vars),
write_file(Dest, NConts),
case lists:member(preserve, Opts) of
true ->
{ok, FileInfo} = file:read_file_info(Src),
file:write_file_info(Dest, FileInfo);
false ->
ok
end.
%% subst(Str, Vars)
%% Vars = [{Var, Val}]
%% Var = Val = string()
%% Substitute all occurrences of %Var% for Val in Str, using the list
%% of variables in Vars.
%%
subst(Str, Vars) ->
subst(Str, Vars, []).
subst([$%, C| Rest], Vars, Result) when $A =< C, C =< $Z ->
subst_var([C| Rest], Vars, Result, []);
subst([$%, C| Rest], Vars, Result) when $a =< C, C =< $z ->
subst_var([C| Rest], Vars, Result, []);
subst([$%, C| Rest], Vars, Result) when C == $_ ->
subst_var([C| Rest], Vars, Result, []);
subst([C| Rest], Vars, Result) ->
subst(Rest, Vars, [C| Result]);
subst([], _Vars, Result) ->
lists:reverse(Result).
subst_var([$%| Rest], Vars, Result, VarAcc) ->
Key = lists:reverse(VarAcc),
case lists:keysearch(Key, 1, Vars) of
{value, {Key, Value}} ->
subst(Rest, Vars, lists:reverse(Value, Result));
false ->
subst(Rest, Vars, [$%| VarAcc ++ [$%| Result]])
end;
subst_var([C| Rest], Vars, Result, VarAcc) ->
subst_var(Rest, Vars, Result, [C| VarAcc]);
subst_var([], Vars, Result, VarAcc) ->
subst([], Vars, [VarAcc ++ [$%| Result]]).
copy_file(Src, Dest) ->
copy_file(Src, Dest, []).
copy_file(Src, Dest, Opts) ->
{ok,_} = file:copy(Src, Dest),
case lists:member(preserve, Opts) of
true ->
{ok, FileInfo} = file:read_file_info(Src),
file:write_file_info(Dest, FileInfo);
false ->
ok
end.
write_file(FName, Conts) ->
Enc = file:native_name_encoding(),
{ok, Fd} = file:open(FName, [write]),
file:write(Fd, unicode:characters_to_binary(Conts,Enc,Enc)),
file:close(Fd).
read_txt_file(File) ->
{ok, Bin} = file:read_file(File),
{ok, binary_to_list(Bin)}.
remove_dir_tree(Dir) ->
remove_all_files(".", [Dir]).
remove_all_files(Dir, Files) ->
lists:foreach(fun(File) ->
FilePath = filename:join([Dir, File]),
case filelib:is_dir(FilePath) of
true ->
{ok, DirFiles} = file:list_dir(FilePath),
remove_all_files(FilePath, DirFiles),
file:del_dir(FilePath);
_ ->
file:delete(FilePath)
end
end, Files).