cosmos 4.0.3-java → 4.1.0-java

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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -5
  3. data/Manifest.txt +11 -1
  4. data/README.md +3 -2
  5. data/Rakefile +18 -4
  6. data/appveyor.yml +19 -0
  7. data/cosmos.gemspec +12 -3
  8. data/data/config/cmd_tlm_server.yaml +3 -0
  9. data/data/crc.txt +63 -60
  10. data/demo/config/targets/INST/cmd_tlm_server.txt +1 -0
  11. data/demo/config/targets/INST/cmd_tlm_server2.txt +7 -0
  12. data/demo/config/tools/cmd_sequence/cmd_sequence.txt +2 -0
  13. data/demo/config/tools/cmd_tlm_server/cmd_tlm_server.txt +8 -12
  14. data/demo/config/tools/cmd_tlm_server/cmd_tlm_server2.txt +7 -9
  15. data/demo/lib/cmd_sequence_exporter.rb +52 -0
  16. data/demo/lib/example_background_task.rb +1 -0
  17. data/demo/procedures/replay_test.rb +32 -0
  18. data/ext/cosmos/ext/structure/structure.c +39 -3
  19. data/install/config/tools/cmd_tlm_server/cmd_tlm_server.txt +1 -0
  20. data/install/config/tools/launcher/launcher.txt +2 -0
  21. data/lib/cosmos/config/config_parser.rb +2 -0
  22. data/lib/cosmos/core_ext/io.rb +89 -60
  23. data/lib/cosmos/gui/qt.rb +5 -8
  24. data/lib/cosmos/gui/qt_tool.rb +8 -8
  25. data/lib/cosmos/gui/text/ruby_editor.rb +12 -12
  26. data/lib/cosmos/gui/utilities/script_module_gui.rb +9 -9
  27. data/lib/cosmos/gui/widgets/realtime_button_bar.rb +18 -17
  28. data/lib/cosmos/interfaces/protocols/fixed_protocol.rb +2 -2
  29. data/lib/cosmos/interfaces/protocols/template_protocol.rb +3 -0
  30. data/lib/cosmos/interfaces/udp_interface.rb +27 -14
  31. data/lib/cosmos/io/buffered_file.rb +0 -1
  32. data/lib/cosmos/io/json_drb.rb +134 -214
  33. data/lib/cosmos/io/json_drb_object.rb +22 -61
  34. data/lib/cosmos/io/json_drb_rack.rb +79 -0
  35. data/lib/cosmos/io/json_rpc.rb +27 -0
  36. data/lib/cosmos/io/udp_sockets.rb +102 -58
  37. data/lib/cosmos/packets/commands.rb +1 -1
  38. data/lib/cosmos/packets/structure.rb +1 -1
  39. data/lib/cosmos/packets/structure_item.rb +37 -5
  40. data/lib/cosmos/script/cmd_tlm_server.rb +76 -2
  41. data/lib/cosmos/script/replay.rb +60 -0
  42. data/lib/cosmos/script/script.rb +20 -2
  43. data/lib/cosmos/script/scripting.rb +9 -9
  44. data/lib/cosmos/script/tools.rb +14 -0
  45. data/lib/cosmos/system/system.rb +185 -92
  46. data/lib/cosmos/system/target.rb +1 -1
  47. data/lib/cosmos/tools/cmd_sequence/cmd_sequence.rb +44 -4
  48. data/lib/cosmos/tools/cmd_sequence/sequence_item.rb +4 -0
  49. data/lib/cosmos/tools/cmd_sequence/sequence_list.rb +7 -0
  50. data/lib/cosmos/tools/cmd_tlm_server/api.rb +347 -20
  51. data/lib/cosmos/tools/cmd_tlm_server/background_tasks.rb +3 -0
  52. data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server.rb +329 -111
  53. data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_config.rb +13 -0
  54. data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui.rb +261 -95
  55. data/lib/cosmos/tools/cmd_tlm_server/gui/interfaces_tab.rb +46 -35
  56. data/lib/cosmos/tools/cmd_tlm_server/gui/logging_tab.rb +18 -8
  57. data/lib/cosmos/tools/cmd_tlm_server/gui/packets_tab.rb +39 -28
  58. data/lib/cosmos/tools/cmd_tlm_server/gui/replay_tab.rb +242 -0
  59. data/lib/cosmos/tools/cmd_tlm_server/gui/status_tab.rb +24 -8
  60. data/lib/cosmos/tools/cmd_tlm_server/gui/targets_tab.rb +18 -6
  61. data/lib/cosmos/tools/cmd_tlm_server/limits_groups_background_task.rb +5 -4
  62. data/lib/cosmos/tools/cmd_tlm_server/replay_backend.rb +375 -0
  63. data/lib/cosmos/tools/cmd_tlm_server/routers.rb +10 -2
  64. data/lib/cosmos/tools/data_viewer/data_viewer.rb +40 -5
  65. data/lib/cosmos/tools/handbook_creator/handbook_creator_config.rb +18 -20
  66. data/lib/cosmos/tools/launcher/launcher_config.rb +5 -16
  67. data/lib/cosmos/tools/limits_monitor/limits_monitor.rb +65 -39
  68. data/lib/cosmos/tools/packet_viewer/packet_viewer.rb +19 -0
  69. data/lib/cosmos/tools/replay/replay.rb +5 -505
  70. data/lib/cosmos/tools/script_runner/script_audit.rb +1 -0
  71. data/lib/cosmos/tools/script_runner/script_runner.rb +3 -4
  72. data/lib/cosmos/tools/script_runner/script_runner_config.rb +3 -4
  73. data/lib/cosmos/tools/script_runner/script_runner_frame.rb +44 -23
  74. data/lib/cosmos/tools/test_runner/results_writer.rb +4 -0
  75. data/lib/cosmos/tools/test_runner/test_runner.rb +0 -3
  76. data/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_realtime_thread.rb +6 -2
  77. data/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_tool.rb +26 -1
  78. data/lib/cosmos/tools/tlm_viewer/screen.rb +24 -1
  79. data/lib/cosmos/tools/tlm_viewer/tlm_viewer.rb +25 -0
  80. data/lib/cosmos/tools/tlm_viewer/tlm_viewer_config.rb +24 -14
  81. data/lib/cosmos/top_level.rb +34 -24
  82. data/lib/cosmos/utilities/csv.rb +60 -8
  83. data/lib/cosmos/version.rb +5 -5
  84. data/spec/config/config_parser_spec.rb +10 -1
  85. data/spec/core_ext/socket_spec.rb +4 -2
  86. data/spec/gui/utilities/script_module_gui_spec.rb +102 -0
  87. data/spec/install/config/data/data.txt +1 -0
  88. data/spec/install/config/targets/INST/cmd_tlm/inst_cmds.txt +2 -0
  89. data/spec/interfaces/cmd_tlm_server_interface_spec.rb +1 -2
  90. data/spec/interfaces/protocols/template_protocol_spec.rb +72 -2
  91. data/spec/interfaces/serial_interface_spec.rb +1 -1
  92. data/spec/interfaces/udp_interface_spec.rb +14 -0
  93. data/spec/io/buffered_file_spec.rb +37 -0
  94. data/spec/io/json_drb_object_spec.rb +2 -15
  95. data/spec/io/json_drb_spec.rb +61 -121
  96. data/spec/io/udp_sockets_spec.rb +42 -2
  97. data/spec/packet_logs/packet_log_reader_spec.rb +5 -2
  98. data/spec/packets/binary_accessor_spec.rb +1 -1
  99. data/spec/packets/packet_item_spec.rb +1 -1
  100. data/spec/packets/structure_item_spec.rb +5 -6
  101. data/spec/script/cmd_tlm_server_spec.rb +39 -4
  102. data/spec/script/commands_disconnect_spec.rb +1 -1
  103. data/spec/script/commands_spec.rb +2 -1
  104. data/spec/script/scripting_spec.rb +18 -3
  105. data/spec/script/telemetry_spec.rb +5 -0
  106. data/spec/spec_helper.rb +43 -26
  107. data/spec/streams/tcpip_socket_stream_spec.rb +2 -2
  108. data/spec/system/system_spec.rb +11 -9
  109. data/spec/system/target_spec.rb +3 -0
  110. data/spec/tools/cmd_tlm_server/api_spec.rb +543 -29
  111. data/spec/tools/cmd_tlm_server/background_task_spec.rb +2 -2
  112. data/spec/tools/cmd_tlm_server/background_tasks_spec.rb +31 -75
  113. data/spec/tools/cmd_tlm_server/cmd_tlm_server_config_spec.rb +199 -66
  114. data/spec/tools/cmd_tlm_server/cmd_tlm_server_spec.rb +85 -9
  115. data/spec/tools/cmd_tlm_server/interface_thread_spec.rb +29 -127
  116. data/spec/tools/cmd_tlm_server/router_thread_spec.rb +10 -50
  117. data/spec/tools/launcher/launcher_config_spec.rb +1 -1
  118. data/spec/tools/table_manager/table_item_spec.rb +1 -1
  119. data/spec/tools/table_manager/tablemanager_core_spec.rb +4 -4
  120. data/spec/top_level/top_level_spec.rb +151 -3
  121. data/spec/utilities/csv_spec.rb +24 -5
  122. metadata +61 -9
  123. data/lib/cosmos/tools/replay/replay_server.rb +0 -91
