Skip to content
Open
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
285 changes: 256 additions & 29 deletions content/self-host/client-deployment/_index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,79 +225,306 @@ winget install --id=RustDesk.RustDesk -e

## macOS Bash

This script handles the full macOS deployment lifecycle: downloading the latest release (auto-detecting Apple Silicon vs Intel), installing to `/Applications`, initializing the config directory as the logged-in user, setting the password and relay server configuration, and optionally launching in headless server mode. It includes proper error handling, timestamped logging to `/tmp/rustdesk-install.log`, and a LaunchAgent fallback for machines where no user is logged in at install time (e.g., DEP/MDM enrollments).

```sh
#!/bin/bash

# Assign the value random password to the password variable
rustdesk_pw=$(openssl rand -hex 4)

# Get your config string from your Web portal and Fill Below
rustdesk_cfg="configstring"
## Relay server configuration
## Set these to your own RustDesk relay server details
RELAY_SERVER="your.relay.server.com"
RELAY_KEY="YOUR_RELAY_KEY_HERE"
MSP="yourorg"
## Server mode - set to "y" to launch RustDesk in headless mode on login
SERVER="Y"

LOG="/tmp/rustdesk-install.log"
rm -f "$LOG"
# Create log file with world-writable permissions so the user-context subscript can write to it
touch "$LOG"
chmod 666 "$LOG"
Comment on lines +246 to +248
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Log file permissions and secret logging expose credentials.

World-writable /tmp/rustdesk-install.log plus logging of RustDesk password and relay key leaks sensitive data and allows tampering.

🔐 Suggested hardening
-touch "$LOG"
-chmod 666 "$LOG"
+touch "$LOG"
+chmod 600 "$LOG"
...
-echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk Password: $rustdesk_pw" | tee -a "$LOG"
+echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk Password: [REDACTED]" | tee -a "$LOG"
...
-echo "Relay Key: \$RELAY_KEY"
+echo "Relay Key: [REDACTED]"

Also applies to: 385-387, 466-468

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/self-host/client-deployment/_index.en.md` around lines 246 - 248, The
log file is created world-writable via touch "$LOG" and chmod 666 "$LOG" (and
repeated in the other occurrences), and secrets (RustDesk password and relay
key) are being written to it; change the log creation to set restrictive
permissions (e.g., owner-only 600 or 640) and ensure the file owner is the
intended service account, and stop writing sensitive values to the log — redact
or omit the RustDesk password and relay key from any logging statements (or
store them in a secure vault/secure file with limited access) and update all
instances that use touch "$LOG"/chmod 666 "$LOG" to use the secure permission
pattern and remove secret output.

echo "Version 1.4.6.85800" >> ${LOG}

# Trap unexpected exits and log them
_on_exit() {
local code=$?
[[ $code -ne 0 ]] && echo "$(date '+%Y-%m-%d %H:%M:%S') | FATAL: Script terminated unexpectedly (exit code $code)" | tee -a "$LOG"
}
trap _on_exit EXIT

################################## Please Do Not Edit Below This Line #########################################


######################################################################
############# Please Do Not Edit Below This Line #####################
######################################################################

# Root password request for privilege escalation
[ "$UID" -eq 0 ] || exec sudo bash "$0" "$@"

# Helper functions
console_user() {
/usr/bin/who | grep console | cut -d ' ' -f1 | head -1
}

run_as_user() {
local u
u="$(console_user || true)"
[[ -n "$u" && "$u" != "loginwindow" && "$u" != "_mbsetupuser" ]] || return 1
/bin/launchctl asuser "$(/usr/bin/id -u "$u")" /usr/bin/sudo -u "$u" "$@"
}

# Specify the mount point for the DMG (temporary directory)
mount_point="/Volumes/RustDesk"

# Download the rustdesk.dmg file
echo "Downloading RustDesk Now"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Downloading RustDesk Now" | tee -a "$LOG"

