cosmos 4.0.3 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -5
- data/Manifest.txt +11 -1
- data/README.md +3 -2
- data/Rakefile +18 -4
- data/appveyor.yml +19 -0
- data/cosmos.gemspec +14 -3
- data/data/config/cmd_tlm_server.yaml +3 -0
- data/data/crc.txt +63 -60
- data/demo/config/targets/INST/cmd_tlm_server.txt +1 -0
- data/demo/config/targets/INST/cmd_tlm_server2.txt +7 -0
- data/demo/config/tools/cmd_sequence/cmd_sequence.txt +2 -0
- data/demo/config/tools/cmd_tlm_server/cmd_tlm_server.txt +8 -12
- data/demo/config/tools/cmd_tlm_server/cmd_tlm_server2.txt +7 -9
- data/demo/lib/cmd_sequence_exporter.rb +52 -0
- data/demo/lib/example_background_task.rb +1 -0
- data/demo/procedures/replay_test.rb +32 -0
- data/ext/cosmos/ext/structure/structure.c +39 -3
- data/install/config/tools/cmd_tlm_server/cmd_tlm_server.txt +1 -0
- data/install/config/tools/launcher/launcher.txt +2 -0
- data/lib/cosmos/config/config_parser.rb +2 -0
- data/lib/cosmos/core_ext/io.rb +89 -60
- data/lib/cosmos/gui/qt.rb +5 -8
- data/lib/cosmos/gui/qt_tool.rb +8 -8
- data/lib/cosmos/gui/text/ruby_editor.rb +12 -12
- data/lib/cosmos/gui/utilities/script_module_gui.rb +9 -9
- data/lib/cosmos/gui/widgets/realtime_button_bar.rb +18 -17
- data/lib/cosmos/interfaces/protocols/fixed_protocol.rb +2 -2
- data/lib/cosmos/interfaces/protocols/template_protocol.rb +3 -0
- data/lib/cosmos/interfaces/udp_interface.rb +27 -14
- data/lib/cosmos/io/buffered_file.rb +0 -1
- data/lib/cosmos/io/json_drb.rb +134 -214
- data/lib/cosmos/io/json_drb_object.rb +22 -61
- data/lib/cosmos/io/json_drb_rack.rb +79 -0
- data/lib/cosmos/io/json_rpc.rb +27 -0
- data/lib/cosmos/io/udp_sockets.rb +102 -58
- data/lib/cosmos/packets/commands.rb +1 -1
- data/lib/cosmos/packets/structure.rb +1 -1
- data/lib/cosmos/packets/structure_item.rb +37 -5
- data/lib/cosmos/script/cmd_tlm_server.rb +76 -2
- data/lib/cosmos/script/replay.rb +60 -0
- data/lib/cosmos/script/script.rb +20 -2
- data/lib/cosmos/script/scripting.rb +9 -9
- data/lib/cosmos/script/tools.rb +14 -0
- data/lib/cosmos/system/system.rb +185 -92
- data/lib/cosmos/system/target.rb +1 -1
- data/lib/cosmos/tools/cmd_sequence/cmd_sequence.rb +44 -4
- data/lib/cosmos/tools/cmd_sequence/sequence_item.rb +4 -0
- data/lib/cosmos/tools/cmd_sequence/sequence_list.rb +7 -0
- data/lib/cosmos/tools/cmd_tlm_server/api.rb +347 -20
- data/lib/cosmos/tools/cmd_tlm_server/background_tasks.rb +3 -0
- data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server.rb +329 -111
- data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_config.rb +13 -0
- data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui.rb +261 -95
- data/lib/cosmos/tools/cmd_tlm_server/gui/interfaces_tab.rb +46 -35
- data/lib/cosmos/tools/cmd_tlm_server/gui/logging_tab.rb +18 -8
- data/lib/cosmos/tools/cmd_tlm_server/gui/packets_tab.rb +39 -28
- data/lib/cosmos/tools/cmd_tlm_server/gui/replay_tab.rb +242 -0
- data/lib/cosmos/tools/cmd_tlm_server/gui/status_tab.rb +24 -8
- data/lib/cosmos/tools/cmd_tlm_server/gui/targets_tab.rb +18 -6
- data/lib/cosmos/tools/cmd_tlm_server/limits_groups_background_task.rb +5 -4
- data/lib/cosmos/tools/cmd_tlm_server/replay_backend.rb +375 -0
- data/lib/cosmos/tools/cmd_tlm_server/routers.rb +10 -2
- data/lib/cosmos/tools/data_viewer/data_viewer.rb +40 -5
- data/lib/cosmos/tools/handbook_creator/handbook_creator_config.rb +18 -20
- data/lib/cosmos/tools/launcher/launcher_config.rb +5 -16
- data/lib/cosmos/tools/limits_monitor/limits_monitor.rb +65 -39
- data/lib/cosmos/tools/packet_viewer/packet_viewer.rb +19 -0
- data/lib/cosmos/tools/replay/replay.rb +5 -505
- data/lib/cosmos/tools/script_runner/script_audit.rb +1 -0
- data/lib/cosmos/tools/script_runner/script_runner.rb +3 -4
- data/lib/cosmos/tools/script_runner/script_runner_config.rb +3 -4
- data/lib/cosmos/tools/script_runner/script_runner_frame.rb +44 -23
- data/lib/cosmos/tools/test_runner/results_writer.rb +4 -0
- data/lib/cosmos/tools/test_runner/test_runner.rb +0 -3
- data/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_realtime_thread.rb +6 -2
- data/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_tool.rb +26 -1
- data/lib/cosmos/tools/tlm_viewer/screen.rb +24 -1
- data/lib/cosmos/tools/tlm_viewer/tlm_viewer.rb +25 -0
- data/lib/cosmos/tools/tlm_viewer/tlm_viewer_config.rb +24 -14
- data/lib/cosmos/top_level.rb +34 -24
- data/lib/cosmos/utilities/csv.rb +60 -8
- data/lib/cosmos/version.rb +5 -5
- data/spec/config/config_parser_spec.rb +10 -1
- data/spec/core_ext/socket_spec.rb +4 -2
- data/spec/gui/utilities/script_module_gui_spec.rb +102 -0
- data/spec/install/config/data/data.txt +1 -0
- data/spec/install/config/targets/INST/cmd_tlm/inst_cmds.txt +2 -0
- data/spec/interfaces/cmd_tlm_server_interface_spec.rb +1 -2
- data/spec/interfaces/protocols/template_protocol_spec.rb +72 -2
- data/spec/interfaces/serial_interface_spec.rb +1 -1
- data/spec/interfaces/udp_interface_spec.rb +14 -0
- data/spec/io/buffered_file_spec.rb +37 -0
- data/spec/io/json_drb_object_spec.rb +2 -15
- data/spec/io/json_drb_spec.rb +61 -121
- data/spec/io/udp_sockets_spec.rb +42 -2
- data/spec/packet_logs/packet_log_reader_spec.rb +5 -2
- data/spec/packets/binary_accessor_spec.rb +1 -1
- data/spec/packets/packet_item_spec.rb +1 -1
- data/spec/packets/structure_item_spec.rb +5 -6
- data/spec/script/cmd_tlm_server_spec.rb +39 -4
- data/spec/script/commands_disconnect_spec.rb +1 -1
- data/spec/script/commands_spec.rb +2 -1
- data/spec/script/scripting_spec.rb +18 -3
- data/spec/script/telemetry_spec.rb +5 -0
- data/spec/spec_helper.rb +43 -26
- data/spec/streams/tcpip_socket_stream_spec.rb +2 -2
- data/spec/system/system_spec.rb +11 -9
- data/spec/system/target_spec.rb +3 -0
- data/spec/tools/cmd_tlm_server/api_spec.rb +543 -29
- data/spec/tools/cmd_tlm_server/background_task_spec.rb +2 -2
- data/spec/tools/cmd_tlm_server/background_tasks_spec.rb +31 -75
- data/spec/tools/cmd_tlm_server/cmd_tlm_server_config_spec.rb +199 -66
- data/spec/tools/cmd_tlm_server/cmd_tlm_server_spec.rb +85 -9
- data/spec/tools/cmd_tlm_server/interface_thread_spec.rb +29 -127
- data/spec/tools/cmd_tlm_server/router_thread_spec.rb +10 -50
- data/spec/tools/launcher/launcher_config_spec.rb +1 -1
- data/spec/tools/table_manager/table_item_spec.rb +1 -1
- data/spec/tools/table_manager/tablemanager_core_spec.rb +4 -4
- data/spec/top_level/top_level_spec.rb +151 -3
- data/spec/utilities/csv_spec.rb +24 -5
- metadata +61 -9
- data/lib/cosmos/tools/replay/replay_server.rb +0 -91
data/lib/cosmos/gui/qt.rb
CHANGED
@@ -252,6 +252,7 @@ module Cosmos
|
|
252
252
|
end
|
253
253
|
end
|
254
254
|
end
|
255
|
+
return config_change_success, change_error
|
255
256
|
end
|
256
257
|
end
|
257
258
|
|
@@ -458,17 +459,13 @@ class Qt::PlainTextEdit
|
|
458
459
|
text << "\n"
|
459
460
|
end
|
460
461
|
if text =~ /<G>/ or color == Cosmos::GREEN
|
461
|
-
text.gsub
|
462
|
-
addText(text, Cosmos::GREEN)
|
462
|
+
addText(text.gsub(/<G>/, BLANK), Cosmos::GREEN)
|
463
463
|
elsif text =~ /<Y>/ or color == Cosmos::YELLOW
|
464
|
-
text.gsub
|
465
|
-
addText(text, Cosmos::YELLOW)
|
464
|
+
addText(text.gsub(/<Y>/, BLANK), Cosmos::YELLOW)
|
466
465
|
elsif text =~ /<R>/ or color == Cosmos::RED
|
467
|
-
text.gsub
|
468
|
-
addText(text, Cosmos::RED)
|
466
|
+
addText(text.gsub(/<R>/, BLANK), Cosmos::RED)
|
469
467
|
elsif text =~ /<B>/ or color == Cosmos::BLUE
|
470
|
-
text.gsub
|
471
|
-
addText(text, Cosmos::BLUE)
|
468
|
+
addText(text.gsub(/<B>/, BLANK), Cosmos::BLUE)
|
472
469
|
else
|
473
470
|
addText(text) # default is Cosmos::BLACK
|
474
471
|
end
|
data/lib/cosmos/gui/qt_tool.rb
CHANGED
@@ -75,21 +75,21 @@ module Cosmos
|
|
75
75
|
# @param filename [String] Path to a configuration file
|
76
76
|
# @param type [String] File extension, e.g. '.txt'
|
77
77
|
# @param tool_name [String] Name of the tool calling this method
|
78
|
-
# @return [String|nil] Path to a configuration file
|
78
|
+
# @return [String|nil] Path to a configuration file. If a filename was
|
79
|
+
# passed and the configuration file is not found, the original filename
|
80
|
+
# is returned. If no filename was passed nil is returned.
|
79
81
|
def config_path(filename, type, tool_name)
|
80
82
|
return filename if filename && File.exist?(filename)
|
81
83
|
if filename
|
82
84
|
# Add the configuration dir onto the filename
|
83
|
-
|
85
|
+
new_filename = File.join(@options.config_dir, filename)
|
86
|
+
return new_filename if File.exist?(new_filename)
|
84
87
|
else
|
85
88
|
# No file passed so default to a file named after the class
|
86
|
-
|
87
|
-
|
88
|
-
if File.exist? filename
|
89
|
-
filename
|
90
|
-
else
|
91
|
-
nil
|
89
|
+
new_filename = File.join(@options.config_dir, "#{tool_name}#{type}")
|
90
|
+
return new_filename if File.exist?(new_filename)
|
92
91
|
end
|
92
|
+
filename
|
93
93
|
end
|
94
94
|
|
95
95
|
# Create the exit_action and the about_action. The exit_action is not
|
@@ -403,37 +403,37 @@ module Cosmos
|
|
403
403
|
end
|
404
404
|
|
405
405
|
def create_add_breakpoint_action(point)
|
406
|
-
|
407
|
-
|
408
|
-
|
406
|
+
action = Qt::Action.new(tr("Add Breakpoint"), self)
|
407
|
+
action.statusTip = tr("Add a breakpoint at this line")
|
408
|
+
action.connect(SIGNAL('triggered()')) do
|
409
409
|
line_at_point(point) do |line|
|
410
410
|
add_breakpoint(line)
|
411
411
|
emit breakpoint_set(line)
|
412
412
|
end
|
413
413
|
end
|
414
|
-
|
414
|
+
action
|
415
415
|
end
|
416
416
|
|
417
417
|
def create_clear_breakpoint_action(point)
|
418
|
-
|
419
|
-
|
420
|
-
|
418
|
+
action = Qt::Action.new(tr("Clear Breakpoint"), self)
|
419
|
+
action.statusTip = tr("Clear an existing breakpoint at this line")
|
420
|
+
action.connect(SIGNAL('triggered()')) do
|
421
421
|
line_at_point(point) do |line|
|
422
422
|
clear_breakpoint(line)
|
423
423
|
emit breakpoint_cleared(line)
|
424
424
|
end
|
425
425
|
end
|
426
|
-
|
426
|
+
action
|
427
427
|
end
|
428
428
|
|
429
429
|
def create_clear_all_breakpoints_action
|
430
|
-
|
431
|
-
|
432
|
-
|
430
|
+
action = Qt::Action.new(tr("Clear All Breakpoints"), self)
|
431
|
+
action.statusTip = tr("Clear all existing breakpoints")
|
432
|
+
action.connect(SIGNAL('triggered()')) do
|
433
433
|
clear_breakpoints
|
434
434
|
emit breakpoints_cleared
|
435
435
|
end
|
436
|
-
|
436
|
+
action
|
437
437
|
end
|
438
438
|
|
439
439
|
# Get the top and bottom coordinates of the block in viewport coordinates
|
@@ -76,16 +76,16 @@ module Cosmos
|
|
76
76
|
return result
|
77
77
|
end
|
78
78
|
|
79
|
-
def save_file_dialog(directory = Cosmos::USERPATH, message = "Save File")
|
80
|
-
_get_main_thread_gui {|window| Qt::FileDialog.getSaveFileName(window, message, directory) }
|
79
|
+
def save_file_dialog(directory = Cosmos::USERPATH, message = "Save File", filter = "All Files (*.*)")
|
80
|
+
_get_main_thread_gui {|window| Qt::FileDialog.getSaveFileName(window, message, directory, filter) }
|
81
81
|
end
|
82
82
|
|
83
|
-
def open_file_dialog(directory = Cosmos::USERPATH, message = "Open File")
|
84
|
-
_get_main_thread_gui {|window| Qt::FileDialog.getOpenFileName(window, message, directory) }
|
83
|
+
def open_file_dialog(directory = Cosmos::USERPATH, message = "Open File", filter = "All Files (*.*)")
|
84
|
+
_get_main_thread_gui {|window| Qt::FileDialog.getOpenFileName(window, message, directory, filter) }
|
85
85
|
end
|
86
86
|
|
87
|
-
def open_files_dialog(directory = Cosmos::USERPATH, message = "Open File(s)")
|
88
|
-
_get_main_thread_gui {|window| Qt::FileDialog.getOpenFileNames(window, message, directory) }
|
87
|
+
def open_files_dialog(directory = Cosmos::USERPATH, message = "Open File(s)", filter = "All Files (*.*)")
|
88
|
+
_get_main_thread_gui {|window| Qt::FileDialog.getOpenFileNames(window, message, directory, filter) }
|
89
89
|
end
|
90
90
|
|
91
91
|
def open_directory_dialog(directory = Cosmos::USERPATH, message = "Open Directory")
|
@@ -134,7 +134,7 @@ module Cosmos
|
|
134
134
|
result = nil
|
135
135
|
_get_main_thread_gui do |window|
|
136
136
|
msg = Qt::MessageBox.new(window)
|
137
|
-
msg.setIcon(Qt::MessageBox::
|
137
|
+
msg.setIcon(Qt::MessageBox::Question)
|
138
138
|
msg.setText(message)
|
139
139
|
msg.setWindowTitle(title)
|
140
140
|
msg.setStandardButtons(Qt::MessageBox::Yes | Qt::MessageBox::No)
|
@@ -221,7 +221,7 @@ module Cosmos
|
|
221
221
|
|
222
222
|
def prompt_vertical_message_box(string, buttons)
|
223
223
|
loop do
|
224
|
-
result = buttons[0]
|
224
|
+
result = buttons[0].clone
|
225
225
|
Qt.execute_in_main_thread(true, 0.05) do
|
226
226
|
dialog = _build_dialog(string)
|
227
227
|
|
@@ -258,7 +258,7 @@ module Cosmos
|
|
258
258
|
|
259
259
|
def prompt_combo_box(string, options)
|
260
260
|
loop do
|
261
|
-
result = options[0]
|
261
|
+
result = options[0].clone
|
262
262
|
Qt.execute_in_main_thread(true, 0.05) do
|
263
263
|
dialog = _build_dialog(string)
|
264
264
|
# Check if the last parameter is false which means they don't want
|
@@ -15,25 +15,20 @@ require 'cosmos'
|
|
15
15
|
require 'cosmos/gui/qt'
|
16
16
|
|
17
17
|
module Cosmos
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# This class implements the RealtimeButtonBar
|
22
|
-
#
|
18
|
+
# Create a horizontal or vertical widget which contains buttons to control
|
19
|
+
# scripting. The Step button is hidden by default, and Start, Pause, and
|
20
|
+
# Stop are visible.
|
23
21
|
class RealtimeButtonBar < Qt::Widget
|
24
|
-
|
25
|
-
# Accessors for callbacks
|
22
|
+
attr_accessor :step_callback
|
26
23
|
attr_accessor :start_callback
|
27
24
|
attr_accessor :pause_callback
|
28
25
|
attr_accessor :stop_callback
|
29
|
-
|
30
|
-
# Readers for buttons
|
26
|
+
attr_reader :step_button
|
31
27
|
attr_reader :start_button
|
32
28
|
attr_reader :pause_button
|
33
29
|
attr_reader :stop_button
|
34
30
|
|
35
|
-
|
36
|
-
def initialize (parent, orientation = Qt::Horizontal)
|
31
|
+
def initialize(parent, orientation = Qt::Horizontal)
|
37
32
|
super(parent)
|
38
33
|
if orientation == Qt::Horizontal
|
39
34
|
# Horizontal Frame for overall widget
|
@@ -56,15 +51,21 @@ module Cosmos
|
|
56
51
|
@stop_button = Qt::PushButton.new('Stop')
|
57
52
|
@pause_button = Qt::PushButton.new('Pause')
|
58
53
|
@start_button = Qt::PushButton.new('Start')
|
54
|
+
@step_button = Qt::PushButton.new('Step')
|
55
|
+
@step_button.setHidden(true)
|
56
|
+
@overall_frame.addWidget(@step_button)
|
59
57
|
@overall_frame.addWidget(@start_button)
|
60
58
|
@overall_frame.addWidget(@pause_button)
|
61
59
|
@overall_frame.addWidget(@stop_button)
|
62
60
|
else
|
63
61
|
# Buttons
|
64
62
|
@button_frame = Qt::HBoxLayout.new
|
63
|
+
@step_button = Qt::PushButton.new('Step')
|
64
|
+
@step_button.setHidden(true)
|
65
65
|
@start_button = Qt::PushButton.new('Start')
|
66
66
|
@pause_button = Qt::PushButton.new('Pause')
|
67
67
|
@stop_button = Qt::PushButton.new('Stop')
|
68
|
+
@button_frame.addWidget(@step_button)
|
68
69
|
@button_frame.addWidget(@start_button)
|
69
70
|
@button_frame.addWidget(@pause_button)
|
70
71
|
@button_frame.addWidget(@stop_button)
|
@@ -75,9 +76,11 @@ module Cosmos
|
|
75
76
|
|
76
77
|
# Connect handlers
|
77
78
|
@stop_button.connect(SIGNAL('clicked()')) { @stop_callback.call if @stop_callback }
|
78
|
-
@pause_button.connect(SIGNAL('clicked()'))
|
79
|
-
@start_button.connect(SIGNAL('clicked()'))
|
79
|
+
@pause_button.connect(SIGNAL('clicked()')) { @pause_callback.call if @pause_callback }
|
80
|
+
@start_button.connect(SIGNAL('clicked()')) { @start_callback.call if @start_callback }
|
81
|
+
@step_button.connect(SIGNAL('clicked()')) { @step_callback.call if @step_callback }
|
80
82
|
|
83
|
+
@step_callback = nil
|
81
84
|
@start_callback = nil
|
82
85
|
@pause_callback = nil
|
83
86
|
@stop_callback = nil
|
@@ -92,7 +95,5 @@ module Cosmos
|
|
92
95
|
def state= (new_state)
|
93
96
|
@state.setText(new_state.to_s)
|
94
97
|
end
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
end # module Cosmos
|
98
|
+
end
|
99
|
+
end
|
@@ -53,7 +53,7 @@ module Cosmos
|
|
53
53
|
|
54
54
|
# Identifies an unknown buffer of data as a Packet. The raw data is
|
55
55
|
# returned but the packet that matched is recorded so it can be set in the
|
56
|
-
#
|
56
|
+
# read_packet callback.
|
57
57
|
#
|
58
58
|
# @return [String|Symbol] The identified packet data or :STOP if more data
|
59
59
|
# is required to build a packet
|
@@ -82,7 +82,7 @@ module Cosmos
|
|
82
82
|
return :STOP if @data.length < identified_packet.defined_length
|
83
83
|
end
|
84
84
|
# Set some variables so we can update the packet in
|
85
|
-
#
|
85
|
+
# read_packet
|
86
86
|
@received_time = Time.now.sys
|
87
87
|
@target_name = identified_packet.target_name
|
88
88
|
@packet_name = identified_packet.packet_name
|
@@ -121,6 +121,9 @@ module Cosmos
|
|
121
121
|
# Grab the response packet specified in the command
|
122
122
|
result_packet = System.telemetry.packet(@interface.target_names[0], @response_packet).clone
|
123
123
|
result_packet.received_time = nil
|
124
|
+
result_packet.id_items.each do |item|
|
125
|
+
result_packet.write_item(item, item.id_value, :RAW)
|
126
|
+
end
|
124
127
|
|
125
128
|
# Convert the response template into a Regexp
|
126
129
|
response_item_names = []
|
@@ -75,18 +75,29 @@ module Cosmos
|
|
75
75
|
# the constructor and a new {UdpReadSocket} if the read_port was given in
|
76
76
|
# the constructor.
|
77
77
|
def connect
|
78
|
-
@
|
79
|
-
@
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
@
|
87
|
-
|
88
|
-
@
|
89
|
-
|
78
|
+
if @read_port and @write_dest_port and @write_src_port and (@read_port == @write_src_port)
|
79
|
+
@read_socket = UdpReadWriteSocket.new(
|
80
|
+
@read_port,
|
81
|
+
@bind_address,
|
82
|
+
@write_dest_port,
|
83
|
+
@hostname,
|
84
|
+
@interface_address,
|
85
|
+
@ttl)
|
86
|
+
@write_socket = @read_socket
|
87
|
+
else
|
88
|
+
@read_socket = UdpReadSocket.new(
|
89
|
+
@read_port,
|
90
|
+
@hostname,
|
91
|
+
@interface_address,
|
92
|
+
@bind_address) if @read_port
|
93
|
+
@write_socket = UdpWriteSocket.new(
|
94
|
+
@hostname,
|
95
|
+
@write_dest_port,
|
96
|
+
@write_src_port,
|
97
|
+
@interface_address,
|
98
|
+
@ttl,
|
99
|
+
@bind_address) if @write_dest_port
|
100
|
+
end
|
90
101
|
@thread_sleeper = nil
|
91
102
|
end
|
92
103
|
|
@@ -105,9 +116,11 @@ module Cosmos
|
|
105
116
|
|
106
117
|
# Close the active ports (read and/or write) and set the sockets to nil.
|
107
118
|
def disconnect
|
108
|
-
|
109
|
-
|
119
|
+
if @write_socket != @read_socket
|
120
|
+
Cosmos.close_socket(@write_socket)
|
121
|
+
end
|
110
122
|
Cosmos.close_socket(@read_socket)
|
123
|
+
@write_socket = nil
|
111
124
|
@read_socket = nil
|
112
125
|
@thread_sleeper.cancel if @thread_sleeper
|
113
126
|
@thread_sleeper = nil
|
data/lib/cosmos/io/json_drb.rb
CHANGED
@@ -15,6 +15,28 @@ require 'drb/acl'
|
|
15
15
|
require 'drb/drb'
|
16
16
|
require 'set'
|
17
17
|
require 'cosmos/io/json_rpc'
|
18
|
+
require 'cosmos/io/json_drb_rack'
|
19
|
+
require 'rack/handler/puma'
|
20
|
+
if RUBY_ENGINE == 'ruby' and %w(2.2.7 2.2.8 2.3.4 2.4.1).include? RUBY_VERSION
|
21
|
+
require 'stopgap_13632'
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add methods to the Puma::Launcher and Puma::Single class so we can tell
|
25
|
+
# if the server has been started.
|
26
|
+
module Puma
|
27
|
+
class Launcher
|
28
|
+
def running
|
29
|
+
@runner and @runner.running
|
30
|
+
end
|
31
|
+
end
|
32
|
+
class Runner
|
33
|
+
end
|
34
|
+
class Single < Runner
|
35
|
+
def running
|
36
|
+
@server and @server.running
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
18
40
|
|
19
41
|
module Cosmos
|
20
42
|
|
@@ -25,7 +47,6 @@ module Cosmos
|
|
25
47
|
# methods.
|
26
48
|
class JsonDRb
|
27
49
|
MINIMUM_REQUEST_TIME = 0.0001
|
28
|
-
FAST_READ = (RUBY_VERSION > "2.1")
|
29
50
|
|
30
51
|
@@debug = false
|
31
52
|
|
@@ -37,7 +58,6 @@ module Cosmos
|
|
37
58
|
attr_accessor :acl
|
38
59
|
|
39
60
|
def initialize
|
40
|
-
@listen_socket = nil
|
41
61
|
@thread = nil
|
42
62
|
@acl = nil
|
43
63
|
@object = nil
|
@@ -46,52 +66,46 @@ module Cosmos
|
|
46
66
|
@request_times = []
|
47
67
|
@request_times_index = 0
|
48
68
|
@request_mutex = Mutex.new
|
49
|
-
@
|
50
|
-
@
|
51
|
-
@client_pipe_writers = []
|
52
|
-
@client_mutex = Mutex.new
|
53
|
-
@thread_reader, @thread_writer = IO.pipe
|
69
|
+
@server = nil
|
70
|
+
@server_mutex = Mutex.new
|
54
71
|
end
|
55
72
|
|
56
73
|
# Returns the number of connected clients
|
57
74
|
# @return [Integer] The number of connected clients
|
58
75
|
def num_clients
|
59
|
-
|
76
|
+
clients = 0
|
77
|
+
@server_mutex.synchronize do
|
78
|
+
if @server
|
79
|
+
# @server.stats() returns a string like: { "backlog": 0, "running": 0 }
|
80
|
+
# "running" indicates the number of server threads running, and
|
81
|
+
# therefore the number of clients connected.
|
82
|
+
stats = @server.stats()
|
83
|
+
stats =~ /"running": \d*/
|
84
|
+
clients = $&.split(":")[1].to_i
|
85
|
+
end
|
86
|
+
end
|
87
|
+
return clients
|
60
88
|
end
|
61
89
|
|
62
90
|
# Stops the DRb service by closing the socket and the processing thread
|
63
91
|
def stop_service
|
64
|
-
|
92
|
+
# Kill the server thread; it can take a while, so use
|
93
|
+
# graceful_timeout = 5, timeout_interval = 0.1, hard_timeout = 5
|
94
|
+
Cosmos.kill_thread(self, @thread, 5, 0.1, 5)
|
65
95
|
@thread = nil
|
66
|
-
|
67
|
-
|
68
|
-
client_threads = nil
|
69
|
-
@client_mutex.synchronize do
|
70
|
-
@client_sockets.each do |client_socket|
|
71
|
-
Cosmos.close_socket(client_socket)
|
72
|
-
end
|
73
|
-
@client_pipe_writers.each do |client_pipe_writer|
|
74
|
-
client_pipe_writer.write('.')
|
75
|
-
end
|
76
|
-
client_threads = @client_threads.clone
|
77
|
-
end
|
78
|
-
|
79
|
-
# This cannot be inside of the client_mutex or the threads will not
|
80
|
-
# be able to shutdown because they will stick on the client_mutex
|
81
|
-
client_threads.each do |client_thread|
|
82
|
-
Cosmos.kill_thread(self, client_thread)
|
83
|
-
end
|
84
|
-
|
85
|
-
@client_mutex.synchronize do
|
86
|
-
@client_threads.clear
|
87
|
-
@client_sockets.clear
|
88
|
-
@client_pipe_writers.clear
|
96
|
+
@server_mutex.synchronize do
|
97
|
+
@server = nil
|
89
98
|
end
|
90
99
|
end
|
91
100
|
|
92
101
|
# Gracefully kill the thread
|
93
102
|
def graceful_kill
|
94
|
-
@
|
103
|
+
@server_mutex.synchronize do
|
104
|
+
begin
|
105
|
+
@server.stop if @server and @server.running
|
106
|
+
rescue
|
107
|
+
end
|
108
|
+
end
|
95
109
|
end
|
96
110
|
|
97
111
|
# @param hostname [String] The host to start the service on
|
@@ -99,58 +113,80 @@ module Cosmos
|
|
99
113
|
# @param object [Object] The object to send the DRb requests to. This
|
100
114
|
# object must either include the Cosmos::Script module or be the
|
101
115
|
# CmdTlmServer.
|
102
|
-
def start_service(hostname = nil, port = nil, object = nil)
|
116
|
+
def start_service(hostname = nil, port = nil, object = nil, max_threads = 1000)
|
117
|
+
server_started = false
|
118
|
+
@server_mutex.synchronize do
|
119
|
+
server_started = true if @server
|
120
|
+
end
|
121
|
+
return if server_started
|
122
|
+
|
103
123
|
if hostname and port and object
|
104
|
-
@thread_reader, @thread_writer = IO.pipe
|
105
124
|
@object = object
|
106
125
|
hostname = '127.0.0.1'.freeze if (hostname.to_s.upcase == 'LOCALHOST'.freeze)
|
107
126
|
|
108
|
-
# Create a socket to accept connections from clients
|
109
|
-
begin
|
110
|
-
@listen_socket = TCPServer.new(hostname, port)
|
111
|
-
@listen_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) unless Kernel.is_windows?
|
112
|
-
# The address is use error is pretty typical if an existing
|
113
|
-
# CmdTlmServer is running so explicitly rescue this
|
114
|
-
rescue Errno::EADDRINUSE
|
115
|
-
raise "Error binding to port #{port}.\n" +
|
116
|
-
"Either another application is using this port\n" +
|
117
|
-
"or the operating system is being slow cleaning up.\n" +
|
118
|
-
"Make sure all sockets/streams are closed in all applications,\n" +
|
119
|
-
"wait 1 minute and try again."
|
120
|
-
# Something else went wrong which is fatal
|
121
|
-
rescue => error
|
122
|
-
Logger.error "JsonDRb listen thread unable to be created.\n#{error.formatted}"
|
123
|
-
Cosmos.handle_fatal_exception(error)
|
124
|
-
end
|
125
|
-
|
126
|
-
# Start the listen thread which accepts connections
|
127
127
|
@thread = Thread.new do
|
128
|
+
|
129
|
+
# Create an http server to accept requests from clients
|
128
130
|
begin
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
131
|
+
server_config = {
|
132
|
+
:Host => hostname,
|
133
|
+
:Port => port,
|
134
|
+
:Silent => true,
|
135
|
+
:Verbose => false,
|
136
|
+
:Threads => "0:#{max_threads}",
|
137
|
+
}
|
138
|
+
|
139
|
+
# The run call will block until the server is stopped.
|
140
|
+
Rack::Handler::Puma.run(JsonDrbRack.new(self), server_config) do |server|
|
141
|
+
@server_mutex.synchronize do
|
142
|
+
@server = server
|
140
143
|
end
|
144
|
+
end
|
141
145
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
146
|
+
# Wait for all puma threads to stop before trying to close
|
147
|
+
# the sockets
|
148
|
+
start_time = Time.now
|
149
|
+
while true
|
150
|
+
puma_threads = false
|
151
|
+
Thread.list.each {|thread| puma_threads = true if thread.inspect.match(/puma/)}
|
152
|
+
break if !puma_threads
|
153
|
+
break if (Time.now - start_time) > 10.0
|
154
|
+
sleep 0.25
|
148
155
|
end
|
149
|
-
|
150
|
-
|
156
|
+
|
157
|
+
# Puma doesn't clean up it's own sockets after shutting down,
|
158
|
+
# so we'll do that here.
|
159
|
+
@server_mutex.synchronize do
|
160
|
+
@server.binder.close() if @server
|
161
|
+
end
|
162
|
+
|
163
|
+
# The address in use error is pretty typical if an existing
|
164
|
+
# CmdTlmServer is running so explicitly rescue this
|
165
|
+
rescue Errno::EADDRINUSE
|
166
|
+
@server = nil
|
167
|
+
raise "Error binding to port #{port}.\n" +
|
168
|
+
"Either another application is using this port\n" +
|
169
|
+
"or the operating system is being slow cleaning up.\n" +
|
170
|
+
"Make sure all sockets/streams are closed in all applications,\n" +
|
171
|
+
"wait 1 minute and try again."
|
172
|
+
# Something else went wrong which is fatal
|
173
|
+
rescue => error
|
174
|
+
@server = nil
|
175
|
+
Logger.error "JsonDRb http server could not be started or unexpectedly died.\n#{error.formatted}"
|
151
176
|
Cosmos.handle_fatal_exception(error)
|
152
177
|
end
|
153
178
|
end
|
179
|
+
|
180
|
+
# Wait for the server to be started in the thread before returning.
|
181
|
+
start_time = Time.now
|
182
|
+
while ((Time.now - start_time) < 5.0) and !server_started
|
183
|
+
sleep(0.1)
|
184
|
+
@server_mutex.synchronize do
|
185
|
+
server_started = true if @server and @server.running
|
186
|
+
end
|
187
|
+
end
|
188
|
+
raise "JsonDRb http server could not be started." unless server_started
|
189
|
+
|
154
190
|
elsif hostname or port or object
|
155
191
|
raise "0 or 3 parameters must be given"
|
156
192
|
else
|
@@ -188,81 +224,6 @@ module Cosmos
|
|
188
224
|
avg
|
189
225
|
end
|
190
226
|
|
191
|
-
# @param socket [Socket] The socket to the client
|
192
|
-
# @param data [String] Binary data which has already been read from the
|
193
|
-
# socket.
|
194
|
-
# @param pipe_reader [IO.pipe] Used to break out of select
|
195
|
-
# @return [String] The request message
|
196
|
-
def self.receive_message(socket, data, pipe_reader)
|
197
|
-
self.get_at_least_x_bytes_of_data(socket, data, 4, pipe_reader)
|
198
|
-
if data.length >= 4
|
199
|
-
length = data[0..3].unpack('N'.freeze)[0]
|
200
|
-
data.replace(data[4..-1])
|
201
|
-
else
|
202
|
-
return nil
|
203
|
-
end
|
204
|
-
|
205
|
-
self.get_at_least_x_bytes_of_data(socket, data, length, pipe_reader)
|
206
|
-
if data.length >= length
|
207
|
-
message = data[0..(length - 1)]
|
208
|
-
data.replace(data[length..-1])
|
209
|
-
return message
|
210
|
-
else
|
211
|
-
return nil
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
# @param socket [Socket] The socket to the client
|
216
|
-
# @param current_data [String] Binary data read from the socket
|
217
|
-
# @param required_num_bytes [Integer] The minimum number of bytes to read
|
218
|
-
# @param pipe_reader [IO.pipe] Used to break out of select
|
219
|
-
# before returning
|
220
|
-
def self.get_at_least_x_bytes_of_data(socket, current_data, required_num_bytes, pipe_reader)
|
221
|
-
while (current_data.length < required_num_bytes)
|
222
|
-
if FAST_READ
|
223
|
-
data = socket.read_nonblock(65535, exception: false)
|
224
|
-
raise EOFError, 'end of file reached' unless data
|
225
|
-
if data == :wait_readable
|
226
|
-
IO.fast_select([socket, pipe_reader], nil, nil, nil)
|
227
|
-
else
|
228
|
-
current_data << data
|
229
|
-
end
|
230
|
-
else
|
231
|
-
begin
|
232
|
-
current_data << socket.read_nonblock(65535)
|
233
|
-
rescue IO::WaitReadable
|
234
|
-
IO.fast_select([socket, pipe_reader], nil, nil, nil)
|
235
|
-
end
|
236
|
-
end
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
# @param socket [Socket] The socket to the client
|
241
|
-
# @param data [String] Binary data to send to the socket
|
242
|
-
# @param send_timeout [Float] The number of seconds to wait for the send to
|
243
|
-
# complete
|
244
|
-
def self.send_data(socket, data, send_timeout = 10.0)
|
245
|
-
num_bytes_to_send = data.length + 4
|
246
|
-
total_bytes_sent = 0
|
247
|
-
bytes_sent = 0
|
248
|
-
data_to_send = [data.length].pack('N'.freeze) << data.clone
|
249
|
-
|
250
|
-
loop do
|
251
|
-
begin
|
252
|
-
bytes_sent = socket.write_nonblock(data_to_send[total_bytes_sent..-1])
|
253
|
-
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
254
|
-
result = IO.fast_select(nil, [socket], nil, send_timeout)
|
255
|
-
if result
|
256
|
-
retry
|
257
|
-
else
|
258
|
-
raise Timeout::Error, "Send Timeout"
|
259
|
-
end
|
260
|
-
end
|
261
|
-
total_bytes_sent += bytes_sent
|
262
|
-
break if total_bytes_sent >= num_bytes_to_send
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
227
|
# @return [Boolean] Whether debug messages are enabled
|
267
228
|
def self.debug?
|
268
229
|
@@debug
|
@@ -273,66 +234,20 @@ module Cosmos
|
|
273
234
|
@@debug = value
|
274
235
|
end
|
275
236
|
|
276
|
-
|
277
|
-
|
278
|
-
# Creates a new Thread to service the JSON DRb requests from the client.
|
279
|
-
#
|
280
|
-
# @param socket [Socket] The socket which the server accepted from the
|
281
|
-
# client.
|
282
|
-
def create_client_thread(socket)
|
283
|
-
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
284
|
-
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
285
|
-
|
286
|
-
Thread.new(socket) do |my_socket|
|
287
|
-
pipe_reader, pipe_writer = IO.pipe
|
288
|
-
@client_mutex.synchronize do
|
289
|
-
@client_sockets << my_socket
|
290
|
-
@client_threads << Thread.current
|
291
|
-
@client_pipe_writers << pipe_writer
|
292
|
-
end
|
293
|
-
|
294
|
-
data = ''
|
295
|
-
|
296
|
-
begin
|
297
|
-
while true
|
298
|
-
begin
|
299
|
-
request_data = JsonDRb.receive_message(my_socket, data, pipe_reader)
|
300
|
-
start_time = Time.now.sys
|
301
|
-
@request_count += 1
|
302
|
-
rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ENOTSOCK, IOError
|
303
|
-
# Socket was closed
|
304
|
-
break
|
305
|
-
end
|
306
|
-
if request_data
|
307
|
-
break unless process_request(request_data, my_socket, start_time)
|
308
|
-
else
|
309
|
-
# Socket was closed by client
|
310
|
-
break
|
311
|
-
end
|
312
|
-
end
|
313
|
-
rescue Exception => error
|
314
|
-
Logger.error "JsonDrb client thread unexpectedly died.\n#{error.formatted}"
|
315
|
-
end
|
316
|
-
|
317
|
-
@client_mutex.synchronize do
|
318
|
-
Cosmos.close_socket(my_socket)
|
319
|
-
@client_sockets.delete(my_socket)
|
320
|
-
@client_threads.delete(Thread.current)
|
321
|
-
@client_pipe_writers.delete(pipe_writer)
|
322
|
-
end
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
# Process the JSON request data, execute the method, and send the response.
|
237
|
+
# Process the JSON request data, execute the method, and create a response.
|
327
238
|
#
|
328
239
|
# @param request_data [String] The JSON encoded request
|
329
|
-
# @param my_socket [Socket] The socket to send the response out on
|
330
240
|
# @param start_time [Time] The time when the initial request was received
|
331
|
-
|
241
|
+
# @return response_data, error_code [String, Integer/nil] The JSON encoded
|
242
|
+
# response and error code
|
243
|
+
def process_request(request_data, start_time)
|
244
|
+
@request_count += 1
|
332
245
|
STDOUT.puts request_data if JsonDRb.debug?
|
333
246
|
begin
|
334
247
|
request = JsonRpcRequest.from_json(request_data)
|
335
248
|
response = nil
|
249
|
+
error_code = nil
|
250
|
+
response_data = nil
|
336
251
|
|
337
252
|
if (@method_whitelist and @method_whitelist.include?(request.method)) or
|
338
253
|
(!@method_whitelist and !JsonRpcRequest::DANGEROUS_METHODS.include?(request.method))
|
@@ -344,41 +259,46 @@ module Cosmos
|
|
344
259
|
rescue Exception => error
|
345
260
|
if request.id
|
346
261
|
if NoMethodError === error
|
262
|
+
error_code = JsonRpcError::ErrorCode::METHOD_NOT_FOUND
|
347
263
|
response = JsonRpcErrorResponse.new(
|
348
|
-
JsonRpcError.new(
|
264
|
+
JsonRpcError.new(error_code, "Method not found", error), request.id)
|
349
265
|
elsif ArgumentError === error
|
266
|
+
error_code = JsonRpcError::ErrorCode::INVALID_PARAMS
|
350
267
|
response = JsonRpcErrorResponse.new(
|
351
|
-
JsonRpcError.new(
|
268
|
+
JsonRpcError.new(error_code, "Invalid params", error), request.id)
|
352
269
|
else
|
270
|
+
error_code = JsonRpcError::ErrorCode::OTHER_ERROR
|
353
271
|
response = JsonRpcErrorResponse.new(
|
354
|
-
JsonRpcError.new(
|
272
|
+
JsonRpcError.new(error_code, error.message, error), request.id)
|
355
273
|
end
|
356
274
|
end
|
357
275
|
end
|
358
276
|
else
|
359
277
|
if request.id
|
278
|
+
error_code = JsonRpcError::ErrorCode::OTHER_ERROR
|
360
279
|
response = JsonRpcErrorResponse.new(
|
361
|
-
JsonRpcError.new(
|
280
|
+
JsonRpcError.new(error_code, "Cannot call unauthorized methods"), request.id)
|
362
281
|
end
|
363
282
|
end
|
364
|
-
process_response(response,
|
283
|
+
response_data = process_response(response, start_time) if response
|
284
|
+
return response_data, error_code
|
365
285
|
rescue => error
|
366
|
-
|
367
|
-
|
286
|
+
error_code = JsonRpcError::ErrorCode::INVALID_REQUEST
|
287
|
+
response = JsonRpcErrorResponse.new(JsonRpcError.new(error_code, "Invalid Request", error), nil)
|
288
|
+
response_data = process_response(response, start_time)
|
289
|
+
return response_data, error_code
|
368
290
|
end
|
369
|
-
true
|
370
291
|
end
|
371
292
|
|
372
|
-
|
293
|
+
protected
|
294
|
+
|
295
|
+
def process_response(response, start_time)
|
373
296
|
response_data = response.to_json(:allow_nan => true)
|
374
297
|
STDOUT.puts response_data if JsonDRb.debug?
|
375
|
-
JsonDRb.send_data(socket, response_data)
|
376
298
|
end_time = Time.now.sys
|
377
299
|
request_time = end_time - start_time
|
378
300
|
add_request_time(request_time)
|
379
|
-
|
380
|
-
# Socket was closed?
|
381
|
-
return false
|
301
|
+
return response_data
|
382
302
|
end
|
383
303
|
|
384
304
|
end
|