buzz twice to give users more time
[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         'status': CmdEntry(sendcmd(tuerSock, 'status'), 'Shows internal state and sensor information.'),
90 },{
91         # aliases
92         'open': 'unlock',
93 })
94
95 def complete_command(cmd):
96         '''returns a list of commands (as strings) starting with cmd'''
97         return list(filter(lambda x: x.startswith(cmd), commands.keys()))
98 readline.set_completer(lambda cmd, num: (complete_command(cmd)+[None])[num]) # wrap complete_command for readline's weird completer API
99 readline.parse_and_bind("tab: complete") # run completion on tab
100
101 # input loop
102 print("Welcome to tyshell. Use help to see what you can do.")
103 while True:
104         try:
105                 command = input("$ ")
106         except EOFError:
107                 print()
108                 break
109         command = shlex.split(command)
110         if not len(command): continue
111         # find suiting commands
112         if command[0] in commands: # needed in case a complete command is a prefix of another one
113                 cmdoptions = [command[0]]
114         else:
115                 cmdoptions = complete_command(command[0])
116         # check how many we found
117         if len(cmdoptions) == 0: # no commands fit prefix
118                 print("Command %s not found. Use help." % command[0])
119         elif len(cmdoptions) == 1: # exactly one command fits (prefix)
120                 try:
121                         res = commands[cmdoptions[0]].function(command)
122                         if res: break
123                 except Exception as e:
124                         print("Error while executing %s: %s" % (command[0], str(e)))
125                         #print(traceback.format_exc())
126         else: # multiple commands fit the prefix
127                 print("Ambiguous command prefix, please choose one of the following:")
128                 print("\t", " ".join(cmdoptions))
129                 # TODO: put current "command[0]" into the shell for the next command, but such that it is deletable with backspace
130