Skip to content
Open
Changes from all commits
Commits
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
185 changes: 160 additions & 25 deletions call_host.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,94 @@ if [ -f "$CALL_HOST_CONFIG" ]; then
source "$CALL_HOST_CONFIG"
fi

# zsh / bash compatibility helpers
is_zsh(){
# detect the current shell process name (portable ps usage)
if command -v ps >/dev/null 2>&1; then
# get last path component if ps returns full path
p="$(ps -p $$ -o comm= 2>/dev/null | awk -F/ '{print $NF}')"
case "$p" in
zsh) return 0 ;;
bash) return 1 ;;
esac
fi

# fallback: check common names for $0 or $ZSH_NAME (login shells may have a leading dash)
case "$(basename -- "${ZSH_NAME:-$0}" 2>/dev/null)" in
zsh|-zsh) return 0 ;;
esac

return 1
}

if is_zsh; then
# export a function to the environment for child shells (zsh)
export_func(){
typeset -fx "$1" 2>/dev/null || true
}
# declare an associative array (zsh) - create a named array using eval so dynamic name works
declare_assoc(){
eval "typeset -A $1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if eval necessary here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, in zsh, typeset -A $1 would try to create an array literally named $1.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I just enter /bin/zsh, the following works as the similar test did above for bash:

TEST1=TEST2
typeset -A $TEST1
TEST2[foo]=bar
echo "${TEST2[@]}"

output:

bar

}
# get current function name in zsh (be tolerant if indices differ)
current_funcname(){
# Ensure standard zsh array indexing (1-based) regardless of user options
emulate -L zsh
# funcstack[1] is current function in zsh (1-indexed by default)
# Handle potential edge cases with fallbacks
printf '%s' "${funcstack[2]:-}"
}
# get function definition (zsh)
get_function(){
functions "$1" 2>/dev/null
}
else
# bash
export_func(){
[ -n "$1" ] || return
# shellcheck disable=SC2163
export -f "$1" 2>/dev/null || true
}
declare_assoc(){
# create named associative array in bash
declare -gA "$1"
}
current_funcname(){
# return the caller function name if available (FUNCNAME[1]), otherwise fall back to FUNCNAME[0]
if [ -n "${FUNCNAME[1]:-}" ]; then
echo "${FUNCNAME[1]}"
else
echo "${FUNCNAME[0]:-}"
fi
}
# get function definition (bash)
get_function(){
declare -f "$1" 2>/dev/null
}
fi

# portable indirect variable access (works in both bash and zsh)
getvar(){
eval "printf '%s' \"\${$1:-}\""
}

# validation
call_host_valid(){
VAR_TO_VALIDATE="$1"
# shellcheck disable=SC2076
if [[ ! " enable disable " =~ " ${!VAR_TO_VALIDATE} " ]]; then
echo "Warning: unsupported value ${!VAR_TO_VALIDATE} for $VAR_TO_VALIDATE; disabling"
eval "export $VAR_TO_VALIDATE=disable"
fi
# retrieve the value of the named variable
VARVAL="$(getvar "$VAR_TO_VALIDATE")"
# check allowed values using portable case statement
case "$VARVAL" in
enable|disable)
# valid value, do nothing
;;
*)
echo "Warning: unsupported value $VARVAL for $VAR_TO_VALIDATE; disabling"
eval "export $VAR_TO_VALIDATE=disable"
;;
esac
}
export_func call_host_valid

# default values
: ${CALL_HOST_STATUS:=enable}
Expand Down Expand Up @@ -46,18 +125,41 @@ if [ ! -d "$CALL_HOST_DIR" ]; then
echo "Warning: could not create specified dir CALL_HOST_DIR $CALL_HOST_DIR. disabling"
export CALL_HOST_STATUS=disable
fi
# ensure the pipe dir is bound
export APPTAINER_BIND=${APPTAINER_BIND}${APPTAINER_BIND:+,}${CALL_HOST_DIR}

# helper: add value to a PATH-like variable only if not already present
add_path_unique(){
# args: varname value [sep]
varname="$1"; val="$2"; sep="${3:-:}"
# retrieve current value portably
cur="$(getvar "$varname")"
# if empty, set and export
if [ -z "$cur" ]; then
eval "export $varname=\"\$val\""
return
fi
# check for whole-element match using separators to avoid substrings
case "${sep}${cur}${sep}" in
*"${sep}${val}${sep}"*)
# already present
return
;;
esac
# append with separator
eval "export $varname=\"${cur}${sep}${val}\""
}

# ensure the pipe dir is bound (use comma separator for APPTAINER_BIND)
add_path_unique APPTAINER_BIND "$CALL_HOST_DIR" ","

