• 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.16
/plugin/core/src/main/java/com/perl5/lang/perl/util/PerlRunUtil.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.util;
18

19
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
20
import com.intellij.execution.ExecutionException;
21
import com.intellij.execution.Executor;
22
import com.intellij.execution.executors.DefaultRunExecutor;
23
import com.intellij.execution.process.ProcessAdapter;
24
import com.intellij.execution.process.ProcessEvent;
25
import com.intellij.execution.process.ProcessHandler;
26
import com.intellij.execution.process.ProcessListener;
27
import com.intellij.execution.ui.ConsoleViewContentType;
28
import com.intellij.execution.ui.RunContentDescriptor;
29
import com.intellij.execution.ui.RunContentManager;
30
import com.intellij.notification.Notification;
31
import com.intellij.notification.NotificationType;
32
import com.intellij.notification.Notifications;
33
import com.intellij.openapi.Disposable;
34
import com.intellij.openapi.actionSystem.AnAction;
35
import com.intellij.openapi.actionSystem.AnActionEvent;
36
import com.intellij.openapi.application.ApplicationManager;
37
import com.intellij.openapi.application.ReadAction;
38
import com.intellij.openapi.application.WriteAction;
39
import com.intellij.openapi.diagnostic.Logger;
40
import com.intellij.openapi.progress.ProgressIndicator;
41
import com.intellij.openapi.progress.ProgressManager;
42
import com.intellij.openapi.progress.Task;
43
import com.intellij.openapi.project.Project;
44
import com.intellij.openapi.project.RootsChangeRescanningInfo;
45
import com.intellij.openapi.projectRoots.Sdk;
46
import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
47
import com.intellij.openapi.util.Disposer;
48
import com.intellij.openapi.util.EmptyRunnable;
49
import com.intellij.openapi.util.Key;
50
import com.intellij.openapi.util.NlsSafe;
51
import com.intellij.openapi.util.text.StringUtil;
52
import com.intellij.openapi.vfs.VfsUtil;
53
import com.intellij.openapi.vfs.VirtualFile;
54
import com.intellij.util.ObjectUtils;
55
import com.intellij.util.concurrency.Semaphore;
56
import com.intellij.util.containers.ContainerUtil;
57
import com.perl5.PerlBundle;
58
import com.perl5.PerlIcons;
59
import com.perl5.lang.perl.adapters.PackageManagerAdapter;
60
import com.perl5.lang.perl.idea.actions.PerlDumbAwareAction;
61
import com.perl5.lang.perl.idea.execution.PerlCommandLine;
62
import com.perl5.lang.perl.idea.execution.PerlTerminalExecutionConsole;
63
import com.perl5.lang.perl.idea.project.PerlProjectManager;
64
import com.perl5.lang.perl.idea.run.PerlRunConsole;
65
import com.perl5.lang.perl.idea.sdk.PerlSdkType;
66
import com.perl5.lang.perl.idea.sdk.host.PerlConsoleView;
67
import com.perl5.lang.perl.idea.sdk.host.PerlHostData;
68
import com.perl5.lang.perl.idea.sdk.host.os.PerlOsHandler;
69
import com.perl5.lang.perl.idea.sdk.versionManager.PerlVersionManagerData;
70
import org.jetbrains.annotations.*;
71

72
import java.io.File;
73
import java.util.*;
74
import java.util.stream.Stream;
75

76

