fix: don't close client sockets too early
[saartuer.git] / tyshell
1 #!/usr/bin/python3
2 import os
3 import readline
4 import shlex
5 import sys
6 import subprocess
7 import socket
8 import pwd
9 import grp
10 import traceback
11 from collections import namedtuple
12
13 tuerSock = "/run/tuer.sock"
14
15 # use a histfile
16 histfile = os.path.join(os.path.expanduser("~"), ".tyshellhist")
17 try:
18     readline.read_history_file(histfile)
19 except IOError:
20     pass
21 import atexit
22 atexit.register(readline.write_history_file, histfile)
23
24 # available commands
25 def helpcmd(c):
26         if (len(c) > 1):
27                 print(commands.get(c[1],(None,'Can\'t find help for command %s'%(c[1]))).helpstring)
28         else:
29                 print("Available commands: %s" % ", ".join(sorted(commands.keys())))
30                 print("Use 'help command' to get more information on the command 'command'")
31
32 def extcmd(cmd):
33         def run(c):
34                 ret = subprocess.call(cmd)
35                 if ret != 0:
36                         print("Command returned non-zero exit statis %d" % ret)
37         return run
38
39 def sendcmd(addr, cmd):
40         def run(c):
41                 print("206 Sending command %s..." % (cmd))
42                 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
43                 s.connect(addr)
44                 s.settimeout(60.0)
45                 s.send(cmd.encode())
46                 while True:
47                         data = s.recv(256)
48                         if not len(data): break
49                         print(data.decode('utf-8'))
50                 s.close()
51         return run
52
53 def exitcmd(c):
54         print("Bye")
55         return True
56
57 def whocmd(c):
58         for n in grp.getgrnam("tuer").gr_mem:
59                 p = pwd.getpwnam(n)
60                 print (p.pw_name, " - ", p.pw_gecos)
61
62 def alias (cmds, aliases):
63         for newname, oldname in aliases.items():
64                 cmds[newname] = cmds[oldname]
65         return cmds
66
67 def prompt_sure(f,msg):
68         def run(c):
69                 try:
70                         command = input("%s Are you sure? (yes/no) > " % msg)
71                 except EOFError:
72                         print()
73                         return
74                 if command[0] == 'y':
75                         return f(c)
76         return run
77
78 CmdEntry = namedtuple('CmdEntry','function helpstring')
79
80 commands = alias({
81         'exit': CmdEntry(exitcmd, 'Quits this shell'),
82         'help': CmdEntry(helpcmd, 'Helps you getting to know the available commands'),
83         'unlock': CmdEntry(sendcmd(tuerSock, 'unlock'), 'Will try to unlock the apartment door'),
84         'lock': CmdEntry(sendcmd(tuerSock, 'lock'), 'If in fallback mode, try to lock the apartment door. If not in fallback mode, you must use the switch near the door.'),
85         'buzz': CmdEntry(sendcmd(tuerSock, 'buzz'), 'Will buzz the buzzer for the street door'),
86         'who': CmdEntry(whocmd, 'Shows the list of people, who are allowed to control this system'),
87         'fallback_mode_on': CmdEntry(prompt_sure(sendcmd(tuerSock, 'fallback_mode_on'),'WARNING: This action will be reported to the admins. Use this only in case of Sphinx hardware failure when you need to ignore erroneous sensor input!'), 'Sets the system in a state where it is less dependent on sensoric input. Use it only when sensors are broken.'),
88         'fallback_mode_off': CmdEntry(prompt_sure(sendcmd(tuerSock, 'fallback_mode_off'),'WARNING: This action will be reported to the admins. Use this only if you have fixed the sensors of the Sphinx or activated fallback mode by accident!'), 'Resets the system to the default state. Use this when you have just repaired the sensors of the Sphinx.'),
89 },{
90         # aliases
91         'open': 'unlock',
92 })
93
94 def complete_command(cmd):
95         '''returns a list of commands (as strings) starting with cmd'''
96         return list(filter(lambda x: x.startswith(cmd), commands.keys()))
97 readline.set_completer(lambda cmd, num: (complete_command(cmd)+[None])[num]) # wrap complete_command for readline's weird completer API
98 readline.parse_and_bind("tab: complete") # run completion on tab
99
100 # input loop
101 print("Welcome to tyshell. Use help to see what you can do.")
102 while True:
103         try:
104                 command = input("$ ")
105         except EOFError:
106                 print()
107                 break
108         command = shlex.split(command)
109         if not len(command): continue
110         # find suiting commands
111         if command[0] in commands: # needed in case a complete command is a prefix of another one
112                 cmdoptions = [command[0]]
113         else:
114                 cmdoptions = complete_command(command[0])
115         # check how many we found
116         if len(cmdoptions) == 0: # no commands fit prefix
117                 print("Command %s not found. Use help." % command[0])
118         elif len(cmdoptions) == 1: # exactly one command fits (prefix)
119                 try:
120                         res = commands[cmdoptions[0]].function(command)
121                         if res: break
122                 except Exception as e:
123                         print("Error while executing %s: %s" % (command[0], str(e)))
124                         #print(traceback.format_exc())
125         else: # multiple commands fit the prefix
126                 print("Ambiguous command prefix, please choose one of the following:")
127                 print("\t", " ".join(cmdoptions))
128                 # TODO: put current "command[0]" into the shell for the next command, but such that it is deletable with backspace
129