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.

Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +1 -1
  3. data/data/config/_interfaces.yaml +4 -4
  4. data/data/config/command_modifiers.yaml +4 -0
  5. data/data/config/interface_modifiers.yaml +18 -8
  6. data/data/config/item_modifiers.yaml +34 -26
  7. data/data/config/microservice.yaml +4 -1
  8. data/data/config/param_item_modifiers.yaml +16 -0
  9. data/data/config/parameter_modifiers.yaml +29 -12
  10. data/data/config/plugins.yaml +3 -3
  11. data/data/config/screen.yaml +7 -7
  12. data/data/config/telemetry_modifiers.yaml +9 -4
  13. data/data/config/widgets.yaml +41 -14
  14. data/ext/openc3/ext/packet/packet.c +6 -0
  15. data/lib/openc3/accessors/accessor.rb +1 -0
  16. data/lib/openc3/accessors/binary_accessor.rb +170 -11
  17. data/lib/openc3/api/cmd_api.rb +39 -35
  18. data/lib/openc3/api/config_api.rb +10 -10
  19. data/lib/openc3/api/interface_api.rb +28 -21
  20. data/lib/openc3/api/limits_api.rb +29 -29
  21. data/lib/openc3/api/metrics_api.rb +3 -3
  22. data/lib/openc3/api/offline_access_api.rb +5 -5
  23. data/lib/openc3/api/router_api.rb +25 -19
  24. data/lib/openc3/api/settings_api.rb +10 -10
  25. data/lib/openc3/api/stash_api.rb +10 -10
  26. data/lib/openc3/api/target_api.rb +10 -10
  27. data/lib/openc3/api/tlm_api.rb +44 -44
  28. data/lib/openc3/conversions/bit_reverse_conversion.rb +60 -0
  29. data/lib/openc3/conversions/ip_read_conversion.rb +59 -0
  30. data/lib/openc3/conversions/ip_write_conversion.rb +61 -0
  31. data/lib/openc3/conversions/object_read_conversion.rb +88 -0
  32. data/lib/openc3/conversions/object_write_conversion.rb +38 -0
  33. data/lib/openc3/conversions.rb +6 -1
  34. data/lib/openc3/io/json_drb.rb +19 -21
  35. data/lib/openc3/io/json_rpc.rb +14 -13
  36. data/lib/openc3/microservices/microservice.rb +11 -11
  37. data/lib/openc3/microservices/scope_cleanup_microservice.rb +1 -1
  38. data/lib/openc3/microservices/timeline_microservice.rb +76 -51
  39. data/lib/openc3/models/activity_model.rb +25 -21
  40. data/lib/openc3/models/scope_model.rb +44 -13
  41. data/lib/openc3/models/sorted_model.rb +1 -1
  42. data/lib/openc3/models/target_model.rb +4 -1
  43. data/lib/openc3/operators/microservice_operator.rb +2 -2
  44. data/lib/openc3/operators/operator.rb +9 -9
  45. data/lib/openc3/packets/packet.rb +18 -1
  46. data/lib/openc3/packets/packet_config.rb +37 -16
  47. data/lib/openc3/packets/packet_item.rb +5 -0
  48. data/lib/openc3/packets/structure.rb +67 -3
  49. data/lib/openc3/packets/structure_item.rb +49 -12
  50. data/lib/openc3/script/calendar.rb +2 -2
  51. data/lib/openc3/script/extract.rb +5 -3
  52. data/lib/openc3/script/web_socket_api.rb +11 -0
  53. data/lib/openc3/topics/decom_interface_topic.rb +2 -1
  54. data/lib/openc3/topics/system_events_topic.rb +40 -0
  55. data/lib/openc3/utilities/authentication.rb +2 -1
  56. data/lib/openc3/utilities/authorization.rb +2 -2
  57. data/lib/openc3/version.rb +5 -5
  58. data/templates/tool_angular/package.json +5 -5
  59. data/templates/tool_react/package.json +8 -8
  60. data/templates/tool_svelte/package.json +10 -10
  61. data/templates/tool_vue/package.json +10 -10
  62. data/templates/widget/package.json +10 -10
  63. data/templates/widget/src/Widget.vue +0 -1
  64. metadata +22 -2
