From 70acabfa3ec1e588f84be103bbae9bd1dd1ae40e Mon Sep 17 00:00:00 2001 From: Nicholas Buchanan Date: Wed, 11 Dec 2024 16:26:28 -0600 Subject: [PATCH 1/6] update mix deps versions --- mix.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index ad332d8..a6e679f 100644 --- a/mix.exs +++ b/mix.exs @@ -48,8 +48,8 @@ defmodule SwitchX.MixProject do [ {:uuid, "~> 1.1"}, {:mock, "~> 0.3.0", only: :test}, - {:telemetry, "~> 0.4.2"}, - {:ex_doc, "~> 0.18.0", only: :dev, runtime: false} + {:telemetry, "~> 1.0"}, + {:ex_doc, "~> 0.30", only: :dev, runtime: false} ] end end From 148ba1ce9d220afcbeff49b987b538413cfa8569 Mon Sep 17 00:00:00 2001 From: Nicholas Buchanan Date: Sat, 12 Apr 2025 13:00:58 -0500 Subject: [PATCH 2/6] fix warnings and add stop to terminate pid nicely --- lib/switchx.ex | 5 +++++ lib/switchx/connection.ex | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/switchx.ex b/lib/switchx.ex index 455f0f5..5c4b3ff 100644 --- a/lib/switchx.ex +++ b/lib/switchx.ex @@ -216,6 +216,11 @@ defmodule SwitchX do :gen_statem.stop(conn, :normal, 1_000) end + @spec stop(conn :: Pid) :: :ok | {:error, term} + def stop(conn) do + :gen_statem.stop(conn, :normal, 1_000) + end + @doc """ Hang up the call with a hangup_cause. """ diff --git a/lib/switchx/connection.ex b/lib/switchx/connection.ex index 941dbbe..68c0a7e 100644 --- a/lib/switchx/connection.ex +++ b/lib/switchx/connection.ex @@ -21,6 +21,7 @@ defmodule SwitchX.Connection do applications_pending: Map.new() ] + @impl true def callback_mode() do :handle_event_function end @@ -33,6 +34,7 @@ defmodule SwitchX.Connection do :gen_statem.start_link(__MODULE__, [session_module, socket, :outbound], []) end + @impl true def init([owner, socket, :inbound]) when is_port(socket) do {:ok, {host, port}} = :inet.peername(socket) @@ -49,6 +51,7 @@ defmodule SwitchX.Connection do {:ok, :connecting, data} end + @impl true def init([session_module, socket, :outbound]) when is_port(socket) do {:ok, {host, port}} = :inet.peername(socket) @@ -72,22 +75,25 @@ defmodule SwitchX.Connection do end ## Handler events ## - - def handle_event({:call, from}, {:close}, state, data) do + @impl true + def handle_event({:call, from}, {:close}, _state, data) do :gen_tcp.close(data.socket) :gen_statem.reply(from, :ok) {:next_state, :disconnected, data} end + @impl true def handle_event({:call, from}, message, state, data) do apply(__MODULE__, state, [:call, message, from, data]) end + @impl true def handle_event(:info, {:tcp, _socket, "\n"}, _state, data) do # Empty line discarding {:keep_state, data} end + @impl true def handle_event(:info, :read_data, :connecting, data) do :gen_tcp.send(data.socket, "connect\n\n") event = Socket.recv(data.socket) @@ -99,6 +105,7 @@ defmodule SwitchX.Connection do {:next_state, :ready, data} end + @impl true def handle_event(:info, {:tcp, socket, payload}, state, data) do event = Socket.recv(socket, payload) :inet.setopts(socket, active: :once) @@ -111,10 +118,12 @@ defmodule SwitchX.Connection do end end + @impl true def handle_event(:info, _message, _state, data) do {:keep_state, data} end + @impl true def handle_event(:disconnect, event, state, data) do case event.headers["Content-Disposition"] do "linger" -> @@ -155,6 +164,7 @@ defmodule SwitchX.Connection do def ready(:call, {:api, args}, from, data) do data = put_in(data.api_calls, :queue.in(from, data.api_calls)) + :gen_tcp.send(data.socket, "api #{args}\n\n") {:keep_state, data} end @@ -246,13 +256,13 @@ defmodule SwitchX.Connection do end def ready(:call, {:bgapi, args}, from, data) do - job_uuid = UUID.uuid4() + # job_uuid = UUID.uuid4() :gen_tcp.send(data.socket, "bgapi #{args}\n\n") data = put_in(data.commands_sent, :queue.in(from, data.commands_sent)) {:keep_state, data} end - def disconnected(:call, payload, from, data) do + def disconnected(:call, _payload, from, data) do :gen_statem.reply(from, {:error, :disconnected}) {:keep_state, data} end @@ -278,7 +288,7 @@ defmodule SwitchX.Connection do %{headers: %{"Content-Type" => "command/reply", "Reply-Text" => "+OK accepted"}}, data ) do - Logger.info("Connected") + Logger.info("ESL Authenticated to FreeSWITCH") {:next_state, :ready, reply_from_queue("commands_sent", {:ok, "Accepted"}, data)} end @@ -330,7 +340,7 @@ defmodule SwitchX.Connection do {:keep_state, data} end - def disconnected(:event, payload, data) do + def disconnected(:event, _payload, data) do {:keep_state, data} end From 622901c6030d16b8479537abc0a77f169212f78c Mon Sep 17 00:00:00 2001 From: Nicholas Buchanan Date: Fri, 9 May 2025 16:43:33 -0500 Subject: [PATCH 3/6] adding start and close connection functions --- lib/switchx/connection.ex | 4 ---- lib/switchx/connection/inbound.ex | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/switchx/connection.ex b/lib/switchx/connection.ex index f9bf714..4d3bbd7 100644 --- a/lib/switchx/connection.ex +++ b/lib/switchx/connection.ex @@ -346,10 +346,6 @@ defmodule SwitchX.Connection do {:keep_state, data} end - def disconnected(:event, _payload, data) do - {:keep_state, data} - end - @impl true def terminate(reason, _state, data) do :telemetry.execute([:switchx, :connection, data.connection_mode], %{value: -1}, %{}) diff --git a/lib/switchx/connection/inbound.ex b/lib/switchx/connection/inbound.ex index 0647f39..ca38493 100644 --- a/lib/switchx/connection/inbound.ex +++ b/lib/switchx/connection/inbound.ex @@ -5,6 +5,9 @@ defmodule SwitchX.Connection.Inbound do Inbound mode means you run your applications as clients, and connect to the FreeSWITCH server to invoke commands and control FreeSWITCH. """ + + require Logger + @mode :inbound @socket_opts [:binary, active: :once, packet: :line] @timeout 5_000 @@ -40,6 +43,28 @@ defmodule SwitchX.Connection.Inbound do end end + # added socket to the return tuple in a successful connection + def start(opts) do + host = Keyword.fetch!(opts, :host) + port = Keyword.fetch!(opts, :port) + + case perform_connect(host, port, @socket_opts, @timeout) do + {:ok, socket} -> + {:ok, client} = SwitchX.Connection.start_link(self(), socket, @mode) + :gen_tcp.controlling_process(socket, client) + {:ok, client, socket} + + {:error, reason} -> + {:error, reason} + end + end + + def close(client, socket) do + Logger.info("Closing gen_tcp connection") + :gen_tcp.close(socket) + :gen_statem.stop(client) + end + defp perform_connect(host, port, socket_opts, timeout) when is_binary(host) do host = host From 05fc13ed851cbbe64ebef449f199697eb37e112e Mon Sep 17 00:00:00 2001 From: Nicholas Buchanan Date: Mon, 12 May 2025 09:45:01 -0500 Subject: [PATCH 4/6] add back disconnected :event --- lib/switchx/connection.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/switchx/connection.ex b/lib/switchx/connection.ex index 4d3bbd7..aff8e98 100644 --- a/lib/switchx/connection.ex +++ b/lib/switchx/connection.ex @@ -346,6 +346,11 @@ defmodule SwitchX.Connection do {:keep_state, data} end + def disconnected(:event, _payload, data) do + {:keep_state, data} + end + + @impl true def terminate(reason, _state, data) do :telemetry.execute([:switchx, :connection, data.connection_mode], %{value: -1}, %{}) From de786953d993e807d13e2d9264048a148d716780 Mon Sep 17 00:00:00 2001 From: Nicholas Buchanan Date: Mon, 12 May 2025 13:47:07 -0500 Subject: [PATCH 5/6] adding debug logs --- lib/switchx/connection.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/switchx/connection.ex b/lib/switchx/connection.ex index aff8e98..979b54a 100644 --- a/lib/switchx/connection.ex +++ b/lib/switchx/connection.ex @@ -125,6 +125,7 @@ defmodule SwitchX.Connection do @impl true def handle_event(:disconnect, event, state, data) do + Logger.info("handle_event :disconnect #{inspect(event)}") case event.headers["Content-Disposition"] do "linger" -> Logger.info("Disconnect hold due to linger, keeping state #{inspect(state)}") @@ -338,15 +339,20 @@ defmodule SwitchX.Connection do end def ready(:event, event, data) do + # Logger.info("SwitchX ready: #{inspect(event)}") send(data.owner, {:switchx_event, event}) {:keep_state, data} end - def disconnected(:event, %{headers: %{"Content-Type" => "text/disconnect-notice"}}, data) do + def disconnected(:event, %{headers: %{"Content-Type" => "text/disconnect-notice"}} = event, data) do + + Logger.info("SwitchX disconnected with text/disconnect-notice: #{inspect(event)}") + send(data.owner, {:switchx_event, event}) {:keep_state, data} end - def disconnected(:event, _payload, data) do + def disconnected(:event, payload, data) do + Logger.info("SwitchX disconnected: #{inspect(payload)}") {:keep_state, data} end From 20976dce151a083fbaf32e82fff020ce3063980c Mon Sep 17 00:00:00 2001 From: Nicholas Buchanan Date: Tue, 7 Apr 2026 14:38:26 -0500 Subject: [PATCH 6/6] allow hostname for connection --- lib/switchx/connection/inbound.ex | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/switchx/connection/inbound.ex b/lib/switchx/connection/inbound.ex index ca38493..db81d97 100644 --- a/lib/switchx/connection/inbound.ex +++ b/lib/switchx/connection/inbound.ex @@ -66,13 +66,15 @@ defmodule SwitchX.Connection.Inbound do end defp perform_connect(host, port, socket_opts, timeout) when is_binary(host) do - host = - host - |> String.split(".") - |> Enum.map(&String.to_integer/1) - |> List.to_tuple() - - perform_connect(host, port, socket_opts, timeout) + # Try to parse as an IP address first, otherwise treat as hostname + case :inet.parse_address(String.to_charlist(host)) do + {:ok, ip_tuple} -> + :gen_tcp.connect(ip_tuple, port, socket_opts, timeout) + + {:error, :einval} -> + # Not a valid IP address, treat as hostname (charlist format) + :gen_tcp.connect(String.to_charlist(host), port, socket_opts, timeout) + end end defp perform_connect(host, port, socket_opts, timeout) when is_tuple(host) do