openc3 5.11.3 → 5.13.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of openc3 might be problematic. Click here for more details.

Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/bin/openc3cli +29 -15
  4. data/data/config/_id_items.yaml +6 -4
  5. data/data/config/_id_params.yaml +9 -6
  6. data/data/config/_items.yaml +6 -4
  7. data/data/config/_params.yaml +3 -2
  8. data/data/config/graph_settings.yaml +1 -1
  9. data/data/config/interface_modifiers.yaml +1 -1
  10. data/data/config/item_modifiers.yaml +1 -2
  11. data/data/config/microservice.yaml +10 -1
  12. data/data/config/parameter_modifiers.yaml +13 -14
  13. data/data/config/plugins.yaml +13 -3
  14. data/data/config/screen.yaml +1 -2
  15. data/data/config/target.yaml +9 -0
  16. data/data/config/target_config.yaml +14 -6
  17. data/data/config/tool.yaml +12 -3
  18. data/lib/openc3/api/api.rb +1 -1
  19. data/lib/openc3/api/cmd_api.rb +123 -59
  20. data/lib/openc3/api/config_api.rb +12 -12
  21. data/lib/openc3/api/limits_api.rb +4 -3
  22. data/lib/openc3/api/settings_api.rb +5 -2
  23. data/lib/openc3/api/tlm_api.rb +70 -34
  24. data/lib/openc3/conversions/unix_time_conversion.rb +8 -6
  25. data/lib/openc3/interfaces/mqtt_interface.rb +11 -9
  26. data/lib/openc3/interfaces/mqtt_stream_interface.rb +78 -0
  27. data/lib/openc3/interfaces/tcpip_server_interface.rb +0 -7
  28. data/lib/openc3/io/json_drb.rb +3 -2
  29. data/lib/openc3/io/json_rpc.rb +6 -6
  30. data/lib/openc3/logs/buffered_packet_log_writer.rb +4 -2
  31. data/lib/openc3/logs/packet_log_reader.rb +2 -2
  32. data/lib/openc3/logs/packet_log_writer.rb +22 -7
  33. data/lib/openc3/logs/text_log_writer.rb +3 -2
  34. data/lib/openc3/microservices/cleanup_microservice.rb +8 -1
  35. data/lib/openc3/microservices/decom_microservice.rb +1 -1
  36. data/lib/openc3/microservices/interface_microservice.rb +2 -2
  37. data/lib/openc3/microservices/microservice.rb +5 -2
  38. data/lib/openc3/microservices/reaction_microservice.rb +1 -0
  39. data/lib/openc3/microservices/timeline_microservice.rb +7 -5
  40. data/lib/openc3/microservices/trigger_group_microservice.rb +2 -1
  41. data/lib/openc3/migrations/20231022000000_tlm_viewer_config.rb +22 -0
  42. data/lib/openc3/models/activity_model.rb +21 -3
  43. data/lib/openc3/models/cvt_model.rb +2 -1
  44. data/lib/openc3/models/gem_model.rb +4 -1
  45. data/lib/openc3/models/interface_model.rb +11 -5
  46. data/lib/openc3/models/metadata_model.rb +11 -0
  47. data/lib/openc3/models/microservice_model.rb +16 -3
  48. data/lib/openc3/models/model.rb +18 -0
  49. data/lib/openc3/models/note_model.rb +11 -0
  50. data/lib/openc3/models/plugin_model.rb +56 -4
  51. data/lib/openc3/models/python_package_model.rb +104 -0
  52. data/lib/openc3/models/scope_model.rb +2 -0
  53. data/lib/openc3/models/sorted_model.rb +17 -8
  54. data/lib/openc3/models/target_model.rb +53 -18
  55. data/lib/openc3/models/tool_config_model.rb +9 -3
  56. data/lib/openc3/models/tool_model.rb +22 -7
  57. data/lib/openc3/models/widget_model.rb +19 -3
  58. data/lib/openc3/operators/microservice_operator.rb +2 -0
  59. data/lib/openc3/packets/json_packet.rb +46 -15
  60. data/lib/openc3/packets/limits.rb +6 -18
  61. data/lib/openc3/packets/packet.rb +1 -0
  62. data/lib/openc3/packets/packet_config.rb +2 -1
  63. data/lib/openc3/packets/parsers/format_string_parser.rb +4 -4
  64. data/lib/openc3/packets/parsers/limits_parser.rb +4 -4
  65. data/lib/openc3/packets/parsers/limits_response_parser.rb +5 -5
  66. data/lib/openc3/packets/parsers/processor_parser.rb +4 -4
  67. data/lib/openc3/packets/parsers/state_parser.rb +3 -3
  68. data/lib/openc3/packets/parsers/xtce_parser.rb +5 -1
  69. data/lib/openc3/script/api_shared.rb +81 -63
  70. data/lib/openc3/script/calendar.rb +109 -0
  71. data/lib/openc3/script/commands.rb +18 -19
  72. data/lib/openc3/script/limits.rb +1 -1
  73. data/lib/openc3/script/{gems.rb → packages.rb} +20 -16
  74. data/lib/openc3/script/script.rb +49 -38
  75. data/lib/openc3/script/storage.rb +4 -4
  76. data/lib/openc3/script/web_socket_api.rb +2 -2
  77. data/lib/openc3/streams/mqtt_stream.rb +109 -0
  78. data/lib/openc3/system/system.rb +2 -0
  79. data/lib/openc3/system/target.rb +10 -1
  80. data/lib/openc3/top_level.rb +2 -2
  81. data/lib/openc3/utilities/aws_bucket.rb +3 -2
  82. data/lib/openc3/utilities/bucket_file_cache.rb +1 -1
  83. data/lib/openc3/utilities/cli_generator.rb +33 -20
  84. data/lib/openc3/utilities/local_mode.rb +5 -3
  85. data/lib/openc3/utilities/logger.rb +18 -17
  86. data/lib/openc3/utilities/process_manager.rb +1 -1
  87. data/lib/openc3/utilities/ruby_lex_utils.rb +0 -8
  88. data/lib/openc3/version.rb +6 -6
  89. data/templates/conversion/conversion.py +28 -0
  90. data/templates/conversion/conversion.rb +1 -18
  91. data/templates/limits_response/response.py +37 -0
  92. data/templates/limits_response/response.rb +0 -17
  93. data/templates/microservice/microservices/TEMPLATE/microservice.py +54 -0
  94. data/templates/microservice/microservices/TEMPLATE/microservice.rb +0 -7
  95. data/templates/plugin/.gitignore +1 -0
  96. data/templates/target/targets/TARGET/lib/target.py +9 -0
  97. data/templates/target/targets/TARGET/procedures/procedure.py +3 -0
  98. data/templates/tool_angular/package.json +22 -21
  99. data/templates/tool_angular/yarn.lock +2319 -3156
  100. data/templates/tool_react/package.json +16 -16
  101. data/templates/tool_react/yarn.lock +763 -645
  102. data/templates/tool_svelte/package.json +15 -14
  103. data/templates/tool_svelte/src/services/openc3-api.js +33 -82
  104. data/templates/tool_svelte/yarn.lock +748 -538
  105. data/templates/tool_vue/package.json +15 -14
  106. data/templates/tool_vue/yarn.lock +150 -64
  107. data/templates/widget/package.json +14 -13
  108. data/templates/widget/yarn.lock +133 -58
  109. metadata +60 -7
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2023, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -79,7 +79,7 @@ module OpenC3
79
79
  end
