Skip to content
Snippets Groups Projects
Commit f8bdc087 authored by Anders Lindh Olsson's avatar Anders Lindh Olsson :8ball:
Browse files

Rewrite iocsh

The entry point for the e3 IOC shell has been rewritten from being a shell/bash
script to instead be in python. Together with this comes a plethora of changes -
see the CHANGELOG for a more comprehensive description.
parent 862b3c5e
No related branches found
No related tags found
1 merge request!147E3-615: Replace `iocsh` shell script with python script
......@@ -17,6 +17,18 @@ in the wrapper would cause all of the module `make` commands to fail.
### Other changes
* Rewrite `iocsh` converting it from being a shell script to a python (3.6) script
* Remove support for file extensions: `.so`, `.dbd`, `.db`, `.substitutions`, `.template`, `.iocsh`
* Remove support for `nice`
* Remove support for sequencer programs
* Remove optional argument passing to `gdb`
* Change the IOC shell to use both stdout and stderr (previously only stdout)
* Change default prompt
* Change fallback IOC-name (used when `IOCNAME` is not set)
* Multiple argument changes; e.g.
* Shortened argument for printing version and exit changed from `-v` to `-V`
* Make running IOC as realtime or with debuggers mutually exclusive
* Change how arguments are passed to `gdb` and `valgrind` (see help: `--help`)
* Replaced `tclx` script to expand .dbd files with a python script
* Removed `hdrs` target. Note that this has been entirely replaced by the automatic db expansion.
* Removed ability to pass `args` to require (which have not been used within e3)
......
#!/bin/bash
#
# Copyright (c) 2004 - 2017 Paul Scherrer Institute
# Copyright (c) 2017 - Present European Spallation Source ERIC
#
# The program is free software: you can redistribute
# it and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 2 of the
# License, or any newer version.
#
# 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see https://www.gnu.org/licenses/gpl-2.0.txt
#
SC_SCRIPT="$(readlink -e "$0")"
SC_SCRIPTNAME=${0##*/}
SC_TOP="${SC_SCRIPT%/*}"
TMP_PATH="/tmp/systemd-private-e3-iocsh-$(whoami)"
declare -r SC_SCRIPT SC_SCRIPTNAME SC_TOP TMP_PATH
declare SC_VERSION="${E3_REQUIRE_VERSION}"
# shellcheck source=require-ess/tools/iocsh_functions.bash
. "${SC_TOP}"/iocsh_functions.bash
# To get the absolute path where iocsh is executed
IOCSH_TOP=${PWD}
# Check whether an expected e3 environment variable is defined.
#
if [[ -z "${EPICS_DRIVER_PATH}" ]]; then
set -a
# shellcheck source=require-ess/tools/activate
. "${SC_TOP}"/activate "no_msg"
set +a
fi
# Check that the e3 environment definition and the path for this script
# are consistent
if [ "${E3_REQUIRE_BIN}" != "${SC_TOP}" ]; then
echo "Error: Configured e3 environment does not match path for iocsh executable."
echo "Please source the appropriate activation file for the version of iocsh"
echo "you wish to use, or run iocsh from a clean environment so it can set the"
echo "environment correctly."
echo "Expected path to iocsh from environment = ${E3_REQUIRE_BIN}"
echo "Actual path to iocsh = ${SC_TOP}"
die 2
fi
check_mandatory_env_settings
# ${BASHPID} returns iocsh PID
iocsh_id=${BASHPID}
#
SC_VERSION+=-PID-${iocsh_id}
#
# We define HOSTNAME + iocsh_id
IOCSH_PS1=$(iocsh_ps1 "${iocsh_id}")
REQUIRE_IOC=$(require_ioc "${iocsh_id}")
#
# Default Initial Startup file for REQUIRE and minimal environment
# Create TMP_PATH path in order to keep tmp files secure until
# an IOC will be closed.
mkdir -p "${TMP_PATH}"
IOC_STARTUP=$(mktemp -p "${TMP_PATH}" -q --suffix=_iocsh_${SC_VERSION}) || die 1 "${SC_SCRIPTNAME} CANNOT create the startup file, please check the disk space"
# EPICS_DRIVER_PATH defined in iocsh and startup.script_common
# Remember, driver is equal to module, so EPICS_DRIVER_PATH is the module directory
# In our jargon. It is the same as ${EPICS_MODULES}
print_iocsh_header
# shellcheck disable=SC2064
trap "softIoc_end ${IOC_STARTUP}" EXIT HUP INT TERM
{
printIocEnv
print_header_line "IOC startup commands"
printf "# // Set REQUIRE_IOC for its internal PVs\n"
printf "epicsEnvSet REQUIRE_IOC \"%s\"\n" "${REQUIRE_IOC}"
printf "#\n"
printf "# // Set E3_IOCSH_TOP for the absolute path where %s is executed.\n" "${SC_SCRIPTNAME}"
printf "epicsEnvSet E3_IOCSH_TOP \"%s\"\n" "${IOCSH_TOP}"
printf "#\n"
loadRequire
setPaths "$@"
loadFiles "$@"
printf "# // Set the IOC Prompt String\n"
printf "epicsEnvSet IOCSH_PS1 \"%s\"\n" "$IOCSH_PS1"
printf "#\n"
if [ "$REALTIME" == "RT" ]; then
printf "# Real Time \"%s\"\n" "$REALTIME"
fi
if [ "$init" != NO ]; then
printf "iocInit\n"
fi
} >"${IOC_STARTUP}"
ulimit -c unlimited
if [ "$REALTIME" == "RT" ]; then
export LD_BIND_NOW=1
printf "## \n"
printf "## Better support for Real-Time IOC Application.\n"
printf "## Now we set 'export LD_BIND_NOW=%s'\n" "$LD_BIND_NOW"
printf "## If one may meet the 'Operation not permitted' message, \n"
printf "## please run %s without the real-time option\n" "$SC_SCRIPTNAME"
printf "##\n"
fi
# shellcheck disable=SC2086
${__LOADER__}${EPICS_BASE}/bin/${EPICS_HOST_ARCH}/softIocPVA -D ${EPICS_BASE}/dbd/softIocPVA.dbd "${IOC_STARTUP}" 2>&1
#! /usr/bin/env python3
"""Entry point for the IOC shell."""
import argparse
import os
import logging
from pathlib import Path
import subprocess
import sys
import re
from typing import List
from iocsh_utils import verify_environment_and_return_require_version
from iocsh_utils import TemporaryStartupScript
from iocsh_utils import generate_banner
def iocsh(
file: Path,
module: List[str],
command: List[str],
database: List[Path],
cell_path: List[Path],
no_init: bool,
realtime: bool,
gdb: bool,
valgrind: str,
debug: bool,
):
with TemporaryStartupScript() as tmp_startup_script:
require_version = verify_environment_and_return_require_version()
epics_base_dir = Path(os.environ["EPICS_BASE"])
print(generate_banner())
print(f"Starting e3 IOC shell version {require_version}")
try:
iocname = os.environ["IOCNAME"]
except KeyError:
logging.warning("Environment variable IOCNAME is not set.")
else:
logging.info(f"IOCNAME is set to {iocname}")
logging.debug(f"PID for iocsh {os.getpid()}")
logging.debug(f"Script path is {Path(__file__).resolve()}")
logging.debug(f"Executed from {Path.cwd()}")
logging.debug(f"Temporary startup script at {tmp_startup_script.name}")
if cell_path:
cell_paths = ":".join(
[
str(
p.resolve() / epics_base_dir.name / f"require-{require_version}"
)
for p in reversed(cell_path)
]
)
tmp_startup_script.set_variable(
"EPICS_DRIVER_PATH", f"{cell_paths}:{os.environ['EPICS_DRIVER_PATH']}"
)
if debug:
tmp_startup_script.add_command("var requireDebug 1")
for entry in module:
tmp_startup_script.load_module(entry)
if file:
tmp_startup_script.set_variable("E3_CMD_TOP", {str(file.parent)})
tmp_startup_script.load_snippet(file)
for entry in command:
tmp_startup_script.add_command(entry)
for entry in database:
tmp_startup_script.load_database(entry)
if not no_init:
# if file isn't passed we shouldn't try to read its' content
if not file or re.search(r"^\s*iocInit\b", file.read_text()) is None:
tmp_startup_script.add_command("iocInit")
tmp_startup_script.save()
cmd = [
str(epics_base_dir / "bin" / os.environ["EPICS_HOST_ARCH"] / "softIocPVA"),
"-D",
str(epics_base_dir / "dbd" / "softIocPVA.dbd"),
tmp_startup_script.name,
]
if gdb:
cmd = ["gdb", "--args", cmd]
elif valgrind:
cmd = ["valgrind"] + valgrind.split(" ") + cmd
elif realtime:
os.environ["LD_BIND_NOW"] = 1
cmd = "chrt --fifo 1".split(" ") + cmd
logging.debug(f"Running command `{' '.join(cmd)}`")
try:
subprocess.run(cmd, check=True)
except (KeyboardInterrupt, subprocess.CalledProcessError):
sys.exit(-1)
def generate_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="IOC shell for e3",
description="ESS EPICS environment (e3) wrapper for softIocPVA",
)
# How this is set up has a funny but sort of good side-effect:
# if no environment is sourced the IOC shell will refuse to start in the first place
# even if you try to execute something like `iocsh -h`
parser.add_argument(
"-V",
"--version",
action="version",
version="%(prog)s version {_version}".format(
_version=verify_environment_and_return_require_version()
),
help="print version and exit",
)
parser.add_argument(
"file", type=Path, nargs="?", default=None, help="path to startup script"
)
parser.add_argument(
"-r",
"--require",
dest="module",
action="append",
default=list(),
help="load module(s), optionally with version using the syntax `module,version`",
)
parser.add_argument(
"-c", "--command", action="append", default=list(), help="execute command(s)"
)
parser.add_argument(
"-d",
"--database",
action="append",
default=list(),
type=Path,
help="load database file(s) (`dbLoadRecords`)",
)
parser.add_argument(
"-l",
"--cell-path",
type=Path,
action="append",
default=list(),
metavar="PATH",
help="path(s) to cell(s)",
)
mutex_group_modifiers = parser.add_mutually_exclusive_group()
mutex_group_modifiers.add_argument("-rt", "--realtime", action="store_true")
mutex_group_modifiers.add_argument(
"-dg",
"--gdb",
action="store_true",
help="run with gdb",
)
mutex_group_modifiers.add_argument(
"-dv",
"--valgrind",
nargs="?",
const="--leak-check=full",
metavar="ARGUMENT",
help="run with valgrind",
)
mutex_group_modifiers.add_argument(
"--debug",
action="store_true",
)
parser.add_argument("-i", "--no-init", action="store_true")
return parser
if __name__ == "__main__":
logging.basicConfig(format="%(levelname)s: %(message)s ", level=logging.DEBUG)
parser = generate_parser()
args = parser.parse_args()
iocsh(**vars(args))
#!/usr/bin/env bash
# shellcheck disable=SC2034
# -*- mode: sh -*-
#
# Copyright (c) 2004 - 2017 Paul Scherrer Institute
# Copyright (c) 2017 - 2021 European Spallation Source ERIC
#
# The program is free software: you can redistribute
# it and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 2 of the
# License, or any newer version.
#
# 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see https://www.gnu.org/licenses/gpl-2.0.txt
#
#
# PSI original iocsh author : Dirk Zimoch
# ESS author : Jeong Han Lee
# ESS maintainer : Simon Rose
# email : simon.rose@ess.eu
# date : 2021-12-10
#
REALTIME=
__LOADER__=
# Usage :
# e3_version="$(read_file_get_string "${file_name}" "E3_VERSION:=")";
function read_file_get_string {
local FILENAME=$1
local PREFIX=$2
sed -n "s/^$PREFIX\(.*\)/\1/p" "$FILENAME"
}
# Base Code is defined with 8 digits numbers
# Two digits are enough to cover all others (I think)
# 7.0.6.1 : 07000601
#
# First Two : 00 (EPICS VERSION)
# Second Two : 00 (EPICS_REVISION)
# Third Two : 00 (EPICS_MODIFICATION)
# Fourth Two : 00 (EPICS_PATCH_LEVEL)
function basecode_generator() { #@ Generator BASECODE
#@ USAGE: BASECODE=$(basecode_generator)
local epics_ver_maj epics_ver_mid epics_ver_min epics_ver_patch
epics_ver_maj="$(read_file_get_string "${EPICS_BASE}/configure/CONFIG_BASE_VERSION" "EPICS_VERSION = ")"
epics_ver_mid="$(read_file_get_string "${EPICS_BASE}/configure/CONFIG_BASE_VERSION" "EPICS_REVISION = ")"
epics_ver_min="$(read_file_get_string "${EPICS_BASE}/configure/CONFIG_BASE_VERSION" "EPICS_MODIFICATION = ")"
epics_ver_patch="$(read_file_get_string "${EPICS_BASE}/configure/CONFIG_BASE_VERSION" "EPICS_PATCH_LEVEL = ")"
local base_code=""
if [[ ${#epics_ver_maj} -lt 2 ]]; then
epics_ver_maj="00${epics_ver_maj}"
epics_ver_maj="${epics_ver_maj: -2}"
fi
if [[ ${#epics_ver_mid} -lt 2 ]]; then
epics_ver_mid="00${epics_ver_mid}"
epics_ver_mid="${epics_ver_mid: -2}"
fi
if [[ ${#epics_ver_min} -lt 2 ]]; then
epics_ver_min="00${epics_ver_min}"
epics_ver_min="${epics_ver_min: -2}"
fi
if [[ ${#epics_ver_patch} -lt 2 ]]; then
epics_ver_patch="00${epics_ver_patch}"
epics_ver_patch="${epics_ver_patch: -2}"
fi
base_code=${epics_ver_maj}${epics_ver_mid}${epics_ver_min}${epics_ver_patch}
echo "$base_code"
}
function version() {
printf "%s : %s%s\n" "European Spallation Source ERIC" "$SC_SCRIPTNAME" ${SC_VERSION:+" ($SC_VERSION)"} >&2
exit
}
function print_iocsh_header() {
printf "███████╗██████╗ ██╗ ██████╗ ██████╗ ███████╗██╗ ██╗███████╗██╗ ██╗ \n"
printf "██╔════╝╚════██╗ ██║██╔═══██╗██╔════╝ ██╔════╝██║ ██║██╔════╝██║ ██║ \n"
printf "█████╗ █████╔╝ ██║██║ ██║██║ ███████╗███████║█████╗ ██║ ██║ \n"
printf "██╔══╝ ╚═══██╗ ██║██║ ██║██║ ╚════██║██╔══██║██╔══╝ ██║ ██║ \n"
printf "███████╗██████╔╝ ██║╚██████╔╝╚██████╗ ███████║██║ ██║███████╗███████╗███████╗\n"
printf "╚══════╝╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝\n"
printf "\n"
}
function print_header_line() {
printf "############################################################################\n"
printf "## %s\n" "${1}"
printf "############################################################################\n"
}
function print_vars() {
print_header_line "$1"
shift
for var in "$@"; do
printf "# %s = \"%s\"\n" "${var}" "${!var}"
done
printf "#\n"
}
function printIocEnv() {
printf "# Start at \"%s\"\n" "$(date +%Y-W%V-%b%d-%H%M-%S-%Z)"
printf "# %s : %s%s\n" "European Spallation Source ERIC" "$SC_SCRIPTNAME" ${SC_VERSION:+" ($SC_VERSION)"}
printf "#\n"
print_vars "Shell and environment variables" PWD USER LOGNAME PATH
print_vars "EPICS variables" EPICS_BASE EPICS_HOST_ARCH EPICS_DRIVER_PATH EPICS_CA_AUTO_ADDR_LIST EPICS_CA_ADDR_LIST EPICS_PVA_AUTO_ADDR_LIST EPICS_PVA_ADDR_LIST
print_vars "e3-specific variables" E3_REQUIRE_VERSION E3_REQUIRE_LOCATION E3_REQUIRE_BIN E3_REQUIRE_DB E3_REQUIRE_DBD E3_REQUIRE_INC E3_REQUIRE_LIB
}
# Ctrl+c : OK
# exit : OK
# kill softioc process : OK
# kill main process : Enter twice in terminal,
# close softIoc, but STARTUP file is remained.
#
function softIoc_end() {
local startup_file=$1
rm -f "${startup_file}"
# only clean terminal when stdout is opened on a terminal
# avoid "stty: standard input: Inappropriate ioctl for device" otherwise
[[ -t 1 ]] && stty sane
exit
}
function die() { #@ Print error message and exit with error code
#@ USAGE: die [errno [message]]
error=${1:-1}
## exits with 1 if error number not given
shift
[ -n "$*" ] &&
printf "%s%s: %s\n" "$SC_SCRIPTNAME" ${SC_VERSION:+" ($SC_VERSION)"} "$*" >&2
exit "$error"
}
function iocsh_ps1() {
local iocsh_ps1=""
local pid="$1"
# If IOCNAME is not set use pid instead
if [ -z "${IOCNAME}" ]; then
iocsh_ps1+=${pid}
else
iocsh_ps1+="${IOCNAME}"
fi
iocsh_ps1+=" > "
echo "${iocsh_ps1}"
}
# Please look at the limitation in require.c in registerModule()
# /*
# Require DB has the following four PVs:
# - $(REQUIRE_IOC):$(MODULE)_VER
# - $(REQUIRE_IOC):MOD_VER
# - $(REQUIRE_IOC):VERSIONS
# - $(REQUIRE_IOC):MODULES
# We reserved 30 chars for :$(MODULE)_VER, so MODULE has the maximum 24 chars.
# And we've reserved for 30 chars for $(REQUIRE_IOC).
# So, the whole PV and record name in moduleversion.template has 59 + 1.
# */
function require_ioc() {
# e3-ioc-hash-hostname-pid fails when host has icslab-ser03 and IOCUSER-VIRTUALBOX
# so better to keep simple in case when hostname is long.
# And it has the limitation of PV length
# #define PVNAME_STRINGSZ 61 in EPICS_BASE/include/dbDefs.h
local require_ioc=""
# Test if IOCNAME is defined
if [ -z "${IOCNAME}" ]; then
local pid="$1"
# Keep only short hostname (without domain)
local hostname=${HOSTNAME%%.*}
# Record name should not have . character, because it is used inside record
require_ioc="REQMOD" # char 6 ( 6)
require_ioc+=":" # char 1 ( 7)
require_ioc+=${hostname:0:15} # char 15 (22)
require_ioc+="-" # char 1 (23)
require_ioc+=${pid} # char 7 (30), max pid in 64 bit 4194304 (7),
else
require_ioc=${IOCNAME:0:30}
fi
echo "${require_ioc}"
}
function loadRequire() {
local libPrefix=lib
local libPostfix=.so
local libName=${libPrefix}${E3_REQUIRE_NAME}${libPostfix}
local require_lib=${E3_REQUIRE_LIB}/${EPICS_HOST_ARCH}/${libName}
local require_dbd=${E3_REQUIRE_DBD}/${E3_REQUIRE_NAME}.dbd
printf "# // Load %s module, version %s\n" "${E3_REQUIRE_NAME}" "${E3_REQUIRE_VERSION}"
printf "#\n"
printf "dlload %s\n" "${require_lib}"
printf "dbLoadDatabase %s\n" "${require_dbd}"
printf "%s_registerRecordDeviceDriver\n\n" "${E3_REQUIRE_NAME%-*}"
printf "# \n"
}
function check_mandatory_env_settings() {
declare -a var_list=()
var_list+=(EPICS_HOST_ARCH)
var_list+=(EPICS_BASE)
var_list+=(E3_REQUIRE_NAME)
var_list+=(E3_REQUIRE_BIN)
var_list+=(E3_REQUIRE_LIB)
var_list+=(E3_REQUIRE_DB)
var_list+=(E3_REQUIRE_DBD)
var_list+=(E3_REQUIRE_VERSION)
for var in "${var_list[@]}"; do
if [[ -z "${!var}" ]]; then
die 1 " $var is not defined!. Please source ${E3_REQUIRE_BIN}/activate"
fi
done
if [[ -z "$IOCNAME" ]]; then
echo "Warning: environment variable IOCNAME is not set." >&2
else
echo "IOCNAME is set to $IOCNAME"
fi
}
function setPaths() {
while [ "$#" -gt 0 ]; do
arg="$1"
case $arg in
-l)
shift
add_path="$1/$(basename "${EPICS_BASE}")/require-${E3_REQUIRE_VERSION}"
printf "epicsEnvSet EPICS_DRIVER_PATH %s:${EPICS_DRIVER_PATH}\n" "$add_path"
EPICS_DRIVER_PATH="$add_path:$EPICS_DRIVER_PATH"
;;
esac
shift
done
}
function loadFiles() {
while [ "$#" -gt 0 ]; do
arg=$1
case $arg in
-rt | -RT | -realtime | --realtime)
REALTIME="RT"
__LOADER__="chrt --fifo 1 "
;;
@*)
loadFiles "$(cat "${arg#@}")"
;;
*=*)
echo -n "$arg" | awk -F '=' '{printf "epicsEnvSet %s '\''%s'\''\n" $1 $2}'
;;
-c)
shift
case $1 in
seq*)
if [ "$init" != NO ]; then
echo "iocInit"
init=NO
fi
;;
iocInit)
init=NO
;;
esac
echo "$1"
;;
-s)
shift
if [ "$init" != NO ]; then
echo "iocInit"
init=NO
fi
echo "seq $1"
;;
-i | -noinit | --noinit)
init=NO
;;
-r)
shift
echo "require $1"
;;
-l) # This is taken care of in setPaths()
shift
;;
-dg)
if [[ -n "${2%--dgarg=*}" ]]; then
__LOADER__="gdb --eval-command run --args "
else
shift
if [[ -z "${1#*=}" ]]; then
__LOADER__="gdb "
else
__LOADER__="gdb ${1#*=} "
fi
fi
;;
-dv)
if [[ -n "${2%--dvarg=*}" ]]; then
__LOADER__="valgrind --leak-check=full "
else
shift
if [[ -z "${1#*=}" ]]; then
__LOADER__="valgrind "
else
__LOADER__="valgrind ${1#*=} "
fi
fi
;;
-n)
__LOADER__="nice --10 "
shift
;;
-*)
printf "Unknown option %s\n\n" "$1" >&2
help
;;
*.so)
echo "dlload \"$arg\""
;;
*)
subst=""
while [ "$#" -gt 1 ]; do
case $2 in
*=*)
subst="$subst,$2"
shift
;;
*)
break
;;
esac
done
subst=${subst#,}
case $arg in
*.db | *.template)
echo "dbLoadRecords '$arg','$subst'"
;;
*.subs | *.subst)
echo "dbLoadTemplate '$arg','$subst'"
;;
*.dbd)
# some dbd files must be loaded before main to take effect
echo "dbLoadDatabase '$arg','$DBD','$subst'"
;;
*)
set_e3_cmd_top "$arg"
echo "iocshLoad '$arg','$subst'"
# Search for any instance of iocInit at the start of the line.
# If found, do not add the iocInit to the startup script. Any
# other occurrence of iocInit (e.g. in comments) is not matched
# and the script will add an active iocInit.
if grep -q "^\s*iocInit\b" "$arg"; then
init=NO
fi
;;
esac
;;
esac
shift
done
}
function set_e3_cmd_top() {
local file=$1
local file_path=""
local file_top=""
local file_name=""
if [ -f "$file" ]; then
file_path="$(readlink -e "$file")"
file_top="${file_path%/*}"
file_name=${file##*/}
printf "# Set E3_CMD_TOP for the absolute path where %s exists\n" "$file_name"
printf "epicsEnvSet E3_CMD_TOP \"%s\"\n" "$file_top"
printf "#\n"
fi
}
function help() {
{
printf "\n"
printf "USAGE: iocsh [startup files]\n"
printf "\n"
printf "Start the ESS iocsh and load startup scripts.\n\n"
printf "Options:\n\n"
printf " -?, -h, --help Show this page and exit.\n"
printf " -v, --version Show version and exit.\n"
printf " -rt Execute in realtime mode.\n"
printf " (Also -RT, -realtime, --realtime)\n"
printf " -c 'cmd args' Ioc shell command.\n"
printf " -s 'prog m=v' Sequencer program (and arguments), run with 'seq'.\n"
printf " This forces an 'iocInit' before running the program.\n"
printf " -i Do not add iocInit. This option does not override\n"
printf " a valid iocInit in the startup script.\n"
printf " (Also -noinit, --noinit)\n"
printf " -r module[,ver] Module (optionally with version) loaded via 'require'.\n"
printf " -l 'cell path' Run Ioc with a cell path.\n"
printf " -dg [--dgarg='gdb-options'] Run with debugger gdb with user selected options or default option.\n"
printf " -dv [--dvarg='valgrind-options'] Run with valgrind with user selected options or default option.\n"
printf " -n Run with 'nice --10' (requires sudo).\n"
printf " @file More arguments are read from file.\n\n"
printf "Supported filetypes:\n\n"
printf " *.db, *.dbt, *.template loaded via 'dbLoadRecords'\n"
printf " *.subs, *.subst loaded via 'dbLoadTemplate'\n"
printf " *.dbd loaded via 'dbLoadDatabase'\n"
printf " *.so loaded via 'dlload'\n"
printf "\n"
printf "All other files are executed as startup scripts by the EPICS shell.\n"
printf "After a file you can specify substitutions like m1=v1 m2=v1 for that file.\n\n"
printf "Examples:\n"
printf " iocsh st.cmd\n"
printf " iocsh my_database.template P=XY M=3\n"
printf " iocsh -r my_module,version -c 'initModule()'\n"
printf " iocsh -c 'var requireDebug 1' st.cmd\n"
printf " iocsh -i st.cmd\n"
printf " iocsh -dv --dvarg='--vgdb=full'\n"
printf " iocsh -dv st.cmd\n\n"
} >&2
exit
}
for arg in "$@"; do
case $arg in
-h | "-?" | -help | --help)
help
;;
-v | -ver | --ver | -version | --version)
version
;;
*) ;;
esac
done
"""Utilities for iocsh."""
import atexit
import os
import logging
import socket
import sys
from tempfile import NamedTemporaryFile
from pathlib import Path
@atexit.register
def graceful_shutdown() -> None:
print("\nExiting e3 IOC shell")
os.system("stty sane")
class TemporaryStartupScript:
"""Class to manage IOC shell commands.
Holds on to commands in a buffer which it writes to file.
"""
def __init__(self) -> None:
self.file = NamedTemporaryFile(delete=False)
self.command_buffer = []
self._saved = False
self.set_variable("REQUIRE_IOC", generate_prefix())
self.set_variable("IOCSH_TOP", Path.cwd())
self.set_variable("IOCSH_PS1", generate_prompt())
# load require
self.add_command(
f"dlload {str(Path(os.environ['E3_REQUIRE_LIB']) / os.environ['EPICS_HOST_ARCH'] / 'librequire.so')}"
)
self.add_command(
f"dbLoadDatabase {str(Path(os.environ['E3_REQUIRE_DBD']) / 'require.dbd')}"
)
self.add_command("require_registerRecordDeviceDriver")
@property
def name(self) -> str:
return self.file.name
def __enter__(self) -> None:
return self
def __exit__(self, *args) -> None:
self.file.close()
def _get_content(self) -> str:
"""Return list of commands as multi-line string."""
return "\n".join(self.command_buffer) + "\n" # ensure newline at EOF
def save(self) -> None:
"""Store command 'buffer' to file."""
if self._saved:
raise RuntimeError("File has already been saved")
self.file.write(self._get_content().encode())
self.file.flush()
self._saved = True
def set_variable(self, var: str, val: str) -> None:
self.add_command(
f'epicsEnvSet {var} "{val}"'
) # add quotation marks to avoid symbols being interpreted
def load_snippet(self, name: str) -> None:
self.add_command(f"iocshLoad {name}")
def load_module(self, name: str) -> None:
self.add_command(f"require {name}")
def load_database(self, name: str) -> None:
self.add_command(f"dbLoadRecords {name}")
def add_command(self, command: str) -> None:
self.command_buffer.append(command)
def verify_environment_and_return_require_version() -> str:
"""Verify select known EPICS and e3 variables and return current require version."""
def check_mandatory_env_vars() -> None:
mandatory_vars = (
"EPICS_HOST_ARCH",
"EPICS_BASE",
"E3_REQUIRE_VERSION",
"E3_REQUIRE_NAME",
"E3_REQUIRE_BIN",
"E3_REQUIRE_LIB",
"E3_REQUIRE_DB",
"E3_REQUIRE_DBD",
)
for var in mandatory_vars:
_ = os.environ[var]
try:
check_mandatory_env_vars()
except KeyError as e:
logging.debug(f"Environment variable {e} is not set")
sys.exit("Please source an environment before you try to use the IOC shell")
# compare path of this script to sourced environment's executables
if not str(Path(__file__).resolve().parent) == os.environ["E3_REQUIRE_BIN"]:
logging.debug(
f"Sourced environment is '{os.environ['E3_REQUIRE_BIN']}' and this script is from '{Path(__file__).resolve().parent}'"
)
sys.exit(
"You have sourced a different environment than what this IOC shell is from"
)
return os.environ["E3_REQUIRE_VERSION"]
def extract_require_version() -> str:
"""Return loaded environment's version of require."""
try:
return os.environ["E3_REQUIRE_VERSION"]
except KeyError:
sys.exit("Please source an environment before you try to use the IOC shell")
def generate_prompt(separator: str = " > ") -> str:
"""Return IOC shell prompt."""
try:
prompt = os.environ["IOCNAME"]
except KeyError:
fqdn = socket.gethostname()
hostname, *_ = fqdn.partition(".")
prompt = f"{hostname}-{os.getpid()}"
return f"{prompt}{separator}"
def generate_prefix(prefix: str = "TEST:") -> str:
"""Return fallback IOC-name."""
try:
iocname = os.environ["IOCNAME"]
except KeyError:
try:
user = os.getlogin()
except OSError: # for wonky cases
user = "UNKNOWN"
iocname = f"{prefix}{user}-{os.getpid()}"
return iocname
def generate_banner() -> str:
"""Return ascii art banner."""
ascii_art = """
,----. ,--. ,-----. ,-----. ,--. ,--.,--.
,---. '.-. | | |' .-. '' .--./ ,---. | ,---. ,---. | || |
| .-. : .' < | || | | || | ( .-' | .-. || .-. :| || |
\ --./'-' | | |' '-' '' '--'\ .-' `)| | | |\ --.| || |
`----'`----' `--' `-----' `-----' `----' `--' `--' `----'`--'`--'
"""
return ascii_art
import os
import pytest
from .utils import Wrapper
......@@ -58,7 +60,7 @@ def test_debug_arch_is_not_used_when_unspecified(wrapper: Wrapper):
assert rc == 0
# Note that output from run-iocsh goes to stderr
assert f'EPICS_HOST_ARCH = "{wrapper.host_arch}"' in errs
assert os.environ["EPICS_HOST_ARCH"] == wrapper.host_arch
def test_debug_arch_is_used_when_specified(wrapper: Wrapper):
......@@ -70,5 +72,4 @@ def test_debug_arch_is_used_when_specified(wrapper: Wrapper):
rc, _, errs = wrapper.run_make("test", EPICS_HOST_ARCH=debug_arch)
assert rc == 0
# Note that output from run-iocsh goes to stderr
assert f'EPICS_HOST_ARCH = "{debug_arch}"' in errs
assert os.environ["EPICS_HOST_ARCH"] == debug_arch
......@@ -49,7 +49,7 @@ def test_version(wrapper: Wrapper, requested, expected, installed):
)
assert returncode == 0
rc, stdout, _ = run_ioc_get_output(
rc, stdout, stderr = run_ioc_get_output(
module=wrapper.name, version=requested, cell_path=wrapper.path / "cellMods"
)
......@@ -65,7 +65,7 @@ def test_version(wrapper: Wrapper, requested, expected, installed):
RE_MODULE_NOT_LOADED.format(
module=wrapper.name, required=re.escape(requested)
),
stdout,
stderr,
)
assert match
assert rc != 0
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment