be able to reboot the DEC server when it dies
[uccvend-vendserver.git] / sql-edition / servers / VendServer.py
1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5
6 import sys, os, string, re, pwd
7 if USE_DB: import pg
8 from time import time, sleep
9 from popen2 import popen2
10 from LATClient import LATClient
11 from VendingMachine import VendingMachine
12 from HorizScroll import HorizScroll
13 from random import random, seed
14 from Idler import TrainIdler,GrayIdler
15
16 GREETING = 'UCC SNACKS'
17 PIN_LENGTH = 4
18
19 DOOR = 1
20 SWITCH = 2
21 KEY = 3
22
23 class DispenseDatabase:
24         def __init__(self, vending_machine, host, name, user, password):
25                 self.vending_machine = vending_machine
26                 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
27                 self.db.query('LISTEN vend_requests')
28
29         def process_requests(self):
30                 print 'processing'
31                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
32                 try:
33                         outstanding = self.db.query(query).getresult()
34                 except (pg.error,), db_err:
35                         sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
36                         return
37                 for (id, slot) in outstanding:
38                         (worked, code, string) = self.vending_machine.vend(slot)
39                         print (worked, code, string)
40                         if worked:
41                                 query = 'SELECT vend_success(%s)'%id
42                                 self.db.query(query).getresult()
43                         else:
44                                 query = 'SELECT vend_failed(%s)'%id
45                                 self.db.query(query).getresult()
46
47         def handle_events(self):
48                 notifier = self.db.getnotify()
49                 while notifier is not None:
50                         self.process_requests()
51                         notify = self.db.getnotify()
52
53 def scroll_options(username, mk, welcome = False):
54         if welcome:
55                 msg = [(center('WELCOME'), False, 0.8),
56                            (center(username), False, 0.8)]
57         else:
58                 msg = []
59         choices = ' '*10+'CHOICES: '
60         try:
61                 coke_machine = file('/home/other/coke/coke_contents')
62                 cokes = coke_machine.readlines()
63                 coke_machine.close()
64         except:
65                 cokes = []
66                 pass
67         for c in cokes:
68                 c = c.strip()
69                 (slot_num, price, slot_name) = c.split(' ', 2)
70                 if slot_name == 'dead': continue
71                 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
72         choices += '55-DOOR '
73         choices += 'OR A SNACK. '
74         choices += '99 TO READ AGAIN. '
75         choices += 'CHOICE?   '
76         msg.append((choices, False, None))
77         mk.set_messages(msg)
78
79 def get_pin(uid):
80         try:
81                 info = pwd.getpwuid(uid)
82         except KeyError:
83                 return None
84         if info.pw_dir == None: return False
85         pinfile = os.path.join(info.pw_dir, '.pin')
86         try:
87                 s = os.stat(pinfile)
88         except OSError:
89                 return None
90         if s.st_mode & 077:
91                 return None
92         try:
93                 f = file(pinfile)
94         except IOError:
95                 return None
96         pinstr = f.readline()
97         f.close()
98         if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
99                 return None
100         return int(pinstr)
101
102 def has_good_pin(uid):
103         return get_pin(uid) != None
104
105 def verify_user_pin(uid, pin):
106         if get_pin(uid) == pin:
107                 info = pwd.getpwuid(uid)
108                 return info.pw_name
109         else:
110                 return None
111
112 def door_open_mode(vending_machine):
113         print "Entering open door mode"
114         v.display("-FEED  ME-")
115         while True:
116                 e = v.next_event()
117                 if e == None: break
118                 (event, params) = e
119                 if event == DOOR:
120                         if params == 1: # door closed
121                                 v.display("-YUM YUM!-")
122                                 sleep(1)
123                                 return
124
125 def cookie(v):
126         seed(time())
127         messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
128         choice = int(random()*len(messages))
129         msg = messages[choice]
130         left = range(len(msg))
131         for i in range(len(msg)):
132                 if msg[i] == ' ': left.remove(i)
133         reveal = 1
134         while left:
135                 s = ''
136                 for i in range(0, len(msg)):
137                         if i in left:
138                                 if reveal == 0:
139                                         left.remove(i)
140                                         s += msg[i]
141                                 else:
142                                         s += chr(int(random()*26)+ord('A'))
143                                 reveal += 1
144                                 reveal %= 17
145                         else:
146                                 s += msg[i]
147                 v.display(s)
148
149 def center(str):
150         LEN = 10
151         return ' '*((LEN-len(str))/2)+str
152
153 class MessageKeeper:
154         def __init__(self, vendie):
155                 # Each element of scrolling_message should be a 3-tuple of
156                 # ('message', True/False if it is to be repeated, time to display)
157                 self.scrolling_message = []
158                 self.v = vendie
159                 self.next_update = None
160
161         def set_message(self, string):
162                 self.scrolling_message = [(string, False, None)]
163                 self.update_display(True)
164
165         def set_messages(self, strings):
166                 self.scrolling_message = strings
167                 self.update_display(True)
168
169         def update_display(self, forced = False):
170                 if not forced and self.next_update != None and time() < self.next_update:
171                         return
172                 if len(self.scrolling_message) > 0:
173                         if len(self.scrolling_message[0][0]) > 10:
174                                 (m, r, t) = self.scrolling_message[0]
175                                 a = []
176                                 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
177                                 if t == None:
178                                         t = 0.1
179                                 else:
180                                         t = t / len(exp)
181                                 for x in exp:
182                                         a.append((x, r, t))
183                                 del self.scrolling_message[0]
184                                 self.scrolling_message = a + self.scrolling_message
185                         newmsg = self.scrolling_message[0]
186                         if newmsg[2] != None:
187                                 self.next_update = time() + newmsg[2]
188                         else:
189                                 self.next_update = None
190                         self.v.display(self.scrolling_message[0][0])
191                         if self.scrolling_message[0][1]:
192                                 self.scrolling_message.append(self.scrolling_message[0])
193                         del self.scrolling_message[0]
194
195         def done(self):
196                 return len(self.scrolling_message) == 0
197
198 def run_forever(rfh, wfh):
199         v = VendingMachine(rfh, wfh)
200         print 'PING is', v.ping()
201
202         if USE_DB: db = DispenseDatabase(v, DBServer, DBName, DBUser, DBPassword)
203         cur_user = ''
204         cur_pin = ''
205         cur_selection = ''
206
207         mk = MessageKeeper(v)
208         mk.set_message(GREETING)
209         time_to_autologout = None
210         #idler = TrainIdler(v)
211         #idler = GrayIdler(v)
212         idler = GrayIdler(v,one="*",zero="-")
213         time_to_idle = None
214         last_timeout_refresh = None
215
216         while True:
217                 if USE_DB: db.handle_events()
218
219                 if time_to_autologout != None:
220                         time_left = time_to_autologout - time()
221                         if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
222                                 mk.set_message('LOGOUT: '+str(int(time_left)))
223                                 last_timeout_refresh = int(time_left)
224                                 cur_selection = ''
225
226                 if time_to_autologout != None and time_to_autologout - time() <= 0:
227                         time_to_autologout = None
228                         cur_user = ''
229                         cur_pin = ''
230                         cur_selection = ''
231                         mk.set_message(GREETING)
232
233                 if time_to_autologout and not mk.done(): time_to_autologout = None
234                 if cur_user == '' and time_to_autologout: time_to_autologout = None
235                 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
236                         # start autologout
237                         time_to_autologout = time() + 15
238
239                 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
240                 if time_to_idle != None and cur_user != '': time_to_idle = None
241                 if time_to_idle is not None and time() > time_to_idle: idler.next()
242
243                 mk.update_display()
244
245                 e = v.next_event(0)
246                 if e == None:
247                         e = v.next_event(0.1)
248                         if e == None:
249                                 continue
250                 time_to_idle = None
251                 (event, params) = e
252                 print e
253                 if event == DOOR:
254                         if params == 0:
255                                 door_open_mode(v);
256                                 cur_user = ''
257                                 cur_pin = ''
258                                 mk.set_message(GREETING)
259                 elif event == SWITCH:
260                         # don't care right now.
261                         pass
262                 elif event == KEY:
263                         key = params
264                         # complicated key handling here:
265                         if len(cur_user) < 5:
266                                 if key == 11:
267                                         cur_user = ''
268                                         mk.set_message(GREETING)
269                                         continue
270                                 cur_user += chr(key + ord('0'))
271                                 mk.set_message('UID: '+cur_user)
272                                 if len(cur_user) == 5:
273                                         uid = int(cur_user)
274                                         if not has_good_pin(uid):
275                                                 #mk.set_messages(
276                                                         #[(center('INVALID'), False, 0.7),
277                                                          #(center('PIN'), False, 0.7),
278                                                          #(center('SETUP'), False, 1.0),
279                                                          #(GREETING, False, None)])
280                                                 mk.set_messages(
281                                                         [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
282                                                          (GREETING, False, None)])
283                                                 cur_user = ''
284                                                 cur_pin = ''
285                                                 continue
286                                         cur_pin = ''
287                                         mk.set_message('PIN: ')
288                                         continue
289                         elif len(cur_pin) < PIN_LENGTH:
290                                 if key == 11:
291                                         if cur_pin == '':
292                                                 cur_user = ''
293                                                 mk.set_message(GREETING)
294                                                 continue
295                                         cur_pin = ''
296                                         mk.set_message('PIN: ')
297                                         continue
298                                 cur_pin += chr(key + ord('0'))
299                                 mk.set_message('PIN: '+'X'*len(cur_pin))
300                                 if len(cur_pin) == PIN_LENGTH:
301                                         username = verify_user_pin(int(cur_user), int(cur_pin))
302                                         if username:
303                                                 v.beep(0, False)
304                                                 cur_selection = ''
305                                                 scroll_options(username, mk, True)
306                                                 continue
307                                         else:
308                                                 v.beep(40, False)
309                                                 mk.set_messages(
310                                                         [(center('BAD PIN'), False, 1.0),
311                                                          (center('SORRY'), False, 0.5),
312                                                          (GREETING, False, None)])
313                                                 cur_user = ''
314                                                 cur_pin = ''
315                                                 continue
316                         elif len(cur_selection) == 0:
317                                 if key == 11:
318                                         cur_pin = ''
319                                         cur_user = ''
320                                         cur_selection = ''
321                                         mk.set_messages(
322                                                 [(center('BYE!'), False, 1.5),
323                                                  (GREETING, False, None)])
324                                         continue
325                                 cur_selection += chr(key + ord('0'))
326                                 mk.set_message('SELECT: '+cur_selection)
327                                 time_to_autologout = None
328                         elif len(cur_selection) == 1:
329                                 if key == 11:
330                                         cur_selection = ''
331                                         time_to_autologout = None
332                                         scroll_options(username, mk)
333                                         continue
334                                 else:
335                                         cur_selection += chr(key + ord('0'))
336                                         #make_selection(cur_selection)
337                                         # XXX this should move somewhere else:
338                                         if cur_selection == '55':
339                                                 mk.set_message('OPENSESAME')
340                                                 ret = os.system('su - "%s" -c "dispense door"'%username)
341                                                 if ret == 0:
342                                                         mk.set_message(center('DOOR OPEN'))
343                                                 else:
344                                                         mk.set_message(center('BAD DOOR'))
345                                                 sleep(1)
346                                         elif cur_selection == '91':
347                                                 cookie(v)
348                                         elif cur_selection == '99':
349                                                 scroll_options(username, mk)
350                                                 cur_selection = ''
351                                                 continue
352                                         elif cur_selection[1] == '8':
353                                                 v.display('GOT COKE?')
354                                                 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
355                                         else:
356                                                 v.display('HERES A '+cur_selection)
357                                                 v.vend(cur_selection)
358                                         sleep(0.5)
359                                         v.display('THANK YOU')
360                                         sleep(0.5)
361                                         cur_selection = ''
362                                         time_to_autologout = time() + 8
363
364 # FIXME: a less ugly way of passing all these options would be nice
365 def connect_to_vend(options, DBServer, DBName, DBUser, DBPassword, ServiceName, ServicePassword):
366         # Open vending machine via LAT
367         if options.use_lat:
368                 latclient = LATClient(service = ServiceName, password = ServicePassword)
369                 rfh, wfh = latclient.get_fh()
370         else:
371                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
372                 import socket
373                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
374                 sock.connect((options.host, options.port))
375                 rfh = sock.makefile('r')
376                 wfh = sock.makefile('w')
377                 
378         return rfh, wfh
379
380 if __name__ == '__main__':
381         from ConfigParser import ConfigParser
382         from optparse import OptionParser
383
384         op = OptionParser(usage="%prog [OPTION]...")
385         op.add_option('-f', '--config-file', default='/etc/dispense/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
386         op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
387         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
388         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
389         options, args = op.parse_args()
390
391         if len(args) != 0:
392                 op.error('extra command line arguments: ' + ' '.join(args))
393
394         cp = ConfigParser()
395         cp.read(options.config_file)
396         DBServer = cp.get('Database', 'Server')
397         DBName = cp.get('Database', 'Name')
398         DBUser = cp.get('VendingMachine', 'DBUser')
399         DBPassword = cp.get('VendingMachine', 'DBPassword')
400
401         ServiceName = cp.get('VendingMachine', 'ServiceName')
402         ServicePassword = cp.get('VendingMachine', 'Password')
403
404         while True:
405                 rfh, wfh = connect_to_vend(options, DBServer, DBName, DBUser, DBPassword, ServiceName, ServicePassword)
406                 try:
407                         run_forever(rfh, wfh)
408                 except VendingException:
409                         print "Connection died, trying again..."
410

UCC git Repository :: git.ucc.asn.au