Merge pull request #175 from KernelGhost/main

Added ability to automatically pause and resume Windows.
This commit is contained in:
Rohan Barar 2024-07-31 07:56:30 +10:00 committed by GitHub
commit 89759b2a38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 436 additions and 74 deletions

104
README.md
View File

@ -299,16 +299,104 @@ sudo flatpak override --filesystem=home com.freerdp.FreeRDP # To use `+home-driv
### Step 3: Create a WinApps Configuration File ### Step 3: Create a WinApps Configuration File
Create a configuration file at `~/.config/winapps/winapps.conf` containing the following: Create a configuration file at `~/.config/winapps/winapps.conf` containing the following:
```bash ```bash
##################################
# WINAPPS CONFIGURATION FILE #
##################################
# INSTRUCTIONS
# - Leading and trailing whitespace are ignored.
# - Empty lines are ignored.
# - Lines starting with '#' are ignored.
# - All characters following a '#' are ignored.
# [WINDOWS USERNAME]
RDP_USER="MyWindowsUser" RDP_USER="MyWindowsUser"
# [WINDOWS PASSWORD]
RDP_PASS="MyWindowsPassword" RDP_PASS="MyWindowsPassword"
#RDP_DOMAIN="MYDOMAIN"
#RDP_IP="192.168.123.111" # [WINDOWS DOMAIN]
#WAFLAVOR="docker" # Acceptable values are 'docker', 'podman' and 'libvirt'. # DEFAULT VALUE: '' (BLANK)
#RDP_SCALE=100 # Acceptable values are 100, 140, and 180. RDP_DOMAIN=""
#RDP_FLAGS=""
#MULTIMON="true" # [WINDOWS IPV4 ADDRESS]
#DEBUG="true" # NOTES:
#FREERDP_COMMAND="xfreerdp" # - If using 'libvirt', 'RDP_IP' will be determined by WinApps at runtime if left unspecified.
# DEFAULT VALUE:
# - 'docker': '127.0.0.1'
# - 'podman': '127.0.0.1'
# - 'libvirt': '' (BLANK)
RDP_IP=""
# [WINAPPS BACKEND]
# DEFAULT VALUE: 'docker'
# VALID VALUES:
# - 'docker'
# - 'podman'
# - 'libvirt'
WAFLAVOR="docker"
# [DISPLAY SCALING FACTOR]
# NOTES:
# - If an unsupported value is specified, a warning will be displayed.
# - If an unsupported value is specified, WinApps will use the closest supported value.
# DEFAULT VALUE: '100'
# VALID VALUES:
# - '100'
# - '140'
# - '180'
RDP_SCALE="100"
# [ADDITIONAL FREERDP FLAGS & ARGUMENTS]
# DEFAULT VALUE: '' (BLANK)
# VALID VALUES: See https://github.com/awakecoding/FreeRDP-Manuals/blob/master/User/FreeRDP-User-Manual.markdown
RDP_FLAGS=""
# [MULTIPLE MONITORS]
# NOTES:
# - If enabled, a FreeRDP bug *might* produce a black screen.
# DEFAULT VALUE: 'false'
# VALID VALUES:
# - 'true'
# - 'false'
MULTIMON="false"
# [DEBUG WINAPPS]
# NOTES:
# - Creates and appends to ~/.local/share/winapps/winapps.log when running WinApps.
# DEFAULT VALUE: 'true'
# VALID VALUES:
# - 'true'
# - 'false'
DEBUG="true"
# [AUTOMATICALLY PAUSE WINDOWS]
# NOTES:
# - This is currently INCOMPATIBLE with 'docker'.
# - See https://github.com/dockur/windows/issues/674
# DEFAULT VALUE: 'off'
# VALID VALUES:
# - 'on'
# - 'off'
AUTOPAUSE="off"
# [AUTOMATICALLY PAUSE WINDOWS TIMEOUT]
# NOTES:
# - This setting determines the duration of inactivity to tolerate before Windows is automatically paused.
# - This setting is ignored if 'AUTOPAUSE' is set to 'off'.
# - The value must be specified in seconds (to the nearest 10 seconds e.g., '30', '40', '50', etc.).
# - For RemoteApp RDP sessions, there is a mandatory 20-second delay, so the minimum value that can be specified here is '20'.
# - Source: https://techcommunity.microsoft.com/t5/security-compliance-and-identity/terminal-services-remoteapp-8482-session-termination-logic/ba-p/246566
# DEFAULT VALUE: '300'
# VALID VALUES: >=20
AUTOPAUSE_TIME="300"
# [FREERDP COMMAND]
# NOTES:
# - WinApps will attempt to automatically detect the correct command to use for your system.
# DEFAULT VALUE: '' (BLANK)
# VALID VALUES: The command required to run FreeRDPv3 on your system (e.g., 'xfreerdp', 'xfreerdp3', etc.).
FREERDP_COMMAND=""
``` ```
> [!NOTE] > [!NOTE]

