c7816252d8376a5a4022259b099529c5e37097bf
[idea/community.git] / python / pydevSrc / com / jetbrains / python / debugger / pydev / transport / ClientModeDebuggerTransport.java
1 package com.jetbrains.python.debugger.pydev.transport;
2
3 import com.intellij.openapi.application.ApplicationManager;
4 import com.intellij.openapi.diagnostic.Logger;
5 import com.intellij.openapi.vfs.CharsetToolkit;
6 import com.jetbrains.python.debugger.PyDebuggerException;
7 import com.jetbrains.python.debugger.pydev.AbstractCommand;
8 import com.jetbrains.python.debugger.pydev.ClientModeMultiProcessDebugger;
9 import com.jetbrains.python.debugger.pydev.RemoteDebugger;
10 import org.jetbrains.annotations.NotNull;
11 import org.jetbrains.annotations.Nullable;
12
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.io.OutputStream;
16 import java.net.ConnectException;
17 import java.net.InetSocketAddress;
18 import java.net.Socket;
19 import java.util.concurrent.*;
20
21 /**
22  * {@link DebuggerTransport} implementation that expects a debugging script to behave as a server. The main process of the debugging script
23  * and all of the Python processes forked and created within it receives incoming connections on the <b>same port</b> using
24  * {@code SO_REUSEPORT} server socket option on Linux (available since 3.9 core version), Mac OS, BSD platforms and {@code SO_REUSEADDR}
25  * server socket option on Windows platforms (see {@code start_server(port)} method in <i>pydevd_comm.py</i>).
26  * <p>
27  * Each Python process within the debugging script requires <b>single connection</b> from the IDE. When a new Python process is created the
28  * originator process sends {@link AbstractCommand#PROCESS_CREATED} message to the IDE. The new process binds server socket to the same
29  * address and port as the originator process and starts listening for an incoming connection. The IDE tries to establish a new
30  * connection with the script.
31  * <p>
32  * At the last point the following problem could arise. When several processes are created almost simultaneously and they become
33  * bound to the single port the IDE could establish the connection to some of the processes twice or more times. The first connection is
34  * accepted by the Python process but the others are not. Other connections would stay in <i>completed connection queue</i> until a timeout
35  * for a response for the {@link AbstractCommand#RUN} arouse. To solve this problem {@link ClientModeDebuggerTransport} has several
36  * states. The transport is created with {@link State#INIT}. After the connection is established the transport object is transferred to
37  * {@link State#CONNECTED}. After the first message is received from the debugging script the transport is transferred to
38  * {@link State#APPROVED}. After the transport is transferred to {@link State#CONNECTED} a task is scheduled in
39  * {@link ClientModeDebuggerTransport#CHECK_CONNECTION_APPROVED_DELAY} to check if the state has been changed to {@link State#APPROVED}. If
40  * the state had been changed then the current connection is kept and the normal communication with debugging script is performed. If the
41  * state has not been changed for this period of time the transport tries to reconnect and schedules the check again.
42  *
43  * @author Alexander Koshevoy
44  * @see ClientModeMultiProcessDebugger
45  */
46 public class ClientModeDebuggerTransport extends BaseDebuggerTransport {
47   private static final Logger LOG = Logger.getInstance(ClientModeDebuggerTransport.class);
48
49   private static final int MAX_CONNECTION_TRIES = 20;
50   private static final long CHECK_CONNECTION_APPROVED_DELAY = 1000L;
51   private static final long SLEEP_TIME_BETWEEN_CONNECTION_TRIES = 150L;
52
53   @NotNull private final String myHost;
54   private final int myPort;
55
56   @NotNull private volatile State myState = State.INIT;
57
58   @Nullable private Socket mySocket;
59   @Nullable private volatile DebuggerReader myDebuggerReader;
60
61   public ClientModeDebuggerTransport(@NotNull RemoteDebugger debugger,
62                                      @NotNull String host,
63                                      int port) {
64     super(debugger);
65     myHost = host;
66     myPort = port;
67   }
68
69   @Override
70   public void waitForConnect() throws IOException {
71     if (myState != State.INIT) {
72       throw new IllegalStateException(
73         "Inappropriate state of Python debugger for connecting to Python debugger: " + myState + "; " + State.INIT + " is expected");
74     }
75
76     doConnect();
77   }
78
79   private void doConnect() throws IOException {
80     synchronized (mySocketObject) {
81       if (mySocket != null) {
82         try {
83           mySocket.close();
84         }
85         catch (IOException e) {
86           LOG.debug("Failed to close previously opened socket", e);
87         }
88         finally {
89           mySocket = null;
90         }
91       }
92     }
93
94     int i = 0;
95     boolean connected = false;
96     while (!connected && i < MAX_CONNECTION_TRIES) {
97       i++;
98
99       int attempt = i;
100       LOG.debug(String.format("[%d] Trying to connect: #%d attempt", hashCode(), attempt));
101
102       try {
103         Socket clientSocket = new Socket();
104         clientSocket.setSoTimeout(0);
105         clientSocket.connect(new InetSocketAddress(myHost, myPort));
106
107         synchronized (mySocketObject) {
108           mySocket = clientSocket;
109           myState = State.CONNECTED;
110         }
111
112         try {
113           InputStream stream;
114           synchronized (mySocketObject) {
115             stream = mySocket.getInputStream();
116           }
117           myDebuggerReader = new DebuggerReader(myDebugger, stream);
118         }
119         catch (IOException e) {
120           LOG.debug("Failed to create debugger reader", e);
121           throw e;
122         }
123
124         CountDownLatch beforeHandshake = new CountDownLatch(1);
125         Future<Boolean> future = ApplicationManager.getApplication().executeOnPooledThread(() -> {
126           beforeHandshake.countDown();
127           try {
128             myDebugger.handshake();
129             return true;
130           }
131           catch (PyDebuggerException e) {
132             LOG.debug(String.format("[%d] Handshake failed: #%d attempt", hashCode(), attempt));
133             return false;
134           }
135         });
136         try {
137           beforeHandshake.await();
138           connected = future.get(CHECK_CONNECTION_APPROVED_DELAY, TimeUnit.MILLISECONDS);
139         }
140         catch (InterruptedException e) {
141           LOG.debug(String.format("[%d] Waiting for handshake interrupted: #%d attempt", hashCode(), attempt), e);
142           myDebuggerReader.close();
143           throw new IOException("Waiting for subprocess interrupted", e);
144         }
145         catch (ExecutionException e) {
146           LOG.debug(String.format("[%d] Execution exception occurred: #%d attempt", hashCode(), attempt), e);
147         }
148         catch (TimeoutException e) {
149           LOG.debug(String.format("[%d] Timeout: #%d attempt", hashCode(), attempt), e);
150           future.cancel(true);
151         }
152
153         if (!connected) {
154           myDebuggerReader.close();
155           try {
156             Thread.sleep(SLEEP_TIME_BETWEEN_CONNECTION_TRIES);
157           }
158           catch (InterruptedException e) {
159             throw new IOException(e);
160           }
161         }
162       }
163       catch (ConnectException e) {
164         if (i < MAX_CONNECTION_TRIES) {
165           try {
166             Thread.sleep(SLEEP_TIME_BETWEEN_CONNECTION_TRIES);
167           }
168           catch (InterruptedException e1) {
169             throw new IOException(e1);
170           }
171         }
172       }
173     }
174
175     if (!connected) {
176       myState = State.DISCONNECTED;
177       throw new IOException("Failed to connect to debugging script");
178     }
179
180     myState = State.APPROVED;
181     LOG.debug(String.format("[%d] Connected to Python debugger script on #%d attempt", hashCode(), i));
182   }
183
184   @Override
185   protected boolean sendMessageImpl(byte[] packed) throws IOException {
186     synchronized (mySocketObject) {
187       if (mySocket == null || mySocket.isClosed()) {
188         return false;
189       }
190       final OutputStream os = mySocket.getOutputStream();
191       os.write(packed);
192       os.flush();
193       return true;
194     }
195   }
196
197   @Override
198   public void close() {
199     try {
200       DebuggerReader debuggerReader = myDebuggerReader;
201       if (debuggerReader != null) {
202         debuggerReader.stop();
203       }
204     }
205     finally {
206       synchronized (mySocketObject) {
207         if (mySocket != null) {
208           try {
209             mySocket.close();
210           }
211           catch (IOException ignored) {
212           }
213         }
214       }
215     }
216   }
217
218   @Override
219   public boolean isConnected() {
220     return myState == State.APPROVED;
221   }
222
223   @Override
224   public void disconnect() {
225     // TODO disconnect?
226   }
227
228   private enum State {
229     /**
230      * Before calling {@link #waitForConnect()}
231      */
232     INIT,
233     /**
234      * Socket connection to the debugger host:port address established and no messages has been received from the debugging script yet.
235      * The connection might be ephemeral at this point (see {@link ClientModeDebuggerTransport}).
236      */
237     CONNECTED,
238     /**
239      * Socket connection to the debugger host:port address established and at least one message has been received from the debugging script.
240      * This state means that a script is on the other end had accepted the connection.
241      */
242     APPROVED,
243     /**
244      * Debugger disconnected
245      */
246     DISCONNECTED
247   }
248
249   public class DebuggerReader extends BaseDebuggerReader {
250     public DebuggerReader(@NotNull RemoteDebugger debugger, @NotNull InputStream stream) throws IOException {
251       super(stream, CharsetToolkit.UTF8_CHARSET, debugger); //TODO: correct encoding?
252       start(getClass().getName());
253     }
254
255     @Override
256     protected void onExit() {
257       if (myState == State.APPROVED) {
258         getDebugger().fireExitEvent();
259       }
260     }
261
262     @Override
263     protected void onCommunicationError() {
264       if (myState == State.APPROVED) {
265         getDebugger().fireCommunicationError();
266       }
267     }
268   }
269 }