查看源代码 创建和升级目标系统

当使用 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 执行以下操作

  1. 读取文件 mysystem.rel 并创建一个新文件 plain.rel。新文件与原始文件相同,只是它只列出 Kernel 和 STDLIB 应用程序。

  2. 通过调用 systools:make_script/2,从文件 mysystem.relplain.rel 创建文件 mysystem.scriptmysystem.bootplain.scriptplain.boot

  3. 通过调用 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 存档中被复制,无需手动复制。

  1. 创建临时目录 tmp 并将 tar 文件 mysystem.tar.gz 解压缩到该目录中。
  2. tmp/erts-5.10.4/bin 中删除文件 erlstart。这些文件在安装发行版时会从源代码重新创建。
  3. 创建目录 tmp/bin
  4. 将先前创建的文件 plain.boot 复制到 tmp/bin/start.boot
  5. 将文件 epmdrun_erlto_erl 从目录 tmp/erts-5.10.4/bin 复制到目录 tmp/bin
  6. 创建目录 tmp/log,如果系统使用 bin/start 脚本作为嵌入式系统启动,则会使用该目录。
  7. 创建文件 tmp/releases/start_erl.data,内容为 "5.10.4 FIRST"。此文件将作为数据文件传递给 start_erl 脚本。
  8. 从目录 tmp 中的目录重新创建文件 mysystem.tar.gz 并删除 tmp

安装目标系统

步骤 4. 将创建的目标系统安装在合适的目录中。

2> target_system:install("mysystem", "/usr/local/erl-target").

函数 target_system:install/2 执行以下操作

  1. 将 tar 文件 mysystem.tar.gz 解压缩到目标目录 /usr/local/erl-target
  2. 在目标目录中,读取文件 releases/start_erl.data 以查找 Erlang 运行时系统版本 ("5.10.4")。
  3. 在目标 erts-5.10.4/bin 目录的文件 erl.srcstart.srcstart_erl.src 中,分别将 %FINAL_ROOTDIR%%EMU% 替换为 /usr/local/erl-targetbeam,并将生成的文件 erlstartrun_erl 放入目标 bin 目录中。
  4. 最后,从文件 releases/mysystem.rel 中的数据创建目标 releases/RELEASES 文件。

启动目标系统

现在我们有一个可以通过各种方式启动的目标系统。我们通过调用以下命令将其作为基本目标系统启动

% /usr/local/erl-target/bin/erl

这里只启动 Kernel 和 STDLIB 应用程序,也就是说,系统是作为普通的开发系统启动的。要使所有这些工作正常进行,只需要两个文件

  1. bin/erl(从 erts-5.10.4/bin/erl.src 获取)
  2. bin/start.bootplain.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/start_erl(大致来说,start_erlerl 的嵌入式变体)。

shell 脚本 start 是在安装过程中从 erts-5.10.4/bin/start.src 生成的,它只是一个示例。对其进行编辑以满足您的需求。通常,它在 UNIX 系统启动时执行。

run_erl 是一个包装器,它提供将运行时系统的输出记录到文件中。它还提供了一个简单的机制来附加到 Erlang shell (to_erl)。

start_erl 需要

  1. 根目录("/usr/local/erl-target"
  2. 发行版目录("/usr/local/erl-target/releases"
  3. 文件 start_erl.data 的位置

它执行以下操作

  1. 从文件 start_erl.data 中读取运行时系统版本("5.10.4")和发行版版本("FIRST")。
  2. 启动找到的版本的运行时系统。
  3. 提供 -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).