cosmos 5.0.4 → 5.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -32,6 +32,7 @@ require 'cosmos/models/tool_model'
32
32
  require 'cosmos/models/widget_model'
33
33
  require 'cosmos/models/microservice_model'
34
34
  require 'tmpdir'
35
+ require 'tempfile'
35
36
 
36
37
  module Cosmos
37
38
  # Represents a COSMOS plugin that can consist of targets, interfaces, routers
@@ -41,6 +42,9 @@ module Cosmos
41
42
  PRIMARY_KEY = 'cosmos_plugins'
42
43
 
43
44
  attr_accessor :variables
45
+ attr_accessor :plugin_txt_lines
46
+ attr_accessor :needs_dependencies
47
+
44
48
 
45
49
  # NOTE: The following three class methods are used by the ModelController
46
50
  # and are reimplemented to enable various Model class methods to work
@@ -58,71 +62,100 @@ module Cosmos
58
62
 
59
63
  # Called by the PluginsController to parse the plugin variables
60
64
  # Doesn't actaully create the plugin during the phase
61
- def self.install_phase1(gem_file_path, existing_variables = nil, scope:)
62
- gem_filename = File.basename(gem_file_path)
63
-
64
- # Load gem to internal gem server
65
- Cosmos::GemModel.put(gem_file_path, gem_install: false, scope: scope)
66
-
67
- # Extract gem and process plugin.txt to determine what VARIABLEs need to be filled in
68
- pkg = Gem::Package.new(gem_file_path)
65
+ def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil, process_existing: false, scope:, validate_only: false)
66
+ gem_name = File.basename(gem_file_path).split("__")[0]
69
67
 
70
68
  temp_dir = Dir.mktmpdir
69
+ tf = nil
71
70
  begin
72
- pkg.extract_files(temp_dir)
73
- plugin_txt_path = File.join(temp_dir, 'plugin.txt')
74
- if File.exist?(plugin_txt_path)
75
- parser = Cosmos::ConfigParser.new("http://cosmosc2.com")
76
-
77
- # Phase 1 Gather Variables
78
- variables = {}
79
- parser.parse_file(plugin_txt_path,
80
- false,
81
- true,
82
- false) do |keyword, params|
83
- case keyword
84
- when 'VARIABLE'
85
- usage = "#{keyword} <Variable Name> <Default Value>"
86
- parser.verify_num_parameters(2, nil, usage)
87
- variable_name = params[0]
88
- value = params[1..-1].join(" ")
89
- variables[variable_name] = value
90
- if existing_variables && existing_variables.key?(variable_name)
91
- variables[variable_name] = existing_variables[variable_name]
92
- end
93
- # Ignore everything else during phase 1
94
- end
71
+ if File.exists?(gem_file_path)
72
+ # Load gem to internal gem server
73
+ Cosmos::GemModel.put(gem_file_path, gem_install: false, scope: scope) unless validate_only
74
+ else
75
+ gem_file_path = Cosmos::GemModel.get(temp_dir, gem_name)
76
+ end
77
+
78
+ # Extract gem and process plugin.txt to determine what VARIABLEs need to be filled in
79
+ pkg = Gem::Package.new(gem_file_path)
80
+
81
+ if existing_plugin_txt_lines and process_existing
82
+ # This is only used in cosmos load when everything is known
83
+ plugin_txt_lines = existing_plugin_txt_lines
84
+ file_data = existing_plugin_txt_lines.join("\n")
85
+ tf = Tempfile.new("plugin.txt")
86
+ tf.write(file_data)
87
+ tf.close
88
+ plugin_txt_path = tf.path
89
+ else
90
+ # Otherwise we always process the new and return both
91
+ pkg.extract_files(temp_dir)
92
+ plugin_txt_path = File.join(temp_dir, 'plugin.txt')
93
+ plugin_text = File.read(plugin_txt_path)
94
+ plugin_txt_lines = []
95
+ plugin_text.each_line do |line|
96
+ plugin_txt_lines << line.chomp
95
97
  end
98
+ end
99
+
100
+ parser = Cosmos::ConfigParser.new("http://cosmosc2.com")
96
101
 
