Packages
mob_dev
0.6.13
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/mob.watch.ex
defmodule Mix.Tasks.Mob.Watch do
use Mix.Task
@shortdoc "Watch for source changes and auto hot-push to running mob devices"
@moduledoc """
Watches `lib/` for source changes and automatically compiles + hot-pushes
updated modules to all running Android and iOS devices.
Apps must already be running. Modules are loaded in place — no restart.
Only modules that actually changed are pushed each cycle.
Press Ctrl-C to stop.
Options:
--cookie Erlang cookie (default: mob_secret)
--debounce ms to wait after a change before compiling (default: 300)
--interval ms between file-change polls (default: 500)
Examples:
mix mob.watch
mix mob.watch --debounce 500
mix mob.watch --cookie my_cookie
## Under the hood
`mix mob.watch` is an mtime-polling file watcher that drives `mix compile` and
`nl/1` in a loop:
# On startup:
writes OS PID → _build/mob_watch.pid # used by mix mob.watch_stop
# Each cycle (every --interval ms):
snapshot mtimes of lib/**/*.ex
if any changed:
:timer.sleep(debounce_ms) # wait for format-on-save to settle
System.cmd("mix", ["compile"]) # compile in a subprocess
for each changed BEAM, on each node:
:rpc.call(node, :code, :load_binary, [...])
Compile runs in a subprocess (not `Mix.Task.run("compile")`) so that Mix's
task cache doesn't prevent recompilation on subsequent file saves.
The watch loop is equivalent to running `mix compile && nl(ChangedModule)` in
a terminal after every save — `mix mob.watch` just does it automatically.
"""
@pid_file "_build/mob_watch.pid"
@spec pid_file() :: String.t()
def pid_file, do: @pid_file
@impl Mix.Task
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
switches: [cookie: :string, debounce: :integer, interval: :integer],
aliases: [c: :cookie]
)
cookie = opts |> Keyword.get(:cookie, "mob_secret") |> String.to_atom()
debounce = Keyword.get(opts, :debounce, 300)
interval = Keyword.get(opts, :interval, 500)
File.mkdir_p!("_build")
File.write!(@pid_file, to_string(:os.getpid()))
IO.puts("")
IO.puts(
"#{IO.ANSI.cyan()}mob.watch#{IO.ANSI.reset()} — watching lib/ for changes (Ctrl-C to stop)\n"
)
nodes = connect_with_retry(cookie)
# Initial compile + push everything so device is in sync.
recompile()
{pushed, _} = MobDev.HotPush.push_all(nodes)
if pushed > 0 do
IO.puts(" #{IO.ANSI.green()}✓ initial push: #{pushed} module(s)#{IO.ANSI.reset()}")
end
# Snapshot source mtimes.
sources = MobDev.SourceWatch.snapshot()
IO.puts(" Watching #{map_size(sources)} source file(s)...\n")
watch_loop(sources, nodes, cookie, debounce, interval)
end
# ── Loop ────────────────────────────────────────────────────────────────────
defp watch_loop(sources, nodes, cookie, debounce, interval) do
:timer.sleep(interval)
current = MobDev.SourceWatch.snapshot()
changed_files = MobDev.SourceWatch.diff(sources, current)
if changed_files == [] do
watch_loop(current, nodes, cookie, debounce, interval)
else
IO.puts("#{IO.ANSI.cyan()}◉ #{length(changed_files)} file(s) changed#{IO.ANSI.reset()}")
Enum.each(changed_files, fn f ->
IO.puts(" #{Path.relative_to_cwd(f)}")
end)
# Debounce — wait in case more saves are incoming (e.g. format-on-save).
:timer.sleep(debounce)
current2 = MobDev.SourceWatch.snapshot()
# Re-connect if any nodes dropped (device rebooted, app restarted, etc.)
live_nodes = reconnect_if_needed(nodes, cookie)
snapshot = MobDev.HotPush.snapshot_beams()
recompile()
{pushed, failed} = MobDev.HotPush.push_changed(live_nodes, snapshot)
cond do
pushed > 0 ->
node_str = Enum.map_join(live_nodes, " ", &short_node/1)
IO.puts(" #{IO.ANSI.green()}✓ #{pushed} module(s) → #{node_str}#{IO.ANSI.reset()}")
failed != [] ->
Enum.each(failed, fn {mod, reason} ->
IO.puts(" #{IO.ANSI.red()}✗ #{mod}: #{inspect(reason)}#{IO.ANSI.reset()}")
end)
true ->
IO.puts(
" #{IO.ANSI.yellow()}(compile ran but no new BEAMs — syntax error?)#{IO.ANSI.reset()}"
)
end
IO.puts("")
watch_loop(current2, live_nodes, cookie, debounce, interval)
end
end
# ── Helpers ─────────────────────────────────────────────────────────────────
defp connect_with_retry(cookie) do
IO.write("Connecting to devices...")
nodes = MobDev.HotPush.connect(cookie: cookie)
if nodes == [] do
IO.puts(" #{IO.ANSI.yellow()}none found#{IO.ANSI.reset()}")
IO.puts(" Start apps first: mix mob.connect")
IO.puts(" Watching anyway — will connect when nodes come up.\n")
else
IO.puts(" #{IO.ANSI.green()}#{length(nodes)} node(s)#{IO.ANSI.reset()}")
Enum.each(nodes, fn n -> IO.puts(" #{IO.ANSI.green()}✓#{IO.ANSI.reset()} #{n}") end)
IO.puts("")
end
nodes
end
defp reconnect_if_needed(nodes, cookie) do
alive = Enum.filter(nodes, &(Node.connect(&1) == true))
new_nodes = MobDev.HotPush.connect(cookie: cookie)
# Union: keep existing alive nodes + any newly discovered ones
Enum.uniq(alive ++ new_nodes)
end
# Lines from mix compile subprocess we don't want to echo.
@noise_prefixes [
"warning! Erlang/OTP",
"Regexes will be re-compiled",
"This can be fixed by using"
]
defp recompile do
# Run in a subprocess — Mix task caches are process-local and can't be
# fully cleared with reenable/1 when inside another running mix task.
mix = System.find_executable("mix") || "mix"
{output, _} = System.cmd(mix, ["compile"], cd: File.cwd!(), stderr_to_stdout: true)
output
|> String.split("\n", trim: true)
|> Enum.reject(fn line -> Enum.any?(@noise_prefixes, &String.starts_with?(line, &1)) end)
|> Enum.each(&IO.puts/1)
end
defp short_node(node) do
node |> to_string() |> String.split("@") |> hd()
end
end