From 21123e3a950df112b6ac469b7840aa484ac4cd5c Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Wed, 9 Aug 2023 22:28:54 -0700 Subject: [PATCH] Added remote debug shell Added a simple remote debugging shell which supplants the debug app we were using previously. This one allows us to connect observer as well. I had to update project node to handle this, as it assumed that _any_ nodedown message was coming from the project node. --- .iex.exs | 157 +--------------- .iex.namespaced.exs | 1 + .../lexical/remote_control/project_node.ex | 45 +++-- apps/server/lib/lexical/server/iex/helpers.ex | 167 ++++++++++++++++++ bin/debug_shell.sh | 9 + bin/start_lexical.sh | 1 + mix.exs | 19 -- pages/installation.md | 1 + rel/debug/env.bat.eex | 8 - rel/debug/env.sh.eex | 5 - rel/debug/overlays/remote.sh | 22 --- rel/debug/remote.vm.args.eex | 8 - rel/debug/vm.args.eex | 8 - 13 files changed, 211 insertions(+), 240 deletions(-) create mode 100644 .iex.namespaced.exs create mode 100644 apps/server/lib/lexical/server/iex/helpers.ex create mode 100755 bin/debug_shell.sh delete mode 100644 rel/debug/env.bat.eex delete mode 100644 rel/debug/env.sh.eex delete mode 100755 rel/debug/overlays/remote.sh delete mode 100644 rel/debug/remote.vm.args.eex delete mode 100644 rel/debug/vm.args.eex diff --git a/.iex.exs b/.iex.exs index 825495763..b3bca57e8 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,156 +1 @@ -alias Lexical.RemoteControl -alias Lexical.Document -alias Lexical.Document.Position - -defmodule Helpers do - alias Lexical.Document.Position - alias Lexical.Project - alias Lexical.Protocol.Types.Completion - alias Lexical.Server.CodeIntelligence - - def observer do - :observer.start() - end - - def observer(project) do - project - |> ensure_project() - |> RemoteControl.call(:observer, :start) - end - - def doc(text) do - doc(:lexical, text) - end - - def doc(project, text) do - root_path = - project - |> project() - |> Project.root_path() - - [root_path, "lib", "file.ex"] - |> Path.join() - |> Document.Path.to_uri() - |> Document.new(text, 0) - end - - def pos(line, character) do - Position.new(line, character) - end - - def compile_project(project) do - project - |> ensure_project() - |> RemoteControl.Api.schedule_compile(true) - end - - def compile_file(project, source) when is_binary(source) do - project - |> ensure_project() - |> compile_file(doc(source)) - end - - def compile_file(project, %Document{} = document) do - project - |> ensure_project() - |> RemoteControl.Api.compile_document(document) - end - - def complete(project, source, context \\ nil) - - def complete(project, source, context) when is_binary(source) do - case completion_position(source) do - {:found, line, character} -> - complete(project, doc(source), line, character, context) - - other -> - other - end - end - - def complete(project, %Document{} = source, line, character, context) do - context = - if is_nil(context) do - Completion.Context.new(trigger_kind: nil) - else - context - end - - position = pos(line, character) - - project - |> ensure_project() - |> CodeIntelligence.Completion.complete(source, position, context) - end - - def connect do - manager_name = manager_name() - Node.start(:"r@127.0.0.1") - Node.set_cookie(:lexical) - Node.connect(:"#{manager_name}@127.0.0.1") - end - - def project(project) when is_atom(project) do - project_path = - [File.cwd!(), "..", to_string(project)] - |> Path.join() - |> Path.expand() - - project_uri = "file://#{project_path}" - Lexical.Project.new(project_uri) - end - - def stop_project(project) do - project - |> ensure_project() - |> Lexical.Server.Project.Supervisor.stop() - end - - def start_project(project) do - project - |> ensure_project() - |> Lexical.Server.Project.Supervisor.start() - end - - defp manager_name do - {:ok, names} = :erl_epmd.names() - - names - |> Enum.map(fn {name, _port} -> List.to_string(name) end) - |> Enum.find(&String.starts_with?(&1, "manager")) - end - - defp completion_position(source_string) do - source_string - |> String.split(["\r\n", "\r", "\n"]) - |> Enum.with_index() - |> Enum.reduce_while(:not_found, fn {line, line_number}, _ -> - if String.contains?(line, "|") do - index = - line - |> String.graphemes() - |> Enum.find_index(&(&1 == "|")) - - {:halt, {:found, line_number + 1, index + 1}} - else - {:cont, :not_found} - end - end) - end - - defp ensure_project(%Project{} = project) do - project - end - - defp ensure_project(project) when is_binary(project) do - project - |> String.to_atom() - |> project() - end - - defp ensure_project(project) when is_atom(project) do - project(project) - end -end - -import Helpers +use Lexical.Server.IEx.Helpers diff --git a/.iex.namespaced.exs b/.iex.namespaced.exs new file mode 100644 index 000000000..e04fd5ded --- /dev/null +++ b/.iex.namespaced.exs @@ -0,0 +1 @@ +use LXical.Server.IEx.Helpers diff --git a/apps/remote_control/lib/lexical/remote_control/project_node.ex b/apps/remote_control/lib/lexical/remote_control/project_node.ex index a5ba4314c..73597d466 100644 --- a/apps/remote_control/lib/lexical/remote_control/project_node.ex +++ b/apps/remote_control/lib/lexical/remote_control/project_node.ex @@ -35,7 +35,10 @@ defmodule Lexical.RemoteControl.ProjectNode do erlang_env = Enum.map(environment_variables, fn {key, value} -> - {String.to_charlist(key), String.to_charlist(value)} + # using to_string ensures nil values won't blow things up + erl_key = key |> to_string() |> String.to_charlist() + erl_value = value |> to_string() |> String.to_charlist() + {erl_key, erl_value} end) args = [ @@ -70,12 +73,25 @@ defmodule Lexical.RemoteControl.ProjectNode do %{state | status: :stopped} end - def on_nodeup(%__MODULE__{} = state) do - %{state | status: :started} + def on_nodeup(%__MODULE__{} = state, node_name) do + if node_name == Project.node_name(state.project) do + {pid, _ref} = state.started_by + Process.monitor(pid) + GenServer.reply(state.started_by, :ok) + + %{state | status: :started} + else + state + end end - def on_nodedown(%__MODULE__{} = state) do - %{state | status: :stopped} + def on_nodedown(%__MODULE__{} = state, node_name) do + if node_name == Project.node_name(state.project) do + GenServer.reply(state.stopped_by, :ok) + {:shutdown, %{state | status: :stopped}} + else + :continue + end end def on_monitored_dead(%__MODULE__{} = state) do @@ -168,11 +184,8 @@ defmodule Lexical.RemoteControl.ProjectNode do end @impl true - def handle_info({:nodeup, _node, _}, %State{} = state) do - {pid, _ref} = state.started_by - Process.monitor(pid) - GenServer.reply(state.started_by, :ok) - state = State.on_nodeup(state) + def handle_info({:nodeup, node, _}, %State{} = state) do + state = State.on_nodeup(state, node) {:noreply, state} end @@ -188,10 +201,14 @@ defmodule Lexical.RemoteControl.ProjectNode do end @impl true - def handle_info({:nodedown, _, _}, %State{} = state) do - GenServer.reply(state.stopped_by, :ok) - state = State.on_nodedown(state) - {:stop, :shutdown, state} + def handle_info({:nodedown, node_name, _}, %State{} = state) do + case State.on_nodedown(state, node_name) do + {:shutdown, new_state} -> + {:stop, :shutdown, new_state} + + :continue -> + {:noreply, state} + end end @impl true diff --git a/apps/server/lib/lexical/server/iex/helpers.ex b/apps/server/lib/lexical/server/iex/helpers.ex new file mode 100644 index 000000000..be068b472 --- /dev/null +++ b/apps/server/lib/lexical/server/iex/helpers.ex @@ -0,0 +1,167 @@ +defmodule Lexical.Server.IEx.Helpers do + alias Lexical.Document + alias Lexical.Document.Position + alias Lexical.ProcessCache + alias Lexical.Project + alias Lexical.Protocol.Types.Completion + alias Lexical.RemoteControl + alias Lexical.Server.CodeIntelligence + + defmacro __using__(_) do + quote do + alias Lexical.Document + alias Lexical.Document.Position + alias Lexical.RemoteControl + import unquote(__MODULE__) + end + end + + def observer do + # credo:disable-for-next-line + apply(:observer, :start, []) + end + + def observer(project) do + project + |> ensure_project() + |> RemoteControl.call(:observer, :start) + end + + def doc(text) do + doc(:lexical, text) + end + + def doc(project, text) do + root_path = + project + |> project() + |> Project.root_path() + + [root_path, "lib", "file.ex"] + |> Path.join() + |> Document.Path.to_uri() + |> Document.new(text, 0) + end + + def pos(line, character) do + Position.new(line, character) + end + + def compile_project(project) do + project + |> ensure_project() + |> RemoteControl.Api.schedule_compile(true) + end + + def compile_file(project, source) when is_binary(source) do + project + |> ensure_project() + |> compile_file(doc(source)) + end + + def compile_file(project, %Document{} = document) do + project + |> ensure_project() + |> RemoteControl.Api.compile_document(document) + end + + def complete(project, source, context \\ nil) + + def complete(project, source, context) when is_binary(source) do + case completion_position(source) do + {:found, line, character} -> + complete(project, doc(source), line, character, context) + + other -> + other + end + end + + def complete(project, %Document{} = source, line, character, context) do + context = + if is_nil(context) do + Completion.Context.new(trigger_kind: nil) + else + context + end + + position = pos(line, character) + + project + |> ensure_project() + |> CodeIntelligence.Completion.complete(source, position, context) + end + + def connect do + manager_name = manager_name() + Node.start(:"r@127.0.0.1") + Node.set_cookie(:lexical) + Node.connect(:"#{manager_name}@127.0.0.1") + end + + def project(project) when is_atom(project) do + # We're using a cache here because we need project's + # entropy to be the same after every call. + ProcessCache.trans(project, fn -> + project_path = + [File.cwd!(), "..", to_string(project)] + |> Path.join() + |> Path.expand() + + project_uri = "file://#{project_path}" + Lexical.Project.new(project_uri) + end) + end + + def stop_project(project) do + project + |> ensure_project() + |> Lexical.Server.Project.Supervisor.stop() + end + + def start_project(project) do + project + |> ensure_project() + |> Lexical.Server.Project.Supervisor.start() + end + + defp manager_name do + {:ok, names} = :erl_epmd.names() + + names + |> Enum.map(fn {name, _port} -> List.to_string(name) end) + |> Enum.find(&String.starts_with?(&1, "manager")) + end + + defp completion_position(source_string) do + source_string + |> String.split(["\r\n", "\r", "\n"]) + |> Enum.with_index() + |> Enum.reduce_while(:not_found, fn {line, line_number}, _ -> + if String.contains?(line, "|") do + index = + line + |> String.graphemes() + |> Enum.find_index(&(&1 == "|")) + + {:halt, {:found, line_number + 1, index + 1}} + else + {:cont, :not_found} + end + end) + end + + defp ensure_project(%Project{} = project) do + project + end + + defp ensure_project(project) when is_binary(project) do + project + |> String.to_atom() + |> project() + end + + defp ensure_project(project) when is_atom(project) do + project(project) + end +end diff --git a/bin/debug_shell.sh b/bin/debug_shell.sh new file mode 100755 index 000000000..22586e6b2 --- /dev/null +++ b/bin/debug_shell.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +project_name=$1 +node_name=$(epmd -names | grep manager-$project_name | awk '{print $2}') + +iex --name "shell@127.0.0.1" \ + --remsh "${node_name}" \ + --dot-iex .iex.namespaced.exs \ + --cookie lexical diff --git a/bin/start_lexical.sh b/bin/start_lexical.sh index 9ff920e4c..dee5a07ac 100755 --- a/bin/start_lexical.sh +++ b/bin/start_lexical.sh @@ -41,4 +41,5 @@ export ERL_LIBS="${SCRIPT_DIR}/../lib" -pa "${SCRIPT_DIR}/../config/" \ -pa "${SCRIPT_DIR}/../priv/" \ --eval "LXical.Server.Boot.start" \ + --cookie "lexical" \ --no-halt diff --git a/mix.exs b/mix.exs index d1eeb91df..daeba2891 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,6 @@ defmodule Lexical.LanguageServer.MixProject do version: "0.2.2", start_permanent: Mix.env() == :prod, deps: deps(), - releases: releases(), aliases: aliases(), docs: docs(), name: "Lexical", @@ -55,24 +54,6 @@ defmodule Lexical.LanguageServer.MixProject do ] end - defp releases do - [ - lexical_debug: [ - applications: [ - server: :permanent, - remote_control: :load, - mix: :load - ], - include_executables_for: [:unix], - include_erts: false, - path: "lexical_debug", - cookie: "lexical", - rel_templates_path: "rel/debug", - strip_beams: false - ] - ] - end - defp aliases do [ compile: "compile --docs --debug-info", diff --git a/pages/installation.md b/pages/installation.md index cae8aba6e..23b211c0b 100644 --- a/pages/installation.md +++ b/pages/installation.md @@ -43,6 +43,7 @@ git clone git@github.com:lexical-lsp/lexical.git ``` Then change to the lexical directory + ```shell cd lexical ``` diff --git a/rel/debug/env.bat.eex b/rel/debug/env.bat.eex deleted file mode 100644 index 0d82afd90..000000000 --- a/rel/debug/env.bat.eex +++ /dev/null @@ -1,8 +0,0 @@ -@echo off -rem Set the release to load code on demand (interactive) instead of preloading (embedded). -rem set RELEASE_MODE=interactive - -rem Set the release to work across nodes. -rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". -rem set RELEASE_DISTRIBUTION=name -rem set RELEASE_NODE=<%= @release.name %> diff --git a/rel/debug/env.sh.eex b/rel/debug/env.sh.eex deleted file mode 100644 index 8366409b1..000000000 --- a/rel/debug/env.sh.eex +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -# for connecting locally -export RELEASE_COOKIE="lexical" -export RELEASE_DISTRIBUTION=name diff --git a/rel/debug/overlays/remote.sh b/rel/debug/overlays/remote.sh deleted file mode 100755 index 284fbe047..000000000 --- a/rel/debug/overlays/remote.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh -readlink_f () { - cd "$(dirname "$1")" > /dev/null || exit 1 - filename="$(basename "$1")" - if [ -h "$filename" ]; then - readlink_f "$(readlink "$filename")" - else - echo "$(pwd -P)/$filename" - fi -} - -if [ -z "${ELS_INSTALL_PREFIX}" ]; then - dir="$(dirname "$(readlink_f "$0")")" -else - dir=${ELS_INSTALL_PREFIX} -fi - -project_name=$1 -node_name=$(epmd -names | grep manager-$project_name | awk '{print $2}') - -export RELEASE_NODE="${node_name}@127.0.0.1" -exec "${dir}/bin/lexical_debug" remote diff --git a/rel/debug/remote.vm.args.eex b/rel/debug/remote.vm.args.eex deleted file mode 100644 index 983397a70..000000000 --- a/rel/debug/remote.vm.args.eex +++ /dev/null @@ -1,8 +0,0 @@ -## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html -## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here - -## Increase number of concurrent ports/sockets -##+Q 65536 - -## Tweak GC to run more often -##-env ERL_FULLSWEEP_AFTER 10 diff --git a/rel/debug/vm.args.eex b/rel/debug/vm.args.eex deleted file mode 100644 index 983397a70..000000000 --- a/rel/debug/vm.args.eex +++ /dev/null @@ -1,8 +0,0 @@ -## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html -## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here - -## Increase number of concurrent ports/sockets -##+Q 65536 - -## Tweak GC to run more often -##-env ERL_FULLSWEEP_AFTER 10