easel-dashboard 0.4.3 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15a1dde769e8fa78c0bf23bb6de115bb8f915f0192a672333f7b6d8e2093b2cd
4
- data.tar.gz: 1c95d1f20659325da69b2fea9454ce503e278f078b4b6fab3445ad5389bfa67b
3
+ metadata.gz: 45b20ce9712ac3e87e83e527b03665b9c64a9b707f571cfcbbd1c0989df494e8
4
+ data.tar.gz: bebb6eeafa225d2a05401fea5f8f8ec7e63674de617a811ec8b6cb2cf7a56640
5
5
  SHA512:
6
- metadata.gz: 4be4da518caa592bdff010e27ca75d933af403b96958c735f0f33b7502070ccc5f5cc3b5d8df39c1210fca381233559fc594129b7255b3a543fe8eca622b3a98
7
- data.tar.gz: 9090eafe766e57975307af322120047931e733534cb0c855dc62baa80fe320477c7001da1e3f5792c363f96756a059ed22a315cb2ee000b3002032413a7aef5a
6
+ metadata.gz: c50fd692b5fcd3826cc6b3ac80aa400e1319501303f951b576c9c4ada4fe43990a383ed7f84e70d2808363a77dd04e1f07e6b0a2218608309bbed2681a42382b
7
+ data.tar.gz: d1353d563bf9b9306ebbf5bb69e7a4b401558b4be77010320b7b734808e74124bdbfaecd12890a61a400a2432ee5d16536c909fa9c4c6f2d13885a488465f788
data/bin/easel CHANGED
@@ -1,4 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'easel'
3
-
4
3
  launch
@@ -29,20 +29,55 @@ def build_app
29
29
  page
30
30
  end
31
31
 
32
+ # build_js
33
+ #
34
+ #
35
+ def build_js
36
+ js_erb = File.new("#{File.dirname(__FILE__)}/../html/controller.js.erb").read
37
+ page = ERB.new(js_erb).result()
32
38
 
33
- def build_error code
34
- error_erb = File.new("#{File.dirname(__FILE__)}/../html/error.html.erb").read
35
- page = ERB.new(error_erb).result(binding)
39
+ "HTTP/1.1 200 OK\r\n" +
40
+ "Content-Type: text/javascript; charset=UTF-8\r\n" +
41
+ "Content-Length: #{page.bytesize}\r\n" +
42
+ "Connection: close\r\n" +
43
+ "\r\n" +
44
+ page
45
+ end
36
46
 
37
- "HTTP/1.1 #{code} #{@code_names[code]}\r\n" +
38
- "Content-Type: text/html; charset=UTF-8\r\n" +
47
+
48
+ # return_js
49
+ #
50
+ #
51
+ def return_js file
52
+
53
+ page = File.new("#{File.dirname(__FILE__)}/../html/#{file}").read
54
+
55
+ "HTTP/1.1 200 OK\r\n" +
56
+ "Content-Type: text/javascript; charset=UTF-8\r\n" +
39
57
  "Content-Length: #{page.bytesize}\r\n" +
40
58
  "Connection: close\r\n" +
41
59
  "\r\n" +
42
60
  page
43
61
  end
44
62
 
63
+ # return_html
64
+ #
65
+ #
66
+ def return_html file
67
+
68
+ page = File.new("#{File.dirname(__FILE__)}/../html/#{file}").read
45
69
 
70
+ "HTTP/1.1 200 OK\r\n" +
71
+ "Content-Type: text/html; charset=UTF-8\r\n" +
72
+ "Content-Length: #{page.bytesize}\r\n" +
73
+ "Connection: close\r\n" +
74
+ "\r\n" +
75
+ page
76
+ end
77
+
78
+ # build_css
79
+ #
80
+ #
46
81
  def build_css
47
82
  error_erb = File.new("#{File.dirname(__FILE__)}/../html/app.css.erb").read
48
83
  css = ERB.new(error_erb).result(binding)
