Skip to content

Commit 78236ef

Browse files
authored
fix: disable shell sessions when fetching the PATH (#177)
* fix: disable shell sessions when fetching the PATH * fix: update open_elixir types
1 parent 9803293 commit 78236ef

File tree

4 files changed

+113
-62
lines changed

4 files changed

+113
-62
lines changed

apps/expert/lib/expert.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ defmodule Expert do
2929

3030
def get_lsp, do: :persistent_term.get(:expert_lsp, nil)
3131

32+
def terminate(message, status \\ 0) do
33+
Logger.error(message)
34+
System.stop(status)
35+
end
36+
3237
def start_link(args) do
3338
Logger.debug(inspect(args))
3439

apps/expert/lib/expert/engine_node.ex

Lines changed: 65 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,16 @@ defmodule Expert.EngineNode do
4545
| path_append_arguments(paths)
4646
]
4747

48-
port = Expert.Port.open_elixir(state.project, args: args)
49-
50-
%{state | port: port, started_by: from}
48+
case Expert.Port.open_elixir(state.project, args: args) do
49+
{:error, :no_elixir, message} ->
50+
GenLSP.error(Expert.get_lsp(), message)
51+
Expert.terminate("Failed to find an elixir executable, shutting down", 1)
52+
{:error, :no_elixir}
53+
54+
port ->
55+
state = %{state | port: port, started_by: from}
56+
{:ok, state}
57+
end
5158
end
5259

5360
def stop(%__MODULE__{} = state, from, stop_timeout) do
@@ -166,48 +173,54 @@ defmodule Expert.EngineNode do
166173
defp glob_paths(%Project{} = project) do
167174
lsp = Expert.get_lsp()
168175
project_name = Project.name(project)
169-
{:ok, elixir, env} = Expert.Port.elixir_executable(project)
170-
171-
GenLSP.info(lsp, "Found elixir for #{project_name} at #{elixir}")
172-
173-
expert_priv = :code.priv_dir(:expert)
174-
packaged_engine_source = Path.join([expert_priv, "engine_source", "apps", "engine"])
175-
176-
engine_source =
177-
"EXPERT_ENGINE_PATH"
178-
|> System.get_env(packaged_engine_source)
179-
|> Path.expand()
180-
181-
build_engine_script = Path.join(expert_priv, "build_engine.exs")
182-
183-
opts =
184-
[
185-
:stderr_to_stdout,
186-
args: [
187-
elixir,
188-
build_engine_script,
189-
"--source-path",
190-
engine_source,
191-
"--vsn",
192-
Expert.vsn()
193-
],
194-
env: Expert.Port.ensure_charlists(env),
195-
cd: engine_source
196-
]
197-
198-
launcher = Expert.Port.path()
199176

200-
GenLSP.info(lsp, "Finding or building engine for project #{project_name}")
201-
202-
with_progress(project, "Building engine for #{project_name}", fn ->
203-
port =
204-
Port.open(
205-
{:spawn_executable, launcher},
206-
opts
207-
)
208-
209-
wait_for_engine(port)
210-
end)
177+
case Expert.Port.elixir_executable(project) do
178+
{:ok, elixir, env} ->
179+
GenLSP.info(lsp, "Found elixir for #{project_name} at #{elixir}")
180+
181+
expert_priv = :code.priv_dir(:expert)
182+
packaged_engine_source = Path.join([expert_priv, "engine_source", "apps", "engine"])
183+
184+
engine_source =
185+
"EXPERT_ENGINE_PATH"
186+
|> System.get_env(packaged_engine_source)
187+
|> Path.expand()
188+
189+
build_engine_script = Path.join(expert_priv, "build_engine.exs")
190+
191+
opts =
192+
[
193+
:stderr_to_stdout,
194+
args: [
195+
elixir,
196+
build_engine_script,
197+
"--source-path",
198+
engine_source,
199+
"--vsn",
200+
Expert.vsn()
201+
],
202+
env: Expert.Port.ensure_charlists(env),
203+
cd: engine_source
204+
]
205+
206+
launcher = Expert.Port.path()
207+
208+
GenLSP.info(lsp, "Finding or building engine for project #{project_name}")
209+
210+
with_progress(project, "Building engine for #{project_name}", fn ->
211+
port =
212+
Port.open(
213+
{:spawn_executable, launcher},
214+
opts
215+
)
216+
217+
wait_for_engine(port)
218+
end)
219+
220+
{:error, :no_elixir, message} ->
221+
GenLSP.error(Expert.get_lsp(), message)
222+
Expert.terminate("Failed to find an elixir executable, shutting down", 1)
223+
end
211224
end
212225

213226
defp wait_for_engine(port) do
@@ -273,8 +286,14 @@ defmodule Expert.EngineNode do
273286
def handle_call({:start, paths}, from, %State{} = state) do
274287
:ok = :net_kernel.monitor_nodes(true, node_type: :all)
275288
Process.send_after(self(), :maybe_start_timeout, @start_timeout)
276-
state = State.start(state, paths, from)
277-
{:noreply, state}
289+
290+
case State.start(state, paths, from) do
291+
{:ok, state} ->
292+
{:noreply, state}
293+
294+
{:error, :no_elixir} ->
295+
{:reply, {:error, :no_elixir}, state}
296+
end
278297
end
279298

