openc3 7.2.1 → 7.3.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 +97 -6
- data/bin/pipinstall +5 -4
- data/bin/pipuninstall +5 -1
- data/bin/uvinstall +149 -0
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +5 -1
- data/data/config/screen.yaml +2 -1
- data/data/config/target.yaml +21 -0
- data/data/config/tool.yaml +3 -1
- data/data/config/widgets.yaml +16 -3
- data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
- data/data/xtce_schemas/xml.xsd +116 -0
- data/ext/openc3/ext/telemetry/telemetry.c +1 -1
- data/lib/openc3/accessors/template_accessor.rb +2 -2
- data/lib/openc3/api/interface_api.rb +2 -2
- data/lib/openc3/api/limits_api.rb +27 -13
- data/lib/openc3/api/router_api.rb +3 -3
- data/lib/openc3/api/tlm_api.rb +1 -1
- data/lib/openc3/interfaces/http_client_interface.rb +1 -1
- data/lib/openc3/io/json_drb.rb +24 -8
- data/lib/openc3/io/json_rpc.rb +0 -2
- data/lib/openc3/logs/log_writer.rb +43 -7
- data/lib/openc3/logs/packet_log_writer.rb +1 -2
- data/lib/openc3/microservices/interface_microservice.rb +1 -1
- data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
- data/lib/openc3/models/cvt_model.rb +5 -5
- data/lib/openc3/models/gem_model.rb +1 -1
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/microservice_model.rb +21 -0
- data/lib/openc3/models/plugin_model.rb +204 -50
- data/lib/openc3/models/python_package_model.rb +207 -11
- data/lib/openc3/models/scope_model.rb +34 -30
- data/lib/openc3/models/target_model.rb +180 -6
- data/lib/openc3/models/tool_model.rb +33 -20
- data/lib/openc3/operators/microservice_operator.rb +33 -4
- data/lib/openc3/packets/commands.rb +4 -4
- data/lib/openc3/packets/limits.rb +15 -7
- data/lib/openc3/packets/packet.rb +1 -1
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
- data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
- data/lib/openc3/packets/structure.rb +2 -2
- data/lib/openc3/packets/telemetry.rb +3 -3
- data/lib/openc3/script/commands.rb +1 -1
- data/lib/openc3/script/extract.rb +20 -25
- data/lib/openc3/script/web_socket_api.rb +44 -4
- data/lib/openc3/topics/command_decom_topic.rb +16 -1
- data/lib/openc3/topics/limits_event_topic.rb +1 -1
- data/lib/openc3/utilities/aws_bucket.rb +10 -2
- data/lib/openc3/utilities/csv.rb +5 -5
- data/lib/openc3/utilities/local_mode.rb +4 -1
- data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
- data/lib/openc3/utilities/running_script.rb +145 -90
- data/lib/openc3/utilities/script.rb +46 -10
- data/lib/openc3/version.rb +6 -6
- data/templates/plugin/plugin.gemspec +1 -1
- 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 +5 -3
- data/templates/tool_vue/.nycrc +0 -3
- data/templates/widget/.nycrc +0 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<?xml version='1.0'?>
|
|
2
|
+
<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" xmlns:xs="http://www.w3.org/2001/XMLSchema" xml:lang="en">
|
|
3
|
+
|
|
4
|
+
<xs:annotation>
|
|
5
|
+
<xs:documentation>
|
|
6
|
+
See http://www.w3.org/XML/1998/namespace.html and
|
|
7
|
+
http://www.w3.org/TR/REC-xml for information about this namespace.
|
|
8
|
+
|
|
9
|
+
This schema document describes the XML namespace, in a form
|
|
10
|
+
suitable for import by other schema documents.
|
|
11
|
+
|
|
12
|
+
Note that local names in this namespace are intended to be defined
|
|
13
|
+
only by the World Wide Web Consortium or its subgroups. The
|
|
14
|
+
following names are currently defined in this namespace and should
|
|
15
|
+
not be used with conflicting semantics by any Working Group,
|
|
16
|
+
specification, or document instance:
|
|
17
|
+
|
|
18
|
+
base (as an attribute name): denotes an attribute whose value
|
|
19
|
+
provides a URI to be used as the base for interpreting any
|
|
20
|
+
relative URIs in the scope of the element on which it
|
|
21
|
+
appears; its value is inherited. This name is reserved
|
|
22
|
+
by virtue of its definition in the XML Base specification.
|
|
23
|
+
|
|
24
|
+
lang (as an attribute name): denotes an attribute whose value
|
|
25
|
+
is a language code for the natural language of the content of
|
|
26
|
+
any element; its value is inherited. This name is reserved
|
|
27
|
+
by virtue of its definition in the XML specification.
|
|
28
|
+
|
|
29
|
+
space (as an attribute name): denotes an attribute whose
|
|
30
|
+
value is a keyword indicating what whitespace processing
|
|
31
|
+
discipline is intended for the content of the element; its
|
|
32
|
+
value is inherited. This name is reserved by virtue of its
|
|
33
|
+
definition in the XML specification.
|
|
34
|
+
|
|
35
|
+
Father (in any context at all): denotes Jon Bosak, the chair of
|
|
36
|
+
the original XML Working Group. This name is reserved by
|
|
37
|
+
the following decision of the W3C XML Plenary and
|
|
38
|
+
XML Coordination groups:
|
|
39
|
+
|
|
40
|
+
In appreciation for his vision, leadership and dedication
|
|
41
|
+
the W3C XML Plenary on this 10th day of February, 2000
|
|
42
|
+
reserves for Jon Bosak in perpetuity the XML name
|
|
43
|
+
xml:Father
|
|
44
|
+
</xs:documentation>
|
|
45
|
+
</xs:annotation>
|
|
46
|
+
|
|
47
|
+
<xs:annotation>
|
|
48
|
+
<xs:documentation>This schema defines attributes and an attribute group
|
|
49
|
+
suitable for use by
|
|
50
|
+
schemas wishing to allow xml:base, xml:lang or xml:space attributes
|
|
51
|
+
on elements they define.
|
|
52
|
+
|
|
53
|
+
To enable this, such a schema must import this schema
|
|
54
|
+
for the XML namespace, e.g. as follows:
|
|
55
|
+
<schema . . .>
|
|
56
|
+
. . .
|
|
57
|
+
<import namespace="http://www.w3.org/XML/1998/namespace"
|
|
58
|
+
schemaLocation="http://www.w3.org/2001/03/xml.xsd"/>
|
|
59
|
+
|
|
60
|
+
Subsequently, qualified reference to any of the attributes
|
|
61
|
+
or the group defined below will have the desired effect, e.g.
|
|
62
|
+
|
|
63
|
+
<type . . .>
|
|
64
|
+
. . .
|
|
65
|
+
<attributeGroup ref="xml:specialAttrs"/>
|
|
66
|
+
|
|
67
|
+
will define a type which will schema-validate an instance
|
|
68
|
+
element with any of those attributes</xs:documentation>
|
|
69
|
+
</xs:annotation>
|
|
70
|
+
|
|
71
|
+
<xs:annotation>
|
|
72
|
+
<xs:documentation>In keeping with the XML Schema WG's standard versioning
|
|
73
|
+
policy, this schema document will persist at
|
|
74
|
+
http://www.w3.org/2001/03/xml.xsd.
|
|
75
|
+
At the date of issue it can also be found at
|
|
76
|
+
http://www.w3.org/2001/xml.xsd.
|
|
77
|
+
The schema document at that URI may however change in the future,
|
|
78
|
+
in order to remain compatible with the latest version of XML Schema
|
|
79
|
+
itself. In other words, if the XML Schema namespace changes, the version
|
|
80
|
+
of this document at
|
|
81
|
+
http://www.w3.org/2001/xml.xsd will change
|
|
82
|
+
accordingly; the version at
|
|
83
|
+
http://www.w3.org/2001/03/xml.xsd will not change.
|
|
84
|
+
</xs:documentation>
|
|
85
|
+
</xs:annotation>
|
|
86
|
+
|
|
87
|
+
<xs:attribute name="lang" type="xs:language">
|
|
88
|
+
<xs:annotation>
|
|
89
|
+
<xs:documentation>In due course, we should install the relevant ISO 2- and 3-letter
|
|
90
|
+
codes as the enumerated possible values . . .</xs:documentation>
|
|
91
|
+
</xs:annotation>
|
|
92
|
+
</xs:attribute>
|
|
93
|
+
|
|
94
|
+
<xs:attribute name="space" default="preserve">
|
|
95
|
+
<xs:simpleType>
|
|
96
|
+
<xs:restriction base="xs:NCName">
|
|
97
|
+
<xs:enumeration value="default"/>
|
|
98
|
+
<xs:enumeration value="preserve"/>
|
|
99
|
+
</xs:restriction>
|
|
100
|
+
</xs:simpleType>
|
|
101
|
+
</xs:attribute>
|
|
102
|
+
|
|
103
|
+
<xs:attribute name="base" type="xs:anyURI">
|
|
104
|
+
<xs:annotation>
|
|
105
|
+
<xs:documentation>See http://www.w3.org/TR/xmlbase/ for
|
|
106
|
+
information about this attribute.</xs:documentation>
|
|
107
|
+
</xs:annotation>
|
|
108
|
+
</xs:attribute>
|
|
109
|
+
|
|
110
|
+
<xs:attributeGroup name="specialAttrs">
|
|
111
|
+
<xs:attribute ref="xml:base"/>
|
|
112
|
+
<xs:attribute ref="xml:lang"/>
|
|
113
|
+
<xs:attribute ref="xml:space"/>
|
|
114
|
+
</xs:attributeGroup>
|
|
115
|
+
|
|
116
|
+
</xs:schema>
|
|
@@ -56,7 +56,7 @@ static VALUE packets(VALUE self, VALUE target_name)
|
|
|
56
56
|
|
|
57
57
|
if (!(RTEST(target_packets)))
|
|
58
58
|
{
|
|
59
|
-
rb_raise(rb_eRuntimeError, "Telemetry target '%s' does not exist", RSTRING_PTR(upcase_target_name));
|
|
59
|
+
rb_raise(rb_eRuntimeError, "Telemetry target '%s' does not exist (packets lookup)", RSTRING_PTR(upcase_target_name));
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return target_packets;
|
|
@@ -109,7 +109,7 @@ module OpenC3
|
|
|
109
109
|
configure()
|
|
110
110
|
|
|
111
111
|
success = buffer.gsub!("#{@left_char}#{item.key}#{@right_char}", value.to_s)
|
|
112
|
-
raise "Key #{item.key} not found in template" unless success
|
|
112
|
+
raise "Key '#{item.key}' not found in template (write_item)" unless success
|
|
113
113
|
return value
|
|
114
114
|
end
|
|
115
115
|
|
|
@@ -118,7 +118,7 @@ module OpenC3
|
|
|
118
118
|
items.each_with_index do |item, index|
|
|
119
119
|
next if item.data_type == :DERIVED
|
|
120
120
|
success = buffer.gsub!("#{@left_char}#{item.key}#{@right_char}", values[index].to_s)
|
|
121
|
-
raise "Key #{item.key} not found in template" unless success
|
|
121
|
+
raise "Key '#{item.key}' not found in template (write_items)" unless success
|
|
122
122
|
end
|
|
123
123
|
return values
|
|
124
124
|
end
|
|
@@ -47,7 +47,7 @@ module OpenC3
|
|
|
47
47
|
def get_interface(interface_name, manual: false, scope: $openc3_scope, token: $openc3_token)
|
|
48
48
|
authorize(permission: 'system', interface_name: interface_name, manual: manual, scope: scope, token: token)
|
|
49
49
|
interface = InterfaceModel.get(name: interface_name, scope: scope)
|
|
50
|
-
raise "Interface '#{interface_name}' does not exist" unless interface
|
|
50
|
+
raise "Interface '#{interface_name}' does not exist (get_interface)" unless interface
|
|
51
51
|
|
|
52
52
|
interface.merge(InterfaceStatusModel.get(name: interface_name, scope: scope))
|
|
53
53
|
end
|
|
@@ -160,7 +160,7 @@ module OpenC3
|
|
|
160
160
|
# TODO: Check if they have command authority for the targets mapped to this interface
|
|
161
161
|
authorize(permission: 'system_set', interface_name: interface_name, manual: manual, scope: scope, token: token)
|
|
162
162
|
interface = InterfaceModel.get_model(name: interface_name, scope: scope)
|
|
163
|
-
raise "Interface '#{interface_name}' does not exist" unless interface
|
|
163
|
+
raise "Interface '#{interface_name}' does not exist (unmap_target_from_interface)" unless interface
|
|
164
164
|
|
|
165
165
|
if Array === target_name
|
|
166
166
|
target_names = target_name
|
|
@@ -134,7 +134,7 @@ module OpenC3
|
|
|
134
134
|
break
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
|
-
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
|
|
137
|
+
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while enabling limits" unless found_item
|
|
138
138
|
|
|
139
139
|
TargetModel.set_packet(target_name, packet_name, packet, scope: scope)
|
|
140
140
|
|
|
@@ -167,7 +167,7 @@ module OpenC3
|
|
|
167
167
|
break
|
|
168
168
|
end
|
|
169
169
|
end
|
|
170
|
-
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
|
|
170
|
+
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while disabling limits" unless found_item
|
|
171
171
|
|
|
172
172
|
TargetModel.set_packet(target_name, packet_name, packet, scope: scope)
|
|
173
173
|
|
|
@@ -239,7 +239,7 @@ module OpenC3
|
|
|
239
239
|
end
|
|
240
240
|
end
|
|
241
241
|
end
|
|
242
|
-
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
|
|
242
|
+
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while setting limits" unless found_item
|
|
243
243
|
message = "Setting '#{target_name} #{packet_name} #{item_name}' limits to #{red_low} #{yellow_low} #{yellow_high} #{red_high}"
|
|
244
244
|
message << " #{green_low} #{green_high}" if green_low && green_high
|
|
245
245
|
message << " in set #{limits_set} with persistence #{persistence} as enabled #{enabled}"
|
|
@@ -262,14 +262,19 @@ module OpenC3
|
|
|
262
262
|
# @param packet_name [String] Packet name
|
|
263
263
|
# @param item_name [String] Item name
|
|
264
264
|
# @param state_name [String] Name of the state to change (e.g. 'CONNECTED')
|
|
265
|
-
# @param color [String] New color for the state. Must be GREEN, YELLOW,
|
|
265
|
+
# @param color [String, nil] New color for the state. Must be GREEN, YELLOW,
|
|
266
|
+
# or RED. Pass nil to clear (remove) the state color. Setting a color also
|
|
267
|
+
# enables limits for the item, but clearing the color (nil) does not change
|
|
268
|
+
# whether limits are enabled.
|
|
266
269
|
def set_state_color(target_name, packet_name, item_name, state_name, color,
|
|
267
270
|
manual: false, scope: $openc3_scope, token: $openc3_token)
|
|
268
271
|
authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
|
|
269
272
|
state_name = state_name.to_s.upcase
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
unless color.nil?
|
|
274
|
+
color = color.to_s.upcase
|
|
275
|
+
unless PacketItem::STATE_COLORS.include?(color.intern)
|
|
276
|
+
raise "Invalid state color '#{color}'. Must be one of #{PacketItem::STATE_COLORS.join(', ')}."
|
|
277
|
+
end
|
|
273
278
|
end
|
|
274
279
|
packet = TargetModel.packet(target_name, packet_name, scope: scope)
|
|
275
280
|
found_item = nil
|
|
@@ -278,16 +283,25 @@ module OpenC3
|
|
|
278
283
|
unless item['states'] && item['states'][state_name]
|
|
279
284
|
raise "State '#{state_name}' does not exist for item '#{target_name} #{packet_name} #{item_name}'"
|
|
280
285
|
end
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
if color.nil?
|
|
287
|
+
# Clear the state color
|
|
288
|
+
item['states'][state_name].delete('color')
|
|
289
|
+
else
|
|
290
|
+
item['states'][state_name]['color'] = color
|
|
291
|
+
# Defining a state color implies limits are enabled for the item
|
|
292
|
+
item['limits'] ||= {}
|
|
293
|
+
item['limits']['enabled'] = true
|
|
294
|
+
end
|
|
285
295
|
found_item = item
|
|
286
296
|
break
|
|
287
297
|
end
|
|
288
298
|
end
|
|
289
|
-
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
|
|
290
|
-
|
|
299
|
+
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while setting state color" unless found_item
|
|
300
|
+
if color.nil?
|
|
301
|
+
message = "Clearing '#{target_name} #{packet_name} #{item_name}' state #{state_name} color"
|
|
302
|
+
else
|
|
303
|
+
message = "Setting '#{target_name} #{packet_name} #{item_name}' state #{state_name} color to #{color}"
|
|
304
|
+
end
|
|
291
305
|
Logger.info(message, scope: scope)
|
|
292
306
|
|
|
293
307
|
TargetModel.set_packet(target_name, packet_name, packet, scope: scope)
|
|
@@ -46,7 +46,7 @@ module OpenC3
|
|
|
46
46
|
def get_router(router_name, manual: false, scope: $openc3_scope, token: $openc3_token)
|
|
47
47
|
authorize(permission: 'system', router_name: router_name, manual: manual, scope: scope, token: token)
|
|
48
48
|
router = RouterModel.get(name: router_name, scope: scope)
|
|
49
|
-
raise "Router '#{router_name}' does not exist" unless router
|
|
49
|
+
raise "Router '#{router_name}' does not exist (get_router)" unless router
|
|
50
50
|
|
|
51
51
|
router.merge(RouterStatusModel.get(name: router_name, scope: scope))
|
|
52
52
|
end
|
|
@@ -144,7 +144,7 @@ module OpenC3
|
|
|
144
144
|
# TODO: Check if they have command authority for the targets mapped to this router
|
|
145
145
|
authorize(permission: 'system_set', router_name: router_name, manual: manual, scope: scope, token: token)
|
|
146
146
|
router = RouterModel.get_model(name: router_name, scope: scope)
|
|
147
|
-
raise "Router '#{router_name}' does not exist" unless router
|
|
147
|
+
raise "Router '#{router_name}' does not exist (map_target_to_router)" unless router
|
|
148
148
|
|
|
149
149
|
if Array === target_name
|
|
150
150
|
target_names = target_name
|
|
@@ -167,7 +167,7 @@ module OpenC3
|
|
|
167
167
|
# TODO: Check if they have command authority for the targets mapped to this router
|
|
168
168
|
authorize(permission: 'system_set', router_name: router_name, manual: manual, scope: scope, token: token)
|
|
169
169
|
router = RouterModel.get_model(name: router_name, scope: scope)
|
|
170
|
-
raise "Router '#{router_name}' does not exist" unless router
|
|
170
|
+
raise "Router '#{router_name}' does not exist (unmap_target_from_router)" unless router
|
|
171
171
|
|
|
172
172
|
if Array === target_name
|
|
173
173
|
target_names = target_name
|
data/lib/openc3/api/tlm_api.rb
CHANGED
|
@@ -123,7 +123,7 @@ module OpenC3
|
|
|
123
123
|
target_name = target_name.upcase
|
|
124
124
|
packet_name = packet_name.upcase
|
|
125
125
|
unless CvtModel::VALUE_TYPES.include?(type)
|
|
126
|
-
raise "Unknown type '#{type}' for #{target_name} #{packet_name}"
|
|
126
|
+
raise "Unknown type '#{type}' for #{target_name} #{packet_name} (inject_tlm)"
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
if item_hash
|
|
@@ -237,7 +237,7 @@ module OpenC3
|
|
|
237
237
|
result['read_timeout'] = @read_timeout
|
|
238
238
|
result['connect_timeout'] = @connect_timeout
|
|
239
239
|
result['include_request_in_response'] = @include_request_in_response
|
|
240
|
-
result['
|
|
240
|
+
result['response_queue_length'] = @response_queue.length
|
|
241
241
|
return result
|
|
242
242
|
end
|
|
243
243
|
end
|
data/lib/openc3/io/json_drb.rb
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
require 'socket'
|
|
19
19
|
require 'json'
|
|
20
|
+
require 'set'
|
|
20
21
|
# require 'drb/acl'
|
|
21
22
|
require 'drb/drb'
|
|
22
23
|
require 'openc3/io/json_rpc'
|
|
@@ -47,8 +48,9 @@ module OpenC3
|
|
|
47
48
|
# JsonDRb implements the JSON-RPC 2.0 Specification to provide an interface
|
|
48
49
|
# for both internal and external tools to access the OpenC3 server. It
|
|
49
50
|
# provides methods to install an access control list to control access to the
|
|
50
|
-
# API.
|
|
51
|
-
#
|
|
51
|
+
# API. Access is controlled by a required method_whitelist which names every
|
|
52
|
+
# method that may be invoked. There is no permissive fallback: any method not
|
|
53
|
+
# explicitly listed is rejected.
|
|
52
54
|
class JsonDRb
|
|
53
55
|
# Minimum amount of time in seconds to receive the JSON request,
|
|
54
56
|
# process it, and send the response. Requests for less than this amount
|
|
@@ -62,18 +64,21 @@ module OpenC3
|
|
|
62
64
|
|
|
63
65
|
# @return [Integer] The number of JSON-RPC requests processed
|
|
64
66
|
attr_accessor :request_count
|
|
65
|
-
# @return [
|
|
66
|
-
|
|
67
|
+
# @return [Set<String>] Set of downcased method names which may be called
|
|
68
|
+
attr_reader :method_whitelist
|
|
67
69
|
# @return [ACL] The access control list
|
|
68
70
|
# attr_accessor :acl
|
|
69
71
|
|
|
70
72
|
attr_accessor :object
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
# @param method_whitelist [Enumerable<String>] Every method name which may
|
|
75
|
+
# be called over JSON-RPC. Required and must not be empty since there is
|
|
76
|
+
# no permissive fallback.
|
|
77
|
+
def initialize(method_whitelist:)
|
|
78
|
+
self.method_whitelist = method_whitelist
|
|
73
79
|
@thread = nil
|
|
74
80
|
# @acl = nil
|
|
75
81
|
@object = nil
|
|
76
|
-
@method_whitelist = nil
|
|
77
82
|
@request_count = 0
|
|
78
83
|
@request_times = []
|
|
79
84
|
@request_times_index = 0
|
|
@@ -82,6 +87,18 @@ module OpenC3
|
|
|
82
87
|
@server_mutex = Mutex.new
|
|
83
88
|
end
|
|
84
89
|
|
|
90
|
+
# Replaces the set of methods which may be called over JSON-RPC. Names are
|
|
91
|
+
# downcased to match the dispatch in {#process_request}.
|
|
92
|
+
#
|
|
93
|
+
# @param whitelist [Enumerable<String>] Every method name which may be called
|
|
94
|
+
def method_whitelist=(whitelist)
|
|
95
|
+
if whitelist.nil? or whitelist.to_a.empty?
|
|
96
|
+
raise ArgumentError, "method_whitelist is required and must not be empty"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@method_whitelist = whitelist.map { |name| name.to_s.downcase }.to_set
|
|
100
|
+
end
|
|
101
|
+
|
|
85
102
|
# Returns the number of connected clients
|
|
86
103
|
# @return [Integer] The number of connected clients
|
|
87
104
|
def num_clients
|
|
@@ -254,8 +271,7 @@ module OpenC3
|
|
|
254
271
|
error_code = nil
|
|
255
272
|
response_data = nil
|
|
256
273
|
|
|
257
|
-
if
|
|
258
|
-
(!@method_whitelist and !JsonRpcRequest::DANGEROUS_METHODS.include?(request.method.downcase()))
|
|
274
|
+
if @method_whitelist.include?(request.method.downcase())
|
|
259
275
|
begin
|
|
260
276
|
if request.keyword_params
|
|
261
277
|
result = @object.public_send(request.method.downcase().intern, *request.params, **request.keyword_params)
|
data/lib/openc3/io/json_rpc.rb
CHANGED
|
@@ -227,8 +227,6 @@ module OpenC3
|
|
|
227
227
|
|
|
228
228
|
# Represents a JSON Remote Procedure Call Request
|
|
229
229
|
class JsonRpcRequest < JsonRpc
|
|
230
|
-
DANGEROUS_METHODS = Set['__send__', 'send', 'instance_eval', 'instance_exec']
|
|
231
|
-
|
|
232
230
|
# @param method_name [String] The name of the method to call
|
|
233
231
|
# @param method_params [Array<String>] Array of strings which represent the
|
|
234
232
|
# parameters to send to the method
|
|
@@ -113,6 +113,7 @@ module OpenC3
|
|
|
113
113
|
@cycle_minute = Integer(@cycle_minute) if @cycle_minute
|
|
114
114
|
@enforce_time_order = ConfigParser.handle_true_false(enforce_time_order)
|
|
115
115
|
@out_of_order = false
|
|
116
|
+
@bad_time = false
|
|
116
117
|
@mutex = Mutex.new
|
|
117
118
|
@file = nil
|
|
118
119
|
@file_size = 0
|
|
@@ -125,6 +126,13 @@ module OpenC3
|
|
|
125
126
|
@cleanup_offsets = []
|
|
126
127
|
@cleanup_times = []
|
|
127
128
|
@previous_time_nsec_since_epoch = nil
|
|
129
|
+
# Stored packets older than 20 years are treated as invalid (e.g. 1970 before
|
|
130
|
+
# clock/GPS sync). They never drive file segmentation nor become the ordering
|
|
131
|
+
# baseline, so a late-syncing clock doesn't thrash log files.
|
|
132
|
+
@min_valid_time_nsec = (@start_time - (20 * 365 * 24 * 60 * 60)).to_nsec_from_epoch
|
|
133
|
+
# Validity (valid/invalid time) of the packets in the currently open file. Used to
|
|
134
|
+
# segregate invalid-time (1970) packets into their own file. nil = no packets yet.
|
|
135
|
+
@current_file_valid = nil
|
|
128
136
|
@tmp_dir = Dir.mktmpdir
|
|
129
137
|
@wait_threads = []
|
|
130
138
|
|
|
@@ -280,6 +288,7 @@ module OpenC3
|
|
|
280
288
|
@first_time = nil
|
|
281
289
|
@last_time = nil
|
|
282
290
|
@previous_time_nsec_since_epoch = nil
|
|
291
|
+
@current_file_valid = nil
|
|
283
292
|
Logger.debug "Log File Opened : #{@filename}"
|
|
284
293
|
rescue => e
|
|
285
294
|
Logger.error "Error starting new log file: #{e.formatted}"
|
|
@@ -289,7 +298,14 @@ module OpenC3
|
|
|
289
298
|
|
|
290
299
|
# @enforce_time_order requires the timestamps on each write to be greater than the previous
|
|
291
300
|
# process_out_of_order ignores the timestamps for the current entry (used to ignore timestamps on metadata entries, vs actual packets)
|
|
292
|
-
|
|
301
|
+
# stored: stored packets roll a new file when GOOD times move backward (a new /
|
|
302
|
+
# overlapping replay run). Packets with an invalid time (e.g. 1970 before clock/GPS
|
|
303
|
+
# sync) - realtime or stored - are segregated into their own file: the first valid
|
|
304
|
+
# packet after a run of 1970s rolls a fresh, properly-named file (and vice-versa).
|
|
305
|
+
def prepare_write(time_nsec_since_epoch, data_length, redis_topic = nil, redis_offset = nil, allow_new_file: true, process_out_of_order: true, stored: false)
|
|
306
|
+
# An invalid time is a packet from before the clock/GPS synced (e.g. 1970). This
|
|
307
|
+
# applies to both realtime and stored packets so neither taints a file's filename.
|
|
308
|
+
packet_valid = !(process_out_of_order and time_nsec_since_epoch < @min_valid_time_nsec)
|
|
293
309
|
# This check includes logging_enabled again because it might have changed since we acquired the mutex
|
|
294
310
|
# Ensures new files based on size, and ensures always increasing time order in files
|
|
295
311
|
if @logging_enabled
|
|
@@ -299,17 +315,37 @@ module OpenC3
|
|
|
299
315
|
elsif @cycle_size and ((@file_size + data_length) > @cycle_size)
|
|
300
316
|
Logger.debug("Log writer start new file due to cycle size #{@cycle_size}")
|
|
301
317
|
start_new_file() if allow_new_file
|
|
318
|
+
elsif process_out_of_order and !@current_file_valid.nil? and packet_valid != @current_file_valid and allow_new_file
|
|
319
|
+
# Time validity changed (1970 <-> valid): segregate invalid-time packets into
|
|
320
|
+
# their own file so valid files keep a clean, correctly-timed filename.
|
|
321
|
+
Logger.debug("Log writer start new file due to time validity change")
|
|
322
|
+
start_new_file()
|
|
302
323
|
elsif process_out_of_order and @enforce_time_order and @previous_time_nsec_since_epoch and (@previous_time_nsec_since_epoch > time_nsec_since_epoch)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
Logger.
|
|
307
|
-
|
|
324
|
+
if stored and packet_valid and allow_new_file
|
|
325
|
+
# Good stored time went backward => new/overlapping replay run. Roll a new
|
|
326
|
+
# file so each stored file stays monotonic (start_new_file resets @previous_time).
|
|
327
|
+
Logger.debug("Log writer start new file due to out of order stored time")
|
|
328
|
+
start_new_file()
|
|
329
|
+
elsif !stored
|
|
330
|
+
# Warning: Creating new files here can cause lots of files to be created if packets make it through out of order
|
|
331
|
+
# Changed to just a warning to prevent file thrashing
|
|
332
|
+
unless @out_of_order
|
|
333
|
+
Logger.warn("Log writer out of order time detected (increase buffer depth?): #{Time.from_nsec_from_epoch(@previous_time_nsec_since_epoch)} #{Time.from_nsec_from_epoch(time_nsec_since_epoch)}")
|
|
334
|
+
@out_of_order = true
|
|
335
|
+
end
|
|
308
336
|
end
|
|
309
337
|
end
|
|
310
338
|
end
|
|
311
339
|
@last_offsets[redis_topic] = redis_offset if redis_topic and redis_offset # This is needed for the redis offset marker entry at the end of the log file
|
|
312
|
-
|
|
340
|
+
if process_out_of_order
|
|
341
|
+
@current_file_valid = packet_valid
|
|
342
|
+
if packet_valid
|
|
343
|
+
@previous_time_nsec_since_epoch = time_nsec_since_epoch
|
|
344
|
+
elsif !@bad_time
|
|
345
|
+
Logger.warn("Log writer segregating invalid packet time (before clock sync?): #{Time.from_nsec_from_epoch(time_nsec_since_epoch)}")
|
|
346
|
+
@bad_time = true
|
|
347
|
+
end
|
|
348
|
+
end
|
|
313
349
|
end
|
|
314
350
|
|
|
315
351
|
# Closing a log file isn't critical so we just log an error. NOTE: This also trims the Redis stream
|
|
@@ -96,13 +96,12 @@ module OpenC3
|
|
|
96
96
|
@mutex.lock if take_mutex
|
|
97
97
|
begin
|
|
98
98
|
if entry_type == :RAW_PACKET or entry_type == :JSON_PACKET
|
|
99
|
-
# Only care about the timestamps on the real packets being in order
|
|
100
99
|
process_out_of_order = true
|
|
101
100
|
else
|
|
102
101
|
# Metadata timestamps don't matter
|
|
103
102
|
process_out_of_order = false
|
|
104
103
|
end
|
|
105
|
-
prepare_write(time_nsec_since_epoch, data.length, redis_topic, redis_offset, allow_new_file: allow_new_file, process_out_of_order: process_out_of_order)
|
|
104
|
+
prepare_write(time_nsec_since_epoch, data.length, redis_topic, redis_offset, allow_new_file: allow_new_file, process_out_of_order: process_out_of_order, stored: stored)
|
|
106
105
|
write_entry(entry_type, cmd_or_tlm, target_name, packet_name, time_nsec_since_epoch, stored, data, id, received_time_nsec_since_epoch: received_time_nsec_since_epoch, extra: extra) if @file
|
|
107
106
|
ensure
|
|
108
107
|
@mutex.unlock if take_mutex
|
|
@@ -195,7 +195,7 @@ module OpenC3
|
|
|
195
195
|
msg_hash = critical_model.cmd_hash
|
|
196
196
|
release_critical = true
|
|
197
197
|
else
|
|
198
|
-
next "Critical command #{msg_hash['release_critical']} not found"
|
|
198
|
+
next "Critical command '#{msg_hash['release_critical']}' not found (interface command processing)"
|
|
199
199
|
end
|
|
200
200
|
end
|
|
201
201
|
if msg_hash.key?('target_control')
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'openc3/utilities/local_mode'
|
|
3
|
+
require 'openc3/utilities/migration'
|
|
4
|
+
require 'openc3/models/scope_model'
|
|
5
|
+
require 'openc3/models/setting_model'
|
|
6
|
+
|
|
7
|
+
module OpenC3
|
|
8
|
+
# The system_health setting was originally stored as a Ruby Hash. Local mode
|
|
9
|
+
# writes settings to disk with File.write and reads them back as a String, so
|
|
10
|
+
# the Hash round tripped into a Ruby inspect String which nothing can parse.
|
|
11
|
+
# Store it as a JSON string like every other setting and repair existing data.
|
|
12
|
+
class SystemHealthJson < Migration
|
|
13
|
+
def self.run
|
|
14
|
+
setting = SettingModel.get(name: 'system_health')
|
|
15
|
+
data = repair(setting && setting['data'])
|
|
16
|
+
SettingModel.set({ name: 'system_health', data: JSON.generate(data) }, scope: 'DEFAULT')
|
|
17
|
+
# Repair the local mode file as well or sync_settings puts the bad data back
|
|
18
|
+
LocalMode.save_setting('DEFAULT', 'system_health', JSON.generate(data))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Hash] the existing settings if they can be recovered, otherwise defaults
|
|
22
|
+
def self.repair(data)
|
|
23
|
+
return ScopeModel::SYSTEM_HEALTH_DEFAULTS if data.nil?
|
|
24
|
+
return data if Hash === data
|
|
25
|
+
return ScopeModel::SYSTEM_HEALTH_DEFAULTS unless String === data
|
|
26
|
+
begin
|
|
27
|
+
parsed = JSON.parse(data)
|
|
28
|
+
rescue JSON::ParserError
|
|
29
|
+
# Ruby inspect format, e.g. {"cpu" => {"redThreshold" => 90, ... => nil}}
|
|
30
|
+
# Ruby 3.3 and earlier had no spaces around the rocket. All the values are
|
|
31
|
+
# numbers, booleans, or nil so there's nothing to accidentally rewrite.
|
|
32
|
+
parsed = JSON.parse(data.gsub(/\s*=>\s*/, ': ').gsub(/\bnil\b/, 'null')) rescue nil
|
|
33
|
+
end
|
|
34
|
+
Hash === parsed ? parsed : ScopeModel::SYSTEM_HEALTH_DEFAULTS
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless ENV['OPENC3_NO_MIGRATE']
|
|
40
|
+
OpenC3::SystemHealthJson.run
|
|
41
|
+
end
|
|
@@ -86,7 +86,7 @@ module OpenC3
|
|
|
86
86
|
return hash if hash and (now - cache_time) < cache_timeout
|
|
87
87
|
end
|
|
88
88
|
packet = store_for_target(target_name, scope: scope).hget(key, packet_name)
|
|
89
|
-
raise "Packet '#{target_name} #{packet_name}'
|
|
89
|
+
raise "Packet '#{target_name} #{packet_name}' has no current values in CVT" unless packet
|
|
90
90
|
hash = JSON.parse(packet, allow_nan: true, create_additions: true)
|
|
91
91
|
@@packet_cache[tgt_pkt_key] = [now, hash]
|
|
92
92
|
hash
|
|
@@ -107,7 +107,7 @@ module OpenC3
|
|
|
107
107
|
hash["#{item_name}__C"] = value
|
|
108
108
|
hash[item_name] = value
|
|
109
109
|
else
|
|
110
|
-
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
|
|
110
|
+
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (set_item)"
|
|
111
111
|
end
|
|
112
112
|
set(hash, target_name: target_name, packet_name: packet_name, queued: queued, scope: scope)
|
|
113
113
|
end
|
|
@@ -252,7 +252,7 @@ module OpenC3
|
|
|
252
252
|
when :FORMATTED, :WITH_UNITS
|
|
253
253
|
hash["#{item_name}__F"] = value.to_s # Always a String
|
|
254
254
|
else
|
|
255
|
-
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
|
|
255
|
+
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (override)"
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
|
|
@@ -277,7 +277,7 @@ module OpenC3
|
|
|
277
277
|
when :FORMATTED, :WITH_UNITS
|
|
278
278
|
hash.delete("#{item_name}__F")
|
|
279
279
|
else
|
|
280
|
-
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
|
|
280
|
+
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (normalize)"
|
|
281
281
|
end
|
|
282
282
|
|
|
283
283
|
tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
|
|
@@ -327,7 +327,7 @@ module OpenC3
|
|
|
327
327
|
when :RAW
|
|
328
328
|
types = [item_name]
|
|
329
329
|
else
|
|
330
|
-
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
|
|
330
|
+
raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (get_item)"
|
|
331
331
|
end
|
|
332
332
|
|
|
333
333
|
tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
|
|
@@ -47,7 +47,7 @@ module OpenC3
|
|
|
47
47
|
return path if File.exist?(path)
|
|
48
48
|
path = "#{ENV['GEM_HOME']}/cache/#{name}"
|
|
49
49
|
return path if File.exist?(path)
|
|
50
|
-
raise "Gem #{name} not found"
|
|
50
|
+
raise "Gem '#{name}' not found"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def self.put(gem_file_path, gem_install: true, scope:)
|
|
@@ -256,7 +256,7 @@ module OpenC3
|
|
|
256
256
|
|
|
257
257
|
def ensure_target_exists(target_name)
|
|
258
258
|
target = TargetModel.get(name: target_name, scope: @scope)
|
|
259
|
-
raise "Target #{target_name} does not exist" unless target
|
|
259
|
+
raise "Target '#{target_name}' does not exist (InterfaceModel)" unless target
|
|
260
260
|
target
|
|
261
261
|
end
|
|
262
262
|
|
|
@@ -140,6 +140,27 @@ module OpenC3
|
|
|
140
140
|
@bucket = Bucket.getClient()
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
+
# Resolve the Python environment variables that will be used at runtime.
|
|
144
|
+
# This mirrors the logic in MicroserviceOperator#convert_microservice_to_process_definition.
|
|
145
|
+
def runtime_python_env
|
|
146
|
+
python_bin = ENV['OPENC3_PYTHON_BIN'] || '/openc3/python/.venv/bin/python'
|
|
147
|
+
result = { 'OPENC3_PYTHON_BIN' => python_bin }
|
|
148
|
+
if @needs_dependencies && @plugin
|
|
149
|
+
sanitized_name = "#{@scope}__#{@plugin}".tr('^a-zA-Z0-9_-', '_')
|
|
150
|
+
candidate = "/gems/plugin_venvs/#{sanitized_name}/.venv"
|
|
151
|
+
if File.directory?(candidate)
|
|
152
|
+
result['VIRTUAL_ENV'] = candidate
|
|
153
|
+
result['PYTHONUSERBASE'] = candidate
|
|
154
|
+
site_packages = Dir.glob("#{candidate}/lib/python*/site-packages").first
|
|
155
|
+
result['PYTHONPATH'] = site_packages
|
|
156
|
+
else
|
|
157
|
+
result['VIRTUAL_ENV'] = nil
|
|
158
|
+
result['PYTHONUSERBASE'] = '/gems/python_packages'
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
result
|
|
162
|
+
end
|
|
163
|
+
|
|
143
164
|
def as_json(*a)
|
|
144
165
|
{
|
|
145
166
|
'name' => @name,
|