defmodule Mix.Tasks.Clje.Nrepl do @moduledoc """ Starts an nREPL server for CljElixir. ## Usage mix clje.nrepl # random port mix clje.nrepl --port 7888 # specific port Writes port to `.nrepl-port` file for editor auto-discovery. Cleans up `.nrepl-port` on shutdown. If `.nrepl-port` exists and a server is already listening on that port, prints the existing port and exits rather than starting a duplicate. """ use Mix.Task @shortdoc "Start a CljElixir nREPL server" @nrepl_port_file ".nrepl-port" @impl Mix.Task def run(args) do {opts, _, _} = OptionParser.parse(args, strict: [port: :integer]) requested_port = Keyword.get(opts, :port) # Check for existing server via .nrepl-port if requested_port == nil and File.exists?(@nrepl_port_file) do case check_existing_server() do {:ok, existing_port} -> IO.puts("nREPL server already running on port #{existing_port}") IO.puts("Connect with: nrepl://127.0.0.1:#{existing_port}") System.halt(0) :stale -> File.rm(@nrepl_port_file) end end port = requested_port || 0 Mix.Task.run("compile") Mix.Task.run("app.start") {:ok, server} = CljElixir.NRepl.Server.start_link(port: port) actual_port = CljElixir.NRepl.Server.port(server) File.write!(@nrepl_port_file, Integer.to_string(actual_port)) # Clean up .nrepl-port on shutdown cleanup_on_exit() IO.puts("nREPL server started on port #{actual_port} on host 127.0.0.1") IO.puts("Port written to #{@nrepl_port_file}") IO.puts("Press Ctrl+C to stop\n") Process.sleep(:infinity) end defp check_existing_server do with {:ok, content} <- File.read(@nrepl_port_file), {port, ""} <- Integer.parse(String.trim(content)), true <- port_alive?(port) do {:ok, port} else _ -> :stale end end defp port_alive?(port) do case :gen_tcp.connect(~c"127.0.0.1", port, [:binary], 500) do {:ok, socket} -> :gen_tcp.close(socket) true {:error, _} -> false end end defp cleanup_on_exit do # Trap exits so we can clean up the port file System.at_exit(fn _status -> File.rm(@nrepl_port_file) end) end end