text: # curio/monitor.py
  #
  # Debugging monitor for curio. To enable the monitor, create a kernel
  # and then attach a monitor to it, like this:
  #
  #    k = Kernel()
  #    Monitor(k)
  #
  # If using the run() function, you can do this:
  #
  #    run(coro, with_monitor=True)
  #
  # run() also looks for the CURIOMONITOR environment variable
  #
  #    env CURIOMONITOR=TRUE python3 someprog.py
  #
  # If you need to change some aspect of the monitor configuration, you
  # can do manual setup:
  #
  #    k = Kernel()
  #    Monitor(k, host, port)
  #
  # Where host and port configure the network address on which the monitor
  # operates.
  #
  # To connect to the monitor, run python3 -m curio.monitor -H [host] -p [port]. For example:
  #
  # Theory of operation:
  # --------------------
  # The monitor works by opening up a loopback socket on the local
  # machine and allowing connections via telnet. By default, it only
  # allows a connection originating from the local machine.  Only a
  # single monitor connection is allowed at any given time.
  #
  # There are two parts to the monitor itself: a user interface and an
  # internal loop that runs on curio itself.  The user interface part
  # runs in a completely separate execution thread.  The reason for this
  # is that it allows curio to be monitored even if the curio kernel is
  # completely deadlocked, occupied with a large CPU-bound task, or
  # otherwise hosed in the some way.  At a minimum, you can connect,
  # look at the task table, and see what the tasks are doing.  
  #
  # The internal monitor loop implemented on curio itself is presently
  # used to implement external task cancellation.  Manipulating any part
  # of the kernel state or task status is unsafe from an outside thread.
  # To make it safe, the user-interface thread of the monitor hands over
  # requests requiring the involvement of the kernel to the monitor
  # loop.  Since this loop runs on curio, it can safely make
  # cancellation requests and perform other kernel-related actions.
  
  import os
  import signal
  import time
  import socket
  import threading
  import telnetlib
  import argparse
  import logging
  
  # --- Curio
  from .task import Task
  from . import queue
  
  # ---
  log = logging.getLogger(__name__)
  
  MONITOR_HOST = '127.0.0.1'
  MONITOR_PORT = 48802
  
  class Monitor(object):
      '''
      Task monitor that runs concurrently to the curio kernel in a
      separate thread. This can watch the kernel and provide debugging.
      '''
  
      def __init__(self, kern, host=MONITOR_HOST, port=MONITOR_PORT):
          self.kernel = kern
          self.address = (host, port)
          self.monitor_queue = queue.UniversalQueue()
                                                 
          log.info('Starting Curio monitor at %s:%d', host, port)
  
          # The monitor launches both a separate thread and helper task
          # that runs inside curio itself to manage cancellation events
          self._ui_thread = threading.Thread(target=self.server, args=(), daemon=True)
          self._closing = threading.Event()
          self._ui_thread.start()
  
          monitor_task = Task(self.monitor_task())
          monitor_task.daemon = True
          kern._ready.append(monitor_task)
          kern._tasks[monitor_task.id] = monitor_task
  
      def close(self):
          self._closing.set()
          self._ui_thread.join()
  
      async def monitor_task(self):
          '''
          Asynchronous task loop for carrying out task cancellation.
          '''
          while True:
              task = await self.monitor_queue.get()
              await task.cancel()
  
      def server(self):
          '''
          Synchronous kernel for the monitor.  This runs in a separate thread
          from curio itself.
          '''
          sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  
          # set the timeout to prevent the server loop from
          # blocking indefinitaly on sock.accept()
          sock.settimeout(0.5)
          sock.bind(self.address)
          sock.listen(1)
          with sock:
              while not self._closing.is_set():
                  try:
                      client, addr = sock.accept()
                      with client:
                          client.settimeout(0.5)
  
                          # This bit of magic is for reading lines of input while still allowing timeouts
                          # and the ability for the monitor to die when curio exits.  See Issue #108.
                          def readlines():
                              buffer = bytearray()
                              while not self._closing.is_set():
                                  index = buffer.find(b'\n')
                                  if index >= 0:
                                      line = buffer[:index + 1].decode('latin-1')
                                      del buffer[:index + 1]
                                      yield line
                                  try:
                                      chunk = client.recv(1000)
                                      if not chunk:
                                          break
                                      buffer.extend(chunk)
                                  except socket.timeout:
                                      pass
  
                          sout = client.makefile('w', encoding='latin-1')
                          self.interactive_loop(sout, readlines())
                  except socket.timeout:
                      continue
  
      def interactive_loop(self, sout, input_lines):
          '''
          Main interactive loop of the monitor
          '''
          sout.write('\nCurio Monitor: %d tasks running\n' % len(self.kernel._tasks))
          sout.write('Type help for commands\n')
          while True:
              sout.write('curio > ')
              sout.flush()
              resp = next(input_lines, None)
              if not resp:
                  return
              try:
                  if resp.startswith('q'):
                      self.command_exit(sout)
                      return
  
                  elif resp.startswith('pa'):
                      _, taskid_s = resp.split()
                      self.command_parents(sout, int(taskid_s))
  
                  elif resp.startswith('p'):
                      self.command_ps(sout)
  
                  elif resp.startswith('exit'):
                      self.command_exit(sout)
                      return
  
                  elif resp.startswith('cancel'):
                      _, taskid_s = resp.split()
                      self.command_cancel(sout, int(taskid_s))
  
                  elif resp.startswith('signal'):
                      _, signame = resp.split()
                      self.command_signal(sout, signame)
  
                  elif resp.startswith('w'):
                      _, taskid_s = resp.split()
                      self.command_where(sout, int(taskid_s))
  
                  elif resp.startswith('h'):
                      self.command_help(sout)
                  else:
                      sout.write('Unknown command. Type help.\n')
              except Exception as e:
                  sout.write('Bad command. %s\n' % e)
  
      def command_help(self, sout):
          sout.write(
              '''Commands:
           ps               : Show task table
           where taskid     : Show stack frames for a task
           cancel taskid    : Cancel an indicated task
           signal signame   : Send a Unix signal
           parents taskid   : List task parents
           quit             : Leave the monitor
  ''')
  
      def command_ps(self, sout):
          headers = ('Task', 'State', 'Cycles', 'Timeout', 'Sleep', 'Task')
          widths = (6, 12, 10, 7, 7, 50)
          for h, w in zip(headers, widths):
              sout.write('%-*s ' % (w, h))
          sout.write('\n')
          sout.write(' '.join(w * '-' for w in widths))
          sout.write('\n')
          timestamp = time.monotonic()
          for taskid in sorted(self.kernel._tasks):
              task = self.kernel._tasks.get(taskid)
              if task:
                  timeout_remaining = format(
                      (task.timeout - timestamp),
                      '0.6f')[:7] if task.timeout else 'None'
                  sleep_remaining = format(
                      (task.sleep - timestamp),
                      '0.6f')[:7] if task.sleep else 'None'
  
                  sout.write('%-*d %-*s %-*d %-*s %-*s %-*s\n' % (widths[0], taskid,
                                                                  widths[1], task.state,
                                                                  widths[2], task.cycles,
                                                                  widths[3], timeout_remaining,
                                                                  widths[4], sleep_remaining,
                                                                  widths[5], task.name))
  
      def command_where(self, sout, taskid):
          task = self.kernel._tasks.get(taskid)
          if task:
              sout.write(task.traceback() + '\n')
          else:
              sout.write('No task %d\n' % taskid)
  
      def command_signal(self, sout, signame):
          if hasattr(signal, signame):
              os.kill(os.getpid(), getattr(signal, signame))
          else:
              sout.write('Unknown signal %s\n' % signame)
  
      def command_cancel(self, sout, taskid):
          task = self.kernel._tasks.get(taskid)
          if task:
              sout.write('Cancelling task %d\n' % taskid)
              self.monitor_queue.put(task)
  
      def command_parents(self, sout, taskid):
          while taskid:
              task = self.kernel._tasks.get(taskid)
              if task:
                  sout.write('%-6d %12s %s\n' % (task.id, task.state, task.name))
                  taskid = task.parentid
              else:
                  break
  
      def command_exit(self, sout):
          sout.write('Leaving monitor. Hit Ctrl-C to exit\n')
          sout.flush()
  
  
  def monitor_client(host, port):
      '''
      Client to connect to the monitor via "telnet"
      '''
      tn = telnetlib.Telnet()
      tn.open(host, port, timeout=0.5)
      try:
          tn.interact()
      except KeyboardInterrupt:
          pass
      finally:
          tn.close()
  
  
  def main():
      parser = argparse.ArgumentParser("usage: python -m curio.monitor [options]")
      parser.add_argument("-H", "--host", dest="monitor_host",
                          default=MONITOR_HOST, type=str,
                          help="monitor host ip")
  
      parser.add_argument("-p", "--port", dest="monitor_port",
                          default=MONITOR_PORT, type=int,
                          help="monitor port number")
      args = parser.parse_args()
      monitor_client(args.monitor_host, args.monitor_port)
  
  
  if __name__ == '__main__':
      main()
  