echo6-docs/runbooks/binary-wrapper-interception.md

279 lines
8.3 KiB
Markdown
Raw Normal View History

# Binary Wrapper Interception
Transparently intercept a CLI binary with a wrapper script that adds pre-flight logic (routing, validation, logging) before exec-ing the real binary. The caller — whether a service, cron job, or another script — never knows the difference.
Use this when you need to modify the behavior of a tool that's called by a system you don't control (e.g., a runner, scheduler, or third-party service), without changing the caller's config or code.
---
## Prerequisites
- The binary to intercept is installed and working
- You have root/sudo access on the target machine
- The caller invokes the binary by absolute path or via PATH lookup
---
## Inputs
Prompt the user for all of these before executing:
```
TARGET_HOST= # SSH alias or IP (e.g., cortex). Use "localhost" if local.
BINARY_NAME= # Name of the binary to intercept (e.g., "whisper-ctranslate2")
BINARY_PATH= # Full path to the binary (e.g., "/usr/local/bin/whisper-ctranslate2")
WRAPPER_NAME= # Name for the wrapper script (e.g., "whisper-smart")
WRAPPER_DIR= # Directory for the wrapper (e.g., "/usr/local/bin")
REAL_SUFFIX= # Suffix for the renamed real binary (default: "-real")
```
---
## Step 1: Locate the Real Binary
Find the actual binary or symlink that will be intercepted.
```bash
ssh $TARGET_HOST "ls -la $BINARY_PATH && file $BINARY_PATH"
```
If it's already a symlink, follow it to the real target:
```bash
ssh $TARGET_HOST "readlink -f $BINARY_PATH"
```
### Gate
Must return a valid file. Record the real binary location — you'll need it for the rename.
---
## Step 2: Rename the Real Binary
Move the original binary out of the way so the wrapper can take its place.
```bash
ssh $TARGET_HOST "sudo mv $BINARY_PATH ${BINARY_PATH}${REAL_SUFFIX}"
```
If the original was a symlink (e.g., pip-installed Python tool):
```bash
# Preserve the symlink target
REAL_TARGET=$(ssh $TARGET_HOST "readlink -f $BINARY_PATH")
ssh $TARGET_HOST "sudo rm $BINARY_PATH && sudo ln -s $REAL_TARGET ${BINARY_PATH}${REAL_SUFFIX}"
```
### Gate
```bash
ssh $TARGET_HOST "ls -la ${BINARY_PATH}${REAL_SUFFIX}"
ssh $TARGET_HOST "${BINARY_PATH}${REAL_SUFFIX} --version 2>/dev/null || ${BINARY_PATH}${REAL_SUFFIX} --help 2>/dev/null | head -1"
```
The renamed binary must exist and be executable. If not, undo immediately:
```bash
ssh $TARGET_HOST "sudo mv ${BINARY_PATH}${REAL_SUFFIX} $BINARY_PATH"
```
---
## Step 3: Write the Wrapper Script
Create the wrapper at `$WRAPPER_DIR/$WRAPPER_NAME`. The wrapper must:
1. Accept all original arguments (`$@`)
2. Perform pre-flight logic (inspection, routing, logging)
3. `exec` the real binary with (possibly modified) arguments
4. Never silently swallow errors — if pre-flight fails, exit with a meaningful code
Template:
```bash
#!/bin/bash
# Wrapper for $BINARY_NAME — transparently intercepts calls
# Real binary at: ${BINARY_PATH}${REAL_SUFFIX}
LOGFILE="/tmp/${WRAPPER_NAME}.log"
# ──── Pre-flight logic ────
# Add your inspection, routing, or validation here.
# Example: inspect input files, check resource availability, choose parameters.
# Parse arguments to find relevant inputs (file paths, flags, etc.)
# This section is use-case specific.
# ──── Logging ────
echo "[WRAPPER] $(date) args: $@" >> "$LOGFILE"
# ──── Execute real binary ────
# Use exec to replace this process — caller sees the real binary's exit code,
# stdout, stderr, and signal handling as if wrapper didn't exist.
exec ${BINARY_PATH}${REAL_SUFFIX} "$@"
```
Deploy the wrapper:
```bash
ssh $TARGET_HOST "sudo tee $WRAPPER_DIR/$WRAPPER_NAME > /dev/null << 'WRAPPER'
<paste wrapper script here>
WRAPPER
sudo chmod +x $WRAPPER_DIR/$WRAPPER_NAME"
```
### Gate
```bash
ssh $TARGET_HOST "ls -la $WRAPPER_DIR/$WRAPPER_NAME && head -1 $WRAPPER_DIR/$WRAPPER_NAME"
```
Must show executable permissions and `#!/bin/bash` shebang.
---
## Step 4: Install the Symlink
Replace the original binary path with a symlink to the wrapper.
```bash
ssh $TARGET_HOST "sudo ln -sf $WRAPPER_DIR/$WRAPPER_NAME $BINARY_PATH"
```
### Gate
```bash
ssh $TARGET_HOST "ls -la $BINARY_PATH"
```
Must show: `$BINARY_PATH -> $WRAPPER_DIR/$WRAPPER_NAME`
Verify the full chain:
```bash
ssh $TARGET_HOST "ls -la $BINARY_PATH && ls -la ${BINARY_PATH}${REAL_SUFFIX}"
```
Should show:
```
BINARY_PATH -> WRAPPER_DIR/WRAPPER_NAME (wrapper)
BINARY_PATH-real -> /path/to/actual/binary (real binary)
```
---
## Step 5: Test the Interception
Run the binary as the caller would. The wrapper should intercept transparently.
```bash
# Direct invocation
ssh $TARGET_HOST "$BINARY_PATH --version"
# Check wrapper log
ssh $TARGET_HOST "tail -5 /tmp/${WRAPPER_NAME}.log"
```
The `--version` output should come from the real binary. The log should show the wrapper fired.
Test with actual workload arguments:
```bash
ssh $TARGET_HOST "$BINARY_PATH <typical args here>"
ssh $TARGET_HOST "tail -1 /tmp/${WRAPPER_NAME}.log"
```
### Gate
Both must succeed. If the binary fails or produces different output than before, the wrapper has a bug — check argument passing (quoting, `$@` vs `$*`).
---
## Step 6: Verify Service Integration
If the binary is called by a service (systemd, cron, etc.), restart that service and confirm it picks up the wrapper.
```bash
ssh $TARGET_HOST "sudo systemctl restart <service-name>"
ssh $TARGET_HOST "sleep 5 && tail -5 /tmp/${WRAPPER_NAME}.log"
```
The log should show entries from the service's invocations, not just your manual tests.
---
## Rollback
To remove the wrapper and restore the original binary:
```bash
ssh $TARGET_HOST "sudo rm $BINARY_PATH && sudo mv ${BINARY_PATH}${REAL_SUFFIX} $BINARY_PATH"
# Or if the original was a symlink:
ssh $TARGET_HOST "sudo rm $BINARY_PATH && sudo ln -s <original-target> $BINARY_PATH"
```
No service restart needed — next invocation hits the real binary directly.
---
## Key Principles
1. **`exec` is mandatory.** Without `exec`, the wrapper runs the binary as a child process, which breaks signal handling (SIGTERM won't reach the real binary) and doubles PID usage. `exec` replaces the wrapper process entirely.
2. **Use `"$@"` not `$@` or `$*`.** Quoted `"$@"` preserves argument boundaries. Unquoted `$@` splits arguments with spaces. `$*` merges all arguments into one string.
3. **Appended flags override earlier ones.** Many CLI tools (argparse, getopt) use last-value-wins for duplicate flags. The wrapper can append `--flag value` after `"$@"` to force overrides without removing the caller's original flags.
4. **Exit codes matter.** If pre-flight fails, exit with a non-zero code that the caller understands. Some callers retry on specific exit codes (e.g., PeerTube runner retries on exit 1).
5. **Log to /tmp, not to the service's log directory.** The wrapper log is a debug artifact, not part of the service's data. `/tmp` is cleaned on reboot, which is fine for wrapper logs.
---
## Troubleshooting
### Wrapper not being called
Check the symlink chain: `ls -la $BINARY_PATH`. If the service uses a hardcoded absolute path that bypasses PATH, the symlink might be in the wrong location.
### Arguments with spaces break
Use `"$@"` (quoted) in the exec line, not `$@` (unquoted).
### Service fails after wrapper install
Check the wrapper's shebang (`#!/bin/bash`), permissions (`chmod +x`), and that `exec` is present. Without exec, the wrapper may exit before the binary finishes.
### Wrapper log is empty
The service might be calling a different path than expected. Check: `which $BINARY_NAME` and compare with what the service config specifies.
---
## Usage Examples
### Whisper transcription routing (PeerTube runner on cortex)
The PeerTube remote runner calls `whisper-ctranslate2` for auto-captioning. The smart wrapper intercepts this to route short videos to GPU and long videos to CPU.
```
BINARY_NAME=whisper-ctranslate2
BINARY_PATH=/usr/local/bin/whisper-ctranslate2
WRAPPER_NAME=whisper-smart
REAL_SUFFIX=-real
Symlink chain:
/usr/local/bin/whisper-ctranslate2 → /usr/local/bin/whisper-smart
/usr/local/bin/whisper-ctranslate2-real → /home/zvx/.local/bin/whisper-ctranslate2
Wrapper logic:
- ffprobe audio duration from first non-flag argument
- < 1hr → exec with --device cuda --compute_type float16
- >= 1hr → exec with --device cpu --compute_type int8
- Appends --model medium after $@ (last-value-wins override)
```
---
*Last updated: 2026-02-17*