Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ node_modules/
/priv/static/dev/

devtools_*

screenshots/
15 changes: 9 additions & 6 deletions devtools/chrome/devtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,24 @@ chrome.devtools.panels.create(
panelWindow = window;
isShown = true;
try {
window.set_iframe_url(await getLiveDebuggerSessionURL(chrome));
window.setIframeUrl(await getLiveDebuggerSessionURL(chrome));
} catch (error) {
window.set_iframe_url(null);
window.setIframeUrl(null);
}
}
});

chrome.webNavigation.onCompleted.addListener(async (details) => {
if (details.tabId === chrome.devtools.inspectedWindow.tabId) {
if (
details.tabId === chrome.devtools.inspectedWindow.tabId &&
allowRedirects(chrome)
) {
try {
panelWindow.set_iframe_url(await getLiveDebuggerSessionURL(chrome));
panelWindow.setIframeUrl(await getLiveDebuggerSessionURL(chrome));
} catch (error) {
panelWindow.set_iframe_url(null);
panelWindow.setIframeUrl(null);
}
}
});
},
}
);
19 changes: 11 additions & 8 deletions devtools/common/devtools.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<script src="url.js"></script>
<script src="devtools.js"></script>
</body>

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>

<body>
<script src="url.js"></script>
<script src="devtools.js"></script>
</body>

</html>
2 changes: 1 addition & 1 deletion devtools/common/panel.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const iframe = document.getElementById("content");
const errorInfo = document.getElementById("error-info");

function set_iframe_url(url) {
function setIframeUrl(url) {
if (url) {
iframe.src = url;
iframe.hidden = false;
Expand Down
25 changes: 25 additions & 0 deletions devtools/common/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,28 @@ function getLiveDebuggerSessionURL(browserElement) {
);
});
}

function allowRedirects() {
return new Promise((resolve, reject) => {
const script = `
(function() {
const metaTag = document.querySelector('meta[name="live-debugger-config"]');
if (metaTag) {
return metaTag.getAttribute('devtools-allow-redirects') === 'true';
}
return false;
})();
`;

browserElement.devtools.inspectedWindow.eval(
script,
(result, isException) => {
if (isException) {
reject(new Error("Error checking allow redirects"));
} else {
resolve(result);
}
}
);
});
}
15 changes: 9 additions & 6 deletions devtools/firefox/devtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,24 @@ browser.devtools.panels.create(
panelWindow = window;
isShown = true;
try {
window.set_iframe_url(await getLiveDebuggerSessionURL(browser));
window.setIframeUrl(await getLiveDebuggerSessionURL(browser));
} catch (error) {
window.set_iframe_url(null);
window.setIframeUrl(null);
}
}
});

browser.webNavigation.onCompleted.addListener(async (details) => {
if (details.tabId === chrome.devtools.inspectedWindow.tabId) {
if (
details.tabId === chrome.devtools.inspectedWindow.tabId &&
allowRedirects(browser)
) {
try {
panelWindow.set_iframe_url(await getLiveDebuggerSessionURL(browser));
panelWindow.setIframeUrl(await getLiveDebuggerSessionURL(browser));
} catch (error) {
panelWindow.set_iframe_url(null);
panelWindow.setIframeUrl(null);
}
}
});
},
}
);
4 changes: 3 additions & 1 deletion lib/live_debugger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ defmodule LiveDebugger do
debug_button? = Keyword.get(config, :debug_button?, true)
highlighting? = Keyword.get(config, :highlighting?, true)
version = Application.spec(:live_debugger)[:vsn] |> to_string()
devtools_allow_redirects = Keyword.get(config, :devtools_allow_redirects, false)

live_debugger_url = "http://#{ip_string}:#{port}"
live_debugger_assets_url = "http://#{ip_string}:#{port}/#{@assets_path}"
Expand All @@ -96,7 +97,8 @@ defmodule LiveDebugger do
browser_features?: browser_features?,
debug_button?: debug_button?,
highlighting?: highlighting?,
version: version
version: version,
devtools_allow_redirects: devtools_allow_redirects
}

