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 parser.add_option('-S',
581 '--shell',
582 dest='shell',
583 metavar='APPNAME',
584 help=msg)
585
586 msg = 'run web2py in interactive shell or bpython (if installed) with'
587 msg += ' specified appname (if app does not exist it will be created).'
588 msg += '\n Use combined with --shell'
589 parser.add_option('-B',
590 '--bpython',
591 action='store_true',
592 default=False,
593 dest='bpython',
594 help=msg)
595
596 msg = 'only use plain python shell; should be used with --shell option'
597 parser.add_option('-P',
598 '--plain',
599 action='store_true',
600 default=False,
601 dest='plain',
602 help=msg)
603
604 msg = 'auto import model files; default is False; should be used'
605 msg += ' with --shell option'
606 parser.add_option('-M',
607 '--import_models',
608 action='store_true',
609 default=False,
610 dest='import_models',
611 help=msg)
612
613 msg = 'run PYTHON_FILE in web2py environment;'
614 msg += ' should be used with --shell option'
615 parser.add_option('-R',
616 '--run',
617 dest='run',
618 metavar='PYTHON_FILE',
619 default='',
620 help=msg)
621
622 msg = 'run doctests in web2py environment; ' +\
623 'TEST_PATH like a/c/f (c,f optional)'
624 parser.add_option('-T',
625 '--test',
626 dest='test',
627 metavar='TEST_PATH',
628 default=None,
629 help=msg)
630
631 parser.add_option('-W',
632 '--winservice',
633 dest='winservice',
634 default='',
635 help='-W install|start|stop as Windows service')
636
637 msg = 'trigger a cron run manually; usually invoked from a system crontab'
638 parser.add_option('-C',
639 '--cron',
640 action='store_true',
641 dest='extcron',
642 default=False,
643 help=msg)
644
645 msg = 'triggers the use of softcron'
646 parser.add_option('--softcron',
647 action='store_true',
648 dest='softcron',
649 default=False,
650 help=msg)
651
652 parser.add_option('-N',
653 '--no-cron',
654 action='store_true',
655 dest='nocron',
656 default=False,
657 help='do not start cron automatically')
658
659 parser.add_option('-J',
660 '--cronjob',
661 action='store_true',
662 dest='cronjob',
663 default=False,
664 help='identify cron-initiated command')
665
666 parser.add_option('-L',
667 '--config',
668 dest='config',
669 default='',
670 help='config file')
671
672 parser.add_option('-F',
673 '--profiler',
674 dest='profiler_filename',
675 default=None,
676 help='profiler filename')
677
678 parser.add_option('-t',
679 '--taskbar',
680 action='store_true',
681 dest='taskbar',
682 default=False,
683 help='use web2py gui and run in taskbar (system tray)')
684
685 parser.add_option('',
686 '--nogui',
687 action='store_true',
688 default=False,
689 dest='nogui',
690 help='text-only, no GUI')
691
692 parser.add_option('-A',
693 '--args',
694 action='store',
695 dest='args',
696 default=None,
697 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')
698
699 parser.add_option('--no-banner',
700 action='store_true',
701 default=False,
702 dest='nobanner',
703 help='Do not print header banner')
704
705 msg = 'listen on multiple addresses: "ip:port:cert:key;ip2:port2:cert2:key2;..." (:cert:key optional; no spaces)'
706 parser.add_option('--interfaces',
707 action='store',
708 dest='interfaces',
709 default=None,
710 help=msg)
711
712 if '-A' in sys.argv: k = sys.argv.index('-A')
713 elif '--args' in sys.argv: k = sys.argv.index('--args')
714 else: k=len(sys.argv)
715 sys.argv, other_args = sys.argv[:k], sys.argv[k+1:]
716 (options, args) = parser.parse_args()
717 options.args = [options.run] + other_args
718 global_settings.cmd_options = options
719 global_settings.cmd_args = args
720
721 if options.quiet:
722 capture = cStringIO.StringIO()
723 sys.stdout = capture
724 logger.setLevel(logging.CRITICAL + 1)
725 else:
726 logger.setLevel(options.debuglevel)
727
728 if options.config[-3:] == '.py':
729 options.config = options.config[:-3]
730
731 if options.cronjob:
732 global_settings.cronjob = True
733 options.nocron = True
734 options.plain = True
735
736 options.folder = os.path.abspath(options.folder)
737
738
739
740
741 if isinstance(options.interfaces, str):
742 options.interfaces = [interface.split(':') for interface in options.interfaces.split(';')]
743 for interface in options.interfaces:
744 interface[1] = int(interface[1])
745 options.interfaces = [tuple(interface) for interface in options.interfaces]
746
747 if options.numthreads is not None and options.minthreads is None:
748 options.minthreads = options.numthreads
749
750 if not options.cronjob:
751
752 if not os.path.exists('applications/__init__.py'):
753 write_file('applications/__init__.py', '')
754
755 if not os.path.exists('welcome.w2p') or os.path.exists('NEWINSTALL'):
756 try:
757 w2p_pack('welcome.w2p','applications/welcome')
758 os.unlink('NEWINSTALL')
759 except:
760 msg = "New installation: unable to create welcome.w2p file"
761 sys.stderr.write(msg)
762
763 return (options, args)
764
765
767 """ Start server """
768
769
770
771 (options, args) = console()
772
773 if not options.nobanner:
774 print ProgramName
775 print ProgramAuthor
776 print ProgramVersion
777
778 from dal import drivers
779 if not options.nobanner:
780 print 'Database drivers available: %s' % ', '.join(drivers)
781
782
783
784 if options.config:
785 try:
786 options2 = __import__(options.config, {}, {}, '')
787 except Exception:
788 try:
789
790 options2 = __import__(options.config)
791 except Exception:
792 print 'Cannot import config file [%s]' % options.config
793 sys.exit(1)
794 for key in dir(options2):
795 if hasattr(options,key):
796 setattr(options,key,getattr(options2,key))
797
798
799 if hasattr(options,'test') and options.test:
800 test(options.test, verbose=options.verbose)
801 return
802
803
804 if options.shell:
805 if options.args!=None:
806 sys.argv[:] = options.args
807 run(options.shell, plain=options.plain, bpython=options.bpython,
808 import_models=options.import_models, startfile=options.run)
809 return
810
811
812
813
814
815 if options.extcron:
816 print 'Starting extcron...'
817 global_settings.web2py_crontype = 'external'
818 extcron = newcron.extcron(options.folder)
819 extcron.start()
820 extcron.join()
821 return
822 elif cron and not options.nocron and options.softcron:
823 print 'Using softcron (but this is not very efficient)'
824 global_settings.web2py_crontype = 'soft'
825 elif cron and not options.nocron:
826 print 'Starting hardcron...'
827 global_settings.web2py_crontype = 'hard'
828 newcron.hardcron(options.folder).start()
829
830
831 if options.winservice:
832 if os.name == 'nt':
833 web2py_windows_service_handler(['', options.winservice],
834 options.config)
835 else:
836 print 'Error: Windows services not supported on this platform'
837 sys.exit(1)
838 return
839
840
841
842
843 try:
844 options.taskbar
845 except:
846 options.taskbar = False
847
848 if options.taskbar and os.name != 'nt':
849 print 'Error: taskbar not supported on this platform'
850 sys.exit(1)
851
852 root = None
853
854 if not options.nogui:
855 try:
856 import Tkinter
857 havetk = True
858 except ImportError:
859 logger.warn('GUI not available because Tk library is not installed')
860 havetk = False
861
862 if options.password == '<ask>' and havetk or options.taskbar and havetk:
863 try:
864 root = Tkinter.Tk()
865 except:
866 pass
867
868 if root:
869 root.focus_force()
870 if not options.quiet:
871 presentation(root)
872 master = web2pyDialog(root, options)
873 signal.signal(signal.SIGTERM, lambda a, b: master.quit())
874
875 try:
876 root.mainloop()
877 except:
878 master.quit()
879
880 sys.exit()
881
882
883
884 if not root and options.password == '<ask>':
885 options.password = raw_input('choose a password:')
886
887 if not options.password and not options.nobanner:
888 print 'no password, no admin interface'
889
890
891
892 (ip, port) = (options.ip, int(options.port))
893
894 if not options.nobanner:
895 print 'please visit:'
896 print '\thttp://%s:%s' % (ip, port)
897 print 'use "kill -SIGTERM %i" to shutdown the web2py server' % os.getpid()
898
899 server = main.HttpServer(ip=ip,
900 port=port,
901 password=options.password,
902 pid_filename=options.pid_filename,
903 log_filename=options.log_filename,
904 profiler_filename=options.profiler_filename,
905 ssl_certificate=options.ssl_certificate,
906 ssl_private_key=options.ssl_private_key,
907 min_threads=options.minthreads,
908 max_threads=options.maxthreads,
909 server_name=options.server_name,
910 request_queue_size=options.request_queue_size,
911 timeout=options.timeout,
912 shutdown_timeout=options.shutdown_timeout,
913 path=options.folder,
914 interfaces=options.interfaces)
915
916 try:
917 server.start()
918 except KeyboardInterrupt:
919 server.stop()
920 logging.shutdown()
921