From 33273ece58202622822d59456701fa41057ab95a Mon Sep 17 00:00:00 2001 From: Betafer Date: Fri, 21 Aug 2026 22:30:34 +0200 Subject: [PATCH] feat(packaging): add restricted sender control helper --- .../monitor-mode/targetbridge-restart-sender | 136 ++++++++++++++++++ .../monitor-mode/tests/mock_launchctl.zsh | 30 ++++ .../monitor-mode/tests/test_sender_helper.zsh | 117 +++++++++++++++ 3 files changed, 283 insertions(+) create mode 100755 packaging/monitor-mode/targetbridge-restart-sender create mode 100755 packaging/monitor-mode/tests/mock_launchctl.zsh create mode 100755 packaging/monitor-mode/tests/test_sender_helper.zsh diff --git a/packaging/monitor-mode/targetbridge-restart-sender b/packaging/monitor-mode/targetbridge-restart-sender new file mode 100755 index 0000000..f383b82 --- /dev/null +++ b/packaging/monitor-mode/targetbridge-restart-sender @@ -0,0 +1,136 @@ +#!/bin/zsh +set -u + +TARGET_USER="${TB_TARGET_USER:-$(/usr/bin/id -un 2>/dev/null || true)}" +LABEL="com.targetbridge.sender.autostart" +TARGET_UID="${TB_TARGET_UID:-$(/usr/bin/id -u "${TARGET_USER}" 2>/dev/null || true)}" +CONSOLE_USER="${TB_CONSOLE_USER:-$(/usr/bin/stat -f %Su /dev/console 2>/dev/null || true)}" +HOME_DIR="${TB_TARGET_HOME:-${HOME:-}}" +PLIST="${TB_SENDER_PLIST:-${HOME_DIR}/Library/LaunchAgents/${LABEL}.plist}" +STATE_DIR="${TB_SENDER_STATE_DIR:-${HOME_DIR}/Library/Application Support/TargetBridge/Sender}" +ENABLED_FLAG="${STATE_DIR}/enabled" +PATH_FILE="${STATE_DIR}/requested-path" +INPUT_FILE="${STATE_DIR}/requested-input" +SENDER_EXEC="${TB_SENDER_EXEC:-/Applications/TargetBridge.app/Contents/MacOS/TargetBridge}" +SENDER_EXEC_REGEX="${SENDER_EXEC//./\\.}" +LAUNCHCTL_BIN="${TB_LAUNCHCTL_BIN:-/bin/launchctl}" +REQUEST="${SSH_ORIGINAL_COMMAND:-activate}" +REQUESTED_PATH="" +REQUESTED_INPUT="" +STOP_REQUESTED=0 + +case "${REQUEST}" in + activate|"") ;; + "path auto") REQUESTED_PATH="automatic" ;; + "path wired") REQUESTED_PATH="wired" ;; + "path thunderbolt") REQUESTED_PATH="thunderbolt" ;; + "path usb") REQUESTED_PATH="usb" ;; + "path ethernet") REQUESTED_PATH="ethernet" ;; + "path wifi") REQUESTED_PATH="wifi" ;; + "input on") REQUESTED_INPUT="receiver" ;; + "input off") REQUESTED_INPUT="off" ;; + stop) STOP_REQUESTED=1 ;; + *) print -- "REQUEST_NOT_ALLOWED"; exit 25 ;; +esac + +[[ -n "${TARGET_USER}" && -n "${TARGET_UID}" && -n "${HOME_DIR}" ]] \ + || { print -- "LOGIN_REQUIRED"; exit 20; } +DOMAIN="gui/${TARGET_UID}" +SERVICE="${DOMAIN}/${LABEL}" + +if [[ ${STOP_REQUESTED} -eq 1 ]]; then + /bin/rm -f "${ENABLED_FLAG}" + "${LAUNCHCTL_BIN}" kill TERM "${SERVICE}" >/dev/null 2>&1 || true + /usr/bin/pkill -TERM -f "^${SENDER_EXEC_REGEX}( |$)" >/dev/null 2>&1 || true + for wait_index in {1..30}; do + /usr/bin/pgrep -f "^${SENDER_EXEC_REGEX}( |$)" >/dev/null 2>&1 || { + print -- "SENDER_STOPPED" + exit 0 + } + /bin/sleep 0.1 + done + print -- "SENDER_STOP_FAILED" + exit 28 +fi + +[[ "${CONSOLE_USER}" == "${TARGET_USER}" ]] || { print -- "LOGIN_REQUIRED"; exit 20; } +[[ -f "${PLIST}" && -x "${SENDER_EXEC}" ]] || { print -- "SENDER_NOT_INSTALLED"; exit 21; } + +/bin/mkdir -p "${STATE_DIR}" || { print -- "STATE_UPDATE_FAILED"; exit 26; } +/bin/chmod 700 "${STATE_DIR}" >/dev/null 2>&1 || true + +PATH_CHANGED=0 +if [[ -n "${REQUESTED_PATH}" ]]; then + CURRENT_PATH="" + [[ -r "${PATH_FILE}" ]] && IFS= read -r CURRENT_PATH < "${PATH_FILE}" || true + if [[ "${CURRENT_PATH}" != "${REQUESTED_PATH}" ]]; then + TEMP_PATH="$(/usr/bin/mktemp "${STATE_DIR}/requested-path.XXXXXX")" \ + || { print -- "PATH_UPDATE_FAILED"; exit 26; } + print -r -- "${REQUESTED_PATH}" > "${TEMP_PATH}" \ + || { /bin/rm -f "${TEMP_PATH}"; print -- "PATH_UPDATE_FAILED"; exit 26; } + /bin/chmod 600 "${TEMP_PATH}" >/dev/null 2>&1 || true + /bin/mv "${TEMP_PATH}" "${PATH_FILE}" \ + || { print -- "PATH_UPDATE_FAILED"; exit 26; } + PATH_CHANGED=1 + fi +fi + +INPUT_CHANGED=0 +if [[ -n "${REQUESTED_INPUT}" ]]; then + CURRENT_INPUT="" + [[ -r "${INPUT_FILE}" ]] && IFS= read -r CURRENT_INPUT < "${INPUT_FILE}" || true + if [[ "${CURRENT_INPUT}" != "${REQUESTED_INPUT}" ]]; then + TEMP_INPUT="$(/usr/bin/mktemp "${STATE_DIR}/requested-input.XXXXXX")" \ + || { print -- "INPUT_UPDATE_FAILED"; exit 26; } + print -r -- "${REQUESTED_INPUT}" > "${TEMP_INPUT}" \ + || { /bin/rm -f "${TEMP_INPUT}"; print -- "INPUT_UPDATE_FAILED"; exit 26; } + /bin/chmod 600 "${TEMP_INPUT}" >/dev/null 2>&1 || true + /bin/mv "${TEMP_INPUT}" "${INPUT_FILE}" \ + || { print -- "INPUT_UPDATE_FAILED"; exit 26; } + INPUT_CHANGED=1 + fi +fi + +WAS_ENABLED=0 +[[ -f "${ENABLED_FLAG}" ]] && WAS_ENABLED=1 + +# Changing the optional iMac controls while monitor mode is stopped must not +# unexpectedly attach to the Mac mini. The preference is kept for the next +# normal start and the Receiver remains usable locally. +if [[ -n "${REQUESTED_INPUT}" && ${WAS_ENABLED} -eq 0 ]]; then + print -- "INPUT_SET:${REQUESTED_INPUT}" + exit 0 +fi + +/usr/bin/touch "${ENABLED_FLAG}" || { print -- "STATE_UPDATE_FAILED"; exit 26; } +/bin/chmod 600 "${ENABLED_FLAG}" >/dev/null 2>&1 || true + +SERVICE_LOADED=1 +"${LAUNCHCTL_BIN}" print "${SERVICE}" >/dev/null 2>&1 || SERVICE_LOADED=0 +if [[ ${SERVICE_LOADED} -eq 0 ]]; then + "${LAUNCHCTL_BIN}" bootstrap "${DOMAIN}" "${PLIST}" >/dev/null 2>&1 \ + || { print -- "SENDER_BOOTSTRAP_FAILED"; exit 22; } +fi +"${LAUNCHCTL_BIN}" enable "${SERVICE}" >/dev/null 2>&1 || true + +SERVICE_RUNNING=0 +"${LAUNCHCTL_BIN}" print "${SERVICE}" 2>/dev/null | /usr/bin/grep -q 'state = running' \ + && SERVICE_RUNNING=1 +if [[ ${PATH_CHANGED} -eq 1 || ${INPUT_CHANGED} -eq 1 || "${REQUESTED_INPUT}" == "receiver" || ${SERVICE_RUNNING} -eq 0 || ${WAS_ENABLED} -eq 0 ]]; then + "${LAUNCHCTL_BIN}" kickstart -k "${SERVICE}" >/dev/null 2>&1 || true +fi + +# launchd may enforce the LaunchAgent ThrottleInterval after a deliberate +# Stop. Allow enough time for PathState/kickstart to pass that throttle so the +# Receiver does not report a false failure while the Sender is still starting. +for wait_index in {1..120}; do + if "${LAUNCHCTL_BIN}" print "${SERVICE}" 2>/dev/null | /usr/bin/grep -q 'state = running'; then + [[ -z "${REQUESTED_PATH}" ]] || print -- "PATH_SET:${REQUESTED_PATH}" + [[ -z "${REQUESTED_INPUT}" ]] || print -- "INPUT_SET:${REQUESTED_INPUT}" + print -- "SENDER_STARTED" + exit 0 + fi + /bin/sleep 0.1 +done +print -- "SENDER_NOT_RUNNING" +exit 24 diff --git a/packaging/monitor-mode/tests/mock_launchctl.zsh b/packaging/monitor-mode/tests/mock_launchctl.zsh new file mode 100755 index 0000000..6285a77 --- /dev/null +++ b/packaging/monitor-mode/tests/mock_launchctl.zsh @@ -0,0 +1,30 @@ +#!/bin/zsh +set -u + +print -r -- "$*" >> "${TB_TEST_LAUNCHCTL_LOG}" +if [[ -n "${TB_TEST_LAUNCHCTL_FAIL_ACTION:-}" && "${1:-}" == "${TB_TEST_LAUNCHCTL_FAIL_ACTION}" ]]; then + if [[ -z "${TB_TEST_LAUNCHCTL_FAIL_COUNT:-}" ]]; then + print -u2 -- "mock launchctl failure: ${1}" + exit 70 + fi + occurrence="$(/usr/bin/grep -c "^${1} " "${TB_TEST_LAUNCHCTL_LOG}" 2>/dev/null || true)" + if (( occurrence <= TB_TEST_LAUNCHCTL_FAIL_COUNT )); then + print -u2 -- "mock transient launchctl failure: ${1}" + exit 70 + fi +fi +case "${1:-}" in + print) + [[ "${TB_TEST_SERVICE_LOADED:-1}" == "1" ]] || exit 1 + print -- "state = running" + service_pid="${TB_TEST_SERVICE_PID:-12345}" + if [[ -n "${TB_TEST_SERVICE_PID_AFTER:-}" && -n "${TB_TEST_SERVICE_PID_SWITCH_AFTER:-}" ]]; then + print_count="$(/usr/bin/grep -c '^print ' "${TB_TEST_LAUNCHCTL_LOG}" 2>/dev/null || true)" + if (( print_count > TB_TEST_SERVICE_PID_SWITCH_AFTER )); then + service_pid="${TB_TEST_SERVICE_PID_AFTER}" + fi + fi + print -- "pid = ${service_pid}" + ;; +esac +exit 0 diff --git a/packaging/monitor-mode/tests/test_sender_helper.zsh b/packaging/monitor-mode/tests/test_sender_helper.zsh new file mode 100755 index 0000000..d9e8a38 --- /dev/null +++ b/packaging/monitor-mode/tests/test_sender_helper.zsh @@ -0,0 +1,117 @@ +#!/bin/zsh +set -euo pipefail + +TEST_DIR="${0:A:h}" +HELPER="${TEST_DIR:h}/targetbridge-restart-sender" +ROOT="$(/usr/bin/mktemp -d /tmp/targetbridge-helper-test.XXXXXX)" +trap '/bin/rm -rf "${ROOT}"' EXIT + +export TB_TARGET_HOME="${ROOT}/home" +export TB_TARGET_USER="${USER}" +export TB_TARGET_UID="501" +export TB_CONSOLE_USER="${USER}" +export TB_SENDER_STATE_DIR="${TB_TARGET_HOME}/Library/Application Support/TargetBridge/Sender" +export TB_SENDER_PLIST="${TB_TARGET_HOME}/Library/LaunchAgents/com.targetbridge.sender.autostart.plist" +export TB_SENDER_EXEC="${ROOT}/TargetBridge" +export TB_LAUNCHCTL_BIN="${TEST_DIR}/mock_launchctl.zsh" +export TB_TEST_LAUNCHCTL_LOG="${ROOT}/launchctl.log" +export TB_TEST_SERVICE_LOADED=1 + +/bin/mkdir -p "${TB_SENDER_PLIST:h}" +print -- "plist" > "${TB_SENDER_PLIST}" +print -- '#!/bin/zsh' > "${TB_SENDER_EXEC}" +/bin/chmod 700 "${TB_SENDER_EXEC}" "${HELPER}" "${TB_LAUNCHCTL_BIN}" + +failures=0 +checks=0 +check() { + checks=$((checks + 1)) + if ! eval "$2"; then + print -u2 -- "FAIL: $1" + failures=$((failures + 1)) + fi +} + +export SSH_ORIGINAL_COMMAND="path thunderbolt" +result="$(${HELPER})" +check "path action succeeds" '[[ "${result}" == *"SENDER_STARTED"* ]]' +check "path is persisted" '[[ "$(<"${TB_SENDER_STATE_DIR}/requested-path")" == "thunderbolt" ]]' +check "enabled marker exists" '[[ -f "${TB_SENDER_STATE_DIR}/enabled" ]]' +check "path change restarts in place" '/usr/bin/grep -q "kickstart -k gui/501/com.targetbridge.sender.autostart" "${TB_TEST_LAUNCHCTL_LOG}"' +check "path change never removes login item" '! /usr/bin/grep -q "bootout" "${TB_TEST_LAUNCHCTL_LOG}"' +check "loaded path change never bootstraps" '! /usr/bin/grep -q "bootstrap" "${TB_TEST_LAUNCHCTL_LOG}"' + +: > "${TB_TEST_LAUNCHCTL_LOG}" +result="$(${HELPER})" +check "same path succeeds" '[[ "${result}" == *"SENDER_STARTED"* ]]' +check "same running path does not restart" '! /usr/bin/grep -q "kickstart" "${TB_TEST_LAUNCHCTL_LOG}"' + +: > "${TB_TEST_LAUNCHCTL_LOG}" +export SSH_ORIGINAL_COMMAND="input on" +result="$(${HELPER})" +check "input option succeeds" '[[ "${result}" == *"INPUT_SET:receiver"* ]]' +check "input option is persisted" '[[ "$(<"${TB_SENDER_STATE_DIR}/requested-input")" == "receiver" ]]' +check "live input change restarts sender" '/usr/bin/grep -q "kickstart -k gui/501/com.targetbridge.sender.autostart" "${TB_TEST_LAUNCHCTL_LOG}"' + +: > "${TB_TEST_LAUNCHCTL_LOG}" +export SSH_ORIGINAL_COMMAND="input on" +result="$(${HELPER})" +check "same input option succeeds" '[[ "${result}" == *"INPUT_SET:receiver"* ]]' +check "same live input option restarts for permission refresh" '/usr/bin/grep -q "kickstart -k gui/501/com.targetbridge.sender.autostart" "${TB_TEST_LAUNCHCTL_LOG}"' + +export SSH_ORIGINAL_COMMAND="stop" +result="$(${HELPER})" +check "stop succeeds" '[[ "${result}" == *"SENDER_STOPPED"* ]]' +check "stop removes enabled marker" '[[ ! -e "${TB_SENDER_STATE_DIR}/enabled" ]]' +check "stop keeps login item registered" '! /usr/bin/grep -q "bootout" "${TB_TEST_LAUNCHCTL_LOG}"' + +: > "${TB_TEST_LAUNCHCTL_LOG}" +export SSH_ORIGINAL_COMMAND="input off" +result="$(${HELPER})" +check "stopped input option succeeds" '[[ "${result}" == "INPUT_SET:off" ]]' +check "stopped input option is persisted" '[[ "$(<"${TB_SENDER_STATE_DIR}/requested-input")" == "off" ]]' +check "stopped input option does not reconnect" '! /usr/bin/grep -q "kickstart" "${TB_TEST_LAUNCHCTL_LOG}"' + +: > "${TB_TEST_LAUNCHCTL_LOG}" +export SSH_ORIGINAL_COMMAND="path thunderbolt" +result="$(${HELPER})" +check "restart after user stop succeeds" '[[ "${result}" == *"SENDER_STARTED"* ]]' +check "restart after user stop restores marker" '[[ -f "${TB_SENDER_STATE_DIR}/enabled" ]]' +check "restart after user stop kickstarts running app" '/usr/bin/grep -q "kickstart -k gui/501/com.targetbridge.sender.autostart" "${TB_TEST_LAUNCHCTL_LOG}"' + +set +e +export SSH_ORIGINAL_COMMAND="run arbitrary command" +result="$(${HELPER})" +exit_code=$? +set -e +check "unknown request is rejected" '[[ ${exit_code} -eq 25 && "${result}" == "REQUEST_NOT_ALLOWED" ]]' + +set +e +export SSH_ORIGINAL_COMMAND="activate" +export TB_CONSOLE_USER="different-user" +result="$(${HELPER})" +exit_code=$? +set -e +check "activation requires the target console login" '[[ ${exit_code} -eq 20 && "${result}" == "LOGIN_REQUIRED" ]]' +export TB_CONSOLE_USER="${USER}" + +set +e +export TB_SENDER_PLIST="${ROOT}/missing.plist" +result="$(${HELPER})" +exit_code=$? +set -e +check "missing installation is reported" '[[ ${exit_code} -eq 21 && "${result}" == "SENDER_NOT_INSTALLED" ]]' +export TB_SENDER_PLIST="${TB_TARGET_HOME}/Library/LaunchAgents/com.targetbridge.sender.autostart.plist" + +set +e +export TB_TEST_SERVICE_LOADED=0 +export TB_TEST_LAUNCHCTL_FAIL_ACTION="bootstrap" +result="$(${HELPER})" +exit_code=$? +set -e +check "bootstrap failure is actionable" '[[ ${exit_code} -eq 22 && "${result}" == "SENDER_BOOTSTRAP_FAILED" ]]' +unset TB_TEST_LAUNCHCTL_FAIL_ACTION +export TB_TEST_SERVICE_LOADED=1 + +print -- "sender helper tests: ${checks} checks, ${failures} failures" +exit "${failures}"