@@ -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!(/<G>/, BLANK)
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!(/<Y>/, BLANK)
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!(/<R>/, BLANK)
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!(/<B>/, BLANK)
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
@@ -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 or nil if none found
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
- filename = File.join(@options.config_dir, filename)
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
- filename = File.join(@options.config_dir, "#{tool_name}#{type}")
87
- end
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
- add_breakpoint = Qt::Action.new(tr("Add Breakpoint"), self)
407
- add_breakpoint.statusTip = tr("Add a breakpoint at this line")
408
- add_breakpoint.connect(SIGNAL('triggered()')) do
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
- add_breakpoint
414
+ action
415
415
  end
416
416
 
417
417
  def create_clear_breakpoint_action(point)
418
- clear_breakpoint = Qt::Action.new(tr("Clear Breakpoint"), self)
419
- clear_breakpoint.statusTip = tr("Clear an existing breakpoint at this line")
420
- clear_breakpoint.connect(SIGNAL('triggered()')) do
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
- clear_breakpoint
426
+ action
427
427
  end
428
428
 
429
429
  def create_clear_all_breakpoints_action
430
- clear_all_breakpoints = Qt::Action.new(tr("Clear All Breakpoints"), self)
431
- clear_all_breakpoints.statusTip = tr("Clear all existing breakpoints")
432
- clear_all_breakpoints.connect(SIGNAL('triggered()')) do
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
- clear_all_breakpoints
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::Warning)
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
- # RealtimeButtonBar class
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
- # Constructor
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()')) { @pause_callback.call if @pause_callback }
79
- @start_button.connect(SIGNAL('clicked()')) { @start_callback.call if @start_callback }
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
- end # class RealtimeButtonBar
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
- # post_read_packet callback.
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
- # post_read_packet
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
- @read_socket = UdpReadSocket.new(
79
- @read_port,
80
- @hostname,
81
- @interface_address,
82
- @bind_address) if @read_port
83
- @write_socket = UdpWriteSocket.new(
84
- @hostname,
85
- @write_dest_port,
86
- @write_src_port,
87
- @interface_address,
88
- @ttl,
89
- @bind_address) if @write_dest_port
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
- Cosmos.close_socket(@write_socket)
109
- @write_socket = nil
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
@@ -51,7 +51,6 @@ else
51
51
  if @buffer.length <= 0
