• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Camelcade / Perl5-IDEA / #525521583

06 Jun 2025 03:47PM UTC coverage: 82.349% (+0.02%) from 82.33%
#525521583

push

github

hurricup
Qodana baseline update

30862 of 37477 relevant lines covered (82.35%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.5
/plugin/debugger/src/main/java/com/perl5/lang/perl/debugger/PerlDebugThread.java
1
/*
2
 * Copyright 2015-2025 Alexandr Evstigneev
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 * http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package com.perl5.lang.perl.debugger;
18

19
import com.google.gson.*;
20
import com.intellij.execution.ExecutionException;
21
import com.intellij.execution.ExecutionResult;
22
import com.intellij.execution.actions.StopProcessAction;
23
import com.intellij.execution.ui.ConsoleView;
24
import com.intellij.execution.ui.ConsoleViewContentType;
25
import com.intellij.notification.Notification;
26
import com.intellij.notification.NotificationType;
27
import com.intellij.notification.Notifications;
28
import com.intellij.openapi.application.ReadAction;
29
import com.intellij.openapi.application.WriteAction;
30
import com.intellij.openapi.diagnostic.Logger;
31
import com.intellij.openapi.project.Project;
32
import com.intellij.openapi.vfs.VirtualFile;
33
import com.intellij.util.TimeoutUtil;
34
import com.intellij.util.concurrency.Semaphore;
35
import com.intellij.xdebugger.XDebugSession;
36
import com.intellij.xdebugger.impl.XDebugSessionImpl;
37
import com.perl5.lang.perl.debugger.breakpoints.PerlLineBreakPointDescriptor;
38
import com.perl5.lang.perl.debugger.protocol.*;
39
import com.perl5.lang.perl.debugger.run.run.debugger.PerlDebugProfileStateBase;
40
import com.perl5.lang.perl.debugger.run.run.debugger.remote.PerlRemoteDebuggingConfiguration;
41
import com.perl5.lang.perl.debugger.ui.PerlScriptsPanel;
42
import com.perl5.lang.perl.idea.project.PerlProjectManager;
43
import com.perl5.lang.perl.idea.run.debugger.PerlDebugOptions;
44
import com.perl5.lang.perl.util.PerlRunUtil;
45
import it.unimi.dsi.fastutil.bytes.ByteArrayList;
46
import org.jetbrains.annotations.NotNull;
47
import org.jetbrains.annotations.Nullable;
48
import org.jetbrains.annotations.PropertyKey;
49

50
import java.io.IOException;
51
import java.io.InputStream;
52
import java.io.OutputStream;
53
import java.lang.reflect.Modifier;
54
import java.net.ConnectException;
55
import java.net.InetAddress;
56
import java.net.ServerSocket;
57
import java.net.Socket;
58
import java.nio.charset.StandardCharsets;
59
import java.util.Collections;
60
import java.util.List;
61
import java.util.Objects;
62
import java.util.concurrent.ConcurrentHashMap;
63
import java.util.concurrent.CopyOnWriteArrayList;
64
import java.util.concurrent.ExecutorService;
65
import java.util.concurrent.Executors;
66
import java.util.concurrent.atomic.AtomicBoolean;
67
import java.util.concurrent.locks.ReentrantLock;
68

69
import static com.perl5.lang.perl.debugger.PerlDebuggerBundleKt.PATH_TO_BUNDLE;
70
import static com.perl5.lang.perl.debugger.protocol.PerlDebuggingEventReady.MODULE_VERSION_PREFIX;
71
import static com.perl5.lang.perl.debugger.run.run.debugger.PerlDebugProfileState.DEBUG_PACKAGE;
72

73

74
public class PerlDebugThread extends Thread {
75
  private static final Logger LOG = Logger.getInstance(PerlDebugThread.class);
1✔
76
  private final ExecutorService myExecutor = Executors.newSingleThreadExecutor();
1✔
77
  private final ExecutionResult myExecutionResult;
78
  private final Gson myGson;
79
  private final PerlDebugProfileStateBase myDebugProfileState;
80
  private final PerlScriptsPanel myScriptListPanel;
81
  private final PerlScriptsPanel myEvalsListPanel;
82
  private final XDebugSession mySession;
83
  private Socket mySocket;
84
  private ServerSocket myServerSocket;
85
  private OutputStream myOutputStream;
86
  private InputStream myInputStream;
87
  private final AtomicBoolean myStop = new AtomicBoolean(false);
1✔
88
  private final List<PerlLineBreakPointDescriptor> breakpointsDescriptorsQueue = new CopyOnWriteArrayList<>();
1✔
89
  private boolean isReady = false;
1✔
90
  private int transactionId = 0;
1✔
91
  private final ConcurrentHashMap<Integer, PerlDebuggingTransactionHandler> transactionsMap =
1✔
92
    new ConcurrentHashMap<>();
93
  private final ReentrantLock lock = new ReentrantLock();
1✔
94
  private final PerlRemoteFileSystem myPerlRemoteFileSystem = PerlRemoteFileSystem.getInstance();
1✔
95
  private final PerlDebugOptions myPerlDebugOptions;
96

97
  public PerlDebugThread(XDebugSession session, PerlDebugProfileStateBase state, ExecutionResult executionResult) {
98
    super("PerlDebugThread");
1✔
99
    mySession = session;
1✔
100
    myGson = createGson();
1✔
101
    myDebugProfileState = state;
1✔
102
    myExecutionResult = executionResult;
1✔
103
    myScriptListPanel = new PerlScriptsPanel(session.getProject(), this);
1✔
104
    myEvalsListPanel = new PerlScriptsPanel(session.getProject(), this);
1✔
105
    myPerlDebugOptions = state.getDebugOptions();
1✔
106
  }
1✔
107

108
  public void queueLineBreakpointDescriptor(PerlLineBreakPointDescriptor descriptor) {
109
    if (descriptor != null) {
1✔
110
      // fixme potentially risk of race condition between clear and add
111
      breakpointsDescriptorsQueue.add(descriptor);
1✔
112
      if (isReady) {
1✔
113
        sendQueuedBreakpoints();
1✔
114
      }
115
    }
116
  }
1✔
117

118
  protected void sendQueuedBreakpoints() {
119
    sendCommand("b", breakpointsDescriptorsQueue);
1✔
120
    breakpointsDescriptorsQueue.clear();
1✔
121
  }
1✔
122

123
  protected void setUpDebugger() {
124
    PerlSetUpDescriptor perlSetUpDescriptor = new PerlSetUpDescriptor(breakpointsDescriptorsQueue, myDebugProfileState.getDebugOptions());
1✔
125
    sendString(myGson.toJson(perlSetUpDescriptor));
1✔
126
    breakpointsDescriptorsQueue.clear();
1✔
127
  }
1✔
128

129
  private void print(@NotNull @PropertyKey(resourceBundle = PATH_TO_BUNDLE) String key, @NotNull Object... params) {
130
    String textToPrint = PerlDebuggerBundle.message(key, params);
1✔
131
    LOG.debug("Printing: ", textToPrint);
1✔
132
    ((ConsoleView)myExecutionResult.getExecutionConsole()).print(
1✔
133
      textToPrint + "\n", ConsoleViewContentType.SYSTEM_OUTPUT);
134
  }
1✔
135

136
  private void prepareAndConnect() throws ExecutionException, IOException {
137
    myScriptListPanel.clear();
1✔
138
    myEvalsListPanel.clear();
1✔
139
    WriteAction.runAndWait(myPerlRemoteFileSystem::dropFiles);
1✔
140

141
    int debugPort = myDebugProfileState.getDebugPort();
1✔
142
    String debugName;
143
    if (myPerlDebugOptions.getPerlRole().equals(PerlDebugOptions.ROLE_SERVER)) {
1✔
144
      String hostToConnect = myPerlDebugOptions.getHostToConnect();
1✔
145
      debugName = hostToConnect + ":" + debugPort;
1✔
146
      while (!myStop.get() && !PerlDebugProfileStateBase.isReadyForConnection(myExecutionResult.getProcessHandler())) {
1✔
147
        print("perl.debug.waiting.start");
1✔
148
        TimeoutUtil.sleep(1000);
1✔
149
      }
150

151
      if (myStop.get()) {
1✔
152
        return;
1✔
153
      }
154
      print("perl.debug.connecting.to", debugName);
1✔
155
      for (int i = 1; i < 11 && !myStop.get(); i++) {
1✔
156
        try {
157
          mySocket = new Socket(hostToConnect, debugPort);
1✔
158
          break;
1✔
159
        }
160
        catch (ConnectException e) {
1✔
161
          if (i == 10) {
1✔
162
            throw e;
×
163
          }
164
          LOG.info("Connection error: " + e.getMessage());
1✔
165
          print("perl.debug.attempting.again");
1✔
166
          TimeoutUtil.sleep(1000);
1✔
167
        }
168
      }
169
    }
1✔
170
    else {
171
      String hostToBind = myPerlDebugOptions.getHostToBind();
×
172
      debugName = hostToBind + ":" + debugPort;
×
173
      print("perl.debug.listening.on", debugName);
×
174
      myServerSocket = new ServerSocket(debugPort, 50, InetAddress.getByName(hostToBind));
×
175
      mySocket = myServerSocket.accept();
×
176
    }
177
  }
1✔
178

179
  /**
180
   * @return true iff we've reached end of stream (interrupted from the script's side)
181
   */
182
  private boolean doRun() {
183
    try {
184
      prepareAndConnect();
1✔
185
      if (myStop.get()) {
1✔
186
        LOG.debug("Can't start debugger event loop because of stop flag");
1✔
187
        return false;
1✔
188
      }
189
      print("perl.debug.connected");
1✔
190

191
      myOutputStream = mySocket.getOutputStream();
1✔
192
      myInputStream = mySocket.getInputStream();
1✔
193

194
      ByteArrayList response = new ByteArrayList();
1✔
195

196
      while (!myStop.get()) {
1✔
197
        response.clear();
1✔
198

199
        LOG.debug("Reading data from the debugger");
1✔
200

201
        // reading bytes
202
        while (myInputStream != null) {
1✔
203
          int dataByte = myInputStream.read();
1✔
204
          if (dataByte == '\n') {
1✔
205
            break;
1✔
206
          }
207
          else if (dataByte == -1) {
1✔
208
            LOG.debug("Stop debugger event loop because dataByte is -1");
1✔
209
            return true;
1✔
210
          }
211
          else {
212
            response.add((byte)dataByte);
1✔
213
          }
214
        }
1✔
215

216
        processResponse(response);
1✔
217
      }
218
      LOG.debug("Reading loop was stopped by myStop=", myStop);
×
219
    }
220
    catch (IOException | ExecutionException e) {
1✔
221
      LOG.warn(e);
1✔
222
    }
×
223
    return false;
1✔
224
  }
225

226
  @Override
227
  public void run() {
228
    try {
229
      while (doRun() && myPerlDebugOptions.isReconnect()) {
1✔
230
        print("perl.debug.reconnecting");
1✔
231
        closeStreamsAndSockets();
1✔
232
        isReady = false;
1✔
233
        ((XDebugSessionImpl)mySession).reset();
1✔
234
        ReadAction.run(mySession::initBreakpoints);
1✔
235
      }
236
    }
237
    finally {
238
      LOG.debug("Stopping process from run");
1✔
239
      setStop();
1✔
240
    }
241
  }
1✔
242

243
  private void processResponse(ByteArrayList responseBytes) {
244
    final String response = new String(responseBytes.toByteArray(), StandardCharsets.UTF_8);
1✔
245
    LOG.debug("Got response: ", response);
1✔
246

247
    try {
248
      final PerlDebuggingEvent newEvent = myGson.fromJson(response, PerlDebuggingEvent.class);
1✔
249

250
      if (newEvent != null) {
1✔
251
        if (newEvent instanceof PerlDebuggingEventReady debuggingEventReady) {
1✔
252
          if (debuggingEventReady.isValid()) {
1✔
253
            isReady = true;
1✔
254
            setUpDebugger();
1✔
255
          }
256
          else {
257
            var errorMessage = PerlDebuggerBundle.message(
×
258
              "perl.debugger.incorrect.version.message", DEBUG_PACKAGE, MODULE_VERSION_PREFIX,
259
              debuggingEventReady.version);
260
            Notification notification = new Notification(
×
261
              PerlDebugProcess.PERL_DEBUGGER_NOTIFICATION_GROUP_ID,
262
              PerlDebuggerBundle.message("perl.debugger.incorrect.version.title", DEBUG_PACKAGE),
×
263
              errorMessage,
264
              NotificationType.ERROR
265
            );
266
            Project project = myDebugProfileState.getEnvironment().getProject();
×
267
            if (myPerlDebugOptions instanceof PerlRemoteDebuggingConfiguration) {
×
268
              Notifications.Bus.notify(notification, project);
×
269
            }
270
            else {
271
              PerlRunUtil.addInstallActionsAndShow(
×
272
                project, Objects.requireNonNull(PerlProjectManager.getSdk(project)),
×
273
                Collections.singletonList(DEBUG_PACKAGE),
×
274
                notification);
275
            }
276
            LOG.warn(errorMessage);
×
277
            setStop();
×
278
          }
×
279
        }
280
        else {
281
          newEvent.setDebugSession(mySession);
1✔
282
          newEvent.setDebugThread(this);
1✔
283
          myExecutor.execute(newEvent);
1✔
284
        }
285
      }
286
    }
287
    catch (JsonSyntaxException e) {
×
288
      LOG.error("Error parsing JSON response: " + response, e);
×
289
      print("perl.debug.error.parsing.response");
×
290
      setStop();
×
291
    }
1✔
292
  }
1✔
293

294
  public void sendString(String string) {
295
    if (mySocket == null) {
1✔
296
      return;
×
297
    }
298

299
    string = string + "\n";
1✔
300

301
    try {
302
      LOG.debug("Going to send string " + string);
1✔
303

304
      lock.lock();
1✔
305

306
      LOG.debug("Sent string " + string);
1✔
307

308

309
      myOutputStream.write(string.getBytes(StandardCharsets.UTF_8));
1✔
310
    }
311
    catch (IOException e) {
×
312
      LOG.warn(e);
×
313
    }
314
    finally {
315
      lock.unlock();
1✔
316
    }
317
  }
1✔
318

319
  public void sendCommand(String command, Object data) {
320
    sendString(command + " " + myGson.toJson(data));
1✔
321
  }
1✔
322

323
  public void sendCommandAndGetResponse(String command, Object data, PerlDebuggingTransactionHandler transactionHandler) {
324
    if (mySocket == null) {
1✔
325
      return;
×
326
    }
327

328
    try {
329
      lock.lock();
1✔
330
      PerlDebuggingTransactionWrapper transaction = new PerlDebuggingTransactionWrapper(transactionId++, data);
1✔
331
      transactionsMap.put(transaction.getTransactionId(), transactionHandler);
1✔
332
      sendCommand(command, transaction);
1✔
333
    }
334
    finally {
335
      lock.unlock();
1✔
336
    }
337
  }
1✔
338

339
  public Socket getSocket() {
340
    return mySocket;
×
341
  }
342

343
  public void setStop() {
344
    if (!myStop.compareAndSet(false, true)) {
1✔
345
      return;
1✔
346
    }
347
    closeStreamsAndSockets();
1✔
348
    myExecutor.shutdownNow();
1✔
349
    StopProcessAction.stopProcess(myExecutionResult.getProcessHandler());
1✔
350

351
    print("perl.debug.disconnected");
1✔
352
  }
1✔
353

354
  private void closeStreamsAndSockets() {
355
    //noinspection Duplicates
356
    try {
357
      if (myInputStream != null) {
1✔
358
        myInputStream.close();
1✔
359
        myInputStream = null;
1✔
360
      }
361
    }
362
    catch (IOException e) {
×
363
      LOG.warn(e);
×
364
    }
1✔
365

366
    //noinspection Duplicates
367
    try {
368
      if (myOutputStream != null) {
1✔
369
        myOutputStream.close();
1✔
370
        myOutputStream = null;
1✔
371
      }
372
    }
373
    catch (IOException e) {
×
374
      LOG.warn(e);
×
375
    }
1✔
376

377
    //noinspection Duplicates
378
    try {
379
      if (mySocket != null) {
1✔
380
        mySocket.close();
1✔
381
        mySocket = null;
1✔
382
      }
383
    }
384
    catch (IOException e) {
×
385
      LOG.warn(e);
×
386
    }
1✔
387
    try {
388
      if (myServerSocket != null) {
1✔
389
        myServerSocket.close();
×
390
        myServerSocket.close();
×
391
      }
392
    }
393
    catch (IOException e) {
×
394
      LOG.warn(e);
×
395
    }
1✔
396
  }
1✔
397

398
  protected Gson createGson() {
399
    GsonBuilder builder = new GsonBuilder();
1✔
400
    builder.registerTypeAdapter(PerlDebuggingEvent.class, new PerlDebuggingEventsDeserializer(this));
1✔
401
    return builder.excludeFieldsWithModifiers(Modifier.TRANSIENT).create();
1✔
402
  }
403

404
  public @Nullable PerlDebuggingTransactionHandler getTransactionHandler(int transactionId) {
405
    return transactionsMap.remove(transactionId);
1✔
406
  }
407

408
  public PerlScriptsPanel getScriptListPanel() {
409
    return myScriptListPanel;
1✔
410
  }
411

412
  public PerlScriptsPanel getEvalsListPanel() {
413
    return myEvalsListPanel;
1✔
414
  }
415

416
  public @Nullable VirtualFile loadRemoteSource(String filePath) {
417
    LOG.debug("Loading file ", filePath);
1✔
418
    final Semaphore responseSemaphore = new Semaphore();
1✔
419
    responseSemaphore.down();
1✔
420

421
    final String[] response = new String[]{"# Source could not be loaded..."};
1✔
422

423
    PerlDebuggingTransactionHandler perlDebuggingTransactionHandler = new PerlDebuggingTransactionHandler() {
1✔
424

425
      @Override
426
      public void run(JsonObject eventObject, JsonDeserializationContext jsonDeserializationContext) {
427
        response[0] = eventObject.getAsJsonPrimitive("data").getAsString();
1✔
428
        responseSemaphore.up();
1✔
429
      }
1✔
430
    };
431

432
    if (mySocket != null) {
1✔
433
      sendCommandAndGetResponse("get_source", new PerlSourceRequestDescriptor(filePath), perlDebuggingTransactionHandler);
1✔
434
      responseSemaphore.waitFor(2000);
1✔
435
    }
436

437
    return myPerlRemoteFileSystem.registerRemoteFile(filePath, response[0]);
1✔
438
  }
439

440
  public PerlDebugProfileStateBase getDebugProfileState() {
441
    return myDebugProfileState;
1✔
442
  }
443
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc