The source code within this question aims to provide a short-cut for mirroring a local XWindow (or session) to a remote host via SSH port forwarding, such as:
x11vnc-push-xwindow --id=none raspberrypi
The Readme file contains more detailed instructions for setup, but the TLDR is...
Clone
mkdir -vp ~/git/hub/rpi-curious cd ~/git/hub/rpi-curious git clone --recurse-submodules git@github.com:rpi-curious/x11vnc-push- xwindow.git
Link/Install
cd x11vnc-push-xwindow ln -s "${PWD}/x11vnc-push-xwindow" "${HOME}/bin/"
Use
x11vnc-push-xwindow raspberrypi ## Select a XWindow, or use `--id=none` to mirror entire session # x11vnc-push-xwindow --id=none raspberrypi
Exit when done
q # Ctrl^c
I wrote this project because it helps my own posture to look up at my remote device's screen, and currently everything seems to function as intended, but as always there's room for improvement.
Questions
Are there any obvious mistakes?
Any features that are both missing and necessary?
Is there a better way to fully terminate the connection when q is pressed? Currently this is a two-step process of pressing q then Ctrlc to quit and then terminate the connection.
Source Code
Note, source code for this question is maintained on GitHub at rpi-curious/x11vnc-push-xwindow, what is included here are the scripts and shared functions required to test/review without need of any Git fanciness.
x11vnc-push-xwindow
#!/usr/bin/env bash
## Find true directory script resides in, true name, and true path
__SOURCE__="${BASH_SOURCE[0]}"
while [[ -h "${__SOURCE__}" ]]; do
__SOURCE__="$(find "${__SOURCE__}" -type l -ls | sed -n 's@^.* -> \(.*\)@\1@p')"
done
__DIR__="$(cd -P "$(dirname "${__SOURCE__}")" && pwd)"
__NAME__="${__SOURCE__##*/}"
__AUTHOR__='S0AndS0'
__DESCRIPTION__='Pushes/mirrors selected XWindow to remote via SSH port forwarding'
## Source module code within this script
source "${__DIR__}/shared_functions/modules/argument-parser/argument-parser.sh"
source "${__DIR__}/shared_functions/modules/trap-failure/failure.sh"
trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
__license__(){
local _date_year="$(date +'%Y')"
cat <<EOF
${__DESCRIPTION__}
Copyright (C) ${_date_year:-2020} ${__AUTHOR__:-S0AndS0}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
EOF
}
usage() {
local _message="${1}"
cat <<EOF
${__DESCRIPTION__}
## Augments ${__NAME__%.*} responds to
--help | -h
Prints this message and exits
--x11vnc-listen-port="${_x11vnc_listen_port}"
Default '5900', port that x11vnc will serve XWindow session on 'localhost' for this device.
Note, if listen port is already in use then session will be reused, otherwise a new session will be initialized.
--vnc-viewer-port="${_vnc_viewer_port}"
Default '5900', port that remote host will connect to on their relative 'localhost' to view forwarded XWindow session.
Note, if 'xscreensaver' is detected on remote host, then it will be disabled until x11vnc session is terminated
--vnc-viewer-name="${_vnc_viewer_name}"
Default 'vncviewer', executable name of VNC Viewer.
Note, if troubles are had when using a VNC Viewer other than 'vncviewer', please try 'vncviewer' before opening a new Issue.
--id="${_id}"
Default 'pick', XWindow ID to forward to remote host.
Note, if set to 'none' then entire XWindow session will be forwarded.
${_target_host:-<target-host>}
Required, remote SSH host that XWindow session will be forwarded to.
## Example
${__NAME__} raspberrypi
EOF
[[ "${#_message}" -gt '0' ]] && {
printf >&2 '\n## Error: %s\n' "${_message}"
}
}
## Defaults
_target_host=''
_x11vnc_listen_port='5900'
_vnc_viewer_port='5900'
_id='pick'
_vnc_viewer_name='vncviewer'
## Save passed arguments and acceptable arguments to Bash arrays
_passed_args=("${@:?No arguments provided}")
_acceptable_args=(
'--help|-h:bool'
'--x11vnc-listen-port:alpha_numeric'
'--vnc-viewer-port:alpha_numeric'
'--id:alpha_numeric'
'--target-host:path-nil'
)
## Pass arrays by reference/name to the `argument_parser` function
argument_parser '_passed_args' '_acceptable_args'
_exit_status="$?"
## Print documentation for the script and exit, or allow further execution
((_help)) || ((_exit_status)) && {
usage
exit "${_exit_status:-0}"
}
(("${#_target_host}")) || {
usage 'Missing target host parameter'
exit 1
}
## Note, '-shared' with '-forever' and '-threads' or '-once' may be wanted
## in addition to the following options
_x11vnc_server_opts=(
'-quiet'
'-noshared'
'-viewonly'
'-noremote'
'-nobell'
'-nosel'
'-noprimary'
'-nosetprimary'
'-noclipboard'
'-nosetclipboard'
# '-disablefiletransfer' ## Un-comment for older versions
'-cursor' 'most'
'-noipv6'
'-allow' '127.0.0.1'
'-autoport' "${_x11vnc_listen_port}"
'-listen' '127.0.0.1'
'-nopw'
'-nossl'
'-bg'
)
[[ "${_id}" =~ 'none' ]] || {
_x11vnc_server_opts+=(
'-id' "${_id}"
)
}
_vnc_viewer_opts=(
'-viewonly'
'-fullscreen'
"localhost::${_vnc_viewer_port}"
)
grep -q -- "${_x11vnc_listen_port}" <<<"$(netstat -plantu 2>/dev/null)" || {
printf '# Running: x11vnc %s\n' "${_x11vnc_server_opts[*]}"
x11vnc ${_x11vnc_server_opts[@]}
}
initialize_connection() {
ssh -R localhost:${_vnc_viewer_port}:localhost:${_x11vnc_listen_port} "${_target_host}" <<EOF
reinitalize_xscreensaver(){
echo 'Resuming: xscreensaver'
DISPLAY=:0 xscreensaver -no-splash 2>&1 >/dev/null &
sleep 3
DISPLAY=:0 xscreensaver-command -activate
}
initalize_viewer(){
_xscreensaver_time="\$(DISPLAY=:0 xscreensaver-command -time 2>&1)"
[[ "\${_xscreensaver_time}" =~ 'no screensaver is running' ]] || {
trap 'reinitalize_xscreensaver' RETURN SIGINT SIGTERM EXIT
echo 'Halting: xscreensaver'
DISPLAY=:0 xscreensaver-command -deactivate
DISPLAY=:0 xscreensaver-command -exit
}
printf 'Starting: \$(which ${_vnc_viewer_name}) %s\n' "${_vnc_viewer_opts[*]}"
DISPLAY=:0 \$(which ${_vnc_viewer_name}) ${_vnc_viewer_opts[@]}
return "${?}"
}
initalize_viewer
EOF
}
initialize_connection &
_connection_pid="$!"
printf 'Press %s to quit...\n' "q"
while read -n1 -r _input; do
case "${_input,,}" in
q)
printf 'Killing PID %i\n' "${_connection_pid}"
kill "${_connection_pid}"
sleep 2
printf 'Please use Ctrl^c to exit!'
;;
esac
sleep 1
done
shared_functions/modules/argument-parser/argument-parser.sh
#!/usr/bin/env bash
# argument-parser.sh, source it in other Bash scripts for argument parsing
# Copyright (C) 2019 S0AndS0
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
shopt -s extglob
_TRUE='1'
_DEFAULT_ACCEPTABLE_ARG_LIST=('--help|-h:bool' '--foo|-f:print' '--path:path-nil')
arg_scrubber_alpha_numeric(){ printf '%s' "${@//[^a-z0-9A-Z]/}"; }
arg_scrubber_regex(){ printf '%s' "$(sed 's@.@\\.@g' <<<"${@//[^[:print:]$'\t'$'\n']/}")"; }
arg_scrubber_list(){
printf '%s' "$(sed 's@\.\.*@.@g; s@--*@-@g' <<<"${@//[^a-z0-9A-Z,+_./@:-]/}")"
}
arg_scrubber_path(){
printf '%s' "$(sed 's@\.\.*@.@g; s@--*@-@g' <<<"${@//[^a-z0-9A-Z ~+_./@:-]/}")"
}
arg_scrubber_posix(){
_value="${@//[^a-z0-9A-Z_.-]/}"
_value="$(sed 's@^[-_.]@@g; s@[-_.]$@@g; s@\.\.*@.@g; s@--*@-@g' <<<"${_value}")"
printf '%s' "${_value::32}"
}
return_scrubbed_arg(){
_raw_value="${1}"
_opt_type="${2:?## Error - no option type provided to return_scrubbed_arg}"
case "${_opt_type}" in
'bool'*) _value="${_TRUE}" ;;
'raw'*) _value="${_raw_value}" ;;
'path'*) _value="$(arg_scrubber_path "${_raw_value}")" ;;
'posix'*) _value="$(arg_scrubber_posix "${_raw_value}")" ;;
'print'*) _value="${_raw_value//[^[:print:]]/}" ;;
'regex'*) _value="$(arg_scrubber_regex "${_raw_value}")" ;;
'list'*) _value="$(arg_scrubber_list "${_raw_value}")" ;;
'alpha_numeric'*) _value="$(arg_scrubber_alpha_numeric "${_raw_value}")" ;;
esac
if [[ "${_opt_type}" =~ ^'bool'* ]] || [[ "${_raw_value}" == "${_value}" ]]; then
printf '%s' "${_value}"
else
printf '## Error - return_scrubbed_arg detected differences in values\n' >&2
return 1
fi
}
argument_parser(){
local -n _arg_user_ref="${1:?# No reference to an argument list/array provided}"
local -n _arg_accept_ref="${2:-_DEFAULT_ACCEPTABLE_ARG_LIST}"
_args_user_list=("${_arg_user_ref[@]}")
unset _assigned_args
for _acceptable_args in ${_arg_accept_ref[@]}; do
## Take a break when user supplied argument list becomes empty
[[ "${#_args_user_list[@]}" == '0' ]] && break
## First in listed acceptable arg is used as variable name to save value to
## example, '--foo-bar fizz' would transmute into '_foo_bar=fizz'
_opt_name="${_acceptable_args%%[:|]*}"
_var_name="${_opt_name#*[-]}"
_var_name="${_var_name#*[-]}"
_var_name="_${_var_name//-/_}"
## Divine the type of argument allowed for this iteration of acceptable args
case "${_acceptable_args}" in
*':'*) _opt_type="${_acceptable_args##*[:]}" ;;
*) _opt_type="bool" ;;
esac
## Set case expressions to match user arguments against and for non-bool type
## what alternative case expression to match on.
## example '--foo|-f' will also check for '--foo=*|-f=*'
_arg_opt_list="${_acceptable_args%%:*}"
_valid_opts_pattern="@(${_arg_opt_list})"
case "${_arg_opt_list}" in
*'|'*) _valid_opts_pattern_alt="@(${_arg_opt_list//|/=*|}=*)" ;;
*) _valid_opts_pattern_alt="@(${_arg_opt_list}=*)" ;;
esac
## Attempt to match up user supplied arguments with those that are valid
for (( i = 0; i < "${#_args_user_list[@]}"; i++ )); do
_user_opt="${_args_user_list[${i}]}"
case "${_user_opt}" in
${_valid_opts_pattern}) ## Parse for script-name --foo bar or --true
if [[ "${_opt_type}" =~ ^'bool'* ]]; then
_var_value="$(return_scrubbed_arg "${_user_opt}" "${_opt_type}")"
_exit_status="${?}"
else
i+=1
_var_value="$(return_scrubbed_arg "${_args_user_list[${i}]}" "${_opt_type}")"
_exit_status="${?}"
unset _args_user_list[$(( i - 1 ))]
fi
;;
${_valid_opts_pattern_alt}) ## Parse for script-name --foo=bar
_var_value="$(return_scrubbed_arg "${_user_opt#*=}" "${_opt_type}")"
_exit_status="${?}"
;;
*) ## Parse for script-name direct_value
case "${_opt_type}" in
*'nil'|*'none')
_var_value="$(return_scrubbed_arg "${_user_opt}" "${_opt_type}")"
_exit_status="${?}"
;;
esac
;;
esac
if ((_exit_status)); then return ${_exit_status}; fi
## Break on matched options after clearing temp variables and re-assigning
## list (array) of user supplied arguments.
## Note, re-assigning is to ensure the next looping indexes correctly
## and is designed to require less work on each iteration
if [ -n "${_var_value}" ]; then
declare -g "${_var_name}=${_var_value}"
declare -ag "_assigned_args+=('${_opt_name}=\"${_var_value}\"')"
unset _user_opt
unset _var_value
unset _args_user_list[${i}]
unset _exit_status
_args_user_list=("${_args_user_list[@]}")
break
fi
done
unset _opt_type
unset _opt_name
unset _var_name
done
}
Note, the source code for argument-parser.sh is a Git Submodule maintained on GitHub at `bash-utilities/argument-parser, and can be cloned individually via...
mkdir -vp ~/git/hub/bash-utilities
cd ~/git/hub/bash-utilities
git clone git@github.com:bash-utilities/argument-parser.git
shared_functions/modules/trap-failure/failure.sh
#!/usr/bin/env bash
# Bash Trap Failure, a submodule for other Bash scripts tracked by Git
# Copyright (C) 2019 S0AndS0
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## Outputs Front-Mater formatted failures for functions not returning 0
## Use the following line after sourcing this file to set failure trap
## trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
failure(){
local -n _lineno="${1:-LINENO}"
local -n _bash_lineno="${2:-BASH_LINENO}"
local _last_command="${3:-${BASH_COMMAND}}"
local _code="${4:-0}"
## Workaround for read EOF combo tripping traps
((_code)) || {
return "${_code}"
}
local _last_command_height="$(wc -l <<<"${_last_command}")"
local -a _output_array=()
_output_array+=(
'---'
"lines_history: [${_lineno} ${_bash_lineno[*]}]"
"function_trace: [${FUNCNAME[*]}]"
"exit_code: ${_code}"
)
[[ "${#BASH_SOURCE[@]}" -gt '1' ]] && {
_output_array+=('source_trace:')
for _item in "${BASH_SOURCE[@]}"; do
_output_array+=(" - ${_item}")
done
} || {
_output_array+=("source_trace: [${BASH_SOURCE[*]}]")
}
[[ "${_last_command_height}" -gt '1' ]] && {
_output_array+=(
'last_command: ->'
"${_last_command}"
)
} || {
_output_array+=("last_command: ${_last_command}")
}
_output_array+=('---')
printf '%s\n' "${_output_array[@]}" >&2
exit ${_code}
}
Note, the source code for failure.sh is a Git Submodule maintained on GitHub at bash-utilities/trap-failure, and can be cloned individually via...
mkdir -vp ~/git/hub/bash-utilities
cd ~/git/hub/bash-utilities
git clone git@github.com:bash-utilities/trap-failure.git