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