97
- model = PluginModel.new(name: gem_filename, variables: variables, scope: scope)
98
- return model.as_json
102
+ # Phase 1 Gather Variables
103
+ variables = {}
104
+ parser.parse_file(plugin_txt_path,
105
+ false,
106
+ true,
107
+ false) do |keyword, params|
108
+ case keyword
109
+ when 'VARIABLE'
110
+ usage = "#{keyword} <Variable Name> <Default Value>"
111
+ parser.verify_num_parameters(2, nil, usage)
112
+ variable_name = params[0]
113
+ value = params[1..-1].join(" ")
114
+ variables[variable_name] = value
115
+ if existing_variables && existing_variables.key?(variable_name)
116
+ variables[variable_name] = existing_variables[variable_name]
117
+ end
118
+ # Ignore everything else during phase 1
119
+ end
99
120
  end
121
+
122
+ model = PluginModel.new(name: gem_name, variables: variables, plugin_txt_lines: plugin_txt_lines, scope: scope)
123
+ result = model.as_json
124
+ result['existing_plugin_txt_lines'] = existing_plugin_txt_lines if existing_plugin_txt_lines and not process_existing and existing_plugin_txt_lines != result['plugin_txt_lines']
125
+ return result
100
126
  ensure
101
127
  FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
128
+ tf.unlink if tf
102
129
  end
103
130
  end
104
131
 
105
132
  # Called by the PluginsController to create the plugin
106
133
  # Because this uses ERB it must be run in a seperate process from the API to
107
134
  # prevent corruption and single require problems in the current proces
108
- def self.install_phase2(name, variables, scope:)
135
+ def self.install_phase2(plugin_hash, scope:, validate_only: false)
109
136
  rubys3_client = Aws::S3::Client.new
110
137
 
111
138
  # Ensure config bucket exists
112
- begin
113
- rubys3_client.head_bucket(bucket: 'config')
114
- rescue Aws::S3::Errors::NotFound
115
- rubys3_client.create_bucket(bucket: 'config')
139
+ unless validate_only
140
+ begin
141
+ rubys3_client.head_bucket(bucket: 'config')
142
+ rescue Aws::S3::Errors::NotFound
143
+ rubys3_client.create_bucket(bucket: 'config')
144
+ end
116
145
  end
117
146
 
118
147
  # Register plugin to aid in uninstall if install fails
119
- plugin_model = PluginModel.new(name: name, variables: variables, scope: scope)
120
- plugin_model.create
148
+ plugin_hash.delete("existing_plugin_txt_lines")
149
+ plugin_model = PluginModel.new(**(plugin_hash.transform_keys(&:to_sym)), scope: scope)
150
+ plugin_model.create unless validate_only
121
151
 
122
152
  temp_dir = Dir.mktmpdir
123
153
  begin
154
+ tf = nil
155
+
124
156
  # Get the gem from local gem server
125
- gem_file_path = Cosmos::GemModel.get(temp_dir, name)
157
+ gem_name = plugin_hash['name'].split("__")[0]
158
+ gem_file_path = Cosmos::GemModel.get(temp_dir, gem_name)
126
159
 
127
160
  # Actually install the gem now (slow)
128
161
  Cosmos::GemModel.install(gem_file_path, scope: scope)
@@ -133,6 +166,11 @@ module Cosmos
133
166
  pkg = Gem::Package.new(gem_file_path)
134
167
  needs_dependencies = pkg.spec.runtime_dependencies.length > 0
135
168
  pkg.extract_files(gem_path)
169
+ needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
170
+ if needs_dependencies
171
+ plugin_model.needs_dependencies = true
172
+ plugin_model.update
173
+ end
136
174
 
137
175
  # Temporarily add all lib folders from the gem to the end of the load path
138
176
  load_dirs = []
@@ -145,7 +183,12 @@ module Cosmos
145
183
  end
146
184
 
147
185
  # Process plugin.txt file
148
- plugin_txt_path = File.join(gem_path, 'plugin.txt')
186
+ file_data = plugin_hash['plugin_txt_lines'].join("\n")
187
+ tf = Tempfile.new("plugin.txt")
188
+ tf.write(file_data)
189
+ tf.close
190
+ plugin_txt_path = tf.path
191
+ variables = plugin_hash['variables']
149
192
  if File.exist?(plugin_txt_path)
