Skip to content

Commit

Permalink
chg: [d4] sender GenServer
Browse files Browse the repository at this point in the history
  • Loading branch information
gallypette committed Apr 13, 2023
1 parent 27fa12d commit c7ad9a0
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 1 deletion.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ end

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ex_d4>.
be found at <https://hexdocs.pm/ex_d4>.

# Test
D4 server with the following registered sensors:
`b5062e1f-674e-45a0-9c30-2557d6e70ef5`
`b5062e1f-674e-45a0-9c30-2557d6e70ef6`
`b5062e1f-674e-45a0-9c30-2557d6e70ef7`
`b5062e1f-674e-45a0-9c30-deaddeaddead`
135 changes: 135 additions & 0 deletions lib/sender.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
defmodule Sender do
@behaviour :gen_statem

require Logger

defstruct [:d4_connection, :socket, callers: []]

## Public API

def start_link(opts) do
d4_connection = Keyword.fetch!(opts, :d4_connection)
:gen_statem.start_link(__MODULE__, d4_connection, [])
end

def send(pid, payload) do
:gen_statem.call(pid, {:payload, payload})
end

def get_status(pid) do
:gen_statem.call(pid, {:status})
end

## :gen_statem callbacks

@impl true
def callback_mode(), do: [:state_functions, :state_enter]

@impl true
def init(d4_connection) do
data = %__MODULE__{d4_connection: d4_connection}
actions = [{:next_event, :internal, :connect}]
{:ok, :disconnected, data, actions}
end

## Disconnected state

def disconnected(:enter, :disconnected, _data), do: :keep_state_and_data

def disconnected(:enter, :connected, data) do
Logger.error("Connection closed")

Enum.each(data.callers, fn {from} ->
:gen_statem.reply(from, {:error, :disconnected})
end)

data = %{data | socket: nil, callers: []}

actions = [{{:timeout, :reconnect}, 5000, nil}]
{:keep_state, data, actions}
end

def disconnected(:internal, :connect, data) do
socket_opts = [:binary, active: true, verify: :verify_none]

case :ssl.connect(data.d4_connection.destination, data.d4_connection.port, socket_opts, 5_000) do
{:ok, socket} ->
# Create an empty d4 packet to check whether a session already exists with the same uuid
{:ok, packet} = Exd4.encapsulate!(data.d4_connection, "")

case :ssl.send(socket, packet) do
:ok ->
# Logger.debug("Connected to d4 server.")
{:next_state, :connected, %{data | socket: socket}}

{:error, reason} ->
Logger.error("D4 server kicked us out #{inspect({:error, reason})}")
:keep_state_and_data
end

{:error, reason} ->
Logger.error(
"Connection to d4 server could not be established #{inspect({:error, reason})}"
)

:keep_state_and_data
end
end

def disconnected({:timeout, :reconnect}, _, data) do
actions = [{:next_event, :internal, :connect}]
{:keep_state, data, actions}
end

def disconnected({:call, from}, {:payload, _payload}, _data) do
actions = [{:reply, from, {:error, :disconnected}}]
{:keep_state_and_data, actions}
end

def disconnected({:call, from}, {:status}, _data) do
actions = [{:reply, from, {:error, :disconnected}}]
{:keep_state_and_data, actions}
end

## Connected state

def connected({:call, from}, {:status}, _data) do
actions = [{:reply, from, {:ok}}]
{:keep_state_and_data, actions}
end

def connected(:enter, :disconnected, _data) do
:keep_state_and_data
end

def connected(:enter, :connected, _data) do
:keep_state_and_data
end

def connected(:info, {:ssl_closed, socket}, %{socket: socket} = data) do
# Logger.debug(":ssl_closed")
{:next_state, :disconnected, data}
end

def connected({:call, from}, {:payload, payload}, data) do
{:ok, packet} = Exd4.encapsulate!(data.d4_connection, payload)

case :ssl.send(data.socket, packet) do
:ok ->
:gen_statem.reply(from, {:ok})
data = %{data | callers: [data.callers | from]}
{:keep_state, data}

{:error, reason} ->
Logger.error("Connection to d4 server lost #{inspect({:error, reason})}")
:gen_statem.reply(from, {:error, reason})
:ok = :ssl.close(data.socket)
{:next_state, :disconnected, data}
end
end

def connected(:info, {:tcp, _socket, _packet}, _ = _data) do
# D4 server should never send us anything back
:keep_state_and_data
end
end
70 changes: 70 additions & 0 deletions test/sender_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
defmodule SenderTest do
use ExUnit.Case, async: false

require Logger

test "Sender connecting to D4 server" do
d4_connection = %Exd4{
destination: Exd4.d4_ip(),
port: Exd4.d4_port(),
uuid: Exd4.d4_uuid(),
type: Exd4.d4_type(),
version: Exd4.d4_version(),
snaplen: Exd4.d4_snaplen(),
key: Exd4.d4_key()
}

assert {:ok, _} = Sender.start_link(d4_connection: d4_connection)
end

test "Sender sending data to D4 Server" do
d4_connection = %Exd4{
destination: Exd4.d4_ip(),
port: Exd4.d4_port(),
uuid: "b5062e1f-674e-45a0-9c30-2557d6e70ef6",
type: Exd4.d4_type(),
version: Exd4.d4_version(),
snaplen: Exd4.d4_snaplen(),
key: Exd4.d4_key()
}

assert {:ok, pid} = Sender.start_link(d4_connection: d4_connection)

Sender.send(pid, "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 😐 😑 😬 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾 \n")
Sender.send(pid, "blip blop\n")
end

test "Sender thrown out because of duplucate uuid" do
d4_connection = %Exd4{
destination: Exd4.d4_ip(),
port: Exd4.d4_port(),
uuid: "b5062e1f-674e-45a0-9c30-2557d6e70ef7",
type: Exd4.d4_type(),
version: Exd4.d4_version(),
snaplen: Exd4.d4_snaplen(),
key: Exd4.d4_key()
}

assert {:ok, pid1} = Sender.start_link(d4_connection: d4_connection)
assert {:ok} = Sender.send(pid1, "pid1\n")
assert {:ok, pid2} = Sender.start_link(d4_connection: d4_connection)
Process.sleep(1000)
assert {:error, :disconnected} = Sender.get_status(pid2)
end

test "D4 server unreachable" do
d4_connection = %Exd4{
destination: {10, 106, 129, 1},
port: 5000,
uuid: "b5062e1f-674e-45a0-9c30-2557d6e70ef7",
type: Exd4.d4_type(),
version: Exd4.d4_version(),
snaplen: Exd4.d4_snaplen(),
key: Exd4.d4_key()
}

assert {:ok, pid} = Sender.start_link(d4_connection: d4_connection)
Process.sleep(1000)
assert {:error, :disconnected} = Sender.get_status(pid)
end
end

0 comments on commit c7ad9a0

Please sign in to comment.