#!/bin/bash

# Copyright (C) 2007-2023 X2Go Project - https://wiki.x2go.org
# Copyright (C) 2007-2023 Oleksandr Shneyder <o.shneyder@phoca-gmbh.de>
# Copyright (C) 2007-2023 Heinz-Markus Graesing <heinz-m.graesing@obviously-nice.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

typeset x2go_lib_path="$(x2gopath 'libexec')"

typeset msg=''
if [[ "${#}" -lt '7' ]]; then
	msg='not enough command line arguments'
	echo "${msg}" >&2
	"${x2go_lib_path}/x2gosyslog" "${0}" 'err' "${msg}"
	exit '1'
fi

typeset session_name="${1}"
typeset geometry_val="${2}"
typeset link_val="${3}"
typeset pack_val="${4}"
#typeset kbd_layout_val="${5}"
typeset kbd_type_val="${6}"
typeset set_kbd="${7}"
typeset clipboard_val="${8}"
typeset xinerama_val="${9}"

typeset x2go_agent_pid="$("${x2go_lib_path}/x2gogetagent" "${session_name}")"

typeset x2go_root="${HOME}/.x2go"
typeset resize_val='1'
typeset fullscreen_val='0'

# ${HOSTNAME} should be automatically set by bash via gethostname(2), IFF this
# variable is not already set in the environment.
#
# This leads to two problems:
#   - export HOSTNAME="malbox"; x2gostartagent will override the actual system
#     host name and lead to authorization failures when connecting to
#     x2goagent/nxagent later on.
#   - even if the above is not the case, we want to be sure to get the actual
#     system host name.
#
# Workaround: use hostname.
typeset current_host_name=''

if ! current_host_name="$(hostname)"; then
	msg="Unable to retrieve machine's hostname. This is required. Aborting session startup."
	"${x2go_lib_path}/x2gosyslog" "${0}" 'err' "${msg}"

	# Make x2goclient fail.
	echo "${msg}" >&2
	exit '2'
fi

# refresh up ssh-agent-forwarding socket file symlink
typeset x2go_ssh_auth_sock="${x2go_root}/C-${session_name}/ssh-agent.PID"
if [[ -L "${x2go_ssh_auth_sock}" ]]; then
	rm -f -- "${x2go_ssh_auth_sock}"
fi
if [[ -S "${SSH_AUTH_SOCK}" ]]; then
	ln -sf -- "${SSH_AUTH_SOCK}" "${x2go_ssh_auth_sock}"
fi

typeset state="$("${x2go_lib_path}/x2gogetagentstate" "${session_name}")"

# exit if session terminated
if [[ "${state}" = 'TERMINATED' ]] || [[ "${state}" = 'TERMINATING' ]]; then
	msg="session ${session_name} terminated"
	echo "${msg}" >&2
	"${x2go_lib_path}/x2gosyslog" "${0}" 'err' "${msg}"
	exit '3'
fi

typeset -i i='0'
# wait 15 sec. for starting session
while [[ "${state}" = 'RESUMING' ]] || [[ "${state}" = 'STARTING' ]]; do
	sleep '1'
	((++i))
	#if session still not started, try to suspend it
	if [[ "${i}" -gt '15' ]]; then
		x2gosuspend-session "${session_name}"
		sleep '2'
		i='0'
	fi
	state="$("${x2go_lib_path}/x2gogetagentstate" "${session_name}")"
done

#suspend running session
if [[ "${state}" = 'RUNNING' ]]; then
	x2gosuspend-session "${session_name}"
	sleep '2'
	state="$("${x2go_lib_path}/x2gogetagentstate" "${session_name}")"
fi

i='0'
# wait 45 sec., while session suspending
while [[ "${state}" = 'SUSPENDING' ]]; do
	sleep '1'
	((++i))
	if [[ "${i}" -gt '45' ]]; then
		msg='it is taking too long to suspend the session-to-be-resumed; it is possible that the session is in a damaged state'
		echo "${msg}" >&2
		"${x2go_lib_path}/x2gosyslog" "${0}" 'err' "${msg}"
		exit '4'
	fi
	state="$("${x2go_lib_path}/x2gogetagentstate" "${session_name}")"
done

"${x2go_lib_path}/x2gormforward" "${session_name}"

"${x2go_lib_path}/x2gosyslog" "${0}" 'info' "$(basename "${0}") called with options: ${*}"