77
public final class PerlRunUtil {
78
  public static final String PERL_I = "-I";
79
  public static final String PERL_LE = "-le";
80
  public static final String PERL_CTRL_X = "print eval chr(0x24).q{^X}";
81
  public static final String PERL5OPT = "PERL5OPT";
82
  private static final Logger LOG = Logger.getInstance(PerlRunUtil.class);
1✔
83
  private static final String MISSING_MODULE_PREFIX = "(you may need to install the ";
84
  private static final String MISSING_MODULE_SUFFIX = " module)";
85
  private static final String LEGACY_MODULE_PREFIX = "Can't locate ";
86
  private static final String LEGACY_MODULE_SUFFIX = " in @INC";
87
  public static final String BUNDLED_MODULE_NAME = "Bundle::Camelcade";
88
  private static final List<RunContentDescriptor> TEST_CONSOLE_DESCRIPTORS = new ArrayList<>();
1✔
89
  private static Semaphore ourTestSdkRefreshSemaphore;
90
  private static Disposable ourTestDisposable;
91
  // should be synchronized with https://github.com/Camelcade/Bundle-Camelcade/blob/master/dist.ini
92
  private static final Set<String> BUNDLED_MODULE_PARTS = Collections.unmodifiableSet(ContainerUtil.newHashSet(
1✔
93
    "App::cpanminus",
94
    "App::Prove::Plugin::PassEnv",
95
    "B::Deparse",
96
    "Config",
97
    PerlPackageUtil.COVERAGE_MODULE,
98
    PerlPackageUtil.DEBUGGER_MODULE,
99
    PerlPackageUtil.PROFILER_MODULE,
100
    "File::Find",
101
    PerlPackageUtil.JSON_MODULE,
102
    "Perl::Critic",
103
    "Perl::Tidy",
104
    PerlPackageUtil.TAP_FORMATTER_MODULE,
105
    PerlPackageUtil.TEST_HARNESS_MODULE
106
  ));
107

108
  private PerlRunUtil() {
109
  }
110

111
  /**
112
   * Builds non-patched perl command line for {@code project}'s sdk (without patching by version manager)
113
   *
114
   * @return command line if perl support for project or scriptFile is enabled
115
   */
116
  public static @Nullable PerlCommandLine getPerlCommandLine(@NotNull Project project,
117
                                                             @Nullable VirtualFile scriptFile,
118
                                                             String... perlParameters) {
119
    return getPerlCommandLine(
1✔
120
      project, PerlProjectManager.getSdk(project, scriptFile), scriptFile, Arrays.asList(perlParameters), Collections.emptyList());
1✔
121
  }
122

123
  public static @Nullable PerlCommandLine getPerlCommandLine(@NotNull Project project,
124
                                                             @Nullable Sdk perlSdk,
125
                                                             @Nullable VirtualFile scriptFile,
126
                                                             @NotNull List<String> perlParameters,
127
                                                             @NotNull List<String> scriptParameters) {
128
    if (perlSdk == null) {
1✔
129
      perlSdk = PerlProjectManager.getSdk(project, scriptFile);
×
130
    }
131
    return getPerlCommandLine(project, perlSdk, ObjectUtils.doIfNotNull(scriptFile, VirtualFile::getPath), perlParameters,
1✔
132
                              scriptParameters);
133
  }
134

135
  public static PerlCommandLine getPerlCommandLine(@NotNull Project project,
136
                                                   @Nullable String localScriptPath) {
137
    return getPerlCommandLine(project, null, localScriptPath, Collections.emptyList(), Collections.emptyList());
1✔
138
  }
139

140
  /**
141
   * Builds non-patched perl command line (without patching by version manager)
142
   *
143
   * @return new perl command line or null if sdk is missing or corrupted
144
   */
145
  public static @Nullable PerlCommandLine getPerlCommandLine(@NotNull Project project,
146
                                                             @Nullable Sdk perlSdk,
147
                                                             @Nullable String localScriptPath,
148
                                                             @NotNull List<String> perlParameters,
149
                                                             @NotNull List<String> scriptParameters) {
150
    if (perlSdk == null) {
1✔
151
      perlSdk = PerlProjectManager.getSdk(project);
1✔
152
    }
153
    if (perlSdk == null) {
1✔
154
      LOG.error("No sdk provided or available in project " + project);
×
155
      return null;
×
156
    }
157
    String interpreterPath = PerlProjectManager.getInterpreterPath(perlSdk);
1✔
158
    if (StringUtil.isEmpty(interpreterPath)) {
1✔
159
      LOG.warn("Empty interpreter path in " + perlSdk + " while building command line for " + localScriptPath);
×
160
      return null;
×
161
    }
162
    PerlCommandLine commandLine = new PerlCommandLine(interpreterPath).withSdk(perlSdk).withProject(project);
1✔
163
    PerlHostData<?, ?> hostData = PerlHostData.notNullFrom(perlSdk);
1✔
164
    commandLine.addParameters(getPerlRunIncludeArguments(hostData, project));
1✔
165

166
    commandLine.addParameters(perlParameters);
1✔
167

168
    if (StringUtil.isNotEmpty(localScriptPath)) {
1✔
169
      String remoteScriptPath = hostData.getRemotePath(localScriptPath);
1✔
170
      if (remoteScriptPath != null) {
1✔
171
        commandLine.addParameter(remoteScriptPath);
1✔
172
      }
173
    }
174

175
    commandLine.addParameters(scriptParameters);
1✔
176

177
    return commandLine;
1✔
178
  }
179

180
  /**
181
   * @return a list with {@code -I} arguments we need to pass to the Perl to include all in-project library directories and external
182
   * libraries configured by user
183
   */
184
  public static @NotNull List<String> getPerlRunIncludeArguments(@NotNull PerlHostData<?, ?> hostData, @NotNull Project project) {
185
    var result = new ArrayList<String>();
1✔
186
    var perlProjectManager = PerlProjectManager.getInstance(project);
1✔
187
    for (VirtualFile libRoot : perlProjectManager.getModulesLibraryRoots()) {
1✔
188
      result.add(PERL_I + hostData.getRemotePath(libRoot.getCanonicalPath()));
1✔
189
    }
1✔
190
    for (VirtualFile libRoot : perlProjectManager.getExternalLibraryRoots()) {
1✔
191
      result.add(PERL_I + hostData.getRemotePath(libRoot.getCanonicalPath()));
1✔
192
    }
1✔
193
    return result;
1✔
194
  }
195

196
  /**
197
   * Attempts to find a script in project's perl sdk and shows notification to user with suggestion to install a library if
198
   * script was not found
199
   *
200
   * @param project     to get sdk from
201
   * @param scriptName  script name
202
   * @param libraryName library to suggest if script was not found; notification won't be shown if lib is null/empty
203
   * @return script's virtual file if any
204
   */
205
  public static @Nullable VirtualFile findLibraryScriptWithNotification(@NotNull Project project,
206
                                                                        @NotNull String scriptName,
207
                                                                        @Nullable String libraryName) {
208
    return ObjectUtils.doIfNotNull(
1✔
209
      PerlProjectManager.getSdkWithNotification(project),
1✔
210
      it -> findLibraryScriptWithNotification(it, project, scriptName, libraryName));
1✔
211
  }
212

213

214
  /**
215
   * Attempts to find a script in project's perl sdk and shows notification to user with suggestion to install a library if
216
   * script was not found
217
   *
218
   * @param sdk         to find script in
219
   * @param scriptName  script name
220
   * @param libraryName library to suggest if script was not found, notification won't be shown if lib is null/empty
221
   * @return script's virtual file if any
222
   */
223
  public static @Nullable VirtualFile findLibraryScriptWithNotification(@NotNull Sdk sdk,
224
                                                                        @Nullable Project project,
225
                                                                        @NotNull String scriptName,
226
                                                                        @Nullable String libraryName) {
227
    VirtualFile scriptFile = findScript(project, scriptName);
1✔
228
    if (scriptFile != null) {
1✔
229
      return scriptFile;
1✔
230
    }
231

232
    if (StringUtil.isEmpty(libraryName)) {
×
233
      return null;
×
234
    }
235

236
    showMissingLibraryNotification(project, sdk, libraryName);
×
237

238
    return null;
×
239
  }
240

241
  public static void showMissingLibraryNotification(@NotNull Project project, @NotNull Sdk sdk, @NotNull Collection<String> packageNames) {
242
    if (packageNames.isEmpty()) {
1✔
243
      return;
×
244
    }
245
    if (packageNames.size() == 1) {
1✔
246
      showMissingLibraryNotification(project, sdk, packageNames.iterator().next());
1✔
247
      return;
1✔
248
    }
249

250
    Notification notification = new Notification(
×
251
      PerlBundle.message("perl.missing.library.notification"),
×
252
      PerlBundle.message("perl.missing.library.notification.title", packageNames.size()),
×
253
      StringUtil.join(ContainerUtil.sorted(packageNames), ", "),
×
254
      NotificationType.ERROR
255
    );
256
    addInstallActionsAndShow(project, sdk, packageNames, notification);
×
257
  }
×
258

259
  /**
260
   * Adds installation actions to the {@code notification} and shows it in the context of the {@code project}
261
   */
262
  public static void addInstallActionsAndShow(@Nullable Project project,
263
                                              @NotNull Sdk sdk,
264
                                              @NotNull Collection<String> packagesToInstall,
265
                                              @NotNull Notification notification) {
266
    List<AnAction> actions = new ArrayList<>();
1✔
267
    actions.add(PackageManagerAdapter.createInstallAction(sdk, project, packagesToInstall, notification::expire));
1✔
268

269
    if (ContainerUtil.intersects(BUNDLED_MODULE_PARTS, packagesToInstall)) {
1✔
270
      actions.add(new PerlDumbAwareAction(PerlBundle.message("perl.quickfix.install.module", BUNDLED_MODULE_NAME)) {
×
271
        @Override
272
        public void actionPerformed(@NotNull AnActionEvent e) {
273
          notification.expire();
×
274
          var extendedModulesList = new ArrayList<String>();
×
275
          extendedModulesList.add(BUNDLED_MODULE_NAME);
×
276
          extendedModulesList.addAll(ContainerUtil.subtract(packagesToInstall, BUNDLED_MODULE_PARTS));
×
277
          PackageManagerAdapter.installModules(sdk, project, extendedModulesList, notification::expire, true);
×
278
        }
×
279
      });
280
    }
281

282
    actions.forEach(notification::addAction);
1✔
283
    Notifications.Bus.notify(notification, project);
1✔
284
  }
1✔
285

286

287
  private static void showMissingLibraryNotification(@Nullable Project project, @NotNull Sdk sdk, @NotNull String libraryName) {
288
    Notification notification = new Notification(
1✔
289
      PerlBundle.message("perl.missing.library.notification"),
1✔
290
      PerlBundle.message("perl.missing.library.notification.title", libraryName),
1✔
291
      PerlBundle.message("perl.missing.library.notification.message", libraryName),
1✔
292
      NotificationType.ERROR
293
    );
294

295
    addInstallActionsAndShow(project, sdk, Collections.singletonList(libraryName), notification);
1✔
296
  }
1✔
297

298
  /**
299
   * Attempts to find an executable script by name in perl's libraries path
300
   *
301
   * @return script's virtual file if available
302
   * @apiNote returns virtual file of local file, not remote. It finds not a perl script, but executable script. E.g. for windows, it may
303
   * be a bat script
304
   **/
305
  @Contract("null,_->null;_,null->null")
306
  public static @Nullable VirtualFile findScript(@Nullable Project project, @Nullable String scriptName) {
307
    return ReadAction.nonBlocking(() -> {
1✔
308
      var sdk = PerlProjectManager.getSdk(project);
1✔
309
      if (sdk == null || StringUtil.isEmpty(scriptName)) {
1✔
310
        return null;
1✔
311
      }
312
      PerlOsHandler osHandler = PerlOsHandler.notNullFrom(sdk);
1✔
313
      return getBinDirectories(project)
1✔
314
        .map(root -> {
1✔
315
          VirtualFile scriptFile = null;
1✔
316
          if (osHandler.isMsWindows()) {
1✔
317
            scriptFile = root.findChild(scriptName + ".bat");
1✔
318
          }
319
          return scriptFile != null ? scriptFile : root.findChild(scriptName);
1✔
320
        })
321
        .filter(Objects::nonNull)
1✔
322
        .findFirst().orElse(null);
1✔
323
    }).executeSynchronously();
1✔
324
  }
325

326

327
  /**
328
   * @return list of perl bin directories where script from library may be located
329
   **/
330
  public static @NotNull Stream<VirtualFile> getBinDirectories(@NotNull Project project) {
331
    ApplicationManager.getApplication().assertReadAccessAllowed();
1✔
332
    var libraryRoots = PerlProjectManager.getInstance(project).getAllLibraryRoots();
1✔
333
    var files = new ArrayList<>(ContainerUtil.map(libraryRoots, PerlRunUtil::findLibsBin));
1✔
334

335
    var sdk = PerlProjectManager.getSdk(project);
1✔
336
    if (sdk != null) {
1✔
337
      PerlHostData<?, ?> hostData = PerlHostData.notNullFrom(sdk);
1✔
338
      File localSdkBinDir = hostData.getLocalPath(new File(
1✔
339
        StringUtil.notNullize(PerlProjectManager.getInterpreterPath(sdk))).getParentFile());
1✔
340
      if (localSdkBinDir != null) {
1✔
341
        files.add(VfsUtil.findFileByIoFile(localSdkBinDir, false));
1✔
342
      }
343
      PerlVersionManagerData.notNullFrom(sdk).getBinDirsPath().forEach(
1✔
344
        it -> ObjectUtils.doIfNotNull(hostData.getLocalPath(it), localPath -> files.add(VfsUtil.findFileByIoFile(localPath, false))));
1✔
345
    }
346
    return files.stream().filter(Objects::nonNull).distinct();
1✔
347
  }
348

349
  /**
350
   * Finds a bin dir for a library root
351
   *
352
   * @return bin root or null if not available
353
   * @implSpec for now we are traversing tree up to lib dir and resolving {@code ../bin}
354
   */
355
  private static @Nullable VirtualFile findLibsBin(@Nullable VirtualFile libraryRoot) {
356
    if (libraryRoot == null || !libraryRoot.isValid()) {
1✔
357
      return null;
×
358
    }
359
    File binPath = findLibsBin(new File(libraryRoot.getPath()));
1✔
360
    return binPath == null ? null : VfsUtil.findFileByIoFile(binPath, false);
1✔
361
  }
362

363
  /**
364
   * Finds a bin dir for a library root path
365
   *
366
   * @return bin root path or null if not found
367
   * @implSpec for now we are traversing tree up to {@code lib} dir and resolving {@code ../bin}
368
   */
369
  public static @Nullable File findLibsBin(@Nullable File libraryRoot) {
370
    if (libraryRoot == null) {
1✔
371
      return null;
1✔
372
    }
373
    String fileName = libraryRoot.getName();
1✔
374
    if ("lib".equals(fileName)) {
1✔
375
      return new File(libraryRoot.getParentFile(), "bin");
1✔
376
    }
377
    return findLibsBin(libraryRoot.getParentFile());
1✔
378
  }
379

380
  /**
381
   * Gets stdout from executing a perl command with a given parameters, command represented by {@code parameters}.
382
   */
383
  public static @NotNull List<String> getOutputFromPerl(@NotNull Sdk perlSdk, @NotNull String... parameters) {
384
    String interpreterPath = PerlProjectManager.getInterpreterPath(perlSdk);
1✔
385
    if (StringUtil.isEmpty(interpreterPath)) {
1✔
386
      LOG.warn("Empty interpreter path from " + perlSdk);
×
387
      return Collections.emptyList();
×
388
    }
389
    return getOutputFromProgram(new PerlCommandLine(
1✔
390
      interpreterPath).withParameters(parameters).withSdk(perlSdk));
1✔
391
  }
392

393

394
  /**
395
   * Gets stdout from executing a command represented by {@code commands} on the host represented by {@code hostData}
396
   * Commands are going to be patched with version manager, represented by {@code versionManagerData}
397
   */
398
  public static @NotNull List<String> getOutputFromProgram(@NotNull PerlHostData<?, ?> hostData,
399
                                                           @NotNull PerlVersionManagerData<?, ?> versionManagerData,
400
                                                           @NotNull String... commands) {
401
    return getOutputFromProgram(new PerlCommandLine(commands).withHostData(hostData).withVersionManagerData(versionManagerData));
1✔
402
  }
403

404
  /**
405
   * Gets stdout from a {@code commandLine} at host represented by {@code hostData}
406
   */
407
  private static @NotNull List<String> getOutputFromProgram(@NotNull PerlCommandLine commandLine) {
408
    try {
409
      var commandOutput = PerlHostData.execAndGetOutput(commandLine);
1✔
410
      if (commandOutput.getExitCode() != 0) {
1✔
411
        LOG.warn("Non-zero exit code from " + commandLine + "; " + commandOutput);
×
412
      }
413
      return commandOutput.getStdoutLines();
1✔
414
    }
415
    catch (Exception e) {
×
416
      LOG.warn("Error executing " + commandLine, e);
×
417
      return Collections.emptyList();
×
418
    }
419
  }
420

421
  public static @NotNull RunContentDescriptor runInConsole(@NotNull PerlCommandLine perlCommandLine) {
422
    ApplicationManager.getApplication().assertIsDispatchThread();
1✔
423
    Executor runExecutor = DefaultRunExecutor.getRunExecutorInstance();
1✔
424
    Project project = perlCommandLine.getNonNullEffectiveProject();
1✔
425
    boolean isUnitTestMode = ApplicationManager.getApplication().isUnitTestMode();
1✔
426
    PerlConsoleView consoleView = isUnitTestMode ? new PerlRunConsole(project) : new PerlTerminalExecutionConsole(project);
1✔
427
    consoleView.withHostData(perlCommandLine.getEffectiveHostData());
1✔
428
    ProcessHandler processHandler = null;
1✔
429
    try {
430
      processHandler = PerlHostData.createConsoleProcessHandler(perlCommandLine.withPty(!isUnitTestMode));
1✔
431
      if (isUnitTestMode) {
1✔
432
        processHandler.addProcessListener(new ProcessAdapter() {
1✔
433
          @Override
434
          public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) {
435
            LOG.info(outputType + ": " + event.getText());
1✔
436
          }
1✔
437
        });
438
      }
439
    }
440
    catch (ExecutionException e) {
×
441
      consoleView.print(e.getMessage(), ConsoleViewContentType.ERROR_OUTPUT);
×
442
      LOG.warn(e);
×
443
    }
1✔
444

445
    RunContentDescriptor runContentDescriptor = new RunContentDescriptor(
1✔
446
      consoleView,
447
      processHandler,
448
      consoleView.getComponent(),
1✔
449
      ObjectUtils.notNull(perlCommandLine.getConsoleTitle(), perlCommandLine.getCommandLineString()),
1✔
450
      ObjectUtils.notNull(perlCommandLine.getConsoleIcon(), PerlIcons.PERL_LANGUAGE_ICON)
1✔
451
    );
452

453
    RunContentManager.getInstance(project).showRunContent(runExecutor, runContentDescriptor);
1✔
454
    if (processHandler != null) {
1✔
455
      consoleView.attachToProcess(processHandler);
1✔
456
      processHandler.startNotify();
1✔
457
      if (ApplicationManager.getApplication().isUnitTestMode()) {
1✔
458
        LOG.assertTrue(ourTestDisposable != null);
1✔
459
        TEST_CONSOLE_DESCRIPTORS.add(runContentDescriptor);
1✔
460
        Disposer.register(ourTestDisposable, runContentDescriptor.getExecutionConsole());
1✔
461
      }
462
    }
463
    return runContentDescriptor;
1✔
464
  }
