ruby-asterisk 0.1.0 → 1.0.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 +5 -5
- data/.claude/skills/ruby-asterisk-agi/SKILL.md +317 -0
- data/.claude/skills/ruby-asterisk-ami/SKILL.md +187 -0
- data/.claude/skills/ruby-asterisk-ari/SKILL.md +174 -0
- data/.claude/skills/ruby-asterisk-ari-websocket/SKILL.md +148 -0
- data/.github/workflows/ci.yml +79 -0
- data/.gitignore +50 -3
- data/.rubocop.yml +84 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +3 -1
- data/LICENCE +21 -0
- data/README.md +454 -70
- data/Rakefile +3 -1
- data/lib/ruby-asterisk/agi/protocol.rb +160 -0
- data/lib/ruby-asterisk/agi/server.rb +108 -0
- data/lib/ruby-asterisk/agi/session.rb +187 -0
- data/lib/ruby-asterisk/ami/client.rb +135 -0
- data/lib/ruby-asterisk/ami/commands/channel.rb +55 -0
- data/lib/ruby-asterisk/ami/commands/conference.rb +37 -0
- data/lib/ruby-asterisk/ami/commands/extension.rb +17 -0
- data/lib/ruby-asterisk/ami/commands/mailbox.rb +21 -0
- data/lib/ruby-asterisk/ami/commands/monitor.rb +33 -0
- data/lib/ruby-asterisk/ami/commands/queue.rb +39 -0
- data/lib/ruby-asterisk/ami/commands/sip.rb +25 -0
- data/lib/ruby-asterisk/ami/commands/system.rb +50 -0
- data/lib/ruby-asterisk/ami/event.rb +22 -0
- data/lib/ruby-asterisk/ami/event_list_aggregation.rb +82 -0
- data/lib/ruby-asterisk/ami/parser.rb +60 -0
- data/lib/ruby-asterisk/ami/promise.rb +108 -0
- data/lib/ruby-asterisk/ami/reactor.rb +162 -0
- data/lib/ruby-asterisk/ari/client.rb +94 -0
- data/lib/ruby-asterisk/ari/resources/base.rb +23 -0
- data/lib/ruby-asterisk/ari/resources/bridge.rb +22 -0
- data/lib/ruby-asterisk/ari/resources/channel.rb +26 -0
- data/lib/ruby-asterisk/ari/resources/collection.rb +27 -0
- data/lib/ruby-asterisk/ari/resources/endpoint.rb +22 -0
- data/lib/ruby-asterisk/ari/resources/playback.rb +18 -0
- data/lib/ruby-asterisk/ari/websocket/connection.rb +143 -0
- data/lib/ruby-asterisk/ari/websocket/event_handlers.rb +80 -0
- data/lib/ruby-asterisk/ari/websocket/heartbeat.rb +65 -0
- data/lib/ruby-asterisk/ari/websocket/reconnect.rb +54 -0
- data/lib/ruby-asterisk/ari/websocket/socket_adapter.rb +23 -0
- data/lib/ruby-asterisk/ari/websocket.rb +155 -0
- data/lib/ruby-asterisk/compat.rb +7 -0
- data/lib/ruby-asterisk/error.rb +10 -0
- data/lib/ruby-asterisk/parsing_constants.rb +100 -0
- data/lib/ruby-asterisk/request.rb +38 -20
- data/lib/ruby-asterisk/response.rb +50 -137
- data/lib/ruby-asterisk/response_builder.rb +18 -0
- data/lib/ruby-asterisk/response_parser.rb +43 -0
- data/lib/ruby-asterisk/version.rb +4 -2
- data/lib/ruby-asterisk.rb +5 -147
- data/ruby-asterisk.gemspec +26 -17
- data/spec/ruby-asterisk/agi/protocol_spec.rb +239 -0
- data/spec/ruby-asterisk/agi/server_spec.rb +199 -0
- data/spec/ruby-asterisk/agi/session_spec.rb +293 -0
- data/spec/ruby-asterisk/ami/async_client_spec.rb +256 -0
- data/spec/ruby-asterisk/ami/multi_event_spec.rb +57 -0
- data/spec/ruby-asterisk/ami/parser_spec.rb +190 -0
- data/spec/ruby-asterisk/ami/reactor_spec.rb +290 -0
- data/spec/ruby-asterisk/ami_client_spec.rb +68 -0
- data/spec/ruby-asterisk/ari/client_spec.rb +168 -0
- data/spec/ruby-asterisk/ari/resources/bridge_spec.rb +42 -0
- data/spec/ruby-asterisk/ari/resources/channel_spec.rb +57 -0
- data/spec/ruby-asterisk/ari/resources/collection_spec.rb +66 -0
- data/spec/ruby-asterisk/ari/resources/endpoint_spec.rb +30 -0
- data/spec/ruby-asterisk/ari/resources/playback_spec.rb +33 -0
- data/spec/ruby-asterisk/ari/websocket_integration_spec.rb +86 -0
- data/spec/ruby-asterisk/ari/websocket_spec.rb +374 -0
- data/spec/ruby-asterisk/immutability_spec.rb +148 -0
- data/spec/ruby-asterisk/request_spec.rb +29 -9
- data/spec/ruby-asterisk/response_spec.rb +147 -148
- data/spec/spec_helper.rb +14 -0
- data/spec/support/mock_ami_server.rb +50 -0
- data/spec/support/mock_ari_websocket_server.rb +114 -0
- metadata +177 -21
- data/CHANGELOG +0 -3
- data/spec/ruby-asterisk/ruby_asterisk_spec.rb +0 -150
|
@@ -1,185 +1,184 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'spec_helper'
|
|
3
4
|
describe RubyAsterisk::Response do
|
|
4
|
-
|
|
5
5
|
def sip_peers_response
|
|
6
6
|
"Response: Success
|
|
7
7
|
Message: Peer status list will follow
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
Event: PeerEntry
|
|
10
10
|
Channeltype: SIP
|
|
11
11
|
ObjectName: 9915057
|
|
12
|
-
IPport: 5060"
|
|
12
|
+
IPport: 5060\n\n"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def empty_core_show_channels_response
|
|
16
|
+
"Event: CoreShowChannelsComplete
|
|
17
|
+
EventList: Complete
|
|
18
|
+
ListItems: 1
|
|
19
|
+
ActionID: 839\n\n"
|
|
13
20
|
end
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
def core_show_channels_response
|
|
23
|
+
"Event: CoreShowChannel
|
|
24
|
+
ActionID: 839
|
|
25
|
+
Channel: SIP/195.62.226.4-00000025
|
|
26
|
+
UniqueID: 1335448133.61
|
|
27
|
+
Context: incoming
|
|
28
|
+
Extension: s
|
|
29
|
+
Priority: 1
|
|
30
|
+
ChannelState: 6
|
|
31
|
+
ChannelStateDesc: Up
|
|
32
|
+
Application: Parked Call
|
|
33
|
+
ApplicationData:
|
|
34
|
+
CallerIDnum: 123456
|
|
35
|
+
CallerIDname:
|
|
36
|
+
ConnectedLineNum:
|
|
37
|
+
ConnectedLineName:
|
|
38
|
+
Duration: 00:00:05
|
|
39
|
+
AccountCode:
|
|
40
|
+
BridgedChannel:
|
|
41
|
+
BridgedUniqueID:
|
|
42
|
+
|
|
43
|
+
Event: CoreShowChannelsComplete
|
|
44
|
+
EventList: Complete
|
|
18
45
|
ListItems: 1
|
|
19
|
-
ActionID: 839"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Admin: No
|
|
83
|
-
Role: Talk and listen
|
|
84
|
-
MarkedUser: No
|
|
85
|
-
Muted: No
|
|
86
|
-
Talking: Not monitored"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def extension_state_response
|
|
90
|
-
"Response: Success
|
|
91
|
-
ActionID: 180
|
|
92
|
-
Message: Extension Status
|
|
93
|
-
Exten: 9100
|
|
94
|
-
Context: HINT
|
|
95
|
-
Hint: SIP/9100
|
|
96
|
-
Status: 0"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
describe ".new" do
|
|
100
|
-
|
|
101
|
-
describe "receiving a Core Show Channels request" do
|
|
102
|
-
it "should parse correctly data coming from Asterisk about channels" do
|
|
103
|
-
@response = RubyAsterisk::Response.new("CoreShowChannels",core_show_channels_response)
|
|
104
|
-
@response.data[:channels].should_not be_empty
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
it "should correctly fill the fields" do
|
|
108
|
-
@response = RubyAsterisk::Response.new("CoreShowChannels",core_show_channels_response)
|
|
109
|
-
@response.data[:channels][0]["CallerIDnum"].should eq("123456")
|
|
46
|
+
ActionID: 839\n\n"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parked_calls_response
|
|
50
|
+
"Event: ParkedCall
|
|
51
|
+
Parkinglot: default
|
|
52
|
+
Exten: 701
|
|
53
|
+
Channel: SIP/195.62.226.2-00000026
|
|
54
|
+
From: SIP/195.62.226.2-00000026
|
|
55
|
+
Timeout: 3
|
|
56
|
+
CallerIDNum: 123456
|
|
57
|
+
CallerIDName:
|
|
58
|
+
ConnectedLineNum:
|
|
59
|
+
ConnectedLineName:
|
|
60
|
+
ActionID: 899\n\n"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def originate_response
|
|
64
|
+
"Event: Dial
|
|
65
|
+
Privilege: call,all
|
|
66
|
+
SubEvent: End
|
|
67
|
+
Channel: SIP/9100-0000002b
|
|
68
|
+
UniqueID: 1335457364.68
|
|
69
|
+
DialStatus: CHANUNAVAIL\n\n"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def meet_me_list_response
|
|
73
|
+
"Event: MeetmeList
|
|
74
|
+
ActionID: 921
|
|
75
|
+
Conference: 1234
|
|
76
|
+
UserNumber: 1
|
|
77
|
+
CallerIDNum: 123456
|
|
78
|
+
CallerIDName: <no name>
|
|
79
|
+
ConnectedLineNum: <unknown>
|
|
80
|
+
ConnectedLineName: <no name>
|
|
81
|
+
Channel: SIP/195.62.226.18-0000000e
|
|
82
|
+
Admin: No
|
|
83
|
+
Role: Talk and listen
|
|
84
|
+
MarkedUser: No
|
|
85
|
+
Muted: No
|
|
86
|
+
Talking: Not monitored\n\n"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def extension_state_response
|
|
90
|
+
"Response: Success
|
|
91
|
+
ActionID: 180
|
|
92
|
+
Message: Extension Status
|
|
93
|
+
Exten: 9100
|
|
94
|
+
Context: HINT
|
|
95
|
+
Hint: SIP/9100
|
|
96
|
+
Status: 0\n\n"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '.new' do
|
|
100
|
+
describe 'receiving a Core Show Channels request' do
|
|
101
|
+
it 'should parse correctly data coming from Asterisk about channels' do
|
|
102
|
+
@response = RubyAsterisk::Response.new('CoreShowChannels', [core_show_channels_response])
|
|
103
|
+
@response.data[:channels].should_not be_empty
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'should correctly fill the fields' do
|
|
107
|
+
@response = RubyAsterisk::Response.new('CoreShowChannels', [core_show_channels_response])
|
|
108
|
+
@response.data[:channels][0]['CallerIDnum'].should eq('123456')
|
|
110
109
|
end
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
@response = RubyAsterisk::Response.new(
|
|
111
|
+
it 'should have no channels if answer is empty' do
|
|
112
|
+
@response = RubyAsterisk::Response.new('CoreShowChannels', [empty_core_show_channels_response])
|
|
113
|
+
@response.data[:channels].count.should eq(0)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe 'receiving a Parked Calls request' do
|
|
118
|
+
it 'should parse correctly data coming from Asterisk about calls' do
|
|
119
|
+
@response = RubyAsterisk::Response.new('ParkedCalls', [parked_calls_response])
|
|
120
|
+
@response.data[:calls].should_not be_empty
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'should correctly fill the fields' do
|
|
124
|
+
@response = RubyAsterisk::Response.new('ParkedCalls', [parked_calls_response])
|
|
125
|
+
@response.data[:calls][0]['Exten'].should eq('701')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe 'receiving a Originate request' do
|
|
130
|
+
it 'should parse correctly data coming from Asterisk about the call' do
|
|
131
|
+
@response = RubyAsterisk::Response.new('Originate', [originate_response])
|
|
133
132
|
@response.data[:dial].should_not be_empty
|
|
134
133
|
end
|
|
135
134
|
|
|
136
|
-
it
|
|
137
|
-
@response = RubyAsterisk::Response.new(
|
|
138
|
-
@response.data[:dial][0][
|
|
135
|
+
it 'should correctly fill the fields' do
|
|
136
|
+
@response = RubyAsterisk::Response.new('Originate', [originate_response])
|
|
137
|
+
@response.data[:dial][0]['UniqueID'].should eq('1335457364.68')
|
|
139
138
|
end
|
|
140
139
|
|
|
141
|
-
it
|
|
142
|
-
@response = RubyAsterisk::Response.new(
|
|
143
|
-
@response.raw_response.should eq(originate_response)
|
|
140
|
+
it 'should have a field with original raw response' do
|
|
141
|
+
@response = RubyAsterisk::Response.new('Originate', [originate_response])
|
|
142
|
+
@response.raw_response.join.should eq(originate_response)
|
|
144
143
|
end
|
|
145
144
|
end
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
it
|
|
149
|
-
@response = RubyAsterisk::Response.new(
|
|
145
|
+
|
|
146
|
+
describe 'receiving a MeetMeList request' do
|
|
147
|
+
it 'should parse correctly data coming from Asterisk about the conference room' do
|
|
148
|
+
@response = RubyAsterisk::Response.new('MeetMeList', [meet_me_list_response])
|
|
150
149
|
@response.data[:rooms].should_not be_empty
|
|
151
150
|
end
|
|
152
151
|
|
|
153
|
-
it
|
|
154
|
-
@response = RubyAsterisk::Response.new(
|
|
155
|
-
@response.data[:rooms][0][
|
|
152
|
+
it 'should correctly fill the fields' do
|
|
153
|
+
@response = RubyAsterisk::Response.new('MeetMeList', [meet_me_list_response])
|
|
154
|
+
@response.data[:rooms][0]['Conference'].should eq('1234')
|
|
156
155
|
end
|
|
157
156
|
end
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
it
|
|
161
|
-
@response = RubyAsterisk::Response.new(
|
|
158
|
+
describe 'receiving a ExtensionState request' do
|
|
159
|
+
it 'should parse correctly data coming from Asterisk about the state of the extension' do
|
|
160
|
+
@response = RubyAsterisk::Response.new('ExtensionState', [extension_state_response])
|
|
162
161
|
@response.data[:hints].should_not be_empty
|
|
163
162
|
end
|
|
164
163
|
|
|
165
|
-
it
|
|
166
|
-
@response = RubyAsterisk::Response.new(
|
|
167
|
-
@response.data[:hints][0][
|
|
168
|
-
|
|
164
|
+
it 'should correctly fill the fields' do
|
|
165
|
+
@response = RubyAsterisk::Response.new('ExtensionState', [extension_state_response])
|
|
166
|
+
@response.data[:hints][0]['Status'].should eq('0')
|
|
167
|
+
@response.data[:hints][0]['DescriptiveStatus'].should eq('Idle')
|
|
169
168
|
end
|
|
170
169
|
end
|
|
171
|
-
|
|
170
|
+
end
|
|
172
171
|
|
|
173
|
-
describe
|
|
174
|
-
it
|
|
175
|
-
@response = RubyAsterisk::Response.new(
|
|
172
|
+
describe 'receiving a SIPPeers request' do
|
|
173
|
+
it 'should parse correctly data coming from Asterisk abous sip peers' do
|
|
174
|
+
@response = RubyAsterisk::Response.new('SIPpeers', [sip_peers_response])
|
|
176
175
|
@response.data[:peers].should_not be_empty
|
|
177
176
|
end
|
|
178
|
-
|
|
179
|
-
it
|
|
180
|
-
@response = RubyAsterisk::Response.new(
|
|
181
|
-
@response.data[:peers][0][
|
|
182
|
-
@response.data[:peers][0][
|
|
177
|
+
|
|
178
|
+
it 'should correctly fill the fields' do
|
|
179
|
+
@response = RubyAsterisk::Response.new('SIPpeers', [sip_peers_response])
|
|
180
|
+
@response.data[:peers][0]['ObjectName'].should eq('9915057')
|
|
181
|
+
@response.data[:peers][0]['IPport'].should eq('5060')
|
|
183
182
|
end
|
|
184
183
|
end
|
|
185
184
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'simplecov'
|
|
4
|
+
SimpleCov.start
|
|
1
5
|
require 'rubygems'
|
|
2
6
|
require 'bundler'
|
|
3
7
|
|
|
4
8
|
require 'ruby-asterisk'
|
|
9
|
+
|
|
10
|
+
RSpec.configure do |config|
|
|
11
|
+
config.expect_with :rspec do |c|
|
|
12
|
+
c.syntax = %i[should expect]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
config.after(:suite) do
|
|
16
|
+
puts '[CI] Done.'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
|
|
5
|
+
module MockAMIServer
|
|
6
|
+
def self.handle_client(client, &block)
|
|
7
|
+
client.puts 'Asterisk Call Manager/1.1'
|
|
8
|
+
|
|
9
|
+
if block
|
|
10
|
+
yield(client)
|
|
11
|
+
else
|
|
12
|
+
while (line = client.gets)
|
|
13
|
+
client.puts "Echo: #{line}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
puts "Client error: #{e.message}"
|
|
18
|
+
ensure
|
|
19
|
+
begin
|
|
20
|
+
client.close
|
|
21
|
+
rescue StandardError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
puts 'Client disconnected'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.start(port = 0, &block)
|
|
28
|
+
server = TCPServer.new('localhost', port)
|
|
29
|
+
assigned_port = server.addr[1]
|
|
30
|
+
|
|
31
|
+
client_threads = []
|
|
32
|
+
server_thread = Thread.new do
|
|
33
|
+
loop do
|
|
34
|
+
client = server.accept
|
|
35
|
+
puts "Client connected: #{client.peeraddr.inspect}"
|
|
36
|
+
client_threads << Thread.new(client) { |c| handle_client(c, &block) }
|
|
37
|
+
end
|
|
38
|
+
rescue IOError, Errno::EBADF # Server closed
|
|
39
|
+
ensure
|
|
40
|
+
begin
|
|
41
|
+
server.close
|
|
42
|
+
rescue StandardError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
client_threads.each(&:kill)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
[server_thread, assigned_port]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'websocket/driver'
|
|
6
|
+
|
|
7
|
+
# Minimal in-process WebSocket server used by ARI::WebSocket integration specs.
|
|
8
|
+
# Accepts connections on an ephemeral port, completes the WebSocket handshake
|
|
9
|
+
# with websocket-driver in server mode, and records what clients send.
|
|
10
|
+
class MockAriWebSocketServer
|
|
11
|
+
# websocket-driver server adapter: it only needs #write
|
|
12
|
+
class ServerAdapter
|
|
13
|
+
def initialize(io)
|
|
14
|
+
@io = io
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def write(data)
|
|
18
|
+
@io.write(data)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ClientConnection = Struct.new(:socket, :driver)
|
|
23
|
+
|
|
24
|
+
attr_reader :port, :messages, :pings, :connections
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@server = TCPServer.new('127.0.0.1', 0)
|
|
28
|
+
@port = @server.addr[1]
|
|
29
|
+
@messages = Queue.new # text frames received from clients
|
|
30
|
+
@pings = Queue.new # ping frames received from clients
|
|
31
|
+
@connections = Queue.new # one entry per completed handshake
|
|
32
|
+
@clients = []
|
|
33
|
+
@clients_mutex = Mutex.new
|
|
34
|
+
@accept_thread = Thread.new { accept_loop }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Send an event (Hash) to every connected client
|
|
38
|
+
def broadcast(event)
|
|
39
|
+
data = JSON.generate(event)
|
|
40
|
+
each_client { |client| client.driver.text(data) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Close every client socket abruptly (no close frame), simulating a drop
|
|
44
|
+
def drop_clients
|
|
45
|
+
each_client { |client| client.socket.close }
|
|
46
|
+
@clients_mutex.synchronize { @clients.clear }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stop
|
|
50
|
+
@server.close
|
|
51
|
+
@accept_thread.join(1)
|
|
52
|
+
drop_clients
|
|
53
|
+
rescue IOError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Pop from one of the queues, failing the spec on timeout
|
|
58
|
+
def self.pop(queue, timeout = 5)
|
|
59
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
60
|
+
loop do
|
|
61
|
+
return queue.pop(true)
|
|
62
|
+
rescue ThreadError
|
|
63
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
64
|
+
raise "timed out waiting for queue after #{timeout}s"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sleep 0.01
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def accept_loop
|
|
74
|
+
loop do
|
|
75
|
+
socket = @server.accept
|
|
76
|
+
Thread.new { handle_client(socket) }
|
|
77
|
+
end
|
|
78
|
+
rescue IOError, SystemCallError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def handle_client(socket)
|
|
83
|
+
driver = ::WebSocket::Driver.server(ServerAdapter.new(socket))
|
|
84
|
+
client = ClientConnection.new(socket, driver)
|
|
85
|
+
@clients_mutex.synchronize { @clients << client }
|
|
86
|
+
|
|
87
|
+
setup_driver(driver, client)
|
|
88
|
+
driver.parse(socket.readpartial(4096)) until socket.closed?
|
|
89
|
+
rescue IOError, SystemCallError
|
|
90
|
+
nil
|
|
91
|
+
ensure
|
|
92
|
+
begin
|
|
93
|
+
socket.close
|
|
94
|
+
rescue StandardError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def setup_driver(driver, client)
|
|
100
|
+
driver.on(:connect) { driver.start if ::WebSocket::Driver.websocket?(driver.env) }
|
|
101
|
+
driver.on(:open) { @connections << client }
|
|
102
|
+
driver.on(:message) { |event| @messages << event.data }
|
|
103
|
+
driver.on(:ping) { |event| @pings << event }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def each_client
|
|
107
|
+
clients = @clients_mutex.synchronize { @clients.dup }
|
|
108
|
+
clients.each do |client|
|
|
109
|
+
yield client
|
|
110
|
+
rescue IOError, SystemCallError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|