openc3/bin/openc3cli
#!/usr/bin/env ruby
# encoding: ascii-8bit
# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# Modified by OpenC3, Inc.
# All changes Copyright 2024, OpenC3, Inc.
# All Rights Reserved
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.
# This file will handle OpenC3 tasks such as instantiating a new project
require 'openc3'
require 'openc3/utilities/local_mode'
require 'openc3/utilities/bucket'
require 'openc3/utilities/cli_generator'
require 'openc3/models/scope_model'
require 'openc3/models/plugin_model'
require 'openc3/models/gem_model'
require 'openc3/models/migration_model'
require 'openc3/models/tool_model'
require 'openc3/packets/packet_config'
require 'openc3/bridge/bridge'
require 'ostruct'
require 'optparse'
require 'openc3/utilities/zip'
require 'fileutils'
require 'find'
require 'json'
require 'redis'
require 'erb'
require 'irb'
require 'irb/completion'
$redis_url = "redis://#{ENV['OPENC3_REDIS_HOSTNAME']}:#{ENV['OPENC3_REDIS_PORT']}"
# Build the OpenStruct and OptionParser here as constants so we can use in methods
MIGRATE_OPTIONS = OpenStruct.new
MIGRATE_OPTIONS.all = false
MIGRATE_PARSER = OptionParser.new do |opts|
opts.banner = "cli migrate PLUGIN [TGT1...] # Create a OpenC3 plugin from existing COSMOS 4 targets"
opts.on("-a", "--all", " Move all COSMOS 4 targets into a single OpenC3 plugin") do
MIGRATE_OPTIONS.all = true
end
end
ERROR_CODE = 1
CLI_SCRIPT_ACTIONS = %w(help list run spawn)
$script_interrupt_text = ''
trap('INT') do
abort("Interrupted at console; exiting.#{$script_interrupt_text}")
end
# Prints the usage text for the openc3cli executable
def print_usage
puts "Usage:"
puts " cli help # Displays this information"
puts " cli rake # Runs rake in the local directory"
puts " cli irb # Runs irb in the local directory"
puts " cli script list /PATH SCOPE # lists script names filtered by path within scope, 'DEFAULT' if not given"
puts " cli script spawn NAME SCOPE variable1=value1 variable2=value2 # Starts named script remotely"
puts " cli script run NAME SCOPE variable1=value1 variable2=value2 # Starts named script, monitoring status on console,\
by default until error or exit"
puts " PARAMETERS name-value pairs to form the script's runtime environment"
puts " OPTIONS: --wait 0 seconds to monitor status before detaching from the running script; ie --wait 100"
puts " --disconnect run the script in disconnect mode"
puts " cli validate /PATH/FILENAME.gem SCOPE variables.txt # Validate a COSMOS plugin gem file"
puts " cli load /PATH/FILENAME.gem SCOPE variables.txt # Loads a COSMOS plugin gem file"
puts " cli list <SCOPE> # Lists installed plugins, SCOPE is DEFAULT if not given"
puts " cli generate TYPE OPTIONS # Generate various COSMOS entities"
puts " OPTIONS: --ruby or --python is required to specify the language in the generated code unless OPENC3_LANGUAGE is set"
puts " #{MIGRATE_PARSER}"
puts " cli bridge CONFIG_FILENAME # Run COSMOS host bridge"
puts " cli bridgegem gem_name variable1=value1 variable2=value2 # Runs bridge using gem bridge.txt"
puts " cli bridgesetup CONFIG_FILENAME # Create a default config file"
puts " cli pkginstall PKGFILENAME SCOPE # Install loaded package (Ruby gem or python package)"
puts " cli pkguninstall PKGFILENAME SCOPE # Uninstall loaded package (Ruby gem or python package)"
puts " cli rubysloc # Counts Ruby SLOC recursively. Run with --help for more info."
puts " cli xtce_converter # Convert to and from the XTCE format. Run with --help for more info."
puts " cli cstol_converter # Converts CSTOL files (.prc) to COSMOS. Run with --help for more info."
puts ""
end
def check_environment
hostname = ENV['OPENC3_API_HOSTNAME'] || (ENV['OPENC3_DEVEL'] ? '127.0.0.1' : 'openc3-cosmos-cmd-tlm-api')
begin
Resolv.getaddress(hostname)
rescue Resolv::ResolvError
abort "Unable to resolv api hostname: #{hostname}"
end
if hostname =~ /openc3-cosmos-cmd-tlm-api/
$openc3_in_cluster = true
else
$openc3_in_cluster = false
end
unless $openc3_in_cluster
# Make sure the user has all the required environment variables set
abort "OPENC3_API_HOSTNAME environment variable is required" unless ENV['OPENC3_API_HOSTNAME']
abort "OPENC3_API_PORT environment variable is required" unless ENV['OPENC3_API_PORT']
abort "OPENC3_API_PASSWORD environment variable is required" unless ENV['OPENC3_API_PASSWORD']
end
end
def migrate(args)
MIGRATE_PARSER.parse!(args)
abort(MIGRATE_PARSER.to_s) if args.length == 0
if MIGRATE_OPTIONS.all and args.length > 1
puts "Only specify the plugin name when using --all"
abort(MIGRATE_PARSER.to_s)
end
if !MIGRATE_OPTIONS.all and args.length < 2
puts "Specify the individual target names when not using --all"
abort(MIGRATE_PARSER.to_s)
end
if Dir.glob("config/targets/**/*").empty?
puts "No targets found in config/targets/*"
puts "Migrate must be run within an existing COSMOS configuration"
abort(MIGRATE_PARSER.to_s)
end
###############################################################
# Create the framework for the plugin
# NOTE: generate does a chdir to be inside the plugin directory
###############################################################
plugin = args.shift
OpenC3::CliGenerator.generate(['plugin', plugin])
if MIGRATE_OPTIONS.all
# Grab all target directories to match the command line input
args = Dir.glob("../config/targets/*").map { |path| File.basename(path) }
else
# Ensure targets passed in on command line actually exist
args.each do |target|
path = File.join('..', 'config', 'targets', target)
unless File.exist?(path)
puts "Target #{path} does not exist!"
abort(MIGRATE_PARSER.to_s)
end
end
end
# Overwrite plugin.txt with specified targets
plugin = File.open('plugin.txt', 'w')
FileUtils.mkdir 'targets'
args.each do |target|
puts "Migrating target #{target}"
FileUtils.cp_r "../config/targets/#{target}", 'targets'
plugin.puts "TARGET #{target} #{target}"
end
plugin.puts ""
files = Dir.glob('../lib/*')
files.concat(Dir.glob('../procedures/*'))
unless files.empty?
puts "Migrating /lib & /procedures to PROCEDURES target"
FileUtils.cp_r '../lib', "targets/PROCEDURES"
FileUtils.cp_r '../procedures', "targets/PROCEDURES"
end
# Migrate cmd_tlm_server.txt info to plugin.txt
Dir.glob('targets/**/cmd_tlm_server*.txt') do |cmd_tlm_server_file|
File.open(cmd_tlm_server_file) do |file|
file.each do |line|
next if line =~ /^\s*#/ # Ignore comments
next if line.strip.empty? # Ignore empty lines
# Convert TARGET to MAP_TARGET
line.gsub!(/TARGET (\S+)/, 'MAP_TARGET \1')
plugin.puts line
end
end
plugin.puts ''
end
# Migrate target.txt
Dir.glob('targets/**/target.txt') do |filename|
file = File.read(filename)
file.gsub!('LOG_RAW', 'LOG_STREAM')
file.gsub!('AUTO_SCREEN_SUBSTITUTE', '')
File.write(filename, file)
end
# Migrate some of the screens api
Dir.glob('targets/**/screens/*') do |file|
screen = File.read(file)
screen.gsub!('cmd(', 'api.cmd(')
screen.gsub!('cmd_no_checks(', 'api.cmd_no_checks(')
screen.gsub!('cmd_no_range_check(', 'api.cmd_no_range_check(')
screen.gsub!('cmd_no_hazardous_check(', 'api.cmd_no_hazardous_check(')
screen.gsub!('get_named_widget(', 'screen.getNamedWidget(')
lines = screen.split("\n")
lines.map! do |line|
if line.include?('Qt.')
"# FIXME (no Qt): #{line.sub("<%", "< %").sub("%>", "% >")}"
elsif line.include?('Cosmos::')
"# FIXME (no Cosmos::): #{line.sub("<%", "< %").sub("%>", "% >")}"
else
line
end
end
File.write(file, lines.join("\n"))
end
plugin.close
puts "Plugin complete: #{File.expand_path('.')}" # Remember we're inside the plugin dir
end
def xtce_converter(args)
options = {}
option_parser = OptionParser.new do |opts|
opts.banner = "Usage: xtce_converter [options] --import input_xtce_filename --output output_dir\n"+
" xtce_converter [options] --plugin /PATH/FILENAME.gem --output output_dir --variables variables.txt"
opts.separator("")
opts.on("-h", "--help", "Show this message") do
puts opts
exit
end
opts.on("-i VALUE", "--import VALUE", "Import the specified .xtce file") do |arg|
options[:import] = arg
end
opts.on("-o", "--output DIRECTORY", "Create files in the directory") do |arg|
options[:output] = arg
end
opts.on("-p", "--plugin PLUGIN", "Export .xtce file(s) from the plugin") do |arg|
options[:plugin] = arg
end
opts.on("-v", "--variables", "Optional variables file to pass to the plugin") do |arg|
options[:variables] = arg
end
end
begin
option_parser.parse!(args)
rescue
abort(option_parser.to_s)
end
if options[:import] && options[:plugin]
puts "xtce_converter options --import and --plugin are mutually exclusive"
abort(option_parser.to_s)
end
ENV['OPENC3_NO_STORE'] = '1' # it can be anything
OpenC3::Logger.stdout = false
OpenC3::Logger.level = OpenC3::Logger::DEBUG
if options[:import] && options[:output]
packet_config = OpenC3::PacketConfig.new
puts "Processing #{options[:import]}..."
packet_config.process_file(options[:import], nil)
puts "Writing COSMOS config files to #{options[:output]}/"
packet_config.to_config(options[:output])
exit(0)
elsif options[:plugin] && options[:output]
begin
variables = nil
variables = JSON.parse(File.read(options[:variables]), :allow_nan => true, :create_additions => true) if options[:variables]
puts "Installing #{File.basename(options[:plugin])}"
plugin_hash = OpenC3::PluginModel.install_phase1(options[:plugin], existing_variables: variables, scope: 'DEFAULT', validate_only: true)
plugin_hash['variables']['xtce_output'] = options[:output]
OpenC3::PluginModel.install_phase2(plugin_hash, scope: 'DEFAULT', validate_only: true,
gem_file_path: options[:plugin])
result = 0 # bash and Windows consider 0 success
rescue => e
puts "Error: #{e.message}"
puts e.backtrace
result = ERROR_CODE
ensure
exit(result)
end
else
abort(option_parser.to_s)
end
end
# A helper method to make the zip writing recursion work
def write_zip_entries(base_dir, entries, zip_path, io)
io.add(zip_path, base_dir) # Add the directory whether it has entries or not
entries.each do |e|
zip_file_path = File.join(zip_path, e)
disk_file_path = File.join(base_dir, e)
if File.directory? disk_file_path
recursively_deflate_directory(disk_file_path, io, zip_file_path)
else
put_into_archive(disk_file_path, io, zip_file_path)
end
end
end
def recursively_deflate_directory(disk_file_path, io, zip_file_path)
io.add(zip_file_path, disk_file_path)
write_zip_entries(disk_file_path, entries, zip_file_path, io)
end
def put_into_archive(disk_file_path, io, zip_file_path)
io.get_output_stream(zip_file_path) do |f|
data = nil
File.open(disk_file_path, 'rb') { |file| data = file.read }
f.write(data)
end
end
def validate_plugin(plugin_file_path, scope:, variables_file: nil)
ENV['OPENC3_NO_STORE'] = '1' # it can be anything
OpenC3::Logger.stdout = false
OpenC3::Logger.level = OpenC3::Logger::DEBUG
scope ||= 'DEFAULT'
variables = nil
variables = JSON.parse(File.read(variables_file), :allow_nan => true, :create_additions => true) if variables_file
puts "Installing #{File.basename(plugin_file_path)}"
plugin_hash = OpenC3::PluginModel.install_phase1(plugin_file_path, existing_variables: variables, scope: scope, validate_only: true)
OpenC3::PluginModel.install_phase2(plugin_hash, scope: scope, validate_only: true,
gem_file_path: plugin_file_path)
puts "Successfully validated #{File.basename(plugin_file_path)}"
result = 0 # bash and Windows consider 0 success
rescue => e
puts e.message
result = ERROR_CODE
ensure
exit(result)
end
def update_plugin(plugin_file_path, plugin_name, variables: nil, plugin_txt_lines: nil, scope:, existing_plugin_name:, force: false)
new_gem = File.basename(plugin_file_path)
old_gem = existing_plugin_name.split("__")[0]
puts "Updating existing plugin: #{existing_plugin_name} with #{File.basename(plugin_file_path)}"
plugin_model = OpenC3::PluginModel.get_model(name: existing_plugin_name, scope: scope)
begin
# Only update if something has changed
if force or (new_gem != old_gem) or (variables and variables != plugin_model.variables) or (plugin_txt_lines and plugin_txt_lines != plugin_model.plugin_txt_lines)
puts "Gem version change detected - New: #{new_gem}, Old: #{old_gem}" if new_gem != old_gem
if variables and variables != plugin_model.variables
pp_variables = ""
PP.pp(variables, pp_variables)
pp_plugin_model_variables = ""
PP.pp(plugin_model.variables, pp_plugin_model_variables)
puts "Variables change detected\nNew:\n#{pp_variables}\nOld:\n#{pp_plugin_model_variables}"
end
puts "plugin.txt change detected\nNew:\n#{plugin_txt_lines.join("\n")}\n\nOld:\n#{plugin_model.plugin_txt_lines.join("\n")}\n" if plugin_txt_lines and plugin_txt_lines != plugin_model.plugin_txt_lines
variables = plugin_model.variables unless variables
plugin_model.destroy
plugin_hash = OpenC3::PluginModel.install_phase1(plugin_file_path, existing_variables: variables, existing_plugin_txt_lines: plugin_txt_lines, process_existing: true, scope: scope)
puts "Updating plugin: #{plugin_file_path}\n#{plugin_hash}"
plugin_hash = OpenC3::PluginModel.install_phase2(plugin_hash, scope: scope)
OpenC3::LocalMode.update_local_plugin(plugin_file_path, plugin_hash, old_plugin_name: plugin_name, scope: scope)
else
puts "No changes detected - Exiting without change"
end
rescue => e
puts e.formatted
if plugin_model.destroyed?
plugin_model.restore
# Local mode files should still be good because restore will now reuse the old name
end
raise e
end
end
def wait_process_complete(process_name)
STDOUT.flush
state = 'Running'
status = nil
while state == 'Running'
status = plugin_status(process_name)
state = status['state']
sleep(5)
print '.'
STDOUT.flush
end
puts "\nFinished: #{state}"
puts "Output:\n"
puts status['output']
if state == 'Complete'
puts "Success!"
exit 0
else
puts "Failed!"
exit 1
end
end
# Outputs list of installed plugins
def list_plugins(scope:)
scope ||= 'DEFAULT'
check_environment()
names = []
if $openc3_in_cluster
names = OpenC3::PluginModel.names(scope: scope)
else
require 'openc3/script'
names = plugin_list(scope: scope)
end
names.each do |name|
puts name
end
end
# Loads a plugin into the OpenC3 system
# This code is used from the command line and is the same code that gets called if you
# edit/upgrade or install a new plugin from the Admin interface
#
# Usage: cli load gemfile_path [scope] [plugin_hash_file_path] [force]
#
# With just gemfile_path and/or scope: Will do nothing if any plugin
# with the same gem file already exists
#
# Otherwise will do what the plugin_hash_file says to do
# Plugin hash file must have the exact name of an existing plugin for upgrades and edits
# Otherwise, it will be assumed that the plugin is intentionally being installed for a second
# time
#
# Pass true as the last argument to force install even if a plugin with
# the same version number exists
#
def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false)
scope ||= 'DEFAULT'
check_environment()
if $openc3_in_cluster
# In Cluster
# Only create the scope if it doesn't already exist
unless OpenC3::ScopeModel.names.include?(scope)
begin
puts "Creating scope: #{scope}"
scope_model = OpenC3::ScopeModel.new(name: scope)
scope_model.create
scope_model.deploy(".", {})
rescue => e
abort("Error creating scope: #{scope}: #{e.formatted}")
end
end
begin
if plugin_hash_file
# Admin Create / Edit / or Upgrade Plugin
OpenC3::PluginModel.install_phase1(plugin_file_path, scope: scope)
plugin_hash = JSON.parse(File.read(plugin_hash_file), :allow_nan => true, :create_additions => true)
else
# Init or Command Line openc3cli load with no plugin_hash_file
file_full_name = File.basename(plugin_file_path, ".gem")
file_gem_name = file_full_name.split('-')[0..-2].join('-')
found = false
plugin_names = OpenC3::PluginModel.names(scope: scope)
plugin_names.each do |plugin_name|
gem_name = plugin_name.split("__")[0]
full_name = File.basename(gem_name, ".gem")
gem_name = full_name.split('-')[0..-2].join('-')
if file_gem_name == gem_name
found = true
# Upgrade if version changed else do nothing
if file_full_name != full_name
update_plugin(plugin_file_path, plugin_name, scope: scope, existing_plugin_name: plugin_name, force: force)
else
puts "No version change detected for: #{plugin_name}"
end
end
end
return if found
plugin_hash = OpenC3::PluginModel.install_phase1(plugin_file_path, scope: scope)
end
# Determine if plugin named in plugin_hash exists
existing_plugin_hash = OpenC3::PluginModel.get(name: plugin_hash['name'], scope: scope)
# Existing plugin hash will be present if plugin is being edited or upgraded
# However, a missing existing could also be that a plugin was updated in local mode directly from across installations
# changing the plugin name without really meaning to create a new instance of the plugin
# ie.
# User on machine 1 checks in a changed plugin_instance.json with a different name - There is still only one plugin desired and committed
# User on machine 2 starts up with the new configuration, OpenC3::PluginModel.get will return nil because the exact name is different
# In this case, the plugin should be updated without installing a second instance. analyze_local_mode figures this out.
unless existing_plugin_hash
existing_plugin_hash = OpenC3::LocalMode.analyze_local_mode(plugin_name: plugin_hash['name'], scope: scope)
end
if existing_plugin_hash
# Upgrade or Edit
update_plugin(plugin_file_path, plugin_hash['name'], variables: plugin_hash['variables'], scope: scope,
plugin_txt_lines: plugin_hash['plugin_txt_lines'], existing_plugin_name: existing_plugin_hash['name'], force: force)
else
# New Install
puts "Loading new plugin: #{plugin_file_path}\n#{plugin_hash}"
plugin_hash = OpenC3::PluginModel.install_phase2(plugin_hash, scope: scope)
OpenC3::LocalMode.update_local_plugin(plugin_file_path, plugin_hash, scope: scope)
end
rescue => e
abort("Error installing plugin: #{scope}: #{plugin_file_path}\n#{e.formatted}")
end
else
# Outside Cluster
require 'openc3/script'
if plugin_hash_file
plugin_hash = JSON.parse(File.read(plugin_hash_file), :allow_nan => true, :create_additions => true)
else
plugin_hash = plugin_install_phase1(plugin_file_path, scope: scope)
end
process_name = plugin_install_phase2(plugin_hash, scope: scope)
print "Installing..."
wait_process_complete(process_name)
end
end
def unload_plugin(plugin_name, scope:)
scope ||= 'DEFAULT'
check_environment()
if $openc3_in_cluster
begin
plugin_model = OpenC3::PluginModel.get_model(name: plugin_name, scope: scope)
plugin_model.destroy
OpenC3::LocalMode.remove_local_plugin(plugin_name, scope: scope)
OpenC3::Logger.info("PluginModel destroyed: #{plugin_name}", scope: scope)
rescue => e
abort("Error uninstalling plugin: #{scope}: #{plugin_name}: #{e.formatted}")
end
else
# Outside Cluster
require 'openc3/script'
process_name = plugin_uninstall(plugin_name, scope: scope)
print "Uninstalling..."
wait_process_complete(process_name)
end
end
def cli_pkg_install(filename, scope:)
scope ||= 'DEFAULT'
check_environment()
if $openc3_in_cluster
if File.extname(filename) == '.gem'
OpenC3::GemModel.install(filename, scope: scope)
else
OpenC3::PythonPackageModel.install(filename, scope: scope)
end
else
# Outside Cluster
require 'openc3/script'
process_name = package_install(filename, scope: scope)
print "Installing..."
wait_process_complete(process_name)
end
end
def cli_pkg_uninstall(filename, scope:)
scope ||= 'DEFAULT'
check_environment()
if $openc3_in_cluster
if File.extname(filename) == '.rb'
OpenC3::GemModel.destroy(filename)
else
OpenC3::PythonPackageModel.destroy(filename, scope: scope)
end
else
# Outside Cluster
require 'openc3/script'
process_name = package_uninstall(filename, scope: scope)
if File.extname(filename) == '.rb'
puts "Uninstalled"
else
print "Uninstalling..."
wait_process_complete(process_name)
end
end
end
def get_redis_keys
redis = Redis.new(url: $redis_url, username: ENV['OPENC3_REDIS_USERNAME'], password: ENV['OPENC3_REDIS_PASSWORD'])
puts "\n--- COSMOS Redis database keys ---"
cursor = 0
keys = []
loop do
cursor, result = redis.scan(cursor)
keys.concat(result)
cursor = cursor.to_i # cursor is returned as a string
break if cursor == 0
end
keys.uniq!
keys.sort!
keys.select { |item| !item[/^tlm__/] }.each do |key|
puts "#{key}\n #{redis.hkeys(key)}"
rescue Redis::CommandError
begin
# CommandError is raised if you try to hkeys on a stream
puts "Stream: #{key}\n #{redis.xinfo(:stream, key)}"
rescue
puts "Unknown key '#{key}'"
end
end
puts "Packets Defs: #{keys.select { |item| item[/^tlm__/] }}"
end
def run_migrations(folder)
# Determine if this is a brand new installation (no tools installed)
# We don't run migrations on new installations
tools = OpenC3::ToolModel.names(scope: 'DEFAULT')
if tools.length <= 0
puts "Brand new installation detected"
brand_new = true
else
puts "Checking for needed migrations..."
brand_new = false
end
# Run each newly discovered migration unless brand_new
folder = "/openc3/lib/openc3/migrations" unless folder
migrations = OpenC3::MigrationModel.all
entries = Dir.entries(folder).sort # run in alphabetical order
entries.each do |entry|
name = File.basename(entry)
extension = File.extname(name)
if extension == '.rb' and not migrations[name]
unless brand_new
puts "Running Migration: #{name}"
require File.join(folder, entry)
end
OpenC3::MigrationModel.new(name: name).create
end
end
if brand_new
puts "All migrations skipped"
else
puts "Migrations complete"
end
end
def run_bridge(filename, params)
variables = {}
params.each do |param|
name, value = param.split('=')
if name and value
variables[name] = value
else
raise "Invalid variable passed to bridgegem (syntax name=value): #{param}"
end
end
OpenC3::Bridge.new(filename, variables)
begin
while true
sleep(1)
end
rescue Interrupt
exit(0)
end
end
def cli_script_monitor(script_id)
ret_code = ERROR_CODE
require 'openc3/script'
OpenC3::RunningScriptWebSocketApi.new(id: script_id) do |api|
while (resp = api.read) do
# see ScriptRunner.vue for types and states
case resp['type']
when 'error', 'fatal'
$script_interrupt_text = ''
puts 'script failed'
break
when 'file'
puts "Filename #{resp['filename']} scope #{resp['scope']}"
when 'line'
fn = resp['filename'].nil? ? '<no file>' : resp['filename']
puts "At [#{fn}:#{resp['line_no']}] state [#{resp['state']}]"
if resp['state'] == 'error'
$script_interrupt_text = ''
puts 'script failed'
break
end
when 'output'
puts resp['line']
when 'paused'
if resp['state'] == 'fatal'
$script_interrupt_text = ''
puts 'script failed'
break
end
when 'complete'
$script_interrupt_text = ''
puts 'script complete'
ret_code = 0
break
# These conditions are all handled by the else
# when 'running', 'breakpoint', 'waiting', 'time'
else
puts resp.pretty_inspect
end
end
end
return ret_code
end
def cli_script_list(args=[])
path = ''
if (args[0] && args[0][0] == '/')
path = (args.shift)[1..-1]+'/'
end
scope = args[1]
scope ||= 'DEFAULT'
require 'openc3/script'
script_list(scope: scope).each do |script_name|
puts(script_name) if script_name.start_with?(path)
end
return 0
end
def cli_script_run(disconnect=false, environment={}, args=[])
# we are limiting the wait for status, not the script run time
# we make 0 mean 'forever'
ret_code = ERROR_CODE
wait_limit = 0
if (i = args.index('--wait'))
begin
args.delete('--wait')
# pull out the flag
seconds = args[i]
wait_limit = Integer(seconds, 10) # only decimal, ignore leading 0
args.delete_at(i)
# and its value
rescue ArgumentError
abort(" --wait requires a number of seconds to wait, not [#{seconds}]")
end
end
abort("No script file provided") if args[0].nil?
scope = args[1]
scope ||= 'DEFAULT'
require 'openc3/script'
id = script_run(args[0], disconnect: disconnect, environment: environment, scope: scope) # could raise
$script_interrupt_text = " Script #{args[0]} still running remotely.\n" # for Ctrl-C
if (wait_limit < 1) then
ret_code = cli_script_monitor(id)
else
Timeout::timeout(wait_limit, nil, "--wait #{wait_limit} exceeded") do
ret_code = cli_script_monitor(id)
rescue Timeout::ExitException, Timeout::Error => e
# Timeout exceptions are also raised by the Websocket API, so we check
if e.message =~ /^--wait /
puts e.message + ", detaching from running script #{args[0]}"
else
raise
end
end
end
return ret_code
end
def cli_script_spawn(disconnect=false, environment={}, args=[])
ret_code = ERROR_CODE
if (args.index('--wait'))
abort("Did you mean \"script run --wait <seconds> [...]\"?")
end
abort("No script file provided") if args[0].nil?
# heaven help you if you left out the script name
scope = args[1]
scope ||= 'DEFAULT'
require 'openc3/script'
if (id = script_run(args[0], disconnect: disconnect, environment: environment, scope: scope))
puts id
ret_code = 0
end
return ret_code
end
## cli_script(args) turns an ARGV of [spawn|run] <--wait 123...> <--disconnect> SCRIPT <scope> <ENV_A=1 ENV_B=2 ...>
# into function calls and tidied parameters to remote-control a script via RunningScriptWebSocketApi
def cli_script(args=[])
ret_code = ERROR_CODE
check_environment()
# Double check for the OPENC3_API_PASSWORD because it is absolutely required
# We always pass it via openc3.sh even if it's not defined so check for empty
if ENV['OPENC3_API_PASSWORD'].nil? or ENV['OPENC3_API_PASSWORD'].empty?
abort "OPENC3_API_PASSWORD environment variable is required for cli script"
end
command = args.shift
# pull out the disconnect flag
discon = args.delete('--disconnect')
discon = (discon.is_a? String) ? true : false
environ = {}
args.each do |arg|
name, value = arg.split('=')
if name and value
# add env[k]=v ; pull out "k=v"
environ[name] = value
args.delete(arg)
end
end
case command
# for list
# args[] should now be ["/<path>", "<scope>"]
# or ["/<path>"]
# or ["<scope>"]
# or []
when 'list'
ret_code = cli_script_list(args)
# for spawn or run
# args[] should now be ["--wait 100", "script_name", "<scope>"]
# or ["--wait 100", "script_name"]
# or ["script_name", "<scope>"]
# or ["script_name"]
when 'spawn'
ret_code = cli_script_spawn(discon, environ, args)
when 'run'
ret_code = cli_script_run(discon, environ, args)
else
abort 'openc3cli internal error: parsing arguments'
end
exit(ret_code)
end
if not ARGV[0].nil? # argument(s) given
# Handle each task
case ARGV[0].downcase
when 'irb'
ARGV.clear
IRB.start
when 'script'
case ARGV[1]
when 'list', 'run', 'spawn'
cli_script(ARGV[1..-1])
else
# invalid actions, misplaced and malformed leading options and 'help' come here
abort("cli script <action> must be one of #{CLI_SCRIPT_ACTIONS}, not [#{ARGV[1]}]")
end
when 'rake'
if File.exist?('Rakefile')
puts `rake #{ARGV[1..-1].join(' ')}`
else
puts "No Rakefile found! Only run 'rake' in the presence of a Rakefile which is typically at the root of your COSMOS project."
end
when 'validate'
validate_plugin(ARGV[1], scope: ARGV[2], variables_file: ARGV[3])
when 'load'
# force is a boolean so if they pass 'force' it is true
# See plugins_controller.rb install for usage
load_plugin(ARGV[1], scope: ARGV[2], plugin_hash_file: ARGV[3], force: ARGV[4] == 'force')
when 'list'
list_plugins(scope: ARGV[1])
when 'unload'
unload_plugin(ARGV[1], scope: ARGV[2])
when 'pkginstall', 'geminstall'
cli_pkg_install(ARGV[1], scope: ARGV[2])
when 'pkguninstall', 'gemuninstall'
cli_pkg_uninstall(ARGV[1], scope: ARGV[2])
when 'generate'
OpenC3::CliGenerator.generate(ARGV[1..-1])
when 'migrate'
migrate(ARGV[1..-1])
when 'rubysloc'
puts `ruby /openc3/bin/rubysloc #{ARGV[1..-1].join(' ')}`
when 'cstol_converter'
puts `ruby /openc3/bin/cstol_converter #{ARGV[1..-1].join(' ')}`
when 'xtce_converter'
xtce_converter(ARGV[1..-1])
when 'bridge'
ENV['OPENC3_NO_STORE'] = '1'
filename = ARGV[1]
filename = 'bridge.txt' unless filename
params = ARGV[2..-1]
params = [] unless params
run_bridge(filename, params)
when 'bridgegem'
ENV['OPENC3_NO_STORE'] = '1'
filename = nil
gem_name = ARGV[1]
Gem::Specification.each do |s|
# This appears to return the newest version of each gem first,
# so its ok that we stop on the first time it is found
if s.name == gem_name
if Array === Gem.path
Gem.path.each do |gem_path|
filename = File.join(gem_path, 'gems', "#{s.name}-#{s.version}", 'bridge.txt')
puts "Trying #{filename}"
break if File.exist?(filename)
end
else
filename = File.join(Gem.path, 'gems', "#{s.name}-#{s.version}", 'bridge.txt')
end
raise "#{filename} not found" unless File.exist?(filename)
end
end
raise "gem #{gem_name} not found" unless filename
params = ARGV[2..-1]
params = [] unless params
run_bridge(filename, params)
when 'bridgesetup'
ENV['OPENC3_NO_STORE'] = '1'
filename = ARGV[1]
filename = 'bridge.txt' unless filename
unless File.exist?(filename)
OpenC3::BridgeConfig.generate_default(filename)
end
when 'help'
print_usage()
when 'redis'
case (ARGV[1])
when 'keys'
get_redis_keys()
when 'hget'
redis = Redis.new(url: $redis_url, username: ENV['OPENC3_REDIS_USERNAME'], password: ENV['OPENC3_REDIS_PASSWORD'])
puts JSON.parse(redis.hget(ARGV[2], ARGV[3]), :allow_nan => true, :create_additions => true)
else
puts "Unknown redis task: #{ARGV[1]}\n"
puts "Valid redis tasks: keys, hget"
end
when 'removebase'
# Used to remove tool base to better support enterprise upgrade
scopes = OpenC3::ScopeModel.all
scopes.each do |scope_name, _scope|
plugins = OpenC3::PluginModel.all(scope: scope_name)
plugins.each do |plugin_name, plugin|
if plugin["name"] =~ /tool-base/ and plugin["name"] !~ /enterprise/
unload_plugin(plugin_name, scope: scope_name)
end
if plugin["name"] =~ /tool-admin/ and plugin["name"] !~ /enterprise/
unload_plugin(plugin_name, scope: scope_name)
end
end
end
when 'removeenterprise'
# Used to remove enterprise plugins to better support downgrade
scopes = OpenC3::ScopeModel.all
scopes.each do |scope_name, _scope|
plugins = OpenC3::PluginModel.all(scope: scope_name)
plugins.each do |plugin_name, plugin|
if plugin["name"] =~ /enterprise/
unload_plugin(plugin_name, scope: scope_name)
end
end
end
when 'destroyscope'
scope = OpenC3::ScopeModel.get_model(name: ARGV[1])
scope.destroy
when 'localinit'
OpenC3::LocalMode.local_init()
when 'initbuckets'
client = OpenC3::Bucket.getClient()
ENV.map do |key, value|
if key.match(/^OPENC3_(.+)_BUCKET$/) && !value.empty?
client.create(value)
end
end
client.ensure_public(ENV['OPENC3_TOOLS_BUCKET'])
when 'runmigrations'
run_migrations(ARGV[1])
else # Unknown task
print_usage()
abort("Unknown task: #{ARGV[0]}")
end
else # No arguments given
print_usage()
end