3.创建和升级目标系统 | 3. Creating and Upgrading a Target System
3建立和升级目标系统
在使用Erlang/OTP创建系统时,最简单的方法是在某处安装Erlang/OTP,在其他位置安装特定于应用程序的代码,然后启动Erlang运行时系统,确保代码路径包含特定于应用程序的代码。
按原样使用Erlang / OTP系统通常是不理想的。开发人员可以为特定目的创建新的符合Erlang / OTP的应用程序,并且几个原始的Erlang / OTP应用程序可能与所讨论的目的无关。因此,需要能够基于给定的Erlang / OTP系统创建新的系统,其中可移除的应用程序被移除并且包括新的应用程序。文档和源代码是不相关的,因此不包含在新系统中。
本章是关于创建这样一个称为目标系统的系统
。
以下部分讨论具有不同功能要求的目标系统:
- 一个
基本的目标系统
,可以通过调用普通的启动erl
脚本。
- 一个
简单的目标系统
,可以在运行时执行代码替换。
- 一个
嵌入式目标系统
,其中也有从系统日志输出到文件供以后检查的支持,并在该系统可在启动时自动启动。
Erlang/OTP在UNIX系统上运行时,只考虑这种情况。
sasl
应用程序包含示例Erlang模块target_system.erl
,其中包含用于创建和安装目标系统的功能。以下示例中使用了该模块。该模块的源代码列在Listing of target_system.erl
3.1创建目标系统
假设您有一个工作的Erlang/OTP系统,该系统按照OTP设计原则进行结构。
步骤1.
创建一个.rel
文件(请参阅rel(4)
SASL中的手册页),该文件指定了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
)为例)。
第二步。
从mysystem.rel
文件驻留:
os> erl -pa /home/user/target_system/myapps/pea-1.0/ebin
这里也pea-1.0
提供了ebin目录的路径。
第三步。
创建目标系统:
1> target_system:create("mysystem").
target_system:create/1
功能执行以下操作:
- 读取文件
mysystem.rel
并创建一个plain.rel
与前者相同的新文件,只是它仅列出内核和STDLIB应用程序。
- 从文件
mysystem.rel
和plain.rel
创建的文件mysystem.script
,mysystem.boot
,plain.script
,并plain.boot
通过一个呼叫systools:make_script/2
。
mysystem.tar.gz
通过呼叫创建文件systools:make_tar/2
。该文件具有以下内容:
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
到该目录中。
- 删除文件
erl
并start
从tmp/erts-5.10.4/bin
。安装发行版时,这些文件是从源再次创建的。
- 创建目录
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
脚本。
mysystem.tar.gz
从目录中的目录重新创建文件tmp
并删除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”)。
- 替代品
%FINAL_ROOTDIR%
和%EMU%
用于/usr/local/erl-target
和beam
分别,在文件中erl.src
,start.src
以及start_erl.src
目标erts-5.10.4/bin
,目录,并把生成的文件erl
,start
以及run_erl
在目标bin
目录中。
- 最后,根据
releases/RELEASES
文件中的数据创建目标文件releases/mysystem.rel
。
3.3启动目标系统
现在我们有一个可以以各种方式启动的目标系统。我们通过调用以下作为基本目标系统
来启动它:
os> /usr/local/erl-target/bin/erl
这里只启动内核和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
也存在于运行时的代码替换工作。
要启动嵌入式目标系统
,请使用shell脚本bin/start
。脚本调用bin/run_erl
,然后调用bin/start_erl
(粗略地说,start_erl
是嵌入式变体erl
)。
start
在安装过程中从erts-5.10.4/bin/start
.src生成的shell脚本只是一个示例。编辑它以满足您的需求。通常在UNIX系统启动时执行。
run_erl
是一个包装器,它提供了将运行时系统的输出记录到文件中。它还提供了一个附加到Erlang shell(to_erl
)的简单机制。
start_erl
要求:
- 根目录(
"/usr/local/erl-target"
)
- 发行版目录(
"/usr/local/erl-target/releases"
- 文件的位置
start_erl.data
它执行下列工作:
- 读取运行时系统版本(
"5.10.4"
)并从start_erl.data
文件中释放版本("FIRST"
)。
- 启动找到的版本的运行时系统。
- 提供
-boot
指定发现版本found("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
提前创建,因此它成为由目标系统创建的tar文件的一部分target_system:create/1
。实际上,如果你在当前目录下创建的不仅仅是文件mysystem.rel
,还有文件sys.config
,后一个文件默认放在相应的目录中。
3.5与安装脚本的差异
前面的install/2
过程与普通的Install
shell脚本有所不同。事实上,create/1
尽可能使发布包完整,并且install/2
仅通过考虑与位置有关的文件来完成该过程。
3.6创建下一个版本
在这个例子中,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步。
appup(4)
为Pea 创建应用程序升级文件(请参阅SASL 的手册页),例如:
%% 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步。
创建发行版升级文件(请参阅relup(4)
SASL中的手册页):
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。)
第五步。
创建新版本:
2> target_system:create("mysystem2").
鉴于relup
步骤4中生成的文件现在位于当前目录中,它将自动包含在发行包中。
3.7提升目标系统
这部分是在目标节点上完成的,在这个例子中,我们希望节点作为一个带有-heart
选项的嵌入式系统运行,从而允许节点自动重新启动。有关更多信息,请参阅Starting a Target System
。
我们添加-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
第一个目标系统的目录中:
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> 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的更改时完成。有关更多信息,请参阅Upgrade when Erlang/OTP has Changed
。
该节点可通过新管道访问:
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的列表
该模块也可以在examples
SASL应用程序的目录中找到。
-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: ~tp ...~n", [RelFile]),
{ok, [RelSpec]} = file:consult(RelFile),
io:fwrite("Creating file: ~tp from ~tp ...~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 ~tp ...~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 ~tp into directory ~tp ...~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 ~tp ...~n",
[ErtsBinDir]),
file:delete(filename:join([ErtsBinDir, "erl"])),
file:delete(filename:join([ErtsBinDir, "start"])),
io:fwrite("Creating temporary directory ~tp ...~n", [TmpBinDir]),
file:make_dir(TmpBinDir),
io:fwrite("Copying file \"~ts.boot\" to ~tp ...~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"
"~tp to ~tp ...~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 ~tp ...~n", [TmpLogDir]),
ok = file:make_dir(TmpLogDir),
StartErlDataFile = filename:join([TmpDir, "releases", "start_erl.data"]),
io:fwrite("Creating ~tp ...~n", [StartErlDataFile]),
StartErlData = io_lib:fwrite("~s ~s~n", [ErtsVsn, RelVsn]),
write_file(StartErlDataFile, StartErlData),
io:fwrite("Recreating tar file ~tp from contents in directory ~tp ...~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 ~tp ...~n",[TmpDir]),
remove_dir_tree(TmpDir),
ok.
install(RelFileName, RootDir) ->
TarFile = RelFileName ++ ".tar.gz",
io:fwrite("Extracting ~tp ...~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).