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 +4 -4
- data/bin/easel +0 -1
- data/lib/easel/build_pages.rb +56 -5
- data/lib/easel/configuration.rb +54 -6
- data/lib/easel/controller.rb +21 -0
- data/lib/easel/data_gathering.rb +144 -0
- data/lib/easel/server.rb +21 -6
- data/lib/easel/websocket.rb +140 -34
- data/lib/html/app.css.erb +77 -43
- data/lib/html/app.html.erb +62 -147
- data/lib/html/app.js.erb +177 -0
- data/lib/html/controller.js.erb +136 -0
- metadata +38 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45b20ce9712ac3e87e83e527b03665b9c64a9b707f571cfcbbd1c0989df494e8
|
4
|
+
data.tar.gz: bebb6eeafa225d2a05401fea5f8f8ec7e63674de617a811ec8b6cb2cf7a56640
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c50fd692b5fcd3826cc6b3ac80aa400e1319501303f951b576c9c4ada4fe43990a383ed7f84e70d2808363a77dd04e1f07e6b0a2218608309bbed2681a42382b
|
7
|
+
data.tar.gz: d1353d563bf9b9306ebbf5bb69e7a4b401558b4be77010320b7b734808e74124bdbfaecd12890a61a400a2432ee5d16536c909fa9c4c6f2d13885a488465f788
|
data/bin/easel
CHANGED
data/lib/easel/build_pages.rb
CHANGED
@@ -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
|
-
|
34
|
-
|
35
|
-
page
|
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
|
-
|
38
|
-
|
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
|
data/lib/easel/configuration.rb
CHANGED
@@ -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, #
|
15
|
-
hostname: 'localhost', #
|
16
|
-
log_file: STDOUT, #
|
17
|
-
title: 'Easel - Your Custom Dashboard',
|
18
|
-
|
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
|
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
|
|
data/lib/easel/websocket.rb
CHANGED
@@ -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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
msg
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
196
|
-
|
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
|