mirror of
https://github.com/winapps-org/winapps.git
synced 2025-06-01 12:47:19 +02:00
708 lines
30 KiB
Bash
Executable File
708 lines
30 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
### GLOBAL CONSTANTS ###
|
|
# ERROR CODES
|
|
readonly EC_MISSING_CONFIG=1
|
|
readonly EC_MISSING_FREERDP=2
|
|
readonly EC_NOT_IN_GROUP=3
|
|
readonly EC_FAIL_START=4
|
|
readonly EC_FAIL_RESUME=5
|
|
readonly EC_FAIL_DESTROY=6
|
|
readonly EC_SD_TIMEOUT=7
|
|
readonly EC_DIE_TIMEOUT=8
|
|
readonly EC_RESTART_TIMEOUT=9
|
|
readonly EC_NOT_EXIST=10
|
|
readonly EC_UNKNOWN=11
|
|
readonly EC_NO_IP=12
|
|
readonly EC_BAD_PORT=13
|
|
readonly EC_UNSUPPORTED_APP=14
|
|
readonly EC_INVALID_FLAVOR=15
|
|
|
|
# PATHS
|
|
readonly APPDATA_PATH="${HOME}/.local/share/winapps"
|
|
readonly SYS_APP_PATH="/usr/local/share/winapps"
|
|
readonly LASTRUN_PATH="${APPDATA_PATH}/lastrun"
|
|
readonly LOG_PATH="${APPDATA_PATH}/winapps.log"
|
|
readonly CONFIG_PATH="${HOME}/.config/winapps/winapps.conf"
|
|
readonly COMPOSE_PATH="${HOME}/.config/winapps/compose.yaml"
|
|
# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment.
|
|
readonly SCRIPT_DIR_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
|
|
|
# OTHER
|
|
readonly CONTAINER_NAME="WinApps" # FOR 'docker' AND 'podman' ONLY
|
|
readonly RDP_PORT=3389
|
|
readonly DOCKER_IP="127.0.0.1"
|
|
# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment.
|
|
readonly RUNID="${RANDOM}"
|
|
|
|
### GLOBAL VARIABLES ###
|
|
# WINAPPS CONFIGURATION FILE
|
|
RDP_USER=""
|
|
RDP_PASS=""
|
|
RDP_DOMAIN=""
|
|
RDP_IP=""
|
|
VM_NAME="RDPWindows" # FOR 'libvirt' ONLY
|
|
WAFLAVOR="docker"
|
|
RDP_FLAGS=""
|
|
FREERDP_COMMAND=""
|
|
REMOVABLE_MEDIA=""
|
|
RDP_SCALE=100
|
|
AUTOPAUSE="off"
|
|
AUTOPAUSE_TIME="300"
|
|
MULTIMON="false"
|
|
DEBUG="true"
|
|
MULTI_FLAG=""
|
|
|
|
# OTHER
|
|
FREERDP_PID=-1
|
|
|
|
### TRAPS ###
|
|
# Catch SIGINT (CTRL+C) to call 'waCleanUp'.
|
|
trap waCleanUp SIGINT
|
|
|
|
### FUNCTIONS ###
|
|
# Name: 'waCleanUp'
|
|
# Role: Clean up remains prior to exit.
|
|
waCleanUp() {
|
|
# Kill FreeRDP.
|
|
[ "$FREERDP_PID" -gt 0 ] && kill -9 "$FREERDP_PID" &>/dev/null
|
|
|
|
# Remove '.cproc' file.
|
|
[ -f "${APPDATA_PATH}/FreeRDP_Process_${FREERDP_PID}.cproc" ] && rm "${APPDATA_PATH}/FreeRDP_Process_${FREERDP_PID}.cproc" &>/dev/null
|
|
|
|
# Terminate script.
|
|
exit 1
|
|
}
|
|
|
|
# Name: 'waThrowExit'
|
|
# Role: Throw an error message and exit the script.
|
|
function waThrowExit() {
|
|
# Declare variables.
|
|
local ERR_CODE="$1"
|
|
|
|
# Throw error.
|
|
case "$ERR_CODE" in
|
|
"$EC_MISSING_CONFIG")
|
|
# Missing WinApps configuration file.
|
|
dprint "ERROR: MISSING WINAPPS CONFIGURATION FILE. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "The WinApps configuration file is missing.\nPlease create a WinApps configuration file at '${CONFIG_PATH}'."
|
|
;;
|
|
"$EC_MISSING_FREERDP")
|
|
dprint "ERROR: FREERDP VERSION 3 IS NOT INSTALLED. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "FreeRDP version 3 is not installed."
|
|
;;
|
|
"$EC_NOT_IN_GROUP")
|
|
dprint "ERROR: USER NOT PART OF REQUIRED GROUPS. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "The user $(whoami) is not part of the required groups.
|
|
Please run:
|
|
sudo usermod -a -G libvirt $(whoami)
|
|
sudo usermod -a -G kvm $(whoami)"
|
|
;;
|
|
"$EC_FAIL_START")
|
|
dprint "ERROR: WINDOWS FAILED TO START. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows failed to start."
|
|
;;
|
|
"$EC_FAIL_RESUME")
|
|
dprint "ERROR: WINDOWS FAILED TO RESUME. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows failed to resume."
|
|
;;
|
|
"$EC_FAIL_DESTROY")
|
|
dprint "ERROR: WINDOWS FAILED TO IMMEDIATELY UNGRACEFULLY SHUT DOWN WINDOWS. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Failed to ungracefully shut down Windows."
|
|
;;
|
|
"$EC_SD_TIMEOUT")
|
|
dprint "ERROR: WINDOWS TOOK TOO LONG TO SHUT DOWN. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows took too long to shut down."
|
|
;;
|
|
"$EC_DIE_TIMEOUT")
|
|
dprint "ERROR: WINDOWS TOOK TOO LONG TO SHUT DOWN. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows took too long to die."
|
|
;;
|
|
"$EC_RESTART_TIMEOUT")
|
|
dprint "ERROR: WINDOWS TOOK TOO LONG TO RESTART. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows took too long to restart."
|
|
;;
|
|
"$EC_NOT_EXIST")
|
|
dprint "ERROR: WINDOWS NONEXISTENT. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows does not exist."
|
|
;;
|
|
"$EC_UNKNOWN")
|
|
dprint "ERROR: UNKNOWN CONTAINER ERROR. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Unknown Windows container error."
|
|
;;
|
|
"$EC_NO_IP")
|
|
dprint "ERROR: WINDOWS UNREACHABLE. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows is unreachable.\nPlease ensure Windows is assigned an IP address."
|
|
;;
|
|
"$EC_BAD_PORT")
|
|
dprint "ERROR: RDP PORT CLOSED. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "The Windows RDP port '${RDP_PORT}' is closed.\nPlease ensure Remote Desktop is correctly configured on Windows."
|
|
;;
|
|
"$EC_UNSUPPORTED_APP")
|
|
dprint "ERROR: APPLICATION NOT FOUND. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Application not found.\nPlease ensure the program is correctly configured as an officially supported application."
|
|
;;
|
|
"$EC_INVALID_FLAVOR")
|
|
dprint "ERROR: INVALID FLAVOR. EXITING."
|
|
notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Invalid WinApps flavor.\nPlease ensure 'docker', 'podman' or 'libvirt' are specified as the flavor in the WinApps configuration file."
|
|
;;
|
|
esac
|
|
|
|
# Terminate the script.
|
|
exit "$ERR_CODE"
|
|
}
|
|
|
|
# Name: 'dprint'
|
|
# Role: Conditionally print debug messages to a log file, creating it if it does not exist.
|
|
function dprint() {
|
|
[ "$DEBUG" = "true" ] && echo "[$(date)-$RUNID] $1" >>"$LOG_PATH"
|
|
}
|
|
# Name: 'waFixRemovableMedia'
|
|
# Role: If user left REMOVABLE_MEDIA config null,fallback to /run/media for udisks defaults ,warning.
|
|
function waFixRemovableMedia() {
|
|
if [ -z "$REMOVABLE_MEDIA" ]; then
|
|
REMOVABLE_MEDIA="/run/media" # Default for udisks
|
|
dprint "NOTICE: Using default REMOVABLE_MEDIA: $REMOVABLE_MEDIA"
|
|
notify-send --expire-time=3000 --icon="drive-removable-media" \
|
|
"WinApps Notice" "Using default removable media path: $REMOVABLE_MEDIA"
|
|
fi
|
|
}
|
|
# Name: 'waFixScale'
|
|
# Role: Since FreeRDP only supports '/scale' values of 100, 140 or 180, find the closest supported argument to the user's configuration.
|
|
function waFixScale() {
|
|
# Define variables.
|
|
local OLD_SCALE=100
|
|
local VALID_SCALE_1=100
|
|
local VALID_SCALE_2=140
|
|
local VALID_SCALE_3=180
|
|
|
|
# Check for an unsupported value.
|
|
if [ "$RDP_SCALE" != "$VALID_SCALE_1" ] && [ "$RDP_SCALE" != "$VALID_SCALE_2" ] && [ "$RDP_SCALE" != "$VALID_SCALE_3" ]; then
|
|
# Save the unsupported scale.
|
|
OLD_SCALE="$RDP_SCALE"
|
|
|
|
# Calculate the absolute differences.
|
|
local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE ))
|
|
local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE ))
|
|
local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE ))
|
|
|
|
# Set the final scale to the valid scale value with the smallest absolute difference.
|
|
if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then
|
|
RDP_SCALE="$VALID_SCALE_1"
|
|
elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then
|
|
RDP_SCALE="$VALID_SCALE_2"
|
|
else
|
|
RDP_SCALE="$VALID_SCALE_3"
|
|
fi
|
|
|
|
# Print feedback.
|
|
dprint "WARNING: Unsupported RDP_SCALE value '${OLD_SCALE}'. Defaulting to '${RDP_SCALE}'."
|
|
notify-send --expire-time=4000 --icon="dialog-warning" --app-name="WinApps" --urgency="low" "WinApps" "Unsupported RDP_SCALE value '${OLD_SCALE}'.\nDefaulting to '${RDP_SCALE}'."
|
|
fi
|
|
}
|
|
|
|
# Name: 'waLoadConfig'
|
|
# Role: Load the variables within the WinApps configuration file.
|
|
function waLoadConfig() {
|
|
# Load WinApps configuration file.
|
|
if [ -f "$CONFIG_PATH" ]; then
|
|
# shellcheck source=/dev/null # Exclude WinApps configuration file from being checked by ShellCheck.
|
|
source "$CONFIG_PATH"
|
|
else
|
|
waThrowExit $EC_MISSING_CONFIG
|
|
fi
|
|
|
|
# Update 'MULTI_FLAG' based on 'MULTIMON'.
|
|
MULTI_FLAG=$([[ $MULTIMON == "true" ]] && echo "/multimon" || echo "+span")
|
|
|
|
# Update $RDP_SCALE.
|
|
waFixScale
|
|
# Update when $REMOVABLE_MEDIA is null
|
|
waFixRemovableMedia
|
|
# Update $AUTOPAUSE_TIME.
|
|
# RemoteApp RDP sessions take, at minimum, 20 seconds to be terminated by the Windows server.
|
|
# Hence, subtract 20 from the timeout specified by the user, as a 'built in' timeout of 20 seconds will occur.
|
|
# Source: https://techcommunity.microsoft.com/t5/security-compliance-and-identity/terminal-services-remoteapp-8482-session-termination-logic/ba-p/246566
|
|
AUTOPAUSE_TIME=$((AUTOPAUSE_TIME - 20))
|
|
AUTOPAUSE_TIME=$((AUTOPAUSE_TIME < 0 ? 0 : AUTOPAUSE_TIME))
|
|
}
|
|
|
|
# Name: 'waLastRun'
|
|
# Role: Determine the last time this script was run.
|
|
function waLastRun() {
|
|
# Declare variables.
|
|
local LAST_RUN_UNIX_TIME=0
|
|
local CURR_RUN_UNIX_TIME=0
|
|
|
|
# Store the time this script was run last as a unix timestamp.
|
|
if [ -f "$LASTRUN_PATH" ]; then
|
|
LAST_RUN_UNIX_TIME=$(stat -t -c %Y "$LASTRUN_PATH")
|
|
dprint "LAST_RUN: ${LAST_RUN_UNIX_TIME}"
|
|
fi
|
|
|
|
# Update the file modification time with the current time.
|
|
touch "$LASTRUN_PATH"
|
|
CURR_RUN_UNIX_TIME=$(stat -t -c %Y "$LASTRUN_PATH")
|
|
dprint "THIS_RUN: ${CURR_RUN_UNIX_TIME}"
|
|
}
|
|
|
|
# Name: 'waGetFreeRDPCommand'
|
|
# Role: Determine the correct FreeRDP command to use.
|
|
function waGetFreeRDPCommand() {
|
|
# Declare variables.
|
|
local FREERDP_MAJOR_VERSION="" # Stores the major version of the installed copy of FreeRDP.
|
|
|
|
# Attempt to set a FreeRDP command if the command variable is empty.
|
|
if [ -z "$FREERDP_COMMAND" ]; then
|
|
# Check for 'xfreerdp'.
|
|
if command -v xfreerdp &>/dev/null; then
|
|
# Check FreeRDP major version is 3 or greater.
|
|
FREERDP_MAJOR_VERSION=$(xfreerdp --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1)
|
|
if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then
|
|
FREERDP_COMMAND="xfreerdp"
|
|
fi
|
|
fi
|
|
|
|
# Check for 'xfreerdp3' command as a fallback option.
|
|
if [ -z "$FREERDP_COMMAND" ]; then
|
|
if command -v xfreerdp3 &>/dev/null; then
|
|
# Check FreeRDP major version is 3 or greater.
|
|
FREERDP_MAJOR_VERSION=$(xfreerdp3 --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1)
|
|
if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then
|
|
FREERDP_COMMAND="xfreerdp3"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Check for FreeRDP Flatpak (fallback option).
|
|
if [ -z "$FREERDP_COMMAND" ]; then
|
|
if command -v flatpak &>/dev/null; then
|
|
if flatpak list --columns=application | grep -q "^com.freerdp.FreeRDP$"; then
|
|
# Check FreeRDP major version is 3 or greater.
|
|
FREERDP_MAJOR_VERSION=$(flatpak list --columns=application,version | grep "^com.freerdp.FreeRDP" | awk '{print $2}' | cut -d'.' -f1)
|
|
if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then
|
|
FREERDP_COMMAND="flatpak run --command=xfreerdp com.freerdp.FreeRDP"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if command -v "$FREERDP_COMMAND" &>/dev/null || [ "$FREERDP_COMMAND" = "flatpak run --command=xfreerdp com.freerdp.FreeRDP" ]; then
|
|
dprint "Using FreeRDP command '${FREERDP_COMMAND}'."
|
|
|
|
# Append additional flags or parameters to FreeRDP.
|
|
# These additional flags are loaded prior in 'waLoadConfig'.
|
|
[[ -n $RDP_FLAGS ]] && FREERDP_COMMAND="${FREERDP_COMMAND} ${RDP_FLAGS}"
|
|
else
|
|
waThrowExit "$EC_MISSING_FREERDP"
|
|
fi
|
|
}
|
|
|
|
# Name: 'waCheckGroupMembership'
|
|
# Role: Ensures the current user is part of the required groups.
|
|
function waCheckGroupMembership() {
|
|
# Identify groups the current user belongs to.
|
|
# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment.
|
|
local USER_GROUPS=$(groups "$(whoami)")
|
|
|
|
if ! (echo "$USER_GROUPS" | grep -q -E "\blibvirt\b") || ! (echo "$USER_GROUPS" | grep -q -E "\bkvm\b"); then
|
|
waThrowExit "$EC_NOT_IN_GROUP"
|
|
fi
|
|
}
|
|
|
|
# Name: 'waCheckVMRunning'
|
|
# Role: Check if the Windows 'libvirt' VM is running, and attempt to start it if it is not.
|
|
function waCheckVMRunning() {
|
|
# Declare exit status variable.
|
|
local EXIT_STATUS=0
|
|
|
|
# Declare timer variables.
|
|
local TIME_ELAPSED=0
|
|
local TIME_LIMIT=60
|
|
local TIME_INTERVAL=5
|
|
|
|
# Attempt to run the Windows virtual machine.
|
|
# Note: States 'running' and 'idle' do not require intervention, and are not checked for.
|
|
if (virsh list --all --name | xargs | grep -wq "$VM_NAME"); then
|
|
if (virsh list --state-shutoff --name | xargs | grep -wq "$VM_NAME"); then
|
|
dprint "WINDOWS SHUT OFF. BOOTING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Booting Windows."
|
|
virsh start "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_START
|
|
if (virsh list --state-paused --name | xargs | grep -wq "$VM_NAME"); then
|
|
dprint "WINDOWS PAUSED. RESUMING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Resuming Windows."
|
|
virsh resume "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_RESUME
|
|
fi
|
|
elif (virsh list --state-paused --name | xargs | grep -wq "$VM_NAME"); then
|
|
dprint "WINDOWS PAUSED. RESUMING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Resuming Windows."
|
|
virsh resume "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_RESUME
|
|
elif (virsh list --state-other --name | xargs | grep -wq "$VM_NAME"); then
|
|
if (virsh domstate "$VM_NAME" | xargs | grep -wq "shutdown"); then
|
|
dprint "WINDOWS SHUTTING DOWN. WAITING."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Windows is currently shutting down.\nIt will automatically restart once the shutdown process is complete."
|
|
EXIT_STATUS=$EC_SD_TIMEOUT
|
|
while (( TIME_ELAPSED < TIME_LIMIT )); do
|
|
if (virsh list --state-shutoff --name | xargs | grep -wq "$VM_NAME"); then
|
|
EXIT_STATUS=0
|
|
dprint "WINDOWS SHUT OFF. BOOTING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Booting Windows."
|
|
virsh start "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_START
|
|
break
|
|
fi
|
|
sleep $TIME_INTERVAL
|
|
TIME_ELAPSED=$((TIME_ELAPSED + TIME_INTERVAL))
|
|
done
|
|
elif (virsh domstate "$VM_NAME" | xargs | grep -wq "crashed"); then
|
|
dprint "WINDOWS CRASHED. DESTROYING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Windows experienced an unexpected crash.\nAttempting to restart Windows."
|
|
virsh destroy "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_DESTROY
|
|
if [ "$EXIT_STATUS" -eq 0 ]; then
|
|
dprint "WINDOWS DESTROYED. BOOTING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Booting Windows."
|
|
virsh start "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_START
|
|
fi
|
|
elif (virsh domstate "$VM_NAME" | xargs | grep -wq "dying"); then
|
|
dprint "WINDOWS DYING. WAITING."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Windows is currently shutting down unexpectedly.\nIt will try to restart once the shutdown process finishes."
|
|
EXIT_STATUS=$EC_DIE_TIMEOUT
|
|
while (( TIME_ELAPSED < TIME_LIMIT )); do
|
|
if (virsh domstate "$VM_NAME" | xargs | grep -wq "crashed"); then
|
|
EXIT_STATUS=0
|
|
dprint "WINDOWS CRASHED. DESTROYING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Windows experienced an unexpected crash.\nAttempting to restart Windows."
|
|
virsh destroy "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_DESTROY
|
|
if [ "$EXIT_STATUS" -eq 0 ]; then
|
|
dprint "WINDOWS DESTROYED. BOOTING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Booting Windows."
|
|
virsh start "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_START
|
|
fi
|
|
break
|
|
elif (virsh list --state-shutoff --name | xargs | grep -wq "$VM_NAME"); then
|
|
EXIT_STATUS=0
|
|
dprint "WINDOWS SHUT OFF. BOOTING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Booting Windows."
|
|
virsh start "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_START
|
|
break
|
|
fi
|
|
sleep $TIME_INTERVAL
|
|
TIME_ELAPSED=$((TIME_ELAPSED + TIME_INTERVAL))
|
|
done
|
|
elif (virsh domstate "$VM_NAME" | xargs | grep -wq "pmsuspended" ); then
|
|
dprint "WINDOWS SUSPENDED. RESUMING WINDOWS."
|
|
virsh resume "$VM_NAME" &>/dev/null || EXIT_STATUS=$EC_FAIL_RESUME
|
|
fi
|
|
fi
|
|
else
|
|
EXIT_STATUS=$EC_NOT_EXIST
|
|
fi
|
|
|
|
# Handle non-zero exit statuses.
|
|
[ "$EXIT_STATUS" -ne 0 ] && waThrowExit "$EXIT_STATUS"
|
|
}
|
|
|
|
# Name: 'waCheckContainerRunning'
|
|
# Role: Throw an error if the Docker container is not running.
|
|
function waCheckContainerRunning() {
|
|
# Declare variables.
|
|
local EXIT_STATUS=0
|
|
local CONTAINER_STATE=""
|
|
local COMPOSE_COMMAND=""
|
|
local TIME_ELAPSED=0
|
|
local TIME_LIMIT=60
|
|
local TIME_INTERVAL=5
|
|
|
|
# Determine the state of the container.
|
|
CONTAINER_STATE=$("$WAFLAVOR" inspect --format='{{.State.Status}}' "$CONTAINER_NAME")
|
|
|
|
# Determine the compose command.
|
|
case "$WAFLAVOR" in
|
|
"docker") COMPOSE_COMMAND="docker compose" ;;
|
|
"podman") COMPOSE_COMMAND="podman-compose" ;;
|
|
esac
|
|
|
|
# Check container state.
|
|
# Note: Errors DO NOT result in non-zero exit statuses.
|
|
# Docker: 'created', 'restarting', 'running', 'removing', 'paused', 'exited' or 'dead'.
|
|
# Podman: 'created', 'running', 'paused', 'exited' or 'unknown'.
|
|
case "$CONTAINER_STATE" in
|
|
"created")
|
|
dprint "WINDOWS CREATED. BOOTING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Booting Windows."
|
|
$COMPOSE_COMMAND --file "$COMPOSE_PATH" start &>/dev/null
|
|
;;
|
|
"restarting")
|
|
dprint "WINDOWS RESTARTING. WAITING."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Windows is currently restarting. Please wait."
|
|
EXIT_STATUS=$EC_RESTART_TIMEOUT
|
|
while (( TIME_ELAPSED < TIME_LIMIT )); do
|
|
if [[ $("$WAFLAVOR" inspect --format='{{.State.Status}}' "$CONTAINER_NAME") == "running" ]]; then
|
|
EXIT_STATUS=0
|
|
dprint "WINDOWS RESTARTED."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Restarted Windows."
|
|
break
|
|
fi
|
|
sleep $TIME_INTERVAL
|
|
TIME_ELAPSED=$((TIME_ELAPSED + TIME_INTERVAL))
|
|
done
|
|
;;
|
|
"paused")
|
|
dprint "WINDOWS PAUSED. RESUMING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Resuming Windows."
|
|
$COMPOSE_COMMAND --file "$COMPOSE_PATH" unpause &>/dev/null
|
|
;;
|
|
"exited")
|
|
dprint "WINDOWS SHUT OFF. BOOTING WINDOWS."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Booting Windows."
|
|
$COMPOSE_COMMAND --file "$COMPOSE_PATH" start &>/dev/null
|
|
;;
|
|
"dead")
|
|
dprint "WINDOWS DEAD. RECREATING WINDOWS CONTAINER."
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Re-creating and booting Windows."
|
|
$COMPOSE_COMMAND --file "$COMPOSE_PATH" down &>/dev/null && $COMPOSE_COMMAND --file "$COMPOSE_PATH" up -d &>/dev/null
|
|
;;
|
|
"unknown")
|
|
EXIT_STATUS=$EC_UNKNOWN
|
|
;;
|
|
esac
|
|
|
|
# Handle non-zero exit statuses.
|
|
[ "$EXIT_STATUS" -ne 0 ] && waThrowExit "$EXIT_STATUS"
|
|
}
|
|
|
|
# Name: 'waCheckPortOpen'
|
|
# Role: Assesses whether the RDP port on Windows is open.
|
|
function waCheckPortOpen() {
|
|
# Declare variables.
|
|
local VM_MAC="" # Stores the MAC address of the Windows VM.
|
|
local TIME_ELAPSED=0
|
|
local TIME_LIMIT=30
|
|
local TIME_INTERVAL=5
|
|
|
|
# Obtain Windows VM IP Address ('libvirt' ONLY)
|
|
# Note: 'RDP_IP' should not be empty if 'WAFLAVOR' is 'docker', since it is set to localhost before this function is called.
|
|
if [ -z "$RDP_IP" ] && [ "$WAFLAVOR" = "libvirt" ]; then
|
|
VM_MAC=$(virsh domiflist "$VM_NAME" | grep -oE "([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})") # VM MAC address.
|
|
|
|
while (( TIME_ELAPSED < TIME_LIMIT )); do
|
|
if [ "$TIME_ELAPSED" -eq "$TIME_INTERVAL" ]; then
|
|
notify-send --expire-time=4000 --icon="dialog-info" --app-name="WinApps" --urgency="low" "WinApps" "Requesting Windows IP address..."
|
|
fi
|
|
RDP_IP=$(ip neigh show | grep "$VM_MAC" | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}") # VM IP address.
|
|
[ -n "$RDP_IP" ] && break
|
|
sleep $TIME_INTERVAL
|
|
TIME_ELAPSED=$((TIME_ELAPSED + TIME_INTERVAL))
|
|
done
|
|
|
|
[ -z "$RDP_IP" ] && waThrowExit "$EC_NO_IP"
|
|
fi
|
|
|
|
# Check for an open RDP port.
|
|
timeout 10 nc -z "$RDP_IP" "$RDP_PORT" &>/dev/null || waThrowExit "$EC_BAD_PORT"
|
|
}
|
|
|
|
# Name: 'waRunCommand'
|
|
# Role: Run the requested WinApps command.
|
|
function waRunCommand() {
|
|
# Declare variables.
|
|
local ICON=""
|
|
local FILE_PATH=""
|
|
|
|
# Run option.
|
|
if [ "$1" = "windows" ]; then
|
|
# Update timeout (since there is no 'in-built' 20 second delay for full RDP sessions post-logout).
|
|
AUTOPAUSE_TIME=$((AUTOPAUSE_TIME + 20))
|
|
|
|
# Open Windows RDP session.
|
|
dprint "WINDOWS"
|
|
$FREERDP_COMMAND \
|
|
/d:"$RDP_DOMAIN" \
|
|
/u:"$RDP_USER" \
|
|
/p:"$RDP_PASS" \
|
|
/scale:"$RDP_SCALE" \
|
|
+dynamic-resolution \
|
|
+auto-reconnect \
|
|
+home-drive \
|
|
+clipboard \
|
|
-wallpaper \
|
|
/wm-class:"Microsoft Windows" \
|
|
/t:"Windows RDP Session [$RDP_IP]" \
|
|
/v:"$RDP_IP" &>/dev/null &
|
|
|
|
# Capture the process ID.
|
|
FREERDP_PID=$!
|
|
elif [ "$1" = "manual" ]; then
|
|
# Open specified application.
|
|
dprint "MANUAL: ${2}"
|
|
$FREERDP_COMMAND \
|
|
/d:"$RDP_DOMAIN" \
|
|
/u:"$RDP_USER" \
|
|
/p:"$RDP_PASS" \
|
|
/scale:"$RDP_SCALE" \
|
|
+dynamic-resolution \
|
|
+auto-reconnect \
|
|
+home-drive \
|
|
+clipboard \
|
|
-wallpaper \
|
|
"$MULTI_FLAG" \
|
|
/app:program:"$2" \
|
|
/v:"$RDP_IP" &>/dev/null &
|
|
|
|
# Capture the process ID.
|
|
FREERDP_PID=$!
|
|
else
|
|
# Script summoned from right-click menu or application icon (plus/minus a file path).
|
|
if [ -e "${SCRIPT_DIR_PATH}/../apps/${1}/info" ]; then
|
|
# shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck.
|
|
source "${SCRIPT_DIR_PATH}/../apps/${1}/info"
|
|
ICON="${SCRIPT_DIR_PATH}/../apps/${1}/icon.svg"
|
|
elif [ -e "${APPDATA_PATH}/apps/${1}/info" ]; then
|
|
# shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck.
|
|
source "${APPDATA_PATH}/apps/${1}/info"
|
|
ICON="${APPDATA_PATH}/apps/${1}/icon.svg"
|
|
elif [ -e "${SYS_APP_PATH}/apps/${1}/info" ]; then
|
|
# shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck.
|
|
source "${SYS_APP_PATH}/apps/${1}/info"
|
|
ICON="${SYS_APP_PATH}/apps/${1}/icon.svg"
|
|
else
|
|
waThrowExit "$EC_UNSUPPORTED_APP"
|
|
fi
|
|
|
|
# Check if a file path was specified, and pass this to the application.
|
|
if [ -z "$2" ]; then
|
|
# No file path specified.
|
|
$FREERDP_COMMAND \
|
|
/d:"$RDP_DOMAIN" \
|
|
/u:"$RDP_USER" \
|
|
/p:"$RDP_PASS" \
|
|
/scale:"$RDP_SCALE" \
|
|
+dynamic-resolution \
|
|
+auto-reconnect \
|
|
+home-drive \
|
|
+clipboard \
|
|
-wallpaper \
|
|
"$MULTI_FLAG" \
|
|
/wm-class:"$FULL_NAME" \
|
|
/app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:"$FULL_NAME" \
|
|
/v:"$RDP_IP" &>/dev/null &
|
|
|
|
# Capture the process ID.
|
|
FREERDP_PID=$!
|
|
else
|
|
# Convert path from UNIX to Windows style.
|
|
FILE_PATH=$(echo "$2" | sed \
|
|
-e 's|^'"${HOME}"'|\\\\tsclient\\home|' \
|
|
-e 's|^\('"${REMOVABLE_MEDIA//|/\\|}"'\)/[^/]*|\\\\tsclient\\media|' \
|
|
-e 's|/|\\|g')
|
|
dprint "UNIX_FILE_PATH: ${2}"
|
|
dprint "WINDOWS_FILE_PATH: ${FILE_PATH}"
|
|
|
|
$FREERDP_COMMAND \
|
|
/d:"$RDP_DOMAIN" \
|
|
/u:"$RDP_USER" \
|
|
/p:"$RDP_PASS" \
|
|
/scale:"$RDP_SCALE" \
|
|
+dynamic-resolution \
|
|
+auto-reconnect \
|
|
+home-drive \
|
|
+clipboard \
|
|
/drive:media,"$REMOVABLE_MEDIA" \
|
|
-wallpaper \
|
|
"$MULTI_FLAG" \
|
|
/wm-class:"$FULL_NAME" \
|
|
/app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:$"FULL_NAME",cmd:\""$FILE_PATH"\" \
|
|
/v:"$RDP_IP" &>/dev/null &
|
|
|
|
# Capture the process ID.
|
|
FREERDP_PID=$!
|
|
fi
|
|
fi
|
|
|
|
if [ "$FREERDP_PID" -ne -1 ]; then
|
|
# Create a file with the process ID.
|
|
touch "${APPDATA_PATH}/FreeRDP_Process_${FREERDP_PID}.cproc"
|
|
|
|
# Wait for the process to terminate.
|
|
wait $FREERDP_PID
|
|
|
|
# Remove the file with the process ID.
|
|
rm "${APPDATA_PATH}/FreeRDP_Process_${FREERDP_PID}.cproc" &>/dev/null
|
|
fi
|
|
}
|
|
|
|
# Name: 'waCheckIdle'
|
|
# Role: Suspend Windows if idle.
|
|
function waCheckIdle() {
|
|
# Declare variables
|
|
local TIME_INTERVAL=10
|
|
local TIME_ELAPSED=0
|
|
local SUSPEND_WINDOWS=0
|
|
|
|
# Prevent 'autopause' functionality with unsupported Windows backends.
|
|
if [ "$WAFLAVOR" != "manual" ] && [ "$WAFLAVOR" != "docker" ]; then
|
|
# Check if there are no WinApps-related FreeRDP processes running.
|
|
if ! ls "$APPDATA_PATH"/FreeRDP_Process_*.cproc &>/dev/null; then
|
|
SUSPEND_WINDOWS=1
|
|
while (( TIME_ELAPSED < AUTOPAUSE_TIME )); do
|
|
if ls "$APPDATA_PATH"/FreeRDP_Process_*.cproc &>/dev/null; then
|
|
SUSPEND_WINDOWS=0
|
|
break
|
|
fi
|
|
sleep $TIME_INTERVAL
|
|
TIME_ELAPSED=$((TIME_ELAPSED + TIME_INTERVAL))
|
|
done
|
|
fi
|
|
|
|
# Hibernate/Pause Windows.
|
|
if [ "$SUSPEND_WINDOWS" -eq 1 ]; then
|
|
dprint "IDLE FOR ${AUTOPAUSE_TIME} SECONDS. SUSPENDING WINDOWS."
|
|
notify-send --expire-time=8000 --icon="info" --app-name="WinApps" --urgency="low" "WinApps" "Pausing Windows due to inactivity."
|
|
if [ "$WAFLAVOR" = "docker" ]; then
|
|
docker compose --file "$COMPOSE_PATH" pause &>/dev/null
|
|
elif [ "$WAFLAVOR" = "podman" ]; then
|
|
podman-compose --file "$COMPOSE_PATH" pause &>/dev/null
|
|
elif [ "$WAFLAVOR" = "libvirt" ]; then
|
|
virsh suspend "$VM_NAME" &>/dev/null
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
### MAIN LOGIC ###
|
|
#set -x # Enable for debugging.
|
|
dprint "START"
|
|
dprint "SCRIPT_DIR: ${SCRIPT_DIR_PATH}"
|
|
dprint "SCRIPT_ARGS: ${*}"
|
|
dprint "HOME_DIR: ${HOME}"
|
|
mkdir -p "$APPDATA_PATH"
|
|
waLastRun
|
|
waLoadConfig
|
|
waGetFreeRDPCommand
|
|
|
|
# If using podman backend, modify the FreeRDP command to enter a new namespace.
|
|
if [ "$WAFLAVOR" = "podman" ]; then
|
|
FREERDP_COMMAND="podman unshare --rootless-netns ${FREERDP_COMMAND}"
|
|
fi
|
|
|
|
if [ "$WAFLAVOR" = "docker" ] || [ "$WAFLAVOR" = "podman" ]; then
|
|
RDP_IP="$DOCKER_IP"
|
|
waCheckContainerRunning
|
|
elif [ "$WAFLAVOR" = "libvirt" ]; then
|
|
waCheckGroupMembership
|
|
waCheckVMRunning
|
|
elif [ "$WAFLAVOR" = "manual" ]; then
|
|
waCheckPortOpen
|
|
else
|
|
waThrowExit "$EC_INVALID_FLAVOR"
|
|
fi
|
|
|
|
waCheckPortOpen
|
|
waRunCommand "$@"
|
|
|
|
if [[ "$AUTOPAUSE" == "on" ]]; then
|
|
waCheckIdle
|
|
fi
|
|
|
|
dprint "END"
|