465

466
  public static void addMissingPackageListener(@NotNull ProcessHandler handler,
467
                                               @NotNull PerlCommandLine commandLine) {
468
    if (!commandLine.isWithMissingPackageListener()) {
1✔
469
      return;
1✔
470
    }
471
    ProcessListener listener = createMissingPackageListener(commandLine.getEffectiveProject(), commandLine.getEffectiveSdk());
1✔
472
    if (listener != null) {
1✔
473
      handler.addProcessListener(listener);
1✔
474
    }
475
  }
1✔
476

477
  /**
478
   * Creates a listener watching process output and showing notifications about missing libraries
479
   */
480
  private static @Nullable ProcessListener createMissingPackageListener(@Nullable Project project, @Nullable Sdk sdk) {
481
    if (project == null) {
1✔
482
      return null;
1✔
483
    }
484

485
    if (sdk == null) {
1✔
486
      sdk = PerlProjectManager.getSdk(project);
×
487
      if (sdk == null) {
×
488
        return null;
×
489
      }
490
    }
491

492
    Sdk finalSdk = sdk;
1✔
493
    Set<String> missingPackages = new HashSet<>();
1✔
494

495
    return new ProcessAdapter() {
1✔
496
      @Override
497
      public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) {
498
        String text = event.getText();
1✔
499
        if (StringUtil.isEmpty(text)) {
1✔
500
          return;
×
501
        }
502
        int keyOffset = text.indexOf(MISSING_MODULE_PREFIX);
1✔
503
        if (keyOffset == -1) {
1✔
504
          checkLegacyPrefix(text);
1✔
505
          return;
1✔
506
        }
507
        int startOffset = keyOffset + MISSING_MODULE_PREFIX.length();
1✔
508
        int endOffset = text.indexOf(MISSING_MODULE_SUFFIX, startOffset);
1✔
509
        if (endOffset == -1) {
1✔
510
          return;
×
511
        }
512
        processPackage(text.substring(startOffset, endOffset));
1✔
513
      }
1✔
514

515
      private void checkLegacyPrefix(@NotNull String text) {
516
        int keyOffset = text.indexOf(LEGACY_MODULE_PREFIX);
1✔
517
        if (keyOffset == -1) {
1✔
518
          return;
1✔
519
        }
520
        int startOffset = keyOffset + LEGACY_MODULE_PREFIX.length();
×
521
        int endOffset = text.indexOf(LEGACY_MODULE_SUFFIX, startOffset);
×
522
        if (endOffset == -1) {
×
523
          return;
×
524
        }
525
        processPackage(PerlPackageUtil.getPackageNameByPath(text.substring(startOffset, endOffset)));
×
526
      }
×
527

528
      private void processPackage(@Nullable String packageName) {
529
        if (StringUtil.isNotEmpty(packageName) && missingPackages.add(packageName)) {
1✔
530
          showMissingLibraryNotification(project, finalSdk, missingPackages);
1✔
531
        }
532
      }
1✔
533
    };