80
80
 
81
81
  def initialize(name, is_plugin: false)
82
- Logger.info("Microservice running from: ruby #{$0} #{ARGV.join(" ")}")
82
+ @shutdown_complete = false
83
83
  raise "Microservice must be named" unless name
84
84
 
85
85
  @name = name
@@ -120,6 +120,7 @@ module OpenC3
120
120
  # Get configuration for any targets
121
121
  @target_names = @config["target_names"]
122
122
  @target_names ||= []
123
+ # NOTE: setup_targets doesn't do anything if @target_names is empty
123
124
  System.setup_targets(@target_names, @temp_dir, scope: @scope) unless is_plugin
124
125
 
125
126
  # Use at_exit to shutdown cleanly no matter how we die
@@ -199,6 +200,7 @@ module OpenC3
199
200
  end
200
201
 
201
202
  def shutdown
203
+ return if @shutdown_complete
202
204
  @logger.info("Shutting down microservice: #{@name}")
203
205
  @cancel_thread = true
204
206
  @microservice_status_sleeper.cancel if @microservice_status_sleeper
@@ -206,6 +208,7 @@ module OpenC3
206
208
  FileUtils.remove_entry(@temp_dir) if File.exist?(@temp_dir)
207
209
  @metric.shutdown
208
210
  @logger.info("Shutting down microservice complete: #{@name}")
