cosmos 4.0.3 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 +14 -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