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

Camelcade / Perl5-IDEA / #525521705

10 Nov 2025 08:49AM UTC coverage: 75.92% (-0.05%) from 75.97%
#525521705

push

github

hurricup
Passed the reason to the daemon code analyzer

14750 of 22633 branches covered (65.17%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

82 existing lines in 10 files now uncovered.

31070 of 37720 relevant lines covered (82.37%)

0.82 hits per line

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

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

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

75

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

107
  private PerlRunUtil() {
108
  }
109

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

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

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

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

165
    commandLine.addParameters(perlParameters);
1✔
166

167
    if (StringUtil.isNotEmpty(localScriptPath)) {
1!
168
      commandLine.addParameter(hostData.getRemotePath(localScriptPath));
1✔
169
    }
170

171
    commandLine.addParameters(scriptParameters);
1✔
172

173
    return commandLine;
1✔
174
  }
175

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

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

209

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

UNCOV
228
    if (StringUtil.isEmpty(libraryName)) {
×
UNCOV
229
      return null;
×
230
    }
231

232
    showMissingLibraryNotification(project, sdk, libraryName);
×
233

UNCOV
234
    return null;
×
235
  }
236

237
  public static void showMissingLibraryNotification(@NotNull Project project, @NotNull Sdk sdk, @NotNull Collection<String> packageNames) {
238
    if (packageNames.isEmpty()) {
1!
UNCOV
239
      return;
×
240
    }
241
    if (packageNames.size() == 1) {
1!
242
      showMissingLibraryNotification(project, sdk, packageNames.iterator().next());
1✔
243
      return;
1✔
244
    }
245

UNCOV
246
    Notification notification = new Notification(
×
UNCOV
247
      PerlBundle.message("perl.missing.library.notification"),
×
UNCOV
248
      PerlBundle.message("perl.missing.library.notification.title", packageNames.size()),
×
249
      StringUtil.join(ContainerUtil.sorted(packageNames), ", "),
×
250
      NotificationType.ERROR
251
    );
252
    addInstallActionsAndShow(project, sdk, packageNames, notification);
×
UNCOV
253
  }
×
254

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

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

278
    actions.forEach(notification::addAction);
1✔
279
    Notifications.Bus.notify(notification, project);
1✔
280
  }
1✔
281

282

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

291
    addInstallActionsAndShow(project, sdk, Collections.singletonList(libraryName), notification);
1✔
292
  }
1✔
293

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

322

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

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

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

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

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

389

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

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

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

441
    RunContentDescriptor runContentDescriptor = new RunContentDescriptor(
1✔
442
      consoleView,
443
      processHandler,
444
      consoleView.getComponent(),
1✔
445
      ObjectUtils.notNull(perlCommandLine.getConsoleTitle(), perlCommandLine.getCommandLineString()),
1✔
446
      ObjectUtils.notNull(perlCommandLine.getConsoleIcon(), PerlIcons.PERL_LANGUAGE_ICON)
1✔
447
    );
448

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

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

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

481
    if (sdk == null) {
1!
UNCOV
482
      sdk = PerlProjectManager.getSdk(project);
×
UNCOV
483
      if (sdk == null) {
×
UNCOV
484
        return null;
×
485
      }
486
    }
487

488
    Sdk finalSdk = sdk;
1✔
489
    Set<String> missingPackages = new HashSet<>();
1✔
490

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

511
      private void checkLegacyPrefix(@NotNull String text) {
512
        int keyOffset = text.indexOf(LEGACY_MODULE_PREFIX);
1✔
513
        if (keyOffset == -1) {
1!
514
          return;
1✔
515
        }
UNCOV
516
        int startOffset = keyOffset + LEGACY_MODULE_PREFIX.length();
×
UNCOV
517
        int endOffset = text.indexOf(LEGACY_MODULE_SUFFIX, startOffset);
×
UNCOV
518
        if (endOffset == -1) {
×
519
          return;
×
520
        }
521
        processPackage(PerlPackageUtilCore.getPackageNameByPath(text.substring(startOffset, endOffset)));
×
522
      }
×
523

524
      private void processPackage(@Nullable String packageName) {
525
        if (StringUtil.isNotEmpty(packageName) && missingPackages.add(packageName)) {
1!
526
          showMissingLibraryNotification(project, finalSdk, missingPackages);
1✔
527
        }
528
      }
1✔
529
    };
530
  }
531

532

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

548
  public static void refreshSdkDirs(@Nullable Project project) {
UNCOV
549
    refreshSdkDirs(PerlProjectManager.getSdk(project), project);
×
UNCOV
550
  }
×
551

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

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

599
  @TestOnly
600
  public static @NotNull List<RunContentDescriptor> getTestConsoleDescriptors() {
601
    return TEST_CONSOLE_DESCRIPTORS;
1!
602
  }
603

604
  @TestOnly
605
  public static @NotNull Semaphore getSdkRefreshSemaphore() {
606
    return ourTestSdkRefreshSemaphore;
1!
607
  }
608

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

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

629
    String currentOpt = environment.get(PERL5OPT);
1✔
630
    if (StringUtil.isNotEmpty(currentOpt)) {
1!
UNCOV
631
      perlParameters = String.join(" ", currentOpt, perlParameters);
×
632
    }
633
    environment.put(PERL5OPT, perlParameters);
1✔
634
  }
1✔
635
}
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