diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a15496e6e1bea95f350d821afbc91ccc9b3e500..aba74f202d1accd8aa285c6c3db8c02ad5582078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/require-ess/tools/iocsh b/require-ess/tools/iocsh old mode 100644 new mode 100755 index 383006b0a3a8917c32c833b1d38c2aa05a719fb5..cbf364c285fcbb4be1721531bdabcd5555bd8c5b --- a/require-ess/tools/iocsh +++ b/require-ess/tools/iocsh @@ -1,127 +1,186 @@ -#!/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)) diff --git a/require-ess/tools/iocsh_functions.bash b/require-ess/tools/iocsh_functions.bash deleted file mode 100644 index 52594f9c4a6bf0d915cb7e58bc443c8130fcf109..0000000000000000000000000000000000000000 --- a/require-ess/tools/iocsh_functions.bash +++ /dev/null @@ -1,469 +0,0 @@ -#!/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 diff --git a/require-ess/tools/iocsh_utils.py b/require-ess/tools/iocsh_utils.py new file mode 100755 index 0000000000000000000000000000000000000000..19c11c7be01138e2f200ec0e2c0eef33082624d7 --- /dev/null +++ b/require-ess/tools/iocsh_utils.py @@ -0,0 +1,158 @@ +"""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 diff --git a/tests/test_e3.py b/tests/test_e3.py index b79327fd6921423deb1327fb8d710b7317e40ef2..94c1ebe718824941ff90d396b5e96d8a45785df9 100644 --- a/tests/test_e3.py +++ b/tests/test_e3.py @@ -1,3 +1,5 @@ +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 diff --git a/tests/test_versions.py b/tests/test_versions.py index 055684e60294de8e8b09bc5096429170e316afce..c129c87330395a26e9347f9e7379038578ce5b2f 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -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