534
  }
535

536

537
  /**
538
   * Sets {@code newText} to the progress indicator if available.
539
   *
540
   * @return old indicator text
541
   */
542
  public static @NlsSafe @Nullable String setProgressText(@Nullable @Nls String newText) {
543
    ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
1✔
544
    if (indicator != null) {
1✔
545
      String oldText = indicator.getText();
1✔
546
      indicator.setText(newText);
1✔
547
      return oldText;
1✔
548
    }
549
    return null;
×
550
  }
551

552
  public static void refreshSdkDirs(@Nullable Project project) {
553
    refreshSdkDirs(PerlProjectManager.getSdk(project), project);
×
554
  }
×
555

556
  /**
557
   * Asynchronously refreshes directories of sdk. Need to be invoked after installations
558
   */
559
  public static void refreshSdkDirs(@Nullable Sdk sdk, @Nullable Project project, @Nullable Runnable callback) {
560
    if (sdk == null) {
1✔
561
      return;
×
562
    }
563
    LOG.debug("Starting to refresh ", sdk, " on ", Thread.currentThread().getName());
1✔
564
    if (ourTestSdkRefreshSemaphore != null) {
1✔
565
      ourTestSdkRefreshSemaphore.down();
1✔
566
    }
567
    new Task.Backgroundable(project, PerlBundle.message("perl.progress.refreshing.interpreter.information", sdk.getName()), false) {
1✔
568
      @Override
569
      public void run(@NotNull ProgressIndicator indicator) {
570
        PerlSdkType.INSTANCE.setupSdkPaths(sdk);
1✔
571
        if (project != null) {
1✔
572
          WriteAction.runAndWait(() -> {
1✔
573
            if (!project.isDisposed()) {
1✔
574
              ProjectRootManagerEx.getInstanceEx(project)
1✔
575
                .makeRootsChange(EmptyRunnable.getInstance(), RootsChangeRescanningInfo.TOTAL_RESCAN);
1✔
576
              DaemonCodeAnalyzer.getInstance(project).restart();
1✔
577
            }
578
          });
1✔
579
        }
580
        if (callback != null) {
1✔
581
          callback.run();
1✔
582
        }
583
        if (ourTestSdkRefreshSemaphore != null) {
1✔
584
          ourTestSdkRefreshSemaphore.up();
1✔
585
        }
586
        LOG.debug("Finished to refresh ", sdk, " on ", Thread.currentThread().getName());
1✔
587
      }
1✔
588
    }.queue();
1✔
589
  }
