freeswitch-esl
freeswitch-esl is a Ruby gem for interacting with FreeSWITCH through ESL (Event Socket Library).
This version is intentionally focused on one direction only: your Ruby process connects inbound to mod_event_socket and manages one or more Freeswitch::ESL::Client instances.
The implementation keeps the public API small and groups protocol concerns separately:
Freeswitch::ESL::ClientFreeswitch::ESL::ConnectionFreeswitch::ESL::Protocol::MessageFreeswitch::ESL::Protocol::Event
Why ESL
For FreeSWITCH, ESL is still the most complete and stable integration surface for external control. Other interfaces exist, but they are generally less complete:
mod_xml_rpc: useful for some management operations, but not a replacement for event streaming and call control- custom REST/gRPC wrappers: possible, but usually built on top of ESL rather than replacing it
If you need event subscriptions, command execution and background jobs, ESL remains the right interface.
Installation
Choose one installation mode based on your environment.
Local path (development)
Use this while developing the gem and application together:
gem 'freeswitch-esl', path: '/path/to/freeswitch-esl'
Git source (production)
Use this when you want to pin a tag or commit from your repository:
gem 'freeswitch-esl', git: 'https://gitlab.example.com/your-group/freeswitch-esl.git', tag: 'v0.1.0'
RubyGems style (for future releases)
When the gem is published on RubyGems, the classic entry is:
gem 'freeswitch-esl'
Then require it:
require 'freeswitch-esl'
Configuration
Configure the library once and then connect a client:
require 'logger'
require 'freeswitch-esl'
Freeswitch::ESL.configure do |config|
config.freeswitch.host = 'freeswitch.example.com'
config.freeswitch.port = 8021
config.freeswitch.password = 'ClueCon'
config.freeswitch.reconnect = true
config.freeswitch.retry_delay = 1.0
config.freeswitch.max_retries = 5
config.logger = Logger.new($stdout)
config.logger.level = Logger::INFO
config.debug = false
end
Defaults:
config.freeswitch.host = '127.0.0.1'config.freeswitch.port = 8021config.freeswitch.password = nilconfig.freeswitch.reconnect = trueconfig.freeswitch.retry_delay = 1.0config.freeswitch.max_retries = Float::INFINITYconfig.logger = Logger.new(IO::NULL)config.debug = false
FreeSWITCH Configuration
Inbound ESL (mod_event_socket)
FreeSWITCH must expose mod_event_socket.
For local development, a minimal configuration looks like this:
<configuration name="event_socket.conf" description="Socket Client">
<settings>
<param name="listen-ip" value="0.0.0.0"/>
<param name="listen-port" value="8021"/>
<param name="password" value="ClueCon"/>
<param name="apply-inbound-acl" value="lan"/>
</settings>
</configuration>
For production, tighten the ACL further and do not expose port 8021 publicly.
In the included Docker setup, lan is intentional because it allows both loopback and Docker private-network traffic while still avoiding a fully open event socket.
Usage
Connect a client
require 'freeswitch-esl'
Freeswitch::ESL.configure do |config|
config.freeswitch.host = '127.0.0.1'
config.freeswitch.port = 8021
config.freeswitch.password = 'ClueCon'
end
client = Freeswitch::ESL::Client.connect
client.subscribe('CHANNEL_CREATE', 'CHANNEL_HANGUP', 'DTMF')
client.on('CHANNEL_CREATE') do |event|
puts "new channel: #{event.uuid}"
end
client.on('DTMF') do |event|
puts "digit=#{event['DTMF-Digit']} duration=#{event['DTMF-Duration']}"
end
response = client.exec('status').response
puts response.body
client.close
Freeswitch::ESL::Client.connect builds a client with the global configuration,
opens the TCP socket, waits for FreeSWITCH auth/request, authenticates, subscribes
to the default event set, and returns a ready client instance.
Reconfigure an existing client
client = Freeswitch::ESL::Client.connect
client.close
client.configure(freeswitch: { host: 'fs-backup.example.com', reconnect: true })
client.connect
Background API
require 'freeswitch-esl'
client = Freeswitch::ESL::Client.connect
command = client.exec('originate', 'sofia/default/1000 &park')
command.on_complete do |completed|
puts "job #{completed.job_uuid} finished with status=#{completed.status}"
puts completed.response.body if completed.response
end
puts "submitted job=#{command.job_uuid}"
command.wait
client.close
Client#exec wraps bgapi and returns a Freeswitch::ESL::Command object.
You can:
- call
command.waitfor blocking flow - call
command.responseto block until completion and get the finalBACKGROUND_JOBevent - inspect
command.status,command.ok?,command.failed?,command.timeout? - attach
command.on_complete { |cmd| ... }for async completion handling
If you prefer lower-level primitives, Freeswitch::ESL::Client also exposes the inherited connection API:
send_commandbgapisubscribeunsubscribefilteron
Auto reconnect
When config.freeswitch.reconnect is enabled, Client#connect retries on initial
connection/authentication failure and the client reconnects automatically after later
disconnects.
Event handlers remain registered across reconnects because the event dispatcher is preserved. Subscriptions do not: after reconnect, re-subscribe to the events you need.
require 'freeswitch-esl'
client = Freeswitch::ESL::Client.connect
client.on('CHANNEL_HANGUP') do |event|
puts "hangup cause=#{event['Hangup-Cause']}"
end
client.on_reconnect do
client.subscribe('CHANNEL_HANGUP')
end
client.subscribe('CHANNEL_HANGUP')
client.close stops reconnect attempts immediately. The same client instance can be
connected again later with client.connect.
Docker Compose Test Lab
This repository includes a local FreeSWITCH environment for ESL experimentation. It is intended for development and manual testing, and it can later serve as a base for realistic integration tests.
Files included:
docker-compose.ymldocker/freeswitch/autoload_configs/event_socket.conf.xmlexamples/inbound_status.rb
The compose file builds FreeSWITCH locally from the official source repository release.
The Dockerfile is aligned with the upstream reference at
signalwire/freeswitch/docker/examples/Debian11/Dockerfile, adapted for Debian Bookworm and this project's runtime needs.
By default it uses FS_VERSION=latest.
To pin a specific release tag (recommended for reproducible environments), set the build arg in docker-compose.yml:
services:
freeswitch:
build:
args:
FS_VERSION: v1.10.12
Start FreeSWITCH
docker compose up -d
If you change image or base distro later, verify that the configuration directory is still /usr/local/freeswitch/etc/freeswitch.
Check that FreeSWITCH is up
docker compose exec freeswitch fs_cli -x 'status'
Test inbound ESL connectivity
require 'freeswitch-esl'
Freeswitch::ESL.configure do |config|
config.freeswitch.host = '127.0.0.1'
config.freeswitch.port = 8021
config.freeswitch.password = 'ClueCon'
end
client = Freeswitch::ESL::Client.connect
puts client.exec('status').response.body
client.close
Or run the ready-made example:
ruby examples/inbound_status.rb
Production Notes
- keep ESL bound to a private interface whenever possible
- protect the event socket with ACLs and a non-default password
- do not use the included Docker configuration as-is in production
- prefer explicit event subscriptions instead of
ALLunless you really need global event traffic
Development
Run style checks:
bundle exec rubocop
Run tests:
bundle exec rspec
GitLab CI local
Run the full local pipeline:
gitlab-ci-local
Run only the test job:
gitlab-ci-local rspec
Run only lint:
gitlab-ci-local rubocop
Test stability note
Some client specs use reconnect threads. To avoid order-dependent failures in random runs, the test teardown always closes created clients. This makes sure reconnect-related activity is stopped before the next example starts.