Skip to content

Commit 58272b2

Browse files
authored
Task: Add mode for disconnected LiveViews (#412)
* dont remove table when any process is watching * add flag for dead view mode * fix tests * manually compile deploy assets * add config for disabling redirects in devtools
1 parent a888da8 commit 58272b2

26 files changed

Lines changed: 511 additions & 211 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ node_modules/
3434
/priv/static/dev/
3535

3636
devtools_*
37+
38+
screenshots/

devtools/chrome/devtools.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,24 @@ chrome.devtools.panels.create(
1111
panelWindow = window;
1212
isShown = true;
1313
try {
14-
window.set_iframe_url(await getLiveDebuggerSessionURL(chrome));
14+
window.setIframeUrl(await getLiveDebuggerSessionURL(chrome));
1515
} catch (error) {
16-
window.set_iframe_url(null);
16+
window.setIframeUrl(null);
1717
}
1818
}
1919
});
2020

2121
chrome.webNavigation.onCompleted.addListener(async (details) => {
22-
if (details.tabId === chrome.devtools.inspectedWindow.tabId) {
22+
if (
23+
details.tabId === chrome.devtools.inspectedWindow.tabId &&
24+
allowRedirects(chrome)
25+
) {
2326
try {
24-
panelWindow.set_iframe_url(await getLiveDebuggerSessionURL(chrome));
27+
panelWindow.setIframeUrl(await getLiveDebuggerSessionURL(chrome));
2528
} catch (error) {
26-
panelWindow.set_iframe_url(null);
29+
panelWindow.setIframeUrl(null);
2730
}
2831
}
2932
});
30-
},
33+
}
3134
);

devtools/common/devtools.html

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
</head>
7-
<body>
8-
<script src="url.js"></script>
9-
<script src="devtools.js"></script>
10-
</body>
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
</head>
8+
9+
<body>
10+
<script src="url.js"></script>
11+
<script src="devtools.js"></script>
12+
</body>
13+
1114
</html>

devtools/common/panel.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const iframe = document.getElementById("content");
22
const errorInfo = document.getElementById("error-info");
33

4-
function set_iframe_url(url) {
4+
function setIframeUrl(url) {
55
if (url) {
66
iframe.src = url;
77
iframe.hidden = false;

devtools/common/url.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,28 @@ function getLiveDebuggerSessionURL(browserElement) {
7373
);
7474
});
7575
}
76+
77+
function allowRedirects() {
78+
return new Promise((resolve, reject) => {
79+
const script = `
80+
(function() {
81+
const metaTag = document.querySelector('meta[name="live-debugger-config"]');
82+
if (metaTag) {
83+
return metaTag.getAttribute('devtools-allow-redirects') === 'true';
84+
}
85+
return false;
86+
})();
87+
`;
88+
89+
browserElement.devtools.inspectedWindow.eval(
90+
script,
91+
(result, isException) => {
92+
if (isException) {
93+
reject(new Error("Error checking allow redirects"));
94+
} else {
95+
resolve(result);
96+
}
97+
}
98+
);
99+
});
100+
}

devtools/firefox/devtools.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,24 @@ browser.devtools.panels.create(
1111
panelWindow = window;
1212
isShown = true;
1313
try {
14-
window.set_iframe_url(await getLiveDebuggerSessionURL(browser));
14+
window.setIframeUrl(await getLiveDebuggerSessionURL(browser));
1515
} catch (error) {
16-
window.set_iframe_url(null);
16+
window.setIframeUrl(null);
1717
}
1818
}
1919
});
2020

2121
browser.webNavigation.onCompleted.addListener(async (details) => {
22-
if (details.tabId === chrome.devtools.inspectedWindow.tabId) {
22+
if (
23+
details.tabId === chrome.devtools.inspectedWindow.tabId &&
24+
allowRedirects(browser)
25+
) {
2326
try {
24-
panelWindow.set_iframe_url(await getLiveDebuggerSessionURL(browser));
27+
panelWindow.setIframeUrl(await getLiveDebuggerSessionURL(browser));
2528
} catch (error) {
26-
panelWindow.set_iframe_url(null);
29+
panelWindow.setIframeUrl(null);
2730
}
2831
}
2932
});
30-
},
33+
}
3134
);

lib/live_debugger.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ defmodule LiveDebugger do
8686
debug_button? = Keyword.get(config, :debug_button?, true)
8787
highlighting? = Keyword.get(config, :highlighting?, true)
8888
version = Application.spec(:live_debugger)[:vsn] |> to_string()
89+
devtools_allow_redirects = Keyword.get(config, :devtools_allow_redirects, false)
8990