52
52
  return nil
53
53
  end
54
-
55
54
  if read_length <= @buffer.length
56
55
  result = @buffer[@buffer_index, read_length]
57
56
  @buffer_index += read_length
@@ -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
- @client_sockets = []
50
- @client_threads = []
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
- @client_threads.length
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
- Cosmos.kill_thread(self, @thread)
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
- Cosmos.close_socket(@listen_socket)
67
- @listen_socket = nil
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
- @thread_writer.write('.') if @thread
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
- while true
130
- begin
131
- socket = @listen_socket.accept_nonblock
132
- rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EINTR, Errno::EWOULDBLOCK
133
- read_ready, _ = IO.select([@listen_socket, @thread_reader])
134
- if read_ready and read_ready.include?(@thread_reader)
135
- # Thread should be killed
136
- break
137
- else
138
- retry
139
- end
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
- if @acl and !@acl.allow_socket?(socket)
143
- Cosmos.close_socket(socket)
144
- next
145
- end
146
- # Create new thread for new connection
147
- create_client_thread(socket)
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
- rescue Exception => error
150
- Logger.error "JsonDRb listen thread unexpectedly died.\n#{error.formatted}"
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
- protected
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
- def process_request(request_data, my_socket, start_time)
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(-32601, "Method not found", error), request.id)
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(-32602, "Invalid params", error), request.id)
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(-1, error.message, error), request.id)
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(-1, "Cannot call unauthorized methods"), request.id)
280
+ JsonRpcError.new(error_code, "Cannot call unauthorized methods"), request.id)
362
281
  end
363
282
  end
364
- process_response(response, my_socket, start_time) if response
283
+ response_data = process_response(response, start_time) if response
284
+ return response_data, error_code
365
285
  rescue => error
366
- response = JsonRpcErrorResponse.new(JsonRpcError.new(-32600, "Invalid Request", error), nil)
367
- process_response(response, my_socket, start_time)
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
- def process_response(response, socket, start_time)
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
- rescue
380
- # Socket was closed?
381
- return false
301
+ return response_data
382
302
  end
383
303
 
384
304
  end