150
193
  parser = Cosmos::ConfigParser.new("http://cosmosc2.com")
151
194
 
@@ -156,8 +199,8 @@ module Cosmos
156
199
  # Ignore during phase 2
157
200
  when 'TARGET', 'INTERFACE', 'ROUTER', 'MICROSERVICE', 'TOOL', 'WIDGET'
158
201
  if current_model
159
- current_model.create
160
- current_model.deploy(gem_path, variables)
202
+ current_model.create unless validate_only
203
+ current_model.deploy(gem_path, variables, validate_only: validate_only)
161
204
  current_model = nil
162
205
  end
163
206
  current_model = Cosmos.const_get((keyword.capitalize + 'Model').intern).handle_config(parser, keyword, params, plugin: plugin_model.name, needs_dependencies: needs_dependencies, scope: scope)
@@ -170,8 +213,8 @@ module Cosmos
170
213
  end
171
214
  end
172
215
  if current_model
173
- current_model.create
174
- current_model.deploy(gem_path, variables)
216
+ current_model.create unless validate_only
217
+ current_model.deploy(gem_path, variables, validate_only: validate_only)
175
218
  current_model = nil
176
219
  end
177
220
  end
@@ -182,21 +225,28 @@ module Cosmos
182
225
  end
183
226
  rescue => err
184
227
  # Install failed - need to cleanup
185
- plugin_model.destroy
228
+ plugin_model.destroy unless validate_only
186
229
  raise err
187
230
  ensure
188
231
  FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
232
+ tf.unlink if tf
189
233
  end
234
+
235
+ return plugin_model.as_json
190
236
  end
191
237
 
192
238
  def initialize(
193
239
  name:,
194
240
  variables: {},
241
+ plugin_txt_lines: [],
242
+ needs_dependencies: false,
195
243
  updated_at: nil,
196
244
  scope:
197
245
  )
198
246
  super("#{scope}__#{PRIMARY_KEY}", name: name, updated_at: updated_at, scope: scope)
199
247
  @variables = variables
248
+ @plugin_txt_lines = plugin_txt_lines
249
+ @needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
200
250
  end
201
251
 
202
252
  def create(update: false, force: false)
@@ -208,6 +258,8 @@ module Cosmos
208
258
  {
209
259
  'name' => @name,
210
260
  'variables' => @variables,
261
+ 'plugin_txt_lines' => @plugin_txt_lines,
262
+ 'needs_dependencies' => @needs_dependencies,
211
263
  'updated_at' => @updated_at
212
264
  }
213
265
  end
@@ -220,5 +272,13 @@ module Cosmos
220
272
  end
221
273
  end
222
274
  end
275
+
276
+ # Reinstall
277
+ def restore
278
+ plugin_hash = self.as_json
279
+ plugin_hash['name'] = plugin_hash['name'].split("__")[0]
280
+ Cosmos::PluginModel.install_phase2(plugin_hash, scope: @scope)
281
+ @destroyed = false
282
+ end
223
283
  end
224
284
  end
@@ -52,6 +52,8 @@ module Cosmos
52
52
  def deploy(gem_path, variables)
53
53
  seed_database()
54
54
 
55
+ ConfigTopic.initialize_stream(@scope)
56
+
55
57
  # COSMOS Log Microservice
56
58
  microservice_name = "#{@scope}__COSMOS__LOG"
