├── .gitignore ├── .tool-versions ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── harakiri.ex └── harakiri │ ├── action_group.ex │ ├── helpers.ex │ └── worker.ex ├── mix.exs ├── mix.lock └── test ├── harakiri_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | *.swp 6 | *.kate-swp 7 | .kateproject.d 8 | .zedstate 9 | .directory 10 | /doc 11 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 20.0 2 | elixir 1.5.0 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.4.0 4 | otp_release: 5 | - 19.2 6 | sudo: false 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 El Pulgar Del Panda 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Harakiri 腹切 2 | 3 | [![Build Status](https://travis-ci.org/rubencaro/harakiri.svg?branch=master)](https://travis-ci.org/rubencaro/harakiri) 4 | [![Hex Version](http://img.shields.io/hexpm/v/harakiri.svg?style=flat)](https://hex.pm/packages/harakiri) 5 | [![Hex Version](http://img.shields.io/hexpm/dt/harakiri.svg?style=flat)](https://hex.pm/packages/harakiri) 6 | 7 | `Harakiri` was concieved to help applications kill themselves in response to a `touch` to a file on disk. It grew into something scarier. 8 | 9 | Given a list of _files_, an _application_, and an _action_. When any of the files change on disk (i.e. a gentle `touch` is enough), then the given action is fired over the app. 10 | 11 | Everything is in an OTP Application so you can have it running in your 12 | system to help all your other applications kill themselves. 13 | 14 | Actions can be: 15 | 16 | * Any anonymous function. 17 | * `:restart`: Restarts the whole VM, runs `:init.restart`. 18 | * `:stop`: Stops, unloads and deletes app's entry from path. 19 | * `:reload`: like `:stop`, then adds given `lib_path` to path and runs 20 | `Application.ensure_all_started/1`. 21 | 22 | ## Use 23 | 24 | First of all, __add it to your `applications` list__ to ensure it's up before your app starts. 25 | 26 | Then add to your `deps` like this: 27 | 28 | ```elixir 29 | {:harakiri, ">= 1.2.0"} 30 | ``` 31 | 32 | Add an _monitor_ like this: 33 | 34 | ```elixir 35 | Harakiri.monitor "/path/to/tmp/file", &MyModule.myfun 36 | ``` 37 | 38 | Or an _action group_ like this: 39 | 40 | ```elixir 41 | Harakiri.add %{paths: ["file1","file2"], action: &MyModule.myfun} 42 | ``` 43 | 44 | You are done. That would run `MyModule.myfun` when `file1` (or `file2`) is touched. All given files (`file1`, `file2`, etc.) must exist, unless you give the option `:create_paths`. Then all given paths will be created if they do not already exist. 45 | 46 | ## Whole VM restart 47 | 48 | If your app is the main one in the Erlang node, then you may consider a whole `:restart`: 49 | 50 | ```elixir 51 | Harakiri.monitor "/path/to/tmp/restart", :restart 52 | ``` 53 | 54 | That would restart the VM. I.e. stop every application and start them again. __All without stopping the running node__, so it's fast enough for most cases. It tipically takes around one second. See [init.restart/0](http://www.erlang.org/doc/man/init.html#restart-0). 55 | 56 | ## Anonymous functions 57 | 58 | If you need some specific function for your app to be cleanly accessible from outside your VM, then you can pass it as a function. To that function is passed a list with the whole `ActionGroup` and some info on the actual path that fired the event. Like this: 59 | 60 | ```elixir 61 | myfun = fn(data)-> 62 | # check the exact path that fired 63 | case data[:file][:path] do 64 | "/path/to/fire/myfun1" -> do_something1 65 | "/path/to/fire/myfun2" -> do_something2 66 | end 67 | # see all the info you have 68 | data |> inspect |> Logger.info 69 | end 70 | 71 | Harakiri.add %{paths: ["/path/to/fire/myfun1","/path/to/fire/myfun2"], 72 | action: myfun} 73 | ``` 74 | 75 | This way you can code in pure elixir any complex process you need to perform on a production system. You could perform hot code swaps back and forth between releases of some module, go up&down logging levels, some weird maintenance task, etc. All with a simple `touch` of the right file. 76 | 77 | If you perform an `echo` instead of a `touch`, then you could even do something with the contents of the file that fired. 78 | 79 | This is quite powerful. Enjoy it. 80 | 81 | ## Shipped actions 82 | 83 | The `:restart` action is suited for a project deployed as the main application in the entire VM. `:init.restart` will kill all applications and then restart them all again. 84 | 85 | The `:stop` and `reload` actions are suited for quick operations over a single application, not its dependencies. For instance, `:stop` unloads and deletes the app's entry from path. No other application is stopped and removed from path. 86 | 87 | ```elixir 88 | Harakiri.monitor "file1", :stop 89 | ``` 90 | 91 | `:reload` will ensure all dependencies are started before the app as it uses `ensure_all_started`, but it will not bother adding them to the path. So any dependency that changed will most probably not start because it will be missing from path. 92 | 93 | ```elixir 94 | Harakiri.add %{paths: ["file1"], 95 | action: :reload, 96 | lib_path: "path"} 97 | ``` 98 | 99 | `lib_path` is the path to the folder containing the `ebin` folder for the current version of the app, usually a link to it. `lib_path` is only needed by `:reload`. 100 | 101 | ## Demo 102 | 103 | [![asciicast](https://asciinema.org/a/18338.png)](https://asciinema.org/a/18338) 104 | 105 | ## TODOs 106 | 107 | * Support for multiple apps on each action set. 108 | * Support for several actions on each action set. 109 | * Deeper test, complete deploy/upgrade/reload simulation 110 | 111 | ## Changelog 112 | 113 | ### 1.2.0 114 | 115 | * Add `monitor` for simpler use 116 | * Remove Elixir 1.5 warnings 117 | 118 | ### 1.1.1 119 | 120 | * Remove Elixir 1.4 warnings 121 | 122 | ### 1.1.0 123 | 124 | * Add support for async firings 125 | * Make more noise when given function fails 126 | 127 | ### 1.0.2 128 | 129 | * Avoid Elixir 1.3 warnings 130 | 131 | ### 1.0.1 132 | 133 | * Do not touch already existing files on start 134 | 135 | ### 1.0.0 136 | 137 | * Use it on several projects in production without problems 138 | * Avoid race conditions with ETS on testing 139 | 140 | ### 0.6.0 141 | 142 | * Support for anonymous functions as actions 143 | 144 | ### 0.5.1 145 | 146 | * Set initial mtime for created files 147 | 148 | ### 0.5.0 149 | 150 | * Support create paths when asked 151 | * Fix some testing inconsistency 152 | 153 | ### 0.4.0 154 | 155 | * Use ETS to preserve state 156 | * Rearrange using a supervised `Task` for the main loop and regular helpers to access the ETS table. No need for a `GenServer` anymore. 157 | 158 | ### 0.3.0 159 | 160 | * Allow only one instance of the same action group. 161 | 162 | ### 0.2.0 163 | 164 | * First release 165 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # improve testability 4 | loop_sleep_ms = case Mix.env do 5 | :test -> 1 6 | _ -> 5_000 7 | end 8 | config :harakiri, :loop_sleep_ms, loop_sleep_ms -------------------------------------------------------------------------------- /lib/harakiri.ex: -------------------------------------------------------------------------------- 1 | require Harakiri.Helpers, as: H 2 | alias Harakiri, as: Hk 3 | 4 | defmodule Harakiri do 5 | use Application 6 | 7 | @doc """ 8 | Start and supervise a single lonely worker 9 | """ 10 | def start(_,_) do 11 | import Supervisor.Spec 12 | 13 | # start ETS named_table from here, 14 | # thus make it persistent as long as the VM runs (Harakiri should be `permanent`...) 15 | :ets.new(:harakiri_table, [:public,:set,:named_table]) 16 | 17 | loop_sleep_ms = H.env(:loop_sleep_ms, 5_000) 18 | 19 | opts = [strategy: :one_for_one, name: Hk.Supervisor] 20 | children = [ worker(Task, [Hk.Worker,:loop,[loop_sleep_ms]]) ] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | @doc """ 25 | Add given `Map` as an `Harakiri.ActionGroup`. 26 | See README or tests for examples. 27 | """ 28 | def add(data, opts \\ []) when is_map(data), do: data |> H.digest_data |> H.insert(opts) 29 | 30 | @doc """ 31 | Run given `fun` or `action` when given `path` is touched. 32 | """ 33 | def monitor(path, fun, opts \\ []) 34 | def monitor(path, fun, opts) when is_binary(path) and is_function(fun), 35 | do: %{paths: [path], action: fun} |> add(opts) 36 | def monitor(path, action, opts) when is_binary(path) and is_atom(action), 37 | do: %{paths: [path], action: action} |> add(opts) 38 | 39 | @doc """ 40 | Get/set all Harakiri state 41 | """ 42 | def state do 43 | :ets.tab2list(:harakiri_table) 44 | |> Enum.sort 45 | |> Enum.map(fn({_,ag})-> ag end) # remove keys 46 | end 47 | def state(data), do: for( d <- data, do: :ok = H.upsert(d) ) 48 | end 49 | -------------------------------------------------------------------------------- /lib/harakiri/action_group.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule Harakiri.ActionGroup do 3 | defstruct paths: [], 4 | app: nil, 5 | action: nil, 6 | lib_path: nil, 7 | metadata: [loops: 0, hits: 0] 8 | end 9 | -------------------------------------------------------------------------------- /lib/harakiri/helpers.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule Harakiri.Helpers do 3 | 4 | @doc """ 5 | Convenience to get environment bits. Avoid all that repetitive 6 | `Application.get_env( :myapp, :blah, :blah)` noise. 7 | """ 8 | def env(key, default \\ nil), do: env(:harakiri, key, default) 9 | def env(app, key, default), do: Application.get_env(app, key, default) 10 | 11 | @doc """ 12 | Spit to output any passed variable, with location information. 13 | """ 14 | defmacro spit(obj \\ "", inspect_opts \\ []) do 15 | quote do 16 | %{file: file, line: line} = __ENV__ 17 | name = Process.info(self)[:registered_name] 18 | chain = [ :bright, :red, "\n\n#{file}:#{line}", 19 | :normal, "\n #{inspect self}", :green," #{name}"] 20 | 21 | msg = inspect(unquote(obj),unquote(inspect_opts)) 22 | if String.length(msg) > 2, do: chain = chain ++ [:red, "\n\n#{msg}"] 23 | 24 | # chain = chain ++ [:yellow, "\n\n#{inspect Process.info(self)}"] 25 | 26 | (chain ++ ["\n\n", :reset]) |> IO.ANSI.format(true) |> IO.puts 27 | 28 | unquote(obj) 29 | end 30 | end 31 | 32 | @doc """ 33 | Gets a `Map` and puts it into an `ActionGroup` just the way `Harakiri` 34 | needs it. 35 | 36 | The `Map` should look like: 37 | ``` 38 | %{paths: ["file1","file2"], app: :myapp, action: :reload, lib_path: "path"} 39 | ``` 40 | """ 41 | def digest_data(data) when is_map(data) do 42 | data = %Harakiri.ActionGroup{} |> Map.merge(data) # put into an ActionGroup 43 | 44 | # one mtime for each path 45 | paths = for p <- data.paths, into: [], 46 | do: [path: p, mtime: get_file_mtime(p)] 47 | 48 | %{data | paths: paths} 49 | end 50 | 51 | @doc """ 52 | Get the key to be used as on the ETS table 53 | """ 54 | def get_key(data), do: [data.app, data.action] |> inspect |> to_charlist 55 | 56 | @doc """ 57 | Insert given data into `:harakiri_table`. 58 | Returns `{:ok, key}` if inserted, `:duplicate` if given data existed. 59 | 60 | If option `:create_paths` is truthy, then it tries to create every path 61 | in data[:paths]. It returns `{:error, reason}` when that failed. 62 | """ 63 | def insert(data, opts \\ []) when is_map(data) do 64 | case opts[:create_paths] && create_paths(data.paths) do 65 | :ok -> # paths created, need to set initial mtime 66 | data |> set_initial_mtime |> do_insert 67 | nil -> do_insert(data) # no create_paths, go on 68 | x -> x 69 | end 70 | end 71 | 72 | defp set_initial_mtime(data) do 73 | paths = for p <- data.paths, into: [], 74 | do: [path: p[:path], mtime: get_file_mtime(p[:path])] 75 | %{data | paths: paths} 76 | end 77 | 78 | defp do_insert(data) do 79 | key = get_key(data) 80 | res = :ets.insert_new(:harakiri_table, {key, data}) 81 | if res, do: {:ok, key}, else: :duplicate 82 | end 83 | 84 | @doc """ 85 | Insert the given data on the table. Update if it was lready there. 86 | """ 87 | def upsert(data) when is_map(data) do 88 | true = :ets.insert(:harakiri_table, {get_key(data), data}) 89 | :ok 90 | end 91 | 92 | @doc """ 93 | Get first row from the table 94 | """ 95 | def first, do: lookup(:ets.first(:harakiri_table)) 96 | 97 | @doc """ 98 | Get the row for the given key, if it exists. If given key is 99 | `:"$end_of_table"` it will return `nil`. 100 | """ 101 | def lookup(:"$end_of_table"), do: nil 102 | def lookup(key) do 103 | [{_, data}] = :ets.lookup(:harakiri_table, key) 104 | data 105 | end 106 | 107 | @doc """ 108 | Get mtime from the OS for the given path 109 | """ 110 | def get_file_mtime(path) do 111 | :os.cmd('ls -l --time-style=full-iso #{path}') 112 | |> to_string |> String.split |> Enum.at(6) 113 | end 114 | 115 | # Call `create_path/1` for every path in given paths list 116 | # Return `:ok` if success. 117 | # Stop looping and return `{:error, reason}` if failed. 118 | # 119 | defp create_paths([]), do: :ok 120 | defp create_paths([path|rest]) do 121 | case create_path(path[:path]) do 122 | :ok -> create_paths(rest) 123 | x -> x 124 | end 125 | end 126 | 127 | # Create folders and touch the file for the given path 128 | # Returns `:ok` or `{:error, reason}` 129 | # 130 | defp create_path(path) do 131 | res = path |> Path.dirname |> File.mkdir_p 132 | case res do 133 | :ok -> path |> create_file 134 | x -> x 135 | end 136 | end 137 | 138 | # Touches given file path only if it does not exist 139 | # 140 | defp create_file(path) do 141 | if not File.exists?(path), do: File.touch(path) 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/harakiri/worker.ex: -------------------------------------------------------------------------------- 1 | require Harakiri.Helpers, as: H 2 | alias Harakiri, as: Hk 3 | alias Keyword, as: K 4 | 5 | defmodule Harakiri.Worker do 6 | 7 | @doc """ 8 | Perform requested action if any given path is touched. 9 | Else keep an infinite loop sleeping given msecs each time. 10 | """ 11 | def loop(sleep_ms) do 12 | 13 | # go over every ag and update them 14 | updated_ags = for ag <- Hk.state, into: [] do 15 | 16 | # check every path, calc new mtimes and metadata 17 | checked_paths = for p <- ag.paths, into: [] do 18 | new_mtime = check_file(p,ag) 19 | [path: p[:path], mtime: new_mtime, hit: p[:mtime] != new_mtime] 20 | end 21 | 22 | # save new mtimes for paths 23 | paths = for p <- checked_paths, into: [], 24 | do: [path: p[:path], mtime: p[:mtime]] 25 | 26 | # update metadata 27 | md = ag.metadata 28 | md = K.put(md, :loops, md[:loops] + 1) # +1 loops 29 | md = if Enum.any?(checked_paths, &(&1[:hit])), # if any path was hit 30 | do: K.put(md, :hits, md[:hits] + 1), # +1 hits 31 | else: md 32 | 33 | # update ag's data 34 | %{ ag | paths: paths, metadata: md } 35 | end 36 | 37 | # replace old ags with the new ones 38 | Hk.state updated_ags 39 | 40 | # sleep and loop 41 | :timer.sleep sleep_ms 42 | loop sleep_ms 43 | end 44 | 45 | # Fire the corresponding function if any mtime changed 46 | # 47 | defp check_file(path, ag) do 48 | new_mtime = H.get_file_mtime path[:path] 49 | if path[:mtime] && (path[:mtime] != new_mtime), 50 | do: fire(ag.action, ag: ag, file: path) 51 | new_mtime 52 | end 53 | 54 | @doc """ 55 | Fire the `:stop` callback for the given ActionGroup 56 | """ 57 | def fire(:stop, data) do 58 | ag = data[:ag] 59 | res = Application.stop(ag.app) 60 | IO.puts "Stopped #{ag.app}... #{inspect res}" 61 | res = Application.unload ag.app 62 | IO.puts "Unloaded #{ag.app}... #{inspect res}" 63 | res = :code.del_path(ag.app) 64 | IO.puts "Removed from path #{ag.app}... #{inspect res}" 65 | :ok 66 | end 67 | 68 | @doc """ 69 | Fire the `:reload` callback for the given ActionGroup 70 | """ 71 | def fire(:reload, data) do 72 | ag = data[:ag] 73 | :ok = fire :stop, data 74 | res = :code.add_patha('#{ag.lib_path}/ebin') 75 | IO.puts "Added to path #{ag.app}... #{inspect res}" 76 | res = Application.ensure_all_started ag.app 77 | IO.puts "Started #{ag.app}... #{inspect res}" 78 | :ok 79 | end 80 | 81 | @doc """ 82 | Fire the `:restart` callback for the given ActionGroup 83 | """ 84 | def fire(:restart, _data) do 85 | res = :init.restart 86 | IO.puts "Scheduled system restart... #{inspect res}" 87 | :ok 88 | end 89 | 90 | @doc """ 91 | Fire the given anonymous function for the given ActionGroup 92 | """ 93 | def fire(fun, data) when is_function(fun) do 94 | Task.start_link(fn -> 95 | try do 96 | IO.puts "Running requested function..." 97 | res = fun.(data) 98 | IO.puts "Ran requested function: #{inspect res}" 99 | rescue 100 | x -> IO.puts "Error running requested function: #{inspect x}, backtrace: #{inspect System.stacktrace}" 101 | catch 102 | x -> IO.puts "Error running requested function: #{inspect x}" 103 | end 104 | end) 105 | 106 | :ok 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Harakiri.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :harakiri, 6 | version: "1.2.0", 7 | elixir: ">= 1.0.0", 8 | package: package(), 9 | description: """ 10 | Help applications do things to themselves. 11 | """, 12 | deps: [{:ex_doc, ">= 0.0.0", only: :dev}]] 13 | end 14 | 15 | def application do 16 | [mod: {Harakiri, []}] 17 | end 18 | 19 | defp package do 20 | [maintainers: ["Rubén Caro"], 21 | licenses: ["MIT"], 22 | links: %{github: "https://github.com/rubencaro/harakiri"}] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}} 3 | -------------------------------------------------------------------------------- /test/harakiri_test.exs: -------------------------------------------------------------------------------- 1 | alias Harakiri, as: Hk 2 | require Harakiri.Helpers, as: H 3 | alias TestHelpers, as: TH 4 | 5 | defmodule HarakiriTest do 6 | use ExUnit.Case 7 | 8 | test "The supervisor ancestor owns the ETS table" do 9 | # the table exists 10 | refute :ets.info(:harakiri_table) == :undefined 11 | 12 | # get the owner 13 | owner = :ets.info(:harakiri_table)[:owner] 14 | 15 | # get the supervisor ancestor 16 | info = Process.whereis(Harakiri.Supervisor) |> Process.info 17 | sup_ancestor = info[:dictionary][:"$ancestors"] |> List.first 18 | 19 | assert owner == sup_ancestor 20 | end 21 | 22 | test "adds and gets state" do 23 | # put some state 24 | data = %Hk.ActionGroup{paths: [], app: :bogus, action: :stop} 25 | {:ok,_} = Hk.add data 26 | data2 = %Hk.ActionGroup{paths: [], app: :bogus2, action: :stop} 27 | {:ok,_} = Hk.add data2 28 | 29 | # the second time it's not duplicated 30 | :duplicate = Hk.add data 31 | 32 | # check it's there only once 33 | assert 2 == Enum.count(Hk.state, fn(ag)-> ag.app in [:bogus, :bogus2] end) 34 | end 35 | 36 | test "simple signature works as well" do 37 | # the simpler signature works as well 38 | {:ok, k1} = Hk.monitor "/tmp/bogus61", :stop 39 | {:ok, k2} = Hk.monitor "/tmp/bogus62", fn(_) -> :ok end 40 | assert %{paths: [path1], action: :stop} = H.lookup(k1) 41 | assert path1[:path] == "/tmp/bogus61" 42 | assert %{paths: [path2], action: fun} = H.lookup(k2) 43 | assert path2[:path] == "/tmp/bogus62" 44 | assert is_function(fun) 45 | end 46 | 47 | test "fires given action when touching one of given files" do 48 | # create the watched file 49 | :os.cmd 'touch /tmp/bogus3' 50 | # add the ActionGroup 51 | {:ok, key} = Hk.add %Hk.ActionGroup{paths: ["/tmp/bogus3"], app: :bogus3, action: :stop} 52 | # also accept as a regular map 53 | {:ok, key2} = Hk.add %{paths: ["/tmp/bogus4"], app: :bogus4, action: :stop} 54 | 55 | # now it's looping, but no hits for anyone 56 | for k <- [key,key2] do 57 | TH.wait_for fn -> 58 | %{metadata: md} = H.lookup(k) 59 | md[:loops] > 0 and md[:hits] == 0 60 | end 61 | end 62 | 63 | # touch file 64 | :os.cmd 'touch /tmp/bogus3' 65 | 66 | # now bogus it's been fired once 67 | TH.wait_for fn -> 68 | %{metadata: md} = H.lookup(key) 69 | md[:loops] > 0 and md[:hits] == 1 70 | end 71 | 72 | # not the second bogus 73 | TH.wait_for fn -> 74 | %{metadata: md} = H.lookup(key2) 75 | md[:loops] > 0 and md[:hits] == 0 76 | end 77 | end 78 | 79 | test "creates nonexistent watched paths if asked" do 80 | paths = ["/tmp/bogus51","/tmp/bogus52"] 81 | 82 | # ensure each file does not exist 83 | for p <- paths, do: File.rm(p) 84 | 85 | # add the ActionGroup passing `create_paths` 86 | {:ok, k} = Hk.add %{paths: paths, app: :bogus5, action: :stop}, create_paths: true 87 | 88 | # assert they exist now 89 | for p <- paths, do: assert File.exists?(p) 90 | 91 | # and they work as expected 92 | TH.wait_for fn -> 93 | %{metadata: md} = H.lookup(k) 94 | md[:loops] > 0 and md[:hits] == 0 95 | end 96 | 97 | # touch file 98 | :os.cmd 'touch /tmp/bogus51' 99 | 100 | # now bogus it's been fired once 101 | TH.wait_for fn -> 102 | %{metadata: md} = H.lookup(k) 103 | md[:loops] > 0 and md[:hits] == 1 104 | end 105 | end 106 | 107 | test "does not touch existing paths on start" do 108 | # create file and get initial mtime 109 | path = "/tmp/bogus7" 110 | "touch #{path}" |> to_charlist |> :os.cmd 111 | mtime = path |> H.get_file_mtime 112 | 113 | # get Harakiri look over it passing `create_paths` 114 | {:ok, _} = Hk.add %{paths: [path], app: :bogus7, action: :stop}, create_paths: true 115 | 116 | # check mtime stays the same 117 | mtime2 = path |> H.get_file_mtime 118 | assert mtime == mtime2 119 | end 120 | 121 | test "stop does not crash" do 122 | ag = %{paths: ["/tmp/bogus"], app: :bogus, action: :stop} |> H.digest_data 123 | :ok = Hk.Worker.fire :stop, ag: ag, path: "/tmp/bogus" 124 | end 125 | 126 | test "reload does not crash" do 127 | ag = %{paths: ["/tmp/bogus"], app: :bogus, action: :reload} |> H.digest_data 128 | :ok = Hk.Worker.fire :reload, ag: ag, path: "/tmp/bogus" 129 | end 130 | 131 | test "support for anonymous functions as action" do 132 | Agent.start_link(fn -> :did_not_run end, name: :bogus6) 133 | path = "/tmp/bogus6" 134 | 135 | # function to be run 136 | # makes some assertions and updates the Agent's state 137 | fun = fn(data)-> 138 | assert data[:file][:path] == path 139 | Agent.update(:bogus6, fn(_)-> :did_run end) 140 | end 141 | 142 | {:ok, k} = Hk.add %{paths: [path], action: fun}, create_paths: true 143 | 144 | # start the party 145 | :os.cmd 'touch /tmp/bogus6' 146 | 147 | # now bogus it's been fired once 148 | TH.wait_for fn -> 149 | %{metadata: md} = H.lookup(k) 150 | md[:loops] > 0 and md[:hits] == 1 151 | end 152 | 153 | # it should have updated the Agent 154 | TH.wait_for fn -> Agent.get(:bogus6, &(&1)) == :did_run end 155 | end 156 | 157 | end 158 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule TestHelpers do 4 | 5 | @doc """ 6 | Wait for given function to return true. 7 | Optional `msecs` and `step`. 8 | """ 9 | def wait_for(func, msecs \\ 5_000, step \\ 100) do 10 | if func.() do 11 | :ok 12 | else 13 | if msecs <= 0, do: raise "Timeout!" 14 | :timer.sleep step 15 | wait_for func, msecs - step, step 16 | end 17 | end 18 | 19 | @doc """ 20 | Remove :metadata key from given ag list. Useful to compare data on testing 21 | when metadata may change over the time. 22 | """ 23 | def remove_metadata(list) do 24 | for ag <- list, into: [], do: Map.drop(ag,[:metadata]) 25 | end 26 | 27 | end 28 | --------------------------------------------------------------------------------