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