#  fsh - fast remote execution
#  Copyright (C) 1999-2001 by Per Cederqvist.
#
#  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., 675 Mass Ave, Cambridge, MA 02139, USA. */

import errno
import getopt
import getpass
import os
import select
import socket
import stat
import string
import sys
import time

import fshlib
import fshconfig

class remote:
    def __init__(self, method, login, server, background_wanted):
        self.__background_wanted = background_wanted
	in_fd, self.w = os.pipe()
	self.r, out_fd = os.pipe()
	self.child = os.fork()
	if self.child == 0:
	    # child
            os.close(self.w)
            os.close(self.r)
	    cmd = [method]
	    if login != 0:
		cmd = cmd + ["-l", login]
	    cmd = cmd + [server, 'in.fshd']
	    os.dup2(in_fd, 0)
	    os.dup2(out_fd, 1)
	    os.close(in_fd)
	    os.close(out_fd)
	    os.execvp(method, cmd)
	    sys.exit(1)
	# parent
	os.close(in_fd)
	os.close(out_fd)
	fshlib.set_nonblocking(self.w)
	fshlib.set_nonblocking(self.r)
	self.send_queue = []
	self.receive_queue = []
        self.state = 0

    def send(self, data):
	if data != "":
	    self.send_queue.append(data)
	if self.w == -1:
	    self.send_queue = []

    def select_action(self, r, w, e):
	if self.w in w:
	    if fshlib.write(self.w, self.send_queue) == -1:
		os.close(self.w)
		self.w = -1
		print "End of file writing to remote system."

	if self.r in r:
	    if fshlib.read(self.r, self.receive_queue, 4096) == -1:
		print "Tunnel closed."
		os.close(self.r)
		self.r = -1

    def select_set(self):
	# Return a tuple of three lists of file descriptors: the read,
	# write and exception fd sets.
	r = []
	w = []
	if self.w != -1 and self.send_queue != []:
	    w.append(self.w)
	if self.r != -1:
	    r.append(self.r)
	return (r, w, [])

    def __readline(self):
	if len(self.receive_queue) == 0:
	    return None
        ix = string.find(self.receive_queue[0], "\n")
        if ix == -1 and len(self.receive_queue) > 1:
	    self.receive_queue = [string.join(self.receive_queue, "")]
	    ix = string.find(self.receive_queue[0], "\n")
	if ix == -1:
            return None
        res = self.receive_queue[0][:ix]
        self.receive_queue[0] = self.receive_queue[0][ix+1:]
        return res

    def poll_response(self):
        while self.state == 0:
            s = self.__readline()
            if s == None:
                return [None, None, None]
            if s == "fsh 1":
                if self.__background_wanted:
                    background()
                self.state = 1
		print "Connection established"
                break
        return fshlib.parse_line(self.receive_queue, 1)

    def is_closed(self):
	return self.r == -1 or self.w == -1

class client:
    def __init__(self, socket):
        self.socket = socket.fileno()
        self.socket_object = socket
        self.send_queue = []
        self.receive_queue = []
	self.pending_close = 0

    def select_set(self):
        r = []
        w = []
        e = []
        live = 0
        if self.socket != -1:
            r.append(self.socket)
            if self.send_queue != []:
                w.append(self.socket)
            live = 1
        return (r, w, e, live)

    def select_action(self, r, w, e):
        # Handle any possible read/write.
        # Return 0 normally, 1 if the connection was unexpectedly lost.
        if self.socket == -1:
            return 0
        if self.socket in r:
	    if fshlib.read(self.socket, self.receive_queue, 4096) == -1:
		self.socket_object.close()
		self.socket = -1
                return 1

        if self.socket in w:
	    if fshlib.write(self.socket, self.send_queue) == -1:
		self.socket_object.close()
		self.socket = -1
		return 1
	    if self.pending_close and self.send_queue == []:
		self.socket_object.close()
		self.socket = -1

        return 0
                
    def poll_command(self):
        # return a parsed command, if one is available
        return fshlib.parse_line(self.receive_queue, 0)

    def send(self, data):
	if data != "":
	    self.send_queue.append(data)
	if self.socket == -1:
	    self.send_queue = []

    def close(self):
	self.pending_close = 1

    def is_closed(self):
	return self.socket == -1

    def drained(self):
	return self.send_queue == []