211
+ @shutdown_complete = true
209
212
  end
210
213
  end
211
214
  end
@@ -229,6 +229,7 @@ module OpenC3
229
229
 
230
230
  def get_token(username)
231
231
  if ENV['OPENC3_API_CLIENT'].nil?
232
+ ENV['OPENC3_API_PASSWORD'] ||= ENV['OPENC3_SERVICE_PASSWORD']
232
233
  return OpenC3Authentication.new().token
233
234
  else
234
235
  # Check for offline access token
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2023, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -43,6 +43,7 @@ module OpenC3
43
43
 
44
44
  def get_token(username)
45
45
  if ENV['OPENC3_API_CLIENT'].nil?
46
+ ENV['OPENC3_API_PASSWORD'] ||= ENV['OPENC3_SERVICE_PASSWORD']
46
47
  return OpenC3Authentication.new().token
47
48
  else
48
49
  # Check for offline access token
@@ -125,7 +126,8 @@ module OpenC3
125
126
 
126
127
  def clear_expired(activity)
127
128
  begin
128
- ActivityModel.range_destroy(name: @timeline_name, scope: @scope, min: activity.start, max: activity.stop)
129
+ num = ActivityModel.range_destroy(name: @timeline_name, scope: @scope, min: activity.start, max: activity.stop)
130
+ @logger.info "#{@timeline_name} clear_expired removed #{num} items from #{activity.start} to #{activity.stop}"
129
131
  activity.add_event(status: 'completed')
130
132
  rescue StandardError => e
131
133
  @logger.error "#{@timeline_name} clear_expired failed > #{activity.as_json(:allow_nan => true)} #{e.message}"
@@ -231,15 +233,15 @@ module OpenC3
231
233
  @logger.info "#{@timeline_name} timeine manager exiting"
232
234
  end
233
235
 
234
- # Add task to remove events older than 7 time
236
+ # Add task to remove events older than 7 days
235
237
  def add_expire_activity
236
238
  now = Time.now.to_i
237
239
  @expire = now + 3_000
238
240
  activity = ActivityModel.new(
239
241
  name: @timeline_name,
240
242
  scope: @scope,
241
- start: (now - 86_400 * 7),
242
- stop: (now - 82_800 * 7),
243
+ start: 0,
244
+ stop: (now - 86_400 * 7),
243
245
  kind: 'EXPIRE',
244
246
  data: {}
245
247
  )
@@ -333,7 +333,8 @@ module OpenC3
333
333
  )
334
334
  return nil if packet.nil?
335
335
  _, limit = packet.read_with_limits_state(operand[ITEM_TYPE], operand[ITEM_VALUE_TYPE].intern)
336
- return limit
336
+ # Convert limit symbol to string since we'll be comparing with strings
337
+ return limit.to_s
337
338
  end
338
339
 
339
340
  # extract the value outlined in the operand to get the packet item value
@@ -0,0 +1,22 @@
1
+ require 'openc3/utilities/migration'
2
+ require 'openc3/models/tool_config_model'
3
+
4
+ module OpenC3
5
+ class TlmViewerConfig < Migration
6
+ def self.run
7
+ ScopeModel.names.each do |scope|
8
+ # Get all existing ToolConfigModels and change keys from tlm_viewer to telemetry_viewer
9
+ names = ToolConfigModel.list_configs('tlm_viewer')
10
+ names.each do |name|
11
+ config = ToolConfigModel.load_config('tlm_viewer', name)
12
+ ToolConfigModel.save_config('telemetry_viewer', name, config)
13
+ ToolConfigModel.delete_config('tlm_viewer', name)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ unless ENV['OPENC3_NO_MIGRATE']
21
+ OpenC3::TlmViewerConfig.run
22
+ end
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2023, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -94,13 +94,31 @@ module OpenC3
94
94
  # Remove one member from a sorted set.
95
95
  # @return [Integer] count of the members removed
96
96
  def self.destroy(name:, scope:, score:)
97
- Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score)
97
+ result = Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score)
98
+ notification = {
99
+ # start / stop to match SortedModel
100
+ 'data' => JSON.generate({'start' => score}),
101
+ 'kind' => 'deleted',
102
+ 'type' => 'activity',
103
+ 'timeline' => name
104
+ }
105
+ TimelineTopic.write_activity(notification, scope: scope)
106
+ return result
98
107
  end
