235 lines
6.2 KiB
Bash
Executable file
235 lines
6.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# qvm-run - Execute a command in the VM workspace
|
|
#
|
|
# This script:
|
|
# 1. Ensures VM is running (auto-starts if needed)
|
|
# 2. Registers current $PWD as a workspace in workspaces.json
|
|
# 3. SSHes into VM and executes command in workspace mount point
|
|
# 4. Streams output and preserves exit code
|
|
#
|
|
# Usage: qvm-run <command> [args...]
|
|
#
|
|
# Notes:
|
|
# - Workspaces are pre-mounted at VM start time (no dynamic 9p hotplug)
|
|
# - If workspace not already registered, warns to restart VM
|
|
# - Uses workspace hash for mount tag and path
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
# Source common library
|
|
QVM_LIB_DIR="${QVM_LIB_DIR:-$(dirname "$(readlink -f "$0")")/../lib}"
|
|
# shellcheck source=lib/common.sh
|
|
source "$QVM_LIB_DIR/common.sh"
|
|
|
|
#
|
|
# show_usage - Display usage information
|
|
#
|
|
show_usage() {
|
|
cat <<EOF
|
|
Usage: qvm run <command> [args...]
|
|
|
|
Execute a command in the VM at the current workspace directory.
|
|
|
|
The current directory (\$PWD) is automatically registered as a workspace
|
|
and mounted into the VM. Commands run in the mounted workspace directory.
|
|
|
|
Examples:
|
|
qvm run cargo build
|
|
qvm run npm install
|
|
qvm run ls -la
|
|
qvm run bash -c "pwd && ls"
|
|
|
|
Notes:
|
|
- Workspaces are mounted at VM start time
|
|
- If this workspace is new, you'll need to restart the VM
|
|
- Command output streams to your terminal
|
|
- Exit code matches the command's exit code
|
|
|
|
EOF
|
|
}
|
|
|
|
#
|
|
# register_workspace - Add workspace to registry if not already present
|
|
# Args: $1 - absolute path to workspace
|
|
# $2 - workspace hash
|
|
# Returns: 0 if already registered, 1 if newly added (requires VM restart)
|
|
#
|
|
register_workspace() {
|
|
local workspace_path="$1"
|
|
local hash="$2"
|
|
local mount_tag="ws_${hash}"
|
|
local guest_path="/workspace/${hash}"
|
|
|
|
# Create workspaces.json if it doesn't exist
|
|
if [[ ! -f "$QVM_WORKSPACES_FILE" ]]; then
|
|
echo '[]' > "$QVM_WORKSPACES_FILE"
|
|
fi
|
|
|
|
# Check if workspace already registered
|
|
if jq -e --arg path "$workspace_path" '.[] | select(.host_path == $path)' "$QVM_WORKSPACES_FILE" >/dev/null 2>&1; then
|
|
log_info "Workspace already registered: $workspace_path"
|
|
return 0
|
|
fi
|
|
|
|
# Add new workspace to registry
|
|
log_info "Registering new workspace: $workspace_path"
|
|
local temp_file
|
|
temp_file=$(mktemp)
|
|
|
|
jq --arg path "$workspace_path" \
|
|
--arg hash "$hash" \
|
|
--arg tag "$mount_tag" \
|
|
--arg guest "$guest_path" \
|
|
'. += [{
|
|
host_path: $path,
|
|
hash: $hash,
|
|
mount_tag: $tag,
|
|
guest_path: $guest
|
|
}]' "$QVM_WORKSPACES_FILE" > "$temp_file"
|
|
|
|
mv "$temp_file" "$QVM_WORKSPACES_FILE"
|
|
log_info "Workspace registered as $mount_tag -> $guest_path"
|
|
|
|
return 1 # Indicate new workspace added
|
|
}
|
|
|
|
#
|
|
# is_workspace_mounted - Check if workspace is actually mounted in VM
|
|
# Args: $1 - SSH port
|
|
# $2 - guest path
|
|
# Returns: 0 if mounted, 1 if not
|
|
#
|
|
is_workspace_mounted() {
|
|
local ssh_port="$1"
|
|
local guest_path="$2"
|
|
|
|
# SSH into VM and check if guest path exists and is a directory
|
|
if ssh -o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null \
|
|
-o LogLevel=ERROR \
|
|
-p "$ssh_port" \
|
|
root@localhost \
|
|
"test -d '$guest_path'" 2>/dev/null; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
#
|
|
# main - Main execution flow
|
|
#
|
|
main() {
|
|
# Show usage if no arguments
|
|
if [[ $# -eq 0 ]]; then
|
|
show_usage
|
|
exit 1
|
|
fi
|
|
|
|
# Handle help flags
|
|
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
|
show_usage
|
|
exit 0
|
|
fi
|
|
|
|
# Get current workspace (absolute path)
|
|
local workspace_path
|
|
workspace_path="$(pwd)"
|
|
|
|
# Generate workspace hash
|
|
local hash
|
|
hash=$(workspace_hash "$workspace_path")
|
|
|
|
local guest_path="/workspace/${hash}"
|
|
|
|
log_info "Workspace: $workspace_path"
|
|
log_info "Guest path: $guest_path"
|
|
|
|
# Register workspace in registry
|
|
local newly_added=0
|
|
if ! register_workspace "$workspace_path" "$hash"; then
|
|
newly_added=1
|
|
fi
|
|
|
|
# Ensure VM is running
|
|
if ! is_vm_running; then
|
|
log_info "VM not running, starting..."
|
|
|
|
# Path to qvm-start script
|
|
local qvm_start="${QVM_LIB_DIR}/../bin/qvm-start"
|
|
|
|
if ! "$qvm_start"; then
|
|
die "Failed to start VM"
|
|
fi
|
|
fi
|
|
|
|
# Get SSH port
|
|
local ssh_port
|
|
ssh_port=$(get_ssh_port)
|
|
|
|
# Check if workspace is actually mounted in VM
|
|
if ! is_workspace_mounted "$ssh_port" "$guest_path"; then
|
|
log_error "Workspace not mounted in VM"
|
|
echo ""
|
|
echo "This workspace was just registered but is not mounted in the VM."
|
|
echo "Workspaces must be mounted at VM start time."
|
|
echo ""
|
|
echo "Please restart the VM to mount this workspace:"
|
|
echo " qvm stop"
|
|
echo " qvm start"
|
|
echo ""
|
|
echo "Then try your command again."
|
|
exit 1
|
|
fi
|
|
|
|
# Build SSH command
|
|
# - Use -t if stdin is a TTY (for interactive commands)
|
|
# - Suppress SSH warnings (ephemeral VM, host keys change)
|
|
# - cd to guest path and execute command
|
|
local ssh_cmd=(
|
|
ssh
|
|
-o StrictHostKeyChecking=no
|
|
-o UserKnownHostsFile=/dev/null
|
|
-o LogLevel=ERROR
|
|
-p "$ssh_port"
|
|
)
|
|
|
|
# Add -t flag if stdin is a TTY
|
|
if [[ -t 0 ]]; then
|
|
ssh_cmd+=(-t)
|
|
fi
|
|
|
|
# Add connection target
|
|
ssh_cmd+=(root@localhost)
|
|
|
|
# Build remote command: cd to workspace and execute user's command
|
|
# Quote each argument properly to handle spaces and special chars
|
|
local remote_cmd="cd '$guest_path' && "
|
|
|
|
# Append user's command with proper quoting
|
|
local first_arg=1
|
|
for arg in "$@"; do
|
|
if [[ $first_arg -eq 1 ]]; then
|
|
remote_cmd+="$arg"
|
|
first_arg=0
|
|
else
|
|
# Quote arguments that contain spaces or special characters
|
|
if [[ "$arg" =~ [[:space:]] ]]; then
|
|
remote_cmd+=" '$arg'"
|
|
else
|
|
remote_cmd+=" $arg"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Add the remote command as final SSH argument
|
|
ssh_cmd+=("$remote_cmd")
|
|
|
|
# Execute SSH command (replaces current process)
|
|
exec "${ssh_cmd[@]}"
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|