typeset telekinesis_enabled="$(perl -e 'use X2Go::Config qw( get_config ); use X2Go::Utils qw( is_true ); my $Config= get_config(); print is_true($Config->param("telekinesis.enable"));')"

# rootless sessions of geometry fullscreen are invalid
if [[ "${geometry_val}" = 'fullscreen' ]] && [[ "${SESSION_TYPE}" = 'R' ]]; then
	geometry_val=''
fi

# no geometry for desktop sessions shall result in fullscreen desktop sessions
if [[ -z "${geometry_val}" ]] && [[ "${SESSION_TYPE}" = 'D' ]]; then
	geometry_val='fullscreen'
fi
if [[ "${geometry_val}" = 'fullscreen' ]]; then
	fullscreen_val='1'
fi


typeset session_info="$(x2golistsessions | grep "${session_name}" | sed 's/|/,/g')"

# FIXME: what is "GR" supposed to mean? General? Nobody knows and it was never
#        documented ...
typeset gr_port="$(awk -F ',' '{print $9}' <<< "${session_info}")"
typeset sound_port="$(awk -F ',' '{print $10}' <<< "${session_info}")"
typeset fs_port="$(awk -F ',' '{print $14}' <<< "${session_info}")"
#typeset server="$(awk -F ',' '{print $4}' <<< "${session_info}")"

"${x2go_lib_path}/x2gosyslog" "${0}" 'debug' "old ports: ${gr_port}, ${sound_port}, ${fs_port}"

typeset X2GO_INTERNAL_SOURCE='1'
# Make shellcheck happy.
: "${X2GO_INTERNAL_SOURCE}"
. "${x2go_lib_path}/x2gocheckport"
unset X2GO_INTERNAL_SOURCE

# define the full path to the ss utility
typeset ss="$(PATH="${PATH}:/usr/sbin:/sbin" type -P 'ss')"

#check if saved in DB ports free
if ! check_system_port "${ss}" "${gr_port}"; then
	"${x2go_lib_path}/x2gosyslog" "${0}" 'debug' "'gr'(?) port ${gr_port} is already in use"
	"${x2go_lib_path}/x2gormport" "${current_host_name}" "${session_name}" "${gr_port}"
	gr_port=''
fi
if ! check_system_port "${ss}" "${sound_port}"; then
	"${x2go_lib_path}/x2gosyslog" "${0}" 'debug' "sound port ${sound_port} is already in use"
	"${x2go_lib_path}/x2gormport" "${current_host_name}" "${session_name}" "${sound_port}"
	sound_port=''
fi
if ! check_system_port "${ss}" "${fs_port}"; then
	"${x2go_lib_path}/x2gosyslog" "${0}" 'debug' "file system forwarding port ${fs_port} is already in use"
	"${x2go_lib_path}/x2gormport" "${current_host_name}" "${session_name}" "${fs_port}"
	fs_port=''
fi

if [[ "${telekinesis_enabled}" = '1' ]]; then
	typeset tekictrl_port="$(awk -F ',' '{print $15}' <<< "${session_info}")"
	typeset tekidata_port="$(awk -F ',' '{print $16}' <<< "${session_info}")"
	if ! check_system_port "${ss}" "${tekictrl_port}"; then
		"${x2go_lib_path}/x2gosyslog" "${0}" 'debug' "port ${tekictrl_port} is already in use"
		"${x2go_lib_path}/x2gormport" "${current_host_name}" "${session_name}" "${tekictrl_port}"
		tekictrl_port=''
	fi
	if ! check_system_port "${ss}" "${tekidata_port}"; then
		"${x2go_lib_path}/x2gosyslog" "${0}" 'debug' "port ${tekidata_port} is already in use"
		"${x2go_lib_path}/x2gormport" "${current_host_name}" "${session_name}" "${tekidata_port}"
		tekidata_port=''
	fi
else
	"${x2go_lib_path}/x2gormport" "${current_host_name}" "${session_name}" "${tekictrl_port}"
	"${x2go_lib_path}/x2gormport" "${current_host_name}" "${session_name}" "${tekidata_port}"
	tekictrl_port='0'
	tekidata_port='0'
fi

typeset -i ssh_port='0'
if ! ssh_port="$("${x2go_lib_path}/x2gogetrandomport")"; then
	msg="Unable to get (pseudo-)randomized starting port value."
	"${x2go_lib_path}/x2gosyslog" "${0}" 'err' "${msg}"

	# Make x2goclient fail.
	echo "${msg}" >&2
	exit '5'
fi