tags = LiveDebuggerWeb.Components.Config.live_debugger_tags(assigns)
Expand Down
4 changes: 4 additions & 0 deletions lib/live_debugger/feature.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ defmodule LiveDebugger.Feature do
Application.get_env(:live_debugger, :highlighting?, true)
end

def enabled?(:dead_view_mode) do
Application.get_env(:live_debugger, :dead_view_mode?, true)
end

def enabled?(feature_name) do
raise "Feature #{feature_name} is not allowed"
end
Expand Down
148 changes: 114 additions & 34 deletions lib/live_debugger/gen_servers/ets_table_server.ex
Original file line number Diff line number Diff line change
@@ -1,33 +1,58 @@
defmodule LiveDebugger.GenServers.EtsTableServer do
@moduledoc """
This gen_server is responsible for managing ETS tables.

It sends `{:process_status, {:dead, pid}}` to the process status topic.

## Dead View Mode
When in dead view mode, the gen_server will send `{:process_status, {:dead, pid}}` to the process status topic when a process dies.
It will wait for all watchers to be removed and then delete the ETS table and sends `{:process_status, {:dead, pid}}` to the process status topic.
"""

defmodule TableInfo do
@moduledoc """
- `table`: ETS table reference.
- `alive?`: Indicates if the process is alive.
- `watchers`: Set of pids that are watching this table.
"""
defstruct [:table, alive?: true, watchers: MapSet.new()]

@type t() :: %__MODULE__{
alive?: boolean(),
table: :ets.table(),
watchers: MapSet.t()
}
end

use GenServer

alias __MODULE__.TableInfo
alias LiveDebugger.Utils.PubSub, as: PubSubUtils

@ets_table_name :lvdbg_traces
@type state() :: %{pid() => TableInfo.t()}

@type table_refs() :: %{pid() => :ets.table()}
@ets_table_name :lvdbg_traces

## API

@callback table!(pid :: pid()) :: :ets.table()
@callback delete_table!(pid :: pid()) :: :ok
@callback table(pid :: pid()) :: :ets.table()
@callback watch(pid :: pid()) :: :ok | {:error, term()}

@doc """
Returns ETS table reference.
It creates table if none is associated with given pid
"""
@spec table!(pid :: pid()) :: :ets.table()
def table!(pid) when is_pid(pid), do: impl().table!(pid)
@spec table(pid :: pid()) :: :ets.table()
def table(pid) when is_pid(pid), do: impl().table(pid)

@doc """
If table for given `pid` exists it deletes it from ETS.
Adds watcher to indicate when to delete table from ETS.
It uses pid of process which the function was called.
"""
@spec delete_table!(pid :: pid()) :: :ok
def delete_table!(pid) when is_pid(pid), do: impl().delete_table!(pid)
@spec watch(pid :: pid()) :: :ok | {:error, term()}
def watch(pid) when is_pid(pid) do
impl().watch(pid)
end

## GenServer

Expand All @@ -41,33 +66,63 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
{:ok, %{}}
end

# This is for debugged processes monitored
@impl true
def handle_info({:DOWN, _, :process, closed_pid, _}, state)
when is_map_key(state, closed_pid) do
if LiveDebugger.Feature.enabled?(:dead_view_mode) do
PubSubUtils.process_status_topic()
|> PubSubUtils.broadcast({:process_status, {:died, closed_pid}})
end

state =
state
|> Map.update!(closed_pid, fn table_info -> %{table_info | alive?: false} end)
|> maybe_delete_ets_table(closed_pid)

{:noreply, state}
end

