Skip to content

Commit

Permalink
Merge pull request #1501 from OpenC3/cli_script_wait
Browse files Browse the repository at this point in the history
Cli script wait
  • Loading branch information
ryanmelt authored Sep 18, 2024
2 parents bae770e + 77316ed commit 85d2f56
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 14 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ on:
pull_request:
branches: [main]

env:
OPENC3_API_PASSWORD: password

jobs:
openc3-cli:
if: ${{ github.actor != 'dependabot[bot]' }}
Expand Down Expand Up @@ -101,6 +104,19 @@ jobs:
ls targets/MY_CLI/lib | grep example_limits_response.rb
../openc3.sh cliroot rake build VERSION=1.0.3
../openc3.sh cliroot validate openc3-cosmos-cli-test-1.0.3.gem
- name: openc3.sh cli script list, run, spawn
shell: 'script -q -e -c "bash {0}"'
run: |
set -e
docker exec -it cosmos-openc3-redis-1 sh -c "echo -e 'AUTH openc3 openc3password\nset OPENC3__TOKEN 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8' | redis-cli"
# list shows all the available file names
./openc3.sh cliroot script list | tee /dev/tty | grep "INST/procedures/stash.rb"
# spawning a script prints only a PID
./openc3.sh cliroot script spawn INST/procedures/checks.rb | grep -v "^\s*\d+\s*$"
# run a script that will fail and look for the failure message
./openc3.sh cliroot script run --wait 10 INST/procedures/checks.rb | tee /dev/tty | grep -q "script failed"
# run a script that will complete successfully
./openc3.sh cliroot script run INST/procedures/stash.rb | tee /dev/tty | grep "script complete"
- name: openc3.sh util save,load
shell: 'script -q -e -c "bash {0}"'
run: |
Expand Down
2 changes: 2 additions & 0 deletions openc3-cosmos-script-runner-api/app/models/running_script.rb
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ def initialize(id, scope, name, disconnect)

