blob: 1f9c41fc26f21e3157df0f20ebc9d825a0e767e7 [file] [log] [blame]
Alex Light39795712017-12-14 11:58:21 -08001#!/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"""
18A python program that simulates the plugin side of the dt_fd_forward transport for testing.
19
20This program will invoke a given java language runtime program and send down debugging arguments
21that cause it to use the dt_fd_forward transport. This will then create a normal server-port that
22debuggers can attach to.
23"""
24
25import argparse
26import array
27from multiprocessing import Process
28import contextlib
29import ctypes
30import os
31import select
32import socket
33import subprocess
34import sys
35import time
36
Alex Light15b81132018-01-24 13:29:07 -080037NEED_HANDSHAKE_MESSAGE = b"HANDSHAKE:REQD\x00"
38LISTEN_START_MESSAGE = b"dt_fd_forward:START-LISTEN\x00"
39LISTEN_END_MESSAGE = b"dt_fd_forward:END-LISTEN\x00"
40ACCEPTED_MESSAGE = b"dt_fd_forward:ACCEPTED\x00"
41CLOSE_MESSAGE = b"dt_fd_forward:CLOSING\x00"
Alex Light39795712017-12-14 11:58:21 -080042
43libc = ctypes.cdll.LoadLibrary("libc.so.6")
44def 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
51def 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
60def 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
70def send_fds(sock, remote_read, remote_write, remote_event):
71 """
72 Send the three fds over the given socket.
73 """
Alex Light15b81132018-01-24 13:29:07 -080074 sock.sendmsg([NEED_HANDSHAKE_MESSAGE], # We want the transport to handle the handshake.
Alex Light39795712017-12-14 11:58:21 -080075 [(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
80def 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
128def 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
149def 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
196if __name__ == '__main__':
197 main()