@@ -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 2022, OpenC3, Inc.
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 => error
186
+ rescue => e
189
187
  @server = nil
190
- Logger.error "JsonDRb http server could not be started or unexpectedly died.\n#{error.formatted}"
191
- OpenC3.handle_fatal_exception(error)
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 => error
273
+ rescue Exception => e
276
274
  # Filter out the framework stack trace (rails, rack, puma etc)
277
- lines = error.formatted.split("\n")
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 === error
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", error), request.id
283
+ JsonRpcError.new(error_code, "Method not found", e), request.id
286
284
  )
287
- elsif ArgumentError === error
285
+ elsif ArgumentError === e
288
286
  error_code = JsonRpcError::ErrorCode::INVALID_PARAMS
289
287
  response = JsonRpcErrorResponse.new(
290
- JsonRpcError.new(error_code, "Invalid params", error), request.id
288
+ JsonRpcError.new(error_code, "Invalid params", e), request.id
291
289
  )
292
- elsif AuthError === error
290
+ elsif AuthError === e
293
291
  error_code = JsonRpcError::ErrorCode::AUTH_ERROR
294
292
  response = JsonRpcErrorResponse.new(
295
- JsonRpcError.new(error_code, error.message, error), request.id
293
+ JsonRpcError.new(error_code, e.message, e), request.id
296
294
  )
297
- elsif ForbiddenError === error
295
+ elsif ForbiddenError === e
298
296
  error_code = JsonRpcError::ErrorCode::FORBIDDEN_ERROR
299
297
  response = JsonRpcErrorResponse.new(
300
- JsonRpcError.new(error_code, error.message, error), request.id
298
+ JsonRpcError.new(error_code, e.message, e), request.id
301
299
  )
302
- elsif HazardousError === error
300
+ elsif HazardousError === e
303
301
  error_code = JsonRpcError::ErrorCode::HAZARDOUS_ERROR
304
302
  response = JsonRpcErrorResponse.new(
305
- JsonRpcError.new(error_code, error.message, error), request.id
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, error.message, error), request.id
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 => error
323
+ rescue => e
326
324
  error_code = JsonRpcError::ErrorCode::INVALID_REQUEST
327
- response = JsonRpcErrorResponse.new(JsonRpcError.new(error_code, "Invalid Request", error), nil)
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
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 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(options = nil) #:nodoc:
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(options = nil) self end #:nodoc:
46
+ def as_json(_options = nil) self end #:nodoc:
47
47
  end
48
48
 
49
49
  class FalseClass
50
- def as_json(options = nil) self end #:nodoc:
50
+ def as_json(_options = nil) self end #:nodoc:
51
51
  end
52
52
 
53
53
  class NilClass
54
- def as_json(options = nil) self end #:nodoc:
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(options = nil)
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(options = nil) to_s end #:nodoc:
75
+ def as_json(_options = nil) to_s end #:nodoc:
76
76
  end
77
77
 
78
78
  class Numeric
79
- def as_json(options = nil) self end #:nodoc:
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(options = nil)
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(options = nil) to_s end #:nodoc:
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(options = nil) #:nodoc:
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(options = nil) #:nodoc:
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(options = nil) #:nodoc:
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 2023, OpenC3, Inc.
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 => err
58
- if SystemExit === err or SignalException === err
57
+ rescue Exception => e
58
+ if SystemExit === e or SignalException === e
59
59
  microservice.state = 'KILLED'
60
60
  else
61
- microservice.error = err
61
+ microservice.error = e
62
62
  microservice.state = 'DIED_ERROR'