99
108
 
100
109
  # Remove members from min to max of the sorted set.
101
110
  # @return [Integer] count of the members removed
102
111
  def self.range_destroy(name:, scope:, min:, max:)
103
- Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", min, max)
112
+ result = Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", min, max)
113
+ notification = {
114
+ # start / stop to match SortedModel
115
+ 'data' => JSON.generate({'start' => min, 'stop' => max}),
116
+ 'kind' => 'deleted',
117
+ 'type' => 'activity',
118
+ 'timeline' => name
119
+ }
120
+ TimelineTopic.write_activity(notification, scope: scope)
121
+ return result
104
122
  end
105
123
 
106
124
  # @return [ActivityModel] Model generated from the passed JSON
@@ -237,10 +237,11 @@ module OpenC3
237
237
  end
238
238
 
239
239
  tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
240
- @@override_cache[tgt_pkt_key] = [Time.now, hash]
241
240
  if hash.empty?
241
+ @@override_cache.delete(tgt_pkt_key)
242
242
  Store.hdel("#{scope}__override__#{target_name}", packet_name)
243
243
  else
244
+ @@override_cache[tgt_pkt_key] = [Time.now, hash]
244
245
  Store.hset("#{scope}__override__#{target_name}", packet_name, JSON.generate(hash.as_json(:allow_nan => true)))
245
246
  end
246
247
  end
@@ -27,6 +27,7 @@ require 'rubygems'
27
27
  require 'rubygems/uninstaller'
28
28
  require 'tempfile'
29
29
  require 'openc3/utilities/process_manager'
30
+ require 'openc3/api/api'
30
31
  require 'pathname'
31
32
 
32
33
  module OpenC3
@@ -35,6 +36,8 @@ module OpenC3
35
36
  # and destroy to allow interaction with gem files from the PluginModel and
36
37
  # the GemsController.
37
38
  class GemModel
39
+ include Api
40
+
38
41
  def self.names
39
42
  result = Pathname.new("#{ENV['GEM_HOME']}/gems").children.select { |c| c.directory? }.collect { |p| File.basename(p) + '.gem' }
40
43
  return result.sort
@@ -53,7 +56,7 @@ module OpenC3
53
56
  FileUtils.cp(gem_file_path, "#{ENV['GEM_HOME']}/cache/#{File.basename(gem_file_path)}")
54
57
  if gem_install
55
58
  Logger.info "Installing gem: #{gem_filename}"
56
- result = OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "geminstall", gem_filename, scope], "gem_install", gem_filename, Time.now + 3600.0, scope: scope)
59
+ result = OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "geminstall", gem_filename, scope], "package_install", gem_filename, Time.now + 3600.0, scope: scope)
57
60
  return result.name
58
61
  end
59
62
  else
@@ -145,7 +145,13 @@ module OpenC3
145
145
  unless @cmd
146
146
  type = self.class._get_type
147
147
  microservice_name = "#{@scope}__#{type}__#{@name}"
148
- @cmd = ["ruby", "#{type.downcase}_microservice.rb", microservice_name]
148
+ if config_params[0] and File.extname(config_params[0]) == '.py'
149
+ work_dir.sub!('openc3/lib', 'openc3/python')
150
+ @cmd = ["python", "#{type.downcase}_microservice.py", microservice_name]
151
+ else
152
+ # If there are no config_params we assume ruby
153
+ @cmd = ["ruby", "#{type.downcase}_microservice.rb", microservice_name]
154
+ end
149
155
  end
150
156
  @work_dir = work_dir
151
157
  @ports = ports
@@ -417,7 +423,7 @@ module OpenC3
417
423
  # Respawn the microservice
418
424
  type = self.class._get_type
419
425
  microservice_name = "#{@scope}__#{type}__#{@name}"
420
- microservice = MicroserviceModel.get_model(name: microservice_name, scope: scope)
426
+ microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
421
427
  microservice.target_names.delete(target_name) unless @target_names.include?(target_name)
422
428
  microservice.update
423
429
  end
@@ -432,11 +438,11 @@ module OpenC3
432
438
 
433
439
  if unmap_old
434
440
  # Remove from old interface
435
- all_interfaces = InterfaceModel.all(scope: scope)
441
+ all_interfaces = InterfaceModel.all(scope: @scope)
436
442
  old_interface = nil
437
443
  all_interfaces.each do |old_interface_name, old_interface_details|
