Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions packaging/monitor-mode/targetbridge-restart-sender
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions packaging/monitor-mode/tests/mock_launchctl.zsh
Original file line number Diff line number Diff line change
@@ -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
117 changes: 117 additions & 0 deletions packaging/monitor-mode/tests/test_sender_helper.zsh
Original file line number Diff line number Diff line change
@@ -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}"
Loading