9091
live_debugger_url = "http://#{ip_string}:#{port}"
9192
live_debugger_assets_url = "http://#{ip_string}:#{port}/#{@assets_path}"
@@ -96,7 +97,8 @@ defmodule LiveDebugger do
9697
browser_features?: browser_features?,
9798
debug_button?: debug_button?,
9899
highlighting?: highlighting?,
99-
version: version
100+
version: version,
101+
devtools_allow_redirects: devtools_allow_redirects
100102
}
101103

102104
tags = LiveDebuggerWeb.Components.Config.live_debugger_tags(assigns)

lib/live_debugger/feature.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ defmodule LiveDebugger.Feature do
99
Application.get_env(:live_debugger, :highlighting?, true)
1010
end
1111

12+
def enabled?(:dead_view_mode) do
13+
Application.get_env(:live_debugger, :dead_view_mode?, true)
14+
end
15+
1216
def enabled?(feature_name) do
1317
raise "Feature #{feature_name} is not allowed"
1418
end
Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,58 @@
11
defmodule LiveDebugger.GenServers.EtsTableServer do
22
@moduledoc """
33
This gen_server is responsible for managing ETS tables.
4+
5+
It sends `{:process_status, {:dead, pid}}` to the process status topic.
6+
7+
## Dead View Mode
8+
When in dead view mode, the gen_server will send `{:process_status, {:dead, pid}}` to the process status topic when a process dies.
9+
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.
410
"""
511

12+
defmodule TableInfo do
13+
@moduledoc """
14+
- `table`: ETS table reference.
15+
- `alive?`: Indicates if the process is alive.
16+
- `watchers`: Set of pids that are watching this table.
17+
"""
18+
defstruct [:table, alive?: true, watchers: MapSet.new()]
19+
20+
@type t() :: %__MODULE__{
21+
alive?: boolean(),
22+
table: :ets.table(),
23+
watchers: MapSet.t()
24+
}
25+
end
26+
627
use GenServer
728

29+
alias __MODULE__.TableInfo
830
alias LiveDebugger.Utils.PubSub, as: PubSubUtils
931

10-
@ets_table_name :lvdbg_traces
32+
@type state() :: %{pid() => TableInfo.t()}
1133

12-
@type table_refs() :: %{pid() => :ets.table()}
34+
@ets_table_name :lvdbg_traces
1335

1436
## API
1537

16-
@callback table!(pid :: pid()) :: :ets.table()
17-
@callback delete_table!(pid :: pid()) :: :ok
38+
@callback table(pid :: pid()) :: :ets.table()
39+
@callback watch(pid :: pid()) :: :ok | {:error, term()}
1840

1941
@doc """
2042
Returns ETS table reference.
2143
It creates table if none is associated with given pid
2244
"""
23-
@spec table!(pid :: pid()) :: :ets.table()
24-
def table!(pid) when is_pid(pid), do: impl().table!(pid)
45+
@spec table(pid :: pid()) :: :ets.table()
46+
def table(pid) when is_pid(pid), do: impl().table(pid)
2547

2648
@doc """
27-
If table for given `pid` exists it deletes it from ETS.
49+
Adds watcher to indicate when to delete table from ETS.
50+
It uses pid of process which the function was called.
2851
"""
29-
@spec delete_table!(pid :: pid()) :: :ok
30-
def delete_table!(pid) when is_pid(pid), do: impl().delete_table!(pid)
52+
@spec watch(pid :: pid()) :: :ok | {:error, term()}
53+
def watch(pid) when is_pid(pid) do
54+
impl().watch(pid)
55+
end
3156

3257
## GenServer
3358

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

69+
# This is for debugged processes monitored
70+
@impl true
71+
def handle_info({:DOWN, _, :process, closed_pid, _}, state)
72+
when is_map_key(state, closed_pid) do
73+
if LiveDebugger.Feature.enabled?(:dead_view_mode) do
74+
PubSubUtils.process_status_topic()
75+
|> PubSubUtils.broadcast({:process_status, {:died, closed_pid}})
76+
end
77+
78+
state =
79+
state
80+
|> Map.update!(closed_pid, fn table_info -> %{table_info | alive?: false} end)
81+
|> maybe_delete_ets_table(closed_pid)
82+
83+
{:noreply, state}
84+
end
85+
86+
# This is for watchers processes
4487
@impl true
45-
def handle_info({:DOWN, _, :process, closed_pid, _}, table_refs) do
46-
{_, table_refs} = delete_ets_table(closed_pid, table_refs)
88+
def handle_info({:DOWN, _, :process, closed_pid, _}, state) do
89+
{updated_state, touched_pids} = remove_watcher(state, closed_pid)
4790

