#!/usr/bin/perl

# Copyright (C) 2007-2018 X2Go Project - https://wiki.x2go.org
# Copyright (C) 2007-2018 Oleksandr Shneyder <o.shneyder@phoca-gmbh.de>
# Copyright (C) 2007-2018 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.

use strict;

$ENV{'PATH'} = "/bin:/sbin:/usr/bin:/usr/sbin";

use Sys::Hostname;
use Sys::Syslog qw( :standard :macros );
use POSIX;

use X2Go::Config qw( get_config );
use X2Go::Log qw( loglevel );
use X2Go::SupeReNicer qw( superenice );
use X2Go::Server::Agent qw( session_is_suspended session_is_running session_has_terminated );
use X2Go::Server::DB qw( dbsys_rmsessionsroot );
use X2Go::Utils qw( system_capture_stdout_output is_true );
my $x2go_lib_path = system_capture_stdout_output("x2gopath", "libexec");
use Getopt::Long;
use Pod::Usage;

Getopt::Long::Configure("gnu_getopt", "no_auto_abbrev");

my $debug = 0;
my $help = 0;
my $man = 0;
GetOptions('debug|d' => \$debug, 'help|?|h' => \$help, 'man' => \$man) or pod2usage(2);
pod2usage(1) if $help;
pod2usage(-verbose => 2, -exitval => 0) if $man;

openlog($0,'cons,pid','user');
if ($debug)
{
	setlogmask( LOG_UPTO (LOG_DEBUG) );
}
else
{
	setlogmask( LOG_UPTO(loglevel()) );
}


sub check_pid
{
	my $pid=shift;
	my $sess=shift;
	my $sec=shift;
	if ($sec < 30)
	{
		return 1;
	}
	open (F,"</proc/$pid/cmdline") or return 0;
	my $text=<F>;
	close (F);
	if ($text =~ m/$sess/)
	{
		return 1;
	}
	return 0;
}

sub catch_term
{
	unlink("/var/run/x2goserver.pid");
	closelog;
	exit;
}

sub log_message
{
	my $loglevel=shift;
	my $msg=shift;
	syslog($loglevel, $msg);
	if ($debug)
	{
		my $logtime = localtime (time ());
		print "$logtime ::: $loglevel ::: $msg\n";
	}
}

my $uname;
my $serv = hostname;
my $pid;

if (! $debug)
{
	$pid = fork();
}

