"Fossies" - the Fresh Open Source Software Archive

Member "polysh-polysh-0.13/polysh/stdin.py" (11 May 2020, 10076 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 "stdin.py" see the Fossies "Dox" file reference documentation.

    1 """Polysh - Standard Input Routines
    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 errno
   21 import os
   22 import readline  # Just to say we want to use it with raw_input
   23 import signal
   24 import socket
   25 import subprocess
   26 import sys
   27 import tempfile
   28 import termios
   29 from threading import Thread, Event, Lock
   30 
   31 from polysh import dispatchers, remote_dispatcher
   32 from polysh.console import console_output, set_last_status_length
   33 from polysh import completion
   34 from typing import Optional
   35 
   36 the_stdin_thread = None  # type: StdinThread
   37 
   38 
   39 class InputBuffer(object):
   40     """The shared input buffer between the main thread and the stdin thread"""
   41 
   42     def __init__(self) -> None:
   43         self.lock = Lock()
   44         self.buf = b''
   45 
   46     def add(self, data: bytes) -> None:
   47         """Add data to the buffer"""
   48         with self.lock:
   49             self.buf += data
   50 
   51     def get(self) -> bytes:
   52         """Get the content of the buffer"""
   53         data = b''
   54         with self.lock:
   55             data, self.buf = self.buf, b''
   56 
   57         return data
   58 
   59 
   60 def process_input_buffer() -> None:
   61     """Send the content of the input buffer to all remote processes, this must
   62     be called in the main thread"""
   63     from polysh.control_commands_helpers import handle_control_command
   64     data = the_stdin_thread.input_buffer.get()
   65     remote_dispatcher.log(b'> ' + data)
   66 
   67     if data.startswith(b':'):
   68         try:
   69             handle_control_command(data[1:-1].decode())
   70         except UnicodeDecodeError as e:
   71             console_output(b'Could not decode command.')
   72         return
   73 
   74     if data.startswith(b'!'):
   75         try:
   76             retcode = subprocess.call(data[1:], shell=True)
   77         except OSError as e:
   78             if e.errno == errno.EINTR:
   79                 console_output(b'Child was interrupted\n')
   80                 retcode = 0
   81             else:
   82                 raise
   83         if retcode > 128 and retcode <= 192:
   84             retcode = 128 - retcode
   85         if retcode > 0:
   86             console_output('Child returned {:d}\n'.format(retcode).encode())
   87         elif retcode < 0:
   88             console_output('Child was terminated by signal {:d}\n'.format(
   89                 -retcode).encode())
   90         return
   91 
   92     for r in dispatchers.all_instances():
   93         try:
   94             r.dispatch_command(data)
   95         except asyncore.ExitNow as e:
   96             raise e
   97         except Exception as msg:
   98             raise msg
   99             console_output('{} for {}, disconnecting\n'.format(
  100                 str(msg), r.display_name).encode())
  101             r.disconnect()
  102         else:
  103             if r.enabled and r.state is remote_dispatcher.STATE_IDLE:
  104                 r.change_state(remote_dispatcher.STATE_RUNNING)
  105 
  106 # The stdin thread uses a synchronous (with ACK) socket to communicate with the
  107 # main thread, which is most of the time waiting in the poll() loop.
  108 # Socket character protocol:
  109 # d: there is new data to send
  110 # A: ACK, same reply for every message, communications are synchronous, so the
  111 # stdin thread sends a character to the socket, the main thread processes it,
  112 # sends the ACK, and the stdin thread can go on.
  113 
  114 
  115 class SocketNotificationReader(asyncore.dispatcher):
  116     """The socket reader in the main thread"""
  117 
  118     def __init__(self, the_stdin_thread: 'StdinThread') -> None:
  119         asyncore.dispatcher.__init__(self, the_stdin_thread.socket_read)
  120 
  121     def _do(self, c: bytes) -> None:
  122         if c == b'd':
  123             process_input_buffer()
  124         else:
  125             raise Exception('Unknown code: %s' % (c))
  126 
  127     def handle_read(self) -> None:
  128         """Handle all the available character commands in the socket"""
  129         while True:
  130             try:
  131                 c = self.recv(1)
  132             except socket.error as e:
  133                 if e.errno == errno.EWOULDBLOCK:
  134                     return
  135                 else:
  136                     raise
  137             else:
  138                 self._do(c)
  139                 self.socket.setblocking(True)
  140                 self.send(b'A')
  141                 self.socket.setblocking(False)
  142 
  143     def writable(self) -> bool:
  144         """Our writes are blocking"""
  145         return False
  146 
  147 
  148 def write_main_socket(c: bytes) -> None:
  149     """Synchronous write to the main socket, wait for ACK"""
  150     the_stdin_thread.socket_write.send(c)
  151     while True:
  152         try:
  153             the_stdin_thread.socket_write.recv(1)
  154         except socket.error as e:
  155             if e.errno != errno.EINTR:
  156                 raise
  157         else:
  158             break
  159 
  160 
  161 #
  162 # This file descriptor is used to interrupt readline in raw_input().
  163 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
  164 # A Ctrl-C seems to make raw_input() return in all cases, and avoids printing
  165 # a newline
  166 tempfile_fd, tempfile_name = tempfile.mkstemp()
  167 os.remove(tempfile_name)
  168 os.write(tempfile_fd, b'\x03')
  169 
  170 
  171 def get_stdin_pid(cached_result: Optional[int] = None) -> int:
  172     """Try to get the PID of the stdin thread, otherwise get the whole process
  173     ID"""
  174     if cached_result is None:
  175         try:
  176             tasks = os.listdir('/proc/self/task')
  177         except OSError as e:
  178             if e.errno != errno.ENOENT:
  179                 raise
  180             cached_result = os.getpid()
  181         else:
  182             tasks.remove(str(os.getpid()))
  183             assert len(tasks) == 1
  184             cached_result = int(tasks[0])
  185     return cached_result
  186 
  187 
  188 def interrupt_stdin_thread() -> None:
  189     """The stdin thread may be in raw_input(), get out of it"""
  190     dupped_stdin = os.dup(0)  # Backup the stdin fd
  191     assert not the_stdin_thread.interrupt_asked  # Sanity check
  192     the_stdin_thread.interrupt_asked = True  # Not user triggered
  193     os.lseek(tempfile_fd, 0, 0)  # Rewind in the temp file
  194     os.dup2(tempfile_fd, 0)  # This will make raw_input() return
  195     pid = get_stdin_pid()
  196     os.kill(pid, signal.SIGWINCH)  # Try harder to wake up raw_input()
  197     the_stdin_thread.out_of_raw_input.wait()  # Wait for this return
  198     the_stdin_thread.interrupt_asked = False  # Restore sanity
  199     os.dup2(dupped_stdin, 0)  # Restore stdin
  200     os.close(dupped_stdin)  # Cleanup
  201 
  202 
  203 echo_enabled = True
  204 
  205 
  206 def set_echo(echo: bool) -> None:
  207     global echo_enabled
  208     if echo != echo_enabled:
  209         fd = sys.stdin.fileno()
  210         attr = termios.tcgetattr(fd)
  211         # The following raises a mypy warning, as python type hints don't allow
  212         # per list item granularity.  The last item in attr is List[bytes], but
  213         # we don't access that here.
  214         if echo:
  215             attr[3] |= termios.ECHO  # type: ignore
  216         else:
  217             attr[3] &= ~termios.ECHO  # type: ignore
  218         termios.tcsetattr(fd, termios.TCSANOW, attr)
  219         echo_enabled = echo
  220 
  221 
  222 class StdinThread(Thread):
  223     """The stdin thread, used to call raw_input()"""
  224 
  225     def __init__(self, interactive: bool) -> None:
  226         Thread.__init__(self, name='stdin thread')
  227         completion.install_completion_handler()
  228         self.input_buffer = InputBuffer()
  229 
  230         if interactive:
  231             self.raw_input_wanted = Event()
  232             self.in_raw_input = Event()
  233             self.out_of_raw_input = Event()
  234             self.out_of_raw_input.set()
  235             s1, s2 = socket.socketpair()
  236             self.socket_read, self.socket_write = s1, s2
  237             self.interrupt_asked = False
  238             self.setDaemon(True)
  239             self.start()
  240             self.socket_notification = SocketNotificationReader(self)
  241             self.prepend_text = None  # type: Optional[str]
  242             readline.set_pre_input_hook(self.prepend_previous_text)
  243 
  244     def prepend_previous_text(self) -> None:
  245         if self.prepend_text:
  246             readline.insert_text(self.prepend_text)
  247             readline.redisplay()
  248             self.prepend_text = None
  249 
  250     def want_raw_input(self) -> None:
  251         nr, total = dispatchers.count_awaited_processes()
  252         if nr:
  253             prompt = 'waiting (%d/%d)> ' % (nr, total)
  254         else:
  255             prompt = 'ready (%d)> ' % total
  256         self.prompt = prompt
  257         set_last_status_length(len(prompt))
  258         self.raw_input_wanted.set()
  259         while not self.in_raw_input.is_set():
  260             self.socket_notification.handle_read()
  261             self.in_raw_input.wait(0.1)
  262         self.raw_input_wanted.clear()
  263 
  264     def no_raw_input(self) -> None:
  265         if not self.out_of_raw_input.is_set():
  266             interrupt_stdin_thread()
  267 
  268     # Beware of races
  269     def run(self) -> None:
  270         while True:
  271             self.raw_input_wanted.wait()
  272             self.out_of_raw_input.set()
  273             self.in_raw_input.set()
  274             self.out_of_raw_input.clear()
  275             cmd = None
  276             try:
  277                 cmd = input(self.prompt)
  278             except EOFError:
  279                 if self.interrupt_asked:
  280                     cmd = readline.get_line_buffer()
  281                 else:
  282                     cmd = chr(4)  # Ctrl-D
  283             if self.interrupt_asked:
  284                 self.prepend_text = cmd
  285                 cmd = None
  286             self.in_raw_input.clear()
  287             self.out_of_raw_input.set()
  288             if cmd:
  289                 if echo_enabled:
  290                     completion.add_to_history(cmd)
  291                 else:
  292                     completion.remove_last_history_item()
  293             set_echo(True)
  294             if cmd is not None:
  295                 self.input_buffer.add('{}\n'.format(cmd).encode())
  296                 write_main_socket(b'd')