- Fix bash arithmetic crash on MobaXterm/Cygwin: $(date +%s) was returning CR-tainted values landing in $(( )) operands - Mouse mode off by default; opt in via LARRY_MOUSE=1 or /mouse on - Comprehensive CR-safety sweep across lib/*.sh and larry.sh — every command-substitution result, file read, and user input that feeds an arithmetic context, case dispatcher, or path/header is now CR-stripped at the source New shared helper lib/cygwin-safe.sh defines three primitives: coerce_int VAL [DEFAULT] — for arithmetic / integer-test operands strip_cr VAL — for case patterns, regex tests, paths, headers read_clean VAR [PROMPT] — read -r wrapper that strips CR pre-assign Hardened call sites (14 files, 60+ patch points): - larry.sh: status-line date/tput, 3 y/N approvals, auth menu, API key - lib/oauth.sh: cmd_login + cmd_refresh date+%s captures - lib/nc-engine.sh: 5 y/N action prompts + find|wc arithmetic - lib/nc-msgs.sh: parse_time_ms (4 date sites) + meta-TSV time + MSG_COUNT - lib/nc-regression.sh: tr|wc count + hl7-diff ?-fallback arithmetic - lib/nc-smat-diff.sh: A_COUNT/B_COUNT/DIFFS_TOTAL - lib/nc-insert-protocol.sh: every awk-emitted line number → head/tail math - lib/journal.sh: _next_seq wc -l arithmetic - lib/lessons.sh: _next_id/_count + 2 y/N prompts - lib/hl7-sanitize.sh: cmd_count + clear-table y/N - lib/ssh-helper.sh: 4 local+remote wc -c integer compares - lib/nc-find.sh, lib/nc-table.sh, lib/nc-document.sh, larry-rollback.sh Reproduces the exact error Bryan hit: bash: ...: arithmetic syntax error: invalid arithmetic operator (error token is "") lib/cygwin-safe.sh added to MANIFEST so it auto-syncs on next launch. Co-Authored-By: Clover (Claude Opus 4.7) <noreply@anthropic.com>
226 lines
9.0 KiB
Bash
Executable File
226 lines
9.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# nc-engine.sh — Cloverleaf engine process control. Native v3 wrapper
|
|
# around the shipped Cloverleaf binaries — modelled on v1 `bounce`,
|
|
# `bounce_processes`, `pstop`, `start`, etc.
|
|
#
|
|
# Every action goes through the journal so it's reversible. Bounces are
|
|
# journaled as paired stop+start records; the rollback executes them in
|
|
# reverse to restore prior state (best-effort — engine state can drift).
|
|
#
|
|
# Subcommands:
|
|
# stop <thread|process> [more...] stop one or more processes/threads
|
|
# start <thread|process> [more...] start one or more
|
|
# bounce <thread|process> [more...] stop then start (atomic-ish)
|
|
# restart alias of bounce
|
|
# status quick site status via tstat (if available)
|
|
# resend-ob <thread> <file> resend a file outbound (post-xlate)
|
|
# resend-ib <thread> <file> resend a file inbound (pre-xlate)
|
|
# route-test <thread> <file> run Cloverleaf route_test for a thread
|
|
# testxlate <xlate> <xltfile> test an xlate against an xlt file
|
|
# tpstest <msgfile> <proc-args> run a TPS test
|
|
#
|
|
# Options for stop/start/bounce:
|
|
# --site SITE override $HCISITE for this call
|
|
# --confirm yes skip Y/N prompt (still journaled)
|
|
# --dry-run show the binary command but do not execute
|
|
#
|
|
# Cloverleaf binaries used (auto-discovered under $HCIROOT/bin/):
|
|
# hcienginestop hcienginerun hcienginerestart hcienginestat tstat
|
|
# hciengineroutetest hciengineenginesend ...
|
|
set -o pipefail
|
|
|
|
NC_SELF="$0"
|
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
|
JOURNAL="$LIB_DIR/journal.sh"
|
|
|
|
die() { printf 'nc-engine: %s\n' "$*" >&2; exit 1; }
|
|
warn() { printf 'nc-engine: %s\n' "$*" >&2; }
|
|
|
|
# v0.7.5: shared CR-safety primitives (coerce_int / strip_cr / read_clean).
|
|
# Source if available; otherwise inline minimal fallbacks so a partial
|
|
# install doesn't break the y/N prompts.
|
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
|
# shellcheck disable=SC1090,SC1091
|
|
. "$LIB_DIR/cygwin-safe.sh"
|
|
else
|
|
coerce_int() { local r="${1:-}" d="${2:-0}" c; c=$(printf '%s' "$r" | tr -cd '0-9'); printf '%s' "${c:-$d}"; }
|
|
strip_cr() { local v="${1:-}"; printf '%s' "${v//$'\r'/}"; }
|
|
fi
|
|
|
|
# Source journal so journaled actions can call journal_write
|
|
[ -f "$JOURNAL" ] && . "$JOURNAL" || warn "journal.sh not available — actions will not be reversible"
|
|
|
|
resolve_binary() {
|
|
local name="$1"
|
|
if command -v "$name" >/dev/null 2>&1; then command -v "$name"; return; fi
|
|
for d in "${HCIROOT:-}/bin" "${HCIROOT:-}/server/bin"; do
|
|
[ -x "$d/$name" ] && { echo "$d/$name"; return; }
|
|
done
|
|
return 1
|
|
}
|
|
|
|
journal_action() {
|
|
# Record an engine action in the journal as a synthetic "command" entry.
|
|
# We don't snapshot files (these are runtime ops, not file edits) but we
|
|
# write a manifest-style entry so larry-rollback.sh --list shows them.
|
|
local action="$1" target="$2" detail="${3:-}"
|
|
local sessdir="$LARRY_HOME/journal/${LARRY_SESSION_ID:-engine-$(date +%Y-%m-%d-%H%M%S)-$$}"
|
|
mkdir -p "$sessdir" 2>/dev/null
|
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe can suffix the count
|
|
# with \r on a CRLF terminal which then crashes the surrounding $(( + 1 )).
|
|
local _cnt; _cnt=$(coerce_int "$(find "$sessdir" -name '[0-9]*.engine' 2>/dev/null | wc -l)" 0)
|
|
local idx; idx=$(printf '%03d' $((_cnt + 1)))
|
|
local entry="$sessdir/${idx}_${action}_${target//\//_}.engine"
|
|
{
|
|
printf 'action: %s\ntarget: %s\nwhen: %s\nhost: %s\nhciroot: %s\nhcisite: %s\ndetail: %s\n' \
|
|
"$action" "$target" "$(date -Iseconds 2>/dev/null || date)" \
|
|
"$(hostname 2>/dev/null || echo unknown)" "${HCIROOT:-?}" "${HCISITE:-?}" "$detail"
|
|
} > "$entry"
|
|
# Also append to a flat engine log for quick listing
|
|
local elog="$LARRY_HOME/journal/engine-actions.tsv"
|
|
[ -f "$elog" ] || printf 'when\tsession\taction\ttarget\thciroot\thcisite\n' > "$elog"
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$(date -Iseconds 2>/dev/null || date)" \
|
|
"${LARRY_SESSION_ID:-?}" "$action" "$target" "${HCIROOT:-?}" "${HCISITE:-?}" >> "$elog"
|
|
}
|
|
|
|
run_action() {
|
|
local action="$1" target="$2"; shift 2
|
|
local site="${HCISITE:-}"
|
|
local confirm=""
|
|
local dry=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--site) shift; site="$1" ;;
|
|
--confirm) shift; confirm="$1" ;;
|
|
--dry-run) dry=1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
local binary cmd label
|
|
case "$action" in
|
|
stop) binary=$(resolve_binary hcienginestop) || die "hcienginestop not found"; cmd="$binary -p $target"; label="STOP" ;;
|
|
start) binary=$(resolve_binary hcienginerun) || die "hcienginerun not found"; cmd="$binary -p $target"; label="START" ;;
|
|
bounce|restart)
|
|
binary=$(resolve_binary hcienginerestart) \
|
|
&& cmd="$binary -p $target" && label="BOUNCE" \
|
|
|| {
|
|
# Fallback to stop + start
|
|
local sbin; sbin=$(resolve_binary hcienginestop) || die "hcienginestop+hcienginerestart both missing"
|
|
local rbin; rbin=$(resolve_binary hcienginerun) || die "hcienginerun missing"
|
|
cmd="$sbin -p $target && $rbin -p $target"
|
|
label="BOUNCE"
|
|
} ;;
|
|
*) die "unknown action: $action" ;;
|
|
esac
|
|
|
|
printf '\n%s%s%s thread/process=%s site=%s\n' "${C_YELLOW:-}" "$label" "${C_RESET:-}" "$target" "${site:-?}"
|
|
printf ' $ %s\n' "$cmd"
|
|
|
|
if [ "$dry" = "1" ]; then
|
|
printf ' [dry-run] not executed\n'
|
|
return 0
|
|
fi
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
printf ' proceed? [y/N]: '
|
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
|
ans="${ans//$'\r'/}"
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED by user"; return 1; }
|
|
fi
|
|
|
|
journal_action "$action" "$target" "$cmd"
|
|
HCISITE="$site" eval "$cmd"
|
|
local rc=$?
|
|
if [ "$rc" -eq 0 ]; then echo " ✓ ok"; else warn " exit $rc"; fi
|
|
return $rc
|
|
}
|
|
|
|
cmd_status() {
|
|
local site="${HCISITE:-}"
|
|
local binary
|
|
binary=$(resolve_binary hcienginestat) || binary=$(resolve_binary tstat) || die "no engine-status binary on PATH (looked for hcienginestat, tstat)"
|
|
HCISITE="$site" "$binary" "$@"
|
|
}
|
|
|
|
cmd_resend() {
|
|
local kind="$1" thread="$2" file="$3"; shift 3
|
|
[ -n "$thread" ] && [ -f "$file" ] || die "usage: resend-{ib,ob} <thread> <file>"
|
|
local cmd
|
|
case "$kind" in
|
|
ob) cmd="$thread resend_ob $file" ;;
|
|
ib) cmd="$thread resend_ib $file" ;;
|
|
*) die "bad resend kind: $kind" ;;
|
|
esac
|
|
printf '\nRESEND-%s thread=%s file=%s\n $ %s\n proceed? [y/N]: ' "${kind^^}" "$thread" "$file" "$cmd"
|
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
|
ans="${ans//$'\r'/}"
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
|
journal_action "resend-$kind" "$thread" "file=$file"
|
|
eval "$cmd"
|
|
}
|
|
|
|
cmd_route_test() {
|
|
local thread="$1" file="$2"
|
|
[ -n "$thread" ] && [ -f "$file" ] || die "usage: route-test <thread> <file>"
|
|
local cmd="$thread route_test $file"
|
|
printf '\nROUTE-TEST thread=%s input=%s\n $ %s\n proceed? [y/N]: ' "$thread" "$file" "$cmd"
|
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
|
ans="${ans//$'\r'/}"
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
|
journal_action "route-test" "$thread" "file=$file"
|
|
eval "$cmd"
|
|
}
|
|
|
|
cmd_testxlate() {
|
|
local xlate="$1" xltfile="$2"
|
|
[ -n "$xlate" ] && [ -f "$xltfile" ] || die "usage: testxlate <xlate> <xltfile>"
|
|
local cmd="testxlate $xlate $xltfile"
|
|
printf '\nTESTXLATE xlate=%s file=%s\n $ %s\n proceed? [y/N]: ' "$xlate" "$xltfile" "$cmd"
|
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
|
ans="${ans//$'\r'/}"
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
|
journal_action "testxlate" "$xlate" "file=$xltfile"
|
|
eval "$cmd"
|
|
}
|
|
|
|
cmd_tpstest() {
|
|
local msgfile="$1"; shift
|
|
[ -f "$msgfile" ] || die "usage: tpstest <msgfile> <proc-args...>"
|
|
local procs; procs="$*"
|
|
local cmd="tpstest $msgfile $procs"
|
|
printf '\nTPSTEST msgfile=%s procs=%s\n $ %s\n proceed? [y/N]: ' "$msgfile" "$procs" "$cmd"
|
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
|
ans="${ans//$'\r'/}"
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
|
journal_action "tpstest" "$msgfile" "procs=$procs"
|
|
eval "$cmd"
|
|
}
|
|
|
|
SUB="${1:-help}"
|
|
case "$SUB" in
|
|
stop|start|bounce|restart)
|
|
shift
|
|
[ $# -ge 1 ] || die "usage: $SUB <target> [more...] [--site SITE] [--confirm yes] [--dry-run]"
|
|
# Separate targets from flags
|
|
targets=(); flags=()
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in --*) flags+=("$1" "${2:-}"); shift 2 ;; *) targets+=("$1"); shift ;; esac
|
|
done
|
|
for t in "${targets[@]}"; do run_action "$SUB" "$t" "${flags[@]}"; done
|
|
;;
|
|
status) shift; cmd_status "$@" ;;
|
|
resend-ob) shift; cmd_resend ob "$@" ;;
|
|
resend-ib) shift; cmd_resend ib "$@" ;;
|
|
route-test) shift; cmd_route_test "$@" ;;
|
|
testxlate) shift; cmd_testxlate "$@" ;;
|
|
tpstest) shift; cmd_tpstest "$@" ;;
|
|
help|-h|--help) sed -n '2,30p' "$NC_SELF" ;;
|
|
*) die "unknown subcommand: $SUB" ;;
|
|
esac
|