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