438
444
  if old_interface_details['target_names'].include?(target_name)
439
- old_interface = InterfaceModel.from_json(old_interface_details, scope: scope)
445
+ old_interface = InterfaceModel.from_json(old_interface_details, scope: @scope)
440
446
  old_interface.unmap_target(target_name, cmd_only: cmd_only, tlm_only: tlm_only) if old_interface
441
447
  end
442
448
  end
@@ -451,7 +457,7 @@ module OpenC3
451
457
  # Respawn the microservice
452
458
  type = self.class._get_type
453
459
  microservice_name = "#{@scope}__#{type}__#{@name}"
454
- microservice = MicroserviceModel.get_model(name: microservice_name, scope: scope)
460
+ microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
455
461
  microservice.target_names << target_name unless microservice.target_names.include?(target_name)
456
462
  microservice.update
457
463
  end
@@ -33,6 +33,17 @@ module OpenC3
33
33
  "#{scope}#{PRIMARY_KEY}"
34
34
  end
35
35
 
36
+ def self.notify(scope:, kind:, start:, stop: nil)
37
+ json = {'type' => METADATA_TYPE, 'start' => start}
38
+ json['stop'] = stop if stop
39
+ notification = {
40
+ 'data' => JSON.generate(json),
41
+ 'kind' => kind,
42
+ 'type' => 'calendar',
43
+ }
44
+ CalendarTopic.write_entry(notification, scope: scope)
45
+ end
46
+
36
47
  attr_reader :color, :metadata, :constraints, :type
37
48
 
38
49
  # @param [Integer] start - Time metadata is active in seconds from Epoch
@@ -42,6 +42,7 @@ module OpenC3
42
42
  attr_accessor :parent
43
43
  attr_accessor :secrets
44
44
  attr_accessor :prefix
45
+ attr_accessor :disable_erb
45
46
 
46
47
  # NOTE: The following three class methods are used by the ModelController
47
48
  # and are reimplemented to enable various Model class methods to work
@@ -101,6 +102,7 @@ module OpenC3
101
102
  needs_dependencies: false,
102
103
  secrets: [],
103
104
  prefix: nil,
105
+ disable_erb: nil,
104
106
  scope:
105
107
  )
106
108
  parts = name.split("__")
@@ -125,6 +127,7 @@ module OpenC3
125
127
  @needs_dependencies = needs_dependencies
126
128
  @secrets = secrets
127
129
  @prefix = prefix
130
+ @disable_erb = disable_erb
128
131
  @bucket = Bucket.getClient()
129
132
  end
130
133
 
@@ -145,7 +148,8 @@ module OpenC3
145
148
  'plugin' => @plugin,
146
149
  'needs_dependencies' => @needs_dependencies,
147
150
  'secrets' => @secrets.as_json(*a),
148
- 'prefix' => @prefix
151
+ 'prefix' => @prefix,
152
+ 'disable_erb' => @disable_erb
149
153
  }
150
154
  end
151
155
 
@@ -201,6 +205,12 @@ module OpenC3
201
205
  when 'ROUTE_PREFIX'
202
206
  parser.verify_num_parameters(1, 1, "#{keyword} <Route Prefix>")
203
207
  @prefix = parameters[0]
208
+ when 'DISABLE_ERB'
209
+ # 0 to unlimited parameters
210
+ @disable_erb ||= []
211
+ if parameters
212
+ @disable_erb.concat(parameters)
213
+ end
204
214
  else
205
215
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Microservice: #{keyword} #{parameters.join(" ")}")
206
216
  end
@@ -220,8 +230,11 @@ module OpenC3
220
230
 
221
231
  # Load microservice files
222
232
  data = File.read(filename, mode: "rb")
223
- OpenC3.set_working_dir(File.dirname(filename)) do
224
- data = ERB.new(data.comment_erb(), trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable? and File.basename(filename)[0] != '_'
233
+ erb_disabled = check_disable_erb(filename)
234
+ unless erb_disabled
235
+ OpenC3.set_working_dir(File.dirname(filename)) do
236
+ data = ERB.new(data.comment_erb(), trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable? and File.basename(filename)[0] != '_'
237
+ end
225
238
  end
226
239
  unless validate_only
227
240
  @bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: key, body: data)
@@ -182,6 +182,24 @@ module OpenC3
182
182
  'plugin' => @plugin,
183
183
  'scope' => @scope }
184
184
  end
