├── test ├── test_helper.exs └── code_reloader_test.exs ├── lib ├── code_reloader.ex └── code_reloader │ ├── proxy.ex │ ├── server.ex │ └── plug.ex ├── mix.lock ├── README.md ├── .gitignore ├── mix.exs ├── LICENSE └── config └── config.exs /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/code_reloader.ex: -------------------------------------------------------------------------------- 1 | defmodule CodeReloader do 2 | end 3 | -------------------------------------------------------------------------------- /test/code_reloader_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CodeReloaderTest do 2 | use ExUnit.Case 3 | doctest CodeReloader 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, 2 | "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}} 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeReloader 2 | 3 | Code reloader Plug extracted from [Phoenix](https://github.com/phoenixframework/phoenix/) and adapted to be a generic Plug. 4 | 5 | So far it's just a proof of concept to understand if having a generic code reload Plug makes sense or not. 6 | 7 | ## Why 8 | 9 | If you have an Elixir web app using only Plug without Phoenix, you need to restart it everytime you update the code. 10 | 11 | ## Usage 12 | 13 | The the example app at [https://github.com/pilu/code-reload-example](https://github.com/pilu/code-reload-example) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule CodeReloader.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :code_reloader, 6 | version: "0.1.0", 7 | elixir: "~> 1.4", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | # Specify extra applications you'll use from Erlang/Elixir 18 | [extra_applications: [:logger]] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:my_dep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 28 | # 29 | # Type "mix help deps" for more examples and options 30 | defp deps do 31 | [{:plug, "~> 1.0"}] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrea Franz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :code_reloader, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:code_reloader, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/code_reloader/proxy.ex: -------------------------------------------------------------------------------- 1 | # A tiny proxy that stores all output sent to the group leader 2 | # while forwarding all requests to it. 3 | defmodule CodeReloader.Proxy do 4 | @moduledoc false 5 | use GenServer 6 | 7 | def start() do 8 | GenServer.start(__MODULE__, "") 9 | end 10 | 11 | def stop(proxy) do 12 | GenServer.call(proxy, :stop) 13 | end 14 | 15 | ## Callbacks 16 | 17 | def handle_call(:stop, _from, output) do 18 | {:stop, :normal, output, output} 19 | end 20 | 21 | def handle_info(msg, output) do 22 | case msg do 23 | {:io_request, from, reply, {:put_chars, chars}} -> 24 | put_chars(from, reply, chars, output) 25 | 26 | {:io_request, from, reply, {:put_chars, m, f, as}} -> 27 | put_chars(from, reply, apply(m, f, as), output) 28 | 29 | {:io_request, from, reply, {:put_chars, _encoding, chars}} -> 30 | put_chars(from, reply, chars, output) 31 | 32 | {:io_request, from, reply, {:put_chars, _encoding, m, f, as}} -> 33 | put_chars(from, reply, apply(m, f, as), output) 34 | 35 | {:io_request, _from, _reply, _request} = msg -> 36 | send(Process.group_leader, msg) 37 | {:noreply, output} 38 | 39 | _ -> 40 | {:noreply, output} 41 | end 42 | end 43 | 44 | defp put_chars(from, reply, chars, output) do 45 | send(Process.group_leader, {:io_request, from, reply, {:put_chars, chars}}) 46 | {:noreply, output <> IO.chardata_to_string(chars)} 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/code_reloader/server.ex: -------------------------------------------------------------------------------- 1 | defmodule CodeReloader.Server do 2 | @moduledoc false 3 | use GenServer 4 | 5 | require Logger 6 | alias CodeReloader.Proxy 7 | 8 | def start_link() do 9 | GenServer.start_link(__MODULE__, false, name: __MODULE__) 10 | end 11 | 12 | def check_symlinks do 13 | GenServer.call(__MODULE__, :check_symlinks, :infinity) 14 | end 15 | 16 | def reload!(endpoint) do 17 | GenServer.call(__MODULE__, {:reload!, endpoint}, :infinity) 18 | end 19 | 20 | ## Callbacks 21 | 22 | def init(false) do 23 | {:ok, false} 24 | end 25 | 26 | def handle_call(:check_symlinks, _from, checked?) do 27 | if not checked? and Code.ensure_loaded?(Mix.Project) do 28 | build_path = Mix.Project.build_path() 29 | symlink = Path.join(Path.dirname(build_path), "__phoenix__") 30 | 31 | case File.ln_s(build_path, symlink) do 32 | :ok -> 33 | File.rm(symlink) 34 | {:error, :eexist} -> 35 | File.rm(symlink) 36 | {:error, _} -> 37 | Logger.warn "Phoenix is unable to create symlinks. Phoenix' code reloader will run " <> 38 | "considerably faster if symlinks are allowed." <> os_symlink(:os.type) 39 | end 40 | end 41 | 42 | {:reply, :ok, true} 43 | end 44 | 45 | def handle_call({:reload!, endpoint}, from, state) do 46 | compilers = endpoint.config(:reloadable_compilers) 47 | backup = load_backup(endpoint) 48 | froms = all_waiting([from], endpoint) 49 | 50 | {res, out} = 51 | proxy_io(fn -> 52 | try do 53 | mix_compile(Code.ensure_loaded(Mix.Task), compilers) 54 | catch 55 | :exit, {:shutdown, 1} -> 56 | :error 57 | kind, reason -> 58 | IO.puts Exception.format(kind, reason, System.stacktrace) 59 | :error 60 | end 61 | end) 62 | 63 | reply = 64 | case res do 65 | :ok -> 66 | :ok 67 | :error -> 68 | write_backup(backup) 69 | {:error, out} 70 | end 71 | 72 | Enum.each(froms, &GenServer.reply(&1, reply)) 73 | {:noreply, state} 74 | end 75 | 76 | def handle_info(_, state) do 77 | {:noreply, state} 78 | end 79 | 80 | defp os_symlink({:win32, _}), 81 | do: " On Windows, such can be done by starting the shell with \"Run as Administrator\"." 82 | defp os_symlink(_), 83 | do: "" 84 | 85 | defp load_backup(mod) do 86 | mod 87 | |> :code.which() 88 | |> read_backup() 89 | end 90 | defp read_backup(path) when is_list(path) do 91 | case File.read(path) do 92 | {:ok, binary} -> {:ok, path, binary} 93 | _ -> :error 94 | end 95 | end 96 | defp read_backup(_path), do: :error 97 | 98 | defp write_backup({:ok, path, file}), do: File.write!(path, file) 99 | defp write_backup(:error), do: :ok 100 | 101 | defp all_waiting(acc, endpoint) do 102 | receive do 103 | {:"$gen_call", from, {:reload!, ^endpoint}} -> all_waiting([from | acc], endpoint) 104 | after 105 | 0 -> acc 106 | end 107 | end 108 | 109 | # TODO: Remove the function_exported call after 1.3 support is removed 110 | # and just use loaded. apply/3 is used to prevent a compilation 111 | # warning. 112 | defp mix_compile({:module, Mix.Task}, compilers) do 113 | if Mix.Project.umbrella? do 114 | deps = 115 | if function_exported?(Mix.Dep.Umbrella, :cached, 0) do 116 | apply(Mix.Dep.Umbrella, :cached, []) 117 | else 118 | Mix.Dep.Umbrella.loaded 119 | end 120 | Enum.each deps, fn dep -> 121 | Mix.Dep.in_dependency(dep, fn _ -> 122 | mix_compile_unless_stale_config(compilers) 123 | end) 124 | end 125 | else 126 | mix_compile_unless_stale_config(compilers) 127 | :ok 128 | end 129 | end 130 | defp mix_compile({:error, _reason}, _) do 131 | raise "the Code Reloader is enabled but Mix is not available. If you want to " <> 132 | "use the Code Reloader in production or inside an escript, you must add " <> 133 | ":mix to your applications list. Otherwise, you must disable code reloading " <> 134 | "in such environments" 135 | end 136 | 137 | defp mix_compile_unless_stale_config(compilers) do 138 | manifests = Mix.Tasks.Compile.Elixir.manifests 139 | configs = Mix.Project.config_files 140 | 141 | case Mix.Utils.extract_stale(configs, manifests) do 142 | [] -> 143 | mix_compile(compilers) 144 | files -> 145 | raise """ 146 | could not compile application: #{Mix.Project.config[:app]}. 147 | 148 | You must restart your server after changing the following config or lib files: 149 | 150 | * #{Enum.map_join(files, "\n * ", &Path.relative_to_cwd/1)} 151 | """ 152 | end 153 | end 154 | 155 | defp mix_compile(compilers) do 156 | all = Mix.Project.config[:compilers] || Mix.compilers 157 | 158 | compilers = 159 | for compiler <- compilers, compiler in all do 160 | Mix.Task.reenable("compile.#{compiler}") 161 | compiler 162 | end 163 | 164 | # We call build_structure mostly for Windows so new 165 | # assets in priv are copied to the build directory. 166 | Mix.Project.build_structure 167 | res = Enum.map(compilers, &Mix.Task.run("compile.#{&1}", [])) 168 | 169 | if :ok in res && consolidate_protocols?() do 170 | Mix.Task.reenable("compile.protocols") 171 | Mix.Task.run("compile.protocols", []) 172 | end 173 | 174 | res 175 | end 176 | 177 | defp consolidate_protocols? do 178 | Mix.Project.config[:consolidate_protocols] 179 | end 180 | 181 | defp proxy_io(fun) do 182 | original_gl = Process.group_leader 183 | {:ok, proxy_gl} = Proxy.start() 184 | Process.group_leader(self(), proxy_gl) 185 | 186 | try do 187 | {fun.(), Proxy.stop(proxy_gl)} 188 | after 189 | Process.group_leader(self(), original_gl) 190 | Process.exit(proxy_gl, :kill) 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/code_reloader/plug.ex: -------------------------------------------------------------------------------- 1 | defmodule CodeReloader.Plug do 2 | @moduledoc """ 3 | A plug and module to handle automatic code reloading. 4 | 5 | For each request, Phoenix checks if any of the modules previously 6 | compiled requires recompilation via `__phoenix_recompile__?/0` and then 7 | calls `mix compile` for sources exclusive to the `web` directory. 8 | 9 | To avoid race conditions, all code reloads are funneled through a 10 | sequential call operation. 11 | """ 12 | 13 | ## Server delegation 14 | 15 | @doc """ 16 | Reloads code for the current Mix project by invoking the 17 | `:reloadable_compilers`. 18 | 19 | This is configured in your application environment like: 20 | 21 | config :your_app, YourApp.Endpoint, 22 | reloadable_compilers: [:gettext, :phoenix, :elixir] 23 | 24 | Keep in mind `:reloadable_compilers` must be a subset of the 25 | `:compilers` specified in `project/0` in your `mix.exs`. 26 | """ 27 | @spec reload!(module) :: :ok | {:error, binary()} 28 | defdelegate reload!(endpoint), to: CodeReloader.Server 29 | 30 | ## Plug 31 | 32 | @behaviour Plug 33 | import Plug.Conn 34 | import Logger 35 | 36 | @style %{ 37 | primary: "#EB532D", 38 | accent: "#a0b0c0", 39 | text_color: "304050", 40 | logo: "data:image/png;base64,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", 41 | monospace_font: "menlo, consolas, monospace" 42 | } 43 | 44 | @doc """ 45 | API used by Plug to start the code reloader. 46 | """ 47 | def init(opts), do: Keyword.put_new(opts, :reloader, &CodeReloader.Plug.reload!/1) 48 | 49 | @doc """ 50 | API used by Plug to invoke the code reloader on every request. 51 | """ 52 | def call(conn, opts) do 53 | reloader = Keyword.get(opts, :reloader) 54 | endpoint = Keyword.get(opts, :endpoint) 55 | do_call(conn, reloader, endpoint) 56 | end 57 | 58 | defp do_call(conn, _reloader, endpoint) when is_nil(endpoint) do 59 | Logger.error("CodeReloader: couldn't reload. opts[:endpoint] must be specified when using CodeReloader.Plug.") 60 | conn 61 | end 62 | defp do_call(conn, reloader, endpoint) do 63 | Logger.info("CodeReloader: reloading") 64 | case reloader.(endpoint) do 65 | :ok -> 66 | conn 67 | {:error, output} -> 68 | conn 69 | |> put_resp_content_type("text/html") 70 | |> send_resp(500, template(output)) 71 | |> halt() 72 | end 73 | end 74 | 75 | defp template(output) do 76 | {error, headline} = get_error_details(output) 77 | 78 | """ 79 | 80 | 81 |
82 | 83 |#{format_output(output)}
259 |