View File

@ -1,19 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
### GLOBAL CONSTANTS ### ### GLOBAL CONSTANTS ###
# ANSI ESCAPE SEQUENCES
readonly ERROR_TEXT="\033[1;41;37m" # Bold + White + Red Background
readonly CLEAR_TEXT="\033[0m" # Clear
# ERROR CODES # ERROR CODES
readonly EC_MISSING_CONFIG=1 readonly EC_MISSING_CONFIG=1
readonly EC_MISSING_FREERDP=2 readonly EC_MISSING_FREERDP=2
readonly EC_NOT_IN_GROUP=3 readonly EC_NOT_IN_GROUP=3
readonly EC_NOT_RUNNING=4 readonly EC_FAIL_START=4
readonly EC_NO_IP=5 readonly EC_FAIL_RESUME=5
readonly EC_BAD_PORT=6 readonly EC_FAIL_DESTROY=6
readonly EC_UNSUPPORTED_APP=7 readonly EC_SD_TIMEOUT=7
readonly EC_INVALID_FLAVOR=8 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 # PATHS
readonly APPDATA_PATH="${HOME}/.local/share/winapps" readonly APPDATA_PATH="${HOME}/.local/share/winapps"
@ -21,11 +24,13 @@ readonly SYS_APP_PATH="/usr/local/share/winapps"
readonly LASTRUN_PATH="${APPDATA_PATH}/lastrun" readonly LASTRUN_PATH="${APPDATA_PATH}/lastrun"
readonly LOG_PATH="${APPDATA_PATH}/winapps.log" readonly LOG_PATH="${APPDATA_PATH}/winapps.log"
readonly CONFIG_PATH="${HOME}/.config/winapps/winapps.conf" 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. # 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)" readonly SCRIPT_DIR_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
# OTHER # OTHER
readonly VM_NAME="RDPWindows" # FOR 'libvirt' ONLY readonly VM_NAME="RDPWindows" # FOR 'libvirt' ONLY
readonly CONTAINER_NAME="WinApps" # FOR 'docker' AND 'podman' ONLY
readonly RDP_PORT=3389 readonly RDP_PORT=3389
readonly DOCKER_IP="127.0.0.1" readonly DOCKER_IP="127.0.0.1"
# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment. # shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment.
@ -41,11 +46,33 @@ WAFLAVOR="docker"
RDP_FLAGS="" RDP_FLAGS=""
FREERDP_COMMAND="" FREERDP_COMMAND=""
RDP_SCALE=100 RDP_SCALE=100
AUTOPAUSE="off"
AUTOPAUSE_TIME="300"
MULTIMON="false" MULTIMON="false"
DEBUG="true" DEBUG="true"
MULTI_FLAG="" MULTI_FLAG=""
# OTHER
FREERDP_PID=-1
### TRAPS ###
# Catch SIGINT (CTRL+C) to call 'waCleanUp'.
trap waCleanUp SIGINT
### FUNCTIONS ### ### 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' # Name: 'waThrowExit'
# Role: Throw an error message and exit the script. # Role: Throw an error message and exit the script.
function waThrowExit() { function waThrowExit() {
@ -57,52 +84,70 @@ function waThrowExit() {
"$EC_MISSING_CONFIG") "$EC_MISSING_CONFIG")
# Missing WinApps configuration file. # Missing WinApps configuration file.
dprint "ERROR: MISSING WINAPPS CONFIGURATION FILE. EXITING." dprint "ERROR: MISSING WINAPPS CONFIGURATION FILE. EXITING."
echo -e "${ERROR_TEXT}ERROR: MISSING WINAPPS CONFIGURATION FILE.${CLEAR_TEXT}" 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}'."
echo "Please create a WinApps configuration file at '${CONFIG_PATH}'".
;; ;;
"$EC_MISSING_FREERDP") "$EC_MISSING_FREERDP")
dprint "ERROR: FREERDP VERSION 3 IS NOT INSTALLED. EXITING." dprint "ERROR: FREERDP VERSION 3 IS NOT INSTALLED. EXITING."
echo -e "${ERROR_TEXT}ERROR: FREERDP VERSION 3 IS NOT INSTALLED.${CLEAR_TEXT}" notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "FreeRDP version 3 is not installed."
;; ;;
"$EC_NOT_IN_GROUP") "$EC_NOT_IN_GROUP")
dprint "ERROR: USER NOT PART OF REQUIRED GROUPS. EXITING." dprint "ERROR: USER NOT PART OF REQUIRED GROUPS. EXITING."
echo -e "${ERROR_TEXT}ERROR: USER NOT PART OF REQUIRED GROUPS.${CLEAR_TEXT}" notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "The user $(whoami) is not part of the required groups.
echo "Please run:" Please run:
echo " sudo usermod -a -G libvirt $(whoami)" sudo usermod -a -G libvirt $(whoami)
echo " sudo usermod -a -G kvm $(whoami)" sudo usermod -a -G kvm $(whoami)"
;; ;;
"$EC_NOT_RUNNING") "$EC_FAIL_START")
dprint "ERROR: WINDOWS NOT RUNNING. EXITING." dprint "ERROR: WINDOWS FAILED TO START. EXITING."
echo -e "${ERROR_TEXT}ERROR: WINDOWS NOT RUNNING.${CLEAR_TEXT}" notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Windows failed to start."
echo "Please ensure Windows is running." ;;
"$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") "$EC_NO_IP")
dprint "ERROR: WINDOWS UNREACHABLE. EXITING." dprint "ERROR: WINDOWS UNREACHABLE. EXITING."
echo -e "${ERROR_TEXT}ERROR: WINDOWS UNREACHABLE.${CLEAR_TEXT}" 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."
echo "Please ensure Windows is assigned an IP address."
;; ;;
"$EC_BAD_PORT") "$EC_BAD_PORT")
dprint "ERROR: RDP PORT CLOSED. EXITING." dprint "ERROR: RDP PORT CLOSED. EXITING."
echo -e "${ERROR_TEXT}ERROR: RDP PORT CLOSED.${CLEAR_TEXT}" 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."
echo "Please ensure Remote Desktop is correctly configured on Windows."
;; ;;
"$EC_UNSUPPORTED_APP") "$EC_UNSUPPORTED_APP")
dprint "ERROR: APPLICATION NOT FOUND. EXITING." dprint "ERROR: APPLICATION NOT FOUND. EXITING."
echo -e "${ERROR_TEXT}ERROR: APPLICATION NOT FOUND.${CLEAR_TEXT}" 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."
echo "Please ensure the program is correctly configured as an officially supported application."
;; ;;
"$EC_INVALID_FLAVOR") "$EC_INVALID_FLAVOR")
dprint "ERROR: INVALID FLAVOR. EXITING." dprint "ERROR: INVALID FLAVOR. EXITING."
echo -e "${ERROR_TEXT}ERROR: INVALID FLAVOR.${CLEAR_TEXT}" 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."
echo "Please ensure 'docker', 'podman' or 'libvirt' are specified as the flavor in the WinApps configuration file."
;; ;;
esac esac
# Provide generic advice.
echo "Check the WinApps project README for more information."
# Terminate the script. # Terminate the script.
echo "Exiting with status '${ERR_CODE}'."
exit "$ERR_CODE" exit "$ERR_CODE"
} }
@ -142,7 +187,7 @@ function waFixScale() {
# Print feedback. # Print feedback.
dprint "WARNING: Unsupported RDP_SCALE value '${OLD_SCALE}'. Defaulting to '${RDP_SCALE}'." dprint "WARNING: Unsupported RDP_SCALE value '${OLD_SCALE}'. Defaulting to '${RDP_SCALE}'."
echo "WARNING: Unsupported RDP_SCALE value '${OLD_SCALE}' detected. 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 fi
} }
@ -162,6 +207,13 @@ function waLoadConfig() {
# Update $RDP_SCALE. # Update $RDP_SCALE.
waFixScale waFixScale
# 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' # Name: 'waLastRun'
@ -245,32 +297,163 @@ function waCheckGroupMembership() {
} }
# Name: 'waCheckVMRunning' # Name: 'waCheckVMRunning'
# Role: Throw an error if the Windows 'libvirt' VM is not running. # Role: Check if the Windows 'libvirt' VM is running, and attempt to start it if it is not.
function waCheckVMRunning() { function waCheckVMRunning() {
! virsh list --state-running --name | grep -q "^${VM_NAME}$" && waThrowExit "$EC_NOT_RUNNING" # 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' # Name: 'waCheckContainerRunning'
# Role: Throw an error if the Docker container is not running. # Role: Throw an error if the Docker container is not running.
function waCheckContainerRunning() { function waCheckContainerRunning() {
# Declare variables. # Declare variables.
local EXIT_STATUS=0
local CONTAINER_STATE="" local CONTAINER_STATE=""
local COMPOSE_COMMAND=""
local TIME_ELAPSED=0
local TIME_LIMIT=60
local TIME_INTERVAL=5
# Determine container state (docker). # Determine the state of the container.
if command -v docker &>/dev/null; then CONTAINER_STATE=$("$WAFLAVOR" inspect --format='{{.State.Status}}' "$CONTAINER_NAME")
CONTAINER_STATE=$(docker ps --filter name="WinApps" --format '{{.Status}}')
fi
# Determine container state (podman). # Determine the compose command.
if [ -z "$CONTAINER_STATE" ]; then case "$WAFLAVOR" in
CONTAINER_STATE=$(podman ps --filter name="WinApps" --format '{{.Status}}') "docker") COMPOSE_COMMAND="docker compose" ;;
fi "podman") COMPOSE_COMMAND="podman-compose" ;;
esac
CONTAINER_STATE=${CONTAINER_STATE,,} # Convert the string to lowercase.
CONTAINER_STATE=${CONTAINER_STATE%% *} # Extract the first word.
# Check container state. # Check container state.
[[ "$CONTAINER_STATE" != "up" ]] && waThrowExit "$EC_NOT_RUNNING" # 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' # Name: 'waCheckPortOpen'
@ -278,17 +461,30 @@ function waCheckContainerRunning() {
function waCheckPortOpen() { function waCheckPortOpen() {
# Declare variables. # Declare variables.
local VM_MAC="" # Stores the MAC address of the Windows VM. 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) # 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. # 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 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. VM_MAC=$(virsh domiflist "$VM_NAME" | grep -oE "([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})") # VM MAC address.
RDP_IP=$(arp -n | grep "$VM_MAC" | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}") # VM IP 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" [ -z "$RDP_IP" ] && waThrowExit "$EC_NO_IP"
fi fi
# Check for an open RDP port. # Check for an open RDP port.
timeout 5 nc -z "$RDP_IP" "$RDP_PORT" &>/dev/null || waThrowExit "$EC_BAD_PORT" timeout 10 nc -z "$RDP_IP" "$RDP_PORT" &>/dev/null || waThrowExit "$EC_BAD_PORT"
} }
# Name: 'waRunCommand' # Name: 'waRunCommand'
@ -300,6 +496,9 @@ function waRunCommand() {
# Run option. # Run option.
if [ "$1" = "windows" ]; then 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. # Open Windows RDP session.
dprint "WINDOWS" dprint "WINDOWS"
$FREERDP_COMMAND \ $FREERDP_COMMAND \
@ -314,6 +513,9 @@ function waRunCommand() {
/wm-class:"Microsoft Windows" \ /wm-class:"Microsoft Windows" \
/t:"Windows RDP Session [$RDP_IP]" \ /t:"Windows RDP Session [$RDP_IP]" \
/v:"$RDP_IP" &>/dev/null & /v:"$RDP_IP" &>/dev/null &
# Capture the process ID.
FREERDP_PID=$!
elif [ "$1" = "manual" ]; then elif [ "$1" = "manual" ]; then
# Open specified application. # Open specified application.
dprint "MANUAL: ${2}" dprint "MANUAL: ${2}"
@ -332,6 +534,9 @@ function waRunCommand() {
"$MULTI_FLAG" \ "$MULTI_FLAG" \
/app:program:"$2" \ /app:program:"$2" \
/v:"$RDP_IP" &>/dev/null & /v:"$RDP_IP" &>/dev/null &
# Capture the process ID.
FREERDP_PID=$!
else else
# Script summoned from right-click menu with officially supported application name plus/minus a file path. # Script summoned from right-click menu with officially supported application name plus/minus a file path.
if [ -e "${SCRIPT_DIR_PATH}/../apps/${1}/info" ]; then if [ -e "${SCRIPT_DIR_PATH}/../apps/${1}/info" ]; then
@ -368,6 +573,9 @@ function waRunCommand() {
/wm-class:"$FULL_NAME" \ /wm-class:"$FULL_NAME" \
/app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:"$FULL_NAME" \ /app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:"$FULL_NAME" \
/v:"$RDP_IP" &>/dev/null & /v:"$RDP_IP" &>/dev/null &
# Capture the process ID.
FREERDP_PID=$!
else else
# Convert path from UNIX to Windows style. # Convert path from UNIX to Windows style.
FILE_PATH=$(echo "$2" | sed \ FILE_PATH=$(echo "$2" | sed \
@ -392,6 +600,55 @@ function waRunCommand() {
/wm-class:"$FULL_NAME" \ /wm-class:"$FULL_NAME" \
/app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:$"FULL_NAME",cmd:\""$FILE_PATH"\" \ /app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:$"FULL_NAME",cmd:\""$FILE_PATH"\" \
/v:"$RDP_IP" &>/dev/null & /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
# 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 fi
} }
@ -425,4 +682,8 @@ fi
waCheckPortOpen waCheckPortOpen
waRunCommand "$@" waRunCommand "$@"
if [[ "$AUTOPAUSE" == "on" ]]; then
waCheckIdle
fi
dprint "END" dprint "END"

View File

@ -26,7 +26,7 @@ Together, these components form a powerful and flexible virtualization stack, wi
sudo emerge app-emulation/virt-manager # Gentoo Linux sudo emerge app-emulation/virt-manager # Gentoo Linux
``` ```
3. Configure `libvirt` to use the 'system' URI by adding the line `LIBVIRT_DEFAULT_URI="qemu:///system"` to your preferred shell profile file. 3. Configure `libvirt` to use the 'system' URI by adding the line `LIBVIRT_DEFAULT_URI="qemu:///system"` to your preferred shell profile file (e.g., `.bashrc`, `.zshrc`, etc.).
```bash ```bash
echo "export LIBVIRT_DEFAULT_URI=\"qemu:///system\"" >> ~/.bashrc echo "export LIBVIRT_DEFAULT_URI=\"qemu:///system\"" >> ~/.bashrc
``` ```

View File

@ -72,17 +72,6 @@ readonly INQUIRER_PATH="./install/inquirer.sh" # UNIX path to the 'inquirer' scr
readonly VM_NAME="RDPWindows" # Name of the Windows VM (FOR 'libvirt' ONLY). readonly VM_NAME="RDPWindows" # Name of the Windows VM (FOR 'libvirt' ONLY).
readonly RDP_PORT=3389 # Port used for RDP on Windows. readonly RDP_PORT=3389 # Port used for RDP on Windows.
readonly DOCKER_IP="127.0.0.1" # Localhost. readonly DOCKER_IP="127.0.0.1" # Localhost.
readonly WINAPPS_CONFIG="\
RDP_USER=\"MyWindowsUser\"
RDP_PASS=\"MyWindowsPassword\"
#RDP_DOMAIN=\"MYDOMAIN\"
#RDP_IP=\"192.168.123.111\"
#WAFLAVOR=\"docker\" # Acceptable values are 'docker', 'podman' and 'libvirt'.
#RDP_SCALE=100 # Acceptable values are 100, 140, and 180.
#RDP_FLAGS=\"\"
#MULTIMON=\"true\"
#DEBUG=\"true\"
#FREERDP_COMMAND=\"xfreerdp\""
### GLOBAL VARIABLES ### ### GLOBAL VARIABLES ###
# USER INPUT # USER INPUT
@ -100,7 +89,7 @@ WAFLAVOR="docker" # Imported variable.
RDP_SCALE=100 # Imported variable. RDP_SCALE=100 # Imported variable.
RDP_FLAGS="" # Imported variable. RDP_FLAGS="" # Imported variable.
MULTIMON="false" # Imported variable. MULTIMON="false" # Imported variable.
DEBUG="false" # Imported variable. DEBUG="true" # Imported variable.
FREERDP_COMMAND="" # Imported variable. FREERDP_COMMAND="" # Imported variable.
MULTI_FLAG="" # Set based on value of $MULTIMON. MULTI_FLAG="" # Set based on value of $MULTIMON.
@ -443,10 +432,7 @@ function waLoadConfig() {
# Display the suggested action(s). # Display the suggested action(s).
echo "--------------------------------------------------------------------------------" echo "--------------------------------------------------------------------------------"
echo -e "Please create a configuration file at ${COMMAND_TEXT}${CONFIG_PATH}${CLEAR_TEXT}." echo -e "Please create a configuration file at ${COMMAND_TEXT}${CONFIG_PATH}${CLEAR_TEXT}."
echo -e "\nThe configuration file should contain the following:" echo -e "See https://github.com/winapps-org/winapps?tab=readme-ov-file#step-3-create-a-winapps-configuration-file"
echo -e "\n${COMMAND_TEXT}${WINAPPS_CONFIG}${CLEAR_TEXT}"
echo -e "\nThe ${COMMAND_TEXT}RDP_USER${CLEAR_TEXT} and ${COMMAND_TEXT}RDP_PASS${CLEAR_TEXT} fields should contain the Windows user's account name and password."
echo -e "Note that the Windows user's PIN combination CANNOT be used to populate ${COMMAND_TEXT}RDP_PASS${CLEAR_TEXT}."
echo "--------------------------------------------------------------------------------" echo "--------------------------------------------------------------------------------"
# Terminate the script. # Terminate the script.
@ -504,6 +490,33 @@ function waCheckInstallDependencies() {
# Print feedback. # Print feedback.
echo -n "Checking whether dependencies are installed... " echo -n "Checking whether dependencies are installed... "
# 'libnotify'
if ! command -v notify-send &>/dev/null; then
# Complete the previous line.
echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n"
# Display the error type.
echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}"
# Display the error details.
echo -e "${INFO_TEXT}Please install 'libnotify' to proceed.${CLEAR_TEXT}"
# Display the suggested action(s).
echo "--------------------------------------------------------------------------------"
echo "Debian/Ubuntu-based systems:"
echo -e " ${COMMAND_TEXT}sudo apt install libnotify-bin${CLEAR_TEXT}"
echo "Red Hat/Fedora-based systems:"
echo -e " ${COMMAND_TEXT}sudo dnf install libnotify${CLEAR_TEXT}"
echo "Arch Linux systems:"
echo -e " ${COMMAND_TEXT}sudo pacman -S libnotify${CLEAR_TEXT}"
echo "Gentoo Linux systems:"
echo -e " ${COMMAND_TEXT}sudo emerge --ask x11-libs/libnotify${CLEAR_TEXT}"
echo "--------------------------------------------------------------------------------"
# Terminate the script.
return "$EC_MISSING_DEPS"
fi
# 'Netcat' # 'Netcat'
if ! command -v nc &>/dev/null; then if ! command -v nc &>/dev/null; then
# Complete the previous line. # Complete the previous line.