freeswitch-esl 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +83 -22
- data/lib/freeswitch/esl/client.rb +120 -19
- data/lib/freeswitch/esl/command.rb +221 -0
- data/lib/freeswitch/esl/configuration.rb +39 -13
- data/lib/freeswitch/esl/connection/command_dispatcher.rb +258 -0
- data/lib/freeswitch/esl/connection/command_request.rb +154 -0
- data/lib/freeswitch/esl/connection/event_dispatcher.rb +10 -2
- data/lib/freeswitch/esl/connection.rb +53 -75
- data/lib/freeswitch/esl/logger.rb +3 -1
- data/lib/freeswitch/esl/protocol/event.rb +119 -0
- data/lib/freeswitch/esl/protocol/message.rb +7 -2
- data/lib/freeswitch/esl/version.rb +1 -1
- data/lib/freeswitch/esl.rb +3 -1
- metadata +61 -3
- data/lib/freeswitch/esl/connection/message_reader.rb +0 -109
|
@@ -4,7 +4,8 @@ require "timeout"
|
|
|
4
4
|
|
|
5
5
|
require "freeswitch/esl/protocol/event"
|
|
6
6
|
require "freeswitch/esl/protocol/message"
|
|
7
|
-
require "freeswitch/esl/connection/
|
|
7
|
+
require "freeswitch/esl/connection/command_dispatcher"
|
|
8
|
+
require "freeswitch/esl/connection/command_request"
|
|
8
9
|
require "freeswitch/esl/connection/event_dispatcher"
|
|
9
10
|
|
|
10
11
|
module Freeswitch
|
|
@@ -15,77 +16,71 @@ module Freeswitch
|
|
|
15
16
|
# {#initialize_socket} to start the reader and event-dispatcher threads.
|
|
16
17
|
#
|
|
17
18
|
# Threading model:
|
|
18
|
-
# * A
|
|
19
|
-
#
|
|
20
|
-
#
|
|
19
|
+
# * A reader thread owned by {CommandDispatcher} reads messages from the
|
|
20
|
+
# socket and routes them to pending command executions (FIFO) or to the
|
|
21
|
+
# event dispatcher.
|
|
21
22
|
# * A *dispatcher thread* processes @event_queue and calls registered handlers.
|
|
22
|
-
# * Command
|
|
23
|
-
#
|
|
24
|
-
# * On timeout the connection is closed because the protocol state is unknown.
|
|
23
|
+
# * Command enqueue + write is atomic under a mutex in CommandDispatcher,
|
|
24
|
+
# guaranteeing queue order == socket write order.
|
|
25
25
|
class Connection
|
|
26
|
-
|
|
27
|
-
DEFAULT_TIMEOUT = 5
|
|
26
|
+
include Freeswitch::ESL::Logger
|
|
28
27
|
|
|
29
28
|
def initialize(socket)
|
|
30
29
|
initialize_socket(socket)
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
# Send a raw ESL command and return the
|
|
34
|
-
def send_command(command, timeout:
|
|
32
|
+
# Send a raw ESL command and return a CommandRequest object that can be used to wait for the response.
|
|
33
|
+
def send_command(command, timeout: nil)
|
|
35
34
|
raise DisconnectedError, "Connection is closed" if closed?
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
# reply by mistake.
|
|
39
|
-
@send_mutex.synchronize do
|
|
40
|
-
@socket.write("#{command}#{MSG_TERMINATOR}")
|
|
41
|
-
receive_response(timeout: timeout)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Execute a synchronous +api+ command. Returns the {Message} with the
|
|
46
|
-
# result in {Message#body}.
|
|
47
|
-
def api(command, args = nil, timeout: DEFAULT_TIMEOUT)
|
|
48
|
-
parts = ["api", command, args].compact
|
|
49
|
-
@send_mutex.synchronize do
|
|
50
|
-
@socket.write("#{parts.join(' ')}#{MSG_TERMINATOR}")
|
|
51
|
-
receive_response(timeout: timeout)
|
|
52
|
-
end
|
|
36
|
+
@command_dispatcher.execute_command(command, timeout:)
|
|
53
37
|
end
|
|
54
38
|
|
|
55
39
|
# Execute a background +bgapi+ command. Returns the Job-UUID string.
|
|
56
40
|
# The optional block is called with the BACKGROUND_JOB {Event} when the
|
|
57
41
|
# result arrives.
|
|
58
|
-
def bgapi(command, args
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
42
|
+
def bgapi(command, *args, timeout: nil, &block)
|
|
43
|
+
raise DisconnectedError, "Connection is closed" if closed?
|
|
44
|
+
|
|
45
|
+
parts = ["bgapi", command, *args].compact
|
|
46
|
+
logger.debug("[BGAPI] #{parts.join(' ')}")
|
|
47
|
+
cmd = @command_dispatcher.execute_command(parts.join(" "), timeout:).wait
|
|
48
|
+
job_uuid = cmd.response["Job-UUID"]
|
|
49
|
+
raise CommandError, "FreeSWITCH bgapi did not return a Job-UUID" unless job_uuid
|
|
65
50
|
|
|
66
|
-
@event_dispatcher.register_bgapi_handler(job_uuid, block) if block
|
|
51
|
+
@event_dispatcher.register_bgapi_handler(job_uuid, block) if block
|
|
67
52
|
job_uuid
|
|
68
53
|
end
|
|
69
54
|
|
|
55
|
+
# Return the number of commands currently waiting for a response.
|
|
56
|
+
def pending_commands_count
|
|
57
|
+
@command_dispatcher.pending_commands_count
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Return an array of Job-UUID strings for all pending bgapi commands.
|
|
61
|
+
def pending_bgapi_command_uuids
|
|
62
|
+
@event_dispatcher.pending_bgapi_command_uuids
|
|
63
|
+
end
|
|
64
|
+
|
|
70
65
|
# Subscribe to one or more event names (JSON format). Pass no arguments
|
|
71
66
|
# or +"ALL"+ to receive every event.
|
|
72
67
|
def subscribe(*event_names)
|
|
73
68
|
events = event_names.empty? ? "ALL" : event_names.join(" ")
|
|
74
|
-
send_command("event json #{events}")
|
|
69
|
+
send_command("event json #{events}").wait
|
|
75
70
|
end
|
|
76
71
|
|
|
77
72
|
# Cancel subscriptions. Without arguments cancels all events.
|
|
78
73
|
def unsubscribe(*event_names)
|
|
79
74
|
if event_names.empty?
|
|
80
|
-
send_command("noevents")
|
|
75
|
+
send_command("noevents").wait
|
|
81
76
|
else
|
|
82
|
-
event_names.each { |e| send_command("nixevent #{e}") }
|
|
77
|
+
event_names.each { |e| send_command("nixevent #{e}").wait }
|
|
83
78
|
end
|
|
84
79
|
end
|
|
85
80
|
|
|
86
81
|
# Add an event-header filter so FreeSWITCH only sends matching events.
|
|
87
82
|
def filter(header, value)
|
|
88
|
-
send_command("filter #{header} #{value}")
|
|
83
|
+
send_command("filter #{header} #{value}").wait
|
|
89
84
|
end
|
|
90
85
|
|
|
91
86
|
# Register a handler block for an event name. Use +"ALL"+ to handle
|
|
@@ -100,27 +95,27 @@ module Freeswitch
|
|
|
100
95
|
@closed
|
|
101
96
|
end
|
|
102
97
|
|
|
98
|
+
# Stop dispatchers and mark the connection closed. Event handlers are
|
|
99
|
+
# removed together with the dispatcher instances, so a reconnect path must
|
|
100
|
+
# rebuild them through {#initialize_socket}.
|
|
103
101
|
def close
|
|
104
102
|
return if @closed
|
|
105
103
|
|
|
106
104
|
@closed = true
|
|
107
|
-
|
|
108
|
-
@socket.close
|
|
109
|
-
rescue StandardError
|
|
110
|
-
nil
|
|
111
|
-
end
|
|
112
|
-
@message_reader&.stop
|
|
105
|
+
@command_dispatcher&.stop
|
|
113
106
|
@event_dispatcher&.stop
|
|
107
|
+
|
|
108
|
+
send(:remove_instance_variable, :@command_dispatcher)
|
|
109
|
+
send(:remove_instance_variable, :@event_dispatcher)
|
|
114
110
|
end
|
|
115
111
|
|
|
116
112
|
protected
|
|
117
113
|
|
|
118
114
|
# (Re-)initialise all per-connection state for the given socket.
|
|
119
115
|
# Called by subclasses on initial connect and on reconnect.
|
|
120
|
-
def initialize_socket(socket)
|
|
121
|
-
@socket
|
|
122
|
-
@
|
|
123
|
-
@closed = false
|
|
116
|
+
def initialize_socket(socket, debug: false)
|
|
117
|
+
@socket = socket
|
|
118
|
+
@closed = false
|
|
124
119
|
|
|
125
120
|
# Keep dispatcher data across reconnects so existing handlers still work
|
|
126
121
|
# after a new socket is created.
|
|
@@ -129,38 +124,21 @@ module Freeswitch
|
|
|
129
124
|
@event_dispatcher.start
|
|
130
125
|
end
|
|
131
126
|
|
|
132
|
-
# Always create new
|
|
133
|
-
@
|
|
134
|
-
@
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# Authenticate against FreeSWITCH after the initial auth/request message.
|
|
138
|
-
def authenticate!(password, timeout: DEFAULT_TIMEOUT)
|
|
139
|
-
auth_request = receive_response(timeout: timeout)
|
|
140
|
-
unless auth_request.content_type == "auth/request"
|
|
141
|
-
raise AuthenticationError, "Expected auth/request, got: #{auth_request.content_type}"
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# `auth` is a basic ESL command with a command/reply response, so we
|
|
145
|
-
# use send_command (not api/bgapi).
|
|
146
|
-
reply = send_command("auth #{password}", timeout: timeout)
|
|
147
|
-
return if reply.successful?
|
|
148
|
-
|
|
149
|
-
raise AuthenticationError, "Authentication failed: #{reply.reply_text}"
|
|
127
|
+
# Always create a new CommandDispatcher for each socket
|
|
128
|
+
@command_dispatcher = CommandDispatcher.new(@socket, @event_dispatcher, debug:)
|
|
129
|
+
@command_dispatcher.on_disconnect { |error| on_disconnect(error) }
|
|
130
|
+
@command_dispatcher.start
|
|
150
131
|
end
|
|
151
132
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# command, so we close and start from a clean connection.
|
|
157
|
-
close
|
|
158
|
-
raise
|
|
133
|
+
# Wait for the initial auth/request bootstrap message before sending any
|
|
134
|
+
# commands that assume FreeSWITCH is ready to accept authenticated input.
|
|
135
|
+
def wait_for_auth_request(timeout: nil)
|
|
136
|
+
@command_dispatcher.wait_for_auth_request(timeout:)
|
|
159
137
|
end
|
|
160
138
|
|
|
161
139
|
private
|
|
162
140
|
|
|
163
|
-
# Called by
|
|
141
|
+
# Called by the command dispatcher when the current socket disconnects.
|
|
164
142
|
def on_disconnect(_error)
|
|
165
143
|
@closed = true
|
|
166
144
|
end
|
|
@@ -13,7 +13,9 @@ module Freeswitch
|
|
|
13
13
|
def logger
|
|
14
14
|
return @logger if defined?(@logger) && @logger
|
|
15
15
|
|
|
16
|
-
@logger = Freeswitch::ESL.configuration.logger
|
|
16
|
+
@logger = Freeswitch::ESL.configuration.logger.dup
|
|
17
|
+
@logger.progname = self.class.name
|
|
18
|
+
@logger
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -7,36 +7,155 @@ module Freeswitch
|
|
|
7
7
|
module Protocol
|
|
8
8
|
# Represents a FreeSWITCH event received in JSON format.
|
|
9
9
|
class Event
|
|
10
|
+
ERROR_BODY_PREFIX = /\A\s*-(ERR|USAGE)\b/
|
|
11
|
+
|
|
10
12
|
attr_reader :data
|
|
11
13
|
|
|
12
14
|
def initialize(raw_json)
|
|
13
15
|
@data = JSON.parse(raw_json).freeze
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
# Returns the raw value stored in the event payload for the given key.
|
|
19
|
+
# @return [Object, nil] Parsed JSON value for the requested field.
|
|
16
20
|
def [](key)
|
|
17
21
|
data[key]
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# Returns the canonical FreeSWITCH event name.
|
|
25
|
+
# @return [String, nil] Event name such as CHANNEL_CREATE, HEARTBEAT or BACKGROUND_JOB.
|
|
20
26
|
def name
|
|
21
27
|
data["Event-Name"]
|
|
22
28
|
end
|
|
23
29
|
|
|
30
|
+
# Returns the UUID of the emitting FreeSWITCH core instance.
|
|
31
|
+
# @return [String, nil] Core identifier shared across events generated by the same node.
|
|
32
|
+
def core_uuid
|
|
33
|
+
data["Core-UUID"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the hostname reported by the FreeSWITCH node.
|
|
37
|
+
# @return [String, nil] Hostname of the server that emitted the event.
|
|
38
|
+
def hostname
|
|
39
|
+
data["FreeSWITCH-Hostname"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the logical FreeSWITCH switch name.
|
|
43
|
+
# @return [String, nil] Switch name configured on the emitting node.
|
|
44
|
+
def switchname
|
|
45
|
+
data["FreeSWITCH-Switchname"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the IPv4 address advertised by FreeSWITCH.
|
|
49
|
+
# @return [String, nil] IPv4 address of the emitting node.
|
|
50
|
+
def ipv4
|
|
51
|
+
data["FreeSWITCH-IPv4"]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the IPv6 address advertised by FreeSWITCH.
|
|
55
|
+
# @return [String, nil] IPv6 address of the emitting node.
|
|
56
|
+
def ipv6
|
|
57
|
+
data["FreeSWITCH-IPv6"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the event subclass when the event provides one.
|
|
61
|
+
# @return [String, nil] Module-specific subclass, often used by CUSTOM events.
|
|
24
62
|
def subclass
|
|
25
63
|
data["Event-Subclass"]
|
|
26
64
|
end
|
|
27
65
|
|
|
66
|
+
# Returns the event timestamp formatted in the server local timezone.
|
|
67
|
+
# @return [String, nil] Human-readable local date and time string.
|
|
68
|
+
def date_local
|
|
69
|
+
data["Event-Date-Local"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns the event timestamp formatted in GMT.
|
|
73
|
+
# @return [String, nil] Human-readable GMT date and time string.
|
|
74
|
+
def date_gmt
|
|
75
|
+
data["Event-Date-GMT"]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the raw FreeSWITCH event timestamp.
|
|
79
|
+
# @return [String, nil] Epoch timestamp expressed in microseconds.
|
|
80
|
+
def timestamp
|
|
81
|
+
data["Event-Date-Timestamp"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the FreeSWITCH source file that emitted the event.
|
|
85
|
+
# @return [String, nil] Internal source filename from the FreeSWITCH runtime.
|
|
86
|
+
def calling_file
|
|
87
|
+
data["Event-Calling-File"]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns the FreeSWITCH source function that emitted the event.
|
|
91
|
+
# @return [String, nil] Internal function name from the FreeSWITCH runtime.
|
|
92
|
+
def calling_function
|
|
93
|
+
data["Event-Calling-Function"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns the FreeSWITCH source line number that emitted the event.
|
|
97
|
+
# @return [String, nil] Internal line number from the FreeSWITCH runtime.
|
|
98
|
+
def calling_line_number
|
|
99
|
+
data["Event-Calling-Line-Number"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns the monotonic event sequence assigned by FreeSWITCH.
|
|
103
|
+
# @return [String, nil] Sequence number as exposed by the event payload.
|
|
104
|
+
def sequence
|
|
105
|
+
data["Event-Sequence"]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns the channel UUID associated with the event.
|
|
109
|
+
# @return [String, nil] Session identifier when the event refers to a channel.
|
|
28
110
|
def uuid
|
|
29
111
|
data["Unique-ID"]
|
|
30
112
|
end
|
|
31
113
|
|
|
114
|
+
# Returns the UUID assigned to a background job.
|
|
115
|
+
# @return [String, nil] Job identifier for BACKGROUND_JOB events.
|
|
32
116
|
def job_uuid
|
|
33
117
|
data["Job-UUID"]
|
|
34
118
|
end
|
|
35
119
|
|
|
120
|
+
# Returns the command executed by the background job.
|
|
121
|
+
# @return [String, nil] Original command name reported by a BACKGROUND_JOB event.
|
|
122
|
+
def job_command
|
|
123
|
+
data["Job-Command"]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Returns the command executed by a synchronous API event.
|
|
127
|
+
# @return [String, nil] API command name reported by an API event.
|
|
128
|
+
def api_command
|
|
129
|
+
data["API-Command"]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns the human-readable informational text attached to the event.
|
|
133
|
+
# @return [String, nil] Summary text such as the HEARTBEAT status message.
|
|
134
|
+
def info
|
|
135
|
+
data["Event-Info"]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns the optional event body payload.
|
|
139
|
+
# @return [String, nil] Body content, commonly used for api/bgapi command output.
|
|
36
140
|
def body
|
|
37
141
|
data["_body"]
|
|
38
142
|
end
|
|
39
143
|
|
|
144
|
+
# Returns whether the event represents a bgapi job completion.
|
|
145
|
+
# @return [Boolean] True when the event name is BACKGROUND_JOB.
|
|
146
|
+
def bg_job_event?
|
|
147
|
+
name == "BACKGROUND_JOB"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns whether the event body represents an ESL command error.
|
|
151
|
+
# @return [Boolean] True for bodies starting with -ERR or -USAGE,
|
|
152
|
+
# false for non-error bodies, false when the body is missing.
|
|
153
|
+
def error?
|
|
154
|
+
body&.match?(ERROR_BODY_PREFIX) || false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns the value of a channel variable exported in the event payload.
|
|
158
|
+
# @return [String, nil] Value stored under the corresponding variable_* event field.
|
|
40
159
|
def channel_variable(name)
|
|
41
160
|
data["variable_#{name}"]
|
|
42
161
|
end
|
|
@@ -24,14 +24,19 @@ module Freeswitch
|
|
|
24
24
|
headers["Reply-Text"]
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Return true when the message payload represents a successful ESL reply.
|
|
28
|
+
# For +api/response+ messages the success marker lives in the body; for
|
|
29
|
+
# command replies it lives in the Reply-Text header.
|
|
27
30
|
def successful?
|
|
28
31
|
if content_type == "api/response"
|
|
29
|
-
body&.start_with?("+OK")
|
|
32
|
+
body&.start_with?("+OK") || false
|
|
30
33
|
else
|
|
31
|
-
reply_text&.start_with?("+OK")
|
|
34
|
+
reply_text&.start_with?("+OK") || false
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
# Extract the FreeSWITCH error text without the leading +-ERR+ marker.
|
|
39
|
+
# Returns +nil+ for successful replies or when no error payload is present.
|
|
35
40
|
def error_message
|
|
36
41
|
text = content_type == "api/response" ? body : reply_text
|
|
37
42
|
text&.start_with?("-ERR") ? text.sub(/\A-ERR\s*/, "").strip : nil
|
data/lib/freeswitch/esl.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "logger"
|
|
4
|
+
require "ztimer"
|
|
4
5
|
|
|
5
6
|
require "freeswitch/esl/version"
|
|
6
7
|
require "freeswitch/esl/errors"
|
|
@@ -9,6 +10,7 @@ require "freeswitch/esl/configuration"
|
|
|
9
10
|
require "freeswitch/esl/protocol/message"
|
|
10
11
|
require "freeswitch/esl/protocol/event"
|
|
11
12
|
require "freeswitch/esl/connection"
|
|
13
|
+
require "freeswitch/esl/command"
|
|
12
14
|
require "freeswitch/esl/client"
|
|
13
15
|
|
|
14
16
|
module Freeswitch
|
|
@@ -22,7 +24,7 @@ module Freeswitch
|
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def configuration
|
|
25
|
-
@configuration ||= Configuration.
|
|
27
|
+
@configuration ||= Configuration.new
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def reset!
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: freeswitch-esl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Demetra Opinioni.net Srl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: configatron
|
|
@@ -38,6 +38,62 @@ dependencies:
|
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '1.6'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: ztimer
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: csv
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.3'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.3'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: fiddle
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rdoc
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '7.2'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '7.2'
|
|
41
97
|
- !ruby/object:Gem::Dependency
|
|
42
98
|
name: rspec
|
|
43
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -108,10 +164,12 @@ files:
|
|
|
108
164
|
- lib/freeswitch-esl.rb
|
|
109
165
|
- lib/freeswitch/esl.rb
|
|
110
166
|
- lib/freeswitch/esl/client.rb
|
|
167
|
+
- lib/freeswitch/esl/command.rb
|
|
111
168
|
- lib/freeswitch/esl/configuration.rb
|
|
112
169
|
- lib/freeswitch/esl/connection.rb
|
|
170
|
+
- lib/freeswitch/esl/connection/command_dispatcher.rb
|
|
171
|
+
- lib/freeswitch/esl/connection/command_request.rb
|
|
113
172
|
- lib/freeswitch/esl/connection/event_dispatcher.rb
|
|
114
|
-
- lib/freeswitch/esl/connection/message_reader.rb
|
|
115
173
|
- lib/freeswitch/esl/errors.rb
|
|
116
174
|
- lib/freeswitch/esl/logger.rb
|
|
117
175
|
- lib/freeswitch/esl/protocol/event.rb
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "timeout"
|
|
4
|
-
|
|
5
|
-
module Freeswitch
|
|
6
|
-
module ESL
|
|
7
|
-
class Connection
|
|
8
|
-
# Reads ESL protocol messages from the socket and routes them appropriately.
|
|
9
|
-
# Runs in a dedicated thread and handles socket parsing and message dispatch.
|
|
10
|
-
class MessageReader
|
|
11
|
-
def initialize(socket, event_dispatcher, &on_disconnect_callback)
|
|
12
|
-
@socket = socket
|
|
13
|
-
@event_dispatcher = event_dispatcher
|
|
14
|
-
@on_disconnect_callback = on_disconnect_callback
|
|
15
|
-
@response_queue = Queue.new
|
|
16
|
-
@reader_thread = nil
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Get the response queue (where command replies are enqueued).
|
|
20
|
-
attr_reader :response_queue
|
|
21
|
-
|
|
22
|
-
# Start the reader thread.
|
|
23
|
-
def start
|
|
24
|
-
return if @reader_thread
|
|
25
|
-
|
|
26
|
-
@reader_thread = Thread.new do
|
|
27
|
-
loop do
|
|
28
|
-
msg = read_message
|
|
29
|
-
unless msg
|
|
30
|
-
on_disconnect(DisconnectedError.new("Connection closed by remote host"))
|
|
31
|
-
break
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
route_message(msg)
|
|
35
|
-
rescue IOError, Errno::ECONNRESET, Errno::ENOTCONN => e
|
|
36
|
-
on_disconnect(DisconnectedError.new(e.message))
|
|
37
|
-
break
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
@reader_thread.name = "esl-reader"
|
|
41
|
-
@reader_thread.abort_on_exception = false
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Stop the reader thread and join it.
|
|
45
|
-
def stop
|
|
46
|
-
# The thread exits when socket reads hit EOF/error. `stop` is only a
|
|
47
|
-
# synchronisation point to wait for clean shutdown.
|
|
48
|
-
@reader_thread&.join
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Wait for and return a response from the response queue with timeout.
|
|
52
|
-
def receive_response(timeout:)
|
|
53
|
-
result = nil
|
|
54
|
-
Timeout.timeout(timeout) { result = @response_queue.pop }
|
|
55
|
-
raise result if result.is_a?(Exception)
|
|
56
|
-
|
|
57
|
-
result
|
|
58
|
-
rescue Timeout::Error
|
|
59
|
-
raise TimeoutError, "Command timed out after #{timeout}s — connection closed"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Enqueue an error to unblock waiting threads (e.g., on disconnect).
|
|
63
|
-
def enqueue_error(error)
|
|
64
|
-
@response_queue << error
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
def read_message
|
|
70
|
-
headers = {}
|
|
71
|
-
|
|
72
|
-
loop do
|
|
73
|
-
line = @socket.gets("\n")
|
|
74
|
-
return nil unless line
|
|
75
|
-
|
|
76
|
-
line = line.chomp
|
|
77
|
-
break if line.empty?
|
|
78
|
-
|
|
79
|
-
key, value = line.split(": ", 2)
|
|
80
|
-
headers[key] = value
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
return nil if headers.empty?
|
|
84
|
-
|
|
85
|
-
body = @socket.read(headers["Content-Length"].to_i) if headers.key?("Content-Length")
|
|
86
|
-
Protocol::Message.new(headers, body)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def route_message(message)
|
|
90
|
-
# Unknown content types are ignored on purpose.
|
|
91
|
-
case message.content_type
|
|
92
|
-
when "auth/request", "command/reply", "api/response"
|
|
93
|
-
@response_queue << message
|
|
94
|
-
when "text/event-json"
|
|
95
|
-
@event_dispatcher.enqueue_event(Protocol::Event.new(message.body))
|
|
96
|
-
when "text/disconnect-notice"
|
|
97
|
-
on_disconnect(DisconnectedError.new("FreeSWITCH sent disconnect notice"))
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def on_disconnect(error)
|
|
102
|
-
# First wake up waiting command calls, then notify upper layers.
|
|
103
|
-
@response_queue << error # unblock any waiting receive_response
|
|
104
|
-
@on_disconnect_callback&.call(error)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|