3  创建和升级目标系统

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 列表

假设您有一个根据 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 执行以下操作

  1. 读取 mysystem.rel 文件并创建一个名为 plain.rel 的新文件,该文件与前者相同,只是它只列出了 Kernel 和 STDLIB 应用程序。
  2. mysystem.relplain.rel 文件中,通过调用 systools:make_script/2 创建 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。但是,有时解压缩 tar 文件时不会涉及 release_handler(例如,在解压缩第一个目标系统时)。因此,现在该文件在 tar 文件中被复制,因此无需手动复制。

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

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

os> /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,如下所示

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_erlerl 的嵌入式变体)。

在安装过程中从 erts-5.10.4/bin/start.src 生成的 shell 脚本 start 只是一个示例。编辑它以满足您的需求。通常在 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,它将被包含,并且不需要是有效的 Erlang 术语文件,例如 sys.config。在运行发布版本之前,您必须在同一个目录中有一个有效的 sys.config,因此使用 sys.config.src 需要使用一些工具来填充所需的内容,并在启动发布版本之前将 sys.config 写入磁盘。

之前的 install/2 过程与普通的 Install shell 脚本略有不同。实际上,create/1 会尽可能完整地创建发布包,并将仅考虑位置相关文件的完成工作留给 install/2 过程。

在这个例子中,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 文件现在位于当前目录中,它会自动包含在发布包中。

此部分在目标节点上完成,对于此示例,我们希望节点以 -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,因此重新启动节点是安全的。

此模块也可以在 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).