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"""
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
37NEED_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"
42
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  """
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
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()
198