openc3 6.6.0 → 6.8.0
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.
- checksums.yaml +4 -4
- data/bin/openc3cli +77 -16
- data/data/config/command_modifiers.yaml +3 -3
- data/data/config/interface_modifiers.yaml +1 -1
- data/data/config/table_manager.yaml +1 -1
- data/data/config/telemetry_modifiers.yaml +3 -3
- data/data/config/widgets.yaml +2 -2
- data/lib/openc3/accessors.rb +1 -1
- data/lib/openc3/api/cmd_api.rb +15 -4
- data/lib/openc3/api/settings_api.rb +8 -0
- data/lib/openc3/api/stash_api.rb +1 -1
- data/lib/openc3/api/tlm_api.rb +96 -14
- data/lib/openc3/core_ext/kernel.rb +3 -3
- data/lib/openc3/logs/log_writer.rb +16 -12
- data/lib/openc3/microservices/interface_microservice.rb +14 -1
- data/lib/openc3/microservices/plugin_microservice.rb +2 -2
- data/lib/openc3/microservices/queue_microservice.rb +166 -0
- data/lib/openc3/models/cvt_model.rb +140 -3
- data/lib/openc3/models/plugin_model.rb +7 -2
- data/lib/openc3/models/plugin_store_model.rb +70 -0
- data/lib/openc3/models/queue_model.rb +232 -0
- data/lib/openc3/models/target_model.rb +26 -0
- data/lib/openc3/models/tool_model.rb +1 -1
- data/lib/openc3/packets/packet.rb +3 -3
- data/lib/openc3/packets/parsers/state_parser.rb +7 -1
- data/lib/openc3/packets/structure.rb +9 -2
- data/lib/openc3/script/calendar.rb +10 -10
- data/lib/openc3/script/commands.rb +4 -4
- data/lib/openc3/script/queue.rb +80 -0
- data/lib/openc3/script/script.rb +1 -0
- data/lib/openc3/script/script_runner.rb +7 -2
- data/lib/openc3/script/tables.rb +3 -3
- data/lib/openc3/script/web_socket_api.rb +11 -0
- data/lib/openc3/topics/queue_topic.rb +29 -0
- data/lib/openc3/utilities/authorization.rb +1 -1
- data/lib/openc3/utilities/cosmos_rails_formatter.rb +1 -1
- data/lib/openc3/utilities/local_mode.rb +2 -0
- data/lib/openc3/utilities/logger.rb +1 -1
- data/lib/openc3/utilities/running_script.rb +5 -1
- data/lib/openc3/version.rb +5 -5
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -3
- data/templates/widget/package.json +2 -2
- metadata +83 -8
@@ -0,0 +1,166 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
# Copyright 2025 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 'openc3/microservices/microservice'
|
20
|
+
require 'openc3/topics/queue_topic'
|
21
|
+
require 'openc3/utilities/authentication'
|
22
|
+
require 'openc3/script'
|
23
|
+
|
24
|
+
module OpenC3
|
25
|
+
# The queue processor runs in a single thread and processes commands via cmd_api.
|
26
|
+
class QueueProcessor
|
27
|
+
attr_accessor :state
|
28
|
+
attr_reader :name, :scope
|
29
|
+
|
30
|
+
def initialize(name:, state:, logger:, scope:)
|
31
|
+
@name = name
|
32
|
+
@logger = logger
|
33
|
+
@scope = scope
|
34
|
+
@state = state
|
35
|
+
@cancel_thread = false
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_token(username)
|
39
|
+
if ENV['OPENC3_API_CLIENT'].nil?
|
40
|
+
ENV['OPENC3_API_PASSWORD'] ||= ENV['OPENC3_SERVICE_PASSWORD']
|
41
|
+
return OpenC3Authentication.new().token
|
42
|
+
else
|
43
|
+
# Check for offline access token
|
44
|
+
model = nil
|
45
|
+
model = OpenC3::OfflineAccessModel.get_model(name: username, scope: @scope) if username and username != ''
|
46
|
+
if model and model.offline_access_token
|
47
|
+
auth = OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
|
48
|
+
return auth.get_token_from_refresh_token(model.offline_access_token)
|
49
|
+
else
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def run
|
56
|
+
while true
|
57
|
+
if @state == 'RELEASE'
|
58
|
+
process_queued_commands()
|
59
|
+
else
|
60
|
+
sleep 0.2
|
61
|
+
end
|
62
|
+
break if @cancel_thread
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def process_queued_commands
|
67
|
+
while @state == 'RELEASE'
|
68
|
+
begin
|
69
|
+
_queue_name, command_data, _timestamp = Store.bzpopmin("#{@scope}:#{@name}", timeout: 0.2)
|
70
|
+
if command_data
|
71
|
+
command = JSON.parse(command_data)
|
72
|
+
username = command['username']
|
73
|
+
token = get_token(username)
|
74
|
+
# It's important to set queue: false here to avoid infinite recursion when
|
75
|
+
# OPENC3_DEFAULT_QUEUE is set because commands would be re-queued to the default queue
|
76
|
+
cmd_no_hazardous_check(command['value'], queue: false, scope: @scope, token: token)
|
77
|
+
end
|
78
|
+
rescue StandardError => e
|
79
|
+
@logger.error "QueueProcessor failed to process command from queue #{@name}\n#{e.message}"
|
80
|
+
end
|
81
|
+
break if @cancel_thread
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def shutdown
|
86
|
+
@cancel_thread = true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# The queue microservice starts a processor then gets the queue entries from redis.
|
91
|
+
# It then monitors the QueueTopic for changes.
|
92
|
+
class QueueMicroservice < Microservice
|
93
|
+
attr_reader :name, :processor, :processor_thread
|
94
|
+
|
95
|
+
def initialize(*args)
|
96
|
+
super(*args)
|
97
|
+
@queue_name = @name.split('__')[2]
|
98
|
+
|
99
|
+
initial_state = 'HOLD'
|
100
|
+
(@config['options'] || []).each do |option|
|
101
|
+
case option[0].upcase
|
102
|
+
when 'QUEUE_STATE'
|
103
|
+
initial_state = option[1]
|
104
|
+
else
|
105
|
+
@logger.error("Unknown option passed to microservice #{@name}: #{option}")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
@processor = QueueProcessor.new(name: @queue_name, state: initial_state, logger: @logger, scope: @scope)
|
110
|
+
@processor_thread = nil
|
111
|
+
@read_topic = true
|
112
|
+
end
|
113
|
+
|
114
|
+
def run
|
115
|
+
@logger.info "QueueMicroservice running"
|
116
|
+
@processor_thread = Thread.new { @processor.run }
|
117
|
+
|
118
|
+
# Let the frontend know that the microservice has been deployed and is running
|
119
|
+
notification = {
|
120
|
+
'kind' => 'deployed',
|
121
|
+
# name and updated_at fields are required for Event formatting
|
122
|
+
'data' => JSON.generate({
|
123
|
+
'name' => @name,
|
124
|
+
'updated_at' => Time.now.to_nsec_from_epoch,
|
125
|
+
}),
|
126
|
+
}
|
127
|
+
QueueTopic.write_notification(notification, scope: @scope)
|
128
|
+
|
129
|
+
loop do
|
130
|
+
break if @cancel_thread
|
131
|
+
block_for_updates()
|
132
|
+
end
|
133
|
+
@processor.shutdown()
|
134
|
+
@processor_thread.join() if @processor_thread
|
135
|
+
@logger.info "QueueMicroservice exiting"
|
136
|
+
end
|
137
|
+
|
138
|
+
def block_for_updates
|
139
|
+
@read_topic = true
|
140
|
+
while @read_topic && !@cancel_thread
|
141
|
+
begin
|
142
|
+
QueueTopic.read_topics(@topics) do |_topic, _msg_id, msg_hash, _redis|
|
143
|
+
data = JSON.parse(msg_hash['data']) if msg_hash['data']
|
144
|
+
if data['name'] == @queue_name and msg_hash['kind'] == 'updated'
|
145
|
+
@processor.state = data['state']
|
146
|
+
end
|
147
|
+
end
|
148
|
+
rescue StandardError => e
|
149
|
+
@logger.error "QueueMicroservice failed to read topics #{@topics}\n#{e.formatted}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def shutdown
|
155
|
+
@read_topic = false
|
156
|
+
@processor.shutdown() if @processor
|
157
|
+
super
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
if __FILE__ == $0
|
163
|
+
OpenC3::QueueMicroservice.run
|
164
|
+
OpenC3::ThreadManager.instance.shutdown
|
165
|
+
OpenC3::ThreadManager.instance.join
|
166
|
+
end
|
@@ -14,12 +14,14 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2025, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
21
21
|
# if purchased from OpenC3, Inc.
|
22
22
|
|
23
|
+
require 'pg'
|
24
|
+
require 'thread'
|
23
25
|
require 'openc3/utilities/store'
|
24
26
|
require 'openc3/utilities/store_queued'
|
25
27
|
require 'openc3/models/target_model'
|
@@ -28,6 +30,8 @@ module OpenC3
|
|
28
30
|
class CvtModel
|
29
31
|
@@packet_cache = {}
|
30
32
|
@@override_cache = {}
|
33
|
+
@@conn = nil
|
34
|
+
@@conn_mutex = Mutex.new
|
31
35
|
|
32
36
|
VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED, :WITH_UNITS]
|
33
37
|
def self.build_json_from_packet(packet)
|
@@ -117,22 +121,149 @@ module OpenC3
|
|
117
121
|
end
|
118
122
|
end
|
119
123
|
|
124
|
+
def self.tsdb_lookup(items, start_time:, end_time: nil)
|
125
|
+
tables = {}
|
126
|
+
names = []
|
127
|
+
nil_count = 0
|
128
|
+
items.each do |item|
|
129
|
+
target_name, packet_name, item_name, value_type, limits = item
|
130
|
+
# They will all be nil when item is a nil value
|
131
|
+
# A nil value indicates a value that does not exist as returned by get_tlm_available
|
132
|
+
if item_name.nil?
|
133
|
+
# We know timestamp always exists so we can use it to fill in the nil value
|
134
|
+
names << "timestamp as __nil#{nil_count}"
|
135
|
+
nil_count += 1
|
136
|
+
next
|
137
|
+
end
|
138
|
+
# See https://questdb.com/docs/reference/api/ilp/advanced-settings/#name-restrictions
|
139
|
+
table_name = "#{target_name}__#{packet_name}".gsub(/[?,'"\/:\)\(\+\*\%~]/, '_')
|
140
|
+
tables[table_name] = 1
|
141
|
+
index = tables.find_index {|k,v| k == table_name }
|
142
|
+
# See https://questdb.com/docs/reference/api/ilp/advanced-settings/#name-restrictions
|
143
|
+
# NOTE: Semicolon added as it appears invalid
|
144
|
+
item_name = item_name.gsub(/[?\.,'"\\\/:\)\(\+\-\*\%~;]/, '_')
|
145
|
+
case value_type
|
146
|
+
when 'WITH_UNITS'
|
147
|
+
names << "\"T#{index}.#{item_name}__U\""
|
148
|
+
when 'FORMATTED'
|
149
|
+
names << "\"T#{index}.#{item_name}__F\""
|
150
|
+
when 'CONVERTED'
|
151
|
+
names << "\"T#{index}.#{item_name}__C\""
|
152
|
+
else
|
153
|
+
names << "\"T#{index}.#{item_name}\""
|
154
|
+
end
|
155
|
+
if limits
|
156
|
+
names << "\"T#{index}.#{item_name}__L\""
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Build the SQL query
|
161
|
+
query = "SELECT #{names.join(", ")} FROM "
|
162
|
+
tables.each_with_index do |(table_name, _), index|
|
163
|
+
if index == 0
|
164
|
+
query += "#{table_name} as T#{index} "
|
165
|
+
else
|
166
|
+
query += "ASOF JOIN #{table_name} as T#{index} "
|
167
|
+
end
|
168
|
+
end
|
169
|
+
if start_time && !end_time
|
170
|
+
query += "WHERE T0.timestamp < '#{start_time}' LIMIT -1"
|
171
|
+
elsif start_time && end_time
|
172
|
+
query += "WHERE T0.timestamp >= '#{start_time}' AND T0.timestamp < '#{end_time}'"
|
173
|
+
end
|
174
|
+
|
175
|
+
retry_count = 0
|
176
|
+
begin
|
177
|
+
@@conn_mutex.synchronize do
|
178
|
+
@@conn ||= PG::Connection.new(host: ENV['OPENC3_TSDB_HOSTNAME'],
|
179
|
+
port: ENV['OPENC3_TSDB_QUERY_PORT'],
|
180
|
+
user: ENV['OPENC3_TSDB_USERNAME'],
|
181
|
+
password: ENV['OPENC3_TSDB_PASSWORD'],
|
182
|
+
dbname: 'qdb')
|
183
|
+
# Default connection is all strings but we want to map to the correct types
|
184
|
+
if @@conn.type_map_for_results.is_a? PG::TypeMapAllStrings
|
185
|
+
# TODO: This doesn't seem to be round tripping UINT64 correctly
|
186
|
+
# Try playback with P_2.2,2 and P(:6;): from the DEMO
|
187
|
+
@@conn.type_map_for_results = PG::BasicTypeMapForResults.new @@conn
|
188
|
+
end
|
189
|
+
|
190
|
+
result = @@conn.exec(query)
|
191
|
+
if result.nil? or result.ntuples == 0
|
192
|
+
return {}
|
193
|
+
else
|
194
|
+
data = []
|
195
|
+
# Build up a results set that is an array of arrays
|
196
|
+
# Each nested array is a set of 2 items: [value, limits state]
|
197
|
+
# If the item does not have limits the limits state is nil
|
198
|
+
result.each_with_index do |tuples, index|
|
199
|
+
data[index] ||= []
|
200
|
+
row_index = 0
|
201
|
+
tuples.each do |tuple|
|
202
|
+
if tuple[0].include?("__L")
|
203
|
+
data[index][row_index - 1][1] = tuple[1]
|
204
|
+
elsif tuple[0] =~ /^__nil/
|
205
|
+
data[index][row_index] = [nil, nil]
|
206
|
+
row_index += 1
|
207
|
+
else
|
208
|
+
data[index][row_index] = [tuple[1], nil]
|
209
|
+
row_index += 1
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
# If we only have one row then we return a single array
|
214
|
+
if result.ntuples == 1
|
215
|
+
data = data[0]
|
216
|
+
end
|
217
|
+
return data
|
218
|
+
end
|
219
|
+
end
|
220
|
+
rescue IOError, PG::Error => e
|
221
|
+
# Retry the query because various errors can occur that are recoverable
|
222
|
+
retry_count += 1
|
223
|
+
if retry_count > 4
|
224
|
+
# After the 5th retry just raise the error
|
225
|
+
raise "Error querying QuestDB: #{e.message}"
|
226
|
+
end
|
227
|
+
Logger.warn("QuestDB: Retrying due to error: #{e.message}")
|
228
|
+
Logger.warn("QuestDB: Last query: #{query}") # Log the last query for debugging
|
229
|
+
@@conn_mutex.synchronize do
|
230
|
+
if @@conn and !@@conn.finished?
|
231
|
+
@@conn.finish()
|
232
|
+
end
|
233
|
+
@@conn = nil # Force the new connection
|
234
|
+
end
|
235
|
+
sleep 0.1
|
236
|
+
retry
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
120
240
|
# Return all item values and limit state from the CVT
|
121
241
|
#
|
122
242
|
# @param items [Array<String>] Items to return. Must be formatted as TGT__PKT__ITEM__TYPE
|
123
243
|
# @param stale_time [Integer] Time in seconds from Time.now that value will be marked stale
|
124
244
|
# @return [Array] Array of values
|
125
|
-
def self.get_tlm_values(items, stale_time: 30, cache_timeout: nil, scope: $openc3_scope)
|
245
|
+
def self.get_tlm_values(items, stale_time: 30, cache_timeout: nil, start_time: nil, end_time: nil, scope: $openc3_scope)
|
126
246
|
now = Time.now
|
127
247
|
results = []
|
128
248
|
lookups = []
|
129
249
|
packet_lookup = {}
|
130
250
|
overrides = {}
|
251
|
+
|
252
|
+
# If a start_time is passed we're doing a QuestDB lookup and directly return the results
|
253
|
+
# TODO: This currently does NOT support the override values
|
254
|
+
if start_time
|
255
|
+
return tsdb_lookup(items, start_time: start_time, end_time: end_time)
|
256
|
+
end
|
257
|
+
|
131
258
|
# First generate a lookup hash of all the items represented so we can query the CVT
|
132
259
|
items.each { |item| _parse_item(now, lookups, overrides, item, cache_timeout: cache_timeout, scope: scope) }
|
133
260
|
|
134
261
|
now = now.to_f
|
135
262
|
lookups.each do |target_packet_key, target_name, packet_name, value_keys|
|
263
|
+
if target_packet_key.nil?
|
264
|
+
results << [nil, nil]
|
265
|
+
next
|
266
|
+
end
|
136
267
|
unless packet_lookup[target_packet_key]
|
137
268
|
packet_lookup[target_packet_key] = get(target_name: target_name, packet_name: packet_name, cache_timeout: cache_timeout, scope: scope)
|
138
269
|
end
|
@@ -161,7 +292,8 @@ module OpenC3
|
|
161
292
|
if hash.key?(value_keys[-1])
|
162
293
|
item_result[1] = nil
|
163
294
|
else
|
164
|
-
|
295
|
+
item_result[0] = nil
|
296
|
+
item_result[1] = nil
|
165
297
|
end
|
166
298
|
end
|
167
299
|
end
|
@@ -335,6 +467,11 @@ module OpenC3
|
|
335
467
|
# return an ordered array of hash with keys
|
336
468
|
def self._parse_item(now, lookups, overrides, item, cache_timeout:, scope:)
|
337
469
|
target_name, packet_name, item_name, value_type = item
|
470
|
+
# They will all be nil when item is a nil value
|
471
|
+
if item_name.nil?
|
472
|
+
lookups << nil
|
473
|
+
return
|
474
|
+
end
|
338
475
|
|
339
476
|
# We build lookup keys by including all the less formatted types to gracefully degrade lookups
|
340
477
|
# This allows the user to specify WITH_UNITS and if there is no conversions it will simply return the RAW value
|
@@ -55,6 +55,7 @@ module OpenC3
|
|
55
55
|
attr_accessor :variables
|
56
56
|
attr_accessor :plugin_txt_lines
|
57
57
|
attr_accessor :needs_dependencies
|
58
|
+
attr_accessor :store_id
|
58
59
|
|
59
60
|
# NOTE: The following three class methods are used by the ModelController
|
60
61
|
# and are reimplemented to enable various Model class methods to work
|
@@ -72,7 +73,7 @@ module OpenC3
|
|
72
73
|
|
73
74
|
# Called by the PluginsController to parse the plugin variables
|
74
75
|
# Doesn't actually create the plugin during the phase
|
75
|
-
def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil, process_existing: false, scope:, validate_only: false)
|
76
|
+
def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil, store_id: nil, process_existing: false, scope:, validate_only: false)
|
76
77
|
gem_name = File.basename(gem_file_path).split("__")[0]
|
77
78
|
|
78
79
|
temp_dir = Dir.mktmpdir
|
@@ -130,7 +131,8 @@ module OpenC3
|
|
130
131
|
end
|
131
132
|
end
|
132
133
|
|
133
|
-
|
134
|
+
store_id = Integer(store_id) if store_id
|
135
|
+
model = PluginModel.new(name: gem_name, variables: variables, plugin_txt_lines: plugin_txt_lines, store_id: store_id, scope: scope)
|
134
136
|
result = model.as_json(:allow_nan => true)
|
135
137
|
result['existing_plugin_txt_lines'] = existing_plugin_txt_lines if existing_plugin_txt_lines and not process_existing and existing_plugin_txt_lines != result['plugin_txt_lines']
|
136
138
|
return result
|
@@ -299,6 +301,7 @@ module OpenC3
|
|
299
301
|
variables: {},
|
300
302
|
plugin_txt_lines: [],
|
301
303
|
needs_dependencies: false,
|
304
|
+
store_id: nil,
|
302
305
|
updated_at: nil,
|
303
306
|
scope:
|
304
307
|
)
|
@@ -306,6 +309,7 @@ module OpenC3
|
|
306
309
|
@variables = variables
|
307
310
|
@plugin_txt_lines = plugin_txt_lines
|
308
311
|
@needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
|
312
|
+
@store_id = store_id
|
309
313
|
end
|
310
314
|
|
311
315
|
def create(update: false, force: false, queued: false)
|
@@ -319,6 +323,7 @@ module OpenC3
|
|
319
323
|
'variables' => @variables,
|
320
324
|
'plugin_txt_lines' => @plugin_txt_lines,
|
321
325
|
'needs_dependencies' => @needs_dependencies,
|
326
|
+
'store_id' => @store_id,
|
322
327
|
'updated_at' => @updated_at
|
323
328
|
}
|
324
329
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
# Copyright 2025 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 'openc3/models/model'
|
20
|
+
require 'openc3/utilities/store'
|
21
|
+
|
22
|
+
module OpenC3
|
23
|
+
class PluginStoreModel < Model
|
24
|
+
PRIMARY_KEY = 'openc3_plugin_store'
|
25
|
+
|
26
|
+
def self.set(plugin_store_data)
|
27
|
+
Store.set(PRIMARY_KEY, plugin_store_data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.all()
|
31
|
+
Store.get(PRIMARY_KEY)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.plugin_store_error(message)
|
35
|
+
Store.set(PRIMARY_KEY, [{
|
36
|
+
date: Time.now.utc.iso8601,
|
37
|
+
title: 'Plugin Store Error',
|
38
|
+
body: message,
|
39
|
+
error: true,
|
40
|
+
}].to_json)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.get_by_id(id)
|
44
|
+
plugins = JSON.parse(all()) rescue []
|
45
|
+
plugins.find { |plugin| plugin["id"] == Integer(id) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.update
|
49
|
+
setting = SettingModel.get(name: 'store_url', scope: 'DEFAULT')
|
50
|
+
store_url = setting['data'] if setting
|
51
|
+
store_url = 'https://store.openc3.com' if store_url.nil? or store_url.strip.empty?
|
52
|
+
conn = Faraday.new(
|
53
|
+
url: store_url,
|
54
|
+
)
|
55
|
+
response = conn.get('/cosmos_plugins/json')
|
56
|
+
if response.success?
|
57
|
+
self.set(response.body)
|
58
|
+
else
|
59
|
+
self.plugin_store_error("Error contacting plugin store at #{store_url} (status: #{response.status})")
|
60
|
+
end
|
61
|
+
rescue Exception => e
|
62
|
+
self.plugin_store_error("Error contacting plugin store at #{store_url}. #{e.message})")
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.ensure_exists
|
66
|
+
plugins = self.all()
|
67
|
+
self.update() if plugins.nil? or plugins.length.zero? or plugins[0]['error']
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|