1
2
3
4 """
5 This file is part of the web2py Web Framework
6 Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
7 License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
8
9 The widget is called from web2py.
10 """
11
12 import sys
13 import cStringIO
14 import time
15 import thread
16 import re
17 import os
18 import socket
19 import signal
20 import math
21 import logging
22
23 import newcron
24 import main
25
26 from fileutils import w2p_pack
27 from shell import run, test
28 from settings import global_settings
29
30 try:
31 import Tkinter, tkMessageBox
32 import contrib.taskbar_widget
33 from winservice import web2py_windows_service_handler
34 except:
35 pass
36
37
38 try:
39 BaseException
40 except NameError:
41 BaseException = Exception
42
43 ProgramName = 'web2py Web Framework'
44 ProgramAuthor = 'Created by Massimo Di Pierro, Copyright 2007-2011'
45 versioninfo = open('VERSION', 'r')
46 ProgramVersion = versioninfo.read().strip()
47 versioninfo.close()
48
49 ProgramInfo = '''%s
50 %s
51 %s''' % (ProgramName, ProgramAuthor, ProgramVersion)
52
53 if not sys.version[:3] in ['2.4', '2.5', '2.6', '2.7']:
54 msg = 'Warning: web2py requires Python 2.4, 2.5 (recommended), 2.6 or 2.7 but you are running:\n%s'
55 msg = msg % sys.version
56 sys.stderr.write(msg)
57
58 logger = logging.getLogger("web2py")
59
61 """ """
62
64 """ """
65
66 self.buffer = cStringIO.StringIO()
67
69 """ """
70
71 sys.__stdout__.write(data)
72 if hasattr(self, 'callback'):
73 self.callback(data)
74 else:
75 self.buffer.write(data)
76
77
79 """ Try to start the default browser """
80
81 try:
82 import webbrowser
83 webbrowser.open(url)
84 except:
85 print 'warning: unable to detect your browser'
86
87
89 """ Starts the default browser """
90 print 'please visit:'
91 print '\thttp://%s:%s' % (ip, port)
92 print 'starting browser...'
93 try_start_browser('http://%s:%s' % (ip, port))
94
95
97 """ Draw the splash screen """
98
99 root.withdraw()
100
101 dx = root.winfo_screenwidth()
102 dy = root.winfo_screenheight()
103
104 dialog = Tkinter.Toplevel(root, bg='white')
105 dialog.geometry('%ix%i+%i+%i' % (500, 300, dx / 2 - 200, dy / 2 - 150))
106
107 dialog.overrideredirect(1)
108 dialog.focus_force()
109
110 canvas = Tkinter.Canvas(dialog,
111 background='white',
112 width=500,
113 height=300)
114 canvas.pack()
115 root.update()
116
117 img = Tkinter.PhotoImage(file='splashlogo.gif')
118 pnl = Tkinter.Label(canvas, image=img, background='white', bd=0)
119 pnl.pack(side='top', fill='both', expand='yes')
120
121 pnl.image=img
122
123 def add_label(text='Change Me', font_size=12, foreground='#195866', height=1):
124 return Tkinter.Label(
125 master=canvas,
126 width=250,
127 height=height,
128 text=text,
129 font=('Helvetica', font_size),
130 anchor=Tkinter.CENTER,
131 foreground=foreground,
132 background='white'
133 )
134
135 add_label('Welcome to...').pack(side='top')
136 add_label(ProgramName, 18, '#FF5C1F', 2).pack()
137 add_label(ProgramAuthor).pack()
138 add_label(ProgramVersion).pack()
139
140 root.update()
141 time.sleep(5)
142 dialog.destroy()
143 return
144
145
147 """ Main window dialog """
148
150 """ web2pyDialog constructor """
151
152 root.title('web2py server')
153 self.root = Tkinter.Toplevel(root)
154 self.options = options
155 self.menu = Tkinter.Menu(self.root)
156 servermenu = Tkinter.Menu(self.menu, tearoff=0)
157 httplog = os.path.join(self.options.folder, 'httpserver.log')
158
159
160 item = lambda: try_start_browser(httplog)
161 servermenu.add_command(label='View httpserver.log',
162 command=item)
163
164 servermenu.add_command(label='Quit (pid:%i)' % os.getpid(),
165 command=self.quit)
166
167 self.menu.add_cascade(label='Server', menu=servermenu)
168
169 self.pagesmenu = Tkinter.Menu(self.menu, tearoff=0)
170 self.menu.add_cascade(label='Pages', menu=self.pagesmenu)
171
172 helpmenu = Tkinter.Menu(self.menu, tearoff=0)
173
174
175 item = lambda: try_start_browser('http://www.web2py.com')
176 helpmenu.add_command(label='Home Page',
177 command=item)
178
179
180 item = lambda: tkMessageBox.showinfo('About web2py', ProgramInfo)
181 helpmenu.add_command(label='About',
182 command=item)
183
184 self.menu.add_cascade(label='Info', menu=helpmenu)
185
186 self.root.config(menu=self.menu)
187
188 if options.taskbar:
189 self.root.protocol('WM_DELETE_WINDOW',
190 lambda: self.quit(True))
191 else:
192 self.root.protocol('WM_DELETE_WINDOW', self.quit)
193
194 sticky = Tkinter.NW
195
196
197 Tkinter.Label(self.root,
198 text='Server IP:',
199 justify=Tkinter.LEFT).grid(row=0,
200 column=0,
201 sticky=sticky)
202 self.ip = Tkinter.Entry(self.root)
203 self.ip.insert(Tkinter.END, self.options.ip)
204 self.ip.grid(row=0, column=1, sticky=sticky)
205
206
207 Tkinter.Label(self.root,
208 text='Server Port:',
209 justify=Tkinter.LEFT).grid(row=1,
210 column=0,
211 sticky=sticky)
212
213 self.port_number = Tkinter.Entry(self.root)
214 self.port_number.insert(Tkinter.END, self.options.port)
215 self.port_number.grid(row=1, column=1, sticky=sticky)
216
217
218 Tkinter.Label(self.root,
219 text='Choose Password:',
220 justify=Tkinter.LEFT).grid(row=2,
221 column=0,
222 sticky=sticky)
223
224 self.password = Tkinter.Entry(self.root, show='*')
225 self.password.bind('<Return>', lambda e: self.start())
226 self.password.focus_force()
227 self.password.grid(row=2, column=1, sticky=sticky)
228
229
230 self.canvas = Tkinter.Canvas(self.root,
231 width=300,
232 height=100,
233 bg='black')
234 self.canvas.grid(row=3, column=0, columnspan=2)
235 self.canvas.after(1000, self.update_canvas)
236
237
238 frame = Tkinter.Frame(self.root)
239 frame.grid(row=4, column=0, columnspan=2)
240
241
242 self.button_start = Tkinter.Button(frame,
243 text='start server',
244 command=self.start)
245
246 self.button_start.grid(row=0, column=0)
247
248
249 self.button_stop = Tkinter.Button(frame,
250 text='stop server',
251 command=self.stop)
252
253 self.button_stop.grid(row=0, column=1)
254 self.button_stop.configure(state='disabled')
255
256 if options.taskbar:
257 self.tb = contrib.taskbar_widget.TaskBarIcon()
258 self.checkTaskBar()
259
260 if options.password != '<ask>':
261 self.password.insert(0, options.password)
262 self.start()
263 self.root.withdraw()
264 else:
265 self.tb = None
266
268 """ Check taskbar status """
269
270 if self.tb.status:
271 if self.tb.status[0] == self.tb.EnumStatus.QUIT:
272 self.quit()
273 elif self.tb.status[0] == self.tb.EnumStatus.TOGGLE:
274 if self.root.state() == 'withdrawn':
275 self.root.deiconify()
276 else:
277 self.root.withdraw()
278 elif self.tb.status[0] == self.tb.EnumStatus.STOP:
279 self.stop()
280 elif self.tb.status[0] == self.tb.EnumStatus.START:
281 self.start()
282 elif self.tb.status[0] == self.tb.EnumStatus.RESTART:
283 self.stop()
284 self.start()
285 del self.tb.status[0]
286
287 self.root.after(1000, self.checkTaskBar)
288
290 """ Update app text """
291
292 try:
293 self.text.configure(state='normal')
294 self.text.insert('end', text)
295 self.text.configure(state='disabled')
296 except:
297 pass
298
299 - def connect_pages(self):
300 """ Connect pages """
301
302 for arq in os.listdir('applications/'):
303 if os.path.exists('applications/%s/__init__.py' % arq):
304 url = self.url + '/' + arq
305 start_browser = lambda u = url: try_start_browser(u)
306 self.pagesmenu.add_command(label=url,
307 command=start_browser)
308
309 - def quit(self, justHide=False):
310 """ Finish the program execution """
311
312 if justHide:
313 self.root.withdraw()
314 else:
315 try:
316 self.server.stop()
317 except:
318 pass
319
320 try:
321 self.tb.Destroy()
322 except:
323 pass
324
325 self.root.destroy()
326 sys.exit()
327
328 - def error(self, message):
329 """ Show error message """
330
331 tkMessageBox.showerror('web2py start server', message)
332
334 """ Start web2py server """
335
336 password = self.password.get()
337
338 if not password:
339 self.error('no password, no web admin interface')
340
341 ip = self.ip.get()
342
343 regexp = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
344 if ip and not re.compile(regexp).match(ip):
345 return self.error('invalid host ip address')
346
347 try:
348 port = int(self.port_number.get())
349 except:
350 return self.error('invalid port number')
351
352 self.url = 'http://%s:%s' % (ip, port)
353 self.connect_pages()
354 self.button_start.configure(state='disabled')
355
356 try:
357 options = self.options
358 req_queue_size = options.request_queue_size
359 self.server = main.HttpServer(
360 ip,
361 port,
362 password,
363 pid_filename=options.pid_filename,
364 log_filename=options.log_filename,
365 profiler_filename=options.profiler_filename,
366 ssl_certificate=options.ssl_certificate,
367 ssl_private_key=options.ssl_private_key,
368 min_threads=options.minthreads,
369 max_threads=options.maxthreads,
370 server_name=options.server_name,
371 request_queue_size=req_queue_size,
372 timeout=options.timeout,
373 shutdown_timeout=options.shutdown_timeout,
374 path=options.folder,
375 interfaces=options.interfaces)
376
377 thread.start_new_thread(self.server.start, ())
378 except Exception, e:
379 self.button_start.configure(state='normal')
380 return self.error(str(e))
381
382 self.button_stop.configure(state='normal')
383
384 if not options.taskbar:
385 thread.start_new_thread(start_browser, (ip, port))
386
387 self.password.configure(state='readonly')
388 self.ip.configure(state='readonly')
389 self.port_number.configure(state='readonly')
390
391 if self.tb:
392 self.tb.SetServerRunning()
393
395 """ Stop web2py server """
396
397 self.button_start.configure(state='normal')
398 self.button_stop.configure(state='disabled')
399 self.password.configure(state='normal')
400 self.ip.configure(state='normal')
401 self.port_number.configure(state='normal')
402 self.server.stop()
403
404 if self.tb:
405 self.tb.SetServerStopped()
406
408 """ Update canvas """
409
410 try:
411 t1 = os.path.getsize('httpserver.log')
412 except:
413 self.canvas.after(1000, self.update_canvas)
414 return
415
416 try:
417 fp = open('httpserver.log', 'r')
418 fp.seek(self.t0)
419 data = fp.read(t1 - self.t0)
420 fp.close()
421 value = self.p0[1:] + [10 + 90.0 / math.sqrt(1 + data.count('\n'))]
422 self.p0 = value
423
424 for i in xrange(len(self.p0) - 1):
425 c = self.canvas.coords(self.q0[i])
426 self.canvas.coords(self.q0[i],
427 (c[0],
428 self.p0[i],
429 c[2],
430 self.p0[i + 1]))
431 self.t0 = t1
432 except BaseException:
433 self.t0 = time.time()
434 self.t0 = t1
435 self.p0 = [100] * 300
436 self.q0 = [self.canvas.create_line(i, 100, i + 1, 100,
437 fill='green') for i in xrange(len(self.p0) - 1)]
438
439 self.canvas.after(1000, self.update_canvas)
440
441
443 """ Defines the behavior of the console web2py execution """
444 import optparse
445 import textwrap
446
447 usage = "python web2py.py"
448
449 description = """\
450 web2py Web Framework startup script.
451 ATTENTION: unless a password is specified (-a 'passwd') web2py will
452 attempt to run a GUI. In this case command line options are ignored."""
453
454 description = textwrap.dedent(description)
455
456 parser = optparse.OptionParser(usage, None, optparse.Option, ProgramVersion)
457
458 parser.description = description
459
460 parser.add_option('-i',
461 '--ip',
462 default='127.0.0.1',
463 dest='ip',
464 help='ip address of the server (127.0.0.1)')
465
466 parser.add_option('-p',
467 '--port',
468 default='8000',
469 dest='port',
470 type='int',
471 help='port of server (8000)')
472
473 msg = 'password to be used for administration'
474 msg += ' (use -a "<recycle>" to reuse the last password))'
475 parser.add_option('-a',
476 '--password',
477 default='<ask>',
478 dest='password',
479 help=msg)
480
481 parser.add_option('-c',
482 '--ssl_certificate',
483 default='',
484 dest='ssl_certificate',
485 help='file that contains ssl certificate')
486
487 parser.add_option('-k',
488 '--ssl_private_key',
489 default='',
490 dest='ssl_private_key',
491 help='file that contains ssl private key')
492
493 parser.add_option('-d',
494 '--pid_filename',
495 default='httpserver.pid',
496 dest='pid_filename',
497 help='file to store the pid of the server')
498
499 parser.add_option('-l',
500 '--log_filename',
501 default='httpserver.log',
502 dest='log_filename',
503 help='file to log connections')
504
505 parser.add_option('-n',
506 '--numthreads',
507 default=None,
508 type='int',
509 dest='numthreads',
510 help='number of threads (deprecated)')
511
512 parser.add_option('--minthreads',
513 default=None,
514 type='int',
515 dest='minthreads',
516 help='minimum number of server threads')
517
518 parser.add_option('--maxthreads',
519 default=None,
520 type='int',
521 dest='maxthreads',
522 help='maximum number of server threads')
523
524 parser.add_option('-s',
525 '--server_name',
526 default=socket.gethostname(),
527 dest='server_name',
528 help='server name for the web server')
529
530 msg = 'max number of queued requests when server unavailable'
531 parser.add_option('-q',
532 '--request_queue_size',
533 default='5',
534 type='int',
535 dest='request_queue_size',
536 help=msg)
537
538 parser.add_option('-o',
539 '--timeout',
540 default='10',
541 type='int',
542 dest='timeout',
543 help='timeout for individual request (10 seconds)')
544
545 parser.add_option('-z',
546 '--shutdown_timeout',
547 default='5',
548 type='int',
549 dest='shutdown_timeout',
550 help='timeout on shutdown of server (5 seconds)')
551 parser.add_option('-f',
552 '--folder',
553 default=os.getcwd(),
554 dest='folder',
555 help='folder from which to run web2py')
556
557 parser.add_option('-v',
558 '--verbose',
559 action='store_true',
560 dest='verbose',
561 default=False,
562 help='increase --test verbosity')
563
564 parser.add_option('-Q',
565 '--quiet',
566 action='store_true',
567 dest='quiet',
568 default=False,
569 help='disable all output')
570
571 msg = 'set debug output level (0-100, 0 means all, 100 means none;'
572 msg += ' default is 30)'
573 parser.add_option('-D',
574 '--debug',
575 dest='debuglevel',
576 default=30,
577 type='int',
578 help=msg)
579
580 msg = 'run web2py in interactive shell or IPython (if installed) with'
581 msg += ' specified appname (if app does not exist it will be created).'
582 parser.add_option('-S',
583 '--shell',
584 dest='shell',
585 metavar='APPNAME',
586 help=msg)
587
588 msg = 'run web2py in interactive shell or bpython (if installed) with'
589 msg += ' specified appname (if app does not exist it will be created).'
590 msg += '\n Use combined with --shell'
591 parser.add_option('-B',
592 '--bpython',
593 action='store_true',
594 default=False,
595 dest='bpython',
596 help=msg)
597
598 msg = 'only use plain python shell; should be used with --shell option'
599 parser.add_option('-P',
600 '--plain',
601 action='store_true',
602 default=False,
603 dest='plain',
604 help=msg)
605
606 msg = 'auto import model files; default is False; should be used'
607 msg += ' with --shell option'
608 parser.add_option('-M',
609 '--import_models',
610 action='store_true',
611 default=False,
612 dest='import_models',
613 help=msg)
614
615 msg = 'run PYTHON_FILE in web2py environment;'
616 msg += ' should be used with --shell option'
617 parser.add_option('-R',
618 '--run',
619 dest='run',
620 metavar='PYTHON_FILE',
621 default='',
622 help=msg)
623
624 msg = 'run doctests in web2py environment; ' +\
625 'TEST_PATH like a/c/f (c,f optional)'
626 parser.add_option('-T',
627 '--test',
628 dest='test',
629 metavar='TEST_PATH',
630 default=None,
631 help=msg)
632
633 parser.add_option('-W',
634 '--winservice',
635 dest='winservice',
636 default='',
637 help='-W install|start|stop as Windows service')
638
639 msg = 'trigger a cron run manually; usually invoked from a system crontab'
640 parser.add_option('-C',
641 '--cron',
642 action='store_true',
643 dest='extcron',
644 default=False,
645 help=msg)
646
647 msg = 'triggers the use of softcron'
648 parser.add_option('--softcron',
649 action='store_true',
650 dest='softcron',
651 default=False,
652 help=msg)
653
654 parser.add_option('-N',
655 '--no-cron',
656 action='store_true',
657 dest='nocron',
658 default=False,
659 help='do not start cron automatically')
660
661 parser.add_option('-J',
662 '--cronjob',
663 action='store_true',
664 dest='cronjob',
665 default=False,
666 help='identify cron-initiated command')
667
668 parser.add_option('-L',
669 '--config',
670 dest='config',
671 default='',
672 help='config file')
673
674 parser.add_option('-F',
675 '--profiler',
676 dest='profiler_filename',
677 default=None,
678 help='profiler filename')
679
680 parser.add_option('-t',
681 '--taskbar',
682 action='store_true',
683 dest='taskbar',
684 default=False,
685 help='use web2py gui and run in taskbar (system tray)')
686
687 parser.add_option('',
688 '--nogui',
689 action='store_true',
690 default=False,
691 dest='nogui',
692 help='text-only, no GUI')
693
694 parser.add_option('-A',
695 '--args',
696 action='store',
697 dest='args',
698 default=None,
699 help='should be followed by a list of arguments to be passed to script, to be used with -S, -A must be the last option')
700
701 parser.add_option('--no-banner',
702 action='store_true',
703 default=False,
704 dest='nobanner',
705 help='Do not print header banner')
706
707 msg = 'listen on multiple addresses: "ip:port:cert:key;ip2:port2:cert2:key2;..." (:cert:key optional; no spaces)'
708 parser.add_option('--interfaces',
709 action='store',
710 dest='interfaces',
711 default=None,
712 help=msg)
713
714 if '-A' in sys.argv: k = sys.argv.index('-A')
715 elif '--args' in sys.argv: k = sys.argv.index('--args')
716 else: k=len(sys.argv)
717 sys.argv, other_args = sys.argv[:k], sys.argv[k+1:]
718 (options, args) = parser.parse_args()
719 options.args = [options.run] + other_args
720 global_settings.cmd_options = options
721 global_settings.cmd_args = args
722
723 if options.quiet:
724 capture = cStringIO.StringIO()
725 sys.stdout = capture
726 logger.setLevel(logging.CRITICAL + 1)
727 else:
728 logger.setLevel(options.debuglevel)
729
730 if options.config[-3:] == '.py':
731 options.config = options.config[:-3]
732
733 if options.cronjob:
734 global_settings.cronjob = True
735 options.nocron = True
736 options.plain = True
737
738 options.folder = os.path.abspath(options.folder)
739
740
741
742
743 if isinstance(options.interfaces, str):
744 options.interfaces = [interface.split(':') for interface in options.interfaces.split(';')]
745 for interface in options.interfaces:
746 interface[1] = int(interface[1])
747 options.interfaces = [tuple(interface) for interface in options.interfaces]
748
749 if options.numthreads is not None and options.minthreads is None:
750 options.minthreads = options.numthreads
751
752 if not options.cronjob:
753
754 if not os.path.exists('applications/__init__.py'):
755 fp = open('applications/__init__.py', 'w')
756 fp.write('')
757 fp.close()
758
759 if not os.path.exists('welcome.w2p') or os.path.exists('NEWINSTALL'):
760 try:
761 w2p_pack('welcome.w2p','applications/welcome')
762 os.unlink('NEWINSTALL')
763 except:
764 msg = "New installation: unable to create welcome.w2p file"
765 sys.stderr.write(msg)
766
767 return (options, args)
768
769
771 """ Start server """
772
773
774
775 (options, args) = console()
776
777 if not options.nobanner:
778 print ProgramName
779 print ProgramAuthor
780 print ProgramVersion
781
782 from dal import drivers
783 if not options.nobanner:
784 print 'Database drivers available: %s' % ', '.join(drivers)
785
786
787
788 if options.config:
789 try:
790 options2 = __import__(options.config, {}, {}, '')
791 except Exception:
792 try:
793
794 options2 = __import__(options.config)
795 except Exception:
796 print 'Cannot import config file [%s]' % options.config
797 sys.exit(1)
798 for key in dir(options2):
799 if hasattr(options,key):
800 setattr(options,key,getattr(options2,key))
801
802
803 if hasattr(options,'test') and options.test:
804 test(options.test, verbose=options.verbose)
805 return
806
807
808 if options.shell:
809 if options.args!=None:
810 sys.argv[:] = options.args
811 run(options.shell, plain=options.plain, bpython=options.bpython,
812 import_models=options.import_models, startfile=options.run)
813 return
814
815
816
817
818
819 if options.extcron:
820 print 'Starting extcron...'
821 global_settings.web2py_crontype = 'external'
822 extcron = newcron.extcron(options.folder)
823 extcron.start()
824 extcron.join()
825 return
826 elif cron and not options.nocron and options.softcron:
827 print 'Using softcron (but this is not very efficient)'
828 global_settings.web2py_crontype = 'soft'
829 elif cron and not options.nocron:
830 print 'Starting hardcron...'
831 global_settings.web2py_crontype = 'hard'
832 newcron.hardcron(options.folder).start()
833
834
835 if options.winservice:
836 if os.name == 'nt':
837 web2py_windows_service_handler(['', options.winservice],
838 options.config)
839 else:
840 print 'Error: Windows services not supported on this platform'
841 sys.exit(1)
842 return
843
844
845
846
847 try:
848 options.taskbar
849 except:
850 options.taskbar = False
851
852 if options.taskbar and os.name != 'nt':
853 print 'Error: taskbar not supported on this platform'
854 sys.exit(1)
855
856 root = None
857
858 if not options.nogui:
859 try:
860 import Tkinter
861 havetk = True
862 except ImportError:
863 logger.warn('GUI not available because Tk library is not installed')
864 havetk = False
865
866 if options.password == '<ask>' and havetk or options.taskbar and havetk:
867 try:
868 root = Tkinter.Tk()
869 except:
870 pass
871
872 if root:
873 root.focus_force()
874 if not options.quiet:
875 presentation(root)
876 master = web2pyDialog(root, options)
877 signal.signal(signal.SIGTERM, lambda a, b: master.quit())
878
879 try:
880 root.mainloop()
881 except:
882 master.quit()
883
884 sys.exit()
885
886
887
888 if not root and options.password == '<ask>':
889 options.password = raw_input('choose a password:')
890
891 if not options.password and not options.nobanner:
892 print 'no password, no admin interface'
893
894
895
896 (ip, port) = (options.ip, int(options.port))
897
898 if not options.nobanner:
899 print 'please visit:'
900 print '\thttp://%s:%s' % (ip, port)
901 print 'use "kill -SIGTERM %i" to shutdown the web2py server' % os.getpid()
902
903 server = main.HttpServer(ip=ip,
904 port=port,
905 password=options.password,
906 pid_filename=options.pid_filename,
907 log_filename=options.log_filename,
908 profiler_filename=options.profiler_filename,
909 ssl_certificate=options.ssl_certificate,
910 ssl_private_key=options.ssl_private_key,
911 min_threads=options.minthreads,
912 max_threads=options.maxthreads,
913 server_name=options.server_name,
914 request_queue_size=options.request_queue_size,
915 timeout=options.timeout,
916 shutdown_timeout=options.shutdown_timeout,
917 path=options.folder,
918 interfaces=options.interfaces)
919
920 try:
921 server.start()
922 except KeyboardInterrupt:
923 server.stop()
924 logging.shutdown()
925