185
+
186
+ def check_disable_erb(filename)
187
+ erb_disabled = false
188
+ if @disable_erb
189
+ if @disable_erb.length == 0
190
+ # Disable all ERB
191
+ erb_disabled = true
192
+ else
193
+ @disable_erb.each do |pattern|
194
+ if filename =~ Regexp.new(pattern)
195
+ erb_disabled = true
196
+ break
197
+ end
198
+ end
199
+ end
200
+ end
201
+ return erb_disabled
202
+ end
185
203
  end
186
204
 
187
205
  class EphemeralModel < Model
@@ -33,6 +33,17 @@ module OpenC3
33
33
  "#{scope}#{PRIMARY_KEY}"
34
34
  end
35
35
 
36
+ def self.notify(scope:, kind:, start:, stop: nil)
37
+ json = {'type' => NOTE_TYPE, 'start' => start}
38
+ json['stop'] = stop if stop
39
+ notification = {
40
+ 'data' => JSON.generate(json),
41
+ 'kind' => kind,
42
+ 'type' => 'calendar',
43
+ }
44
+ CalendarTopic.write_entry(notification, scope: scope)
45
+ end
46
+
36
47
  attr_reader :stop, :color, :description, :type
37
48
 
38
49
  # @param [String] scope - OpenC3 scope to track event to
@@ -34,6 +34,7 @@ require 'openc3/models/router_model'
34
34
  require 'openc3/models/tool_model'
35
35
  require 'openc3/models/widget_model'
36
36
  require 'openc3/models/microservice_model'
37
+ require 'openc3/api/api'
37
38
  require 'tmpdir'
38
39
  require 'tempfile'
39
40
  require 'fileutils'
@@ -43,6 +44,8 @@ module OpenC3
43
44
  # microservices and tools. The PluginModel installs all these pieces as well
44
45
  # as destroys them all when the plugin is removed.
45
46
  class PluginModel < Model
47
+ include Api
48
+
46
49
  PRIMARY_KEY = 'openc3_plugins'
47
50
  # Reserved VARIABLE names. See local_mode.rb: update_local_plugin()
48
51
  RESERVED_VARIABLE_NAMES = ['target_name', 'microservice_name', 'scope']
@@ -170,6 +173,21 @@ module OpenC3
170
173
  end
171
174
  needs_dependencies = pkg.spec.runtime_dependencies.length > 0
172
175
  needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
176
+
177
+ # Handle python requirements.txt
178
+ if File.exist?(File.join(gem_path, 'requirements.txt'))
179
+ begin
180
+ pypi_url = get_setting('pypi_url', scope: scope)
181
+ rescue
182
+ # If Redis isn't running try the ENV, then simply pypi.org/simple
183
+ pypi_url = ENV['PYPI_URL']
184
+ pypi_url ||= 'https://pypi.org/simple'
185
+ end
186
+ Logger.info "Installing python packages from requirements.txt"
187
+ puts `pip install --user -i #{pypi_url} -r #{File.join(gem_path, 'requirements.txt')}`
188
+ needs_dependencies = true
189
+ end
190
+
173
191
  # If needs_dependencies hasn't already been set we need to scan the plugin.txt
174
192
  # to see if they've explicitly set the NEEDS_DEPENDENCIES keyword
175
193
  unless needs_dependencies
@@ -278,10 +296,15 @@ module OpenC3
278
296
 
279
297
  # Undeploy all models associated with this plugin
280
298
  def undeploy
299
+ errors = []
281
300
  microservice_count = 0
282
301
  microservices = MicroserviceModel.find_all_by_plugin(plugin: @name, scope: @scope)
283
302
  microservices.each do |name, model_instance|
284
- model_instance.destroy
303
+ begin
304
+ model_instance.destroy
305
+ rescue Exception => error
306
+ errors << error
307
+ end
285
308
  microservice_count += 1
286
309
  end
287
310
  # Wait for the operator to wake up and remove the microservice processes
@@ -290,15 +313,44 @@ module OpenC3
290
313
  # Save TargetModel for last as it has the most to cleanup
291
314
  [InterfaceModel, RouterModel, ToolModel, WidgetModel, TargetModel].each do |model|
292
315
  model.find_all_by_plugin(plugin: @name, scope: @scope).each do |name, model_instance|
293
- model_instance.destroy
316
+ begin
317
+ model_instance.destroy
318
+ rescue Exception => error
319
+ errors << error
320
+ end
294
321
  end
