"Fossies" - the Fresh Open Source Software Archive

Member "polysh-polysh-0.13/polysh/main.py" (11 May 2020, 9715 Bytes) of package /linux/privat/polysh-polysh-0.13.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "main.py" see the Fossies "Dox" file reference documentation.

    1 """Polysh - Main Utilities
    2 
    3 Copyright (c) 2006 Guillaume Chazarain <guichaz@gmail.com>
    4 Copyright (c) 2018 InnoGames GmbH
    5 """
    6 # This program is free software: you can redistribute it and/or modify
    7 # it under the terms of the GNU General Public License as published by
    8 # the Free Software Foundation, either version 2 of the License, or
    9 # (at your option) any later version.
   10 #
   11 # This program is distributed in the hope that it will be useful,
   12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14 # GNU General Public License for more details.
   15 #
   16 # You should have received a copy of the GNU General Public License
   17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
   18 
   19 import asyncore
   20 import atexit
   21 import getpass
   22 import locale
   23 import argparse
   24 import os
   25 import signal
   26 import sys
   27 import termios
   28 import readline
   29 import resource
   30 from typing import Callable, List
   31 
   32 from polysh import remote_dispatcher
   33 from polysh import dispatchers
   34 from polysh import stdin
   35 from polysh.console import console_output
   36 from polysh.host_syntax import expand_syntax
   37 from polysh import control_commands
   38 from polysh import VERSION
   39 
   40 
   41 def kill_all() -> None:
   42     """When polysh quits, we kill all the remote shells we started"""
   43     for i in dispatchers.all_instances():
   44         try:
   45             os.kill(-i.pid, signal.SIGKILL)
   46         except OSError:
   47             # The process was already dead, no problem
   48             pass
   49 
   50 
   51 def parse_cmdline() -> argparse.Namespace:
   52     usage = '%s [OPTIONS] HOSTS...\n' % (sys.argv[0]) + \
   53             'Control commands are prefixed by ":".'
   54     parser = argparse.ArgumentParser(usage)
   55     parser.add_argument(
   56         '--hosts-file', type=str, action='append',
   57         dest='hosts_filenames', metavar='FILE', default=[],
   58         help='read hostnames from given file, one per line')
   59     parser.add_argument(
   60         '--command', type=str, dest='command', default=None,
   61         help='command to execute on the remote shells',
   62         metavar='CMD')
   63     def_ssh = 'exec ssh -oLogLevel=Quiet -t %(host)s %(port)s'
   64     parser.add_argument(
   65         '--ssh', type=str, dest='ssh', default=def_ssh,
   66         metavar='SSH', help='ssh command to use [%s]' % def_ssh)
   67     parser.add_argument(
   68         '--user', type=str, dest='user', default=None,
   69         help='remote user to log in as', metavar='USER')
   70     parser.add_argument(
   71         '--no-color', action='store_true', dest='disable_color',
   72         help='disable colored hostnames [enabled]')
   73     parser.add_argument(
   74         '--password-file', type=str, dest='password_file',
   75         default=None, metavar='FILE',
   76         help='read a password from the specified file. - is the tty.')
   77     parser.add_argument(
   78         '--log-file', type=str, dest='log_file',
   79         help='file to log each machine conversation [none]')
   80     parser.add_argument(
   81         '--abort-errors', action='store_true', dest='abort_error',
   82         help='abort if some shell fails to initialize [ignore]')
   83     parser.add_argument(
   84         '--debug', action='store_true', dest='debug',
   85         help='print debugging information')
   86     parser.add_argument(
   87         '--profile', action='store_true', dest='profile',
   88         default=False)
   89     parser.add_argument('host_names', nargs='*')
   90     args = parser.parse_args()
   91 
   92     for filename in args.hosts_filenames:
   93         try:
   94             hosts_file = open(filename, 'r')
   95             for line in hosts_file.readlines():
   96                 if '#' in line:
   97                     line = line[:line.index('#')]
   98                 line = line.strip()
   99                 if line:
  100                     args.host_names.append(line)
  101             hosts_file.close()
  102         except IOError as e:
  103             parser.error(str(e))
  104 
  105     if args.log_file:
  106         try:
  107             args.log_file = open(args.log_file, 'a')
  108         except IOError as e:
  109             print(e)
  110             sys.exit(1)
  111 
  112     if not args.host_names:
  113         parser.error('no hosts given')
  114 
  115     if args.password_file == '-':
  116         args.password = getpass.getpass()
  117     elif args.password_file is not None:
  118         password_file = open(args.password_file, 'r')
  119         args.password = password_file.readline().rstrip('\n')
  120     else:
  121         args.password = None
  122 
  123     return args
  124 
  125 
  126 def find_non_interactive_command(command: str) -> str:
  127     if sys.stdin.isatty():
  128         return command
  129 
  130     stdin = sys.stdin.read()
  131     if stdin and command:
  132         print(
  133             '--command and reading from stdin are incompatible',
  134             file=sys.stderr,
  135         )
  136         sys.exit(1)
  137     if stdin and not stdin.endswith('\n'):
  138         stdin += '\n'
  139     return command or stdin
  140 
  141 
  142 def init_history(histfile: str) -> None:
  143     if hasattr(readline, "read_history_file"):
  144         try:
  145             readline.read_history_file(histfile)
  146         except IOError:
  147             pass
  148 
  149 
  150 def save_history(histfile: str) -> None:
  151     readline.set_history_length(1000)
  152     readline.write_history_file(histfile)
  153 
  154 
  155 def loop(interactive: bool) -> None:
  156     histfile = os.path.expanduser("~/.polysh_history")
  157     init_history(histfile)
  158     next_signal = None
  159     last_status = None
  160     while True:
  161         try:
  162             if next_signal:
  163                 current_signal = next_signal
  164                 next_signal = None
  165                 sig2chr = {signal.SIGINT: 'C', signal.SIGTSTP: 'Z'}
  166                 ctrl = sig2chr[current_signal]
  167                 remote_dispatcher.log('> ^{}\n'.format(ctrl).encode())
  168                 control_commands.do_send_ctrl(ctrl)
  169                 console_output(b'')
  170                 stdin.the_stdin_thread.prepend_text = None
  171             while dispatchers.count_awaited_processes()[0] and \
  172                     remote_dispatcher.main_loop_iteration(timeout=0.2):
  173                 pass
  174             # Now it's quiet
  175             for r in dispatchers.all_instances():
  176                 r.print_unfinished_line()
  177             current_status = dispatchers.count_awaited_processes()
  178             if current_status != last_status:
  179                 console_output(b'')
  180             if remote_dispatcher.options.interactive:
  181                 stdin.the_stdin_thread.want_raw_input()
  182             last_status = current_status
  183             if dispatchers.all_terminated():
  184                 # Clear the prompt
  185                 console_output(b'')
  186                 raise asyncore.ExitNow(remote_dispatcher.options.exit_code)
  187             if not next_signal:
  188                 # possible race here with the signal handler
  189                 remote_dispatcher.main_loop_iteration()
  190         except KeyboardInterrupt:
  191             if interactive:
  192                 next_signal = signal.SIGINT
  193             else:
  194                 kill_all()
  195                 os.kill(0, signal.SIGINT)
  196         except asyncore.ExitNow as e:
  197             console_output(b'')
  198             save_history(histfile)
  199             sys.exit(e.args[0])
  200 
  201 
  202 def _profile(continuation: Callable) -> None:
  203     prof_file = 'polysh.prof'
  204     import cProfile
  205     import pstats
  206     print('Profiling using cProfile')
  207     cProfile.runctx('continuation()', globals(), locals(), prof_file)
  208     stats = pstats.Stats(prof_file)
  209     stats.strip_dirs()
  210     stats.sort_stats('time', 'calls')
  211     stats.print_stats(50)
  212     stats.print_callees(50)
  213     os.remove(prof_file)
  214 
  215 
  216 def restore_tty_on_exit() -> None:
  217     fd = sys.stdin.fileno()
  218     old = termios.tcgetattr(fd)
  219     atexit.register(lambda: termios.tcsetattr(fd, termios.TCSADRAIN, old))
  220 
  221 
  222 def run() -> None:
  223     """Launch polysh"""
  224     locale.setlocale(locale.LC_ALL, '')
  225     atexit.register(kill_all)
  226     signal.signal(signal.SIGPIPE, signal.SIG_DFL)
  227 
  228     args = parse_cmdline()
  229 
  230     args.command = find_non_interactive_command(args.command)
  231     args.exit_code = 0
  232     args.interactive = (
  233         not args.command
  234         and sys.stdin.isatty()
  235         and sys.stdout.isatty())
  236     if args.interactive:
  237         restore_tty_on_exit()
  238 
  239     remote_dispatcher.options = args
  240 
  241     hosts = []  # type: List[str]
  242     for host in args.host_names:
  243         hosts.extend(expand_syntax(host))
  244 
  245     try:
  246         # stdin, stdout, stderr for polysh and each ssh connection
  247         new_soft = 3 + len(hosts) * 3
  248         old_soft, old_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
  249         if new_soft > old_soft:
  250             # We are allowed to change the soft limit as we please but must be
  251             # root to change the hard limit.
  252             new_hard = max(new_soft, old_hard)
  253             resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, new_hard))
  254     except OSError as e:
  255         print(
  256             'Failed to change RLIMIT_NOFILE from soft={} hard={} to soft={} '
  257             'hard={}: {}'.format(old_soft, old_hard, new_soft, new_hard, e),
  258             file=sys.stderr,
  259         )
  260         sys.exit(1)
  261 
  262     dispatchers.create_remote_dispatchers(hosts)
  263 
  264     signal.signal(signal.SIGWINCH, lambda signum, frame:
  265                   dispatchers.update_terminal_size())
  266 
  267     stdin.the_stdin_thread = stdin.StdinThread(args.interactive)
  268 
  269     if args.profile:
  270         def safe_loop() -> None:
  271             try:
  272                 loop(args.interactive)
  273             except BaseException:
  274                 pass
  275         _profile(safe_loop)
  276     else:
  277         loop(args.interactive)
  278 
  279 
  280 def main():
  281     """Wrapper around run() to setup sentry"""
  282 
  283     sentry_dsn = os.environ.get('POLYSH_SENTRY_DSN')
  284 
  285     if sentry_dsn:
  286         from raven import Client
  287         client = Client(
  288             dsn=sentry_dsn,
  289             release='.'.join(map(str, VERSION)),
  290             ignore_exceptions=[
  291                 KeyboardInterrupt
  292             ]
  293         )
  294 
  295         try:
  296             run()
  297         except Exception:
  298             client.captureException()
  299 
  300     else:
  301         run()