#!/usr/bin/env python3 # # Copyright 2017, The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ A python program that simulates the plugin side of the dt_fd_forward transport for testing. This program will invoke a given java language runtime program and send down debugging arguments that cause it to use the dt_fd_forward transport. This will then create a normal server-port that debuggers can attach to. """ import argparse import array from multiprocessing import Process import contextlib import ctypes import os import select import socket import subprocess import sys import time NEED_HANDSHAKE_MESSAGE = b"HANDSHAKE:REQD\x00" LISTEN_START_MESSAGE = b"dt_fd_forward:START-LISTEN\x00" LISTEN_END_MESSAGE = b"dt_fd_forward:END-LISTEN\x00" ACCEPTED_MESSAGE = b"dt_fd_forward:ACCEPTED\x00" HANDSHAKEN_MESSAGE = b"dt_fd_forward:HANDSHAKE-COMPLETE\x00" CLOSE_MESSAGE = b"dt_fd_forward:CLOSING\x00" libc = ctypes.cdll.LoadLibrary("libc.so.6") def eventfd(init_val, flags): """ Creates an eventfd. See 'man 2 eventfd' for more information. """ return libc.eventfd(init_val, flags) @contextlib.contextmanager def make_eventfd(init): """ Creates an eventfd with given initial value that is closed after the manager finishes. """ fd = eventfd(init, 0) yield fd os.close(fd) @contextlib.contextmanager def make_sockets(): """ Make a (remote,local) socket pair. The remote socket is inheritable by forked processes. They are both linked together. """ (rfd, lfd) = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET) yield (rfd, lfd) rfd.close() lfd.close() def send_fds(sock, remote_read, remote_write, remote_event): """ Send the three fds over the given socket. """ sock.sendmsg([NEED_HANDSHAKE_MESSAGE], # We want the transport to handle the handshake. [(socket.SOL_SOCKET, # Send over socket. socket.SCM_RIGHTS, # Payload is file-descriptor array array.array('i', [remote_read, remote_write, remote_event]))]) def HandleSockets(host, port, local_sock, finish_event): """ Handle the IO between the network and the runtime. This is similar to what we will do with the plugin that controls the jdwp connection. The main difference is it will keep around the connection and event-fd in order to let it send ddms packets directly. """ listening = False with socket.socket() as sock: sock.bind((host, port)) sock.listen() while True: sources = [local_sock, finish_event, sock] print("Starting select on " + str(sources)) (rf, _, _) = select.select(sources, [], []) if local_sock in rf: buf = local_sock.recv(1024) print("Local_sock has data: " + str(buf)) if buf == LISTEN_START_MESSAGE: print("listening on " + str(sock)) listening = True elif buf == LISTEN_END_MESSAGE: print("End listening") listening = False elif buf == ACCEPTED_MESSAGE: print("Fds were accepted.") elif buf == HANDSHAKEN_MESSAGE: print("Handshake completed.") elif buf == CLOSE_MESSAGE: # TODO Dup the fds and send a fake DDMS message like the actual plugin would. print("Fds were closed") else: print("Unknown data received from socket " + str(buf)) return elif sock in rf: (conn, addr) = sock.accept() with conn: print("connection accepted from " + str(addr)) if listening: with make_eventfd(1) as efd: print("sending fds ({}, {}, {}) to target.".format(conn.fileno(), conn.fileno(), efd)) send_fds(local_sock, conn.fileno(), conn.fileno(), efd) else: print("Closing fds since we cannot accept them.") if finish_event in rf: print("woke up from finish_event") return def StartChildProcess(cmd_pre, cmd_post, jdwp_lib, jdwp_ops, remote_sock, can_be_runtest): """ Open the child java-language runtime process. """ full_cmd = list(cmd_pre) os.set_inheritable(remote_sock.fileno(), True) jdwp_arg = jdwp_lib + "=" + \ jdwp_ops + "transport=dt_fd_forward,address=" + str(remote_sock.fileno()) if can_be_runtest and cmd_pre[0].endswith("run-test"): print("Assuming run-test. Pass --no-run-test if this isn't true") full_cmd += ["--with-agent", jdwp_arg] else: full_cmd.append("-agentpath:" + jdwp_arg) full_cmd += cmd_post print("Running " + str(full_cmd)) # Start the actual process with the fd being passed down. proc = subprocess.Popen(full_cmd, close_fds=False) # Get rid of the extra socket. remote_sock.close() proc.wait() def main(): parser = argparse.ArgumentParser(description=""" Runs a socket that forwards to dt_fds. Pass '--' to start passing in the program we will pass the debug options down to. """) parser.add_argument("--host", type=str, default="localhost", help="Host we will listen for traffic on. Defaults to 'localhost'.") parser.add_argument("--debug-lib", type=str, default="libjdwp.so", help="jdwp library we pass to -agentpath:. Default is 'libjdwp.so'") parser.add_argument("--debug-options", type=str, default="server=y,suspend=y,", help="non-address options we pass to jdwp agent, default is " + "'server=y,suspend=y,'") parser.add_argument("--port", type=int, default=12345, help="port we will expose the traffic on. Defaults to 12345.") parser.add_argument("--no-run-test", default=False, action="store_true", help="don't pass in arguments for run-test even if it looks like that is " + "the program") parser.add_argument("--pre-end", type=int, default=1, help="number of 'rest' arguments to put before passing in the debug options") end_idx = 0 if '--' not in sys.argv else sys.argv.index('--') if end_idx == 0 and ('--help' in sys.argv or '-h' in sys.argv): parser.print_help() return args = parser.parse_args(sys.argv[:end_idx][1:]) rest = sys.argv[1 + end_idx:] with make_eventfd(0) as wakeup_event: with make_sockets() as (remote_sock, local_sock): invoker = Process(target=StartChildProcess, args=(rest[:args.pre_end], rest[args.pre_end:], args.debug_lib, args.debug_options, remote_sock, not args.no_run_test)) socket_handler = Process(target=HandleSockets, args=(args.host, args.port, local_sock, wakeup_event)) socket_handler.start() invoker.start() invoker.join() # Write any 64 bit value to the wakeup_event to make sure that the socket handler will wake # up and exit. os.write(wakeup_event, b'\x00\x00\x00\x00\x00\x00\x01\x00') socket_handler.join() if __name__ == '__main__': main()