# enable/disable toggles
call_host_enable(){
export CALL_HOST_STATUS=enable
}
export -f call_host_enable
export_func call_host_enable
call_host_disable(){
export CALL_HOST_STATUS=disable
}
export -f call_host_disable
export_func call_host_disable
# single toggle for debug printouts
call_host_debug(){
if [ "$CALL_HOST_DEBUG" = "enable" ]; then
Expand All @@ -66,18 +168,19 @@ call_host_debug(){
export CALL_HOST_DEBUG=enable
fi
}
export -f call_host_debug
export_func call_host_debug
# helper for debug printouts
call_host_debug_print(){
if [ "$CALL_HOST_DEBUG" = "enable" ]; then
echo "$@"
fi
}
export -f call_host_debug_print
export_func call_host_debug_print

call_host_plugin_01(){
# provide htcondor-specific info in container
declare -A CONDOR_OS
# portable associative-array declaration
declare_assoc CONDOR_OS
CONDOR_OS[7]="SL7"
CONDOR_OS[8]="EL8"
CONDOR_OS[9]="EL9"
Expand All @@ -93,7 +196,7 @@ call_host_plugin_01(){
fi
fi
}
export -f call_host_plugin_01
export_func call_host_plugin_01

# concept based on https://stackoverflow.com/questions/32163955/how-to-run-shell-script-on-host-from-docker-container

Expand All @@ -113,7 +216,7 @@ listenhost(){
echo "$tmpexit" > "$3"
done
}
export -f listenhost
export_func listenhost

# creates randomly named pipe and prints the name
makepipe(){
Expand All @@ -122,7 +225,7 @@ makepipe(){
mkfifo "$PIPETMP"
echo "$PIPETMP"
}
export -f makepipe
export_func makepipe

# to be run on host before launching each apptainer session
startpipe(){
Expand All @@ -132,16 +235,19 @@ startpipe(){
# export pipes to apptainer
echo "export APPTAINERENV_HOSTPIPE=$HOSTPIPE; export APPTAINERENV_CONTPIPE=$CONTPIPE; export APPTAINERENV_EXITPIPE=$EXITPIPE"
}
export -f startpipe
export_func startpipe

# sends function to host, then listens for output, and provides exit code from function
call_host(){
# disable ctrl+c to prevent "Interrupted system call"
trap "" SIGINT
if [ "${FUNCNAME[0]}" = "call_host" ]; then

# determine caller function name in a portable way
CURFN="$(current_funcname)"
if [ "$CURFN" = "call_host" ] || [ -z "$CURFN" ]; then
FUNCTMP=
else
FUNCTMP="${FUNCNAME[0]}"
FUNCTMP="$CURFN"
fi

# extra environment settings; set every time because commands are executed on host in subshell
Expand All @@ -152,15 +258,22 @@ call_host(){
cat < "$CONTPIPE"
return "$(cat < "$EXITPIPE")"
}
export -f call_host
export_func call_host

# from https://stackoverflow.com/questions/1203583/how-do-i-rename-a-bash-function
copy_function() {
test -n "$(declare -f "$1")" || return
eval "${_/$1/$2}"
eval "export -f $2"
# portable retrieval of function source and re-definition under a new name
fnsrc="$(get_function "$1")"
if [ -z "$fnsrc" ]; then
return
fi
# replace only the first occurrence of the function name (at definition)
# Use a more portable sed pattern without \b
fnnew="$(printf '%s\n' "$fnsrc" | sed "1s/^$1 /$2 /; 1s/^$1()/$2()/")"
eval "$fnnew"
export_func "$2"
}
export -f copy_function
export_func copy_function

if [ -z "$APPTAINER_ORIG" ]; then
export APPTAINER_ORIG=$(which apptainer)
Expand Down Expand Up @@ -198,11 +311,33 @@ apptainer(){
)
fi
}
export -f apptainer
export_func apptainer

# on host: get list of condor executables
if [ -z "$APPTAINER_CONTAINER" ]; then
export APPTAINERENV_HOSTFNS=$(compgen -c | grep '^condor_\|^eos')
# define command prefixes to search for
HOSTFN_PREFIXES="condor_ eos"

# portable command list discovery:
if command -v compgen >/dev/null 2>&1; then
# bash: use compgen with grep pattern built from prefixes
GREP_PATTERN="$(echo "$HOSTFN_PREFIXES" | sed 's/ /\\|^/g' | sed 's/^/^/')"
export APPTAINERENV_HOSTFNS=$(compgen -c | grep -E "$GREP_PATTERN" | tr '\n' ' ')
else
# fallback: scan PATH for matching executables (portable)
APPTAINERENV_HOSTFNS="$( ( IFS=:
for d in $PATH; do
[ -d "$d" ] || continue
for prefix in $HOSTFN_PREFIXES; do
# shellcheck disable=SC2231
for f in "$d"/${prefix}*; do
[ -e "$f" ] && [ -x "$f" ] && basename "$f"
done
done
done ) | sort -u | tr '\n' ' ')"
export APPTAINERENV_HOSTFNS
fi

if [ -n "$CALL_HOST_USERFNS" ]; then
export APPTAINERENV_HOSTFNS="$APPTAINERENV_HOSTFNS $CALL_HOST_USERFNS"
fi
Expand Down