280299
@impl true

apps/expert/lib/expert/port.ex

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule Expert.Port do
55

66
alias Forge.Project
77

8+
require Logger
9+
810
@type open_opt ::
911
{:env, list()}
1012
| {:cd, String.t() | charlist()}
@@ -19,16 +21,16 @@ defmodule Expert.Port do
1921
This function takes the project's context into account and looks for the executable via calling
2022
`elixir_executable(project)`. Environment variables are also retrieved with that call.
2123
"""
22-
@spec open_elixir(Project.t(), open_opts()) :: port()
24+
@spec open_elixir(Project.t(), open_opts()) :: port() | {:error, :no_elixir, String.t()}
2325
def open_elixir(%Project{} = project, opts) do
24-
{:ok, elixir_executable, environment_variables} = elixir_executable(project)
25-
26-
opts =
27-
opts
28-
|> Keyword.put_new_lazy(:cd, fn -> Project.root_path(project) end)
29-
|> Keyword.put_new(:env, environment_variables)
26+
with {:ok, elixir_executable, environment_variables} <- elixir_executable(project) do
27+
opts =
28+
opts
29+
|> Keyword.put_new_lazy(:cd, fn -> Project.root_path(project) end)
30+
|> Keyword.put_new(:env, environment_variables)
3031

31-
open(project, elixir_executable, opts)
32+
open(project, elixir_executable, opts)
33+
end
3234
end
3335

3436
def elixir_executable(%Project{} = project) do
@@ -39,12 +41,8 @@ defmodule Expert.Port do
3941

4042
case :os.find_executable(~c"elixir", to_charlist(path)) do
4143
false ->
42-
GenLSP.error(
43-
Expert.get_lsp(),
44-
"Couldn't find an elixir executable for project at #{root_path}. Using shell at #{shell} with PATH=#{path}"
45-
)
46-
47-
{:error, :no_elixir}
44+
{:error, :no_elixir,
45+
"Couldn't find an elixir executable for project at #{root_path}. Using shell at #{shell} with PATH=#{path}"}
4846

4947
elixir ->
5048
env =
@@ -64,6 +62,8 @@ defmodule Expert.Port do
6462
# or we get an incomplete PATH not including erl or any other version manager
6563
# managed programs.
6664

65+
env = [{"SHELL_SESSIONS_DISABLE", "1"}]
66+
6767
case Path.basename(shell) do
6868
# Ideally, it should contain the path to shell (e.g. `/usr/bin/fish`),
6969
# but it might contain only the name of the shell (e.g. `fish`).
@@ -72,12 +72,16 @@ defmodule Expert.Port do
7272
# to join the entries with colons and have a standard colon-separated PATH output
7373
# as in bash, which is expected by `:os.find_executable/2`.
7474
{path, 0} =
75-
System.cmd(shell, ["-i", "-l", "-c", "cd #{directory} && string join ':' $PATH"])
75+
System.cmd(shell, ["-i", "-l", "-c", "cd #{directory} && string join ':' $PATH"],
76+
env: env
77+
)
7678

7779
path
7880

7981
_ ->
80-
{path, 0} = System.cmd(shell, ["-i", "-l", "-c", "cd #{directory} && echo $PATH"])
82+
{path, 0} =
83+
System.cmd(shell, ["-i", "-l", "-c", "cd #{directory} && echo $PATH"], env: env)
84+
8185
path
8286
end
8387
end

apps/expert/test/expert/engine_node_test.exs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Expert.EngineNodeTest do
66
import Forge.Test.Fixtures
77

88
use ExUnit.Case, async: false
9+
use Patch
910

1011
setup do
1112
project = project()
@@ -40,4 +41,26 @@ defmodule Expert.EngineNodeTest do
4041
Process.exit(linked_node_process, :kill)
4142
assert_eventually Process.whereis(node_process_name) == nil, 50
4243
end
44+
45+
test "terminates the server if no elixir is found", %{project: project} do
46+
test_pid = self()
47+
48+
patch(Expert.Port, :path_env_at_directory, nil)
49+
50+
patch(Expert, :terminate, fn _, status ->
51+
send(test_pid, {:stopped, status})
52+
end)
53+
54+
# Note(dorgan): ideally we would use GenLSP.Test here, but
55+
# calling `server(Expert)` causes the tests to behave erratically
56+
# and either not run or terminate ExUnit early
57+
patch(GenLSP, :error, fn _, message ->
58+
send(test_pid, {:lsp_log, message})
59+
end)
60+
61+
{:error, :no_elixir} = EngineNode.start(project)
62+
63+
assert_receive {:stopped, 1}
64+
assert_receive {:lsp_log, "Couldn't find an elixir executable for project" <> _}
65+
end
4366
end

0 commit comments

Comments
 (0)