Debugging a single .exs without mix.exs

I’m trying to run a script with Mix.install inside, and set some breakpoints. I’m running VSCode with the ElixirLS extension. I can run the script just fine if I mix run --no-mix-exs file.exs but not with the debugger, even If I add the flag to taskArgs. Haven’t found this functionality doccumented in the extension docs so either I’m doing something wrong or it’s not implemented?

I don’t have a direct solution but figured I’d respond since you haven’t gotten any bites yet.

By debugger do you mean dbg? If so then I’m not sure this is possible since in order for it to be available, you need to add :runtime_tools to :extra_applications in a mix file. I’m sorry I don’t know the answer but maybe that’s a good enough clue?

…and welcome to Elixir Forum! :slight_smile:

1 Like

By debugger do you mean dbg

No @sodapopcan, OP means ElixirLS debug adapter (GitHub - elixir-lsp/vscode-elixir-ls: Elixir language support and debugger for VS Code, powered by ElixirLS.)

Haven’t found this functionality doccumented in the extension docs so either I’m doing something wrong or it’s not implemented?

@kidp330 ElixirLS debug adapter currently requires a mix project (I opened Debug adapter does not support no mixfile projects · Issue #1037 · elixir-lsp/elixir-ls · GitHub for tracking). Trying to run in a folder that does not contain mix.exs will raise Mix.NoMixProjectError. You can create a dummy project and run your scripts from there.

However this is tricky in case of .exs files.

  1. OTP debugger which ElixirLS uses underneath requires modules to be interpreted before you can set breakpoints in them. So you need to put your code in module functions
  2. .exs files are compiled in memory. To interpret them you need to include them in requireFiles so they are purged, recompiled and their beam files saved to disk so the OTP debugger can interpret them
  3. .exs script starts executing when you compile it not giving the debugger required time to interpret and set breakpoints. It’s not possible to purge a module when a process executes it’s code. The reasult is that interpretation happens when the script has already returned. You can overcome this with invoking your code in a separate process started from the script
  4. mix run exits when the script return. By default debugger will end the session when mix task finishes. You need to set exitAfterTaskReturns to false so the debug session will continue

I was able to create a proof of concept. The debugger breaks when the breakpoint in Abc.debug_me is hit from the task I start. I needed to add some delay though. Otherwise the code finished before the breakpoint was set.

poc.exs

defmodule Abc do
  def debug_me() do
    a = [1, 2, 3]
    b = Enum.map(a, & &1 + 1)
    b
  end
end

a = [1, 2, 3]
b = Enum.map(a, & &1 + 1)
IO.puts("done #{inspect(b)}, #{Abc.debug_me()}")

Task.start(fn ->
  Process.sleep(4000)
  IO.puts("done from task #{inspect(b)}, #{Abc.debug_me()}")
end)

launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "mix_task",
      "name": "mix run",
      "request": "launch",
      "task": "run",
      "taskArgs": [
        "--no-mix-exs",
        "poc.exs"
      ],
      "startApps": false,
      "projectDir": "${workspaceRoot}",
      "exitAfterTaskReturns": false,
      "requireFiles": [
        "poc.exs"
      ]
    }
  ]
}

The other possibility is breaking on Kernel.dbg macro. This is easier to achieve. In this case do not put the script into requireFiles as recompilation and interpreting will deadlock the debugger

3 Likes

ElixirLS Debug adapter will support --no-mix-exs in 0.19 release

3 Likes