Alex Light | 3979571 | 2017-12-14 11:58:21 -0800 | [diff] [blame] | 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 | |
Alex Light | 15b8113 | 2018-01-24 13:29:07 -0800 | [diff] [blame] | 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" |
Alex Light | 3979571 | 2017-12-14 11:58:21 -0800 | [diff] [blame] | 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 | """ |
Alex Light | 15b8113 | 2018-01-24 13:29:07 -0800 | [diff] [blame] | 74 | sock.sendmsg([NEED_HANDSHAKE_MESSAGE], # We want the transport to handle the handshake. |
Alex Light | 3979571 | 2017-12-14 11:58:21 -0800 | [diff] [blame] | 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() |