openc3 5.17.0 → 5.18.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.
- checksums.yaml +4 -4
- data/bin/openc3cli +1 -1
- data/data/config/_interfaces.yaml +4 -4
- data/data/config/command_modifiers.yaml +4 -0
- data/data/config/interface_modifiers.yaml +18 -8
- data/data/config/item_modifiers.yaml +34 -26
- data/data/config/microservice.yaml +4 -1
- data/data/config/param_item_modifiers.yaml +16 -0
- data/data/config/parameter_modifiers.yaml +29 -12
- data/data/config/plugins.yaml +3 -3
- data/data/config/screen.yaml +7 -7
- data/data/config/telemetry_modifiers.yaml +9 -4
- data/data/config/widgets.yaml +41 -14
- data/ext/openc3/ext/packet/packet.c +6 -0
- data/lib/openc3/accessors/accessor.rb +1 -0
- data/lib/openc3/accessors/binary_accessor.rb +170 -11
- data/lib/openc3/api/cmd_api.rb +39 -35
- data/lib/openc3/api/config_api.rb +10 -10
- data/lib/openc3/api/interface_api.rb +28 -21
- data/lib/openc3/api/limits_api.rb +29 -29
- data/lib/openc3/api/metrics_api.rb +3 -3
- data/lib/openc3/api/offline_access_api.rb +5 -5
- data/lib/openc3/api/router_api.rb +25 -19
- data/lib/openc3/api/settings_api.rb +10 -10
- data/lib/openc3/api/stash_api.rb +10 -10
- data/lib/openc3/api/target_api.rb +10 -10
- data/lib/openc3/api/tlm_api.rb +44 -44
- data/lib/openc3/conversions/bit_reverse_conversion.rb +60 -0
- data/lib/openc3/conversions/ip_read_conversion.rb +59 -0
- data/lib/openc3/conversions/ip_write_conversion.rb +61 -0
- data/lib/openc3/conversions/object_read_conversion.rb +88 -0
- data/lib/openc3/conversions/object_write_conversion.rb +38 -0
- data/lib/openc3/conversions.rb +6 -1
- data/lib/openc3/io/json_drb.rb +19 -21
- data/lib/openc3/io/json_rpc.rb +14 -13
- data/lib/openc3/microservices/microservice.rb +11 -11
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +1 -1
- data/lib/openc3/microservices/timeline_microservice.rb +76 -51
- data/lib/openc3/models/activity_model.rb +25 -21
- data/lib/openc3/models/scope_model.rb +44 -13
- data/lib/openc3/models/sorted_model.rb +1 -1
- data/lib/openc3/models/target_model.rb +4 -1
- data/lib/openc3/operators/microservice_operator.rb +2 -2
- data/lib/openc3/operators/operator.rb +9 -9
- data/lib/openc3/packets/packet.rb +18 -1
- data/lib/openc3/packets/packet_config.rb +37 -16
- data/lib/openc3/packets/packet_item.rb +5 -0
- data/lib/openc3/packets/structure.rb +67 -3
- data/lib/openc3/packets/structure_item.rb +49 -12
- data/lib/openc3/script/calendar.rb +2 -2
- data/lib/openc3/script/extract.rb +5 -3
- data/lib/openc3/script/web_socket_api.rb +11 -0
- data/lib/openc3/topics/decom_interface_topic.rb +2 -1
- data/lib/openc3/topics/system_events_topic.rb +40 -0
- data/lib/openc3/utilities/authentication.rb +2 -1
- data/lib/openc3/utilities/authorization.rb +2 -2
- data/lib/openc3/version.rb +5 -5
- data/templates/tool_angular/package.json +5 -5
- data/templates/tool_react/package.json +8 -8
- data/templates/tool_svelte/package.json +10 -10
- data/templates/tool_vue/package.json +10 -10
- data/templates/widget/package.json +10 -10
- data/templates/widget/src/Widget.vue +0 -1
- metadata +22 -2
data/lib/openc3/io/json_drb.rb
CHANGED
@@ -14,18 +14,16 @@
|
|
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 2024, 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 'thread'
|
24
23
|
require 'socket'
|
25
24
|
require 'json'
|
26
25
|
# require 'drb/acl'
|
27
26
|
require 'drb/drb'
|
28
|
-
require 'set'
|
29
27
|
require 'openc3/io/json_rpc'
|
30
28
|
require 'openc3/io/json_drb_rack'
|
31
29
|
require 'rackup'
|
@@ -185,10 +183,10 @@ module OpenC3
|
|
185
183
|
"Make sure all sockets/streams are closed in all applications,\n" +
|
186
184
|
"wait 1 minute and try again."
|
187
185
|
# Something else went wrong which is fatal
|
188
|
-
rescue =>
|
186
|
+
rescue => e
|
189
187
|
@server = nil
|
190
|
-
Logger.error "JsonDRb http server could not be started or unexpectedly died.\n#{
|
191
|
-
OpenC3.handle_fatal_exception(
|
188
|
+
Logger.error "JsonDRb http server could not be started or unexpectedly died.\n#{e.formatted}"
|
189
|
+
OpenC3.handle_fatal_exception(e)
|
192
190
|
end
|
193
191
|
|
194
192
|
# Wait for the server to be started in the thread before returning.
|
@@ -272,42 +270,42 @@ module OpenC3
|
|
272
270
|
if request.id
|
273
271
|
response = JsonRpcSuccessResponse.new(result, request.id)
|
274
272
|
end
|
275
|
-
rescue Exception =>
|
273
|
+
rescue Exception => e
|
276
274
|
# Filter out the framework stack trace (rails, rack, puma etc)
|
277
|
-
lines =
|
275
|
+
lines = e.formatted.split("\n")
|
278
276
|
i = lines.find_index { |row| row.include?('actionpack') || row.include?('activesupport') }
|
279
277
|
Logger.error lines[0...i].join("\n")
|
280
278
|
|
281
279
|
if request.id
|
282
|
-
if NoMethodError ===
|
280
|
+
if NoMethodError === e
|
283
281
|
error_code = JsonRpcError::ErrorCode::METHOD_NOT_FOUND
|
284
282
|
response = JsonRpcErrorResponse.new(
|
285
|
-
JsonRpcError.new(error_code, "Method not found",
|
283
|
+
JsonRpcError.new(error_code, "Method not found", e), request.id
|
286
284
|
)
|
287
|
-
elsif ArgumentError ===
|
285
|
+
elsif ArgumentError === e
|
288
286
|
error_code = JsonRpcError::ErrorCode::INVALID_PARAMS
|
289
287
|
response = JsonRpcErrorResponse.new(
|
290
|
-
JsonRpcError.new(error_code, "Invalid params",
|
288
|
+
JsonRpcError.new(error_code, "Invalid params", e), request.id
|
291
289
|
)
|
292
|
-
elsif AuthError ===
|
290
|
+
elsif AuthError === e
|
293
291
|
error_code = JsonRpcError::ErrorCode::AUTH_ERROR
|
294
292
|
response = JsonRpcErrorResponse.new(
|
295
|
-
JsonRpcError.new(error_code,
|
293
|
+
JsonRpcError.new(error_code, e.message, e), request.id
|
296
294
|
)
|
297
|
-
elsif ForbiddenError ===
|
295
|
+
elsif ForbiddenError === e
|
298
296
|
error_code = JsonRpcError::ErrorCode::FORBIDDEN_ERROR
|
299
297
|
response = JsonRpcErrorResponse.new(
|
300
|
-
JsonRpcError.new(error_code,
|
298
|
+
JsonRpcError.new(error_code, e.message, e), request.id
|
301
299
|
)
|
302
|
-
elsif HazardousError ===
|
300
|
+
elsif HazardousError === e
|
303
301
|
error_code = JsonRpcError::ErrorCode::HAZARDOUS_ERROR
|
304
302
|
response = JsonRpcErrorResponse.new(
|
305
|
-
JsonRpcError.new(error_code,
|
303
|
+
JsonRpcError.new(error_code, e.message, e), request.id
|
306
304
|
)
|
307
305
|
else
|
308
306
|
error_code = JsonRpcError::ErrorCode::OTHER_ERROR
|
309
307
|
response = JsonRpcErrorResponse.new(
|
310
|
-
JsonRpcError.new(error_code,
|
308
|
+
JsonRpcError.new(error_code, e.message, e), request.id
|
311
309
|
)
|
312
310
|
end
|
313
311
|
end
|
@@ -322,9 +320,9 @@ module OpenC3
|
|
322
320
|
end
|
323
321
|
response_data = process_response(response, start_time) if response
|
324
322
|
return response_data, error_code
|
325
|
-
rescue =>
|
323
|
+
rescue => e
|
326
324
|
error_code = JsonRpcError::ErrorCode::INVALID_REQUEST
|
327
|
-
response = JsonRpcErrorResponse.new(JsonRpcError.new(error_code, "Invalid Request",
|
325
|
+
response = JsonRpcErrorResponse.new(JsonRpcError.new(error_code, "Invalid Request", e), nil)
|
328
326
|
response_data = process_response(response, start_time)
|
329
327
|
return response_data, error_code
|
330
328
|
end
|
data/lib/openc3/io/json_rpc.rb
CHANGED
@@ -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
|
17
|
+
# All changes Copyright 2024, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
@@ -25,7 +25,7 @@ require 'date'
|
|
25
25
|
require 'openc3/core_ext/string'
|
26
26
|
|
27
27
|
class Object
|
28
|
-
def as_json(
|
28
|
+
def as_json(_options = nil) #:nodoc:
|
29
29
|
if respond_to?(:to_hash)
|
30
30
|
to_hash
|
31
31
|
else
|
@@ -43,21 +43,21 @@ class Struct #:nodoc:
|
|
43
43
|
end
|
44
44
|
|
45
45
|
class TrueClass
|
46
|
-
def as_json(
|
46
|
+
def as_json(_options = nil) self end #:nodoc:
|
47
47
|
end
|
48
48
|
|
49
49
|
class FalseClass
|
50
|
-
def as_json(
|
50
|
+
def as_json(_options = nil) self end #:nodoc:
|
51
51
|
end
|
52
52
|
|
53
53
|
class NilClass
|
54
|
-
def as_json(
|
54
|
+
def as_json(_options = nil) self end #:nodoc:
|
55
55
|
end
|
56
56
|
|
57
57
|
class String
|
58
58
|
NON_ASCII_PRINTABLE = /[^\x21-\x7e\s]/
|
59
59
|
NON_UTF8_PRINTABLE = /[\x00-\x08\x0E-\x1F\x7F]/
|
60
|
-
def as_json(
|
60
|
+
def as_json(_options = nil)
|
61
61
|
as_utf8 = self.dup.force_encoding('UTF-8')
|
62
62
|
if as_utf8.valid_encoding?
|
63
63
|
if as_utf8 =~ NON_UTF8_PRINTABLE
|
@@ -72,11 +72,11 @@ class String
|
|
72
72
|
end
|
73
73
|
|
74
74
|
class Symbol
|
75
|
-
def as_json(
|
75
|
+
def as_json(_options = nil) to_s end #:nodoc:
|
76
76
|
end
|
77
77
|
|
78
78
|
class Numeric
|
79
|
-
def as_json(
|
79
|
+
def as_json(_options = nil) self end #:nodoc:
|
80
80
|
end
|
81
81
|
|
82
82
|
class Float
|
@@ -88,7 +88,7 @@ class Float
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
-
def as_json(
|
91
|
+
def as_json(_options = nil)
|
92
92
|
return { "json_class" => "Float", "raw" => "Infinity" } if self.infinite? == 1
|
93
93
|
return { "json_class" => "Float", "raw" => "-Infinity" } if self.infinite? == -1
|
94
94
|
return { "json_class" => "Float", "raw" => "NaN" } if self.nan?
|
@@ -98,7 +98,7 @@ class Float
|
|
98
98
|
end
|
99
99
|
|
100
100
|
class Regexp
|
101
|
-
def as_json(
|
101
|
+
def as_json(_options = nil) to_s end #:nodoc:
|
102
102
|
end
|
103
103
|
|
104
104
|
module Enumerable
|
@@ -122,19 +122,19 @@ class Hash
|
|
122
122
|
end
|
123
123
|
|
124
124
|
class Time
|
125
|
-
def as_json(
|
125
|
+
def as_json(_options = nil) #:nodoc:
|
126
126
|
self.to_s
|
127
127
|
end
|
128
128
|
end
|
129
129
|
|
130
130
|
class Date
|
131
|
-
def as_json(
|
131
|
+
def as_json(_options = nil) #:nodoc:
|
132
132
|
self.to_s
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
136
136
|
class DateTime
|
137
|
-
def as_json(
|
137
|
+
def as_json(_options = nil) #:nodoc:
|
138
138
|
self.to_s
|
139
139
|
end
|
140
140
|
end
|
@@ -269,6 +269,7 @@ module OpenC3
|
|
269
269
|
def self.from_json(request_data, request_headers)
|
270
270
|
hash = JSON.parse(request_data, :allow_nan => true, :create_additions => true)
|
271
271
|
hash['keyword_params']['token'] = request_headers['HTTP_AUTHORIZATION'] if request_headers['HTTP_AUTHORIZATION']
|
272
|
+
hash['keyword_params']['manual'] = request_headers['HTTP_MANUAL'] if request_headers['HTTP_MANUAL']
|
272
273
|
# Verify the jsonrpc version is correct and there is a method and id
|
273
274
|
raise unless hash['jsonrpc'.freeze] == "2.0".freeze && hash['method'.freeze] && hash['id'.freeze]
|
274
275
|
|
@@ -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
|
17
|
+
# All changes Copyright 2024, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
@@ -54,13 +54,13 @@ module OpenC3
|
|
54
54
|
microservice.state = 'RUNNING'
|
55
55
|
microservice.run
|
56
56
|
microservice.state = 'FINISHED'
|
57
|
-
rescue Exception =>
|
58
|
-
if SystemExit ===
|
57
|
+
rescue Exception => e
|
58
|
+
if SystemExit === e or SignalException === e
|
59
59
|
microservice.state = 'KILLED'
|
60
60
|
else
|
61
|
-
microservice.error =
|
61
|
+
microservice.error = e
|
62
62
|
microservice.state = 'DIED_ERROR'
|
63
|
-
Logger.fatal("Microservice #{name} dying from exception\n#{
|
63
|
+
Logger.fatal("Microservice #{name} dying from exception\n#{e.formatted}")
|
64
64
|
end
|
65
65
|
ensure
|
66
66
|
MicroserviceStatusModel.set(microservice.as_json(:allow_nan => true), scope: microservice.scope)
|
@@ -171,7 +171,7 @@ module OpenC3
|
|
171
171
|
# Run ruby syntax so we can log those
|
172
172
|
syntax_check, _ = Open3.capture2e("ruby -c #{ruby_filename}")
|
173
173
|
if /Syntax OK/.match?(syntax_check)
|
174
|
-
@logger.
|
174
|
+
@logger.debug("Ruby microservice #{@name} file #{ruby_filename} passed syntax check\n", scope: @scope)
|
175
175
|
else
|
176
176
|
@logger.error("Ruby microservice #{@name} file #{ruby_filename} failed syntax check\n#{syntax_check}", scope: @scope)
|
177
177
|
end
|
@@ -188,9 +188,9 @@ module OpenC3
|
|
188
188
|
MicroserviceStatusModel.set(as_json(:allow_nan => true), scope: @scope) unless @cancel_thread
|
189
189
|
break if @microservice_status_sleeper.sleep(@microservice_status_period_seconds)
|
190
190
|
end
|
191
|
-
rescue Exception =>
|
192
|
-
@logger.error "#{@name} status thread died: #{
|
193
|
-
raise
|
191
|
+
rescue Exception => e
|
192
|
+
@logger.error "#{@name} status thread died: #{e.formatted}"
|
193
|
+
raise e
|
194
194
|
end
|
195
195
|
end
|
196
196
|
end
|
@@ -208,7 +208,7 @@ module OpenC3
|
|
208
208
|
MicroserviceStatusModel.set(as_json(:allow_nan => true), scope: @scope)
|
209
209
|
FileUtils.remove_entry(@temp_dir) if File.exist?(@temp_dir)
|
210
210
|
@metric.shutdown
|
211
|
-
@logger.
|
211
|
+
@logger.debug("Shutting down microservice complete: #{@name}")
|
212
212
|
@shutdown_complete = true
|
213
213
|
end
|
214
214
|
|
@@ -220,7 +220,7 @@ module OpenC3
|
|
220
220
|
end
|
221
221
|
|
222
222
|
# Returns if the command was handled
|
223
|
-
def microservice_cmd(topic, msg_id, msg_hash,
|
223
|
+
def microservice_cmd(topic, msg_id, msg_hash, _redis)
|
224
224
|
command = msg_hash['command']
|
225
225
|
case command
|
226
226
|
when 'ADD_TOPICS'
|
@@ -22,7 +22,7 @@ require 'openc3/microservices/cleanup_microservice'
|
|
22
22
|
module OpenC3
|
23
23
|
class ScopeCleanupMicroservice < CleanupMicroservice
|
24
24
|
def run
|
25
|
-
scope = ScopeModel.get_model(name: @scope
|
25
|
+
scope = ScopeModel.get_model(name: @scope)
|
26
26
|
|
27
27
|
areas = [
|
28
28
|
["#{@scope}/text_logs", scope.text_log_retain_time],
|
@@ -24,6 +24,7 @@ require 'openc3/utilities/authentication'
|
|
24
24
|
require 'openc3/microservices/microservice'
|
25
25
|
require 'openc3/models/activity_model'
|
26
26
|
require 'openc3/models/timeline_model'
|
27
|
+
require 'openc3/models/tool_config_model'
|
27
28
|
require 'openc3/topics/timeline_topic'
|
28
29
|
|
29
30
|
require 'openc3/script'
|
@@ -66,61 +67,82 @@ module OpenC3
|
|
66
67
|
|
67
68
|
run_activity(activity)
|
68
69
|
end
|
69
|
-
@logger.info "#{@timeline_name}
|
70
|
+
@logger.info "#{@timeline_name} timeline worker exiting"
|
70
71
|
end
|
71
72
|
|
72
73
|
def run_activity(activity)
|
73
|
-
case activity.kind.
|
74
|
-
when '
|
74
|
+
case activity.kind.downcase
|
75
|
+
when 'command'
|
75
76
|
run_command(activity)
|
76
|
-
when '
|
77
|
+
when 'script'
|
77
78
|
run_script(activity)
|
78
|
-
when '
|
79
|
+
when 'expire'
|
79
80
|
clear_expired(activity)
|
80
81
|
else
|
81
82
|
@logger.error "Unknown kind passed to microservice #{@timeline_name}: #{activity.as_json(:allow_nan => true)}"
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
86
|
+
def get_exec_setting()
|
87
|
+
json = ToolConfigModel.load_config('calendar-settings', 'default', scope: @scope)
|
88
|
+
if json
|
89
|
+
settings = JSON.parse(json)
|
90
|
+
return settings['execEnabled']
|
91
|
+
else
|
92
|
+
# Default is execute
|
93
|
+
return true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
85
97
|
def run_command(activity)
|
86
98
|
@logger.info "#{@timeline_name} run_command > #{activity.as_json(:allow_nan => true)}"
|
87
99
|
begin
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
100
|
+
if get_exec_setting()
|
101
|
+
username = activity.data['username']
|
102
|
+
token = get_token(username)
|
103
|
+
raise "No token available for username: #{username}" unless token
|
104
|
+
cmd_no_hazardous_check(activity.data['command'], scope: @scope, token: token)
|
105
|
+
activity.commit(status: 'completed', fulfillment: true)
|
106
|
+
else
|
107
|
+
activity.commit(status: 'disabled', message: 'Execution is disabled')
|
108
|
+
@logger.warn "#{@timeline_name} run_command disabled > #{activity.as_json(:allow_nan => true)}"
|
109
|
+
end
|
93
110
|
rescue StandardError => e
|
94
111
|
activity.commit(status: 'failed', message: e.message)
|
95
|
-
@logger.error "#{@timeline_name}
|
112
|
+
@logger.error "#{@timeline_name} run_command failed > #{activity.as_json(:allow_nan => true)}, #{e.formatted}"
|
96
113
|
end
|
97
114
|
end
|
98
115
|
|
99
116
|
def run_script(activity)
|
100
117
|
@logger.info "#{@timeline_name} run_script > #{activity.as_json(:allow_nan => true)}"
|
101
118
|
begin
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
119
|
+
if get_exec_setting()
|
120
|
+
username = activity.data['username']
|
121
|
+
token = get_token(username)
|
122
|
+
raise "No token available for username: #{username}" unless token
|
123
|
+
request = Net::HTTP::Post.new(
|
124
|
+
"/script-api/scripts/#{activity.data['script']}/run?scope=#{@scope}",
|
125
|
+
'Content-Type' => 'application/json',
|
126
|
+
'Authorization' => token
|
127
|
+
)
|
128
|
+
request.body = JSON.generate({
|
129
|
+
'scope' => @scope,
|
130
|
+
'environment' => activity.data['environment'],
|
131
|
+
'timeline' => @timeline_name,
|
132
|
+
'id' => activity.start
|
133
|
+
})
|
134
|
+
hostname = ENV['OPENC3_SCRIPT_HOSTNAME'] || 'openc3-cosmos-script-runner-api'
|
135
|
+
response = Net::HTTP.new(hostname, 2902).request(request)
|
136
|
+
raise "failed to call #{hostname}, for script: #{activity.data['script']}, response code: #{response.code}" if response.code != '200'
|
137
|
+
|
138
|
+
activity.commit(status: 'completed', message: "#{activity.data['script']} => #{response.body}", fulfillment: true)
|
139
|
+
else
|
140
|
+
activity.commit(status: 'disabled', message: 'Execution is disabled')
|
141
|
+
@logger.warn "#{@timeline_name} run_script disabled > #{activity.as_json(:allow_nan => true)}"
|
142
|
+
end
|
121
143
|
rescue StandardError => e
|
122
144
|
activity.commit(status: 'failed', message: e.message)
|
123
|
-
@logger.error "#{@timeline_name} run_script failed > #{activity.as_json(:allow_nan => true)
|
145
|
+
@logger.error "#{@timeline_name} run_script failed > #{activity.as_json(:allow_nan => true)}, #{e.message}"
|
124
146
|
end
|
125
147
|
end
|
126
148
|
|
@@ -212,7 +234,7 @@ module OpenC3
|
|
212
234
|
def run
|
213
235
|
@logger.info "#{@timeline_name} timeline manager running"
|
214
236
|
loop do
|
215
|
-
start = Time.now.
|
237
|
+
start = Time.now.to_f
|
216
238
|
@schedule.activities.each do |activity|
|
217
239
|
start_difference = activity.start - start
|
218
240
|
if start_difference <= 0 && @schedule.not_queued?(activity.start)
|
@@ -230,19 +252,19 @@ module OpenC3
|
|
230
252
|
sleep(1)
|
231
253
|
break if @cancel_thread
|
232
254
|
end
|
233
|
-
@logger.info "#{@timeline_name}
|
255
|
+
@logger.info "#{@timeline_name} timeline manager exiting"
|
234
256
|
end
|
235
257
|
|
236
258
|
# Add task to remove events older than 7 days
|
237
259
|
def add_expire_activity
|
238
|
-
now = Time.now.
|
239
|
-
@expire = now +
|
260
|
+
now = Time.now.to_f
|
261
|
+
@expire = now + 3540 # Needs to be less than 3600 which is the hour we store in memory
|
240
262
|
activity = ActivityModel.new(
|
241
263
|
name: @timeline_name,
|
242
264
|
scope: @scope,
|
243
265
|
start: 0,
|
244
|
-
stop: (now - 86_400 * 7),
|
245
|
-
kind: '
|
266
|
+
stop: (now - (86_400 * 7)),
|
267
|
+
kind: 'expire',
|
246
268
|
data: {}
|
247
269
|
)
|
248
270
|
@queue << activity
|
@@ -283,13 +305,13 @@ module OpenC3
|
|
283
305
|
super(name)
|
284
306
|
@timeline_name = name.split('__')[2]
|
285
307
|
@schedule = Schedule.new(@timeline_name)
|
286
|
-
@manager = TimelineManager.new(name: @timeline_name, logger: @logger, scope: scope, schedule: @schedule)
|
308
|
+
@manager = TimelineManager.new(name: @timeline_name, logger: @logger, scope: @scope, schedule: @schedule)
|
287
309
|
@manager_thread = nil
|
288
310
|
@read_topic = true
|
289
311
|
end
|
290
312
|
|
291
313
|
def run
|
292
|
-
@logger.info "#{@name}
|
314
|
+
@logger.info "#{@name} timeline running"
|
293
315
|
@manager_thread = Thread.new { @manager.run }
|
294
316
|
loop do
|
295
317
|
current_activities = ActivityModel.activities(name: @timeline_name, scope: @scope)
|
@@ -299,19 +321,19 @@ module OpenC3
|
|
299
321
|
block_for_updates()
|
300
322
|
break if @cancel_thread
|
301
323
|
end
|
302
|
-
@logger.info "#{@name}
|
324
|
+
@logger.info "#{@name} timeline exiting"
|
303
325
|
end
|
304
326
|
|
305
327
|
def topic_lookup_functions
|
306
328
|
{
|
307
329
|
'timeline' => {
|
308
|
-
'created' => :
|
330
|
+
'created' => :timeline_noop,
|
309
331
|
'refresh' => :schedule_refresh,
|
310
|
-
'updated' => :
|
311
|
-
'deleted' => :
|
332
|
+
'updated' => :timeline_noop,
|
333
|
+
'deleted' => :timeline_noop
|
312
334
|
},
|
313
335
|
'activity' => {
|
314
|
-
'event' => :
|
336
|
+
'event' => :timeline_noop,
|
315
337
|
'created' => :create_activity_from_event,
|
316
338
|
'updated' => :schedule_refresh,
|
317
339
|
'deleted' => :remove_activity_from_event
|
@@ -335,7 +357,7 @@ module OpenC3
|
|
335
357
|
end
|
336
358
|
end
|
337
359
|
|
338
|
-
def
|
360
|
+
def timeline_noop(data)
|
339
361
|
@logger.debug "#{@name} timeline web socket event: #{data}"
|
340
362
|
end
|
341
363
|
|
@@ -347,8 +369,8 @@ module OpenC3
|
|
347
369
|
# Add the activity to the schedule. We don't need to hold the job in memory
|
348
370
|
# if it is longer than an hour away. A refresh task will update that.
|
349
371
|
def create_activity_from_event(data)
|
350
|
-
diff = data['start'] - Time.now.
|
351
|
-
return
|
372
|
+
diff = data['start'] - Time.now.to_f
|
373
|
+
return if diff < 0 or diff > 3600
|
352
374
|
|
353
375
|
activity = ActivityModel.from_json(data, name: @timeline_name, scope: @scope)
|
354
376
|
@schedule.add_activity(activity)
|
@@ -357,17 +379,20 @@ module OpenC3
|
|
357
379
|
# Remove the activity from the schedule. We don't need to remove the activity
|
358
380
|
# if it is longer than an hour away. It will be removed from the data.
|
359
381
|
def remove_activity_from_event(data)
|
360
|
-
diff = data['start'] - Time.now.
|
361
|
-
return
|
382
|
+
diff = data['start'] - Time.now.to_f
|
383
|
+
return if diff < 0 or diff > 3600
|
362
384
|
|
363
385
|
activity = ActivityModel.from_json(data, name: @timeline_name, scope: @scope)
|
364
386
|
@schedule.remove_activity(activity)
|
365
387
|
end
|
366
388
|
|
367
389
|
def shutdown
|
368
|
-
@read_topic = false
|
369
390
|
@manager.shutdown
|
370
|
-
super
|
391
|
+
# super also sets @cancel_thread = true but we want to set it first
|
392
|
+
# so when we set @read_topic = false the run loop stops
|
393
|
+
@cancel_thread = true
|
394
|
+
@read_topic = false
|
395
|
+
super()
|
371
396
|
end
|
372
397
|
end
|
373
398
|
end
|
@@ -28,16 +28,14 @@ require 'securerandom'
|
|
28
28
|
|
29
29
|
module OpenC3
|
30
30
|
class ActivityError < StandardError; end
|
31
|
-
|
32
31
|
class ActivityInputError < ActivityError; end
|
33
|
-
|
34
32
|
class ActivityOverlapError < ActivityError; end
|
35
33
|
|
36
34
|
class ActivityModel < Model
|
37
35
|
MAX_DURATION = Time::SEC_PER_DAY
|
38
36
|
PRIMARY_KEY = '__openc3_timelines'.freeze # MUST be equal to `TimelineModel::PRIMARY_KEY` minus the leading __
|
39
37
|
# See run_activity(activity) in openc3/lib/openc3/microservices/timeline_microservice.rb
|
40
|
-
VALID_KINDS = %w(
|
38
|
+
VALID_KINDS = %w(command script reserve expire)
|
41
39
|
|
42
40
|
# Called via the microservice this gets the previous 00:00:15 to 01:01:00. This should allow
|
43
41
|
# for a small buffer around the timeline to make sure the schedule doesn't get stale.
|
@@ -45,7 +43,7 @@ module OpenC3
|
|
45
43
|
# with 15 slots to make sure we don't miss a planned task.
|
46
44
|
# @return [Array|nil] Array of the next hour in the sorted set
|
47
45
|
def self.activities(name:, scope:)
|
48
|
-
now = Time.now.
|
46
|
+
now = Time.now.to_f
|
49
47
|
start_score = now - 15
|
50
48
|
stop_score = (now + 3660)
|
51
49
|
array = Store.zrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", start_score, stop_score)
|
@@ -158,7 +156,7 @@ module OpenC3
|
|
158
156
|
@updated_at = updated_at
|
159
157
|
end
|
160
158
|
|
161
|
-
# validate_time searches from the current activity @stop
|
159
|
+
# validate_time searches from the current activity @stop (exclusive because we allow overlap of stop with start)
|
162
160
|
# back through @start - MAX_DURATION. The method is trying to validate that this new activity does not
|
163
161
|
# overlap with anything else. The reason we search back past @start through MAX_DURATION is because we
|
164
162
|
# need to return all the activities that may start before us and verify that we don't overlap them.
|
@@ -170,7 +168,8 @@ module OpenC3
|
|
170
168
|
#
|
171
169
|
# @param [Integer] ignore_score - should be nil unless you want to ignore a time when doing an update
|
172
170
|
def validate_time(ignore_score = nil)
|
173
|
-
|
171
|
+
# Adding a '(' makes the max value exclusive
|
172
|
+
array = Store.zrevrangebyscore(@primary_key, "(#{@stop}", @start - MAX_DURATION)
|
174
173
|
array.each do |value|
|
175
174
|
activity = JSON.parse(value, :allow_nan => true, :create_additions => true)
|
176
175
|
if ignore_score == activity['start']
|
@@ -197,15 +196,15 @@ module OpenC3
|
|
197
196
|
rescue Date::Error
|
198
197
|
raise ActivityInputError.new "start and stop must be seconds: #{start}, #{stop}"
|
199
198
|
end
|
200
|
-
|
199
|
+
now_f = Time.now.to_f
|
201
200
|
begin
|
202
201
|
duration = stop - start
|
203
202
|
rescue NoMethodError
|
204
203
|
raise ActivityInputError.new "start and stop must be seconds: #{start}, #{stop}"
|
205
204
|
end
|
206
|
-
if
|
207
|
-
raise ActivityInputError.new "activity must be in the future, current_time: #{
|
208
|
-
elsif duration >= MAX_DURATION
|
205
|
+
if now_f >= start and kind != 'expire'
|
206
|
+
raise ActivityInputError.new "activity must be in the future, current_time: #{now_f} vs #{start}"
|
207
|
+
elsif duration >= MAX_DURATION and kind != 'expire'
|
209
208
|
raise ActivityInputError.new "activity can not be longer than #{MAX_DURATION} seconds"
|
210
209
|
elsif duration <= 0
|
211
210
|
raise ActivityInputError.new "start: #{start} must be before stop: #{stop}"
|
@@ -220,11 +219,12 @@ module OpenC3
|
|
220
219
|
|
221
220
|
# Set the values of the instance, @start, @kind, @data, @events...
|
222
221
|
def set_input(start:, stop:, kind: nil, data: nil, events: nil, fulfillment: nil, recurring: nil)
|
222
|
+
kind = kind.to_s.downcase
|
223
223
|
validate_input(start: start, stop: stop, kind: kind, data: data)
|
224
224
|
@start = start
|
225
225
|
@stop = stop
|
226
226
|
@fulfillment = fulfillment.nil? ? false : fulfillment
|
227
|
-
@kind = kind
|
227
|
+
@kind = kind
|
228
228
|
@data = data.nil? ? @data : data
|
229
229
|
@events = events.nil? ? Array.new : events
|
230
230
|
@recurring = recurring.nil? ? @recurring : recurring
|
@@ -232,7 +232,7 @@ module OpenC3
|
|
232
232
|
|
233
233
|
# Update the Redis hash at primary_key and set the score equal to the start Epoch time
|
234
234
|
# the member is set to the JSON generated via calling as_json
|
235
|
-
def create
|
235
|
+
def create(overlap: true)
|
236
236
|
if @recurring['end'] and @recurring['frequency'] and @recurring['span']
|
237
237
|
# First validate the initial recurring activity ... all others are just offsets
|
238
238
|
validate_input(start: @start, stop: @stop, kind: @kind, data: @data)
|
@@ -284,11 +284,13 @@ module OpenC3
|
|
284
284
|
notify(kind: 'created')
|
285
285
|
else
|
286
286
|
validate_input(start: @start, stop: @stop, kind: @kind, data: @data)
|
287
|
-
|
288
|
-
|
289
|
-
|
287
|
+
if !overlap
|
288
|
+
# If we don't allow overlap we need to validate the time
|
289
|
+
collision = validate_time()
|
290
|
+
unless collision.nil?
|
291
|
+
raise ActivityOverlapError.new "activity overlaps existing at #{collision}"
|
292
|
+
end
|
290
293
|
end
|
291
|
-
|
292
294
|
@updated_at = Time.now.to_nsec_from_epoch
|
293
295
|
add_event(status: 'created')
|
294
296
|
Store.zadd(@primary_key, @start, JSON.generate(self.as_json(:allow_nan => true)))
|
@@ -299,7 +301,7 @@ module OpenC3
|
|
299
301
|
# Update the Redis hash at primary_key and remove the current activity at the current score
|
300
302
|
# and update the score to the new score equal to the start Epoch time this uses a multi
|
301
303
|
# to execute both the remove and create.
|
302
|
-
def update(start:, stop:, kind:, data:)
|
304
|
+
def update(start:, stop:, kind:, data:, overlap: true)
|
303
305
|
array = Store.zrangebyscore(@primary_key, @start, @start)
|
304
306
|
if array.length == 0
|
305
307
|
raise ActivityError.new "failed to find activity at: #{@start}"
|
@@ -308,10 +310,12 @@ module OpenC3
|
|
308
310
|
old_start = @start
|
309
311
|
set_input(start: start, stop: stop, kind: kind, data: data, events: @events)
|
310
312
|
@updated_at = Time.now.to_nsec_from_epoch
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
313
|
+
if !overlap
|
314
|
+
# If we don't allow overlap we need to validate the time
|
315
|
+
collision = validate_time(old_start)
|
316
|
+
unless collision.nil?
|
317
|
+
raise ActivityOverlapError.new "failed to update #{old_start}, no activities can overlap, collision: #{collision}"
|
318
|
+
end
|
315
319
|
end
|
316
320
|
|
317
321
|
add_event(status: 'updated')
|