mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-26 14:41:04 +03:00
Created macOS Auto-Start Service Setup (for Remote / MDM Deployment) (markdown)
572
macOS-Auto‐Start-Service-Setup-(for-Remote---MDM-Deployment).md
Normal file
572
macOS-Auto‐Start-Service-Setup-(for-Remote---MDM-Deployment).md
Normal file
@@ -0,0 +1,572 @@
|
||||
# macOS Auto-Start Service Setup (for Remote / MDM Deployment)
|
||||
|
||||
This guide explains how to use `install_service.sh` to set up RustDesk (or a Custom Client) as an auto-start service on macOS via the command line, for scenarios where the GUI "Install" button is not available.
|
||||
|
||||
<details open><summary>install_service.sh</summary>
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
#
|
||||
# rustdesk-daemon-install - public domain 2024
|
||||
|
||||
# Ver Who Date Note
|
||||
# 001 GK 241219 Initial setup - tested on ProxMox machine - permissions had to be manually responded to in GUI
|
||||
# 002 HM 250224 Fix custom client support: APP_NAME, pref paths, plist labels, MDM user detection
|
||||
# 003 HM 250224 Fix GetAssets timing (require $APP), apply correct_app_name to install.scpt
|
||||
# 004 HM 250224 Split GetAssets: view options (-v) no longer blocked by user/app checks
|
||||
# 005 HM 250224 Fix -a/-d/-s blocked by user validation; fix dscl space-in-path truncation
|
||||
|
||||
|
||||
# Setup RustDesk services for Mac - even in a VM environment (presuming a copy of Preferences from another machine)
|
||||
|
||||
# Optionally set ID and password - to make it work on a MacOS Vm
|
||||
|
||||
# Server is for a user
|
||||
# Service is for the machine
|
||||
|
||||
#
|
||||
# This script will find the app in /Applications or a subfolder or subfolder of a subfolder
|
||||
#
|
||||
|
||||
# From lib/log
|
||||
Msg() { echo "$(date +%d:%k%M%S) $$ $@" >&2; }
|
||||
Err() { Msg "<***> $@ <***>"; return 42; }
|
||||
Throw() { [ -n "$1" ] && Msg "=== $1 ==="; exit ${2:-22}; exit 9; }
|
||||
|
||||
#
|
||||
# ===== CUSTOM CLIENT CONFIGURATION =====
|
||||
# For custom clients, only change DEFAULTPATH to your app path.
|
||||
# Everything else is auto-derived.
|
||||
#
|
||||
# Example:
|
||||
# DEFAULTPATH='/Applications/MyApp.app'
|
||||
# DEFAULTPATH='/Applications/Subfolder/MyApp.app'
|
||||
# DEFAULTPATH='/opt/MyApp.app' (also works — FindApp checks DEFAULTPATH first)
|
||||
#
|
||||
# How naming works (hardcoded in RustDesk, cannot be changed):
|
||||
# APP_NAME = basename of DEFAULTPATH without .app (e.g. "MyApp")
|
||||
# FULL_NAME = "com.carriez.{APP_NAME}" (e.g. "com.carriez.MyApp")
|
||||
# Plist files = /Library/LaunchDaemons/com.carriez.MyApp_service.plist
|
||||
# /Library/LaunchAgents/com.carriez.MyApp_server.plist
|
||||
# Config path = ~/Library/Preferences/com.carriez.MyApp/
|
||||
# Config files = MyApp.toml, MyApp2.toml
|
||||
#
|
||||
# Note: The app's bundle identifier (e.g. com.mycompany.myapp) does NOT affect
|
||||
# any of the above. RustDesk always uses "com.carriez.{APP_NAME}" internally.
|
||||
# ========================================
|
||||
#
|
||||
|
||||
# ========================== CONFIGURATION ==========================
|
||||
|
||||
DEFAULTPATH='/Applications/RustDesk.app'
|
||||
# On reinstall, clear the "stop-service" flag left by a previous uninstall:
|
||||
# 1 = remove stop-service line from existing toml configs before installing
|
||||
# (recommended for MDM redeployment — prevents the "service running but not
|
||||
# connecting" issue when redeploying to a Mac that had the service stopped/uninstalled)
|
||||
# 0 = leave existing config untouched (original behavior)
|
||||
CLEAR_STOP_SERVICE=1
|
||||
# If config files don't exist:
|
||||
# 1 = auto-create empty config files (recommended for custom clients / MDM deployment,
|
||||
# server config is embedded in the binary at build time, service will populate on first run)
|
||||
# 0 = exit with error (original behavior, useful when you need pre-configured toml files)
|
||||
AUTO_CREATE_CONFIG=1
|
||||
|
||||
# ========================== END CONFIGURATION ==========================
|
||||
|
||||
# Derive APP_NAME from DEFAULTPATH (e.g. /Applications/Test2.app -> Test2)
|
||||
APP_NAME="$(basename "$DEFAULTPATH" .app)"
|
||||
|
||||
# FULL_NAME matches RustDesk's get_full_name(): "{ORG}.{APP_NAME}"
|
||||
# ORG is always "com.carriez" on macOS (hardcoded in config.rs)
|
||||
FULL_NAME="com.carriez.${APP_NAME}"
|
||||
|
||||
# Are we running as root - not really normal for a mac - maybe sudo'd
|
||||
# Enhanced for MDM context where SUDO_USER and USER may be empty
|
||||
FORUSER_OVERRIDE=''
|
||||
|
||||
if [ $(id -u) -eq 0 ]; then
|
||||
SUDO=''
|
||||
FORUSER=${SUDO_USER:-$USER}
|
||||
# In MDM postinstall context, both SUDO_USER and USER can be empty
|
||||
if [ -z "$FORUSER" ] || [ "$FORUSER" = "root" ]; then
|
||||
FORUSER=$(scutil <<< "show State:/Users/ConsoleUser" 2>/dev/null | awk '/Name :/ && !/loginwindow/ { print $3 }')
|
||||
fi
|
||||
else
|
||||
SUDO='sudo'
|
||||
FORUSER=$(id -un)
|
||||
fi
|
||||
|
||||
APP=''
|
||||
|
||||
AGENT='' # Assets from Github
|
||||
DAEMON=''
|
||||
ASCPT=''
|
||||
|
||||
USESCRIPT=0 # Use AppleScript instead of direct install
|
||||
USE_ID='' # Set ID of this machine via id= in toml file
|
||||
USE_PWD='' # Set permanent password to this if non-null
|
||||
VIEW_ONLY=0 # Set by -a/-d/-s to skip the infinite wait loop
|
||||
|
||||
agent_path="/Library/LaunchAgents/${FULL_NAME}_server.plist"
|
||||
daemon_path="/Library/LaunchDaemons/${FULL_NAME}_service.plist"
|
||||
root_pref_path="/var/root/Library/Preferences/$FULL_NAME"
|
||||
# user_pref_path is set after FORUSER is resolved (depends on user's home directory)
|
||||
|
||||
pref_files="${APP_NAME}.toml ${APP_NAME}2.toml"
|
||||
|
||||
EnsureConfig()
|
||||
{
|
||||
local i
|
||||
Msg "Verify $FORUSER ($FORUID) preferences..."
|
||||
|
||||
# Ensure preference directory and config files exist
|
||||
if [ ! -d "$user_pref_path" ]; then
|
||||
if [ "$AUTO_CREATE_CONFIG" -eq 1 ]; then
|
||||
Msg "Creating preference directory: $user_pref_path"
|
||||
$SUDO mkdir -p "$user_pref_path"
|
||||
$SUDO chown "$FORUSER" "$user_pref_path"
|
||||
else
|
||||
Throw "Preference directory $user_pref_path does not exist for user $FORUSER"
|
||||
fi
|
||||
fi
|
||||
for i in $pref_files; do
|
||||
if [ ! -s "$user_pref_path/$i" ]; then
|
||||
if [ "$AUTO_CREATE_CONFIG" -eq 1 ]; then
|
||||
Msg "Config file $i not found for user $FORUSER — creating empty config (service will populate on first run)"
|
||||
$SUDO touch "$user_pref_path/$i"
|
||||
$SUDO chown "$FORUSER" "$user_pref_path/$i"
|
||||
else
|
||||
Throw "Missing preference file $i for user $FORUSER - this is needed for the system-wide settings copy
|
||||
|
||||
If running this in a VM
|
||||
Copy a known good $FULL_NAME
|
||||
from ~/Library/Preferences
|
||||
on a real mac
|
||||
(or a Linux .config/$FULL_NAME)
|
||||
to $user_pref_path on this VM
|
||||
|
||||
Then run this script again and use the -i ID option to
|
||||
set the ID of this ${APP_NAME} node.
|
||||
|
||||
Or set AUTO_CREATE_CONFIG=1 to auto-create empty config files.
|
||||
"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
DownloadAssets()
|
||||
{
|
||||
APP_NAME_LOWER=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Read real bundle identifier from the app's Info.plist (matches macos.rs get_bundle_id())
|
||||
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$APP/Contents/Info.plist" 2>/dev/null)
|
||||
if [ -z "$BUNDLE_ID" ]; then
|
||||
Msg "Warning: Cannot read CFBundleIdentifier from $APP/Contents/Info.plist — using com.carriez.${APP_NAME_LOWER} as fallback"
|
||||
BUNDLE_ID="com.carriez.${APP_NAME_LOWER}"
|
||||
fi
|
||||
|
||||
Msg 'Loading assets from github'
|
||||
# Download plist templates and apply replacements to match correct_app_name() in macos.rs:
|
||||
# 1. Replace AssociatedBundleIdentifiers (com.carriez.rustdesk → real bundle ID)
|
||||
# 2. Replace lowercase "rustdesk" → lowercase app name (for log paths etc.)
|
||||
# 3. Replace "RustDesk" → APP_NAME (for labels, executable name, app path)
|
||||
AGENT=$(curl -sf 'https://raw.githubusercontent.com/rustdesk/rustdesk/master/src/platform/privileges_scripts/agent.plist' \
|
||||
| sed -e "s|com.carriez.rustdesk|${BUNDLE_ID}|g" \
|
||||
-e "s|rustdesk|${APP_NAME_LOWER}|g" \
|
||||
-e "s|RustDesk|${APP_NAME}|g" \
|
||||
-e "s|/Applications/${APP_NAME}.app|$APP|g")
|
||||
DAEMON=$(curl -sf 'https://raw.githubusercontent.com/rustdesk/rustdesk/master/src/platform/privileges_scripts/daemon.plist' \
|
||||
| sed -e "s|com.carriez.rustdesk|${BUNDLE_ID}|g" \
|
||||
-e "s|rustdesk|${APP_NAME_LOWER}|g" \
|
||||
-e "s|RustDesk|${APP_NAME}|g" \
|
||||
-e "s|/Applications/${APP_NAME}.app|$APP|g")
|
||||
# Apply same correct_app_name() replacements to install.scpt (matches macos.rs:200)
|
||||
ASCPT=$(curl -sf 'https://raw.githubusercontent.com/rustdesk/rustdesk/master/src/platform/privileges_scripts/install.scpt' \
|
||||
| sed -e "s|com.carriez.rustdesk|${BUNDLE_ID}|g" \
|
||||
-e "s|rustdesk|${APP_NAME_LOWER}|g" \
|
||||
-e "s|RustDesk|${APP_NAME}|g")
|
||||
[ -z "$AGENT" -o -z "$DAEMON" -o -z "$ASCPT" ] && Throw "Error getting one or more assets - url probably changed. (A/D/S sizes: ${#AGENT}/${#DAEMON}/${#ASCPT})"
|
||||
}
|
||||
|
||||
InstallViaAppleScript()
|
||||
{
|
||||
Msg "Current User: $FORUSER"
|
||||
|
||||
# This script is pretty stoopid. It does not get the home directory e.g. by algorithm
|
||||
$SUDO osascript -e "$ASCPT" "$DAEMON" "$AGENT" "$FORUSER"
|
||||
}
|
||||
|
||||
KickstartServerGUI()
|
||||
{
|
||||
if [ -s "$agent_path" ]; then
|
||||
Msg "Kickstart server for $FORUID ($FORUSER)"
|
||||
$SUDO launchctl enable "gui/$FORUID/${FULL_NAME}_server" 2>/dev/null
|
||||
# kickstart -k = kill existing, -p = print PID
|
||||
if ! $SUDO launchctl kickstart -kp "gui/$FORUID/${FULL_NAME}_server" 2>/dev/null; then
|
||||
Msg "Kickstart skipped — GUI session for $FORUSER not available (expected in MDM context). Agent will auto-start on user login."
|
||||
fi
|
||||
else
|
||||
Err "Ooops - where is $agent_path"
|
||||
fi
|
||||
}
|
||||
|
||||
InstallViaShell()
|
||||
{
|
||||
Msg "Install agent: $agent_path"
|
||||
echo "$AGENT" | $SUDO tee "$agent_path" >/dev/null
|
||||
|
||||
Msg "Install daemon: $daemon_path"
|
||||
echo "$DAEMON" | $SUDO tee "$daemon_path" >/dev/null
|
||||
|
||||
# Should already be - but incase they were created by user error
|
||||
$SUDO chown 0:0 "$agent_path" "$daemon_path"
|
||||
|
||||
# Clear stale stop-service flag from previous uninstall (prevents service from
|
||||
# ignoring rendezvous connections even though launchd started it successfully).
|
||||
# The flag is written by RustDesk's uninstall_service() into {APP_NAME}2.toml
|
||||
# (Config2 options store). Controlled by CLEAR_STOP_SERVICE option.
|
||||
if [ "$CLEAR_STOP_SERVICE" -eq 1 ]; then
|
||||
local opts_toml="${APP_NAME}2.toml"
|
||||
for _cfg in "$user_pref_path/$opts_toml" "$root_pref_path/$opts_toml"; do
|
||||
if [ -f "$_cfg" ] && $SUDO grep -qE '^[[:space:]]*"?stop-service"?[[:space:]]*=' "$_cfg" 2>/dev/null; then
|
||||
Msg "Clearing stale stop-service flag in $_cfg"
|
||||
$SUDO sed -i'.bak' '/^[[:space:]]*"*stop-service"*[[:space:]]*=/d' "$_cfg"
|
||||
$SUDO rm -f "$_cfg.bak"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
local i
|
||||
$SUDO mkdir -p "$root_pref_path"
|
||||
for i in $pref_files; do
|
||||
$SUDO cp -a "$user_pref_path/$i" "$root_pref_path"
|
||||
done
|
||||
|
||||
# Use modern launchctl API (bootstrap/bootout) instead of deprecated load/remove
|
||||
# Daemon = system-wide (system domain), Agent = per-user (gui/<uid> domain)
|
||||
|
||||
Msg "Unload / Reload Daemon (system domain)"
|
||||
# Ensure clean state: disable + bootout (handles cases where service is stuck)
|
||||
$SUDO launchctl disable "system/${FULL_NAME}_service" 2>/dev/null
|
||||
$SUDO launchctl bootout "system/${FULL_NAME}_service" 2>/dev/null
|
||||
$SUDO launchctl bootout system "$daemon_path" 2>/dev/null
|
||||
# Also try legacy unload in case bootstrap API state is inconsistent
|
||||
$SUDO launchctl unload -w "$daemon_path" 2>/dev/null
|
||||
sleep 1
|
||||
if ! $SUDO launchctl bootstrap system "$daemon_path" 2>/dev/null; then
|
||||
Msg "bootstrap failed — falling back to launchctl load -w"
|
||||
if ! $SUDO launchctl load -w "$daemon_path"; then
|
||||
Err "Daemon load also failed. Checking /tmp/${APP_NAME_LOWER}_service.err for details."
|
||||
$SUDO cat "/tmp/${APP_NAME_LOWER}_service.err" 2>/dev/null
|
||||
Throw "Failed to load daemon ${FULL_NAME}_service"
|
||||
fi
|
||||
fi
|
||||
$SUDO launchctl enable "system/${FULL_NAME}_service" 2>/dev/null
|
||||
|
||||
Msg "Unload / Reload Agent for $FORUSER (gui/$FORUID domain)"
|
||||
# Agent must be loaded into the user's GUI domain, not the system domain.
|
||||
# In MDM postinstall context, the GUI domain may not exist yet (user not logged in).
|
||||
# In that case, bootstrap will fail — this is expected. launchd will automatically
|
||||
# load the agent from /Library/LaunchAgents/ when the user logs in.
|
||||
$SUDO launchctl bootout "gui/$FORUID/${FULL_NAME}_server" >/dev/null 2>&1
|
||||
if $SUDO launchctl bootstrap "gui/$FORUID" "$agent_path" 2>/dev/null; then
|
||||
$SUDO launchctl enable "gui/$FORUID/${FULL_NAME}_server"
|
||||
else
|
||||
Msg "Agent bootstrap skipped — GUI session for $FORUSER not available (expected in MDM context). Agent will auto-load on user login."
|
||||
fi
|
||||
}
|
||||
|
||||
FindApp()
|
||||
{
|
||||
# First check DEFAULTPATH directly (supports custom paths outside /Applications)
|
||||
if [ -d "$DEFAULTPATH" ]; then
|
||||
APP="$DEFAULTPATH"
|
||||
Msg "Found app at DEFAULTPATH: $APP"
|
||||
return
|
||||
fi
|
||||
# Fall back: scan /Applications up to two levels deep
|
||||
local d
|
||||
APP=''
|
||||
for d in /Applications /Applications/* /Applications/*/*; do if [ -d "$d" ]; then
|
||||
[ -d "$d/${APP_NAME}.app" ] && { APP="$d/${APP_NAME}.app"; Msg "Found app: $APP"; break; }
|
||||
fi; done
|
||||
}
|
||||
|
||||
IsServiceLoaded()
|
||||
{
|
||||
local p
|
||||
p=$($SUDO launchctl list 2>/dev/null | while read pid sta com; do
|
||||
[ "$com" = "$1" ] && { echo "$pid:$sta"; break; }
|
||||
done)
|
||||
[ -n "$p" ]
|
||||
}
|
||||
|
||||
Help()
|
||||
{
|
||||
Throw "
|
||||
|
||||
Usage: ${0##*/} [options]
|
||||
|
||||
-a Show Agent plist contents
|
||||
-d Show Daemon plist contents
|
||||
-h This help
|
||||
-i id Set ${APP_NAME} ID (does not install service)
|
||||
-p pwd Set ${APP_NAME} permanent password (does not install service)
|
||||
-s Show Script contents
|
||||
-S Use AppleScript to install - not recommended
|
||||
-u user Specify target user (for MDM/headless context)
|
||||
-v List the Agent/Daemon plists installed
|
||||
"
|
||||
}
|
||||
|
||||
|
||||
# Begin Script Body
|
||||
Msg "##################################################################"
|
||||
Msg "# $(date) | Starting ${0##*/}"
|
||||
Msg "#* *#"
|
||||
Msg "#** If prompted for a password - that is proably sudo **#"
|
||||
Msg "##################################################################"
|
||||
|
||||
# === Phase 1: Parse all options ===
|
||||
while getopts 'adhi:p:sSvu:' o; do case "$o" in
|
||||
'a'|'d'|'s') VIEW_ONLY=1 ;;
|
||||
'h') Help ;;
|
||||
'i') USE_ID="$OPTARG" ;;
|
||||
'p') USE_PWD="$OPTARG" ;;
|
||||
'u') FORUSER_OVERRIDE="$OPTARG" ;;
|
||||
'v') ls -l "$agent_path" "$daemon_path" 2>/dev/null; Throw 'Done.' ;;
|
||||
'S') USESCRIPT=1 ;;
|
||||
*) ;;
|
||||
esac; done
|
||||
OPTIND=1 # Reset for second pass
|
||||
|
||||
# === Phase 1.5: Handle -i/-p (set ID/password only, no service install) ===
|
||||
if [ -n "$USE_ID" ] || [ -n "$USE_PWD" ]; then
|
||||
FindApp
|
||||
while [ ! -d "$APP" ]; do
|
||||
Msg "Could not find ${APP_NAME}.app installed in /Applications/... - sleeping 30"
|
||||
sleep 30
|
||||
FindApp
|
||||
done
|
||||
local_bin="${APP}/Contents/MacOS/${APP_NAME}"
|
||||
if [ -n "$USE_ID" ]; then
|
||||
Msg "Setting ID to: $USE_ID"
|
||||
$SUDO "$local_bin" --set-id "$USE_ID"
|
||||
fi
|
||||
if [ -n "$USE_PWD" ]; then
|
||||
Msg "Setting permanent password"
|
||||
$SUDO "$local_bin" --password "$USE_PWD"
|
||||
fi
|
||||
Throw "Done." 0
|
||||
fi
|
||||
|
||||
# === Phase 2: Find app + download assets (needed by -a/-d/-s view options) ===
|
||||
FindApp
|
||||
|
||||
# Wait for app to be installed (e.g. MDM deployment in progress)
|
||||
# For view-only options (-a/-d/-s), don't loop — fail fast
|
||||
while [ ! -d "$APP" ]; do
|
||||
if [ "$VIEW_ONLY" -eq 1 ]; then
|
||||
Throw "Cannot find ${APP_NAME}.app in /Applications — required to generate plist/script content"
|
||||
fi
|
||||
Msg "Could not find ${APP_NAME}.app installed in /Applications/... - sleeping 30"
|
||||
sleep 30
|
||||
FindApp
|
||||
done
|
||||
|
||||
Msg "##################################################################"
|
||||
# DownloadAssets requires $APP to be set (reads Info.plist, substitutes app path in plists)
|
||||
DownloadAssets
|
||||
|
||||
# === Phase 3: Handle view options that only need downloaded assets (-a/-d/-s) ===
|
||||
# Also collect install option (-S) for later use
|
||||
while getopts 'adhsSvu:' o; do case "$o" in
|
||||
'a') echo "$AGENT"; Throw 'Done.' ;;
|
||||
'd') echo "$DAEMON"; Throw 'Done.' ;;
|
||||
'h') Help ;;
|
||||
's') echo "$ASCPT"; Throw 'Done.' ;;
|
||||
'S') USESCRIPT=1 ;;
|
||||
'u') ;; # Already handled in phase 1
|
||||
'v') ;; # Already handled in phase 1
|
||||
esac; done; shift $(($OPTIND - 1))
|
||||
|
||||
# === Phase 4: Resolve target user (only needed for actual installation) ===
|
||||
|
||||
# Apply user override if specified via -u
|
||||
if [ -n "$FORUSER_OVERRIDE" ]; then
|
||||
FORUSER="$FORUSER_OVERRIDE"
|
||||
fi
|
||||
|
||||
# Validate FORUSER
|
||||
if [ -z "$FORUSER" ] || [ "$FORUSER" = "root" ]; then
|
||||
Throw "Cannot determine target user. In MDM context, use -u <username> to specify the target user."
|
||||
fi
|
||||
|
||||
FORUID=$(id -u "$FORUSER" 2>/dev/null) || Throw "User '$FORUSER' not found on this system"
|
||||
|
||||
# Resolve user's home directory for preference path
|
||||
# Always use dscl when FORUSER differs from current user (covers both root and non-root with -u)
|
||||
if [ "$FORUSER" != "$(id -un)" ]; then
|
||||
user_home=$(dscl . -read "/Users/$FORUSER" NFSHomeDirectory 2>/dev/null | sed 's/^NFSHomeDirectory:[[:space:]]*//')
|
||||
[ -z "$user_home" ] && Throw "Cannot resolve home directory for user $FORUSER"
|
||||
else
|
||||
user_home="$HOME"
|
||||
fi
|
||||
user_pref_path="${user_home}/Library/Preferences/$FULL_NAME"
|
||||
|
||||
# === Phase 5: Install ===
|
||||
|
||||
IsServiceLoaded "${FULL_NAME}_service" && \
|
||||
Throw "${FULL_NAME}_service is already loaded"
|
||||
|
||||
# EnsureConfig creates preference dirs/files only when actually installing (no side effects for view options)
|
||||
EnsureConfig
|
||||
|
||||
Msg "******* STARTING SERVICE INSTALLATION *******"
|
||||
[ $USESCRIPT -eq 1 ] && InstallViaAppleScript || InstallViaShell
|
||||
KickstartServerGUI
|
||||
|
||||
Msg "Done. Note - if you move the app, you will have to reinstall or edit the LaunchDaemon and LaunchAgent plists"
|
||||
ls -l "$agent_path" "$daemon_path"
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## When Do You Need This Script?
|
||||
|
||||
For normal desktop installations, just click the "Install" button inside the RustDesk app. This script is designed for:
|
||||
|
||||
- Remote deployment via SSH (no GUI access)
|
||||
- MDM batch deployment (postinstall script)
|
||||
- VM environments (headless setup)
|
||||
- Any scenario where you need to automate service registration without user interaction
|
||||
|
||||
## What the Script Does
|
||||
|
||||
The script installs two launchd plist files and loads them:
|
||||
|
||||
| Component | Plist Path | Runs As | Purpose |
|
||||
|-----------|-----------|---------|---------|
|
||||
| Daemon | `/Library/LaunchDaemons/com.carriez.<APP>_service.plist` | root | IPC config management service — runs at boot, no login required |
|
||||
| Agent | `/Library/LaunchAgents/com.carriez.<APP>_server.plist` | user | RustDesk server process — handles screen capture, input, audio, remote connections |
|
||||
|
||||
Both are set to auto-start: the daemon starts at boot, the agent starts at the login screen (LoginWindow session) and persists through user login (Aqua session).
|
||||
|
||||
## What the Script Does NOT Do
|
||||
|
||||
The script does not grant macOS privacy permissions. Screen Recording, Accessibility, and Input Monitoring permissions must be granted separately:
|
||||
|
||||
- Manually: System Settings > Privacy & Security
|
||||
- MDM: Deploy a PPPC (Privacy Preferences Policy Control) configuration profile
|
||||
|
||||
Without these permissions, the service will run, but remote control will not work properly.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS with the app already installed in `/Applications/` (or a subfolder)
|
||||
- Admin privileges (`sudo`)
|
||||
- Internet access (to download plist templates from GitHub)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Standard RustDesk
|
||||
|
||||
```bash
|
||||
sudo bash install_service.sh
|
||||
```
|
||||
|
||||
### Custom Client
|
||||
|
||||
Edit the `DEFAULTPATH` variable at the top of the script:
|
||||
|
||||
```bash
|
||||
DEFAULTPATH='/Applications/MyCustomApp.app'
|
||||
```
|
||||
|
||||
Everything else (app name, plist labels, config paths) is auto-derived from this single path:
|
||||
|
||||
| Item | Example (`DEFAULTPATH = /Applications/MyApp.app`) |
|
||||
|------|---------------------------------------------------|
|
||||
| APP_NAME | `MyApp` |
|
||||
| Plist label | `com.carriez.MyApp_service` / `com.carriez.MyApp_server` |
|
||||
| Config dir | `~/Library/Preferences/com.carriez.MyApp/` |
|
||||
| Config files | `MyApp.toml`, `MyApp2.toml` |
|
||||
|
||||
> The app's bundle identifier (e.g., `com.mycompany.myapp`) does not affect these paths. RustDesk always uses `com.carriez.<APP_NAME>` internally.
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
Usage: install_service.sh [options]
|
||||
|
||||
-i id Set the RustDesk ID (does not install service)
|
||||
-p pwd Set a permanent password (does not install service)
|
||||
-u user Specify target user (for MDM/headless context)
|
||||
-S Use AppleScript to install (not recommended)
|
||||
-a Show generated Agent plist (view only)
|
||||
-d Show generated Daemon plist (view only)
|
||||
-s Show generated AppleScript (view only)
|
||||
-v List installed plist files
|
||||
-h Help
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Set ID and password (service must already be installed)
|
||||
sudo bash install_service.sh -i 123456789 -p "MySecurePassword"
|
||||
|
||||
# Install the auto-start service
|
||||
sudo bash install_service.sh
|
||||
|
||||
# MDM deployment — specify the target user explicitly
|
||||
sudo bash install_service.sh -u johndoe
|
||||
|
||||
# Preview the generated daemon plist without installing
|
||||
bash install_service.sh -d
|
||||
```
|
||||
|
||||
## Configuration Flags
|
||||
|
||||
Two flags at the top of the script control install behavior:
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `AUTO_CREATE_CONFIG` | `1` | Auto-create empty config files if they don't exist. Set to `0` to require pre-configured toml files. |
|
||||
| `CLEAR_STOP_SERVICE` | `0` | Remove the `stop-service` flag from existing config on reinstall. Set to `1` for MDM redeployment to a machine that previously had the service uninstalled. |
|
||||
|
||||
## Re-running the Script
|
||||
|
||||
If the service is already loaded, the script exits immediately with:
|
||||
|
||||
```
|
||||
com.carriez.<APP>_service is already loaded
|
||||
```
|
||||
|
||||
This is safe — existing plist files and auto-start are not affected. To force reinstall, unload the service first:
|
||||
|
||||
```bash
|
||||
sudo launchctl bootout system/com.carriez.<APP_NAME>_service
|
||||
```
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
# Replace <APP_NAME> with your app name (e.g., RustDesk, MyApp)
|
||||
sudo launchctl bootout system/com.carriez.<APP_NAME>_service
|
||||
sudo launchctl bootout gui/$(id -u)/com.carriez.<APP_NAME>_server
|
||||
sudo rm /Library/LaunchDaemons/com.carriez.<APP_NAME>_service.plist
|
||||
sudo rm /Library/LaunchAgents/com.carriez.<APP_NAME>_server.plist
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| "Cannot determine target user" | In MDM context, use `-u <username>` to specify the user |
|
||||
| "Could not find app installed in /Applications" | The script retries every 30 seconds. Ensure the `.app` bundle exists in `/Applications/` or a subfolder |
|
||||
| Service loaded but not connecting | Set `CLEAR_STOP_SERVICE=1` and reinstall — a previous uninstall may have left a `stop-service` flag in config |
|
||||
| Remote control not working after install | Privacy permissions not granted. Deploy a PPPC profile via MDM, or grant Screen Recording / Accessibility / Input Monitoring manually in System Settings |
|
||||
|
||||
Reference in New Issue
Block a user