# Retrieve file
@body = ::Script.body(@scope, name)
raise "Script not found: #{name}" if @body.nil?
breakpoints = @@breakpoints[filename]&.filter { |_, present| present }&.map { |line_number, _| line_number - 1 } # -1 because frontend lines are 0-indexed
breakpoints ||= []
OpenC3::Store.publish(["script-api", "running-script-channel:#{@id}"].compact.join(":"),
Expand Down Expand Up @@ -1271,6 +1272,7 @@ def load_file_into_script(filename)
OpenC3::Store.publish(["script-api", "running-script-channel:#{@id}"].compact.join(":"), JSON.generate({ type: :file, filename: filename, text: @body.to_utf8, breakpoints: breakpoints }))
else
text = ::Script.body(@scope, filename)
raise "Script not found: #{filename}" if text.nil?
@@file_cache[filename] = text
@body = text
OpenC3::Store.publish(["script-api", "running-script-channel:#{@id}"].compact.join(":"), JSON.generate({ type: :file, filename: filename, text: @body.to_utf8, breakpoints: breakpoints }))
Expand Down
6 changes: 2 additions & 4 deletions openc3.bat
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@ if "%1" == "cli" (
REM mapped as volume (-v) /openc3/local and container working directory (-w) also set to /openc3/local.
REM This allows tools running in the container to have a consistent path to the current working directory.
REM Run the command "ruby /openc3/bin/openc3" with all parameters ignoring the first.
docker network create openc3-cosmos-network
docker run -it --rm --env-file %~dp0.env --network openc3-cosmos-network -v %cd%:/openc3/local -w /openc3/local !OPENC3_REGISTRY!/!OPENC3_NAMESPACE!/openc3-operator!OPENC3_IMAGE_SUFFIX!:!OPENC3_TAG! ruby /openc3/bin/openc3cli !params!
docker compose -f %~dp0compose.yaml run -it --rm -v %cd%:/openc3/local -w /openc3/local -e OPENC3_API_PASSWORD=!OPENC3_API_PASSWORD! --no-deps openc3-cosmos-cmd-tlm-api ruby /openc3/bin/openc3cli !params!
GOTO :EOF
)
if "%1" == "cliroot" (
FOR /F "tokens=*" %%i in ('findstr /V /B /L /C:# %~dp0.env') do SET %%i
set params=%*
call set params=%%params:*%1=%%
docker network create openc3-cosmos-network
docker run -it --rm --env-file %~dp0.env --user=root --network openc3-cosmos-network -v %cd%:/openc3/local -w /openc3/local !OPENC3_REGISTRY!/!OPENC3_NAMESPACE!/openc3-operator!OPENC3_IMAGE_SUFFIX!:!OPENC3_TAG! ruby /openc3/bin/openc3cli !params!
docker compose -f %~dp0compose.yaml run -it --rm --user=root -v %cd%:/openc3/local -w /openc3/local -e OPENC3_API_PASSWORD=!OPENC3_API_PASSWORD! --no-deps openc3-cosmos-cmd-tlm-api ruby /openc3/bin/openc3cli !params!
GOTO :EOF
)
if "%1" == "start" (
Expand Down
7 changes: 2 additions & 5 deletions openc3.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,14 @@ case $1 in
# This allows tools running in the container to have a consistent path to the current working directory.
# Run the command "ruby /openc3/bin/openc3cli" with all parameters starting at 2 since the first is 'openc3'
args=`echo $@ | { read _ args; echo $args; }`
# Make sure the network exists
(docker network create openc3-cosmos-network || true) &> /dev/null
docker run -it --rm --env-file "$(dirname -- "$0")/.env" --user=$OPENC3_USER_ID:$OPENC3_GROUP_ID --network openc3-cosmos-network -v `pwd`:/openc3/local:z -w /openc3/local $OPENC3_REGISTRY/$OPENC3_NAMESPACE/openc3-operator$OPENC3_IMAGE_SUFFIX:$OPENC3_TAG ruby /openc3/bin/openc3cli $args
${DOCKER_COMPOSE_COMMAND} -f "$(dirname -- "$0")/compose.yaml" run -it --rm -v `pwd`:/openc3/local:z -w /openc3/local -e OPENC3_API_PASSWORD=$OPENC3_API_PASSWORD --no-deps openc3-cosmos-cmd-tlm-api ruby /openc3/bin/openc3cli $args
set +a
;;
cliroot )
set -a
. "$(dirname -- "$0")/.env"
args=`echo $@ | { read _ args; echo $args; }`
(docker network create openc3-cosmos-network || true) &> /dev/null
docker run -it --rm --env-file "$(dirname -- "$0")/.env" --user=root --network openc3-cosmos-network -v `pwd`:/openc3/local:z -w /openc3/local $OPENC3_REGISTRY/$OPENC3_NAMESPACE/openc3-operator$OPENC3_IMAGE_SUFFIX:$OPENC3_TAG ruby /openc3/bin/openc3cli $args
${DOCKER_COMPOSE_COMMAND} -f "$(dirname -- "$0")/compose.yaml" run -it --rm --user=root -v `pwd`:/openc3/local:z -w /openc3/local -e OPENC3_API_PASSWORD=$OPENC3_API_PASSWORD --no-deps openc3-cosmos-cmd-tlm-api ruby /openc3/bin/openc3cli $args
set +a
;;
start )
Expand Down
192 changes: 187 additions & 5 deletions openc3/bin/openc3cli
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,25 @@ MIGRATE_PARSER = OptionParser.new do |opts|
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"
Expand Down Expand Up @@ -160,8 +173,8 @@ def migrate(args)
end

# Migrate cmd_tlm_server.txt info to plugin.txt
Dir.glob('targets/**/cmd_tlm_server*.txt') do |file|
File.open(file) do |file|
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
Expand Down Expand Up @@ -193,9 +206,9 @@ def migrate(args)
lines = screen.split("\n")
lines.map! do |line|
if line.include?('Qt.')
line = "# FIXME (no Qt): #{line.sub("<%", "< %").sub("%>", "% >")}"
"# FIXME (no Qt): #{line.sub("<%", "< %").sub("%>", "% >")}"
elsif line.include?('Cosmos::')
line = "# FIXME (no Cosmos::): #{line.sub("<%", "< %").sub("%>", "% >")}"
"# FIXME (no Cosmos::): #{line.sub("<%", "< %").sub("%>", "% >")}"
else
line
end
Expand Down Expand Up @@ -493,7 +506,6 @@ def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false)
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
Expand Down Expand Up @@ -648,6 +660,167 @@ def run_bridge(filename, params)
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
Expand All @@ -657,6 +830,15 @@ if not ARGV[0].nil? # argument(s) given
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(' ')}`
Expand Down

0 comments on commit 85d2f56

Please sign in to comment.