48-
PubSubUtils.process_status_topic()
49-
|> PubSubUtils.broadcast({:process_status, {:dead, closed_pid}})
91+
updated_state =
92+
touched_pids
93+
|> Enum.reduce(updated_state, fn pid, state_acc ->
94+
maybe_delete_ets_table(state_acc, pid)
95+
end)
5096

51-
{:noreply, table_refs}
97+
{:noreply, updated_state}
5298
end
5399

54100
@impl true
55-
def handle_call({:get_or_create_table, pid}, _from, table_refs) do
56-
case Map.get(table_refs, pid) do
101+
def handle_call({:get_or_create_table, pid}, _from, state) do
102+
case Map.get(state, pid) do
57103
nil ->
58104
ref = create_ets_table()
59105
Process.monitor(pid)
60-
{:reply, ref, Map.put(table_refs, pid, ref)}
106+
{:reply, ref, Map.put(state, pid, %TableInfo{table: ref})}
61107

62-
ref ->
63-
{:reply, ref, table_refs}
108+
%TableInfo{table: ref} ->
109+
{:reply, ref, state}
64110
end
65111
end
66112

67113
@impl true
68-
def handle_call({:delete_table, pid}, _from, table_refs) do
69-
{_, table_refs} = delete_ets_table(pid, table_refs)
70-
{:reply, :ok, table_refs}
114+
def handle_call({:watch, pid}, {watcher, _}, state) do
115+
case Map.get(state, pid) do
116+
%TableInfo{} ->
117+
updated_state = update_watchers(state, pid, &MapSet.put(&1, watcher))
118+
119+
Process.monitor(watcher)
120+
121+
{:reply, :ok, updated_state}
122+
123+
_ ->
124+
{:reply, {:error, :process_not_found}, state}
125+
end
71126
end
72127

73128
defp impl() do
@@ -80,13 +135,17 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
80135
@server_module LiveDebugger.GenServers.EtsTableServer
81136

82137
@impl true
83-
def table!(pid) do
138+
def table(pid) do
84139
GenServer.call(@server_module, {:get_or_create_table, pid}, 1000)
85140
end
86141

87142
@impl true
88-
def delete_table!(pid) do
89-
GenServer.call(@server_module, {:delete_table, pid}, 1000)
143+
def watch(pid) do
144+
if LiveDebugger.Feature.enabled?(:dead_view_mode) do
145+
GenServer.call(@server_module, {:watch, pid}, 1000)
146+
else
147+
{:error, :not_in_dead_view_mode}
148+
end
90149
end
91150
end
92151

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

98-
@spec delete_ets_table(pid(), table_refs()) :: {boolean(), table_refs()}
99-
defp delete_ets_table(pid, table_refs) do
100-
case Map.pop(table_refs, pid) do
101-
{nil, table_refs} ->
102-
{false, table_refs}
103-
104-
{ref, updated_table_refs} ->
105-
:ets.delete(ref)
106-
{true, updated_table_refs}
157+
@spec maybe_delete_ets_table(state(), pid()) :: state()
158+
defp maybe_delete_ets_table(state, pid) do
159+
with {%TableInfo{alive?: false} = table_info, updated_state} <- Map.pop(state, pid),
160+
true <- Enum.empty?(table_info.watchers) do
161+
PubSubUtils.process_status_topic()
162+
|> PubSubUtils.broadcast({:process_status, {:dead, pid}})
163+
164+
:ets.delete(table_info.table)
165+
updated_state
166+
else
167+
_ ->
168+
state
107169
end
108170
end
171+
172+
@spec remove_watcher(state(), pid()) :: {updated_state :: state(), touched_pids :: [pid()]}
173+
defp remove_watcher(state, watcher) when is_pid(watcher) do
174+
Enum.reduce(state, {state, []}, fn {pid, %{watchers: watchers}}, {state_acc, pids} ->
175+
if Enum.member?(watchers, watcher) do
176+
updated_state = update_watchers(state_acc, pid, &MapSet.delete(&1, watcher))
177+
178+
{updated_state, [pid | pids]}
179+
else
180+
{state_acc, pids}
181+
end
182+
end)
183+
end
184+
185+
defp update_watchers(state, pid, update_fn) when is_map_key(state, pid) do
186+
table_info = Map.get(state, pid)
187+
Map.put(state, pid, %{table_info | watchers: update_fn.(table_info.watchers)})
188+
end
109189
end

0 commit comments

Comments
 (0)