openc3 7.4.1 → 7.5.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/pipinstall +18 -5
- data/bin/pipuninstall +1 -1
- data/bin/uvinstall +36 -8
- data/data/config/default_plugins.yaml +43 -0
- data/data/config/interface_modifiers.yaml +22 -7
- data/data/config/item_modifiers.yaml +14 -4
- data/data/config/plugins.yaml +1 -1
- data/data/config/widgets.yaml +1 -1
- data/lib/openc3/api/cmd_api.rb +10 -13
- data/lib/openc3/api/interface_api.rb +1 -2
- data/lib/openc3/api/limits_api.rb +1 -1
- data/lib/openc3/api/metrics_api.rb +26 -1
- data/lib/openc3/api/router_api.rb +1 -2
- data/lib/openc3/api/tlm_api.rb +6 -10
- data/lib/openc3/config/config_parser.rb +3 -1
- data/lib/openc3/core_ext/tempfile.rb +2 -0
- data/lib/openc3/interfaces/file_interface.rb +6 -1
- data/lib/openc3/interfaces/http_client_interface.rb +14 -6
- data/lib/openc3/interfaces/interface.rb +27 -3
- data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
- data/lib/openc3/interfaces/stream_interface.rb +22 -3
- data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
- data/lib/openc3/interfaces/udp_interface.rb +16 -2
- data/lib/openc3/interfaces.rb +1 -0
- data/lib/openc3/logs/log_writer.rb +13 -4
- data/lib/openc3/microservices/decom_microservice.rb +7 -4
- data/lib/openc3/microservices/interface_microservice.rb +146 -50
- data/lib/openc3/microservices/log_microservice.rb +1 -3
- data/lib/openc3/microservices/microservice.rb +25 -1
- data/lib/openc3/microservices/plugin_microservice.rb +32 -0
- data/lib/openc3/microservices/text_log_microservice.rb +1 -3
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/interface_status_model.rb +6 -0
- data/lib/openc3/models/metric_model.rb +52 -4
- data/lib/openc3/models/microservice_model.rb +7 -3
- data/lib/openc3/models/plugin_model.rb +12 -12
- data/lib/openc3/models/python_package_model.rb +3 -13
- data/lib/openc3/models/target_model.rb +83 -9
- data/lib/openc3/operators/microservice_operator.rb +5 -0
- data/lib/openc3/operators/operator.rb +69 -0
- data/lib/openc3/packets/packet.rb +10 -4
- data/lib/openc3/packets/packet_config.rb +12 -1
- data/lib/openc3/packets/packet_item.rb +27 -14
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/structure_item.rb +3 -2
- data/lib/openc3/script/api_shared.rb +58 -84
- data/lib/openc3/script/extract.rb +215 -19
- data/lib/openc3/script/plugins.rb +4 -11
- data/lib/openc3/script/storage.rb +81 -10
- data/lib/openc3/system/system.rb +9 -6
- data/lib/openc3/topics/command_decom_topic.rb +1 -1
- data/lib/openc3/topics/command_topic.rb +2 -1
- data/lib/openc3/topics/router_topic.rb +2 -1
- data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
- data/lib/openc3/topics/telemetry_topic.rb +3 -2
- data/lib/openc3/topics/topic.rb +154 -2
- data/lib/openc3/utilities/cli_generator.rb +67 -15
- data/lib/openc3/utilities/config_overlay.rb +35 -17
- data/lib/openc3/utilities/default_plugins.rb +33 -0
- data/lib/openc3/utilities/local_mode.rb +82 -46
- data/lib/openc3/utilities/metric.rb +16 -0
- data/lib/openc3/utilities/process_stats.rb +160 -0
- data/lib/openc3/utilities/pypi_url.rb +65 -0
- data/lib/openc3/utilities/questdb_client.rb +14 -0
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/store_autoload.rb +20 -3
- data/lib/openc3/utilities/store_queued.rb +37 -9
- data/lib/openc3/utilities/target_file.rb +57 -12
- data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
- data/lib/openc3/version.rb +6 -6
- data/templates/conversion/conversion.py +6 -2
- data/templates/conversion/conversion.rb +5 -0
- data/templates/plugin/.github/workflows/playwright.yml +62 -0
- data/templates/plugin/.github/workflows/release.yml +45 -0
- data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
- data/templates/plugin/.gitignore +6 -0
- data/templates/plugin/README.md +114 -2
- data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
- data/templates/plugin/plugin.gemspec +10 -1
- data/templates/plugin/pyproject.toml +119 -0
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_angular/pnpm-workspace.yaml +16 -0
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_react/pnpm-workspace.yaml +16 -0
- data/templates/tool_svelte/package.json +2 -1
- data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
- data/templates/tool_vue/package.json +3 -3
- data/templates/tool_vue/pnpm-workspace.yaml +16 -0
- data/templates/widget/package.json +2 -2
- data/templates/widget/pnpm-workspace.yaml +16 -0
- metadata +16 -1
|
@@ -530,7 +530,7 @@ module OpenC3
|
|
|
530
530
|
|
|
531
531
|
value = yield(target_name, packet_name, item_name)
|
|
532
532
|
if comparison_to_eval
|
|
533
|
-
|
|
533
|
+
_check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
|
|
534
534
|
else
|
|
535
535
|
puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value.nil? ? 'nil' : value.inspect}"
|
|
536
536
|
end
|
|
@@ -733,28 +733,16 @@ module OpenC3
|
|
|
733
733
|
return [target_name, packet_name, item_name, comparison_to_eval, timeout, polling_rate]
|
|
734
734
|
end
|
|
735
735
|
|
|
736
|
-
|
|
736
|
+
# Waits for the comparison to be true or the timeout to expire.
|
|
737
|
+
# The comparison is a callable which takes the telemetry value and returns true or false.
|
|
738
|
+
# A block passed by the user takes precedence over the comparison.
|
|
739
|
+
def _openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: $openc3_scope, token: $openc3_token, &block)
|
|
740
|
+
condition = block || comparison
|
|
737
741
|
end_time = Time.now.sys + timeout
|
|
738
|
-
if exp_to_eval and !exp_to_eval.is_printable?
|
|
739
|
-
raise "ERROR: Invalid comparison to non-ascii value"
|
|
740
|
-
end
|
|
741
742
|
while true
|
|
742
743
|
work_start = Time.now.sys
|
|
743
744
|
value = tlm(target_name, packet_name, item_name, type: value_type, scope: scope, token: token)
|
|
744
|
-
if
|
|
745
|
-
if block.call(value)
|
|
746
|
-
return true, value
|
|
747
|
-
end
|
|
748
|
-
else
|
|
749
|
-
begin
|
|
750
|
-
if eval(exp_to_eval)
|
|
751
|
-
return true, value
|
|
752
|
-
end
|
|
753
|
-
# NoMethodError is raised when the tlm() returns nil and we try to eval the expression
|
|
754
|
-
# In this case we just continue and see if eventually we get a good value from tlm()
|
|
755
|
-
rescue NoMethodError
|
|
756
|
-
end
|
|
757
|
-
end
|
|
745
|
+
return true, value if condition and condition.call(value)
|
|
758
746
|
break if Time.now.sys >= end_time
|
|
759
747
|
|
|
760
748
|
delta = Time.now.sys - work_start
|
|
@@ -766,58 +754,62 @@ module OpenC3
|
|
|
766
754
|
|
|
767
755
|
if canceled
|
|
768
756
|
value = tlm(target_name, packet_name, item_name, type: value_type, scope: scope, token: token)
|
|
769
|
-
if
|
|
770
|
-
|
|
771
|
-
return true, value
|
|
772
|
-
else
|
|
773
|
-
return false, value
|
|
774
|
-
end
|
|
757
|
+
if condition and condition.call(value)
|
|
758
|
+
return true, value
|
|
775
759
|
else
|
|
776
|
-
|
|
777
|
-
if eval(exp_to_eval)
|
|
778
|
-
return true, value
|
|
779
|
-
else
|
|
780
|
-
return false, value
|
|
781
|
-
end
|
|
782
|
-
# NoMethodError is raised when the tlm() returns nil and we try to eval the expression
|
|
783
|
-
rescue NoMethodError
|
|
784
|
-
return false, value
|
|
785
|
-
end
|
|
760
|
+
return false, value
|
|
786
761
|
end
|
|
787
762
|
end
|
|
788
763
|
end
|
|
789
764
|
|
|
790
765
|
return false, value
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Builds a callable which compares a telemetry value against the given comparison string,
|
|
769
|
+
# e.g. "> 1". Returns nil if there is no comparison, e.g. a block based wait_check().
|
|
770
|
+
# Raises if the comparison is invalid, e.g. an unsupported operator or unparsable operand.
|
|
771
|
+
def _comparison_implementation(comparison_to_eval)
|
|
772
|
+
return nil unless comparison_to_eval
|
|
773
|
+
|
|
774
|
+
operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval)
|
|
775
|
+
return nil unless operator
|
|
776
|
+
lambda { |value| compare_values(value, operator, operand) }
|
|
799
777
|
end
|
|
800
778
|
|
|
801
779
|
# Wait for a converted telemetry item to pass a comparison
|
|
802
780
|
def _openc3_script_wait_implementation_comparison(target_name, packet_name, item_name, value_type, comparison_to_eval, timeout, polling_rate = DEFAULT_TLM_POLLING_RATE, scope: $openc3_scope, token: $openc3_token, &block)
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
exp_to_eval = nil
|
|
781
|
+
# The comparison text is logged whether or not it is parsed so it is always validated
|
|
782
|
+
if comparison_to_eval and !comparison_to_eval.is_printable?
|
|
783
|
+
raise "ERROR: Invalid comparison to non-ascii value"
|
|
807
784
|
end
|
|
808
|
-
|
|
785
|
+
# A user supplied block is the condition so the comparison text is never parsed
|
|
786
|
+
comparison = block ? nil : _comparison_implementation(comparison_to_eval)
|
|
787
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
809
788
|
end
|
|
810
789
|
|
|
811
790
|
def _openc3_script_wait_implementation_tolerance(target_name, packet_name, item_name, value_type, expected_value, tolerance, timeout, polling_rate = DEFAULT_TLM_POLLING_RATE, scope: $openc3_scope, token: $openc3_token, &block)
|
|
812
|
-
|
|
813
|
-
|
|
791
|
+
comparison = lambda do |value|
|
|
792
|
+
begin
|
|
793
|
+
value >= (expected_value - tolerance) and value <= (expected_value + tolerance)
|
|
794
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
795
|
+
false
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
814
799
|
end
|
|
815
800
|
|
|
816
801
|
def _openc3_script_wait_implementation_array_tolerance(array_size, target_name, packet_name, item_name, value_type, expected_value, tolerance, timeout, polling_rate = DEFAULT_TLM_POLLING_RATE, scope: $openc3_scope, token: $openc3_token, &block)
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
802
|
+
comparison = lambda do |values|
|
|
803
|
+
begin
|
|
804
|
+
next false unless values.is_a?(Array) and values.length == array_size
|
|
805
|
+
array_size.times.all? do |i|
|
|
806
|
+
values[i] >= (expected_value[i] - tolerance[i]) and values[i] <= (expected_value[i] + tolerance[i])
|
|
807
|
+
end
|
|
808
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
809
|
+
false
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
821
813
|
end
|
|
822
814
|
|
|
823
815
|
# Wait on an expression to be true.
|
|
@@ -859,17 +851,15 @@ module OpenC3
|
|
|
859
851
|
end
|
|
860
852
|
end
|
|
861
853
|
|
|
862
|
-
def
|
|
863
|
-
string = "value " + comparison_to_eval
|
|
854
|
+
def _check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
|
|
864
855
|
check_str = "CHECK: #{_upcase(target_name, packet_name, item_name)} #{comparison_to_eval}"
|
|
865
856
|
# Show user the check against a quoted string
|
|
866
|
-
# Note: We have to preserve the original 'value' variable because we're going to eval against it
|
|
867
857
|
value_str = value.is_a?(String) ? "'#{value}'" : value
|
|
868
858
|
value_str = 'nil' if value.nil? # Show user nil value as 'nil'
|
|
869
859
|
with_value = "with value == #{value_str}"
|
|
870
860
|
|
|
871
|
-
|
|
872
|
-
unless
|
|
861
|
+
operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval)
|
|
862
|
+
unless _valid_comparison?(value, operator, operand)
|
|
873
863
|
message = "Invalid comparison for types"
|
|
874
864
|
if $disconnect
|
|
875
865
|
puts "ERROR: #{message}"
|
|
@@ -878,7 +868,7 @@ module OpenC3
|
|
|
878
868
|
end
|
|
879
869
|
end
|
|
880
870
|
|
|
881
|
-
if
|
|
871
|
+
if compare_values(value, operator, operand)
|
|
882
872
|
puts "#{check_str} success #{with_value}"
|
|
883
873
|
else
|
|
884
874
|
message = "#{check_str} failed #{with_value}"
|
|
@@ -888,35 +878,19 @@ module OpenC3
|
|
|
888
878
|
raise CheckError, message
|
|
889
879
|
end
|
|
890
880
|
end
|
|
891
|
-
rescue NameError => e
|
|
892
|
-
if e.message =~ /uninitialized constant OpenC3::ApiShared::(\w+)/
|
|
893
|
-
new_error = NameError.new("Uninitialized constant #{$1}. Did you mean '#{$1}' as a string?")
|
|
894
|
-
new_error.set_backtrace(e.backtrace)
|
|
895
|
-
raise new_error
|
|
896
|
-
else
|
|
897
|
-
raise e
|
|
898
|
-
end
|
|
899
881
|
end
|
|
900
882
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
operator, operand = extract_operator_and_operand_from_comparison(comparison)
|
|
906
|
-
rescue RuntimeError => e
|
|
907
|
-
if e.message.include?("Unable to parse operand")
|
|
908
|
-
# If we can't parse the operand, let the eval happen anyway
|
|
909
|
-
# It will raise an appropriate error (like NameError for undefined constants)
|
|
910
|
-
return true
|
|
911
|
-
end
|
|
912
|
-
raise # Re-raise invalid operator errors
|
|
913
|
-
rescue JSON::ParserError
|
|
914
|
-
return true
|
|
915
|
-
end
|
|
883
|
+
# Returns whether the value and operand can be meaningfully compared with the operator.
|
|
884
|
+
# Note this is only used by check() because wait() polls until the value changes.
|
|
885
|
+
def _valid_comparison?(value, operator, operand)
|
|
886
|
+
return true if operator.nil?
|
|
916
887
|
|
|
917
888
|
if [">=", "<=", ">", "<"].include?(operator)
|
|
918
|
-
return false if value.nil?
|
|
889
|
+
return false if value.nil? or operand.nil?
|
|
890
|
+
return false if value.is_a?(Array) or operand.is_a?(Array)
|
|
891
|
+
return false if value.is_a?(String) != operand.is_a?(String)
|
|
919
892
|
end
|
|
893
|
+
# Note 'in' does not need a check here because the parser already requires a list operand
|
|
920
894
|
|
|
921
895
|
return true
|
|
922
896
|
end
|
|
@@ -26,6 +26,37 @@ module OpenC3
|
|
|
26
26
|
# whitespace around them is optional.
|
|
27
27
|
SCANNING_REGULAR_EXPRESSION = %r{ (?:"(?:[^\\"]|\\.)*") | (?:'(?:[^\\']|\\.)*') | (?:\[(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*\]) | , | [^\s,]+ }x # "
|
|
28
28
|
|
|
29
|
+
# Operators supported by check(), wait() and wait_check() comparisons
|
|
30
|
+
COMPARISON_OPERATORS = ["==", "!=", ">=", "<=", ">", "<", "in"]
|
|
31
|
+
|
|
32
|
+
# Single character escape sequences processed inside a double quoted comparison operand.
|
|
33
|
+
# The numeric (\nnn, \xHH, \uHHHH, \u{...}) forms are handled by unescape_double_quoted().
|
|
34
|
+
DOUBLE_QUOTE_ESCAPES = {
|
|
35
|
+
'\\' => "\\", '"' => '"', "'" => "'", '#' => '#', 'n' => "\n", 't' => "\t",
|
|
36
|
+
'r' => "\r", 'a' => "\a", 'b' => "\b", 'e' => "\e", 'f' => "\f", 'v' => "\v", 's' => ' '
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Tokenizes a double quoted operand into escape sequences, interpolation markers and runs
|
|
40
|
+
# of plain characters. The alternatives are ordered longest first so \u{1F600} is not read
|
|
41
|
+
# as \u, and the escape alternatives precede the markers so \#{ is a literal '#{'.
|
|
42
|
+
DOUBLE_QUOTE_TOKEN_REGEX =
|
|
43
|
+
/\\u\{[\h\s]*\}|\\u\h{4}|\\x\h{1,2}|\\[0-7]{1,3}|\\M-\\C-.|\\M-.|\\C-.|\\c.|\\.|\#[{@$]|[^\\\#]+|\#/m
|
|
44
|
+
|
|
45
|
+
# Escape sequences which have a meaning we deliberately do not implement. Dropping the
|
|
46
|
+
# backslash would silently change the value so they are rejected instead.
|
|
47
|
+
UNSUPPORTED_ESCAPE_REGEX = /\A\\(?:c|C-|M-)/
|
|
48
|
+
|
|
49
|
+
# The Ruby string interpolation markers. Interpolation would be code execution so it is
|
|
50
|
+
# rejected rather than silently compared against the uninterpolated text.
|
|
51
|
+
INTERPOLATION_REGEX = /\A\#[{@$]/
|
|
52
|
+
|
|
53
|
+
# Limits Ruby enforces on a \u escape. A codepoint is written with at most six hex digits,
|
|
54
|
+
# the largest character Unicode defines is 0x10FFFF, and the surrogate range is reserved
|
|
55
|
+
# for UTF-16 pairs so it is not a character on its own.
|
|
56
|
+
MAX_UNICODE_DIGITS = 6
|
|
57
|
+
MAX_UNICODE_CODEPOINT = 0x10FFFF
|
|
58
|
+
UNICODE_SURROGATE_RANGE = (0xD800..0xDFFF)
|
|
59
|
+
|
|
29
60
|
private
|
|
30
61
|
|
|
31
62
|
# Pulls all string keyword arguments into the args array. Raises on any symbol keyword arguments.
|
|
@@ -165,8 +196,6 @@ module OpenC3
|
|
|
165
196
|
|
|
166
197
|
# Splits `check()` comparison expressions, e.g. "== 'foo bar'" becomes ["==", "foo bar"]
|
|
167
198
|
def extract_operator_and_operand_from_comparison(comparison)
|
|
168
|
-
valid_operators = ["==", "!=", ">=", "<=", ">", "<", "in"]
|
|
169
|
-
|
|
170
199
|
operator, operand = comparison.split(nil, 2) # Ruby: second split arg is max number of resultant elements
|
|
171
200
|
|
|
172
201
|
if operand.nil?
|
|
@@ -175,29 +204,196 @@ module OpenC3
|
|
|
175
204
|
return [nil, nil]
|
|
176
205
|
end
|
|
177
206
|
|
|
178
|
-
raise "ERROR: Invalid operator: '#{operator}'" unless
|
|
207
|
+
raise "ERROR: Invalid operator: '#{operator}'" unless COMPARISON_OPERATORS.include?(operator)
|
|
208
|
+
|
|
209
|
+
# split leaves any trailing whitespace on the operand which the anchored matches reject
|
|
210
|
+
operand = extract_operand(operand.strip)
|
|
211
|
+
# 'in' is containment against a list of values in both Ruby and Python.
|
|
212
|
+
# Enforced here so check(), wait() and wait_check() all reject the same thing.
|
|
213
|
+
if operator == "in" and !operand.is_a?(Array)
|
|
214
|
+
raise "ERROR: The 'in' operator requires a list operand: #{operand.inspect}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
return [operator, operand]
|
|
218
|
+
end
|
|
179
219
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
220
|
+
# Converts the operand of a `check()` comparison expression into a Ruby value.
|
|
221
|
+
# Note this deliberately does not eval the operand so only literal values are supported.
|
|
222
|
+
def extract_operand(operand)
|
|
223
|
+
# A quoted operand must be a single complete string literal. Anything trailing the
|
|
224
|
+
# closing quote, e.g. "== 'a' garbage 'b'", is a syntax error rather than a string.
|
|
225
|
+
if (match = operand.match(/\A'((?:[^'\\]|\\.)*)'\z/m))
|
|
226
|
+
# Ruby single quoted strings only escape the backslash and the single quote
|
|
227
|
+
return match[1].gsub(/\\([\\'])/) { $1 }
|
|
228
|
+
end
|
|
229
|
+
if (match = operand.match(/\A"((?:[^"\\]|\\.)*)"\z/m))
|
|
230
|
+
return unescape_double_quoted(match[1])
|
|
184
231
|
end
|
|
232
|
+
return nil if operand == "nil"
|
|
233
|
+
return false if operand == "false"
|
|
234
|
+
return true if operand == "true"
|
|
185
235
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
# Ruby's Float() rejects these but Python's float() accepts them
|
|
237
|
+
case operand.upcase
|
|
238
|
+
when 'INFINITY', '+INFINITY', 'INF', '+INF'
|
|
239
|
+
return Float::INFINITY
|
|
240
|
+
when '-INFINITY', '-INF'
|
|
241
|
+
return -Float::INFINITY
|
|
242
|
+
when 'NAN', '+NAN', '-NAN'
|
|
243
|
+
return Float::NAN
|
|
193
244
|
else
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
245
|
+
# Not an infinity or NaN keyword so fall through to the formats below
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Arrays are parsed recursively so their elements follow exactly the same rules as a
|
|
249
|
+
# bare operand. JSON alone would reject ['ON', 'OFF'], [0xA, 0xB] and [true, nil].
|
|
250
|
+
if operand.start_with?('[') and operand.end_with?(']')
|
|
251
|
+
return split_operand_list(operand[1..-2]).map { |element| extract_operand(element) }
|
|
252
|
+
end
|
|
253
|
+
# JSON handles decimal numbers and hashes
|
|
254
|
+
begin
|
|
255
|
+
return JSON.parse(operand)
|
|
256
|
+
rescue JSON::ParserError
|
|
257
|
+
# Fall through to the number formats JSON does not accept
|
|
258
|
+
end
|
|
259
|
+
# Number formats JSON does not accept
|
|
260
|
+
begin
|
|
261
|
+
# Explicit base prefix: hex (0x), octal (0o) or binary (0b)
|
|
262
|
+
return Integer(operand) if operand.match?(/\A[+-]?0[xXoObB][0-9a-fA-F_]+\z/)
|
|
263
|
+
# Integers with underscore separators, e.g. 1_000. A leading zero is deliberately
|
|
264
|
+
# rejected because it is ambiguous: Ruby reads 010 as octal 8 while Python rejects it.
|
|
265
|
+
# Write 0o10 for octal 8 or 10 for decimal 10.
|
|
266
|
+
return Integer(operand, 10) if operand.match?(/\A[+-]?[1-9][0-9_]*\z/)
|
|
267
|
+
# Floats with underscore separators, e.g. 1_000.5. A leading zero is unambiguous here.
|
|
268
|
+
return Float(operand) if operand.match?(/\A[+-]?[0-9][0-9_]*(?:\.[0-9_]+(?:[eE][+-]?[0-9_]+)?|[eE][+-]?[0-9_]+)\z/)
|
|
269
|
+
rescue ArgumentError
|
|
270
|
+
# Fall through to the error cases
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# A bare word is almost always a string the user forgot to quote
|
|
274
|
+
if operand.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
275
|
+
raise NameError, "Uninitialized constant #{operand}. Did you mean '#{operand}' as a string?"
|
|
276
|
+
end
|
|
277
|
+
raise "ERROR: Unable to parse operand: #{operand}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Processes the escape sequences in the contents of a double quoted operand
|
|
281
|
+
def unescape_double_quoted(string)
|
|
282
|
+
# A \xHH or octal escape can produce a byte which is not valid UTF-8 so build the result
|
|
283
|
+
# in binary. Otherwise preserve the encoding of the comparison the user passed in.
|
|
284
|
+
# The escape must not itself be escaped, e.g. "C:\\0" is a backslash followed by a zero.
|
|
285
|
+
binary = string.match?(/(?<!\\)(?:\\\\)*\\(?:x\h|[0-7])/)
|
|
286
|
+
result = binary ? ''.b : String.new(encoding: string.encoding)
|
|
287
|
+
string.scan(DOUBLE_QUOTE_TOKEN_REGEX).each do |token|
|
|
288
|
+
replacement =
|
|
289
|
+
if token.match?(INTERPOLATION_REGEX)
|
|
290
|
+
raise "ERROR: String interpolation is not supported in an operand: #{token}. " +
|
|
291
|
+
'Interpolate in the script itself or escape it as \#' + token[1] +
|
|
292
|
+
' to compare against the literal text'
|
|
293
|
+
elsif !token.start_with?('\\')
|
|
294
|
+
token
|
|
295
|
+
elsif token.start_with?('\u{')
|
|
296
|
+
# \u{1F600} or \u{48 49} which is one or more whitespace separated codepoints
|
|
297
|
+
token[3..-2].split.map { |codepoint| pack_unicode_codepoint(codepoint, token) }.join
|
|
298
|
+
elsif token.length == 6 and token.start_with?('\u')
|
|
299
|
+
pack_unicode_codepoint(token[2..-1], token)
|
|
300
|
+
elsif token.length > 2 and token.start_with?('\x')
|
|
301
|
+
[token[2..-1].hex].pack('C')
|
|
302
|
+
elsif token.match?(/\A\\[0-7]/)
|
|
303
|
+
[token[1..-1].to_i(8) & 0xFF].pack('C')
|
|
304
|
+
elsif token.match?(UNSUPPORTED_ESCAPE_REGEX)
|
|
305
|
+
raise "ERROR: Unsupported escape sequence in operand: #{token}"
|
|
306
|
+
elsif token == '\u' or token == '\x'
|
|
307
|
+
raise "ERROR: Invalid escape sequence in operand: #{token}"
|
|
308
|
+
else
|
|
309
|
+
# Ruby drops the backslash of an escape which has no special meaning, e.g. "\q"
|
|
310
|
+
DOUBLE_QUOTE_ESCAPES.fetch(token[1], token[1])
|
|
311
|
+
end
|
|
312
|
+
result << (binary ? replacement.b : replacement)
|
|
313
|
+
end
|
|
314
|
+
return result
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Packs a single codepoint of a \u escape into a UTF-8 character. Ruby rejects a codepoint
|
|
318
|
+
# which is too long, above the largest Unicode character or in the UTF-16 surrogate range,
|
|
319
|
+
# so those raise here rather than silently packing into an invalid UTF-8 string.
|
|
320
|
+
def pack_unicode_codepoint(codepoint, token)
|
|
321
|
+
value = codepoint.hex
|
|
322
|
+
if codepoint.length > MAX_UNICODE_DIGITS or value > MAX_UNICODE_CODEPOINT or
|
|
323
|
+
UNICODE_SURROGATE_RANGE.include?(value)
|
|
324
|
+
raise "ERROR: Invalid Unicode codepoint in operand: #{token}"
|
|
325
|
+
end
|
|
326
|
+
return [value].pack('U')
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Splits the contents of a bracketed operand list on the top level commas.
|
|
330
|
+
# Quotes and nesting are tracked so "['a,b', [1, 2]]" is two elements, not four.
|
|
331
|
+
def split_operand_list(text)
|
|
332
|
+
elements = []
|
|
333
|
+
current = String.new(encoding: text.encoding)
|
|
334
|
+
depth = 0
|
|
335
|
+
quote = nil
|
|
336
|
+
escaped = false
|
|
337
|
+
text.each_char do |char|
|
|
338
|
+
if escaped
|
|
339
|
+
escaped = false
|
|
340
|
+
elsif quote
|
|
341
|
+
if char == '\\'
|
|
342
|
+
escaped = true
|
|
343
|
+
elsif char == quote
|
|
344
|
+
quote = nil
|
|
345
|
+
end
|
|
346
|
+
elsif char == "'" or char == '"'
|
|
347
|
+
quote = char
|
|
348
|
+
elsif char == '[' or char == '{'
|
|
349
|
+
depth += 1
|
|
350
|
+
elsif char == ']' or char == '}'
|
|
351
|
+
depth -= 1
|
|
352
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if depth < 0
|
|
353
|
+
elsif char == ',' and depth == 0
|
|
354
|
+
elements << current.strip
|
|
355
|
+
current = String.new(encoding: text.encoding)
|
|
356
|
+
next
|
|
198
357
|
end
|
|
358
|
+
current << char
|
|
199
359
|
end
|
|
200
|
-
|
|
360
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if depth != 0 or quote
|
|
361
|
+
|
|
362
|
+
elements << current.strip
|
|
363
|
+
# A single empty element is an empty list, e.g. []
|
|
364
|
+
return [] if elements.length == 1 and elements[0].empty?
|
|
365
|
+
# A trailing comma is allowed, e.g. [1,], matching both Ruby and Python list literals
|
|
366
|
+
elements.pop if elements[-1].empty?
|
|
367
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if elements.empty? or elements.any?(&:empty?)
|
|
368
|
+
|
|
369
|
+
return elements
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Compares a telemetry value against an operand using the given operator.
|
|
373
|
+
# Returns false rather than raising if the two values can not be compared.
|
|
374
|
+
def compare_values(value, operator, operand)
|
|
375
|
+
case operator
|
|
376
|
+
when "=="
|
|
377
|
+
value == operand
|
|
378
|
+
when "!="
|
|
379
|
+
value != operand
|
|
380
|
+
when ">"
|
|
381
|
+
value > operand
|
|
382
|
+
when ">="
|
|
383
|
+
value >= operand
|
|
384
|
+
when "<"
|
|
385
|
+
value < operand
|
|
386
|
+
when "<="
|
|
387
|
+
value <= operand
|
|
388
|
+
when "in"
|
|
389
|
+
# 'in' is containment against a list of values, matching Python
|
|
390
|
+
operand.is_a?(Array) && operand.include?(value)
|
|
391
|
+
else
|
|
392
|
+
raise "ERROR: Invalid operator: '#{operator}'"
|
|
393
|
+
end
|
|
394
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
395
|
+
# Comparing incompatible types, e.g. nil > 1, is simply not a match
|
|
396
|
+
false
|
|
201
397
|
end
|
|
202
398
|
end
|
|
203
399
|
end
|
|
@@ -19,6 +19,7 @@ module OpenC3
|
|
|
19
19
|
response_body = nil
|
|
20
20
|
begin
|
|
21
21
|
endpoint = "/openc3-api/plugins?scope=#{scope}"
|
|
22
|
+
endpoint += "&exclude_default=true" unless default
|
|
22
23
|
uri = URI.parse($api_server.generate_url + endpoint)
|
|
23
24
|
auth = $api_server.generate_auth
|
|
24
25
|
|
|
@@ -30,19 +31,11 @@ module OpenC3
|
|
|
30
31
|
http.request(request) do |response|
|
|
31
32
|
response_body = response.body
|
|
32
33
|
response.value() # Raises an HTTP error if the response is not 2xx (success)
|
|
33
|
-
|
|
34
|
-
if default
|
|
35
|
-
return plugins
|
|
36
|
-
else
|
|
37
|
-
return plugins.select do |plugin|
|
|
38
|
-
!plugin.include?('openc3-cosmos-tool-') and !plugin.include?('openc3-tool-base') and
|
|
39
|
-
!plugin.include?('openc3-cosmos-enterprise-tool-') and !plugin.include?('openc3-enterprise-tool-base')
|
|
40
|
-
end
|
|
41
|
-
end
|
|
34
|
+
return JSON.parse(response.body, allow_nan: true, create_additions: true)
|
|
42
35
|
end
|
|
43
36
|
end
|
|
44
37
|
rescue => e
|
|
45
|
-
raise "
|
|
38
|
+
raise "plugin_list failed due to #{e.formatted}\nURI:#{uri}\nResponse:\n#{response_body}"
|
|
46
39
|
end
|
|
47
40
|
end
|
|
48
41
|
|
|
@@ -65,7 +58,7 @@ module OpenC3
|
|
|
65
58
|
end
|
|
66
59
|
end
|
|
67
60
|
rescue => e
|
|
68
|
-
raise "
|
|
61
|
+
raise "plugin_get failed due to #{e.formatted}\nURI:#{uri}\nResponse:\n#{response_body}"
|
|
69
62
|
end
|
|
70
63
|
end
|
|
71
64
|
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
require 'tempfile'
|
|
19
19
|
require 'net/http'
|
|
20
|
+
require 'base64'
|
|
21
|
+
require 'openc3/utilities/local_mode'
|
|
20
22
|
|
|
21
23
|
ENV['OPENC3_CLOUD'] ||= 'local'
|
|
22
24
|
|
|
@@ -53,6 +55,15 @@ module OpenC3
|
|
|
53
55
|
|
|
54
56
|
upload_path = "#{scope}/targets_modified/#{path}"
|
|
55
57
|
|
|
58
|
+
if OpenC3::LocalMode.local_only_name?(path)
|
|
59
|
+
# The local mode volume is only mounted in the cluster so from outside
|
|
60
|
+
# write it through the API which never sends it to the bucket
|
|
61
|
+
return _upload_api_file(upload_path, io_or_string, scope: scope) unless $openc3_in_cluster
|
|
62
|
+
puts "Writing local #{upload_path}"
|
|
63
|
+
OpenC3::LocalMode.put_target_file(upload_path, io_or_string, scope: scope)
|
|
64
|
+
return nil
|
|
65
|
+
end
|
|
66
|
+
|
|
56
67
|
if ENV['OPENC3_LOCAL_MODE'] and $openc3_in_cluster
|
|
57
68
|
OpenC3::LocalMode.put_target_file(upload_path, io_or_string, scope: scope)
|
|
58
69
|
io_or_string.rewind if io_or_string.respond_to?(:rewind)
|
|
@@ -91,22 +102,22 @@ module OpenC3
|
|
|
91
102
|
# @param original [Boolean] Whether to get the original or modified file
|
|
92
103
|
# @return [File|nil]
|
|
93
104
|
def get_target_file(path, original: false, scope: $openc3_scope)
|
|
105
|
+
# Local only targets only exist in the local mode volume so original doesn't apply
|
|
106
|
+
if OpenC3::LocalMode.local_only_name?(path)
|
|
107
|
+
# The local mode volume is only mounted in the cluster so from outside
|
|
108
|
+
# read it through the API which serves it from the volume
|
|
109
|
+
return _get_api_file("targets_modified/#{path}", scope: scope) unless $openc3_in_cluster
|
|
110
|
+
return _get_local_file(path, scope: scope)
|
|
111
|
+
end
|
|
112
|
+
|
|
94
113
|
part = "targets"
|
|
95
114
|
part += "_modified" unless original
|
|
96
115
|
# Loop to allow redo when switching from modified to original
|
|
97
116
|
loop do
|
|
98
117
|
begin
|
|
99
118
|
if part == "targets_modified" and ENV['OPENC3_LOCAL_MODE']
|
|
100
|
-
|
|
101
|
-
if
|
|
102
|
-
puts "Reading local #{scope}/#{part}/#{path}"
|
|
103
|
-
file = Tempfile.new('target', binmode: true)
|
|
104
|
-
file.filename = path
|
|
105
|
-
file.write(local_file.read)
|
|
106
|
-
local_file.close
|
|
107
|
-
file.rewind
|
|
108
|
-
return file
|
|
109
|
-
end
|
|
119
|
+
file = _get_local_file(path, scope: scope)
|
|
120
|
+
return file if file
|
|
110
121
|
end
|
|
111
122
|
|
|
112
123
|
return _get_storage_file("#{part}/#{path}", scope: scope)
|
|
@@ -125,7 +136,53 @@ module OpenC3
|
|
|
125
136
|
|
|
126
137
|
# These are helper methods ... should not be used directly
|
|
127
138
|
|
|
139
|
+
# Write a file through the API instead of a presigned URL
|
|
140
|
+
def _upload_api_file(path, io_or_string, bucket: 'OPENC3_CONFIG_BUCKET', scope: $openc3_scope)
|
|
141
|
+
data = String === io_or_string ? io_or_string : io_or_string.read
|
|
142
|
+
endpoint = "/openc3-api/storage/upload_file/#{path}"
|
|
143
|
+
puts "Writing #{path}"
|
|
144
|
+
response = $api_server.request('put', endpoint, query: { bucket: bucket },
|
|
145
|
+
data: { contents: Base64.strict_encode64(data) }, json: true, scope: scope)
|
|
146
|
+
if response.nil? || response.status != 200
|
|
147
|
+
raise "Failed to write #{path}"
|
|
148
|
+
end
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Read a file through the API instead of a presigned URL
|
|
153
|
+
# Returns nil if the file doesn't exist
|
|
154
|
+
def _get_api_file(path, bucket: 'OPENC3_CONFIG_BUCKET', scope: $openc3_scope)
|
|
155
|
+
endpoint = "/openc3-api/storage/download_file/#{scope}/#{path}"
|
|
156
|
+
response = $api_server.request('get', endpoint, query: { bucket: bucket }, scope: scope)
|
|
157
|
+
return nil if response && response.status == 404
|
|
158
|
+
if response.nil? || response.status != 200
|
|
159
|
+
raise "Failed to read #{scope}/#{path}"
|
|
160
|
+
end
|
|
161
|
+
puts "Reading #{scope}/#{path}"
|
|
162
|
+
json = JSON.parse(response.body, allow_nan: true, create_additions: true)
|
|
163
|
+
file = Tempfile.new('target', binmode: true)
|
|
164
|
+
file.filename = path
|
|
165
|
+
file.write(Base64.decode64(json['contents']))
|
|
166
|
+
file.rewind
|
|
167
|
+
return file
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def _get_local_file(path, scope: $openc3_scope)
|
|
171
|
+
local_file = OpenC3::LocalMode.open_local_file(path, scope: scope)
|
|
172
|
+
return nil unless local_file
|
|
173
|
+
puts "Reading local #{scope}/targets_modified/#{path}"
|
|
174
|
+
file = Tempfile.new('target', binmode: true)
|
|
175
|
+
file.filename = path
|
|
176
|
+
file.write(local_file.read)
|
|
177
|
+
local_file.close
|
|
178
|
+
file.rewind
|
|
179
|
+
return file
|
|
180
|
+
end
|
|
181
|
+
|
|
128
182
|
def _get_download_url(path, scope: $openc3_scope)
|
|
183
|
+
if OpenC3::LocalMode.local_only_name?(path)
|
|
184
|
+
raise "#{path} belongs to a local only target (OPENC3_LOCAL_ONLY_TARGETS) and has no bucket download URL"
|
|
185
|
+
end
|
|
129
186
|
targets = "targets_modified" # First try targets_modified
|
|
130
187
|
response = $api_server.request('get', "/openc3-api/storage/exists/#{scope}/#{targets}/#{path}", query: { bucket: 'OPENC3_CONFIG_BUCKET' }, scope: scope)
|
|
131
188
|
if response.status != 200
|
|
@@ -167,6 +224,20 @@ module OpenC3
|
|
|
167
224
|
return file
|
|
168
225
|
end
|
|
169
226
|
|
|
227
|
+
# Retrieve the file selected in open_bucket_dialog
|
|
228
|
+
# input is the hash returned by the frontend: { 'bucket', 'path', 'filename' }
|
|
229
|
+
def _get_bucket_dialog_file(input, scope: $openc3_scope)
|
|
230
|
+
bucket = input['bucket']
|
|
231
|
+
# Path from bucket dialog already includes scope prefix (e.g. DEFAULT/targets/...)
|
|
232
|
+
# _get_storage_file prepends scope, so strip it from the path to avoid doubling
|
|
233
|
+
path = input['path'].sub(/\A#{Regexp.escape(scope)}\//, '')
|
|
234
|
+
file = _get_storage_file(path, bucket: bucket, scope: scope)
|
|
235
|
+
file.filename = input['filename']
|
|
236
|
+
file.bucket = bucket
|
|
237
|
+
file.bucket_path = input['path']
|
|
238
|
+
file
|
|
239
|
+
end
|
|
240
|
+
|
|
170
241
|
def _get_uri(url)
|
|
171
242
|
if $openc3_in_cluster
|
|
172
243
|
case ENV.fetch('OPENC3_CLOUD', nil)
|