Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cli script wait #1501

Merged
merged 26 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3d2c912
run script from command line and wait, monitoring status
JL-Brothers Aug 28, 2024
a20fac6
fix crash when seconds option provided
JL-Brothers Aug 28, 2024
fcff7aa
error exit if no script name provided
JL-Brothers Aug 28, 2024
79823d4
te was correct and meaningful, but CodeSpell, so tmo timeout excp exc…
JL-Brothers Aug 28, 2024
9ab7561
CodeSpell, timeout, and exit exception
JL-Brothers Aug 29, 2024
0bac9c8
more options and env parms for ARGV, and more message types and state…
JL-Brothers Aug 30, 2024
b057f16
incorporated review comments and added 'openc3cli script list /<path>…
JL-Brothers Aug 30, 2024
197e761
add another raise-on-empty-body to running_scripts and two tests for …
JL-Brothers Aug 30, 2024
0a111e6
respond to review comments
JL-Brothers Aug 30, 2024
29a837c
respond to review comments
JL-Brothers Aug 30, 2024
4bde7d6
responses to review comments
JL-Brothers Aug 30, 2024
0a6fd7d
respond to review comments, fix bug in cli_script_spawn, fix tests an…
JL-Brothers Sep 3, 2024
562433c
fix newest test, rename a function and correct a comment
JL-Brothers Sep 3, 2024
079e21a
ran on my machine: this is for debugging in CI
JL-Brothers Sep 3, 2024
b1b19ad
Update openc3.sh to use docker compose run instead of docker run
ryanmelt Sep 5, 2024
c2054d0
Update cli to use cmd-tlm-api container
ryanmelt Sep 5, 2024
1226b56
Use detected docker compose
ryanmelt Sep 5, 2024
bc3f405
fix cli script run/spawn --disconnect flag
JL-Brothers Sep 6, 2024
e798815
Merge branch 'cli_script_wait' of https://github.com/OpenC3/cosmos in…
JL-Brothers Sep 6, 2024
eab95a5
run the test script disconnected from the target
JL-Brothers Sep 6, 2024
94fa393
Merge branch 'main' into cli_script_wait
JL-Brothers Sep 6, 2024
cdeb9f1
set api password env var for cli test
JL-Brothers Sep 6, 2024
f0015b9
push password hash to redis, matching env var
JL-Brothers Sep 6, 2024
fd77845
Add password to env
jmthomas Sep 13, 2024
3a7e13e
Rework script cli
jmthomas Sep 13, 2024
77316ed
Check for empty password
jmthomas Sep 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1270,6 +1271,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"
JL-Brothers marked this conversation as resolved.
Show resolved Hide resolved
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
JL-Brothers marked this conversation as resolved.
Show resolved Hide resolved
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
Loading