class fshd:
    def __init__(self, server, method, login, use_l_flag, background_wanted,
                 timeout):
	# Connect to server using method.  Parse away leading
	# noice until we receive "^fsh ".  Check the version number.
        sockname, sockdir = fshlib.fshd_socket(server, method, login)
        try:
            os.mkdir(sockdir, 0700)
        except os.error, (eno, emsg):
            if eno == errno.EEXIST:
                status = os.lstat(sockdir)
                if status[stat.ST_UID] != os.getuid():
                    raise
                if not stat.S_ISDIR(status[stat.ST_MODE]):
                    raise
                os.chmod(sockdir, 0700)
            else:
                raise
	self.r = remote(method, use_l_flag and login, server,
                        background_wanted)
        self.sessions = {}
        self.listen = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
	self.listen.setblocking(0)
        self.listen.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	try:
	    os.unlink(sockname)
	except os.error, (eno, emsg):
	    if eno != errno.ENOENT:
		raise
        self.listen.bind(sockname)
        self.listen.listen(3)
        self.__timeout = timeout
        self.__expire_time = None
        self.setup_timer()

    def setup_timer(self):
        if self.__expire_time == None and self.__timeout != None:
            self.__expire_time = time.time() + self.__timeout

    def cancel_timer(self):
        if self.__expire_time != None:
            self.__expire_time = None

    def timeout(self):
        if self.__expire_time == None:
            return 10
        else:
            left = self.__expire_time - time.time()
            if left < 0:
                left = 0
            if left > 60:
                left = 60
            return left

    def expired(self):
        return self.__expire_time != None and self.__expire_time <= time.time()

    def toploop(self):
        try:
            while 1:
		drained = self.r.is_closed()
                r, w, e = self.r.select_set()
                r.append(self.listen.fileno())
                live = 0
                for s in self.sessions.keys():
		    if drained and not self.sessions[s].drained():
			drained = 0
                    r1, w1, e1, live1 = self.sessions[s].select_set()
                    r = r + r1
                    w = w + w1
                    e = e + e1
                    live = live + live1
		if drained:
		    sys.exit(0)
                if live > 0:
                    self.cancel_timer()
                else:
                    self.setup_timer()
                    if self.expired():
                        sys.exit(0)
                r, w, e = select.select(r, w, e, self.timeout())
                self.r.select_action(r, w, e)
                if self.listen.fileno() in r:
                    sock, addr = self.listen.accept()
		    sock.setblocking(0)
		    s = 1
		    while (self.sessions.has_key(s) and \
			   not self.sessions[s].is_closed()):
			s = s + 1
                    print s, "start"
                    self.sessions[s] = client(sock)
                while 1:
                    cmd, s, data = self.r.poll_response()
                    if cmd == None:
                        break
                    elif cmd in ["stdout", "stderr", "exit", "signal-exit", \
				 "stdin-flow"]:
			self.sessions[s].send("%s %s\n" % (
			    cmd, fshlib.hollerith(data)))
		    elif cmd in ["eof-stdin", "eof-stdout", "eof-stderr"]:
			self.sessions[s].send("%s\n" % cmd)
		    elif cmd == "eos":
			self.sessions[s].send("eos\n")
			self.sessions[s].close()
			print s, "EOS"
		    else:
			print "Unknown command from in.fshd:", cmd
                for s in self.sessions.keys():
                    if self.sessions[s].select_action(r, w, e) == 1:
                        # fsh died, so kill this session.
                        self.r.send("eos %d\n" % s)
                    while 1:
                        cmd, data = self.sessions[s].poll_command()
                        if cmd == None:
                            break
                        if data == None:
                            self.r.send("%s %d\n" % (cmd, s))
                        else:
                            self.r.send("%s %d %s\n" % (
                                cmd, s, fshlib.hollerith(data)))
			    if cmd == "new":
				print s, "$", data
        except KeyboardInterrupt:
            for s in self.sessions.keys():
                self.sessions[s].send("eos\n")
            sys.exit(0)

def background():
    try:
	signal.signal(signal.SIGTTOU, signal.SIG_IGN)
    except:
	pass
    try:
	signal.signal(signal.SIGTTIN, signal.SIG_IGN)
    except:
	pass
    try:
	signal.signal(signal.SIGTSTP, signal.SIG_IGN)
    except:
	pass

    if os.fork() > 0:
	os._exit(0)	# parent

    try:
	os.setsid()
    except:
	pass

    for i in range(3):
    	try:
	    os.close(i)
	except os.error, (eno, emsg):
	    if eno != errno.EBADF:
		raise
    fd = os.open("/dev/null", os.O_RDWR)
    assert fd == 0
    fd = os.dup(0)
    assert fd == 1
    fd = os.dup(0)
    assert fd == 2

def open_fds():
    """Return a list of file descriptors that might be open.
    """

    try:
        import resource
    except ImportError:
        # Fall back to using a large value.
        return range(1024)
    return range(resource.getrlimit(resource.RLIMIT_NOFILE)[0] + 1)

def usage(ret):
    sys.stderr.write(
	"fshd: usage: fshd [options] host\n"
	"             fshd { -h | --help }\n"
	"             fshd { -V | --version }\n")
    if ret == 0:
        sys.stderr.write(
            "Options:\n"
            "  -b --background       Run in the background.\n"
            "  -r method             Use ``method'' (e.g. ``rsh'') instead "
            "of ssh.\n"
            "  -l login              Log in as user ``login''.\n"
            "  -T --timeout=timeout  Set idle timeout (in seconds); exit when "
            "no session\n                        has existed for this long. "
            "0 disables the timeout.\n")

    sys.exit(ret)

def main():
    method = "ssh"
    login = None
    use_l_flag = 0
    print_version = 0
    b_flag = 0
    timeout = fshconfig.default_fshd_timeout
    try:
	opts, args = getopt.getopt(sys.argv[1:], "hr:l:bVT:",
                                   ["version", "help", "background",
                                    "timeout="])
    except getopt.error, msg:
	sys.stderr.write(str(msg) + "\n")
	sys.exit(1)
    for opt, val in opts:
	if opt == "-r":
	    method = fshlib.shell_unquote(val)
	elif opt == "-l":
	    login = val
	    use_l_flag = 1
	elif opt == "-b" or opt == "--background":
	    b_flag = 1
	elif opt == "-V" or opt == "--version":
	    print_version = 1
	elif opt == "-h" or opt == "--help":
	    usage(0)
        elif opt == "-T" or opt == "--timeout":
            timeout = string.atof(val)
        else:
            assert 0
    if print_version:
	fshlib.print_version("fshd")
    if len(args) != 1:
	usage(1)
    if login == None:
	login = getpass.getuser()

    for i in open_fds():
        if i > 2:
            try:
                os.close(i)
            except os.error, (eno, emsg):
                if eno != errno.EBADF:
                    raise

    s = fshd(args[0], method, login, use_l_flag, b_flag, timeout)
    s.toploop()

if __name__ == '__main__':
    main()
