mirror of
https://github.com/tw93/Mole.git
synced 2026-03-22 23:40:09 +00:00
When Mole is called from GUI applications (e.g., SwiftUI apps), the request_sudo_access() function would fail with '/dev/tty: Device not configured' error because /dev/tty is not available in non-TTY contexts. This commit adds automatic detection of the runtime environment and uses macOS native password dialog (via osascript) when running in GUI mode, while preserving all existing TTY behavior including Touch ID support. Changes: - Detect TTY availability before attempting terminal-based authentication - Use osascript to display native password dialog in GUI mode - Maintain backward compatibility with all terminal-based workflows - Ensure secure password handling (unset after use) Fixes commands like 'mole clean', 'mole optimize', 'mole purge' when invoked from GUI applications.
347 lines
10 KiB
Bash
347 lines
10 KiB
Bash
#!/bin/bash
|
|
# Sudo Session Manager
|
|
# Unified sudo authentication and keepalive management
|
|
|
|
set -euo pipefail
|
|
|
|
# ============================================================================
|
|
# Touch ID and Clamshell Detection
|
|
# ============================================================================
|
|
|
|
check_touchid_support() {
|
|
# Check sudo_local first (Sonoma+)
|
|
if [[ -f /etc/pam.d/sudo_local ]]; then
|
|
grep -q "pam_tid.so" /etc/pam.d/sudo_local 2> /dev/null
|
|
return $?
|
|
fi
|
|
|
|
# Fallback to checking sudo directly
|
|
if [[ -f /etc/pam.d/sudo ]]; then
|
|
grep -q "pam_tid.so" /etc/pam.d/sudo 2> /dev/null
|
|
return $?
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Detect clamshell mode (lid closed)
|
|
is_clamshell_mode() {
|
|
# ioreg is missing (not macOS) -> treat as lid open
|
|
if ! command -v ioreg > /dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
|
|
# Check if lid is closed; ignore pipeline failures so set -e doesn't exit
|
|
local clamshell_state=""
|
|
clamshell_state=$( (ioreg -r -k AppleClamshellState -d 4 2> /dev/null |
|
|
grep "AppleClamshellState" |
|
|
head -1) || true)
|
|
|
|
if [[ "$clamshell_state" =~ \"AppleClamshellState\"\ =\ Yes ]]; then
|
|
return 0 # Lid is closed
|
|
fi
|
|
return 1 # Lid is open
|
|
}
|
|
|
|
_request_password() {
|
|
local tty_path="$1"
|
|
local attempts=0
|
|
local show_hint=true
|
|
|
|
# Extra safety: ensure sudo cache is cleared before password input
|
|
sudo -k 2> /dev/null
|
|
|
|
# Save original terminal settings and ensure they're restored on exit
|
|
local stty_orig
|
|
stty_orig=$(stty -g < "$tty_path" 2> /dev/null || echo "")
|
|
trap '[[ -n "${stty_orig:-}" ]] && stty "${stty_orig:-}" < "$tty_path" 2> /dev/null || true' RETURN
|
|
|
|
while ((attempts < 3)); do
|
|
local password=""
|
|
|
|
# Show hint on first attempt about Touch ID appearing again
|
|
if [[ $show_hint == true ]] && check_touchid_support; then
|
|
echo -e "${GRAY}Note: Touch ID dialog may appear once more, just cancel it${NC}" > "$tty_path"
|
|
show_hint=false
|
|
fi
|
|
|
|
printf "${PURPLE}${ICON_ARROW}${NC} Password: " > "$tty_path"
|
|
|
|
# Disable terminal echo to hide password input (keep canonical mode for reliable input)
|
|
stty -echo < "$tty_path" 2> /dev/null || true
|
|
IFS= read -r password < "$tty_path" || password=""
|
|
# Restore terminal echo immediately
|
|
stty echo < "$tty_path" 2> /dev/null || true
|
|
|
|
printf "\n" > "$tty_path"
|
|
|
|
if [[ -z "$password" ]]; then
|
|
unset password
|
|
attempts=$((attempts + 1))
|
|
if [[ $attempts -lt 3 ]]; then
|
|
echo -e "${GRAY}${ICON_WARNING}${NC} Password cannot be empty" > "$tty_path"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
# Verify password with sudo
|
|
# NOTE: macOS PAM will trigger Touch ID before password auth - this is system behavior
|
|
if printf '%s\n' "$password" | sudo -S -p "" -v > /dev/null 2>&1; then
|
|
unset password
|
|
return 0
|
|
fi
|
|
|
|
unset password
|
|
attempts=$((attempts + 1))
|
|
if [[ $attempts -lt 3 ]]; then
|
|
echo -e "${GRAY}${ICON_WARNING}${NC} Incorrect password, try again" > "$tty_path"
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
request_sudo_access() {
|
|
local prompt_msg="${1:-Admin access required}"
|
|
|
|
# Check if already have sudo access
|
|
if sudo -n true 2> /dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
# Detect if running in TTY environment
|
|
local tty_path="/dev/tty"
|
|
local is_gui_mode=false
|
|
|
|
if [[ ! -r "$tty_path" || ! -w "$tty_path" ]]; then
|
|
tty_path=$(tty 2> /dev/null || echo "")
|
|
if [[ -z "$tty_path" || ! -r "$tty_path" || ! -w "$tty_path" ]]; then
|
|
is_gui_mode=true
|
|
fi
|
|
fi
|
|
|
|
# GUI mode: use osascript for password dialog
|
|
if [[ "$is_gui_mode" == true ]]; then
|
|
# Clear sudo cache before attempting authentication
|
|
sudo -k 2> /dev/null
|
|
|
|
# Display native macOS password dialog
|
|
local password
|
|
password=$(osascript -e "display dialog \"$prompt_msg\" default answer \"\" with title \"Mole\" with icon caution with hidden answer" -e 'text returned of result' 2> /dev/null)
|
|
|
|
if [[ -z "$password" ]]; then
|
|
# User cancelled the dialog
|
|
unset password
|
|
return 1
|
|
fi
|
|
|
|
# Attempt sudo authentication with the provided password
|
|
if printf '%s\n' "$password" | sudo -S -p "" -v > /dev/null 2>&1; then
|
|
unset password
|
|
return 0
|
|
fi
|
|
|
|
# Password was incorrect
|
|
unset password
|
|
return 1
|
|
fi
|
|
|
|
sudo -k
|
|
|
|
# Check if in clamshell mode - if yes, skip Touch ID entirely
|
|
if is_clamshell_mode; then
|
|
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg}"
|
|
if _request_password "$tty_path"; then
|
|
# Clear all prompt lines (use safe clearing method)
|
|
safe_clear_lines 3 "$tty_path"
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
# Not in clamshell mode - try Touch ID if configured
|
|
if ! check_touchid_support; then
|
|
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg}"
|
|
if _request_password "$tty_path"; then
|
|
# Clear all prompt lines (use safe clearing method)
|
|
safe_clear_lines 3 "$tty_path"
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
# Touch ID is available and not in clamshell mode
|
|
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg} ${GRAY}, Touch ID or password${NC}"
|
|
|
|
# Start sudo in background so we can monitor and control it
|
|
sudo -v < /dev/null > /dev/null 2>&1 &
|
|
local sudo_pid=$!
|
|
|
|
# Wait for sudo to complete or timeout (5 seconds)
|
|
local elapsed=0
|
|
local timeout=50 # 50 * 0.1s = 5 seconds
|
|
while ((elapsed < timeout)); do
|
|
if ! kill -0 "$sudo_pid" 2> /dev/null; then
|
|
# Process exited
|
|
wait "$sudo_pid" 2> /dev/null
|
|
local exit_code=$?
|
|
if [[ $exit_code -eq 0 ]] && sudo -n true 2> /dev/null; then
|
|
# Touch ID succeeded - clear the prompt line
|
|
safe_clear_lines 1 "$tty_path"
|
|
return 0
|
|
fi
|
|
# Touch ID failed or cancelled
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
elapsed=$((elapsed + 1))
|
|
done
|
|
|
|
# Touch ID failed/cancelled - clean up thoroughly before password input
|
|
|
|
# Kill the sudo process if still running
|
|
if kill -0 "$sudo_pid" 2> /dev/null; then
|
|
kill -9 "$sudo_pid" 2> /dev/null
|
|
wait "$sudo_pid" 2> /dev/null || true
|
|
fi
|
|
|
|
# Clear sudo state immediately
|
|
sudo -k 2> /dev/null
|
|
|
|
# IMPORTANT: Wait longer for macOS to fully close Touch ID UI and SecurityAgent
|
|
# Without this delay, subsequent sudo calls may re-trigger Touch ID
|
|
sleep 1
|
|
|
|
# Clear any leftover prompts on the screen
|
|
safe_clear_line "$tty_path"
|
|
|
|
# Now use our password input (this should not trigger Touch ID again)
|
|
if _request_password "$tty_path"; then
|
|
# Clear all prompt lines (use safe clearing method)
|
|
safe_clear_lines 3 "$tty_path"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# ============================================================================
|
|
# Sudo Session Management
|
|
# ============================================================================
|
|
|
|
# Global state
|
|
MOLE_SUDO_KEEPALIVE_PID=""
|
|
MOLE_SUDO_ESTABLISHED="false"
|
|
|
|
# Start sudo keepalive
|
|
_start_sudo_keepalive() {
|
|
# Start background keepalive process with all outputs redirected
|
|
# This is critical: command substitution waits for all file descriptors to close
|
|
(
|
|
# Initial delay to let sudo cache stabilize after password entry
|
|
# This prevents immediately triggering Touch ID again
|
|
sleep 2
|
|
|
|
local retry_count=0
|
|
while true; do
|
|
if ! sudo -n -v 2> /dev/null; then
|
|
retry_count=$((retry_count + 1))
|
|
if [[ $retry_count -ge 3 ]]; then
|
|
exit 1
|
|
fi
|
|
sleep 5
|
|
continue
|
|
fi
|
|
retry_count=0
|
|
sleep 30
|
|
kill -0 "$$" 2> /dev/null || exit
|
|
done
|
|
) > /dev/null 2>&1 &
|
|
|
|
local pid=$!
|
|
echo $pid
|
|
}
|
|
|
|
# Stop sudo keepalive
|
|
_stop_sudo_keepalive() {
|
|
local pid="${1:-}"
|
|
if [[ -n "$pid" ]]; then
|
|
kill "$pid" 2> /dev/null || true
|
|
wait "$pid" 2> /dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Check if sudo session is active
|
|
has_sudo_session() {
|
|
sudo -n true 2> /dev/null
|
|
}
|
|
|
|
# Request administrative access
|
|
request_sudo() {
|
|
local prompt_msg="${1:-Admin access required}"
|
|
|
|
if has_sudo_session; then
|
|
return 0
|
|
fi
|
|
|
|
# Use the robust implementation from common.sh
|
|
if request_sudo_access "$prompt_msg"; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Maintain active sudo session with keepalive
|
|
ensure_sudo_session() {
|
|
local prompt="${1:-Admin access required}"
|
|
|
|
# Check if already established
|
|
if has_sudo_session && [[ "$MOLE_SUDO_ESTABLISHED" == "true" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Stop old keepalive if exists
|
|
if [[ -n "$MOLE_SUDO_KEEPALIVE_PID" ]]; then
|
|
_stop_sudo_keepalive "$MOLE_SUDO_KEEPALIVE_PID"
|
|
MOLE_SUDO_KEEPALIVE_PID=""
|
|
fi
|
|
|
|
# Request sudo access
|
|
if ! request_sudo "$prompt"; then
|
|
MOLE_SUDO_ESTABLISHED="false"
|
|
return 1
|
|
fi
|
|
|
|
# Start keepalive
|
|
MOLE_SUDO_KEEPALIVE_PID=$(_start_sudo_keepalive)
|
|
|
|
MOLE_SUDO_ESTABLISHED="true"
|
|
return 0
|
|
}
|
|
|
|
# Stop sudo session and cleanup
|
|
stop_sudo_session() {
|
|
if [[ -n "$MOLE_SUDO_KEEPALIVE_PID" ]]; then
|
|
_stop_sudo_keepalive "$MOLE_SUDO_KEEPALIVE_PID"
|
|
MOLE_SUDO_KEEPALIVE_PID=""
|
|
fi
|
|
MOLE_SUDO_ESTABLISHED="false"
|
|
}
|
|
|
|
# Register cleanup on script exit
|
|
register_sudo_cleanup() {
|
|
trap stop_sudo_session EXIT INT TERM
|
|
}
|
|
|
|
# Predict if operation requires administrative access
|
|
will_need_sudo() {
|
|
local -a operations=("$@")
|
|
for op in "${operations[@]}"; do
|
|
case "$op" in
|
|
system_update | appstore_update | macos_update | firewall | touchid | rosetta | system_fix)
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|