├── .gitignore ├── .travis.yml ├── README.md ├── lib └── exrm │ └── reload.ex ├── mix.exs └── test ├── exrm_reload_test.exs ├── test_application ├── .gitignore ├── config │ ├── config.exs │ ├── test_application.conf │ └── test_application.schema.exs ├── lib │ └── test_application.ex └── mix.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | mix.lock 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | before_install: 3 | - epmd -daemon 4 | language: elixir 5 | elixir: 6 | - 1.1.1 7 | otp_release: 8 | - 17.5 9 | - 18.0 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExrmReload [![Build Status](https://travis-ci.org/xerions/exrm_reload.svg)](https://travis-ci.org/xerions/exrm_reload) 2 | 3 | Build new sys.config from [conform](https://github.com/bitwalker/conform) config and apply it at runtume. 4 | It uses `conform_schema` and `conform_config` command line flags which are set on [exrm](https://github.com/bitwalker/exrm) startup script. 5 | 6 | ## Usage 7 | 8 | 1. Add exrm_reload to your list of dependencies in mix.exs: 9 | 10 | ```elixir 11 | def deps do 12 | [{:exrm_reload, github: "xerions/exrm_reload"}] 13 | end 14 | ``` 15 | 16 | 2. Ensure exrm_reload is started before your application: 17 | 18 | ```elixir 19 | def application do 20 | [applications: [:exrm_reload]] 21 | end 22 | ``` 23 | 24 | 3. Run reconfiguration when you want it: 25 | 26 | ```elixir 27 | > ReleaseManager.Reload.run 28 | ``` 29 | 30 | or you can specify application's list for reconfiguration: 31 | 32 | ```elixir 33 | > ReleaseManager.Reload.run [:hello, :exd, :ecdo] 34 | ``` 35 | 36 | It works with the releases are builded via `exrm`. You just call it by rpc from OS shell: 37 | 38 | $ you_application rpc Elixir.ReleaseManager.Reload run 39 | 40 | The test application uses xerions forks of [exrm](https://github.com/xerions/exrm) and [conform](https://github.com/xerions/conform) but it can work with the original exrm version `>= 0.19.7` and conform. Just override it. 41 | -------------------------------------------------------------------------------- /lib/exrm/reload.ex: -------------------------------------------------------------------------------- 1 | defmodule ReleaseManager.Reload do 2 | @moduledoc """ 3 | Reload plugin for EXRM. 4 | 5 | It works at the runtime system. 6 | It generates new sys.config by conform config and schema and applies changes via application_controller. 7 | 8 | I was inspired by corman (https://github.com/EchoTeam/corman) 9 | """ 10 | 11 | @doc "Reload the configuration for all loaded applications." 12 | def run do 13 | (for {application, _, _} <- Application.loaded_applications, do: application) |> run 14 | end 15 | 16 | @doc "Reload the configuration for list of application." 17 | def run(applications) do 18 | {:ok, [[schema]]} = :init.get_argument(:conform_schema) 19 | {:ok, [[config]]} = :init.get_argument(:conform_config) 20 | {:ok, [[sys_config]]} = :init.get_argument(:config) 21 | case :init.get_argument(:running_conf) do 22 | {:ok, [[running_conf]]} -> File.copy! config, running_conf 23 | _ -> :skip 24 | end 25 | generate_sys_config(schema, config, sys_config) 26 | |> check_config! 27 | |> reload(applications) 28 | end 29 | 30 | 31 | defp generate_sys_config(schema, config, sys_config) do 32 | config = config |> List.to_string |> :conf_parse.file 33 | schema = schema |> List.to_string |> Conform.Schema.load! |> Dict.delete(:import) 34 | :code.is_loaded(Conform.SysConfig) == false and :code.load_file(Conform.SysConfig) 35 | case function_exported?(Conform.SysConfig, :read, 1) do 36 | true -> 37 | {:ok, [conf]} = Conform.SysConfig.read(sys_config |> List.to_string) 38 | final = Conform.Translate.to_config(schema, conf, config) 39 | Conform.SysConfig.write(sys_config, final) == :ok and sys_config 40 | false -> 41 | {:ok, [conf]} = Conform.Config.read(sys_config |> List.to_string) 42 | translated = Conform.Translate.to_config(conf, config, schema) 43 | final = Conform.Config.merge(conf, translated) 44 | Conform.Config.write(sys_config, final) == :ok and sys_config 45 | end 46 | end 47 | 48 | defp check_config!(sys_config) do 49 | {:ok, [data]} = :file.consult(sys_config) 50 | data 51 | end 52 | 53 | defp reload(config, applications) do 54 | applications |> application_specs |> change_application_data(config) 55 | end 56 | 57 | defp application_specs(applications) do 58 | specs = for application <- applications, do: {:application, application, make_application_spec(application)} 59 | incorrect_apps = for {_, application, :incorrect_spec} <- specs, do: application 60 | case incorrect_apps do 61 | [] -> specs 62 | _ -> {:incorrect_specs, incorrect_apps} 63 | end 64 | end 65 | 66 | defp make_application_spec(application) when is_atom(application) do 67 | {:ok, loaded_app_apec} = :application.get_all_key(application) 68 | case :code.where_is_file(Atom.to_char_list(application) ++ '.app') do 69 | :non_existing -> loaded_app_apec 70 | app_spec_path when is_list(app_spec_path) -> parse_app_file(app_spec_path) 71 | end 72 | end 73 | 74 | defp parse_app_file(app_spec_path) do 75 | case :file.consult(app_spec_path) do 76 | {:ok, [{:application, _, spec}]} -> spec 77 | {:error, _Reason} -> :incorrect_spec 78 | end 79 | end 80 | 81 | defp change_application_data(specs, config) do 82 | old_env = :application_controller.prep_config_change 83 | :ok = :application_controller.change_application_data(specs, config) 84 | :application_controller.config_change(old_env) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExrmReload.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :exrm_reload, 6 | version: "0.2.1", 7 | elixir: ">= 1.0.5", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:conform]] 15 | end 16 | 17 | defp deps do 18 | [{:conform, github: "xerions/conform", branch: "master"}] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/exrm_reload_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExrmReloadTest do 2 | use ExUnit.Case 3 | doctest ReleaseManager.Reload 4 | 5 | setup do 6 | {:ok, _} = :net_kernel.start([:master, :longnames]) 7 | true = :erlang.set_cookie(node, :test_application) 8 | System.cmd("mix", ["do", "deps.get,", "compile,", "release"], [cd: "test/test_application"]) 9 | :os.cmd('./test/test_application/rel/test_application/bin/test_application start') |> IO.inspect 10 | :pong = ping 11 | on_exit fn -> 12 | :os.cmd('./test/test_application/rel/test_application/bin/test_application stop') 13 | System.cmd("rm", ["-Rf", "deps", "_build", "rel"], [cd: "test/test_application"]) 14 | end 15 | end 16 | 17 | test "test_application" do 18 | assert true == rpc(Application, :get_env, [:test_application, :test_value]) 19 | assert 10 == rpc(Application, :get_env, [:test_application, :test_value2]) 20 | 21 | {:ok, [[conf]]} = rpc(:init, :get_argument, [:conform_config]) 22 | conf |> List.to_string |> File.write!("test_value = false") 23 | assert :ok == rpc(ReleaseManager.Reload, :run) 24 | 25 | assert false == rpc(Application, :get_env, [:test_application, :test_value]) 26 | assert 10 == rpc(Application, :get_env, [:test_application, :test_value2]) 27 | assert "new" == rpc(Application, :get_env, [:test_application, :new_value]) 28 | end 29 | 30 | defp rpc(module, function, args \\ []) do 31 | :rpc.call(:"test_application@127.0.0.1", module, function, args) 32 | end 33 | 34 | defp ping(), do: ping(3) 35 | defp ping(n) do 36 | :timer.sleep(5000) 37 | case :net_adm.ping :"test_application@127.0.0.1" do 38 | :pong -> :pong 39 | _ -> if n < 0, do: :pang, else: ping(n-1) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/test_application/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | mix.lock 7 | -------------------------------------------------------------------------------- /test/test_application/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | config :test_application, test_value2: 10 3 | -------------------------------------------------------------------------------- /test/test_application/config/test_application.conf: -------------------------------------------------------------------------------- 1 | test_value = true 2 | -------------------------------------------------------------------------------- /test/test_application/config/test_application.schema.exs: -------------------------------------------------------------------------------- 1 | [ 2 | mappings: [ 3 | "test_value": [ 4 | to: "test_application.test_value", 5 | datatype: :boolean, 6 | default: true 7 | ] 8 | ], 9 | translations: [] 10 | ] 11 | -------------------------------------------------------------------------------- /test/test_application/lib/test_application.ex: -------------------------------------------------------------------------------- 1 | defmodule TestApplication do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | import Supervisor.Spec, warn: false 6 | 7 | opts = [strategy: :one_for_one, name: TestApplication.Supervisor] 8 | Supervisor.start_link([], opts) 9 | end 10 | 11 | def config_change(_changed, _new, _removed) do 12 | spawn fn -> :application.set_env(:test_application, :new_value, "new") end 13 | :ok 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/test_application/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TestApplication.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :test_application, 6 | version: "0.0.1", 7 | deps: deps] 8 | end 9 | 10 | def application do 11 | [mod: {TestApplication, []}, 12 | applications: [:exrm_reload]] 13 | end 14 | 15 | def deps do 16 | [{:exrm_reload, path: "../../"}, 17 | {:conform, github: "xerions/conform", branch: "master"}, 18 | {:exrm, github: "xerions/exrm", branch: "master"}] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(timeout: 60000) 2 | --------------------------------------------------------------------------------