3 创建和升级目标系统
使用 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 列表
3.1 创建目标系统
假设您有一个根据 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
os> erl -pa /home/user/target_system/myapps/pea-1.0/ebin
这里还提供了 pea-1.0 ebin 目录的路径。
**步骤 3.** 创建目标系统
1> target_system:create("mysystem").
函数 target_system:create/1 执行以下操作
- 读取 mysystem.rel 文件并创建一个名为 plain.rel 的新文件,该文件与前者相同,只是它只列出了 Kernel 和 STDLIB 应用程序。
- 从 mysystem.rel 和 plain.rel 文件中,通过调用 systools:make_script/2 创建 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。但是,有时解压缩 tar 文件时不会涉及 release_handler(例如,在解压缩第一个目标系统时)。因此,现在该文件在 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。
3.2 安装目标系统
**步骤 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 文件。
3.3 启动目标系统
现在,我们有一个可以通过多种方式启动的目标系统。我们通过调用以下命令将其作为**基本目标系统**启动
os> /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,如下所示
os> /usr/local/erl-target/bin/erl -boot /usr/local/erl-target/releases/FIRST/start
我们以与上面相同的方式启动**简单目标系统**。唯一的区别是,该 releases/RELEASES 文件也存在,以便运行时的代码替换能够工作。
要启动**嵌入式目标系统**,请使用 bin/start shell 脚本。该脚本调用 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 脚本。
3.4 系统配置参数
如上一节所述,start_erl 需要发布版本目录中的 sys.config (“releases/FIRST/sys.config”)。如果不存在此文件,则系统启动将失败。因此,必须添加此文件。
如果您有既不依赖于文件位置也不依赖于站点的系统配置数据,那么早点创建 sys.config 可能会很方便,这样它就会成为 target_system:create/1 创建的目标系统 tar 文件的一部分。实际上,如果您在当前目录中不仅创建了 mysystem.rel 文件,而且还创建了 sys.config 文件,则后者会默契地放入适当的目录中。
但是,在解压缩后但启动发布版本之前,在目标系统中替换 sys.config 中的变量也可能很方便。如果您有 sys.config.src,它将被包含,并且不需要是有效的 Erlang 术语文件,例如 sys.config。在运行发布版本之前,您必须在同一个目录中有一个有效的 sys.config,因此使用 sys.config.src 需要使用一些工具来填充所需的内容,并在启动发布版本之前将 sys.config 写入磁盘。
3.5 与安装脚本的差异
之前的 install/2 过程与普通的 Install shell 脚本略有不同。实际上,create/1 会尽可能完整地创建发布包,并将仅考虑位置相关文件的完成工作留给 install/2 过程。
3.6 创建下一个版本
在这个例子中,Pea 应用程序已经改变,ERTS、内核、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. 创建应用程序升级文件(参见 SASL 中的 appup(4) 手册页),例如 Pea
%% pea.appup {"2.0", [{"1.0",[{load_module,pea_lib}]}], [{"1.0",[{load_module,pea_lib}]}]}.
步骤 3. 从文件 mysystem2.rel 所在的目录启动 Erlang/OTP 系统,并提供 Pea 新版本的路径
os> erl -pa /home/user/target_system/myapps/pea-2.0/ebin
步骤 4. 创建发布升级文件(参见 SASL 中的 relup(4) 手册页)
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 文件现在位于当前目录中,它会自动包含在发布包中。
3.7 升级目标系统
此部分在目标节点上完成,对于此示例,我们希望节点以 -heart 选项运行为嵌入式系统,允许节点自动重启。有关更多信息,请参见 启动目标系统。
我们在 bin/start 中添加 -heart
#!/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 目录中
os> cp mysystem2.tar.gz /usr/local/erl-target/releases
假设节点已按以下方式启动
os> /usr/local/erl-target/bin/start
它可以按以下方式访问
os> /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、内核、STDLIB 或 SASL 应用程序的更改时,始终会这样做。有关更多信息,请参见 当 Erlang/OTP 发生更改时的升级。
该节点可以通过新的管道访问
os> /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,因此重新启动节点是安全的。
3.8 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).