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

Camelcade / Perl5-IDEA / #525521586

19 Jun 2025 10:27AM UTC coverage: 82.376% (+0.03%) from 82.349%
#525521586

push

github

hurricup
Migrated to NioFiles.deleteRecursively

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

54 existing lines in 7 files now uncovered.

30872 of 37477 relevant lines covered (82.38%)

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.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
    PerlPackageUtil.COVERAGE_MODULE,
97
    PerlPackageUtil.DEBUGGER_MODULE,
98
    PerlPackageUtil.PROFILER_MODULE,
99
    "File::Find",
100
    PerlPackageUtil.JSON_MODULE,
101
    "Perl::Critic",
102
    "Perl::Tidy",
103
    PerlPackageUtil.TAP_FORMATTER_MODULE,
104
    PerlPackageUtil.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✔
UNCOV
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✔
UNCOV
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✔
UNCOV
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
      String remoteScriptPath = hostData.getRemotePath(localScriptPath);
1✔
169
      if (remoteScriptPath != null) {
1✔
170
        commandLine.addParameter(remoteScriptPath);
1✔
171
      }
172
    }
173

174
    commandLine.addParameters(scriptParameters);
1✔
175

176
    return commandLine;
1✔
177
  }
178

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

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

212

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

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

UNCOV
235
    showMissingLibraryNotification(project, sdk, libraryName);
×
236

UNCOV
237
    return null;
×
238
  }
239

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

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

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

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

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

285

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

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

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

325

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

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

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

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

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

392

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

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

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

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

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

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

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

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

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

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

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

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

535

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

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

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

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

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

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

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

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

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