# This is for watchers processes
@impl true
def handle_info({:DOWN, _, :process, closed_pid, _}, table_refs) do
{_, table_refs} = delete_ets_table(closed_pid, table_refs)
def handle_info({:DOWN, _, :process, closed_pid, _}, state) do
{updated_state, touched_pids} = remove_watcher(state, closed_pid)

PubSubUtils.process_status_topic()
|> PubSubUtils.broadcast({:process_status, {:dead, closed_pid}})
updated_state =
touched_pids
|> Enum.reduce(updated_state, fn pid, state_acc ->
maybe_delete_ets_table(state_acc, pid)
end)

{:noreply, table_refs}
{:noreply, updated_state}
end

@impl true
def handle_call({:get_or_create_table, pid}, _from, table_refs) do
case Map.get(table_refs, pid) do
def handle_call({:get_or_create_table, pid}, _from, state) do
case Map.get(state, pid) do
nil ->
ref = create_ets_table()
Process.monitor(pid)
{:reply, ref, Map.put(table_refs, pid, ref)}
{:reply, ref, Map.put(state, pid, %TableInfo{table: ref})}

ref ->
{:reply, ref, table_refs}
%TableInfo{table: ref} ->
{:reply, ref, state}
end
end

@impl true
def handle_call({:delete_table, pid}, _from, table_refs) do
{_, table_refs} = delete_ets_table(pid, table_refs)
{:reply, :ok, table_refs}
def handle_call({:watch, pid}, {watcher, _}, state) do
case Map.get(state, pid) do
%TableInfo{} ->
updated_state = update_watchers(state, pid, &MapSet.put(&1, watcher))

Process.monitor(watcher)

{:reply, :ok, updated_state}

_ ->
{:reply, {:error, :process_not_found}, state}
end
end

defp impl() do
Expand All @@ -80,13 +135,17 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
@server_module LiveDebugger.GenServers.EtsTableServer

@impl true
def table!(pid) do
def table(pid) do
GenServer.call(@server_module, {:get_or_create_table, pid}, 1000)
end

@impl true
def delete_table!(pid) do
GenServer.call(@server_module, {:delete_table, pid}, 1000)
def watch(pid) do
if LiveDebugger.Feature.enabled?(:dead_view_mode) do
GenServer.call(@server_module, {:watch, pid}, 1000)
else
{:error, :not_in_dead_view_mode}
end
end
end

Expand All @@ -95,15 +154,36 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
:ets.new(@ets_table_name, [:ordered_set, :public])
end

@spec delete_ets_table(pid(), table_refs()) :: {boolean(), table_refs()}
defp delete_ets_table(pid, table_refs) do
case Map.pop(table_refs, pid) do
{nil, table_refs} ->
{false, table_refs}

{ref, updated_table_refs} ->
:ets.delete(ref)
{true, updated_table_refs}
@spec maybe_delete_ets_table(state(), pid()) :: state()
defp maybe_delete_ets_table(state, pid) do
with {%TableInfo{alive?: false} = table_info, updated_state} <- Map.pop(state, pid),
true <- Enum.empty?(table_info.watchers) do
PubSubUtils.process_status_topic()
|> PubSubUtils.broadcast({:process_status, {:dead, pid}})

:ets.delete(table_info.table)
updated_state
else
_ ->
state
end
end

@spec remove_watcher(state(), pid()) :: {updated_state :: state(), touched_pids :: [pid()]}
defp remove_watcher(state, watcher) when is_pid(watcher) do
Enum.reduce(state, {state, []}, fn {pid, %{watchers: watchers}}, {state_acc, pids} ->
if Enum.member?(watchers, watcher) do
updated_state = update_watchers(state_acc, pid, &MapSet.delete(&1, watcher))

{updated_state, [pid | pids]}
else
{state_acc, pids}
end
end)
end

defp update_watchers(state, pid, update_fn) when is_map_key(state, pid) do
table_info = Map.get(state, pid)
Map.put(state, pid, %{table_info | watchers: update_fn.(table_info.watchers)})
end
end
Loading