57
59
  microservice = MicroserviceModel.new(
@@ -119,6 +119,7 @@ module Cosmos
119
119
  def create(update: false)
120
120
  validate_start(update: update)
121
121
  @updated_at = Time.now.to_nsec_from_epoch
122
+ SortedModel.destroy(scope: @scope, start: update) if update
122
123
  Store.zadd(@primary_key, @start, JSON.generate(as_json()))
123
124
  if update
124
125
  notify(kind: 'updated')
@@ -129,8 +130,9 @@ module Cosmos
129
130
 
130
131
  # Update the Redis hash at primary_key
131
132
  def update(start:)
133
+ orig_start = @start
132
134
  @start = start
133
- create(update: true)
135
+ create(update: orig_start)
134
136
  end
135
137
 
136
138
  # destroy the activity from the redis database
@@ -22,6 +22,7 @@ require 'cosmos/models/model'
22
22
  require 'cosmos/models/cvt_model'
23
23
  require 'cosmos/models/microservice_model'
24
24
  require 'cosmos/topics/limits_event_topic'
25
+ require 'cosmos/topics/config_topic'
25
26
  require 'cosmos/system'
26
27
  require 'cosmos/utilities/s3'
27
28
  require 'cosmos/utilities/zip'
@@ -360,7 +361,7 @@ module Cosmos
360
361
  return nil
361
362
  end
362
363
 
363
- def deploy(gem_path, variables)
364
+ def deploy(gem_path, variables, validate_only: false)
364
365
  rubys3_client = Aws::S3::Client.new
365
366
  variables["target_name"] = @name
366
367
  start_path = "/targets/#{@folder_name}/"
@@ -389,14 +390,23 @@ module Cosmos
389
390
  FileUtils.mkdir_p(File.dirname(local_path))
390
391
  File.open(local_path, 'wb') { |file| file.write(data) }
391
392
  found = true
392
- rubys3_client.put_object(bucket: 'config', key: key, body: data)
393
+ rubys3_client.put_object(bucket: 'config', key: key, body: data) unless validate_only
393
394
  end
394
395
  raise "No target files found at #{target_path}" unless found
395
396
 
396
397
  target_folder = File.join(temp_dir, @name)
397
- build_target_archive(rubys3_client, temp_dir, target_folder)
398
- system = update_store(temp_dir)
399
- deploy_microservices(gem_path, variables, system)
398
+ # Build a System for just this target
399
+ system = System.new([@name], temp_dir)
400
+ if variables["xtce_output"]
401
+ puts "Converting target #{@name} to .xtce files in #{variables["xtce_output"]}/#{@name}"
402
+ system.packet_config.to_xtce(variables["xtce_output"])
403
+ end
404
+ unless validate_only
405
+ build_target_archive(rubys3_client, temp_dir, target_folder)
406
+ system = update_store(system)
407
+ deploy_microservices(gem_path, variables, system)
408
+ ConfigTopic.write({ kind: 'created', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
409
+ end
400
410
  ensure
401
411
  FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
402
412
  end
@@ -430,6 +440,7 @@ module Cosmos
430
440
  model = MicroserviceModel.get_model(name: "#{@scope}__#{type}__#{@name}", scope: @scope)
431
441
  model.destroy if model
432
442
  end
443
+ ConfigTopic.write({ kind: 'deleted', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
433
444
  end
434
445
 
435
446
  ##################################################
@@ -497,9 +508,7 @@ module Cosmos
497
508
  end
498
509
  end
499
510
 
500
- def update_store(temp_dir)
501
- # Build a System for just this target
502
- system = System.new([@name], temp_dir)
511
+ def update_store(system)
503
512
  target = system.targets[@name]
504
513
 
505
514
  # Add in the information from the target and update
@@ -606,7 +615,7 @@ module Cosmos
606
615
  ],
607
616
  topics: command_topic_list,
608
617
  target_names: [@name],
609
- plugin: plugin,
618
+ plugin: @plugin,
610
619
  needs_dependencies: @needs_dependencies,
611
620
  scope: @scope
612
621
  )
@@ -629,7 +638,7 @@ module Cosmos
629
638
  ],
630
639
  topics: decom_command_topic_list,
631
640
  target_names: [@name],
632
- plugin: plugin,
641
+ plugin: @plugin,
633
642
  needs_dependencies: @needs_dependencies,
634
643
  scope: @scope
635
644
  )
@@ -654,7 +663,7 @@ module Cosmos
654
663
  ],
655
664
  topics: packet_topic_list,
656
665
  target_names: [@name],
657
- plugin: plugin,
666
+ plugin: @plugin,
658
667
  needs_dependencies: @needs_dependencies,
659
668
  scope: @scope
660
669
  )
@@ -677,7 +686,7 @@ module Cosmos
677
686
  ],
678
687
  topics: decom_topic_list,
679
688
  target_names: [@name],
680
- plugin: plugin,
689
+ plugin: @plugin,
681
690
  needs_dependencies: @needs_dependencies,
682
691
  scope: @scope
683
692
  )
@@ -694,7 +703,7 @@ module Cosmos
694
703
  work_dir: '/cosmos/lib/cosmos/microservices',
