easel-dashboard 0.4.3 → 0.5

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 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