Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python3
      2 #
      3 # Copyright 2017, The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #     http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 """
     18 A python program that simulates the plugin side of the dt_fd_forward transport for testing.
     19 
     20 This program will invoke a given java language runtime program and send down debugging arguments
     21 that cause it to use the dt_fd_forward transport. This will then create a normal server-port that
     22 debuggers can attach to.
     23 """
     24 
     25 import argparse
     26 import array
     27 from multiprocessing import Process
     28 import contextlib
     29 import ctypes
     30 import os
     31 import select
     32 import socket
     33 import subprocess
     34 import sys
     35 import time
     36 
     37 NEED_HANDSHAKE_MESSAGE = b"HANDSHAKE:REQD\x00"
     38 LISTEN_START_MESSAGE   = b"dt_fd_forward:START-LISTEN\x00"
     39 LISTEN_END_MESSAGE     = b"dt_fd_forward:END-LISTEN\x00"
     40 ACCEPTED_MESSAGE       = b"dt_fd_forward:ACCEPTED\x00"
     41 CLOSE_MESSAGE          = b"dt_fd_forward:CLOSING\x00"
     42 
     43 libc = ctypes.cdll.LoadLibrary("libc.so.6")
     44 def eventfd(init_val, flags):
     45   """
     46   Creates an eventfd. See 'man 2 eventfd' for more information.
     47   """
     48   return libc.eventfd(init_val, flags)
     49 
     50 @contextlib.contextmanager
     51 def make_eventfd(init):
     52   """
     53   Creates an eventfd with given initial value that is closed after the manager finishes.
     54   """
     55   fd = eventfd(init, 0)
     56   yield fd
     57   os.close(fd)
     58 
     59 @contextlib.contextmanager
     60 def make_sockets():
     61   """
     62   Make a (remote,local) socket pair. The remote socket is inheritable by forked processes. They are
     63   both linked together.
     64   """
     65   (rfd, lfd) = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
     66   yield (rfd, lfd)
     67   rfd.close()
     68   lfd.close()
     69 
     70 def send_fds(sock, remote_read, remote_write, remote_event):
     71   """
     72   Send the three fds over the given socket.
     73   """
     74   sock.sendmsg([NEED_HANDSHAKE_MESSAGE],  # We want the transport to handle the handshake.
     75                [(socket.SOL_SOCKET,  # Send over socket.
     76                  socket.SCM_RIGHTS,  # Payload is file-descriptor array
     77                  array.array('i', [remote_read, remote_write, remote_event]))])
     78 
     79 
     80 def HandleSockets(host, port, local_sock, finish_event):
     81   """
     82   Handle the IO between the network and the runtime.
     83 
     84   This is similar to what we will do with the plugin that controls the jdwp connection.
     85 
     86   The main difference is it will keep around the connection and event-fd in order to let it send
     87   ddms packets directly.
     88   """
     89   listening = False
     90   with socket.socket() as sock:
     91     sock.bind((host, port))
     92     sock.listen()
     93     while True:
     94       sources = [local_sock, finish_event, sock]
     95       print("Starting select on " + str(sources))
     96       (rf, _, _) = select.select(sources, [], [])
     97       if local_sock in rf:
     98         buf = local_sock.recv(1024)
     99         print("Local_sock has data: " + str(buf))
    100         if buf == LISTEN_START_MESSAGE:
    101           print("listening on " + str(sock))
    102           listening = True
    103         elif buf == LISTEN_END_MESSAGE:
    104           print("End listening")
    105           listening = False
    106         elif buf == ACCEPTED_MESSAGE:
    107           print("Fds were accepted.")
    108         elif buf == CLOSE_MESSAGE:
    109           # TODO Dup the fds and send a fake DDMS message like the actual plugin would.
    110           print("Fds were closed")
    111         else:
    112           print("Unknown data received from socket " + str(buf))
    113           return
    114       elif sock in rf:
    115         (conn, addr) = sock.accept()
    116         with conn:
    117           print("connection accepted from " + str(addr))
    118           if listening:
    119             with make_eventfd(1) as efd:
    120               print("sending fds ({}, {}, {}) to target.".format(conn.fileno(), conn.fileno(), efd))
    121               send_fds(local_sock, conn.fileno(), conn.fileno(), efd)
    122           else:
    123             print("Closing fds since we cannot accept them.")
    124       if finish_event in rf:
    125         print("woke up from finish_event")
    126         return
    127 
    128 def StartChildProcess(cmd_pre, cmd_post, jdwp_lib, jdwp_ops, remote_sock, can_be_runtest):
    129   """
    130   Open the child java-language runtime process.
    131   """
    132   full_cmd = list(cmd_pre)
    133   os.set_inheritable(remote_sock.fileno(), True)
    134   jdwp_arg = jdwp_lib + "=" + \
    135              jdwp_ops + "transport=dt_fd_forward,address=" + str(remote_sock.fileno())
    136   if can_be_runtest and cmd_pre[0].endswith("run-test"):
    137     print("Assuming run-test. Pass --no-run-test if this isn't true")
    138     full_cmd += ["--with-agent", jdwp_arg]
    139   else:
    140     full_cmd.append("-agentpath:" + jdwp_arg)
    141   full_cmd += cmd_post
    142   print("Running " + str(full_cmd))
    143   # Start the actual process with the fd being passed down.
    144   proc = subprocess.Popen(full_cmd, close_fds=False)
    145   # Get rid of the extra socket.
    146   remote_sock.close()
    147   proc.wait()
    148 
    149 def main():
    150   parser = argparse.ArgumentParser(description="""
    151                                    Runs a socket that forwards to dt_fds.
    152 
    153                                    Pass '--' to start passing in the program we will pass the debug
    154                                    options down to.
    155                                    """)
    156   parser.add_argument("--host", type=str, default="localhost",
    157                       help="Host we will listen for traffic on. Defaults to 'localhost'.")
    158   parser.add_argument("--debug-lib", type=str, default="libjdwp.so",
    159                       help="jdwp library we pass to -agentpath:. Default is 'libjdwp.so'")
    160   parser.add_argument("--debug-options", type=str, default="server=y,suspend=y,",
    161                       help="non-address options we pass to jdwp agent, default is " +
    162                            "'server=y,suspend=y,'")
    163   parser.add_argument("--port", type=int, default=12345,
    164                       help="port we will expose the traffic on. Defaults to 12345.")
    165   parser.add_argument("--no-run-test", default=False, action="store_true",
    166                       help="don't pass in arguments for run-test even if it looks like that is " +
    167                            "the program")
    168   parser.add_argument("--pre-end", type=int, default=1,
    169                       help="number of 'rest' arguments to put before passing in the debug options")
    170   end_idx = 0 if '--' not in sys.argv else sys.argv.index('--')
    171   if end_idx == 0 and ('--help' in sys.argv or '-h' in sys.argv):
    172     parser.print_help()
    173     return
    174   args = parser.parse_args(sys.argv[:end_idx][1:])
    175   rest = sys.argv[1 + end_idx:]
    176 
    177   with make_eventfd(0) as wakeup_event:
    178     with make_sockets() as (remote_sock, local_sock):
    179       invoker = Process(target=StartChildProcess,
    180                         args=(rest[:args.pre_end],
    181                               rest[args.pre_end:],
    182                               args.debug_lib,
    183                               args.debug_options,
    184                               remote_sock,
    185                               not args.no_run_test))
    186       socket_handler = Process(target=HandleSockets,
    187                                args=(args.host, args.port, local_sock, wakeup_event))
    188       socket_handler.start()
    189       invoker.start()
    190     invoker.join()
    191     # Write any 64 bit value to the wakeup_event to make sure that the socket handler will wake
    192     # up and exit.
    193     os.write(wakeup_event, b'\x00\x00\x00\x00\x00\x00\x01\x00')
    194     socket_handler.join()
    195 
    196 if __name__ == '__main__':
    197   main()
    198