695
704
  topics: packet_topic_list,
696
705
  target_names: [@name],
697
- plugin: plugin,
706
+ plugin: @plugin,
698
707
  needs_dependencies: @needs_dependencies,
699
708
  scope: @scope
700
709
  )
@@ -710,7 +719,7 @@ module Cosmos
710
719
  cmd: ["ruby", "reducer_microservice.rb", microservice_name],
711
720
  work_dir: '/cosmos/lib/cosmos/microservices',
712
721
  topics: decom_topic_list,
713
- plugin: plugin,
722
+ plugin: @plugin,
714
723
  needs_dependencies: @needs_dependencies,
715
724
  scope: @scope
716
725
  )
@@ -727,7 +736,7 @@ module Cosmos
727
736
  name: microservice_name,
728
737
  cmd: ["ruby", "cleanup_microservice.rb", microservice_name],
729
738
  work_dir: '/cosmos/lib/cosmos/microservices',
730
- plugin: plugin,
739
+ plugin: @plugin,
731
740
  scope: @scope
732
741
  )
733
742
  microservice.create
@@ -209,13 +209,11 @@ module Cosmos
209
209
  return nil
210
210
  end
211
211
 
212
- def deploy(gem_path, variables)
212
+ def deploy(gem_path, variables, validate_only: false)
213
213
  return unless @folder_name
214
214
 
215
- rubys3_client = Aws::S3::Client.new
216
-
217
215
  # Ensure tools bucket exists
218
- Cosmos::S3Utilities.ensure_public_bucket('tools')
216
+ Cosmos::S3Utilities.ensure_public_bucket('tools') unless validate_only
219
217
 
220
218
  variables["tool_name"] = @name
221
219
  start_path = "/tools/#{@folder_name}/"
@@ -223,16 +221,17 @@ module Cosmos
223
221
  next if filename == '.' or filename == '..' or File.directory?(filename)
224
222
 
225
223
  key = filename.split(gem_path + '/tools/')[-1]
226
-
227
224
  extension = filename.split('.')[-1]
228
225
  content_type = Rack::Mime.mime_type(".#{extension}")
229
226
 
230
- cache_control = Cosmos::S3Utilities.get_cache_control(filename)
231
-
232
227
  # Load tool files
233
228
  data = File.read(filename, mode: "rb")