typeset -i retry='0'
typeset -i max_retry='10'
typeset -i free_port='0'
typeset output=''
while [[ -z "${gr_port}" ]] || [[ -z "${sound_port}" ]] || [[ -z "${fs_port}" ]] || [[ -z "${tekictrl_port}" ]] || [[ -z "${tekidata_port}" ]]; do
	output=''
	for ((retry = 0; retry < max_retry; ++retry)); do
		free_port='0'
		if free_port="$("${x2go_lib_path}/x2gogetfreeport" "${current_host_name}" "${ss}" 'lowlevel' "${ssh_port}")"; then
			ssh_port="${free_port}"

			output="$("${x2go_lib_path}/x2goinsertport" "${current_host_name}" "${session_name}" "${ssh_port}")"

			if [[ "${output}" = 'inserted' ]]; then
				break
			else
				"${x2go_lib_path}/x2gosyslog" "${0}" 'warning' "unable to insert port into database. Retrying (run $((retry + 1)))."
			fi
		else
			"${x2go_lib_path}/x2gosyslog" "${0}" 'warning' "no free port available, cannot start new session. Retrying (run $((retry + 1)))."
		fi
	done

	if [[ "${output}" != 'inserted' ]]; then
		msg="Unable to find free port or insert new session into database; parameters: hostname (${current_host_name}), session name (${session_name}) and port (${ssh_port})."
		"${x2go_lib_path}/x2gosyslog" "${0}" 'err' "${msg}"

		# Make x2goclient fail.
		echo "${msg}" >&2
		exit '6'
	fi

	if [[ -z "${gr_port}" ]]; then
		gr_port="${ssh_port}"
	elif [[ -z "${sound_port}" ]]; then
		sound_port="${ssh_port}"
	elif [[ -z "${fs_port}" ]]; then
		fs_port="${ssh_port}"
	elif [[ -z "${tekictrl_port}" ]]; then
		tekictrl_port="${ssh_port}"
	elif [[ -z "${tekidata_port}" ]]; then
		tekidata_port="${ssh_port}"
	fi
done


typeset session_dir="${x2go_root}/C-${session_name}"
typeset agent_options_base64="$(base64 -w '0' < "${session_dir}/options")"

# Sessions started with older X2Go Server versions do not feature
# a xinerama option, so handle this gracefully.
# Note that Xinerama support defaulted to on and was handled by the client,
# so keep it like that.
typeset has_xinerama="$(x2gooptionsstring -e -c -b -- "${agent_options_base64}" \
			"$(printf '%s' 'xinerama' | base64 -w '0')" \
			2>'/dev/null')"
if [ -z "${has_xinerama}" ]; then
	has_xinerama='0'
else
	has_xinerama='1'
fi

kbd_type_val="$(sed -e 's#/#\\/#' <<< "${kbd_type_val}")"

typeset keyboard_type="${kbd_type_val}"
if [[ "${set_kbd}" = '0' ]] || [[ "${kbd_type_val}" = 'auto' ]]; then
	keyboard_type='null\/null'
fi

typeset tmp_regex='^(0|none|client|server|both|1)$'
if [[ -n "${clipboard_val}" ]] && [[ "${clipboard_val}" =~ ${tmp_regex} ]]; then
	clipboard="clipboard=${clipboard_val}"
else
	clipboard='clipboard=both'
fi

typeset xinerama_option="$("${x2go_lib_path}/x2goistrue" "${xinerama_val}")"

# I'd really, really, really like to use the bash here string feature to pass
# strings to the base64 program, but we can't.
# Here strings add new line characters to the strings' end and we don't want
# that.
# Work around that "issue" (which, really, makes sense if you interpret here
# strings as text files) and pass it via printf instead.
typeset new_agent_options_base64=''
new_agent_options_base64="$(x2gooptionsstring -t -c -b -- "${agent_options_base64}" \
			    "$(printf 'link=%s' "${link_val}" | base64 -w '0')" \
			    "$(printf 'pack=%s' "${pack_val}" | base64 -w '0')" \
			    "$(printf 'kbtype=%s' "${keyboard_type}" | base64 -w '0')" \
			    "$(printf 'geometry=%s' "${geometry_val}" | base64 -w '0')" \
			    "$(printf 'resize=%s' "${resize_val}" | base64 -w '0')" \
			    "$(printf 'listen=%s' "${gr_port}" | base64 -w '0')" \
			    "$(printf 'fullscreen=%s' "${fullscreen_val}" | base64 -w '0')" \
			    "$(printf '%s' "${clipboard}" | base64 -w '0')" \
			    2>'/dev/null')"

