Class: Freeswitch::ESL::Client

Inherits:
Connection show all
Includes:
Logger
Defined in:
lib/freeswitch/esl/client.rb

Overview

Inbound ESL client: connects to FreeSWITCH mod_event_socket.

Constant Summary collapse

DEFAULT_EVENTS =
[
  # Events useful for channel state tracking
  "CHANNEL_CREATE",
  "CHANNEL_STATE",
  "CHANNEL_CALLSTATE",
  "CHANNEL_ANSWER",
  "CHANNEL_BRIDGE",
  "CHANNEL_UNBRIDGE",
  "CHANNEL_HANGUP",
  "CHANNEL_HANGUP_COMPLETE",
  "CHANNEL_DESTROY",

  # Extra CHANNEL events
  "CHANNEL_EXECUTE",
  "CHANNEL_EXECUTE_COMPLETE",
  "CHANNEL_PROGRESS",
  "CHANNEL_PROGRESS_MEDIA",

  # Receive BACKGROUND_JOB events for bgapi commands.
  "BACKGROUND_JOB",

  # Other events that might be useful for various applications
  "DTMF",
  "PLAYBACK_START",
  "PLAYBACK_STOP",
  "RECORD_START",
  "RECORD_STOP",
  "HEARTBEAT",
  "CUSTOM"
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logger

default_logger, #logger

Methods inherited from Connection

#bgapi, #closed?, #filter, #on, #pending_bgapi_command_uuids, #pending_commands_count, #send_command, #subscribe, #unsubscribe

Constructor Details

#initialize(freeswitch: {}, logger: nil, debug: false) ⇒ Client

rubocop:disable Lint/MissingSuper



60
61
62
63
64
65
66
67
68
# File 'lib/freeswitch/esl/client.rb', line 60

def initialize(freeswitch: {}, logger: nil, debug: false) # rubocop:disable Lint/MissingSuper
  @reconnect_handlers = []
  @intentionally_closed = false
  @ready = false
  @mutex = Mutex.new
  @ready_cv = ConditionVariable.new

  configure(freeswitch:, logger:, debug:)
end

Class Method Details

.connectObject



44
45
46
# File 'lib/freeswitch/esl/client.rb', line 44

def connect(**)
  new(**).connect
end

Instance Method Details

#attempt_reconnect(sync: false) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/freeswitch/esl/client.rb', line 174

def attempt_reconnect(sync: false)
  # If sync is true, the reconnect attempt is made in the current thread and blocks until it succeeds or fails.
  return reconnect_with_backoff if sync

  # If a reconnect thread is already running, do not start another one.
  return if @reconnect_thread&.alive?

  # Start a new thread to handle reconnect attempts in the background.
  @reconnect_thread = Thread.new do
    reconnect_with_backoff
  end
  @reconnect_thread.name = "esl-reconnect"
end

#authenticate!Object

Authenticate against FreeSWITCH after the initial auth/request message.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/freeswitch/esl/client.rb', line 142

def authenticate!
  # Wait for the initial auth/request message initiated by FreeSWITCH before sending the auth command.
  # This ensures we don't send auth before FreeSWITCH is ready to receive it,
  # which would cause an immediate disconnect.
  logger.debug "Waiting for auth/request from FreeSWITCH..."
  wait_for_auth_request(timeout: 10)

  # Send the auth command and wait for the response to confirm authentication succeeded.
  cmd = send_command("auth #{config.freeswitch.password}", timeout: 10).wait(raise_error: false)
  return if cmd.ok?

  raise AuthenticationError,
        "Authentication failed: #{cmd.error.class} - #{cmd.error.message.inspect}"
end

#build_socketObject



135
136
137
138
139
# File 'lib/freeswitch/esl/client.rb', line 135

def build_socket
  socket = TCPSocket.new(config.freeswitch.host, config.freeswitch.port)
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
  socket
end

#closeObject



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/freeswitch/esl/client.rb', line 93

def close
  @mutex.synchronize { @ready = false }

  logger.info "Closing FreeSWITCH ESL client connection"
  @intentionally_closed = true
  if @reconnect_thread
    # Stop reconnect thread now so it cannot keep running after close.
    @reconnect_thread.kill
    @reconnect_thread.join(0.1)
  end
  super
end

#configObject



49
50
51
# File 'lib/freeswitch/esl/client.rb', line 49

def config
  @config ||= Freeswitch::ESL.configuration
end

#configureObject



53
54
55
56
57
58
# File 'lib/freeswitch/esl/client.rb', line 53

def configure(**)
  close if instance_variable_defined?(:@socket) && @socket && !closed?
  config.update(**)
  @intentionally_closed = false
  self
end

#connectObject



70
71
72
73
74
75
# File 'lib/freeswitch/esl/client.rb', line 70

def connect
  @intentionally_closed = false
  config.freeswitch.reconnect ? attempt_reconnect(sync: true) : establish_connection

  self
end

#establish_connectionObject



126
127
128
129
130
131
132
133
# File 'lib/freeswitch/esl/client.rb', line 126

def establish_connection
  logger.info "Connecting to FreeSWITCH ESL at #{config.freeswitch.host}:#{config.freeswitch.port}"
  initialize_socket(build_socket, debug: config.debug)
  authenticate!
  subscribe(*DEFAULT_EVENTS)
  ready!
  logger.info "FreeSWITCH ESL client authenticated and ready!"
end

#exec(command, timeout: Command::DEFAULT_TIMEOUT, raise_error: true) {|cmd| ... } ⇒ Command

Execute a command via bgapi and return a Freeswitch::ESL::Command object.

Parameters:

  • command (String)

    the FreeSWITCH API command (e.g. “originate”)

  • args (String, nil)

    command arguments

  • timeout (Integer) (defaults to: Command::DEFAULT_TIMEOUT)

    seconds to wait for the bgapi result event

Yield Parameters:

  • cmd (Command)

    called on completion (success or failure)

Returns:



119
120
121
122
123
124
# File 'lib/freeswitch/esl/client.rb', line 119

def exec(command, *, timeout: Command::DEFAULT_TIMEOUT, raise_error: true, &)
  cmd = Command.new(self, command, *, timeout:, raise_error:, &)
  cmd.execute!

  cmd
end

#on_disconnect(error) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
# File 'lib/freeswitch/esl/client.rb', line 162

def on_disconnect(error)
  super

  @mutex.synchronize { @ready = false }
  return if @intentionally_closed

  logger.warn "Disconnected from FreeSWITCH ESL: #{error.message}"
  # Check the flag again in case user code called close during
  # disconnect handling.
  attempt_reconnect unless @intentionally_closed
end

#on_reconnect(&block) ⇒ Object



106
107
108
109
110
# File 'lib/freeswitch/esl/client.rb', line 106

def on_reconnect(&block)
  logger.debug "Registering FreeSWITCH ESL client reconnect handler"
  @reconnect_handlers << block
  self
end

#ready!Object



157
158
159
160
# File 'lib/freeswitch/esl/client.rb', line 157

def ready!
  @mutex.synchronize { @ready = true }
  @ready_cv.broadcast
end

#ready?Boolean

Returns:

  • (Boolean)


89
90
91
# File 'lib/freeswitch/esl/client.rb', line 89

def ready?
  @ready
end

#reconnect_onceObject



204
205
206
207
208
209
210
211
212
# File 'lib/freeswitch/esl/client.rb', line 204

def reconnect_once
  establish_connection
  @reconnect_handlers.each(&:call)
  true
rescue StandardError => e
  # Return false so the outer loop can try again.
  logger.warn "Reconnect attempt failed: #{e.message}"
  false
end

#reconnect_with_backoffObject



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/freeswitch/esl/client.rb', line 188

def reconnect_with_backoff
  retries = 0
  delay = config.freeswitch.retry_delay

  loop do
    break if @intentionally_closed
    break if reconnect_once

    # Wait before each retry to avoid a tight retry loop.
    sleep(delay)

    retries += 1
    break if stop_reconnect?(retries)
  end
end

#stop_reconnect?(retries) ⇒ Boolean

Returns:

  • (Boolean)


214
215
216
# File 'lib/freeswitch/esl/client.rb', line 214

def stop_reconnect?(retries)
  retries >= config.freeswitch.max_retries || @intentionally_closed
end

#wait_until_ready(timeout: nil, raise_error: true) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/freeswitch/esl/client.rb', line 77

def wait_until_ready(timeout: nil, raise_error: true)
  @mutex.synchronize do
    return self if ready?

    @ready_cv.wait(@mutex, timeout)

    raise TimeoutError, "Timed out waiting for FreeSWITCH ESL client to be ready" if raise_error && !ready?
  end

  self
end