diff --git a/.gitignore b/.gitignore index 9ccab38..b409e41 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ erl_crash.dump *.beam /tmp/ .elixir_ls/ +.nrepl-port diff --git a/bb.edn b/bb.edn new file mode 100644 index 0000000..5b34a14 --- /dev/null +++ b/bb.edn @@ -0,0 +1,27 @@ +{:tasks + {compile {:doc "Compile all .clje and .ex files" + :task (shell "mix compile")} + + test {:doc "Run all tests" + :task (shell "mix test")} + + test:trace {:doc "Run all tests with trace output" + :task (shell "mix test --trace")} + + repl {:doc "Start interactive CljElixir REPL" + :task (shell "rlwrap mix clje.repl")} + + repl:basic {:doc "Start REPL without rlwrap" + :task (shell "mix clje.repl")} + + nrepl {:doc "Start nREPL server (random port)" + :task (shell "mix clje.nrepl")} + + nrepl:port {:doc "Start nREPL on port 7888" + :task (shell "mix clje.nrepl --port 7888")} + + clean {:doc "Clean build artifacts" + :task (shell "mix clean")} + + fmt {:doc "Format Elixir source files" + :task (shell "mix format")}}} diff --git a/lib/mix/tasks/clje.nrepl.ex b/lib/mix/tasks/clje.nrepl.ex index c98ca76..4f404af 100644 --- a/lib/mix/tasks/clje.nrepl.ex +++ b/lib/mix/tasks/clje.nrepl.ex @@ -8,16 +8,36 @@ defmodule Mix.Tasks.Clje.Nrepl do 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]) - port = Keyword.get(opts, :port, 0) + 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") @@ -25,12 +45,43 @@ defmodule Mix.Tasks.Clje.Nrepl do {:ok, server} = CljElixir.NRepl.Server.start_link(port: port) actual_port = CljElixir.NRepl.Server.port(server) - File.write!(".nrepl-port", Integer.to_string(actual_port)) + 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") + 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