if [ '1' -eq "${has_xinerama}" ]; then
			new_agent_options_base64="$(x2gooptionsstring -t -c -b -- "${new_agent_options_base64}" \
						    "$(printf 'xinerama=%s' "${xinerama_option}" | base64 -w '0')" \
						    2>'/dev/null')"
fi

if [[ -z "${geometry_val}" ]] || [[ "${geometry_val}" = 'fullscreen' ]]; then
	new_agent_options_base64="$(x2gooptionsstring -t -c -b -- "${new_agent_options_base64}" \
				    "$(printf '-geometry' | base64 -w '0')" \
				    2>'/dev/null')"
fi

typeset x2go_client="$(awk '{print $1}' <<< "${SSH_CLIENT}")"
if [[ -z "${x2go_client}" ]]; then
	x2go_client="${current_host_name}"
fi

printf '%s' "${new_agent_options_base64}" | base64 -d >"${session_dir}/options"

# run x2goserver-extensions for pre-resume
x2gofeature 'X2GO_RUN_EXTENSIONS' &>'/dev/null' && x2goserver-run-extensions "${session_name}" 'pre-resume' || true

# clear old keyboard file
rm -Rf "${session_dir}/keyboard"

if kill -HUP "${x2go_agent_pid}" &>'/dev/null'; then
	"${x2go_lib_path}/x2goresume" "${x2go_client}" "${session_name}" "${gr_port}" "${sound_port}" "${fs_port}" >'/dev/null'
	"${x2go_lib_path}/x2gosyslog" "${0}" 'notice' "client ${x2go_client} has successfully resumed session with ID ${session_name}"

	typeset x2go_display="$(cut -d '-' -f '2' <<< "${session_name}")"
	# set client-side keyboard model, type, variant, etc.
	if [[ "${set_kbd}" != '0' ]] && [[ "${kbd_type_val}" = 'auto' ]]; then
		export DISPLAY=":${x2go_display}.0"
		x2gosetkeyboard "${session_name}" &>'/dev/null' &
	fi
	if [[ "${set_kbd}" != '0'  &&  "${kbd_type_val}" != 'auto' ]]; then
		typeset xkbmap="$(sed -e 's/\\//' -e 's/\// /' -e 's/(/ /'  -e 's/)/ /'<<< "${kbd_type_val}")"
		typeset model="$(awk '{print $1}' <<< "${xkbmap}")"
		typeset layout="$(awk '{print $2}' <<< "${xkbmap}")"
		typeset variant="$(awk '{print $3}' <<< "${xkbmap}")"
		typeset xkbmap_arg="-model ${model} -layout ${layout}"
		if [[ "${variant}" != "" ]]; then
			xkbmap_arg="${xkbmap_arg} -variant ${variant}"
		fi
		export DISPLAY=":${x2go_display}.0"
		setxkbmap ${xkbmap_arg}
	fi
	# resume x2godesktopsharing, if it has been in use before the session got suspended
	x2gofeature 'X2GO_DESKTOPSHARING' &>'/dev/null' && x2goresume-desktopsharing "${session_name}" || true

	# run x2goserver-extensions for post-resume
	x2gofeature 'X2GO_RUN_EXTENSIONS' &>'/dev/null' && x2goserver-run-extensions "${session_name}" 'post-resume' || true
	grep 'PPid' "/proc/${PPID}/status" >"${x2go_root}/C-${session_name}/sshd.pid"
else
	msg="ERROR: failed to resume session with ID ${session_name}"
	echo "${msg}" 1>&2
	"${x2go_lib_path}/x2gosyslog" "${0}" 'err' "${msg}"

	# If we reach here it means that the x2goagent process of the session has vanished
	# If this happens than we mark the session as finished...
	"${x2go_lib_path}/x2gochangestatus" 'F' "${session_name}" >'/dev/null'

	# run x2goserver-extensions for fail-resume
	x2gofeature 'X2GO_RUN_EXTENSIONS' &>'/dev/null' && x2goserver-run-extensions "${session_name}" 'fail-resume' || true
fi

printf 'gr_port=%d\nsound_port=%d\nfs_port=%d\n' "${gr_port}" "${sound_port}" "${fs_port}"
if [[ "${telekinesis_enabled}" = '1' ]]; then
	printf 'tekictrl_port=%d\ntekidata_port=%d\n' "${tekictrl_port}" "${tekidata_port}"
fi
