Make a link work when it is pointed at a machine's id

6478ec8 · Gabriel Morell · 2026-09-17 01:59

21 files +172 -2
Message
{commit_body(@commit)}

Files changed

modified android/app/src/main/java/org/neiam/lambent/app/ui/LinksScreen.kt
+1 −1
@@ -199,7 +199,7 @@ private fun CreateLinkDialog(
199 199 onClick = {
200 200 val s = source ?: return@TextButton
201 201 val t = target ?: return@TextButton
202 − onCreate(name, s.id, t.mac, if (channels > 1) chan else "all")
202 + onCreate(name, s.name, t.mac, if (channels > 1) chan else "all")
203 203 },
204 204 ) { Text("Create") }
205 205 },
modified lib/lambent_ex/link.ex
+12 −0
@@ -44,6 +44,11 @@ defmodule LambentEx.Link do
44 44 fade_steps: @fade_steps
45 45 ])
46 46
47 + # a machine publishes under its name, but it is listed by id, and a client
48 + # working from that listing can hand back either. a link pointed at an id
49 + # would sit there looking healthy and never hear a frame.
50 + opts = Keyword.put(opts, :source, source(opts[:source]))
51 +
47 52 Phoenix.PubSub.subscribe(@pubsub_name, @pubsub_topic <> opts[:source])
48 53 Phoenix.PubSub.subscribe(@pubsub_name, @pubsub_pfx_on <> opts[:target])
49 54
@@ -99,6 +104,13 @@ defmodule LambentEx.Link do
99 104
100 105 defp via_tuple(name), do: {:via, Registry, {@registry, name}}
101 106
107 + defp source(source) do
108 + case LambentEx.Machine.running?(source) do
109 + true -> source
110 + false -> LambentEx.Machine.name_for_id(source) || source
111 + end
112 + end
113 +
102 114 # public
103 115
104 116 def toggle(id) do
modified lib/lambent_ex/machine.ex
+47 −1
@@ -113,7 +113,19 @@ defmodule LambentEx.Machine do
113 113 fullkeys: [],
114 114 # load opts
115 115 full_opts: opts
116 − }}
116 + }
117 + |> register_id()}
118 + end
119 +
120 + # the name is what a machine publishes on and what a link subscribes to; the
121 + # id is what it is listed by. both lead here, so a link built from a listing
122 + # lands on the right machine either way.
123 + defp register_id(state) do
124 + Registry.register(@registry, id_key(id(state)), state[:name])
125 + state
126 + rescue
127 + # nothing else can be carrying this id: the name in it is already unique
128 + ArgumentError -> state
117 129 end
118 130
119 131 defp via_tuple(name), do: {:via, Registry, {@registry, name}}
@@ -157,6 +169,35 @@ defmodule LambentEx.Machine do
157 169 via_tuple("machine-#{id}") |> GenServer.cast(:quit)
158 170 end
159 171
172 + @doc "What a machine publishes about itself, or an empty map if it is gone."
173 + def info(name) do
174 + via_tuple("machine-#{name}") |> GenServer.call(:info)
175 + catch
176 + :exit, _reason -> %{}
177 + end
178 +
179 + @doc """
180 + The name of the machine carrying this id.
181 +
182 + A machine is addressed by name -- that is what it publishes on -- but it is
183 + listed by id, and a client holding the listing can end up handing back the
184 + wrong one of the two. A machine puts its id in the registry alongside its
185 + name for this, rather than every caller asking all of them in turn.
186 + """
187 + def name_for_id(id) do
188 + case Registry.lookup(@registry, id_key(id)) do
189 + [{_pid, name}] -> name
190 + [] -> nil
191 + end
192 + end
193 +
194 + defp id_key(id), do: "machine-id-#{id}"
195 +
196 + @doc "Whether a machine is running under this name."
197 + def running?(name) do
198 + Registry.lookup(@registry, "machine-#{name}") != []
199 + end
200 +
160 201 # impls
161 202
162 203 defp bmath({key, i}, state) do
@@ -489,6 +530,11 @@ defmodule LambentEx.Machine do
489 530 {:noreply, state}
490 531 end
491 532
533 + @impl true
534 + def handle_call(:info, _from, state) do
535 + {:reply, publish(state), state}
536 + end
537 +
492 538 def handle_call(_msg, _from, state) do
493 539 {:reply, :ok, state}
494 540 end
added test/lambent_ex/link_source_test.exs
+78 −0
@@ -0,0 +1,78 @@
1 +defmodule LambentEx.LinkSourceTest do
2 + use ExUnit.Case, async: false
3 +
4 + alias LambentEx.Machine
5 +
6 + setup do
7 + {:ok, pid} =
8 + Machine.start_link(nil,
9 + name: "source-test",
10 + step: LambentEx.Machine.Steps.Solid,
11 + step_opts: %{h: 84},
12 + count: 1,
13 + single: true
14 + )
15 +
16 + on_exit(fn -> if Process.alive?(pid), do: Process.exit(pid, :kill) end)
17 +
18 + # the machine is listed by id and addressed by name
19 + %{machine: pid, id: Machine.info("source-test")[:id]}
20 + end
21 +
22 + test "a machine knows its own id", ctx do
23 + assert ctx.id == "Solidxsource-test"
24 + assert Machine.name_for_id(ctx.id) == "source-test"
25 + assert Machine.running?("source-test")
26 + refute Machine.running?(ctx.id)
27 + end
28 +
29 + test "a link pointed at a machine's id finds the machine anyway", ctx do
30 + {:ok, pid} =
31 + LambentEx.Link.start_link(nil,
32 + name: "source-test-link",
33 + source: ctx.id,
34 + target: "not-a-real-device",
35 + chan: 0
36 + )
37 +
38 + state = :sys.get_state(pid)
39 +
40 + assert state.source == "source-test"
41 + assert state.full_opts[:source] == "source-test"
42 +
43 + # and it is listening where the machine actually publishes
44 + Phoenix.PubSub.broadcast(LambentEx.PubSub, "machine-source-test", {:publish, [[1, 2, 3]]})
45 + assert :sys.get_state(pid).last == [1, 2, 3]
46 +
47 + LambentEx.LinkSupervisor.abort_child(pid)
48 + end
49 +
50 + test "a link pointed at a name is left alone", ctx do
51 + {:ok, pid} =
52 + LambentEx.Link.start_link(nil,
53 + name: "source-test-link-2",
54 + source: "source-test",
55 + target: "not-a-real-device",
56 + chan: 0
57 + )
58 +
59 + assert :sys.get_state(pid).source == "source-test"
60 + assert ctx.id != "source-test"
61 +
62 + LambentEx.LinkSupervisor.abort_child(pid)
63 + end
64 +
65 + test "a source that names nothing is kept as given" do
66 + {:ok, pid} =
67 + LambentEx.Link.start_link(nil,
68 + name: "source-test-link-3",
69 + source: "nothing-by-this-name",
70 + target: "not-a-real-device",
71 + chan: 0
72 + )
73 +
74 + assert :sys.get_state(pid).source == "nothing-by-this-name"
75 +
76 + LambentEx.LinkSupervisor.abort_child(pid)
77 + end
78 +end
modified test/lambent_ex_web/channels/channels_test.exs
+34 −0
@@ -51,6 +51,40 @@ defmodule LambentExWeb.ChannelsTest do
51 51 end
52 52 end
53 53
54 + describe "links made over the channel" do
55 + test "a link created with a machine's id still finds the machine" do
56 + {:ok, machine} =
57 + LambentEx.Machine.start_link(nil,
58 + name: "channel-source",
59 + step: LambentEx.Machine.Steps.Solid,
60 + step_opts: %{h: 84},
61 + count: 1,
62 + single: true
63 + )
64 +
65 + id = LambentEx.Machine.info("channel-source")[:id]
66 + socket = connect_to("links")
67 +
68 + ref =
69 + push(socket, "create", %{
70 + "link" => %{
71 + "name" => "channel-link",
72 + "source_id" => id,
73 + "target_id" => "aa:bb:cc:dd:ee:99",
74 + "chan" => "all"
75 + }
76 + })
77 +
78 + assert_reply ref, :ok
79 +
80 + assert [{pid, _}] = Registry.lookup(:lambent_links, "link-channel-link")
81 + assert :sys.get_state(pid).source == "channel-source"
82 +
83 + LambentEx.LinkSupervisor.abort_child(pid)
84 + Process.exit(machine, :kill)
85 + end
86 + end
87 +
54 88 describe "re-probing" do
55 89 test "a client can ask every device what it runs" do
56 90 socket = connect_to("devices")

Parents: e7a4389