234
229
  data = ERB.new(data, trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
235
- rubys3_client.put_object(bucket: 'tools', content_type: content_type, cache_control: cache_control, key: key, body: data)
230
+ unless validate_only
231
+ cache_control = Cosmos::S3Utilities.get_cache_control(filename)
232
+ Aws::S3::Client.new.put_object(bucket: 'tools', content_type: content_type, cache_control: cache_control, key: key, body: data)
233
+ ConfigTopic.write({ kind: 'created', type: 'tool', name: @folder_name, plugin: @plugin }, scope: @scope)
234
+ end
236
235
  end
237
236
  end
238
237
 
@@ -242,6 +241,7 @@ module Cosmos
242
241
  prefix = "#{@folder_name}/"
243
242
  rubys3_client.list_objects(bucket: 'tools', prefix: prefix).contents.each do |object|
244
243
  rubys3_client.delete_object(bucket: 'tools', key: object.key)
244
+ ConfigTopic.write({ kind: 'deleted', type: 'tool', name: @folder_name, plugin: @plugin }, scope: @scope)
245
245
  end
246
246
  end
247
247
  end
@@ -108,25 +108,25 @@ module Cosmos
108
108
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Widget: #{keyword} #{parameters.join(" ")}")
109
109
  end
110
110
 
111
- def deploy(gem_path, variables)
112
- rubys3_client = Aws::S3::Client.new
113
-
111
+ def deploy(gem_path, variables, validate_only: false)
114
112
  # Ensure tools bucket exists
115
- Cosmos::S3Utilities.ensure_public_bucket('tools')
113
+ Cosmos::S3Utilities.ensure_public_bucket('tools') unless validate_only
116
114
 
117
115
  filename = gem_path + "/tools/widgets/" + @full_name + '/' + @filename
118
116
 
119
- cache_control = Cosmos::S3Utilities.get_cache_control(@filename)
120
-
121
117
  # Load widget file
122
118
  data = File.read(filename, mode: "rb")
123
119
  Cosmos.set_working_dir(File.dirname(filename)) do
124
120
  data = ERB.new(data, trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
125
121
  end
126
- # TODO: support widgets that aren't just a single js file (and its associated map file)
127
- rubys3_client.put_object(bucket: 'tools', content_type: 'application/javascript', cache_control: cache_control, key: @s3_key, body: data)
128
- data = File.read(filename + '.map', mode: "rb")
129
- rubys3_client.put_object(bucket: 'tools', content_type: 'application/json', cache_control: cache_control, key: @s3_key + '.map', body: data)
122
+ unless validate_only
123
+ cache_control = Cosmos::S3Utilities.get_cache_control(@filename)
124
+ # TODO: support widgets that aren't just a single js file (and its associated map file)
125
+ rubys3_client = Aws::S3::Client.new
126
+ rubys3_client.put_object(bucket: 'tools', content_type: 'application/javascript', cache_control: cache_control, key: @s3_key, body: data)
127
+ data = File.read(filename + '.map', mode: "rb")
128
+ rubys3_client.put_object(bucket: 'tools', content_type: 'application/json', cache_control: cache_control, key: @s3_key + '.map', body: data)
129
+ end
130
130
  end
131
131
 
132
132
  def undeploy
@@ -98,7 +98,7 @@ module Cosmos
98
98
  # @param buffer [String] String buffer to hold the packet data
99
99
  # @param item_class [Class] Class used to instantiate items (Must be a
100
100
  # subclass of PacketItem)
101
- def initialize(target_name, packet_name, default_endianness = :BIG_ENDIAN, description = nil, buffer = '', item_class = PacketItem)
101
+ def initialize(target_name, packet_name, default_endianness = :BIG_ENDIAN, description = nil, buffer = nil, item_class = PacketItem)
102
102
  super(default_endianness, buffer, item_class)
103
103
  # Explictly call the defined setter methods
104
104
  self.target_name = target_name
@@ -66,7 +66,7 @@ module Cosmos
66
66
  # @param buffer [String] Buffer used to store the structure
67
67
  # @param item_class [Class] Class used to instantiate new structure items.
68
68
  # Must be StructureItem or one of its subclasses.
69
- def initialize(default_endianness = BinaryAccessor::HOST_ENDIANNESS, buffer = '', item_class = StructureItem)
69
+ def initialize(default_endianness = BinaryAccessor::HOST_ENDIANNESS, buffer = nil, item_class = StructureItem)
70
70
  if (default_endianness == :BIG_ENDIAN) || (default_endianness == :LITTLE_ENDIAN)
71
71
  @default_endianness = default_endianness
72
72
  if buffer
@@ -102,14 +102,11 @@ module Cosmos
102
102
  def read_item(item, value_type = :RAW, buffer = @buffer)
103
103
  return nil if item.data_type == :DERIVED
104
104
 
105
- if buffer
106
- if item.array_size
107
- return BinaryAccessor.read_array(item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness)
108
- else
109
- return BinaryAccessor.read(item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness)
110
- end
105
+ buffer = allocate_buffer_if_needed() unless buffer
106
+ if item.array_size
107
+ return BinaryAccessor.read_array(item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness)
111
108
  else
112
- raise "No buffer given to read_item"
109
+ return BinaryAccessor.read(item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness)
113
110
  end
114
111
  end
115
112
 
@@ -117,9 +114,8 @@ module Cosmos
117
114
  #
118
115
  # @return [Integer] Size of the buffer in bytes
119
116
  def length
120
- return @buffer.length if @buffer
121
-
122
- return 0
117
+ allocate_buffer_if_needed()
118
+ return @buffer.length
123
119
  end
124
120
 
125
121
  # Resize the buffer at least the defined length of the structure
@@ -129,12 +125,23 @@ module Cosmos
129
125
  if @buffer.length < @defined_length
130
126
  @buffer << (ZERO_STRING * (@defined_length - @buffer.length))
131
127
  end
128
+ else
129
+ allocate_buffer_if_needed()
132
130
  end
133
131
 
134
132
  return self
135
133
  end
136
134
  end
137
135
 
136
+ # Allocate a buffer if not available
137
+ def allocate_buffer_if_needed
138
+ unless @buffer
139
+ @buffer = ZERO_STRING * @defined_length
140
+ @buffer.force_encoding(ASCII_8BIT_STRING)
141
+ end
142
+ return @buffer
143
+ end
144
+
138
145
  # Indicates if any items have been defined for this structure
139
146
  # @return [TrueClass or FalseClass]
140
147
  def defined?
@@ -340,14 +347,11 @@ module Cosmos
340
347
  # parameter to check whether to perform conversions on the item.
341
348
  # @param buffer [String] The binary buffer to write the value to
342
349
  def write_item(item, value, value_type = :RAW, buffer = @buffer)
343
- if buffer
344
- if item.array_size
345
- BinaryAccessor.write_array(value, item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness, item.overflow)
346
- else
347
- BinaryAccessor.write(value, item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness, item.overflow)
348
- end
350
+ buffer = allocate_buffer_if_needed() unless buffer
351
+ if item.array_size
352
+ BinaryAccessor.write_array(value, item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness, item.overflow)
349
353
  else
350
- raise "No buffer given to write_item"
354
+ BinaryAccessor.write(value, item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness, item.overflow)
351
355
  end
352
356
  end
353
357
 
@@ -432,14 +436,11 @@ module Cosmos
432
436
  # @param copy [TrueClass/FalseClass] Whether to copy the buffer
433
437
  # @return [String] Data buffer backing the structure
434
438
  def buffer(copy = true)
435
- if @buffer
436
- if copy
437
- return @buffer.dup
438
- else
439
- return @buffer
440
- end
439
+ local_buffer = allocate_buffer_if_needed()
440
+ if copy
441
+ return local_buffer.dup
441
442
  else
442
- return nil
443
+ return local_buffer
443
444
  end
444
445
  end
445
446
 
@@ -525,15 +526,11 @@ module Cosmos
525
526
  module MethodMissing
526
527
  # Method missing provides reading/writing item values as if they were methods to the class
527
528
  def method_missing(name, value = nil)
528
- if @buffer
529
- if value
530
- # Strip off the equals sign before looking up the item
531
- return write(name.to_s[0..-2], value)
532
- else
533
- return read(name.to_s)
534
- end
529
+ if value
530
+ # Strip off the equals sign before looking up the item
531
+ return write(name.to_s[0..-2], value)
535
532
  else
536
- raise "No buffer available for method_missing"
533
+ return read(name.to_s)
537
534
  end
538
535
  end
539
536
  end
@@ -42,6 +42,9 @@ module Cosmos
42
42
  # @param start [Time] Metadata time value, if nil will be current time
43
43
  # @return The result of the method call.
44
44
  def set_metadata(metadata, color: nil, start: nil)
45
+ unless metadata.is_a?(Hash)
46
+ raise "metadata must be a Hash: #{metadata} is a #{metadata.class}"
47
+ end
45
48
  color = color.nil? ? '#003784' : color
46
49
  data = { color: color, metadata: metadata }
47
50
  data[:start] = start.iso8601 unless start.nil?
@@ -52,12 +55,20 @@ module Cosmos
52
55
 
53
56
  # Updates the metadata
54
57
  #
55
- # @param start [Integer] Metadata time value as integer seconds from epoch
56
58
  # @param metadata [Hash<Symbol, Variable>] A hash of metadata
57
59
  # @param color [String] Events color to show on Calendar tool, if nil will be blue
60
+ # @param start [Integer] Metadata time value as integer seconds from epoch
58
61
  # @return The result of the method call.
59
- def update_metadata(start, metadata, color: nil)
62
+ def update_metadata(metadata, color: nil, start: nil)
63
+ unless metadata.is_a?(Hash)
64
+ raise "metadata must be a Hash: #{metadata} is a #{metadata.class}"
65
+ end
60
66
  color = color.nil? ? '#003784' : color
67
+ if start == nil
68
+ existing = get_metadata()
69
+ start = existing['start']
70
+ metadata = existing['metadata'].merge(metadata)
71
+ end
61
72
  data = { :color => color, :metadata => metadata }
62
73
  data[:start] = Time.at(start).iso8601
63
74
  response = $api_server.request('put', "/cosmos-api/metadata/#{start}", data: data, json: true)