295
322
  end
296
323
  # Cleanup Redis stuff that might have been left by microservices
297
324
  microservices.each do |name, model_instance|
298
- model_instance.cleanup
325
+ begin
326
+ model_instance.cleanup
327
+ rescue Exception => error
328
+ errors << error
329
+ end
330
+ end
331
+ # Raise all the errors at once
332
+ if errors.length > 0
333
+ message = ''
334
+ errors.each do |error|
335
+ message += "\n#{error.formatted}\n"
336
+ end
337
+ raise message
299
338
  end
300
339
  rescue Exception => error
301
- Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to #{error}")
340
+ Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to: #{error}")
341
+ ensure
342
+ # Double check everything is gone
343
+ found = []
344
+ [MicroserviceModel, InterfaceModel, RouterModel, ToolModel, WidgetModel, TargetModel].each do |model|
345
+ model.find_all_by_plugin(plugin: @name, scope: @scope).each do |name, model_instance|
346
+ found << model_instance
347
+ end
348
+ end
349
+ if found.length > 0
350
+ # If undeploy failed we need to not move forward with anything else
351
+ Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to: Plugin submodels still exist after undeploy = #{found.length}")
352
+ raise "Plugin #{@name} submodels still exist after undeploy = #{found.length}"
353
+ end
302
354
  end
303
355
 
304
356
  # Reinstall
@@ -0,0 +1,104 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2023 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ require 'fileutils'
20
+ require 'openc3/utilities/process_manager'
21
+ require 'pathname'
22
+
23
+ module OpenC3
24
+ # This class acts like a Model but doesn't inherit from Model because it doesn't
25
+ # actual interact with the Store (Redis). Instead we implement names, get, put
26
+ # and destroy to allow interaction with python package files from the PluginModel and
27
+ # the PackagesController.
28
+ class PythonPackageModel
29
+ def self.names
30
+ paths = Dir.glob("#{ENV['PYTHONUSERBASE']}/lib/*")
31
+ results = []
32
+ paths.each do |path|
33
+ results.concat(Pathname.new(File.join(path, 'site-packages')).children.select { |c| c.directory? and File.extname(c) == '.dist-info' }.collect { |p| File.basename(p, '.dist-info') })
34
+ end
35
+ return results.sort
36
+ end
37
+
38
+ def self.get(name)
39
+ path = "#{ENV['PYTHONUSERBASE']}/cache"
40
+ FileUtils.mkdir_p(path) unless Dir.exist?(path)
41
+ result = Pathname.new(path).children.select { |c| c.file? and File.basename(c, File.extname(c)) == name }
42
+ if result.length > 0
43
+ return result[0] if File.exist?(result[0])
44
+ end
45
+ raise "Package #{name} not found"
46
+ end
47
+
48
+ def self.put(package_file_path, package_install: true, scope:)
49
+ if File.file?(package_file_path)
50
+ package_filename = File.basename(package_file_path)
51
+ FileUtils.mkdir_p("#{ENV['PYTHONUSERBASE']}/cache") unless Dir.exist?("#{ENV['PYTHONUSERBASE']}/cache")
52
+ cache_path = "#{ENV['PYTHONUSERBASE']}/cache/#{File.basename(package_file_path)}"
53
+ FileUtils.cp(package_file_path, cache_path)
54
+ if package_install
55
+ return self.install(cache_path, scope: scope)
56
+ end
57
+ else
58
+ message = "Package file #{package_file_path} does not exist!"
59
+ Logger.error message
60
+ raise message
61
+ end
62
+ return nil
63
+ end
64
+
65
+ def self.install(name_or_path, scope:)
66
+ if File.exist?(name_or_path)
67
+ package_file_path = name_or_path
68
+ else
69
+ package_file_path = get(name_or_path)
70
+ end
71
+ package_filename = File.basename(package_file_path)
72
+ begin
73
+ pypi_url = get_setting('pypi_url', scope: scope)
74
+ rescue
75
+ # If Redis isn't running try the ENV, then simply pypi.org/simple
76
+ pypi_url = ENV['PYPI_URL']
77
+ pypi_url ||= 'https://pypi.org/simple'
78
+ end
79
+ Logger.info "Installing python package: #{name_or_path}"
80
+ result = OpenC3::ProcessManager.instance.spawn(["pip", "install", "--user", "-i", pypi_url, package_file_path], "package_install", package_filename, Time.now + 3600.0, scope: scope)
81
+ return result.name
82
+ end
83
+
84
+ def self.destroy(name, scope:)
85
+ package_name, version = self.extract_name_and_version(name)
86
+ Logger.info "Uninstalling package: #{name}"
87
+ result = OpenC3::ProcessManager.instance.spawn(["pip", "uninstall", package_name, "-y"], "package_uninstall", name, Time.now + 3600.0, scope: scope)
88
+ return result.name
89
+ end
90
+
91
+ def self.extract_name_and_version(name)
92
+ split_name = name.split('-')
93
+ if split_name.length > 1
94
+ package_name = split_name[0..-2].join('-')
95
+ version = File.basename(split_name[-1], '.dist-info')
96
+ else
97
+ package_name = name
98
+ version = "Unknown"
99
+ end
100
+
101
+ return package_name, version
102
+ end
103
+ end
104
+ end
@@ -305,6 +305,8 @@ module OpenC3
305
305
  SettingModel.set({ name: 'source_url', data: 'https://github.com/OpenC3/cosmos' }, scope: @scope) unless setting