if [[ $(arch) == 'arm64' ]]; then
rd_link=$(curl -sL https://github.com/rustdesk/rustdesk/releases/latest | grep -Eo "(http|https)://[a-zA-Z0-9./?=_-]*/\d{1}.\d{1,2}.\d{1,2}/rustdesk.\d{1}.\d{1,2}.\d{1,2}.aarch64.dmg")
dmg_file=$(echo $rd_link | grep -Eo "rustdesk.\d{1}.\d{1,2}.\d{1,2}.aarch64.dmg")
curl -L "$rd_link" --output "$dmg_file"
dmg_file="/tmp/$(echo $rd_link | grep -Eo "rustdesk.\d{1}.\d{1,2}.\d{1,2}.aarch64.dmg")"
else
rd_link=$(curl -sL https://github.com/rustdesk/rustdesk/releases/latest | grep -Eo "(http|https)://[a-zA-Z0-9./?=_-]*/\d{1}.\d{1,2}.\d{1,2}/rustdesk.\d{1}.\d{1,2}.\d{1,2}.x86_64.dmg")
dmg_file=$(echo $rd_link | grep -Eo "rustdesk.\d{1}.\d{1,2}.\d{1,2}.x86_64.dmg")
curl -L "$rd_link" --output "$dmg_file"
dmg_file="/tmp/$(echo $rd_link | grep -Eo "rustdesk.\d{1}.\d{1,2}.\d{1,2}.x86_64.dmg")"
Comment on lines 286 to +290
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify regex behavior used in the script (read-only).
set -euo pipefail

sample='https://github.com/rustdesk/rustdesk/releases/download/1.4.0/rustdesk-1.4.0-aarch64.dmg'

echo "Test 1: current style with \\d and grep -E"
echo "$sample" | grep -Eo '\d{1}\.\d{1,2}\.\d{1,2}' || echo "NO_MATCH"

echo "Test 2: ERE-safe style"
echo "$sample" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' || echo "NO_MATCH"

Repository: rustdesk/doc.rustdesk.com

Length of output: 154


Replace \d with [0-9]+ for portable regex matching with grep -Eo.

The current pattern using \d is not valid POSIX ERE and fails to match version numbers on macOS/BSD (and some Linux configurations). The shell test confirms grep -Eo '\d{1}\.\d{1,2}\.\d{1,2}' returns NO_MATCH while grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' correctly extracts version strings. This causes silent extraction failures and installation aborts.

Replace at lines 286–290 and 294–297:

Affected locations Lines 286–290 (aarch64) and 294–297 (x86_64)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/self-host/client-deployment/_index.en.md` around lines 286 - 290, The
grep -Eo regexes used when setting rd_link and dmg_file (symbols: rd_link and
dmg_file) use \d which is not POSIX ERE; update both patterns for aarch64 and
x86_64 to use [0-9]+ with escaped dots (e.g., replace occurrences like
"\d{1}\.\d{1,2}\.\d{1,2}" and the full filename patterns using \d with
corresponding [0-9]+ quantifiers) so grep -Eo reliably extracts version numbers
and the rustdesk .dmg filenames on macOS/BSD and other environments.

fi

# Verify download link was found
if [ -z "$rd_link" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Failed to find RustDesk download link. Installation aborted." | tee -a "$LOG"
exit 1
fi

# Download the DMG
echo "$(date '+%Y-%m-%d %H:%M:%S') | Download URL: $rd_link" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Architecture: $(arch)" | tee -a "$LOG"
if ! curl -fL "$rd_link" --output "$dmg_file"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Failed to download RustDesk. Installation aborted." | tee -a "$LOG"
exit 1
fi

# Verify file was downloaded and is not empty
if [ ! -s "$dmg_file" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Downloaded file is empty or missing. Installation aborted." | tee -a "$LOG"
exit 1
fi

# Mount the DMG file to the specified mount point
echo "$(date '+%Y-%m-%d %H:%M:%S') | Mounting DMG: $dmg_file" | tee -a "$LOG"
hdiutil attach "$dmg_file" -mountpoint "$mount_point" &> /dev/null

# Check if the mounting was successful
if [ $? -eq 0 ]; then
# Move the contents of the mounted DMG to the /Applications folder
cp -R "$mount_point/RustDesk.app" "/Applications/" &> /dev/null
echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk.app copied to /Applications/" | tee -a "$LOG"
Comment on lines 320 to +321
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Copy success is assumed without checking return code.

cp -R ... &> /dev/null can fail silently, but success is logged immediately; this can mask install failures and cause downstream errors.

💡 Suggested fix
-    cp -R "$mount_point/RustDesk.app" "/Applications/" &> /dev/null
-    echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk.app copied to /Applications/" | tee -a "$LOG"
+    if cp -R "$mount_point/RustDesk.app" "/Applications/" &> /dev/null; then
+        echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk.app copied to /Applications/" | tee -a "$LOG"
+    else
+        echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Failed to copy RustDesk.app to /Applications." | tee -a "$LOG"
+        exit 1
+    fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cp -R "$mount_point/RustDesk.app" "/Applications/" &> /dev/null
echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk.app copied to /Applications/" | tee -a "$LOG"
if cp -R "$mount_point/RustDesk.app" "/Applications/" &> /dev/null; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk.app copied to /Applications/" | tee -a "$LOG"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Failed to copy RustDesk.app to /Applications." | tee -a "$LOG"
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/self-host/client-deployment/_index.en.md` around lines 320 - 321, The
script currently assumes the copy succeeded by running cp -R
"$mount_point/RustDesk.app" "/Applications/" &> /dev/null and immediately
echoing a success line to $LOG; change this to check cp's exit status (or use an
if cp ...; then ... else ...) so failures are detected: run cp without silencing
stderr (or redirect stderr to the $LOG), then test the command result and on
success write the timestamped success message to $LOG, otherwise write a
timestamped error with the cp exit code and stderr and exit non‑zero to stop the
installer; update the lines referencing cp -R "$mount_point/RustDesk.app"
"/Applications/" and the subsequent echo to implement this check.


# Unmount the DMG file
hdiutil detach "$mount_point" &> /dev/null
else
echo "Failed to mount the RustDesk DMG. Installation aborted."
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Failed to mount the RustDesk DMG. Installation aborted." | tee -a "$LOG"
exit 1
fi

# Run the rustdesk command with --get-id and store the output in the rustdesk_id variable
cd /Applications/RustDesk.app/Contents/MacOS/
rustdesk_id=$(./RustDesk --get-id)
# Initialize the config directory via Launch Services (RustDesk becomes responsible process for TCC)
CONSOLE_USER=$(/usr/bin/who | grep console | cut -d ' ' -f1 | head -1)
echo "$(date '+%Y-%m-%d %H:%M:%S') | Console user detected: '${CONSOLE_USER:-<empty>}'" | tee -a "$LOG"

# Apply new password to RustDesk
./RustDesk --server &
/Applications/RustDesk.app/Contents/MacOS/RustDesk --password $rustdesk_pw &> /dev/null
if [[ -z "$CONSOLE_USER" || "$CONSOLE_USER" == "loginwindow" || "$CONSOLE_USER" == "_mbsetupuser" ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: No valid console user found. Cannot initialize RustDesk config directory." | tee -a "$LOG"
exit 1
fi
Comment on lines +334 to +337
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fallback path is currently unreachable for no-user installs.

exit 1 on Line 336 aborts before the LaunchAgent fallback path, so DEP/MDM “no logged-in user” installs never reach the intended recovery flow.

💡 Suggested fix
-if [[ -z "$CONSOLE_USER" || "$CONSOLE_USER" == "loginwindow" || "$CONSOLE_USER" == "_mbsetupuser" ]]; then
-    echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: No valid console user found. Cannot initialize RustDesk config directory." | tee -a "$LOG"
-    exit 1
-fi
+if [[ -z "$CONSOLE_USER" || "$CONSOLE_USER" == "loginwindow" || "$CONSOLE_USER" == "_mbsetupuser" ]]; then
+    echo "$(date '+%Y-%m-%d %H:%M:%S') | No valid console user found. Deferring user-scoped configuration to LaunchAgent." | tee -a "$LOG"
+else
+    # existing user-initialization + wait-for-config flow
+fi

Also applies to: 497-516

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/self-host/client-deployment/_index.en.md` around lines 334 - 337, The
script currently aborts when CONSOLE_USER is missing (the block checking
CONSOLE_USER and writing to LOG), which prevents the LaunchAgent fallback for
DEP/MDM no-user installs; instead of calling exit 1 in that conditional, log the
error to LOG and set a marker (e.g., export NO_CONSOLE=true or leave
CONSOLE_USER empty) so execution continues into the LaunchAgent fallback flow;
update the conditional around CONSOLE_USER (the block that echoes to LOG) to not
exit, and ensure later code that configures the RustDesk directory/LaunchAgent
checks NO_CONSOLE or an empty CONSOLE_USER to take the fallback path.


/Applications/RustDesk.app/Contents/MacOS/RustDesk --config $rustdesk_cfg
echo "$(date '+%Y-%m-%d %H:%M:%S') | Launching RustDesk as '$CONSOLE_USER' to initialize config directory..." | tee -a "$LOG"
if ! run_as_user open /Applications/RustDesk.app --args --server >> "$LOG" 2>&1; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: Failed to launch RustDesk as '$CONSOLE_USER'. Check if RustDesk.app is installed." | tee -a "$LOG"
exit 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk launched successfully." | tee -a "$LOG"

# Wait for RustDesk to initialize its config directory rather than using a fixed sleep.
# Also confirm the directory is owned by the console user, not root.
RDCONFIG_DIR="/Users/$CONSOLE_USER/Library/Preferences/com.carriez.RustDesk"
MAX_WAIT=30
WAITED=0
echo "$(date '+%Y-%m-%d %H:%M:%S') | Waiting up to ${MAX_WAIT}s for config directory to appear and be user-owned..." | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Expected path: $RDCONFIG_DIR" | tee -a "$LOG"

while [[ $WAITED -lt $MAX_WAIT ]]; do
if [[ -d "$RDCONFIG_DIR" && "$(stat -f '%Su' "$RDCONFIG_DIR")" == "$CONSOLE_USER" ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | Config directory ready and user-owned after ${WAITED}s." | tee -a "$LOG"
break
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') | Still waiting... (${WAITED}s elapsed)" | tee -a "$LOG"
sleep 1
WAITED=$((WAITED + 1))
done

if [[ ! -d "$RDCONFIG_DIR" || "$(stat -f '%Su' "$RDCONFIG_DIR")" != "$CONSOLE_USER" ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | ERROR: RustDesk config directory never appeared or is not owned by '$CONSOLE_USER' after ${MAX_WAIT}s. Aborting." | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Diagnostic — directory exists: $([[ -d "$RDCONFIG_DIR" ]] && echo yes || echo no)" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Diagnostic — directory owner: $(stat -f '%Su' "$RDCONFIG_DIR" 2>/dev/null || echo 'N/A')" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Diagnostic — RustDesk processes: $(pgrep -la RustDesk 2>/dev/null || echo none)" | tee -a "$LOG"
exit 1
fi

# Kill all processes named RustDesk
rdpid=$(pgrep RustDesk)
kill $rdpid &> /dev/null
# Install the password (config operation, no TCC involvement)
echo "$(date '+%Y-%m-%d %H:%M:%S') | Setting RustDesk password..." | tee -a "$LOG"
/Applications/RustDesk.app/Contents/MacOS/RustDesk --password $rustdesk_pw
sleep 1
# Get RustDesk ID
rustdesk_id=$(/Applications/RustDesk.app/Contents/MacOS/RustDesk --get-id)
sleep 1

echo "..............................................."
# Check if the rustdesk_id is not empty
if [ -n "$rustdesk_id" ]; then
echo "RustDesk ID: $rustdesk_id"
else
echo "Failed to get RustDesk ID."
# Kill RustDesk
pkill -x RustDesk &>/dev/null || true

echo "$(date '+%Y-%m-%d %H:%M:%S') | ..............................................." | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Install complete!" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk ID: $rustdesk_id" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | RustDesk Password: $rustdesk_pw" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | ..............................................." | tee -a "$LOG"

# Configuration paths
CONFIG_SCRIPT_DIR="/usr/local/bin"
CONFIG_SCRIPT="$CONFIG_SCRIPT_DIR/configure_rustdesk.sh"
LAUNCHAGENT_PLIST="/Library/LaunchAgents/com.${MSP}.rustdesk-config.plist"

# Create directory for config script
[[ ! -d "$CONFIG_SCRIPT_DIR" ]] && mkdir -p "$CONFIG_SCRIPT_DIR"


# Write the configure script
cat > "$CONFIG_SCRIPT" <<SCRIPT_EOF
#!/bin/bash

# RustDesk Client Configuration Script
# Runs as the logged-in user to configure relay server

LOG="$LOG"
exec > >(tee -a "\$LOG") 2>&1

echo "=== RustDesk Config Script Started: \$(date) ==="

RELAY_SERVER="$RELAY_SERVER"
RELAY_KEY="$RELAY_KEY"
SERVER_MODE="$SERVER"
CONSOLE_USER=\$(/usr/bin/who | grep console | cut -d ' ' -f1 | head -1)

echo "Console user: \$CONSOLE_USER"

if [[ -z "\$CONSOLE_USER" || "\$CONSOLE_USER" == "loginwindow" ]]; then
echo "ERROR: No valid user logged in"
exit 1
fi

# Echo the value of the password variable
echo "Password: $rustdesk_pw"
CONFIG_DIR="/Users/\$CONSOLE_USER/Library/Preferences/com.carriez.RustDesk"
BINARY="/Applications/RustDesk.app/Contents/MacOS/RustDesk"

echo "Config dir: \$CONFIG_DIR"
echo "Binary: \$BINARY"

# Verify binary exists
if [[ ! -x "\$BINARY" ]]; then
echo "ERROR: RustDesk binary not found or not executable"
exit 1
fi

# Verify config directory exists
if [[ ! -d "\$CONFIG_DIR" ]]; then
echo "ERROR: Config directory does not exist: \$CONFIG_DIR"
exit 1
fi

echo "=== Writing RustDesk Configuration ==="

# Write config file
if ! cat > "/tmp/RustDesk2.toml" <<TOML_EOF
rendezvous_server = '\$RELAY_SERVER'
nat_type = 1
serial = 0
unlock_pin = ''
trusted_devices = ''

[options]
custom-rendezvous-server = '\$RELAY_SERVER'
relay-server = '\$RELAY_SERVER'
api-server = ''
key = '\$RELAY_KEY'
TOML_EOF
then
echo "ERROR: Failed to write config file"
exit 1
fi

rm -f "\$CONFIG_DIR/RustDesk2.toml"
cp /tmp/RustDesk2.toml "\$CONFIG_DIR/"

echo "..............................................."
echo "Configuration complete!"
echo "Relay Server: \$RELAY_SERVER"
echo "Relay Key: \$RELAY_KEY"
echo "..............................................."

# Cleanup - remove LaunchAgent and this script
rm -f "$LAUNCHAGENT_PLIST"

# Start RustDesk based on configured mode
if [[ "\$SERVER_MODE" == "y" || "\$SERVER_MODE" == "Y" ]]; then
echo "\$(date '+%Y-%m-%d %H:%M:%S') | SERVER MODE: Launching RustDesk in headless/server mode" | tee -a "\$LOG"
open /Applications/RustDesk.app --args --server
echo "\$(date '+%Y-%m-%d %H:%M:%S') | SERVER MODE: RustDesk server launched (exit code: \$?)" | tee -a "\$LOG"
elif [[ "\$SERVER_MODE" == "n" || "\$SERVER_MODE" == "N" ]]; then
echo "\$(date '+%Y-%m-%d %H:%M:%S') | CLIENT MODE: Launching RustDesk in GUI mode" | tee -a "\$LOG"
open -n /Applications/RustDesk.app
echo "\$(date '+%Y-%m-%d %H:%M:%S') | CLIENT MODE: RustDesk launched (exit code: \$?)" | tee -a "\$LOG"
fi
SCRIPT_EOF

chmod +x "$CONFIG_SCRIPT"

# Try to run as currently logged-in user
if run_as_user /bin/bash "$CONFIG_SCRIPT"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') | ..............................................." | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Configuration completed for logged-in user" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Server mode: $SERVER" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | ..............................................." | tee -a "$LOG"
rm -f "$dmg_file"
exit 0
fi

echo "Please complete install on GUI, launching RustDesk now."
open -n /Applications/RustDesk.app
# No user logged in - create LaunchAgent for next login
echo "$(date '+%Y-%m-%d %H:%M:%S') | No user logged in. Creating LaunchAgent for next login..." | tee -a "$LOG"

cat > "$LAUNCHAGENT_PLIST" <<PLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.${MSP}.rustdesk-config</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>$CONFIG_SCRIPT</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
PLIST_EOF

chmod 644 "$LAUNCHAGENT_PLIST"

echo "$(date '+%Y-%m-%d %H:%M:%S') | ..............................................." | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Install Complete (configuration pending login)" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | Server mode: $SERVER" | tee -a "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') | ..............................................." | tee -a "$LOG"
# Cleanup downloaded DMG
rm -f "$dmg_file"

exit 0
```

## Linux
Expand Down