1 | # -*- coding: utf-8 -*- |
---|
2 | # vim: set ts=4 sw=4 et ai: |
---|
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 | GUI widget and services start function |
---|
10 | -------------------------------------- |
---|
11 | """ |
---|
12 | |
---|
13 | from __future__ import print_function |
---|
14 | |
---|
15 | import time |
---|
16 | import sys |
---|
17 | import os |
---|
18 | from collections import OrderedDict |
---|
19 | import socket |
---|
20 | import threading |
---|
21 | import math |
---|
22 | import logging |
---|
23 | import signal |
---|
24 | import getpass |
---|
25 | |
---|
26 | from gluon.fileutils import read_file, create_welcome_w2p |
---|
27 | from gluon.shell import die, run, test |
---|
28 | from gluon._compat import PY2, xrange |
---|
29 | from gluon.utils import (getipaddrinfo, is_loopback_ip_address, |
---|
30 | is_valid_ip_address) |
---|
31 | from gluon.console import is_appdir, console |
---|
32 | from gluon import newcron |
---|
33 | from gluon import main |
---|
34 | from gluon.settings import global_settings |
---|
35 | |
---|
36 | |
---|
37 | ProgramName = 'web2py Web Framework' |
---|
38 | ProgramAuthor = 'Created by Massimo Di Pierro, Copyright 2007-' + str( |
---|
39 | time.localtime().tm_year) |
---|
40 | ProgramVersion = read_file('VERSION').rstrip() |
---|
41 | |
---|
42 | if sys.version_info < (2, 7) or (3, 0) < sys.version_info < (3, 5): |
---|
43 | from platform import python_version |
---|
44 | sys.stderr.write("Warning: web2py requires at least Python 2.7/3.5" |
---|
45 | " but you are running %s\n" % python_version()) |
---|
46 | |
---|
47 | |
---|
48 | def run_system_tests(options): |
---|
49 | """ |
---|
50 | Runs unittests for gluon.tests |
---|
51 | """ |
---|
52 | # see "python -m unittest -h" for unittest options help |
---|
53 | # NOTE: someone might be interested either in using the |
---|
54 | # -f (--failfast) option to stop testing on first failure, or |
---|
55 | # in customizing the test selection, for example to run only |
---|
56 | # 'gluon.tests.<module>', 'gluon.tests.<module>.<class>' (this |
---|
57 | # could be shortened as 'gluon.tests.<class>'), or even |
---|
58 | # 'gluon.tests.<module>.<class>.<method>' (or |
---|
59 | # the shorter 'gluon.tests.<class>.<method>') |
---|
60 | call_args = ['-m', 'unittest', '-c', 'gluon.tests'] |
---|
61 | if options.verbose: |
---|
62 | call_args.insert(-1, '-v') |
---|
63 | if options.with_coverage: |
---|
64 | try: |
---|
65 | import coverage |
---|
66 | except: |
---|
67 | die('Coverage not installed') |
---|
68 | if not PY2: |
---|
69 | sys.stderr.write('Experimental ') |
---|
70 | sys.stderr.write("Python %s\n" % sys.version) |
---|
71 | if options.with_coverage: |
---|
72 | coverage_exec = 'coverage2' if PY2 else 'coverage3' |
---|
73 | coverage_config_file = os.path.join('gluon', 'tests', 'coverage.ini') |
---|
74 | coverage_config = os.environ.setdefault("COVERAGE_PROCESS_START", |
---|
75 | coverage_config_file) |
---|
76 | run_args = [coverage_exec, 'run', '--rcfile=%s' % coverage_config] |
---|
77 | # replace the current process |
---|
78 | os.execvpe(run_args[0], run_args + call_args, os.environ) |
---|
79 | else: |
---|
80 | run_args = [sys.executable] |
---|
81 | # replace the current process |
---|
82 | os.execv(run_args[0], run_args + call_args) |
---|
83 | |
---|
84 | |
---|
85 | def get_url(host, path='/', proto='http', port=80): |
---|
86 | if ':' in host: |
---|
87 | host = '[%s]' % host |
---|
88 | elif host == '0.0.0.0': |
---|
89 | host = '127.0.0.1' |
---|
90 | if path.startswith('/'): |
---|
91 | path = path[1:] |
---|
92 | if proto.endswith(':'): |
---|
93 | proto = proto[:-1] |
---|
94 | if not port or port == 80: |
---|
95 | port = '' |
---|
96 | else: |
---|
97 | port = ':%s' % port |
---|
98 | return '%s://%s%s/%s' % (proto, host, port, path) |
---|
99 | |
---|
100 | |
---|
101 | def start_browser(url, startup=False): |
---|
102 | if startup: |
---|
103 | print('please visit:') |
---|
104 | print('\t' + url) |
---|
105 | print('starting browser...') |
---|
106 | try: |
---|
107 | import webbrowser |
---|
108 | webbrowser.open(url) |
---|
109 | except: |
---|
110 | print('warning: unable to detect your browser') |
---|
111 | |
---|
112 | |
---|
113 | class web2pyDialog(object): |
---|
114 | """ Main window dialog """ |
---|
115 | |
---|
116 | def __init__(self, root, options): |
---|
117 | """ web2pyDialog constructor """ |
---|
118 | |
---|
119 | if PY2: |
---|
120 | import Tkinter as tkinter |
---|
121 | import tkMessageBox as messagebox |
---|
122 | else: |
---|
123 | import tkinter |
---|
124 | from tkinter import messagebox |
---|
125 | |
---|
126 | root.withdraw() |
---|
127 | |
---|
128 | bg_color = 'white' |
---|
129 | self.root = tkinter.Toplevel(root, bg=bg_color) |
---|
130 | self.root.resizable(0, 0) |
---|
131 | self.root.title(ProgramName) |
---|
132 | |
---|
133 | self.options = options |
---|
134 | self.scheduler_processes_lock = threading.RLock() |
---|
135 | self.scheduler_processes = OrderedDict() |
---|
136 | |
---|
137 | iconphoto = os.path.join('extras', 'icons', 'web2py.gif') |
---|
138 | if os.path.exists(iconphoto): |
---|
139 | img = tkinter.PhotoImage(file=iconphoto) |
---|
140 | self.root.tk.call('wm', 'iconphoto', self.root._w, img) |
---|
141 | |
---|
142 | # Building the Menu |
---|
143 | self.menu = tkinter.Menu(self.root) |
---|
144 | servermenu = tkinter.Menu(self.menu, tearoff=0) |
---|
145 | |
---|
146 | httplog = os.path.join(options.folder, options.log_filename) |
---|
147 | item = lambda: start_browser(httplog) |
---|
148 | servermenu.add_command(label='View httpserver.log', |
---|
149 | command=item) |
---|
150 | |
---|
151 | servermenu.add_command(label='Quit (pid:%i)' % os.getpid(), |
---|
152 | command=self.quit) |
---|
153 | |
---|
154 | self.menu.add_cascade(label='Server', menu=servermenu) |
---|
155 | |
---|
156 | self.pagesmenu = tkinter.Menu(self.menu, tearoff=0) |
---|
157 | self.menu.add_cascade(label='Pages', menu=self.pagesmenu) |
---|
158 | |
---|
159 | self.schedmenu = tkinter.Menu(self.menu, tearoff=0) |
---|
160 | self.menu.add_cascade(label='Scheduler', menu=self.schedmenu) |
---|
161 | # register and start schedulers |
---|
162 | self.update_schedulers(start=True) |
---|
163 | |
---|
164 | helpmenu = tkinter.Menu(self.menu, tearoff=0) |
---|
165 | |
---|
166 | # Home Page |
---|
167 | item = lambda: start_browser('http://www.web2py.com/') |
---|
168 | helpmenu.add_command(label='Home Page', |
---|
169 | command=item) |
---|
170 | |
---|
171 | # About |
---|
172 | ProgramInfo = """%s |
---|
173 | %s |
---|
174 | %s""" % (ProgramName, ProgramAuthor, ProgramVersion) |
---|
175 | item = lambda: messagebox.showinfo('About web2py', ProgramInfo) |
---|
176 | helpmenu.add_command(label='About', |
---|
177 | command=item) |
---|
178 | |
---|
179 | self.menu.add_cascade(label='Info', menu=helpmenu) |
---|
180 | |
---|
181 | self.root.config(menu=self.menu) |
---|
182 | |
---|
183 | if options.taskbar: |
---|
184 | self.root.protocol('WM_DELETE_WINDOW', |
---|
185 | lambda: self.quit(True)) |
---|
186 | else: |
---|
187 | self.root.protocol('WM_DELETE_WINDOW', self.quit) |
---|
188 | |
---|
189 | sticky = tkinter.NW |
---|
190 | |
---|
191 | # Prepare the logo area |
---|
192 | self.logoarea = tkinter.Canvas(self.root, |
---|
193 | background=bg_color, |
---|
194 | width=300, |
---|
195 | height=300) |
---|
196 | self.logoarea.grid(row=0, column=0, columnspan=4, sticky=sticky) |
---|
197 | self.logoarea.after(1000, self.update_canvas) |
---|
198 | |
---|
199 | logo = os.path.join('extras', 'icons', 'splashlogo.gif') |
---|
200 | if os.path.exists(logo): |
---|
201 | img = tkinter.PhotoImage(file=logo) |
---|
202 | pnl = tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0) |
---|
203 | pnl.pack(side='top', fill='both', expand='yes') |
---|
204 | # Prevent garbage collection of img |
---|
205 | pnl.image = img |
---|
206 | |
---|
207 | # Prepare the banner area |
---|
208 | self.bannerarea = tkinter.Canvas(self.root, |
---|
209 | bg=bg_color, |
---|
210 | width=300, |
---|
211 | height=300) |
---|
212 | self.bannerarea.grid(row=1, column=1, columnspan=2, sticky=sticky) |
---|
213 | |
---|
214 | tkinter.Label(self.bannerarea, anchor=tkinter.N, |
---|
215 | text=str(ProgramVersion + "\n" + ProgramAuthor), |
---|
216 | font=('Helvetica', 11), justify=tkinter.CENTER, |
---|
217 | foreground='#195866', background=bg_color, |
---|
218 | height=3).pack(side='top', |
---|
219 | fill='both', |
---|
220 | expand='yes') |
---|
221 | |
---|
222 | self.bannerarea.after(1000, self.update_canvas) |
---|
223 | |
---|
224 | # IP |
---|
225 | # retrieves the list of server IP addresses |
---|
226 | try: |
---|
227 | if_ips = list(set( # no duplicates |
---|
228 | [addrinfo[4][0] for addrinfo in getipaddrinfo(socket.getfqdn()) |
---|
229 | if not is_loopback_ip_address(addrinfo=addrinfo)])) |
---|
230 | except socket.gaierror: |
---|
231 | if_ips = [] |
---|
232 | |
---|
233 | tkinter.Label(self.root, |
---|
234 | text='Server IP:', bg=bg_color, |
---|
235 | justify=tkinter.RIGHT).grid(row=4, |
---|
236 | column=1, |
---|
237 | sticky=sticky) |
---|
238 | self.ips = {} |
---|
239 | self.selected_ip = tkinter.StringVar() |
---|
240 | row = 4 |
---|
241 | ips = [('127.0.0.1', 'Local (IPv4)')] + \ |
---|
242 | ([('::1', 'Local (IPv6)')] if socket.has_ipv6 else []) + \ |
---|
243 | [(ip, 'Public') for ip in if_ips] + \ |
---|
244 | [('0.0.0.0', 'Public')] |
---|
245 | for ip, legend in ips: |
---|
246 | self.ips[ip] = tkinter.Radiobutton( |
---|
247 | self.root, bg=bg_color, highlightthickness=0, |
---|
248 | selectcolor='light grey', width=30, |
---|
249 | anchor=tkinter.W, text='%s (%s)' % (legend, ip), |
---|
250 | justify=tkinter.LEFT, |
---|
251 | variable=self.selected_ip, value=ip) |
---|
252 | self.ips[ip].grid(row=row, column=2, sticky=sticky) |
---|
253 | if row == 4: |
---|
254 | self.ips[ip].select() |
---|
255 | row += 1 |
---|
256 | shift = row |
---|
257 | |
---|
258 | # Port |
---|
259 | tkinter.Label(self.root, |
---|
260 | text='Server Port:', bg=bg_color, |
---|
261 | justify=tkinter.RIGHT).grid(row=shift, |
---|
262 | column=1, pady=10, |
---|
263 | sticky=sticky) |
---|
264 | |
---|
265 | self.port_number = tkinter.Entry(self.root) |
---|
266 | self.port_number.insert(tkinter.END, options.port) |
---|
267 | self.port_number.grid(row=shift, column=2, sticky=sticky, pady=10) |
---|
268 | |
---|
269 | # Password |
---|
270 | tkinter.Label(self.root, |
---|
271 | text='Choose Password:', bg=bg_color, |
---|
272 | justify=tkinter.RIGHT).grid(row=shift + 1, |
---|
273 | column=1, |
---|
274 | sticky=sticky) |
---|
275 | |
---|
276 | self.password = tkinter.Entry(self.root, show='*') |
---|
277 | self.password.bind('<Return>', lambda e: self.start()) |
---|
278 | self.password.focus_force() |
---|
279 | self.password.grid(row=shift + 1, column=2, sticky=sticky) |
---|
280 | |
---|
281 | # Prepare the canvas |
---|
282 | self.canvas = tkinter.Canvas(self.root, |
---|
283 | width=400, |
---|
284 | height=100, |
---|
285 | bg='black') |
---|
286 | self.canvas.grid(row=shift + 2, column=1, columnspan=2, pady=5, |
---|
287 | sticky=sticky) |
---|
288 | self.canvas.after(1000, self.update_canvas) |
---|
289 | |
---|
290 | # Prepare the frame |
---|
291 | frame = tkinter.Frame(self.root) |
---|
292 | frame.grid(row=shift + 3, column=1, columnspan=2, pady=5, |
---|
293 | sticky=sticky) |
---|
294 | |
---|
295 | # Start button |
---|
296 | self.button_start = tkinter.Button(frame, |
---|
297 | text='start server', |
---|
298 | command=self.start) |
---|
299 | |
---|
300 | self.button_start.grid(row=0, column=0, sticky=sticky) |
---|
301 | |
---|
302 | # Stop button |
---|
303 | self.button_stop = tkinter.Button(frame, |
---|
304 | text='stop server', |
---|
305 | command=self.stop) |
---|
306 | |
---|
307 | self.button_stop.grid(row=0, column=1, sticky=sticky) |
---|
308 | self.button_stop.configure(state='disabled') |
---|
309 | |
---|
310 | if options.taskbar: |
---|
311 | import gluon.contrib.taskbar_widget |
---|
312 | self.tb = gluon.contrib.taskbar_widget.TaskBarIcon() |
---|
313 | self.checkTaskBar() |
---|
314 | |
---|
315 | if options.password != '<ask>': |
---|
316 | self.password.insert(0, options.password) |
---|
317 | self.start() |
---|
318 | self.root.withdraw() |
---|
319 | else: |
---|
320 | self.tb = None |
---|
321 | |
---|
322 | def update_schedulers(self, start=False): |
---|
323 | applications_folder = os.path.join(self.options.folder, 'applications') |
---|
324 | available_apps = [ |
---|
325 | arq for arq in os.listdir(applications_folder) |
---|
326 | if os.path.isdir(os.path.join(applications_folder, arq)) |
---|
327 | ] |
---|
328 | with self.scheduler_processes_lock: |
---|
329 | # reset the menu |
---|
330 | # since applications can disappear (be disinstalled) must |
---|
331 | # clear the menu (should use tkinter.END or tkinter.LAST) |
---|
332 | self.schedmenu.delete(0, 'end') |
---|
333 | for arq in available_apps: |
---|
334 | if arq not in self.scheduler_processes: |
---|
335 | item = lambda a = arq: self.try_start_scheduler(a) |
---|
336 | self.schedmenu.add_command(label="start %s" % arq, |
---|
337 | command=item) |
---|
338 | if arq in self.scheduler_processes: |
---|
339 | item = lambda a = arq: self.try_stop_scheduler(a) |
---|
340 | self.schedmenu.add_command(label="stop %s" % arq, |
---|
341 | command=item) |
---|
342 | |
---|
343 | if start and self.options.with_scheduler and self.options.schedulers: |
---|
344 | # the widget takes care of starting the schedulers |
---|
345 | apps = [ag.split(':', 1)[0] for ag in self.options.schedulers] |
---|
346 | else: |
---|
347 | apps = [] |
---|
348 | for app in apps: |
---|
349 | self.try_start_scheduler(app) |
---|
350 | |
---|
351 | def start_schedulers(self, app): |
---|
352 | from multiprocessing import Process |
---|
353 | code = "from gluon.globals import current;current._scheduler.loop()" |
---|
354 | print('starting scheduler from widget for "%s"...' % app) |
---|
355 | args = (app, True, True, None, False, code, False, True) |
---|
356 | p = Process(target=run, args=args) |
---|
357 | with self.scheduler_processes_lock: |
---|
358 | self.scheduler_processes[app] = p |
---|
359 | self.update_schedulers() |
---|
360 | print("Currently running %s scheduler processes" % ( |
---|
361 | len(self.scheduler_processes))) |
---|
362 | p.start() |
---|
363 | print("Processes started") |
---|
364 | |
---|
365 | def try_stop_scheduler(self, app, skip_update=False): |
---|
366 | p = None |
---|
367 | with self.scheduler_processes_lock: |
---|
368 | if app in self.scheduler_processes: |
---|
369 | p = self.scheduler_processes[app] |
---|
370 | del self.scheduler_processes[app] |
---|
371 | if p is not None: |
---|
372 | p.terminate() |
---|
373 | p.join() |
---|
374 | if not skip_update: |
---|
375 | self.update_schedulers() |
---|
376 | |
---|
377 | def try_start_scheduler(self, app): |
---|
378 | t = None |
---|
379 | with self.scheduler_processes_lock: |
---|
380 | if not is_appdir(self.options.folder, app): |
---|
381 | self.schedmenu.delete("start %s" % app) |
---|
382 | return |
---|
383 | if app not in self.scheduler_processes: |
---|
384 | t = threading.Thread(target=self.start_schedulers, args=(app,)) |
---|
385 | if t is not None: |
---|
386 | t.start() |
---|
387 | |
---|
388 | def checkTaskBar(self): |
---|
389 | """ Checks taskbar status """ |
---|
390 | tb = self.tb |
---|
391 | if tb.status: |
---|
392 | st0 = tb.status[0] |
---|
393 | EnumStatus = tb.EnumStatus |
---|
394 | if st0 == EnumStatus.QUIT: |
---|
395 | self.quit() |
---|
396 | elif st0 == EnumStatus.TOGGLE: |
---|
397 | if self.root.state() == 'withdrawn': |
---|
398 | self.root.deiconify() |
---|
399 | else: |
---|
400 | self.root.withdraw() |
---|
401 | elif st0 == EnumStatus.STOP: |
---|
402 | self.stop() |
---|
403 | elif st0 == EnumStatus.START: |
---|
404 | self.start() |
---|
405 | elif st0 == EnumStatus.RESTART: |
---|
406 | self.stop() |
---|
407 | self.start() |
---|
408 | del tb.status[0] |
---|
409 | |
---|
410 | self.root.after(1000, self.checkTaskBar) |
---|
411 | |
---|
412 | def connect_pages(self): |
---|
413 | """ Connects pages """ |
---|
414 | # reset the menu, |
---|
415 | # since applications can disappear (be disinstalled) must |
---|
416 | # clear the menu (should use tkinter.END or tkinter.LAST) |
---|
417 | self.pagesmenu.delete(0, 'end') |
---|
418 | applications_folder = os.path.join(self.options.folder, 'applications') |
---|
419 | available_apps = [ |
---|
420 | arq for arq in os.listdir(applications_folder) |
---|
421 | if os.path.exists(os.path.join(applications_folder, arq, '__init__.py')) |
---|
422 | ] |
---|
423 | for arq in available_apps: |
---|
424 | url = self.url + arq |
---|
425 | item = lambda a = arq: self.try_start_browser(a) |
---|
426 | self.pagesmenu.add_command( |
---|
427 | label=url, command=item) |
---|
428 | |
---|
429 | def try_start_browser(self, app): |
---|
430 | url = self.url + app |
---|
431 | if not is_appdir(self.options.folder, app): |
---|
432 | self.pagesmenu.delete(url) |
---|
433 | return |
---|
434 | start_browser(url) |
---|
435 | |
---|
436 | def quit(self, justHide=False): |
---|
437 | """ Finishes the program execution """ |
---|
438 | if justHide: |
---|
439 | self.root.withdraw() |
---|
440 | else: |
---|
441 | try: |
---|
442 | with self.scheduler_processes_lock: |
---|
443 | scheds = list(self.scheduler_processes.keys()) |
---|
444 | for t in scheds: |
---|
445 | self.try_stop_scheduler(t, skip_update=True) |
---|
446 | except: |
---|
447 | pass |
---|
448 | if self.options.with_cron and not self.options.soft_cron: |
---|
449 | # shutting down hardcron |
---|
450 | try: |
---|
451 | newcron.stopcron() |
---|
452 | except: |
---|
453 | pass |
---|
454 | try: |
---|
455 | # HttpServer.stop takes care of stopping softcron |
---|
456 | self.server.stop() |
---|
457 | except: |
---|
458 | pass |
---|
459 | try: |
---|
460 | self.tb.Destroy() |
---|
461 | except: |
---|
462 | pass |
---|
463 | |
---|
464 | self.root.destroy() |
---|
465 | sys.exit(0) |
---|
466 | |
---|
467 | def error(self, message): |
---|
468 | """ Shows error message """ |
---|
469 | if PY2: |
---|
470 | import tkMessageBox as messagebox |
---|
471 | else: |
---|
472 | from tkinter import messagebox |
---|
473 | messagebox.showerror('web2py start server', message) |
---|
474 | |
---|
475 | def start(self): |
---|
476 | """ Starts web2py server """ |
---|
477 | password = self.password.get() |
---|
478 | if not password: |
---|
479 | self.error('no password, no web admin interface') |
---|
480 | |
---|
481 | ip = self.selected_ip.get() |
---|
482 | if not is_valid_ip_address(ip): |
---|
483 | return self.error('invalid host ip address') |
---|
484 | try: |
---|
485 | port = int(self.port_number.get()) |
---|
486 | except ValueError: |
---|
487 | return self.error('invalid port number') |
---|
488 | |
---|
489 | if self.options.server_key and self.options.server_cert: |
---|
490 | proto = 'https' |
---|
491 | else: |
---|
492 | proto = 'http' |
---|
493 | self.url = get_url(ip, proto=proto, port=port) |
---|
494 | |
---|
495 | self.connect_pages() |
---|
496 | self.update_schedulers() |
---|
497 | |
---|
498 | # softcron is stopped with HttpServer, thus if starting again |
---|
499 | # need to reset newcron._stopping to re-enable cron |
---|
500 | if self.options.soft_cron: |
---|
501 | newcron.reset() |
---|
502 | |
---|
503 | # FIXME: if the HttpServer is stopped, then started again, |
---|
504 | # does not start because of following error: |
---|
505 | # WARNING:Rocket.Errors.Port8000:Listener started when not ready. |
---|
506 | |
---|
507 | self.button_start.configure(state='disabled') |
---|
508 | |
---|
509 | try: |
---|
510 | options = self.options |
---|
511 | req_queue_size = options.request_queue_size |
---|
512 | self.server = main.HttpServer( |
---|
513 | ip, |
---|
514 | port, |
---|
515 | password, |
---|
516 | pid_filename=options.pid_filename, |
---|
517 | log_filename=options.log_filename, |
---|
518 | profiler_dir=options.profiler_dir, |
---|
519 | ssl_certificate=options.server_cert, |
---|
520 | ssl_private_key=options.server_key, |
---|
521 | ssl_ca_certificate=options.ca_cert, |
---|
522 | min_threads=options.min_threads, |
---|
523 | max_threads=options.max_threads, |
---|
524 | server_name=options.server_name, |
---|
525 | request_queue_size=req_queue_size, |
---|
526 | timeout=options.timeout, |
---|
527 | shutdown_timeout=options.shutdown_timeout, |
---|
528 | path=options.folder, |
---|
529 | interfaces=options.interfaces) |
---|
530 | |
---|
531 | threading.Thread(target=self.server.start).start() |
---|
532 | except Exception as e: |
---|
533 | self.button_start.configure(state='normal') |
---|
534 | return self.error(str(e)) |
---|
535 | |
---|
536 | if not self.server_ready(): |
---|
537 | self.button_start.configure(state='normal') |
---|
538 | return |
---|
539 | |
---|
540 | self.button_stop.configure(state='normal') |
---|
541 | |
---|
542 | if not options.taskbar: |
---|
543 | cpt = threading.Thread(target=start_browser, |
---|
544 | args=(get_url(ip, proto=proto, port=port), True)) |
---|
545 | cpt.setDaemon(True) |
---|
546 | cpt.start() |
---|
547 | |
---|
548 | self.password.configure(state='readonly') |
---|
549 | for ip in self.ips.values(): |
---|
550 | ip.configure(state='disabled') |
---|
551 | self.port_number.configure(state='readonly') |
---|
552 | |
---|
553 | if self.tb: |
---|
554 | self.tb.SetServerRunning() |
---|
555 | |
---|
556 | def server_ready(self): |
---|
557 | for listener in self.server.server.listeners: |
---|
558 | if listener.ready: |
---|
559 | return True |
---|
560 | return False |
---|
561 | |
---|
562 | def stop(self): |
---|
563 | """ Stops web2py server """ |
---|
564 | self.button_start.configure(state='normal') |
---|
565 | self.button_stop.configure(state='disabled') |
---|
566 | self.password.configure(state='normal') |
---|
567 | for ip in self.ips.values(): |
---|
568 | ip.configure(state='normal') |
---|
569 | self.port_number.configure(state='normal') |
---|
570 | self.server.stop() |
---|
571 | |
---|
572 | if self.tb: |
---|
573 | self.tb.SetServerStopped() |
---|
574 | |
---|
575 | def update_canvas(self): |
---|
576 | """ Updates canvas """ |
---|
577 | httplog = os.path.join(self.options.folder, self.options.log_filename) |
---|
578 | canvas = self.canvas |
---|
579 | try: |
---|
580 | t1 = os.path.getsize(httplog) |
---|
581 | except OSError: |
---|
582 | canvas.after(1000, self.update_canvas) |
---|
583 | return |
---|
584 | |
---|
585 | points = 400 |
---|
586 | try: |
---|
587 | pvalues = self.p0[1:] |
---|
588 | with open(httplog, 'r') as fp: |
---|
589 | fp.seek(self.t0) |
---|
590 | data = fp.read(t1 - self.t0) |
---|
591 | self.p0 = pvalues + [10 + 90.0 / math.sqrt(1 + data.count('\n'))] |
---|
592 | |
---|
593 | for i in xrange(points - 1): |
---|
594 | c = canvas.coords(self.q0[i]) |
---|
595 | canvas.coords(self.q0[i], |
---|
596 | (c[0], self.p0[i], |
---|
597 | c[2], self.p0[i + 1])) |
---|
598 | self.t0 = t1 |
---|
599 | except AttributeError: |
---|
600 | self.t0 = time.time() |
---|
601 | self.t0 = t1 |
---|
602 | self.p0 = [100] * points |
---|
603 | self.q0 = [canvas.create_line(i, 100, i + 1, 100, |
---|
604 | fill='green') for i in xrange(points - 1)] |
---|
605 | |
---|
606 | canvas.after(1000, self.update_canvas) |
---|
607 | |
---|
608 | |
---|
609 | def get_code_for_scheduler(applications_parent, app_groups): |
---|
610 | app = app_groups[0] |
---|
611 | if not is_appdir(applications_parent, app): |
---|
612 | print("Application '%s' doesn't exist, skipping" % app) |
---|
613 | return None, None |
---|
614 | code = 'from gluon.globals import current;' |
---|
615 | if len(app_groups) > 1: |
---|
616 | code += "current._scheduler.group_names=['%s'];" % "','".join( |
---|
617 | app_groups[1:]) |
---|
618 | code += "current._scheduler.loop()" |
---|
619 | return app, code |
---|
620 | |
---|
621 | |
---|
622 | def start_schedulers(options): |
---|
623 | from multiprocessing import Process |
---|
624 | apps = [ag.split(':') for ag in options.schedulers] |
---|
625 | if not options.with_scheduler and len(apps) == 1: |
---|
626 | app, code = get_code_for_scheduler(options.folder, apps[0]) |
---|
627 | if not app: |
---|
628 | return |
---|
629 | print('starting single-scheduler for "%s"...' % app) |
---|
630 | run(app, True, True, None, False, code, False, True) |
---|
631 | return |
---|
632 | |
---|
633 | # Work around OS X problem: http://bugs.python.org/issue9405 |
---|
634 | if PY2: |
---|
635 | import urllib |
---|
636 | else: |
---|
637 | import urllib.request as urllib |
---|
638 | urllib.getproxies() |
---|
639 | |
---|
640 | processes = [] |
---|
641 | for app_groups in apps: |
---|
642 | app, code = get_code_for_scheduler(options.folder, app_groups) |
---|
643 | if not app: |
---|
644 | continue |
---|
645 | print('starting scheduler for "%s"...' % app) |
---|
646 | args = (app, True, True, None, False, code, False, True) |
---|
647 | p = Process(target=run, args=args) |
---|
648 | processes.append(p) |
---|
649 | print("Currently running %s scheduler processes" % (len(processes))) |
---|
650 | p.start() |
---|
651 | ##to avoid bashing the db at the same time |
---|
652 | time.sleep(0.7) |
---|
653 | print("Processes started") |
---|
654 | for p in processes: |
---|
655 | try: |
---|
656 | p.join() |
---|
657 | except (KeyboardInterrupt, SystemExit): |
---|
658 | print("Processes stopped") |
---|
659 | except: |
---|
660 | p.terminate() |
---|
661 | p.join() |
---|
662 | |
---|
663 | |
---|
664 | def start(): |
---|
665 | """ Starts server and other services """ |
---|
666 | |
---|
667 | # get command line arguments |
---|
668 | options = console(version=ProgramVersion) |
---|
669 | |
---|
670 | if options.with_scheduler or len(options.schedulers) > 1: |
---|
671 | try: |
---|
672 | from multiprocessing import Process |
---|
673 | except: |
---|
674 | die('Sorry, -K/--scheduler only supported for Python 2.6+') |
---|
675 | |
---|
676 | if options.gae: |
---|
677 | # write app.yaml, gaehandler.py, and exit |
---|
678 | if not os.path.exists('app.yaml'): |
---|
679 | name = options.gae |
---|
680 | # for backward compatibility |
---|
681 | if name == 'configure': |
---|
682 | if PY2: input = raw_input |
---|
683 | name = input("Your GAE app name: ") |
---|
684 | content = open(os.path.join('examples', 'app.example.yaml'), 'rb').read() |
---|
685 | open('app.yaml', 'wb').write(content.replace("yourappname", name)) |
---|
686 | else: |
---|
687 | print("app.yaml alreday exists in the web2py folder") |
---|
688 | if not os.path.exists('gaehandler.py'): |
---|
689 | content = open(os.path.join('handlers', 'gaehandler.py'), 'rb').read() |
---|
690 | open('gaehandler.py', 'wb').write(content) |
---|
691 | else: |
---|
692 | print("gaehandler.py alreday exists in the web2py folder") |
---|
693 | return |
---|
694 | |
---|
695 | logger = logging.getLogger("web2py") |
---|
696 | logger.setLevel(options.log_level) |
---|
697 | logging.getLogger().setLevel(options.log_level) # root logger |
---|
698 | |
---|
699 | # on new installation build the scaffolding app |
---|
700 | create_welcome_w2p() |
---|
701 | |
---|
702 | if options.run_system_tests: |
---|
703 | # run system test and exit |
---|
704 | run_system_tests(options) |
---|
705 | |
---|
706 | if options.quiet: |
---|
707 | # to prevent writes on stdout set a null stream |
---|
708 | class NullFile(object): |
---|
709 | def write(self, x): |
---|
710 | pass |
---|
711 | sys.stdout = NullFile() |
---|
712 | # but still has to mute existing loggers, to do that iterate |
---|
713 | # over all existing loggers (root logger included) and remove |
---|
714 | # all attached logging.StreamHandler instances currently |
---|
715 | # streaming on sys.stdout or sys.stderr |
---|
716 | loggers = [logging.getLogger()] |
---|
717 | loggers.extend(logging.Logger.manager.loggerDict.values()) |
---|
718 | for l in loggers: |
---|
719 | if isinstance(l, logging.PlaceHolder): continue |
---|
720 | for h in l.handlers[:]: |
---|
721 | if isinstance(h, logging.StreamHandler) and \ |
---|
722 | h.stream in (sys.stdout, sys.stderr): |
---|
723 | l.removeHandler(h) |
---|
724 | # NOTE: stderr.write() is still working |
---|
725 | |
---|
726 | if not options.no_banner: |
---|
727 | # banner |
---|
728 | print(ProgramName) |
---|
729 | print(ProgramAuthor) |
---|
730 | print(ProgramVersion) |
---|
731 | from pydal.drivers import DRIVERS |
---|
732 | print('Database drivers available: %s' % ', '.join(DRIVERS)) |
---|
733 | |
---|
734 | if options.run_doctests: |
---|
735 | # run doctests and exit |
---|
736 | test(options.run_doctests, verbose=options.verbose) |
---|
737 | return |
---|
738 | |
---|
739 | if options.shell: |
---|
740 | # run interactive shell and exit |
---|
741 | sys.argv = [options.run or ''] + options.args |
---|
742 | run(options.shell, plain=options.plain, bpython=options.bpython, |
---|
743 | import_models=options.import_models, startfile=options.run, |
---|
744 | cron_job=options.cron_job, force_migrate=options.force_migrate, |
---|
745 | fake_migrate=options.fake_migrate) |
---|
746 | return |
---|
747 | |
---|
748 | # set size of cron thread pools |
---|
749 | newcron.dancer_size(options.min_threads) |
---|
750 | newcron.launcher_size(options.cron_threads) |
---|
751 | |
---|
752 | if options.cron_run: |
---|
753 | # run cron (extcron) and exit |
---|
754 | logger.debug('Running extcron...') |
---|
755 | global_settings.web2py_crontype = 'external' |
---|
756 | newcron.extcron(options.folder, apps=options.crontabs) |
---|
757 | return |
---|
758 | |
---|
759 | if not options.with_scheduler and options.schedulers: |
---|
760 | # run schedulers and exit |
---|
761 | try: |
---|
762 | start_schedulers(options) |
---|
763 | except KeyboardInterrupt: |
---|
764 | pass |
---|
765 | return |
---|
766 | |
---|
767 | if options.with_cron: |
---|
768 | if options.soft_cron: |
---|
769 | print('Using cron software emulation (but this is not very efficient)') |
---|
770 | global_settings.web2py_crontype = 'soft' |
---|
771 | else: |
---|
772 | # start hardcron thread |
---|
773 | logger.debug('Starting hardcron...') |
---|
774 | global_settings.web2py_crontype = 'hard' |
---|
775 | newcron.hardcron(options.folder, apps=options.crontabs).start() |
---|
776 | |
---|
777 | # if no password provided and have Tk library start GUI (when not |
---|
778 | # explicitly disabled), we also need a GUI to put in taskbar (system tray) |
---|
779 | # when requested |
---|
780 | root = None |
---|
781 | |
---|
782 | if (not options.no_gui and options.password == '<ask>') or options.taskbar: |
---|
783 | try: |
---|
784 | if PY2: |
---|
785 | import Tkinter as tkinter |
---|
786 | else: |
---|
787 | import tkinter |
---|
788 | root = tkinter.Tk() |
---|
789 | except (ImportError, OSError): |
---|
790 | logger.warn( |
---|
791 | 'GUI not available because Tk library is not installed') |
---|
792 | options.no_gui = True |
---|
793 | except: |
---|
794 | logger.exception('cannot get Tk root window, GUI disabled') |
---|
795 | options.no_gui = True |
---|
796 | |
---|
797 | if root: |
---|
798 | # run GUI and exit |
---|
799 | root.focus_force() |
---|
800 | |
---|
801 | # Mac OS X - make the GUI window rise to the top |
---|
802 | if os.path.exists("/usr/bin/osascript"): |
---|
803 | applescript = """ |
---|
804 | tell application "System Events" |
---|
805 | set proc to first process whose unix id is %d |
---|
806 | set frontmost of proc to true |
---|
807 | end tell |
---|
808 | """ % (os.getpid()) |
---|
809 | os.system("/usr/bin/osascript -e '%s'" % applescript) |
---|
810 | |
---|
811 | # web2pyDialog takes care of schedulers |
---|
812 | master = web2pyDialog(root, options) |
---|
813 | signal.signal(signal.SIGTERM, lambda a, b: master.quit()) |
---|
814 | |
---|
815 | try: |
---|
816 | root.mainloop() |
---|
817 | except: |
---|
818 | master.quit() |
---|
819 | |
---|
820 | sys.exit() |
---|
821 | |
---|
822 | spt = None |
---|
823 | |
---|
824 | if options.with_scheduler and options.schedulers: |
---|
825 | # start schedulers in a separate thread |
---|
826 | spt = threading.Thread(target=start_schedulers, args=(options,)) |
---|
827 | spt.start() |
---|
828 | |
---|
829 | # start server |
---|
830 | |
---|
831 | if options.password == '<ask>': |
---|
832 | options.password = getpass.getpass('choose a password:') |
---|
833 | |
---|
834 | if not options.password and not options.no_banner: |
---|
835 | print('no password, no web admin interface') |
---|
836 | |
---|
837 | # Use first interface IP and port if interfaces specified, since the |
---|
838 | # interfaces option overrides the IP (and related) options. |
---|
839 | if not options.interfaces: |
---|
840 | ip = options.ip |
---|
841 | port = options.port |
---|
842 | else: |
---|
843 | first_if = options.interfaces[0] |
---|
844 | ip = first_if[0] |
---|
845 | port = first_if[1] |
---|
846 | |
---|
847 | if options.server_key and options.server_cert: |
---|
848 | proto = 'https' |
---|
849 | else: |
---|
850 | proto = 'http' |
---|
851 | |
---|
852 | url = get_url(ip, proto=proto, port=port) |
---|
853 | |
---|
854 | if not options.no_banner: |
---|
855 | message = '\nplease visit:\n\t%s\n' |
---|
856 | if sys.platform.startswith('win'): |
---|
857 | message += 'use "taskkill /f /pid %i" to shutdown the web2py server\n\n' |
---|
858 | else: |
---|
859 | message += 'use "kill -SIGTERM %i" to shutdown the web2py server\n\n' |
---|
860 | print(message % (url, os.getpid())) |
---|
861 | |
---|
862 | # enhance linecache.getline (used by debugger) to look at the source file |
---|
863 | # if the line was not found (under py2exe & when file was modified) |
---|
864 | import linecache |
---|
865 | py2exe_getline = linecache.getline |
---|
866 | |
---|
867 | def getline(filename, lineno, *args, **kwargs): |
---|
868 | line = py2exe_getline(filename, lineno, *args, **kwargs) |
---|
869 | if not line: |
---|
870 | try: |
---|
871 | with open(filename, "rb") as f: |
---|
872 | for i, line in enumerate(f): |
---|
873 | line = line.decode('utf-8') |
---|
874 | if lineno == i + 1: |
---|
875 | break |
---|
876 | else: |
---|
877 | line = '' |
---|
878 | except (IOError, OSError): |
---|
879 | line = '' |
---|
880 | return line |
---|
881 | linecache.getline = getline |
---|
882 | |
---|
883 | server = main.HttpServer(ip=ip, |
---|
884 | port=port, |
---|
885 | password=options.password, |
---|
886 | pid_filename=options.pid_filename, |
---|
887 | log_filename=options.log_filename, |
---|
888 | profiler_dir=options.profiler_dir, |
---|
889 | ssl_certificate=options.server_cert, |
---|
890 | ssl_private_key=options.server_key, |
---|
891 | ssl_ca_certificate=options.ca_cert, |
---|
892 | min_threads=options.min_threads, |
---|
893 | max_threads=options.max_threads, |
---|
894 | server_name=options.server_name, |
---|
895 | request_queue_size=options.request_queue_size, |
---|
896 | timeout=options.timeout, |
---|
897 | socket_timeout=options.socket_timeout, |
---|
898 | shutdown_timeout=options.shutdown_timeout, |
---|
899 | path=options.folder, |
---|
900 | interfaces=options.interfaces) |
---|
901 | |
---|
902 | try: |
---|
903 | server.start() |
---|
904 | except KeyboardInterrupt: |
---|
905 | server.stop() |
---|
906 | if spt is not None: |
---|
907 | try: |
---|
908 | spt.join() |
---|
909 | except: |
---|
910 | logger.exception('error terminating schedulers') |
---|
911 | logging.shutdown() |
---|