306
306
  setting = SettingModel.get(name: 'rubygems_url')
307
307
  SettingModel.set({ name: 'rubygems_url', data: 'https://rubygems.org' }, scope: @scope) unless setting
308
+ setting = SettingModel.get(name: 'pypi_url')
309
+ SettingModel.set({ name: 'pypi_url', data: 'https://pypi.org/simple' }, scope: @scope) unless setting
308
310
  end
309
311
  end
310
312
  end
@@ -14,10 +14,10 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2023, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
- # This file may also be used under the terms of a commercial license
20
+ # This file may also be used under the terms of a commercial license
21
21
  # if purchased from OpenC3, Inc.
22
22
 
23
23
  # https://www.rubydoc.info/gems/redis/Redis/Commands/SortedSets
@@ -39,7 +39,12 @@ module OpenC3
39
39
 
40
40
  # MUST be overriden by any subclasses
41
41
  def self.pk(scope)
42
- "#{scope}#{PRIMARY_KEY}"
42
+ return "#{scope}#{PRIMARY_KEY}"
43
+ end
44
+
45
+ # MUST be overriden by any subclasses
46
+ def self.notify(scope:, kind:, start:, stop: nil)
47
+ # Do nothing by default
43
48
  end
44
49
 
45
50
  # @return [String|nil] String of the saved json or nil if start not found
@@ -52,7 +57,7 @@ module OpenC3
52
57
  # @return [Array<Hash>] Array up to the limit of the models (as Hash objects) stored under the primary key
53
58
  def self.all(scope:, limit: 100)
54
59
  result = Store.zrevrangebyscore(self.pk(scope), '+inf', '-inf', limit: [0, limit])
55
- result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
60
+ return result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
56
61
  end
57
62
 
58
63
  # @return [String|nil] json or nil if metadata empty
@@ -71,24 +76,28 @@ module OpenC3
71
76
  raise SortedInputError.new "start: #{start} must be before stop: #{stop}"
72
77
  end
73
78
  result = Store.zrangebyscore(self.pk(scope), start, stop, limit: [0, limit])
74
- result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
79
+ return result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
75
80
  end
76
81
 
77
82
  # @return [Integer] count of the members stored under the primary key
78
83
  def self.count(scope:)
79
- Store.zcard(self.pk(scope))
84
+ return Store.zcard(self.pk(scope))
80
85
  end
81
86
 
82
87
  # Remove member from a sorted set
83
88
  # @return [Integer] count of the members removed, 0 if not found
84
89
  def self.destroy(scope:, start:)
85
- Store.zremrangebyscore(self.pk(scope), start, start)
90
+ result = Store.zremrangebyscore(self.pk(scope), start, start)
91
+ self.notify(kind: 'deleted', start: start, scope: scope)
92
+ return result
86
93
  end
87
94
 
88
95
  # Remove members from min to max of the sorted set.
89
96
  # @return [Integer] count of the members removed
90
97
  def self.range_destroy(scope:, start:, stop:)
91
- Store.zremrangebyscore(self.pk(scope), start, stop)
98
+ result = Store.zremrangebyscore(self.pk(scope), start, stop)
99
+ self.notify(kind: 'deleted', start: start, stop: stop, scope: scope)
100
+ return result
92
101
  end
93
102
 
94
103
  attr_reader :start