@@ -54,3 +89,19 @@ def build_css
54
89
  "\r\n" +
55
90
  css
56
91
  end
92
+
93
+
94
+ # build_error
95
+ #
96
+ #
97
+ def build_error code
98
+ error_erb = File.new("#{File.dirname(__FILE__)}/../html/error.html.erb").read
99
+ page = ERB.new(error_erb).result(binding)
100
+
101
+ "HTTP/1.1 #{code} #{@code_names[code]}\r\n" +
102
+ "Content-Type: text/html; charset=UTF-8\r\n" +
103
+ "Content-Length: #{page.bytesize}\r\n" +
104
+ "Connection: close\r\n" +
105
+ "\r\n" +
106
+ page
107
+ end
@@ -11,11 +11,13 @@
11
11
  # Global Variables
12
12
  $config = {
13
13
  logging: 2, # 0=Fatal, 1=Error, 2=Warning, 3=Info
14
- port: 4200, # Default port
15
- hostname: 'localhost', # Default hostname
16
- log_file: STDOUT, # Default logging to STDOUT
17
- title: 'Easel - Your Custom Dashboard',
18
- colours: {
14
+ port: 4200, # Port # to bind Easel to
15
+ hostname: 'localhost', # Hostname to accept. "" Accepts all connections.
16
+ log_file: STDOUT, # Where to write the logs to.
17
+ title: 'Easel - Your Custom Dashboard', # The title of the dashboard webpage.
18
+ header_logo: '', # TODO: have nil mean default to Easel's, otherwise put in src=""
19
+ header_title: '<a class="on-hover-secondary" href="https://easeldashboard.com">Easel Dashboard</a>', # TODO: the same as the logo line basically.
20
+ colours: { # The RGB values for the dashboard. TODO: accept hsl in HTML format.
19
21
  surface: '#222222',
20
22
  background: '#000000',
21
23
  primary: '#7DF9FF',
@@ -28,7 +30,7 @@ $config = {
28
30
  stdout_colour: '#ffffff',
29
31
  stderr_colour: '#00FF00'
30
32
  },
31
- commands: [
33
+ commands: [ # A list of commands to allow the user to run via Easel.
32
34
  {
33
35
  name: 'Test 1',
34
36
  cmd: 'echo "this is the output of Test 1"',
@@ -39,5 +41,51 @@ $config = {
39
41
  cmd: 'echo "this is the output of Test 2"',
40
42
  desc: 'Simple output test #2'
41
43
  }
44
+ ],
45
+ collect_data_period: 15,
46
+ dashboards: [ # A list of dashboards to display
47
+ {
48
+ name: "Server Status",
49
+ desc: "Basic server status.",
50
+ id: "A", # TODO: dashboard and element IDs should be automatically added.
51
+ elements: [
52
+ {
53
+ name: "CPU Load",
54
+ type: "time-series", # uses Time.now.strftime("%H:%M") as x asix.
55
+ data: [
56
+ {
57
+ cmd: "uptime",
58
+ name: "1min Average",
59
+ regex: "average: (\\d+.\\d+)"
60
+ },
61
+ {
62
+ cmd: "uptime",
63
+ name: "5min Average",
64
+ regex: ", (\\d+.\\d+),"
65
+ },{
66
+ cmd: "uptime",
67
+ name: "15min Average",
68
+ regex: ", (\\d+.\\d+)\\n"
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ name: "Memory",
74
+ type: "time-series", # uses Time.now.strftime("%H:%M") as x asix.
75
+ data: [
76
+ {
77
+ cmd: "free",
78
+ name: "Total Memory",
79
+ regex: "Mem:\\W+(\\d+)"
80
+ },
81
+ {
82
+ cmd: "free",
83
+ name: "Free Memory",
84
+ regex: "Mem:\\W+\\d+\\W+(\\d+)"
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ }
42
90
  ]
43
91
  }
@@ -0,0 +1,21 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+
5
+ # Imports
6
+ require_relative './configuration'
7
+
8
+
9
+ def launch_easel config
10
+
11
+ # r_server_data = Ractor.new config do
12
+ # 'this is a message' # TODO: Implement this.
13
+ # end
14
+
15
+ tcp_listener = Ractor.new(config) do |config|
16
+ launch_server config
17
+ end
18
+
19
+ tcp_listener.take
20
+
21
+ end
@@ -0,0 +1,144 @@
1
+ #!/snap/bin/ruby
2
+ #
3
+ # Author: Eric Power
4
+ #
5
+ # Description:
6
+ # Collects information about the system to display via the Easel dashboard.
7
+ # Uses semaphores and mutexes to provide atomic reads and writes. Allows a
8
+ # single writer at a time, but any number of readers can read at once.
9
+ #
10
+ # WARNING: this feature currently holds all information in memory, so don't
11
+ # turn this on if you're expecting to leave Easel running for a long time.
12
+ #
13
+ # Plan for adressing this:
14
+ # - Move 'historical' data to a proper datastore (eg. a SQLite database)
15
+ # - Have only the most recent readings in @collected_data, to allow the
16
+ # websocket threads to pull the up to date data from there.
17
+ # - Eventually allow the clients to query the historial datastore, so it's
18
+ # likely that the best bet is to have the database store the data with
19
+ # the time as the dense index.
20
+
21
+
22
+ # Imports
23
+ require 'thread'
24
+ require 'concurrent'
25
+
26
+ # Key Variables
27
+ @collected_data = {}
28
+ @writers_semaphore = Concurrent::Semaphore.new(0) # Used to count currently active writers
29
+ @readers_semaphore = Concurrent::Semaphore.new(0) # Used to count currently active readers
30
+ @join_mutex = Mutex.new
31
+
32
+ # launch_data_collection
33
+ #
34
+ # Launch a background thread to start collecting system info in the background.
35
+ def launch_data_collection
36
+ Thread.new do
37
+ loop do
38
+ collect_data
39
+ sleep $config[:collect_data_period]
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ # collect_data
46
+ #
47
+ # Collects information on the current state of the system, and writes it to
48
+ # @collected_data.
49
+ def collect_data
50
+ new_data = {}
51
+
52
+ # TODO: Check which element type something is to figure out how to handle it.
53
+
54
+ log_info "Collecting data."
55
+ $config[:dashboards].each{ |dashboard|
56
+
57
+ new_data[dashboard[:id]] = {}
58
+ dashboard[:elements].each_with_index{ |element, e_index|
59
+ new_data[dashboard[:id]][e_index] = {}
60
+
61
+ element[:data].each_with_index{ |data, index|
62
+ output = `#{data[:cmd]}`
63
+ log_info "Ran `#{data[:cmd]}`, got: #{output}"
64
+ begin
65
+ value = output.match(/#{data[:regex]}/)[1]
66
+ rescue NoMethodError => e
67
+ log_error "Data failed to be parsed. Regex: /#{data[:regex]}/ -- Output: #{output}"
68
+ end
69
+ new_data[dashboard[:id]][e_index][index] = [
70
+ Time.new.strftime("%H:%M:%S"),
71
+ value
72
+ ]
73
+ }
74
+ }
75
+ }
76
+ write_data new_data
77
+ end
78
+
79
+
80
+ # write_data
81
+ #
82
+ # Writes the data collected to @collected_data, and handles the Readers/Writer
83
+ # problem by using semaphores and a mutex.
84
+ def write_data data
85
+
86
+ joined = false
87
+ until joined
88
+ @join_mutex.synchronize {
89
+ if @readers_semaphore.available_permits == 0 and @writers_semaphore.available_permits == 0
90
+ @writers_semaphore.release 1 # Increment @writers_semaphore
91
+ joined = true
92
+ end
93
+ }
94
+ sleep 0.05 # Wait 50ms to give another thread time to lock the @join_mutex.
95
+ end
96
+
97
+ # Write Data
98
+ data.each_key { |key|
99
+ case key
100
+ when :load
101
+ @collected_data[:load] = [] if @collected_data[:load].nil?
102
+ @collected_data[:load] << data[:load]
103
+ else
104
+ @collected_data[key] = data[key]
105
+ end
106
+ }
107
+
108
+ # Log if @collected_data has gotten too large.
109
+ if not @collected_data[:load].nil? and @collected_data[:load].length > 240
110
+ if @collected_data[:load].length > 1000
111
+ log_error "Easel dashboard has run collect_data more than a 1000 times. Memory size likely to be large."
112
+ else
113
+ log_warning "Easel dashboard starting to take up a lot of memory due to data_collection being turned on."
114
+ end
115
+ end
116
+
117
+ @writers_semaphore.acquire 1 # Decrement @writers_semaphore
118
+ end
119
+
120
+
121
+ # read_data
122
+ #
123
+ # Reads (copies) @collected_data, and handles the Readers/Writer
124
+ # problem by using semaphores and a mutex. Returns a copy
125
+ def read_data
126
+
127
+ joined = false
128
+ until joined
129
+ @join_mutex.synchronize {
130
+ if @writers_semaphore.available_permits == 0
131
+ @readers_semaphore.release 1 # Increment @readers_semaphore
132
+ # TODO: likely need to release the mutex.
133
+ joined = true
134
+ end
135
+ }
136
+ sleep 0.05 unless joined # Wait 50ms to give another thread time to lock the @join_mutex.
137
+ end
138
+
139
+ # Read Data
140
+ data = @collected_data.dup
141
+
142
+ @readers_semaphore.acquire 1 # Decrement @readers_semaphore
143
+ data
144
+ end
data/lib/easel/server.rb CHANGED
@@ -7,8 +7,9 @@
7
7
 
8
8
  # Imports
9
9
  require 'socket'
10
- require_relative './build_pages.rb'
10
+ require_relative './build_pages'
11
11
  require_relative './websocket'
12
+ require_relative './data_gathering'
12
13
 
13
14
 
14
15
  def launch_server
@@ -20,6 +21,9 @@ def launch_server
20
21
  log_fatal "Server could not start. Error message: #{e}"
21
22
  end
22
23
 
24
+ # Lauch data collection if turned on.
25
+ launch_data_collection unless $config[:collect_data_period] == 0
26
+
23
27
  Thread.abort_on_exception = true
24
28
 
25
29
  # Main Loop
@@ -29,10 +33,10 @@ def launch_server
29
33
  handle_request client
30
34
  end
31
35
  }
32
-
33
- # Handle shutting down.
34
- rescue Interrupt
36
+ rescue Interrupt # Handle shutting down.
35
37
  log_info "Interrupt received, server shutting down..."
38
+ rescue Exception => e
39
+ log_error "Unexpected error occured and closed client connection. Error: #{e}"
36
40
  end
37
41
  end
38
42
 
@@ -55,7 +59,6 @@ def handle_request socket
55
59
  # TODO: Deal with HEAD request. https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
56
60
  else
57
61
  # TODO: respond with an appropriate error.
58
- response "I don't understand what you sent - go away."
59
62
  socket.print "HTTP/1.1 200 OK\r\n" +
60
63
  "Content-Type: text/plain\r\n" +
61
64
  "Content-Length: #{response.bytesize}\r\n" +
@@ -78,9 +81,22 @@ def handle_get(socket, request)
78
81
  when "/", "/index.html"
79
82
  socket.print build_app
80
83
  socket.close
84
+ when "/test.html" # TODO: Remove this!
85
+ socket.print return_html "test.html"
86
+ socket.close
81
87
  when "/app.css"
82
88
  socket.print build_css
83
89
  socket.close
90
+ when "/controller.js"
91
+ log_info "building controller"
92
+ socket.print build_js
93
+ socket.close
94
+ when "/dashboardElements.js"
95
+ socket.print return_js 'dashboardElements.js'
96
+ socket.close
97
+ when "/createComponents.js"
98
+ socket.print return_js 'createComponents.js'
99
+ socket.close
84
100
  else
85
101
  socket.print build_error 404
86
102
  socket.close
@@ -102,7 +118,6 @@ def read_HTTP_message socket
102
118
  end
103
119
  end
104
120
 
105
-
106
121
  request = {fields: {}}
107
122
  (request[:method], request[:url], request[:protocol]) = message[0].split(" ")
108
123
 
@@ -9,14 +9,66 @@
9
9
  #
10
10
  # Note: Websocket code is based on the code provided in the following article:
11
11
  # https://www.honeybadger.io/blog/building-a-simple-websockets-server-from-scratch-in-ruby/
12
+ #
13
+ # Overview of the websocket protocol:
14
+ # Communication over the websocket is split into two message types:
15
+ #
16
+ # 1. Command specific messages
17
+ # These messages are sent from either the client or the server, and
18
+ # are associated with a particular command via an ID. This ID is set
19
+ # by the server when building a page.
20
+ #
21
+ # The server sends:
22
+ # - ID:OUT:XXXXXXX where the contents of XXXX is new content in
23
+ # the stdOUT of the command that's associated with ID.
24
+ # - ID:ERR:XXXXXXX where the contents of XXXX is new content in
25
+ # the stdERR of the command that's associated with ID.
26
+ # - ID:FINISHED tells the client that the command associated with
27
+ # ID has finished running.
28
+ # - ID:CLEAR tells the client that the command associated with ID
29
+ # wants to clear the current output. This is useful when
30
+ # handling X-term escape sequences (eg. `top` wants a fresh page)
31
+ # The client sends:
32
+ # - ID:RUN requests that the server start running the command
33
+ # associated with ID.
34
+ # - ID:STOP requests that the server stop running the command
35
+ # associated with ID.
36
+ #
37
+ # 2. Dashboard Information
38
+ # These messages let the client indicate what information it wants to
39
+ # handle, and let the server send that content to the client either
40
+ # as a blob (if the information is historical) or as a stream (if
41
+ # the information is real time). When the client requests data, it
42
+ # includes a DID. The difference between the DID and an ID that
43
+ # is associated with a command is that the DID contains a letter
44
+ # at the start (eg. A12).
45
+ #
46
+ # DIDs are structured as a letter that represents a dashboard, and
47
+ # a number that represents the element on the dashboard.
48
+ #
49
+ # The server sends:
50
+ # - DID:Y:XXXXXXXX sends data that should be connected to the
51
+ # DID. The Y is either an A (representing "ALL") to say that the
52
+ # message is a self-contained data point, or a ratio like (1/2)
53
+ # to say its the first of two messages that when combined form
54
+ # a data point.
55
+ #
56
+ # At this point, the client does not send anything related to DIDs.
57
+ # Options in the future include letting the client request a range of
58
+ # data, but that will likely wait until the data on the server end
59
+ # is moved out of memory an on to disk.
60
+
12
61
 
13
62
 
14
63
  # Imports
15
- require 'digest'
16
- require 'open3'
64
+ require 'digest' # Allows hashing of websocket authentication value
65
+ require 'open3' # Allows capturing stdout and stderr of system commands
66
+ require 'thread' # Allows use of mutexes.
67
+
68
+ require_relative 'data_gathering'
17
69
 
18
70
  # Key Variables
19
- MAX_WS_FRAME_SIZE = 50.0
71
+ MAX_WS_FRAME_SIZE = 50.0 # Must be a float number to allow a non-truncated division result.
20
72
 
21
73
  # run_websocket
22
74
  #
@@ -25,6 +77,15 @@ def run_websocket(socket, initial_request)
25
77
 
26
78
  accept_connection(socket, initial_request[:fields][:Sec_WebSocket_Key][0..-3])
27
79
  child_threads = {}
80
+ send_msg_mutex = Mutex.new # One mutex per websocket to control sending messages.
81
+
82
+ Thread.new { # Periodically update the generic dashboard if set.
83
+ loop do
84
+ data = read_data
85
+ send_msg(socket, send_msg_mutex, nil, "DASH", data)
86
+ sleep $config[:collect_data_period]
87
+ end
88
+ } unless $config[:collect_data_period] == 0
28
89
 
29
90
  loop {
30
91
  msg = receive_msg socket
@@ -36,10 +97,11 @@ def run_websocket(socket, initial_request)
36
97
 
37
98
  unless child_threads[cmd_id]
38
99
  child_threads[cmd_id] = Thread.new do
39
- run_command_and_stream(socket, cmd_id)
100
+ run_command_and_stream(socket, cmd_id, send_msg_mutex)
40
101
  child_threads[cmd_id] = nil
41
102
  end
42
103
  end
104
+
43
105
  when "STOP"
44
106
 
45
107
  cmd_id = msg.match(/^STOP:(.*)$/)[1].to_i
@@ -59,7 +121,7 @@ end
59
121
  # run_command_and_stream
60
122
  #
61
123
  # Run a command and stream the stdout and stderr through the websocket.
62
- def run_command_and_stream(socket, cmd_id)
124
+ def run_command_and_stream(socket, cmd_id, send_msg_mutex)
63
125
 
64
126
  cmd = get_command cmd_id
65
127
  if cmd.nil?
@@ -78,9 +140,9 @@ def run_command_and_stream(socket, cmd_id)
78
140
  break
79
141
  end
80
142
  if fd == stdout
81
- send_msg(socket, cmd_id, "OUT", resp, )
143
+ send_msg(socket, send_msg_mutex, cmd_id, "OUT", resp)
82
144
  elsif fd == stderr
83
- send_msg(socket, cmd_id, "ERR", resp, )
145
+ send_msg(socket, send_msg_mutex, cmd_id, "ERR", resp)
84
146
  else
85
147
  raise "Received output from popen3(#{cmd}) that was not via stdout or stderr."
86
148
  end
@@ -89,7 +151,7 @@ def run_command_and_stream(socket, cmd_id)
89
151
  end
90
152
 
91
153
  cmd_thread.join
92
- send_msg(socket, cmd_id, "FINISHED")
154
+ send_msg(socket, send_msg_mutex, cmd_id, "FINISHED")
93
155
  end
94
156
  end
95
157
 
@@ -161,47 +223,91 @@ end
161
223
  # send_msg
162
224
  #
163
225
  #
164
- def send_msg(socket, cmd_id, msg_type, msg=nil)
226
+ def send_msg(socket, send_msg_mutex, cmd_id, msg_type, msg=nil)
165
227
 
166
228
 
167
- # TODO: Figure out the proper frame size (MAX_WS_FRAME_SIZE).
168
- def send_frame(socket, fmsg)
169
- output = [0b10000001, fmsg.size, fmsg]
170
- socket.write output.pack("CCA#{fmsg.size}")
171
- end
172
-
173
229
  case msg_type
174
- when "OUT", "ERR"
230
+ when "OUT", "ERR" # See comments at the top of the file to explain this part of the protocol.
175
231
  header = "#{cmd_id}:#{msg_type}:"
176
232
  if header.length > MAX_WS_FRAME_SIZE
177
233
  log_error "Message header '#{msg_type}' is too long. Msg: #{msg}."
178
234
  elsif msg.nil?
179
235
  log_error "Message of type '#{msg_type}' sent without a message."
180
236
  else
181
- if msg.length > MAX_WS_FRAME_SIZE - header.length
182
- msg_part_len = MAX_WS_FRAME_SIZE - header.length
183
- msg_parts = (0..(msg.length-1)/msg_part_len).map{ |i|
184
- msg[i*msg_part_len,msg_part_len]
185
- }
186
- msg_parts.each{ |part|
187
- send_frame(socket, header + part)
188
- }
189
- else
190
- send_frame(socket, header + msg)
191
- end
237
+ send_msg_mutex.synchronize {
238
+ if msg.length > MAX_WS_FRAME_SIZE - header.length
239
+ msg_part_len = MAX_WS_FRAME_SIZE - header.length
240
+ msg_parts = (0..(msg.length-1)/msg_part_len).map{ |i|
241
+ msg[i*msg_part_len,msg_part_len]
242
+ }
243
+ msg_parts.each{ |part|
244
+ send_frame(socket, header + part)
245
+ }
246
+ else
247
+ send_frame(socket, header + msg)
248
+ end
249
+ }
192
250
  end
193
251
 
252
+ when "DASH" # See comments at the top of the file to explain this part of the protocol.
253
+ if msg.nil?
254
+ log_error "Message of type '#{msg_type}' sent without a message."
255
+ end
256
+ msg.each_key { |dash_id|
257
+ msg[dash_id].each_key { |ele_index|
258
+ did = "#{dash_id}#{ele_index}"
259
+ send_msg_mutex.synchronize {
260
+ msg[dash_id][ele_index].each_key { |key|
261
+ data_fragment = "#{key}->#{msg[dash_id][ele_index][key]}"
262
+ if data_fragment.length > MAX_WS_FRAME_SIZE - (did.length + 3)
263
+ msg_part_len = MAX_WS_FRAME_SIZE - (did.length + 7) # TODO: Handle case where header is longer than DID:XX/XX:
264
+ msg_parts = (0..(data_fragment.length-1)/msg_part_len).map{ |i|
265
+ data_fragment[i*msg_part_len,msg_part_len]
266
+ }
267
+ msg_parts.each_with_index{ |part, index|
268
+ header = did + ":#{index + 1}/#{msg_parts.length}:"
269
+ if header.length > MAX_WS_FRAME_SIZE
270
+ log_error "Message header '#{msg_type}' is too long. Data: #{data_fragment}."
271
+ end
272
+ send_frame(socket, header + part)
273
+ }
274
+ else
275
+ send_frame(socket, did + ":A:" + data_fragment)
276
+ end
277
+ }
278
+ }
279
+ }
280
+ }
281
+
194
282
  when "CLEAR", "FINISHED"
195
- to_send = "#{cmd_id}:#{msg_type}"
196
- if to_send.length > MAX_WS_FRAME_SIZE
197
- log_error "Message of type '#{msg_type}' is too long. Msg: #{to_send}."
198
- elsif !msg.nil?
199
- log_error "Message of type '#{msg_type}' passed a message. Msg: #{msg}."
200
- else
201
- send_frame(socket, to_send)
283
+ if !msg.nil?
284
+ log_error "Message of type '#{msg_type}' passed an empty message. Msg: #{msg}."
202
285
  end
286
+ to_send = "#{cmd_id}:#{msg_type}"
287
+ send_msg_mutex.synchronize {
288
+ if to_send.length > MAX_WS_FRAME_SIZE
289
+ log_error "Message of type '#{msg_type}' is too long. Msg: #{to_send}."
290
+ else
291
+ send_frame(socket, to_send)
292
+ end
293
+ }
203
294
  else
204
295
  log_error "Trying to send a websocket message with unrecognized type: #{msg_type}"
205
296
  end
206
297
 
207
298
  end
299
+
300
+ # send_frame
301
+ #
302
+ # Sends a message over the websocket. Requires that the message be an appropriate
303
+ # length, and have the right format (eg. checks should be done before calling
304
+ # this function).
305
+ # TODO: Figure out the proper frame size (MAX_WS_FRAME_SIZE).
306
+ def send_frame(socket, msg)
307
+ output = [0b10000001, msg.size, msg]
308
+ begin
309
+ socket.write output.pack("CCA#{msg.size}")
310
+ rescue IOError
311
+ log_error "WebSocket is closed. Msg: #{msg}"
312
+ end
313
+ end