63
- Logger.fatal("Microservice #{name} dying from exception\n#{err.formatted}")
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.info("Ruby microservice #{@name} file #{ruby_filename} passed syntax check\n", scope: @scope)
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 => err
192
- @logger.error "#{@name} status thread died: #{err.formatted}"
193
- raise err
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.info("Shutting down microservice complete: #{@name}")
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, redis)
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, scope: @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} timeine worker exiting"
70
+ @logger.info "#{@timeline_name} timeline worker exiting"
70
71
  end
71
72
 
72
73
  def run_activity(activity)
73
- case activity.kind.upcase
74
- when 'COMMAND'
74
+ case activity.kind.downcase
75
+ when 'command'
75
76
  run_command(activity)
76
- when 'SCRIPT'
77
+ when 'script'
77
78
  run_script(activity)
78
- when 'EXPIRE'
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
- username = activity.data['username']
89
- token = get_token(username)
90
- raise "No token available for username: #{username}" unless token
91
- cmd_no_hazardous_check(activity.data['command'], scope: @scope, token: token)
92
- activity.commit(status: 'completed', fulfillment: true)
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} run_cmd failed > #{activity.as_json(:allow_nan => true)}, #{e.formatted}"
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
- username = activity.data['username']
103
- token = get_token(username)
104
- raise "No token available for username: #{username}" unless token
105
- request = Net::HTTP::Post.new(
106
- "/script-api/scripts/#{activity.data['script']}/run?scope=#{@scope}",
107
- 'Content-Type' => 'application/json',
108
- 'Authorization' => token
109
- )
110
- request.body = JSON.generate({
111
- 'scope' => @scope,
112
- 'environment' => activity.data['environment'],
113
- 'timeline' => @timeline_name,
114
- 'id' => activity.start
115
- })
116
- hostname = ENV['OPENC3_SCRIPT_HOSTNAME'] || 'openc3-cosmos-script-runner-api'
117
- response = Net::HTTP.new(hostname, 2902).request(request)
118
- raise "failed to call #{hostname}, for script: #{activity.data['script']}, response code: #{response.code}" if response.code != '200'
119
-
120
- activity.commit(status: 'completed', message: "#{activity.data['script']} => #{response.body}", fulfillment: true)
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).to_s}, #{e.message}"
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.to_i
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} timeine manager exiting"
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.to_i
239
- @expire = now + 3_000
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: 'EXPIRE',
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} timeine running"
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} timeine exitting"
324
+ @logger.info "#{@name} timeline exiting"
303
325
  end
304
326
 
305
327
  def topic_lookup_functions
306
328
  {
307
329
  'timeline' => {
308
- 'created' => :timeline_nop,
330
+ 'created' => :timeline_noop,
309
331
  'refresh' => :schedule_refresh,
310
- 'updated' => :timeline_nop,
311
- 'deleted' => :timeline_nop
332
+ 'updated' => :timeline_noop,
333
+ 'deleted' => :timeline_noop
312
334
  },
313
335
  'activity' => {
314
- 'event' => :timeline_nop,
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 timeline_nop(data)
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.to_i
351
- return unless (2..3600).include? diff
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.to_i
361
- return unless (2..3600).include? diff
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(COMMAND SCRIPT RESERVE EXPIRE)
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.to_i
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 - 1 (because we allow overlap of stop with start)
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
- array = Store.zrevrangebyscore(@primary_key, @stop - 1, @start - MAX_DURATION)
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
- now_i = Time.now.to_i
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 now_i >= start and kind != 'EXPIRE'
207
- raise ActivityInputError.new "activity must be in the future, current_time: #{now_i} vs #{start}"
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.nil? ? @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
- collision = validate_time()
288
- unless collision.nil?
289
- raise ActivityOverlapError.new "activity overlaps existing at #{collision}"
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
- # copy of create
312
- collision = validate_time(old_start)
313
- unless collision.nil?
314
- raise ActivityOverlapError.new "failed to update #{old_start}, no activities can overlap, collision: #{collision}"
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')