if ((!$debug) && (not defined $pid))
{
	print "resources not avilable.\n";
}
elsif ($pid != 0)
{
	open (F,">/var/run/x2goserver.pid");
	print F "$pid\n";
	close(F);
	closelog;
}
elsif ($pid == 0 )
{
	# check if we are to use the superenicer script for throttling does the nice level
	# of suspended sessions...
	my $Config = get_config();

	my $superenice_enable = is_true($Config->param("superenicer.enable"));
	my $superenice_forceuser = is_true($Config->param("superenicer.force-user-renice"));

	my $superenice_normal=$Config->param("superenicer.normal-nice-level");
	my $superenice_idle=$Config->param("superenicer.idle-nice-level");
	my $superenice_ignoredusers=$Config->param("superenicer.ignored-users");

	if ( ! $debug )
	{
		# close any open file descriptor left open by our parent before the fork
		my $fd;
		for (glob "/proc/$$/fd/*") {
			if ( ! -e $_ ) { next; }
			if ($_ =~ m/\/proc\/\d+\/fd\/(\d+)/) {
				$fd = $1;
				if ( $fd < 3 ) { next; }
				if (! POSIX::close($fd)) {
					log_message('warning', "Error Closing $_: $!");
				}
			}
		}

		# redirect stdin, stdout and stderr
		open *STDIN, q{<}, '/dev/null';
		open *STDOUT, q{>>}, '/dev/null';
		open *STDERR, q{>>}, '/dev/null';

	}

	$SIG{TERM}=\&catch_term;
	$SIG{CHLD} = sub { wait };

	my @remembered_finished_sessions = ();
	my %remembered_sessions_status = ();
	my %remembered_sessions_status_since_time = ();
	my %remembered_sessions_status_since_iterations = ();

	my $last_reniced = 0;

	my $user;
	my $effective_user;

	while(sleep 2)
	{
		my $outp=system_capture_stdout_output("$x2go_lib_path/x2golistsessions_sql", "$serv");
		my @outp=split("\n","$outp");

		# forget earlier remembered blocked sessions
		while ( my ($session, $remembered_since) = each(%remembered_sessions_status_since_time) )
		{
			if (! join(',', @outp)=~m/$session/)
			{
				delete $remembered_sessions_status{$session};
				delete $remembered_sessions_status_since_time{$session};
				delete $remembered_sessions_status_since_iterations{$session};
			}
		}

		push (@outp, @remembered_finished_sessions);

		for (my $i=0;$i<@outp;$i++)
		{

			my @sinfo=split('\\|',"@outp[$i]");

			# Clean up invalid sessions (i.e., those for which no nxagent process is running anymore)
			# from the session database, if the status didn't change for more than 10 seconds.
			if ((!@sinfo[0]) && (defined ($remembered_sessions_status_since_time{@sinfo[1]})) && ((time () - $remembered_sessions_status_since_time{@sinfo[1]}) >= 10))
			{
				dbsys_rmsessionsroot(@sinfo[1]);
				next;
			}

			# Update current status once per session. Avoids race conditions.
			my $current_status = system_capture_stdout_output ("$x2go_lib_path/x2gogetstatus", "@sinfo[1]");
			if (length ($current_status)) {
				if (@sinfo[4] ne $current_status) {
					log_message ('debug', "@sinfo[1]: updating session status from '@sinfo[4]' to '$current_status'.");
				}
				@sinfo[4] = $current_status;
			}
			else {
				log_message ('debug', "@sinfo[1]: removed from database, not updating status.");
			}

			# Record the status of either previously unseen or status-changing sessions and the current time.
			if (!(defined ($remembered_sessions_status_since_time{@sinfo[1]})) || ($remembered_sessions_status{@sinfo[1]} !~ m/@sinfo[4]/)) {
				$remembered_sessions_status{@sinfo[1]} = @sinfo[4];
				$remembered_sessions_status_since_time{@sinfo[1]} = time ();
				$remembered_sessions_status_since_iterations{@sinfo[1]} = 0;
			}
			else {
				# Current status matches previously seen status and a time is recorded.
				# Increment counter for this session.
				++$remembered_sessions_status_since_iterations{$sinfo[1]};
			}

			#print @sinfo[1], ': ', $remembered_sessions_status_since_time{@sinfo[1]},' (' , $remembered_sessions_status_since_iterations{@sinfo[1]} ,'iterations) ',$remembered_sessions_status{@sinfo[1]},"\n";

			if (@sinfo[4] eq 'F')
			{
				if (!check_pid (@sinfo[0], @sinfo[1], 100)) {
					# No clean up necessary, as the agent is dead.
					# Removing sockets again would only lead to potentially
					# overwriting the socket another session claimed in-between.
					log_message ('debug', "@sinfo[1]: in failed state, but agent is gone, forgetting.");
					@remembered_finished_sessions = grep (!/\Q@sinfo[1]\E/, @remembered_finished_sessions);
					delete $remembered_sessions_status{@sinfo[1]};
					delete $remembered_sessions_status_since_time{@sinfo[1]};
					delete $remembered_sessions_status_since_iterations{@sinfo[1]};

					next;
				}

				# Reaching this part means that nxagent is still executing.
				log_message ('debug', "@sinfo[1]: is blocked.");
				# Only add to finished list if it isn't in there already.
				if (!(grep { ((defined ($_)) && ($_ =~ m/\Q@sinfo[1]\E/)) } @remembered_finished_sessions)) {
					log_message ('debug', "@sinfo[1]: adding to finished list.");
					push (@remembered_finished_sessions, join ('|', @sinfo));
				}

				# Kill the process if blocked for more than 20 seconds and nxagent is still up.
				if ((time () - $remembered_sessions_status_since_time{@sinfo[1]}) >= 20)
				{
					log_message ('debug', "@sinfo[1]: blocked for more than 20 seconds.");
					# send SIGKILL to dangling X-server processes
					log_message('warning', "@sinfo[1]: found stale X-server process (@sinfo[0]), sending SIGKILL");
					system("kill", "-9", "@sinfo[0]");

					# Remove all references to this sessions. We will never see it again.
					@remembered_finished_sessions = grep (!/\Q@sinfo[1]\E/, @remembered_finished_sessions);
					delete $remembered_sessions_status{@sinfo[1]};
					delete $remembered_sessions_status_since_time{@sinfo[1]};
					delete $remembered_sessions_status_since_iterations{@sinfo[1]};

					my $display = @sinfo[2];
					if (-S "/tmp/.X11-unix/X$display") {
						# remove the NX-X11 socket file (as the agent will not have managed after a kill -9)
						log_message('warning', "@sinfo[1], pid @sinfo[0]: cleaning up stale X11 socket file: /tmp/.X11-unix/X$display");
						unlink("/tmp/.X11-unix/X$display");
					}
					if (-e "/tmp/.X$display-lock") {
						# remove the NX-X11 lock file (as the agent will not have managed after a kill -9)
						log_message('warning', "@sinfo[1], pid @sinfo[0]: cleaning up stale X11 lock file: /tmp/.X$display-lock");
						unlink("/tmp/.X$display-lock");
					}
					log_message('debug', "@sinfo[1]: unmounting all shares");
					system( "su", "@sinfo[11]", "-s", "/bin/sh", "-c", "x2goumount-session @sinfo[1]");
					#remove port forwarding
					system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "$x2go_lib_path/x2gormforward @sinfo[1]");
				}
			}
			elsif (! check_pid (@sinfo[0],@sinfo[1],@sinfo[12]))
			{
				$user = @sinfo[11];

				# For shadow sessions we need to su to the user who provided the shared desktop (not the one who
				# requested the desktop sharing)...
				if ( @sinfo[1] =~ m/$user-[0-9]{2,}-[0-9]{10,}_stS(0|1)XSHAD.*XSHAD.*/ )
				{
					$effective_user = @sinfo[1];
					$effective_user =~ s/$user\-[0-9]{2,}\-[0-9]{10}_stS[0-1]XSHAD(.*)XSHAD.*/$1/;
					$user = $effective_user;
				}

				log_message('debug', "@sinfo[1], pid @sinfo[0]: does not exist, changing status from @sinfo[4] to F");
				system("su", "$user", "-s", "/bin/sh", "-c", "$x2go_lib_path/x2gochangestatus 'F' @sinfo[1]");

				my $display = @sinfo[2];
				if (-S "/tmp/.X11-unix/X$display") {
					# remove the NX-X11 socket file (we don't know how the agent disappeared,
					# someone might have shot it with kill -9)
					log_message('warning', "@sinfo[1], pid @sinfo[0]: cleaning up stale X11 socket file: /tmp/.X11-unix/X$display");
					unlink("/tmp/.X11-unix/X$display");
				}
				if (-e "/tmp/.X$display-lock") {
					# remove the NX-X11 lock file (we don't know how the agent disappeared,
					# someone might have shot it with kill -9)
					log_message('warning', "@sinfo[1], pid @sinfo[0]: cleaning up stale X11 lock file: /tmp/.X$display-lock");
					unlink("/tmp/.X$display-lock");
				}
				log_message('debug', "@sinfo[1]: unmounting all shares");
				system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "x2goumount-session @sinfo[1]");
			}
			else
			{
				if (@sinfo[4] eq 'R')
				{
					if (session_is_suspended(@sinfo[1],@sinfo[11]))
					{
						system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "$x2go_lib_path/x2gochangestatus S @sinfo[1]");
						log_message('debug', "@sinfo[1]: is suspended, changing status from @sinfo[4] to S");
						log_message('debug', "@sinfo[1]: unmounting all shares");
						system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "x2goumount-session @sinfo[1]");
						#remove port forwarding
						system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "$x2go_lib_path/x2gormforward @sinfo[1]");
					}
				}
				if (@sinfo[4] eq 'S')
				{
					if (session_is_running(@sinfo[1],@sinfo[11])) {
						# Give the session a grace period of one iteration.
						# If it didn't change into suspended state by then, suspend it "forcefully".
						if ($remembered_sessions_status_since_iterations{@sinfo[1]} == 1) {
							log_message('debug', "@sinfo[1]: unmounting all shares");
							system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "x2goumount-session @sinfo[1]");
							system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "x2gosuspend-session @sinfo[1]");
							log_message('debug', "@sinfo[1]: was found running and has now been suspended");
						}
						elsif ($remembered_sessions_status_since_iterations{@sinfo[1]} == 2) {
							# Issue a diagnostic warning in case suspension was already tried, but failed to
							# actually change the status for some reason.
							log_message('warning', "@sinfo[1]: session status @sinfo[4] desynchronized with current status (R) and session suspend already tried unsuccessfully");
						}
					}
				}
				if (@sinfo[4] eq 'T')
				{
					if (!session_has_terminated(@sinfo[1],@sinfo[11]))
					{
						log_message('debug', "@sinfo[1]: unmounting all shares");
						system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "x2goumount-session @sinfo[1]");
						system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "x2goterminate-session @sinfo[1]");
						log_message('debug', "@sinfo[1]: termination has been requested via the session DB");
						#remove port forwarding
						system("su", "@sinfo[11]", "-s", "/bin/sh", "-c", "$x2go_lib_path/x2gormforward @sinfo[1]");
					}
				}
			}
		}

		# call superenicer script if requested through x2goserver.conf every six seconds
		if ( $superenice_enable ) {
			$last_reniced += 2;
			if ( $last_reniced ge 4 ) {
				superenice($superenice_normal, $superenice_idle, $superenice_ignoredusers, $superenice_forceuser);
				$last_reniced = 0;
			}
		}

	}
}

__END__
=head1 NAME

x2gocleansessions - X2Go Server Cleanup Daemon

=head1 SYNOPSIS

x2gocleansessions [options]

  Options:
    --help|-h|-?            brief help message
    --man                   full documentation
    --debug|-d              enable debugging and don't daemonize

=head1 OPTIONS

=over 8

=item B<--help>|B<-?>|B<-h>

Print a brief help message and exits.

=item B<--man>

Prints the manual page and exits.

=item B<--debug>|B<-d>

Override debugging setting in global config and keep application in foreground
instead of daemonizing.

=back

=head1 DESCRIPTION

B<x2gocleansessions> is run as a service on X2Go servers to handle the cleanup
of stale sessions.

B<x2gocleansessions> must be run (as a service) with root privileges.

=head1 AUTHOR

This manual has been written by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
for the X2Go project (https://www.x2go.org).

=cut
