diff --git a/README.md b/README.md index 0f282dd..c575bef 100644 --- a/README.md +++ b/README.md @@ -299,16 +299,104 @@ sudo flatpak override --filesystem=home com.freerdp.FreeRDP # To use `+home-driv ### Step 3: Create a WinApps Configuration File Create a configuration file at `~/.config/winapps/winapps.conf` containing the following: ```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" + +# [WINDOWS PASSWORD] 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" + +# [WINDOWS DOMAIN] +# DEFAULT VALUE: '' (BLANK) +RDP_DOMAIN="" + +# [WINDOWS IPV4 ADDRESS] +# NOTES: +# - 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] diff --git a/bin/winapps b/bin/winapps index 2486ae7..2c96366 100755 --- a/bin/winapps +++ b/bin/winapps @@ -1,19 +1,22 @@ #!/usr/bin/env bash ### GLOBAL CONSTANTS ### -# ANSI ESCAPE SEQUENCES -readonly ERROR_TEXT="\033[1;41;37m" # Bold + White + Red Background -readonly CLEAR_TEXT="\033[0m" # Clear - # ERROR CODES readonly EC_MISSING_CONFIG=1 readonly EC_MISSING_FREERDP=2 readonly EC_NOT_IN_GROUP=3 -readonly EC_NOT_RUNNING=4 -readonly EC_NO_IP=5 -readonly EC_BAD_PORT=6 -readonly EC_UNSUPPORTED_APP=7 -readonly EC_INVALID_FLAVOR=8 +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" @@ -21,11 +24,13 @@ 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 VM_NAME="RDPWindows" # FOR 'libvirt' ONLY +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. @@ -41,11 +46,33 @@ WAFLAVOR="docker" RDP_FLAGS="" FREERDP_COMMAND="" 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() { @@ -57,52 +84,70 @@ function waThrowExit() { "$EC_MISSING_CONFIG") # Missing WinApps configuration file. dprint "ERROR: MISSING WINAPPS CONFIGURATION FILE. EXITING." - echo -e "${ERROR_TEXT}ERROR: MISSING WINAPPS CONFIGURATION FILE.${CLEAR_TEXT}" - echo "Please create a WinApps configuration file at '${CONFIG_PATH}'". + 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." - 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") dprint "ERROR: USER NOT PART OF REQUIRED GROUPS. EXITING." - echo -e "${ERROR_TEXT}ERROR: USER NOT PART OF REQUIRED GROUPS.${CLEAR_TEXT}" - echo "Please run:" - echo " sudo usermod -a -G libvirt $(whoami)" - echo " sudo usermod -a -G kvm $(whoami)" + 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_NOT_RUNNING") - dprint "ERROR: WINDOWS NOT RUNNING. EXITING." - echo -e "${ERROR_TEXT}ERROR: WINDOWS NOT RUNNING.${CLEAR_TEXT}" - echo "Please ensure Windows is running." + "$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." - echo -e "${ERROR_TEXT}ERROR: WINDOWS UNREACHABLE.${CLEAR_TEXT}" - echo "Please ensure Windows is assigned an IP address." + 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." - echo -e "${ERROR_TEXT}ERROR: RDP PORT CLOSED.${CLEAR_TEXT}" - echo "Please ensure Remote Desktop is correctly configured on Windows." + 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." - echo -e "${ERROR_TEXT}ERROR: APPLICATION NOT FOUND.${CLEAR_TEXT}" - echo "Please ensure the program is correctly configured as an officially supported application." + 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." - echo -e "${ERROR_TEXT}ERROR: INVALID FLAVOR.${CLEAR_TEXT}" - echo "Please ensure 'docker', 'podman' or 'libvirt' are specified as the flavor in the WinApps configuration file." + 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 - # Provide generic advice. - echo "Check the WinApps project README for more information." - # Terminate the script. - echo "Exiting with status '${ERR_CODE}'." exit "$ERR_CODE" } @@ -142,7 +187,7 @@ function waFixScale() { # Print feedback. 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 } @@ -162,6 +207,13 @@ function waLoadConfig() { # Update $RDP_SCALE. 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' @@ -245,32 +297,163 @@ function waCheckGroupMembership() { } # 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() { - ! 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' # 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 container state (docker). - if command -v docker &>/dev/null; then - CONTAINER_STATE=$(docker ps --filter name="WinApps" --format '{{.Status}}') - fi + # Determine the state of the container. + CONTAINER_STATE=$("$WAFLAVOR" inspect --format='{{.State.Status}}' "$CONTAINER_NAME") - # Determine container state (podman). - if [ -z "$CONTAINER_STATE" ]; then - CONTAINER_STATE=$(podman ps --filter name="WinApps" --format '{{.Status}}') - fi - - CONTAINER_STATE=${CONTAINER_STATE,,} # Convert the string to lowercase. - CONTAINER_STATE=${CONTAINER_STATE%% *} # Extract the first word. + # Determine the compose command. + case "$WAFLAVOR" in + "docker") COMPOSE_COMMAND="docker compose" ;; + "podman") COMPOSE_COMMAND="podman-compose" ;; + esac # 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' @@ -278,17 +461,30 @@ function waCheckContainerRunning() { 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. - 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" fi # 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' @@ -300,6 +496,9 @@ function waRunCommand() { # 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 \ @@ -314,6 +513,9 @@ function waRunCommand() { /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}" @@ -332,6 +534,9 @@ function waRunCommand() { "$MULTI_FLAG" \ /app:program:"$2" \ /v:"$RDP_IP" &>/dev/null & + + # Capture the process ID. + FREERDP_PID=$! else # 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 @@ -368,6 +573,9 @@ function waRunCommand() { /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 \ @@ -392,6 +600,55 @@ function waRunCommand() { /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 + + # 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 } @@ -425,4 +682,8 @@ fi waCheckPortOpen waRunCommand "$@" +if [[ "$AUTOPAUSE" == "on" ]]; then + waCheckIdle +fi + dprint "END" diff --git a/docs/libvirt.md b/docs/libvirt.md index a3aa27f..992819e 100644 --- a/docs/libvirt.md +++ b/docs/libvirt.md @@ -26,7 +26,7 @@ Together, these components form a powerful and flexible virtualization stack, wi 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 echo "export LIBVIRT_DEFAULT_URI=\"qemu:///system\"" >> ~/.bashrc ``` diff --git a/installer.sh b/installer.sh index 8d2480c..d3a3d76 100755 --- a/installer.sh +++ b/installer.sh @@ -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 RDP_PORT=3389 # Port used for RDP on Windows. 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 ### # USER INPUT @@ -100,7 +89,7 @@ WAFLAVOR="docker" # Imported variable. RDP_SCALE=100 # Imported variable. RDP_FLAGS="" # Imported variable. MULTIMON="false" # Imported variable. -DEBUG="false" # Imported variable. +DEBUG="true" # Imported variable. FREERDP_COMMAND="" # Imported variable. MULTI_FLAG="" # Set based on value of $MULTIMON. @@ -443,10 +432,7 @@ function waLoadConfig() { # Display the suggested action(s). echo "--------------------------------------------------------------------------------" 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 "\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 -e "See https://github.com/winapps-org/winapps?tab=readme-ov-file#step-3-create-a-winapps-configuration-file" echo "--------------------------------------------------------------------------------" # Terminate the script. @@ -504,6 +490,33 @@ function waCheckInstallDependencies() { # Print feedback. 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' if ! command -v nc &>/dev/null; then # Complete the previous line.