comparison env/lib/python3.9/site-packages/galaxy/util/pastescript/serve.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # Most of this code is:
2
3 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
4 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
5
6 # The server command includes the additional header:
7
8 # For discussion of daemonizing:
9 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
10 # Code taken also from QP:
11 # http://www.mems-exchange.org/software/qp/
12 # From lib/site.py
13
14 # Galaxy originally used PasteScript and PasteDeploy for application
15 # loading, to maintain compatibility we've internalized some of that
16 # code here, stripping out uneeded functionality.
17
18 # All top level imports from each package moved here and organized
19
20 import atexit
21 import configparser
22 import errno
23 import grp
24 import logging
25 import optparse
26 import os
27 import pwd
28 import re
29 import resource
30 import signal
31 import socket
32 import subprocess
33 import sys
34 import textwrap
35 import threading
36 import time
37 from gettext import gettext as _
38 from logging.config import fileConfig
39 from typing import Optional
40
41 from .loadwsgi import loadapp, loadserver
42
43
44 difflib = None
45
46 # ---- from paste.script.bool_optparse --------------------------------
47
48 """
49 A subclass of ``optparse.OptionParser`` that allows boolean long
50 options (like ``--verbose``) to also take arguments (like
51 ``--verbose=true``). Arguments *must* use ``=``.
52 """
53
54
55 class BoolOptionParser(optparse.OptionParser):
56
57 def _process_long_opt(self, rargs, values):
58 arg = rargs.pop(0)
59
60 # Value explicitly attached to arg? Pretend it's the next
61 # argument.
62 if "=" in arg:
63 (opt, next_arg) = arg.split("=", 1)
64 rargs.insert(0, next_arg)
65 had_explicit_value = True
66 else:
67 opt = arg
68 had_explicit_value = False
69
70 opt = self._match_long_opt(opt)
71 option = self._long_opt[opt]
72 if option.takes_value():
73 nargs = option.nargs
74 if len(rargs) < nargs:
75 if nargs == 1:
76 self.error(_("%s option requires an argument") % opt)
77 else:
78 self.error(_("%s option requires %d arguments")
79 % (opt, nargs))
80 elif nargs == 1:
81 value = rargs.pop(0)
82 else:
83 value = tuple(rargs[0:nargs])
84 del rargs[0:nargs]
85
86 elif had_explicit_value:
87 value = rargs[0].lower().strip()
88 del rargs[0:1]
89 if value in ('true', 'yes', 'on', '1', 'y', 't'):
90 value = None
91 elif value in ('false', 'no', 'off', '0', 'n', 'f'):
92 # Don't process
93 return
94 else:
95 self.error(_('%s option takes a boolean value only (true/false)') % opt)
96
97 else:
98 value = None
99
100 option.process(opt, value, values, self)
101
102 # ---- from paste.script.command --------------------------------------
103
104 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
105 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
106
107
108 class BadCommand(Exception):
109
110 def __init__(self, message, exit_code=2):
111 self.message = message
112 self.exit_code = exit_code
113 Exception.__init__(self, message)
114
115 def _get_message(self):
116 """Getter for 'message'; needed only to override deprecation
117 in BaseException."""
118 return self.__message
119
120 def _set_message(self, value):
121 """Setter for 'message'; needed only to override deprecation
122 in BaseException."""
123 self.__message = value
124
125 # BaseException.message has been deprecated since Python 2.6.
126 # To prevent DeprecationWarning from popping up over this
127 # pre-existing attribute, use a new property that takes lookup
128 # precedence.
129 message = property(_get_message, _set_message)
130
131
132 class NoDefault:
133 pass
134
135
136 # run and invoke methods moved below ServeCommand
137 class Command:
138
139 def __init__(self, name):
140 self.command_name = name
141
142 max_args = None
143 max_args_error = 'You must provide no more than %(max_args)s arguments'
144 min_args: Optional[int] = None
145 min_args_error = 'You must provide at least %(min_args)s arguments'
146 required_args = None
147 # If this command takes a configuration file, set this to 1 or -1
148 # Then if invoked through #! the config file will be put into the positional
149 # arguments -- at the beginning with 1, at the end with -1
150 takes_config_file: Optional[int] = None
151
152 # Grouped in help messages by this:
153 group_name = ''
154
155 required_args = ()
156 description: Optional[str] = None
157 usage = ''
158 hidden = False
159 # This is the default verbosity level; --quiet subtracts,
160 # --verbose adds:
161 default_verbosity = 0
162 # This is the default interactive state:
163 default_interactive = 0
164 return_code = 0
165
166 BadCommand = BadCommand
167
168 # Must define:
169 # parser
170 # summary
171 # command()
172
173 def run(self, args):
174 self.parse_args(args)
175
176 # Setup defaults:
177 for name, default in [('verbose', 0),
178 ('quiet', 0),
179 ('interactive', False),
180 ('overwrite', False)]:
181 if not hasattr(self.options, name):
182 setattr(self.options, name, default)
183 if getattr(self.options, 'simulate', False):
184 self.options.verbose = max(self.options.verbose, 1)
185 self.interactive = self.default_interactive
186 if getattr(self.options, 'interactive', False):
187 self.interactive += self.options.interactive
188 if getattr(self.options, 'no_interactive', False):
189 self.interactive = False
190 self.verbose = self.default_verbosity
191 self.verbose += self.options.verbose
192 self.verbose -= self.options.quiet
193 self.simulate = getattr(self.options, 'simulate', False)
194
195 # For #! situations:
196 if os.environ.get('PASTE_CONFIG_FILE') and self.takes_config_file is not None:
197 take = self.takes_config_file
198 filename = os.environ.get('PASTE_CONFIG_FILE')
199 if take == 1:
200 self.args.insert(0, filename)
201 elif take == -1:
202 self.args.append(filename)
203 else:
204 assert 0, (
205 "Value takes_config_file must be None, 1, or -1 (not %r)"
206 % take)
207
208 if os.environ.get('PASTE_DEFAULT_QUIET'):
209 self.verbose = 0
210
211 # Validate:
212 if self.min_args is not None and len(self.args) < self.min_args:
213 raise BadCommand(
214 self.min_args_error % {'min_args': self.min_args,
215 'actual_args': len(self.args)})
216 if self.max_args is not None and len(self.args) > self.max_args:
217 raise BadCommand(
218 self.max_args_error % {'max_args': self.max_args,
219 'actual_args': len(self.args)})
220 for var_name, option_name in self.required_args:
221 if not getattr(self.options, var_name, None):
222 raise BadCommand(
223 'You must provide the option %s' % option_name)
224 result = self.command()
225 if result is None:
226 return self.return_code
227 else:
228 return result
229
230 def parse_args(self, args):
231 if self.usage:
232 usage = ' ' + self.usage
233 else:
234 usage = ''
235 self.parser.usage = "%prog [options]{}\n{}".format(
236 usage, self.summary)
237 self.parser.prog = self._prog_name()
238 if self.description:
239 desc = self.description
240 desc = textwrap.dedent(desc)
241 self.parser.description = desc
242 self.options, self.args = self.parser.parse_args(args)
243
244 def _prog_name(self):
245 return '{} {}'.format(os.path.basename(sys.argv[0]), self.command_name)
246
247 ########################################
248 # Utility methods
249 ########################################
250
251 def pad(self, s, length, dir='left'):
252 if len(s) >= length:
253 return s
254 if dir == 'left':
255 return s + ' ' * (length - len(s))
256 else:
257 return ' ' * (length - len(s)) + s
258
259 def _standard_parser(cls, verbose=True,
260 interactive=False,
261 no_interactive=False,
262 simulate=False,
263 quiet=False,
264 overwrite=False):
265 """
266 Create a standard ``OptionParser`` instance.
267
268 Typically used like::
269
270 class MyCommand(Command):
271 parser = Command.standard_parser()
272
273 Subclasses may redefine ``standard_parser``, so use the
274 nearest superclass's class method.
275 """
276 parser = BoolOptionParser()
277 if verbose:
278 parser.add_option('-v', '--verbose',
279 action='count',
280 dest='verbose',
281 default=0)
282 if quiet:
283 parser.add_option('-q', '--quiet',
284 action='count',
285 dest='quiet',
286 default=0)
287 if no_interactive:
288 parser.add_option('--no-interactive',
289 action="count",
290 dest="no_interactive",
291 default=0)
292 if interactive:
293 parser.add_option('-i', '--interactive',
294 action='count',
295 dest='interactive',
296 default=0)
297 if simulate:
298 parser.add_option('-n', '--simulate',
299 action='store_true',
300 dest='simulate',
301 default=False)
302 if overwrite:
303 parser.add_option('-f', '--overwrite',
304 dest="overwrite",
305 action="store_true",
306 help="Overwrite files (warnings will be emitted for non-matching files otherwise)")
307 return parser
308
309 standard_parser = classmethod(_standard_parser)
310
311 def quote_first_command_arg(self, arg):
312 """
313 There's a bug in Windows when running an executable that's
314 located inside a path with a space in it. This method handles
315 that case, or on non-Windows systems or an executable with no
316 spaces, it just leaves well enough alone.
317 """
318 if sys.platform != 'win32' or ' ' not in arg:
319 # Problem does not apply:
320 return arg
321 try:
322 import win32api
323 except ImportError:
324 raise ValueError(
325 "The executable %r contains a space, and in order to "
326 "handle this issue you must have the win32api module "
327 "installed" % arg)
328 arg = win32api.GetShortPathName(arg)
329 return arg
330
331 def parse_vars(self, args):
332 """
333 Given variables like ``['a=b', 'c=d']`` turns it into ``{'a':
334 'b', 'c': 'd'}``
335 """
336 result = {}
337 for arg in args:
338 if '=' not in arg:
339 raise BadCommand(
340 'Variable assignment %r invalid (no "=")'
341 % arg)
342 name, value = arg.split('=', 1)
343 result[name] = value
344 return result
345
346 def logging_file_config(self, config_file):
347 """
348 Setup logging via the logging module's fileConfig function with the
349 specified ``config_file``, if applicable.
350
351 ConfigParser defaults are specified for the special ``__file__``
352 and ``here`` variables, similar to PasteDeploy config loading.
353 """
354 parser = configparser.ConfigParser()
355 parser.read([config_file])
356 if parser.has_section('loggers'):
357 config_file = os.path.abspath(config_file)
358 fileConfig(config_file, dict(__file__=config_file,
359 here=os.path.dirname(config_file)))
360
361
362 class NotFoundCommand(Command):
363
364 def run(self, args):
365 print('Command %r not known (you may need to run setup.py egg_info)'
366 % self.command_name)
367 commands = list()
368 commands.sort()
369 if not commands:
370 print('No commands registered.')
371 print('Have you installed Paste Script?')
372 print('(try running python setup.py develop)')
373 return 2
374 print('Known commands:')
375 longest = max([len(n) for n, c in commands])
376 for name, command in commands:
377 print(' {} {}'.format(self.pad(name, length=longest),
378 command.load().summary))
379 return 2
380
381
382 # ---- From paste.script.serve ----------------------------------------
383
384 MAXFD = 1024
385
386 jython = sys.platform.startswith('java')
387
388
389 class DaemonizeException(Exception):
390 pass
391
392
393 class ServeCommand(Command):
394
395 min_args = 0
396 usage = 'CONFIG_FILE [start|stop|restart|status] [var=value]'
397 takes_config_file = 1
398 summary = "Serve the described application"
399 description: Optional[str] = """\
400 This command serves a web application that uses a paste.deploy
401 configuration file for the server and application.
402
403 If start/stop/restart is given, then --daemon is implied, and it will
404 start (normal operation), stop (--stop-daemon), or do both.
405
406 You can also include variable assignments like 'http_port=8080'
407 and then use %(http_port)s in your config files.
408 """
409
410 # used by subclasses that configure apps and servers differently
411 requires_config_file = True
412
413 parser = Command.standard_parser(quiet=True)
414 parser.add_option('-n', '--app-name',
415 dest='app_name',
416 metavar='NAME',
417 help="Load the named application (default main)")
418 parser.add_option('-s', '--server',
419 dest='server',
420 metavar='SERVER_TYPE',
421 help="Use the named server.")
422 parser.add_option('--server-name',
423 dest='server_name',
424 metavar='SECTION_NAME',
425 help="Use the named server as defined in the configuration file (default: main)")
426 if hasattr(os, 'fork'):
427 parser.add_option('--daemon',
428 dest="daemon",
429 action="store_true",
430 help="Run in daemon (background) mode")
431 parser.add_option('--pid-file',
432 dest='pid_file',
433 metavar='FILENAME',
434 help="Save PID to file (default to paster.pid if running in daemon mode)")
435 parser.add_option('--log-file',
436 dest='log_file',
437 metavar='LOG_FILE',
438 help="Save output to the given log file (redirects stdout)")
439 parser.add_option('--reload',
440 dest='reload',
441 action='store_true',
442 help="Use auto-restart file monitor")
443 parser.add_option('--reload-interval',
444 dest='reload_interval',
445 default=1,
446 help="Seconds between checking files (low number can cause significant CPU usage)")
447 parser.add_option('--monitor-restart',
448 dest='monitor_restart',
449 action='store_true',
450 help="Auto-restart server if it dies")
451 parser.add_option('--status',
452 action='store_true',
453 dest='show_status',
454 help="Show the status of the (presumably daemonized) server")
455
456 if hasattr(os, 'setuid'):
457 # I don't think these are available on Windows
458 parser.add_option('--user',
459 dest='set_user',
460 metavar="USERNAME",
461 help="Set the user (usually only possible when run as root)")
462 parser.add_option('--group',
463 dest='set_group',
464 metavar="GROUP",
465 help="Set the group (usually only possible when run as root)")
466
467 parser.add_option('--stop-daemon',
468 dest='stop_daemon',
469 action='store_true',
470 help='Stop a daemonized server (given a PID file, or default paster.pid file)')
471
472 if jython:
473 parser.add_option('--disable-jython-reloader',
474 action='store_true',
475 dest='disable_jython_reloader',
476 help="Disable the Jython reloader")
477
478 _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
479
480 default_verbosity = 1
481
482 _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
483 _monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN'
484
485 possible_subcommands = ('start', 'stop', 'restart', 'status')
486
487 def command(self):
488 if self.options.stop_daemon:
489 return self.stop_daemon()
490
491 if not hasattr(self.options, 'set_user'):
492 # Windows case:
493 self.options.set_user = self.options.set_group = None
494 # @@: Is this the right stage to set the user at?
495 self.change_user_group(
496 self.options.set_user, self.options.set_group)
497
498 if self.requires_config_file:
499 if not self.args:
500 raise BadCommand('You must give a config file')
501 app_spec = self.args[0]
502 if len(self.args) > 1 and self.args[1] in self.possible_subcommands:
503 cmd = self.args[1]
504 restvars = self.args[2:]
505 else:
506 cmd = None
507 restvars = self.args[1:]
508 else:
509 app_spec = ""
510 if self.args and self.args[0] in self.possible_subcommands:
511 cmd = self.args[0]
512 restvars = self.args[1:]
513 else:
514 cmd = None
515 restvars = self.args[:]
516
517 if (getattr(self.options, 'daemon', False) and
518 getattr(self.options, 'reload', False)):
519 raise BadCommand('The --daemon and --reload options may not be used together')
520
521 jython_monitor = False
522 if self.options.reload:
523 if jython and not self.options.disable_jython_reloader:
524 # JythonMonitor raises the special SystemRestart
525 # exception that'll cause the Jython interpreter to
526 # reload in the existing Java process (avoiding
527 # subprocess startup time)
528 try:
529 from paste.reloader import JythonMonitor
530 except ImportError:
531 pass
532 else:
533 jython_monitor = JythonMonitor(poll_interval=int(
534 self.options.reload_interval))
535 if self.requires_config_file:
536 jython_monitor.watch_file(self.args[0])
537
538 if not jython_monitor:
539 if os.environ.get(self._reloader_environ_key):
540 from paste import reloader
541 if self.verbose > 1:
542 print('Running reloading file monitor')
543 reloader.install(int(self.options.reload_interval))
544 if self.requires_config_file:
545 reloader.watch_file(self.args[0])
546 else:
547 return self.restart_with_reloader()
548
549 if cmd not in (None, 'start', 'stop', 'restart', 'status'):
550 raise BadCommand(
551 'Error: must give start|stop|restart (not %s)' % cmd)
552
553 if cmd == 'status' or self.options.show_status:
554 return self.show_status()
555
556 if cmd == 'restart' or cmd == 'stop':
557 result = self.stop_daemon()
558 if result:
559 print("Could not stop daemon")
560 # It's ok to continue trying to restart if stop_daemon returns
561 # a 1, otherwise shortcut and return.
562 if cmd == 'restart' and result != 1:
563 return result
564 if cmd == 'stop':
565 return result
566 self.options.daemon = True
567
568 if cmd == 'start':
569 self.options.daemon = True
570
571 app_name = self.options.app_name
572 vars = self.parse_vars(restvars)
573 if not self._scheme_re.search(app_spec):
574 app_spec = 'config:' + app_spec
575 server_name = self.options.server_name
576 if self.options.server:
577 server_spec = 'egg:PasteScript'
578 assert server_name is None
579 server_name = self.options.server
580 else:
581 server_spec = app_spec
582 base = os.getcwd()
583
584 if getattr(self.options, 'daemon', False):
585 if not self.options.pid_file:
586 self.options.pid_file = 'paster.pid'
587 if not self.options.log_file:
588 self.options.log_file = 'paster.log'
589
590 # Ensure the log file is writeable
591 if self.options.log_file:
592 try:
593 writeable_log_file = open(self.options.log_file, 'a')
594 except OSError as ioe:
595 msg = 'Error: Unable to write to log file: %s' % ioe
596 raise BadCommand(msg)
597 writeable_log_file.close()
598
599 # Ensure the pid file is writeable
600 if self.options.pid_file:
601 try:
602 writeable_pid_file = open(self.options.pid_file, 'a')
603 except OSError as ioe:
604 msg = 'Error: Unable to write to pid file: %s' % ioe
605 raise BadCommand(msg)
606 writeable_pid_file.close()
607
608 if getattr(self.options, 'daemon', False):
609 try:
610 self.daemonize()
611 except DaemonizeException as ex:
612 if self.verbose > 0:
613 print(str(ex))
614 return
615
616 if (self.options.monitor_restart and not
617 os.environ.get(self._monitor_environ_key)):
618 return self.restart_with_monitor()
619
620 if self.options.pid_file:
621 self.record_pid(self.options.pid_file)
622
623 if self.options.log_file:
624 stdout_log = LazyWriter(self.options.log_file, 'a')
625 sys.stdout = stdout_log
626 sys.stderr = stdout_log
627 logging.basicConfig(stream=stdout_log)
628
629 log_fn = app_spec
630 if log_fn.startswith('config:'):
631 log_fn = app_spec[len('config:'):]
632 elif log_fn.startswith('egg:'):
633 log_fn = None
634 if log_fn:
635 log_fn = os.path.join(base, log_fn)
636 self.logging_file_config(log_fn)
637
638 server = loadserver(server_spec, name=server_name, relative_to=base, global_conf=vars)
639
640 app = loadapp(app_spec, name=app_name, relative_to=base, global_conf=vars)
641
642 if self.verbose > 0:
643 if hasattr(os, 'getpid'):
644 msg = 'Starting server in PID %i.' % os.getpid()
645 else:
646 msg = 'Starting server.'
647 print(msg)
648
649 def serve():
650 try:
651 server(app)
652 except (SystemExit, KeyboardInterrupt) as e:
653 if self.verbose > 1:
654 raise
655 if str(e):
656 msg = ' ' + str(e)
657 else:
658 msg = ''
659 print('Exiting%s (-v to see traceback)' % msg)
660 except AttributeError as e:
661 # Capturing bad error response from paste
662 if str(e) == "'WSGIThreadPoolServer' object has no attribute 'thread_pool'":
663 raise OSError(98, 'Address already in use')
664 else:
665 raise AttributeError(e)
666
667 if jython_monitor:
668 # JythonMonitor has to be ran from the main thread
669 threading.Thread(target=serve).start()
670 print('Starting Jython file monitor')
671 jython_monitor.periodic_reload()
672 else:
673 serve()
674
675 def daemonize(self):
676 pid = live_pidfile(self.options.pid_file)
677 if pid:
678 raise DaemonizeException(
679 "Daemon is already running (PID: %s from PID file %s)"
680 % (pid, self.options.pid_file))
681
682 if self.verbose > 0:
683 print('Entering daemon mode')
684 pid = os.fork()
685 if pid:
686 # The forked process also has a handle on resources, so we
687 # *don't* want proper termination of the process, we just
688 # want to exit quick (which os._exit() does)
689 os._exit(0)
690 # Make this the session leader
691 os.setsid()
692 # Fork again for good measure!
693 pid = os.fork()
694 if pid:
695 os._exit(0)
696
697 # @@: Should we set the umask and cwd now?
698
699 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
700 if maxfd == resource.RLIM_INFINITY:
701 maxfd = MAXFD
702 # Iterate through and close all file descriptors.
703 for fd in range(0, maxfd):
704 try:
705 os.close(fd)
706 except OSError: # ERROR, fd wasn't open to begin with (ignored)
707 pass
708
709 if hasattr(os, "devnull"):
710 REDIRECT_TO = os.devnull
711 else:
712 REDIRECT_TO = "/dev/null"
713 os.open(REDIRECT_TO, os.O_RDWR) # standard input (0)
714 # Duplicate standard input to standard output and standard error.
715 os.dup2(0, 1) # standard output (1)
716 os.dup2(0, 2) # standard error (2)
717
718 def record_pid(self, pid_file):
719 pid = os.getpid()
720 if self.verbose > 1:
721 print(f'Writing PID {pid} to {pid_file}')
722 f = open(pid_file, 'w')
723 f.write(str(pid))
724 f.close()
725 atexit.register(_remove_pid_file, pid, pid_file, self.verbose)
726
727 def stop_daemon(self):
728 pid_file = self.options.pid_file or 'paster.pid'
729 if not os.path.exists(pid_file):
730 print('No PID file exists in %s' % pid_file)
731 return 1
732 pid = read_pidfile(pid_file)
733 if not pid:
734 print("Not a valid PID file in %s" % pid_file)
735 return 1
736 pid = live_pidfile(pid_file)
737 if not pid:
738 print("PID in %s is not valid (deleting)" % pid_file)
739 try:
740 os.unlink(pid_file)
741 except OSError as e:
742 print("Could not delete: %s" % e)
743 return 2
744 return 1
745 for _i in range(10):
746 if not live_pidfile(pid_file):
747 break
748 os.kill(pid, signal.SIGTERM)
749 time.sleep(1)
750 else:
751 print("failed to kill web process %s" % pid)
752 return 3
753 if os.path.exists(pid_file):
754 os.unlink(pid_file)
755 return 0
756
757 def show_status(self):
758 pid_file = self.options.pid_file or 'paster.pid'
759 if not os.path.exists(pid_file):
760 print('No PID file %s' % pid_file)
761 return 1
762 pid = read_pidfile(pid_file)
763 if not pid:
764 print('No PID in file %s' % pid_file)
765 return 1
766 pid = live_pidfile(pid_file)
767 if not pid:
768 print(f'PID {pid} in {pid_file} is not running')
769 return 1
770 print('Server running in PID %s' % pid)
771 return 0
772
773 def restart_with_reloader(self):
774 self.restart_with_monitor(reloader=True)
775
776 def restart_with_monitor(self, reloader=False):
777 if self.verbose > 0:
778 if reloader:
779 print('Starting subprocess with file monitor')
780 else:
781 print('Starting subprocess with monitor parent')
782 while 1:
783 args = [self.quote_first_command_arg(sys.executable)] + sys.argv
784 new_environ = os.environ.copy()
785 if reloader:
786 new_environ[self._reloader_environ_key] = 'true'
787 else:
788 new_environ[self._monitor_environ_key] = 'true'
789 proc = None
790 try:
791 try:
792 _turn_sigterm_into_systemexit()
793 proc = subprocess.Popen(args, env=new_environ)
794 exit_code = proc.wait()
795 proc = None
796 except KeyboardInterrupt:
797 print('^C caught in monitor process')
798 if self.verbose > 1:
799 raise
800 return 1
801 finally:
802 if proc is not None and hasattr(os, 'kill'):
803 try:
804 os.kill(proc.pid, signal.SIGTERM)
805 except OSError:
806 pass
807
808 if reloader:
809 # Reloader always exits with code 3; but if we are
810 # a monitor, any exit code will restart
811 if exit_code != 3:
812 return exit_code
813 if self.verbose > 0:
814 print('-' * 20, 'Restarting', '-' * 20)
815
816 def change_user_group(self, user, group):
817 if not user and not group:
818 return
819 uid = gid = None
820 if group:
821 try:
822 gid = int(group)
823 group = grp.getgrgid(gid).gr_name
824 except ValueError:
825 try:
826 entry = grp.getgrnam(group)
827 except KeyError:
828 raise BadCommand(
829 "Bad group: %r; no such group exists" % group)
830 gid = entry.gr_gid
831 try:
832 uid = int(user)
833 user = pwd.getpwuid(uid).pw_name
834 except ValueError:
835 try:
836 entry = pwd.getpwnam(user)
837 except KeyError:
838 raise BadCommand(
839 "Bad username: %r; no such user exists" % user)
840 if not gid:
841 gid = entry.pw_gid
842 uid = entry.pw_uid
843 if self.verbose > 0:
844 print('Changing user to {}:{} ({}:{})'.format(
845 user, group or '(unknown)', uid, gid))
846 if hasattr(os, 'initgroups'):
847 os.initgroups(user, gid)
848 else:
849 os.setgroups([e.gr_gid for e in grp.getgrall()
850 if user in e.gr_mem] + [gid])
851 if gid:
852 os.setgid(gid)
853 if uid:
854 os.setuid(uid)
855
856
857 class LazyWriter:
858
859 """
860 File-like object that opens a file lazily when it is first written
861 to.
862 """
863
864 def __init__(self, filename, mode='w'):
865 self.filename = filename
866 self.fileobj = None
867 self.lock = threading.Lock()
868 self.mode = mode
869
870 def open(self):
871 if self.fileobj is None:
872 self.lock.acquire()
873 try:
874 if self.fileobj is None:
875 self.fileobj = open(self.filename, self.mode)
876 finally:
877 self.lock.release()
878 return self.fileobj
879
880 def write(self, text):
881 fileobj = self.open()
882 fileobj.write(text)
883 fileobj.flush()
884
885 def writelines(self, text):
886 fileobj = self.open()
887 fileobj.writelines(text)
888 fileobj.flush()
889
890 def flush(self):
891 self.open().flush()
892
893
894 def live_pidfile(pidfile):
895 """(pidfile:str) -> int | None
896 Returns an int found in the named file, if there is one,
897 and if there is a running process with that process id.
898 Return None if no such process exists.
899 """
900 pid = read_pidfile(pidfile)
901 if pid:
902 try:
903 os.kill(int(pid), 0)
904 return pid
905 except OSError as e:
906 if e.errno == errno.EPERM:
907 return pid
908 return None
909
910
911 def read_pidfile(filename):
912 if os.path.exists(filename):
913 try:
914 f = open(filename)
915 content = f.read()
916 f.close()
917 return int(content.strip())
918 except (ValueError, OSError):
919 return None
920 else:
921 return None
922
923
924 def _remove_pid_file(written_pid, filename, verbosity):
925 current_pid = os.getpid()
926 if written_pid != current_pid:
927 # A forked process must be exiting, not the process that
928 # wrote the PID file
929 return
930 if not os.path.exists(filename):
931 return
932 f = open(filename)
933 content = f.read().strip()
934 f.close()
935 try:
936 pid_in_file = int(content)
937 except ValueError:
938 pass
939 else:
940 if pid_in_file != current_pid:
941 print("PID file {} contains {}, not expected PID {}".format(
942 filename, pid_in_file, current_pid))
943 return
944 if verbosity > 0:
945 print("Removing PID file %s" % filename)
946 try:
947 os.unlink(filename)
948 return
949 except OSError as e:
950 # Record, but don't give traceback
951 print("Cannot remove PID file: %s" % e)
952 # well, at least lets not leave the invalid PID around...
953 try:
954 f = open(filename, 'w')
955 f.write('')
956 f.close()
957 except OSError as e:
958 print(f'Stale PID left in file: {filename} ({e:e})')
959 else:
960 print('Stale PID removed')
961
962
963 def ensure_port_cleanup(bound_addresses, maxtries=30, sleeptime=2):
964 """
965 This makes sure any open ports are closed.
966
967 Does this by connecting to them until they give connection
968 refused. Servers should call like::
969
970 import paste.script
971 ensure_port_cleanup([80, 443])
972 """
973 atexit.register(_cleanup_ports, bound_addresses, maxtries=maxtries,
974 sleeptime=sleeptime)
975
976
977 def _cleanup_ports(bound_addresses, maxtries=30, sleeptime=2):
978 # Wait for the server to bind to the port.
979 for bound_address in bound_addresses:
980 for _i in range(maxtries):
981 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
982 try:
983 sock.connect(bound_address)
984 except OSError as e:
985 if e.errno != errno.ECONNREFUSED:
986 raise
987 break
988 else:
989 time.sleep(sleeptime)
990 else:
991 raise SystemExit('Timeout waiting for port.')
992 sock.close()
993
994
995 def _turn_sigterm_into_systemexit():
996 """
997 Attempts to turn a SIGTERM exception into a SystemExit exception.
998 """
999
1000 def handle_term(signo, frame):
1001 raise SystemExit
1002 signal.signal(signal.SIGTERM, handle_term)
1003
1004
1005 # ---- from paste.script.command --------------------------------------
1006 python_version = sys.version.splitlines()[0].strip()
1007
1008 parser = optparse.OptionParser(add_help_option=False,
1009 # version='%s from %s (python %s)'
1010 # % (dist, dist.location, python_version),
1011 usage='%prog [paster_options] COMMAND [command_options]')
1012
1013 parser.add_option(
1014 '-h', '--help',
1015 action='store_true',
1016 dest='do_help',
1017 help="Show this help message")
1018 parser.disable_interspersed_args()
1019
1020 # @@: Add an option to run this in another Python interpreter
1021
1022 commands = {
1023 'serve': ServeCommand
1024 }
1025
1026
1027 def run(args=None):
1028 if (not args and len(sys.argv) >= 2 and os.environ.get('_') and
1029 sys.argv[0] != os.environ['_'] and os.environ['_'] == sys.argv[1]):
1030 # probably it's an exe execution
1031 args = ['exe', os.environ['_']] + sys.argv[2:]
1032 if args is None:
1033 args = sys.argv[1:]
1034 options, args = parser.parse_args(args)
1035 options.base_parser = parser
1036 if options.do_help:
1037 args = ['help'] + args
1038 if not args:
1039 print('Usage: %s COMMAND' % sys.argv[0])
1040 args = ['help']
1041 command_name = args[0]
1042 if command_name not in commands:
1043 command = NotFoundCommand
1044 else:
1045 command = commands[command_name]
1046 invoke(command, command_name, options, args[1:])
1047
1048
1049 def invoke(command, command_name, options, args):
1050 try:
1051 runner = command(command_name)
1052 exit_code = runner.run(args)
1053 except BadCommand as e:
1054 print(e)
1055 exit_code = e.exit_code
1056 sys.exit(exit_code)