1✔
590

591
  /**
592
   * Asynchronously refreshes directories of the provided sdk.
593
   * This method sets up sdk paths and rescans the roots of the project to ensure that all changes are accounted for.
594
   *
595
   * @param sdk      The sdk to refresh.
596
   * @param project  The project associated with the sdk.
597
   * @param callback A callback function to execute after the sdk is refreshed (optional).
598
   */
599
  public static void refreshSdkDirs(@Nullable Sdk sdk, @Nullable Project project) {
600
    refreshSdkDirs(sdk, project, null);
1✔
601
  }
1✔
602

603
  @TestOnly
604
  public static @NotNull List<RunContentDescriptor> getTestConsoleDescriptors() {
605
    return TEST_CONSOLE_DESCRIPTORS;
1✔
606
  }
607

608
  @TestOnly
609
  public static @NotNull Semaphore getSdkRefreshSemaphore() {
610
    return ourTestSdkRefreshSemaphore;
1✔
611
  }
612

613
  @TestOnly
614
  public static void setUpForTests(@NotNull Disposable testDisposable) {
615
    ourTestDisposable = testDisposable;
1✔
616
    ourTestSdkRefreshSemaphore = new Semaphore();
1✔
617
    Disposer.register(testDisposable, () -> {
1✔
618
      TEST_CONSOLE_DESCRIPTORS.clear();
1✔
619
      ourTestSdkRefreshSemaphore = null;
1✔
620
      ourTestDisposable = null;
1✔
621
    });
1✔
622
  }
1✔
623

624
  /**
625
   * Updates {@code environment} with {@code perlArguments} packed to {@code PERL5OPT} environment variable to pass it transparently.
626
   */
627
  public static void updatePerl5Opt(@NotNull Map<? super String, String> environment, @NotNull List<String> perlArguments) {
628
    if (perlArguments.isEmpty()) {
1✔
629
      return;
1✔
630
    }
631
    var perlParameters = StringUtil.join(perlArguments, " ");
1✔
632

633
    String currentOpt = environment.get(PERL5OPT);
1✔
634
    if (StringUtil.isNotEmpty(currentOpt)) {
1✔
635
      perlParameters = String.join(" ", currentOpt, perlParameters);
×
636
    }
637
    environment.put(PERL5OPT, perlParameters);
1✔
638
  }
1✔
639
}
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