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

devonfw / IDEasy / 8111080112

01 Mar 2024 12:07PM UTC coverage: 58.254% (+1.1%) from 57.131%
8111080112

push

github

web-flow
#9: background process (#200)

1519 of 2867 branches covered (52.98%)

Branch coverage included in aggregate %.

3954 of 6528 relevant lines covered (60.57%)

2.62 hits per line

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

51.91
cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.List;
7
import java.util.Set;
8
import java.util.stream.Stream;
9

10
import com.devonfw.tools.ide.cli.CliException;
11
import com.devonfw.tools.ide.commandlet.Commandlet;
12
import com.devonfw.tools.ide.common.Tag;
13
import com.devonfw.tools.ide.common.Tags;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.environment.EnvironmentVariables;
16
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
17
import com.devonfw.tools.ide.io.FileAccess;
18
import com.devonfw.tools.ide.io.TarCompression;
19
import com.devonfw.tools.ide.os.MacOsHelper;
20
import com.devonfw.tools.ide.process.ProcessContext;
21
import com.devonfw.tools.ide.process.ProcessErrorHandling;
22
import com.devonfw.tools.ide.process.ProcessMode;
23
import com.devonfw.tools.ide.property.StringListProperty;
24
import com.devonfw.tools.ide.util.FilenameUtil;
25
import com.devonfw.tools.ide.version.VersionIdentifier;
26

27
/**
28
 * {@link Commandlet} for a tool integrated into the IDE.
29
 */
30
public abstract class ToolCommandlet extends Commandlet implements Tags {
1✔
31

32
  /** @see #getName() */
33
  protected final String tool;
34

35
  private final Set<Tag> tags;
36

37
  /** The commandline arguments to pass to the tool. */
38
  public final StringListProperty arguments;
39

40
  private MacOsHelper macOsHelper;
41

42
  /**
43
   * The constructor.
44
   *
45
   * @param context the {@link IdeContext}.
46
   * @param tool the {@link #getName() tool name}.
47
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of}
48
   *        method.
49
   */
50
  public ToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
51

52
    super(context);
3✔
53
    this.tool = tool;
3✔
54
    this.tags = tags;
3✔
55
    addKeyword(tool);
3✔
56
    this.arguments = add(new StringListProperty("", false, "args"));
11✔
57
  }
1✔
58

59
  /**
60
   * @return the name of the tool (e.g. "java", "mvn", "npm", "node").
61
   */
62
  @Override
63
  public String getName() {
64

65
    return this.tool;
3✔
66
  }
67

68
  /**
69
   * @return the name of the binary executable for this tool.
70
   */
71
  protected String getBinaryName() {
72

73
    return this.tool;
×
74
  }
75

76
  @Override
77
  public final Set<Tag> getTags() {
78

79
    return this.tags;
×
80
  }
81

82
  @Override
83
  public void run() {
84

85
    runTool(ProcessMode.DEFAULT, null, this.arguments.asArray());
×
86
  }
×
87

88
  /**
89
   * Ensures the tool is installed and then runs this tool with the given arguments.
90
   *
91
   * @param processMode see {@link ProcessMode}
92
   * @param toolVersion the explicit version (pattern) to run. Typically {@code null} to ensure the configured version
93
   *        is installed and use that one. Otherwise, the specified version will be installed in the software repository
94
   *        without touching and IDE installation and used to run.
95
   * @param args the command-line arguments to run the tool.
96
   */
97
  public void runTool(ProcessMode processMode, VersionIdentifier toolVersion, String... args) {
98

99
    Path binaryPath;
100
    Path toolPath = Path.of(getBinaryName());
×
101
    if (toolVersion == null) {
×
102
      install(true);
×
103
      binaryPath = toolPath;
×
104
    } else {
105
      throw new UnsupportedOperationException("Not yet implemented!");
×
106
    }
107
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(binaryPath)
×
108
        .addArgs(args);
×
109

110
    pc.run(processMode);
×
111
  }
×
112

113
  /**
114
   * @param toolVersion the explicit {@link VersionIdentifier} of the tool to run.
115
   * @param args the command-line arguments to run the tool.
116
   * @see ToolCommandlet#runTool(ProcessMode, VersionIdentifier, String...)
117
   */
118
  public void runTool(VersionIdentifier toolVersion, String... args) {
119

120
    runTool(ProcessMode.DEFAULT, toolVersion, args);
×
121
  }
×
122

123
  /**
124
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
125
   */
126
  public String getEdition() {
127

128
    return this.context.getVariables().getToolEdition(getName());
7✔
129
  }
130

131
  /**
132
   * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as
133
   *         tool.
134
   * @see #getToolWithEdition(String, String)
135
   */
136
  protected final String getToolWithEdition() {
137

138
    return getToolWithEdition(getName(), getEdition());
×
139
  }
140

141
  /**
142
   * @param tool the tool name.
143
   * @param edition the edition.
144
   * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as
145
   *         tool.
146
   */
147
  protected final static String getToolWithEdition(String tool, String edition) {
148

149
    if (tool.equals(edition)) {
×
150
      return tool;
×
151
    }
152
    return tool + "/" + edition;
×
153
  }
154

155
  /**
156
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
157
   */
158
  public VersionIdentifier getConfiguredVersion() {
159

160
    return this.context.getVariables().getToolVersion(getName());
7✔
161
  }
162

163
  /**
164
   * Method to be called for {@link #install(boolean)} from dependent {@link Commandlet}s.
165
   *
166
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
167
   *         nothing has changed.
168
   */
169
  public boolean install() {
170

171
    return install(true);
×
172
  }
173

174
  /**
175
   * Performs the installation of the {@link #getName() tool} managed by this {@link Commandlet}.
176
   *
177
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
178
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
179
   *         nothing has changed.
180
   */
181
  public boolean install(boolean silent) {
182

183
    return doInstall(silent);
4✔
184
  }
185

186
  /**
187
   * Installs or updates the managed {@link #getName() tool}.
188
   *
189
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
190
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
191
   *         nothing has changed.
192
   */
193
  protected abstract boolean doInstall(boolean silent);
194

195
  /**
196
   * This method is called after the tool has been newly installed or updated to a new version.
197
   */
198
  protected void postInstall() {
199

200
    // nothing to do by default
201
  }
1✔
202

203
  /**
204
   * @param path the {@link Path} to start the recursive search from.
205
   * @return the deepest subdir {@code s} of the passed path such that all directories between {@code s} and the passed
206
   *         path (including {@code s}) are the sole item in their respective directory and {@code s} is not named
207
   *         "bin".
208
   */
209
  private Path getProperInstallationSubDirOf(Path path) {
210

211
    try (Stream<Path> stream = Files.list(path)) {
3✔
212
      Path[] subFiles = stream.toArray(Path[]::new);
8✔
213
      if (subFiles.length == 0) {
3!
214
        throw new CliException("The downloaded package for the tool " + this.tool
×
215
            + " seems to be empty as you can check in the extracted folder " + path);
216
      } else if (subFiles.length == 1) {
4✔
217
        String filename = subFiles[0].getFileName().toString();
6✔
218
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS)
10!
219
            && !filename.endsWith(".app") && Files.isDirectory(subFiles[0])) {
9!
220
          return getProperInstallationSubDirOf(subFiles[0]);
8✔
221
        }
222
      }
223
      return path;
4✔
224
    } catch (IOException e) {
4!
225
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
226
    }
227
  }
228

229
  /**
230
   * @param file the {@link Path} to the file to extract.
231
   * @param targetDir the {@link Path} to the directory where to extract (or copy) the file.
232
   */
233
  protected void extract(Path file, Path targetDir) {
234

235
    FileAccess fileAccess = this.context.getFileAccess();
4✔
236
    if (isExtract()) {
3!
237
      Path tmpDir = this.context.getFileAccess().createTempDir("extract-" + file.getFileName());
8✔
238
      this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", file, tmpDir, targetDir);
18✔
239
      String extension = FilenameUtil.getExtension(file.getFileName().toString());
5✔
240
      this.context.trace("Determined file extension {}", extension);
10✔
241
      TarCompression tarCompression = TarCompression.of(extension);
3✔
242
      if (tarCompression != null) {
2!
243
        fileAccess.untar(file, tmpDir, tarCompression);
6✔
244
      } else if ("zip".equals(extension) || "jar".equals(extension)) {
×
245
        fileAccess.unzip(file, tmpDir);
×
246
      } else if ("dmg".equals(extension)) {
×
247
        assert this.context.getSystemInfo().isMac();
×
248
        Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
249
        fileAccess.mkdirs(mountPath);
×
250
        ProcessContext pc = this.context.newProcess();
×
251
        pc.executable("hdiutil");
×
252
        pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
253
        pc.run();
×
254
        Path appPath = fileAccess.findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
255
        if (appPath == null) {
×
256
          throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
257
        }
258
        fileAccess.copy(appPath, tmpDir);
×
259
        pc.addArgs("detach", "-force", mountPath);
×
260
        pc.run();
×
261
      } else if ("msi".equals(extension)) {
×
262
        this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + tmpDir).run();
×
263
        // msiexec also creates a copy of the MSI
264
        Path msiCopy = tmpDir.resolve(file.getFileName());
×
265
        fileAccess.delete(msiCopy);
×
266
      } else if ("pkg".equals(extension)) {
×
267

268
        Path tmpDirPkg = fileAccess.createTempDir("ide-pkg-");
×
269
        ProcessContext pc = this.context.newProcess();
×
270
        // we might also be able to use cpio from commons-compression instead of external xar...
271
        pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
272
        Path contentPath = fileAccess.findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
273
        fileAccess.untar(contentPath, tmpDir, TarCompression.GZ);
×
274
        fileAccess.delete(tmpDirPkg);
×
275
      } else {
×
276
        throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + file);
×
277
      }
278
      moveAndProcessExtraction(getProperInstallationSubDirOf(tmpDir), targetDir);
6✔
279
      fileAccess.delete(tmpDir);
3✔
280
    } else {
1✔
281
      this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", getName(), file);
×
282

283
      if (Files.isDirectory(file)) {
×
284
        fileAccess.move(file, targetDir);
×
285
      } else {
286
        try {
287
          Files.createDirectories(targetDir);
×
288
        } catch (IOException e) {
×
289
          throw new IllegalStateException("Failed to create folder " + targetDir);
×
290
        }
×
291
        fileAccess.move(file, targetDir.resolve(file.getFileName()));
×
292
      }
293
    }
294
  }
1✔
295

296
  /**
297
   * Moves the extracted content to the final destination {@link Path}. May be overridden to customize the extraction
298
   * process.
299
   *
300
   * @param from the source {@link Path} to move.
301
   * @param to the target {@link Path} to move to.
302
   */
303
  protected void moveAndProcessExtraction(Path from, Path to) {
304

305
    this.context.getFileAccess().move(from, to);
6✔
306
  }
1✔
307

308
  /**
309
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
310
   */
311
  protected boolean isExtract() {
312

313
    return true;
2✔
314
  }
315

316
  /**
317
   * @return the {@link MacOsHelper} instance.
318
   */
319
  protected MacOsHelper getMacOsHelper() {
320

321
    if (this.macOsHelper == null) {
3!
322
      this.macOsHelper = new MacOsHelper(this.context);
7✔
323
    }
324
    return this.macOsHelper;
3✔
325
  }
326

327
  /**
328
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
329
   */
330
  public VersionIdentifier getInstalledVersion() {
331

332
    return getInstalledVersion(this.context.getSoftwarePath().resolve(getName()));
9✔
333
  }
334

335
  /**
336
   * @param toolPath the installation {@link Path} where to find the version file.
337
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
338
   */
339
  protected VersionIdentifier getInstalledVersion(Path toolPath) {
340

341
    if (!Files.isDirectory(toolPath)) {
5✔
342
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
15✔
343
      return null;
2✔
344
    }
345
    Path toolVersionFile = toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
346
    if (!Files.exists(toolVersionFile)) {
5✔
347
      Path legacyToolVersionFile = toolPath.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION);
4✔
348
      if (Files.exists(legacyToolVersionFile)) {
5✔
349
        toolVersionFile = legacyToolVersionFile;
3✔
350
      } else {
351
        this.context.warning("Tool {} is missing version file in {}", getName(), toolVersionFile);
15✔
352
        return null;
2✔
353
      }
354
    }
355
    try {
356
      String version = Files.readString(toolVersionFile).trim();
4✔
357
      return VersionIdentifier.of(version);
3✔
358
    } catch (IOException e) {
×
359
      throw new IllegalStateException("Failed to read file " + toolVersionFile, e);
×
360
    }
361
  }
362

363
  /**
364
   * @return the installed edition of this tool or {@code null} if not installed.
365
   */
366
  public String getInstalledEdition() {
367

368
    return getInstalledEdition(this.context.getSoftwarePath().resolve(getName()));
9✔
369
  }
370

371
  /**
372
   * @param toolPath the installation {@link Path} where to find currently installed tool. The name of the parent
373
   *        directory of the real path corresponding to the passed {@link Path path} must be the name of the edition.
374
   * @return the installed edition of this tool or {@code null} if not installed.
375
   */
376
  public String getInstalledEdition(Path toolPath) {
377

378
    if (!Files.isDirectory(toolPath)) {
5!
379
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
×
380
      return null;
×
381
    }
382
    try {
383
      String edition = toolPath.toRealPath().getParent().getFileName().toString();
8✔
384
      if (!this.context.getUrls().getSortedEditions(getName()).contains(edition)) {
9!
385
        edition = getEdition();
3✔
386
      }
387
      return edition;
2✔
388
    } catch (IOException e) {
×
389
      throw new IllegalStateException("Couldn't determine the edition of " + getName()
×
390
          + " from the directory structure of its software path " + toolPath
391
          + ", assuming the name of the parent directory of the real path of the software path to be the edition "
392
          + "of the tool.", e);
393
    }
394

395
  }
396

397
  /**
398
   * List the available editions of this tool.
399
   */
400
  public void listEditions() {
401

402
    List<String> editions = this.context.getUrls().getSortedEditions(getName());
7✔
403
    for (String edition : editions) {
10✔
404
      this.context.info(edition);
4✔
405
    }
1✔
406
  }
1✔
407

408
  /**
409
   * List the available versions of this tool.
410
   */
411
  public void listVersions() {
412

413
    List<VersionIdentifier> versions = this.context.getUrls().getSortedVersions(getName(), getEdition());
9✔
414
    for (VersionIdentifier vi : versions) {
10✔
415
      this.context.info(vi.toString());
5✔
416
    }
1✔
417
  }
1✔
418

419
  /**
420
   * Sets the tool version in the environment variable configuration file.
421
   *
422
   * @param version the version (pattern) to set.
423
   */
424
  public void setVersion(String version) {
425

426
    if ((version == null) || version.isBlank()) {
×
427
      throw new IllegalStateException("Version has to be specified!");
×
428
    }
429
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
430
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
431
      this.context.warning("Version {} seems to be invalid", version);
×
432
    }
433
    setVersion(configuredVersion, true);
×
434
  }
×
435

436
  /**
437
   * Sets the tool version in the environment variable configuration file.
438
   *
439
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
440
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
441
   */
442
  public void setVersion(VersionIdentifier version, boolean hint) {
443

444
    EnvironmentVariables variables = this.context.getVariables();
4✔
445
    EnvironmentVariables settingsVariables = variables.getByType(EnvironmentVariablesType.SETTINGS);
4✔
446
    String edition = getEdition();
3✔
447
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
448
    VersionIdentifier resolvedVersion = this.context.getUrls().getVersion(this.tool, edition, version);
9✔
449
    if (version.isPattern()) {
3!
450
      this.context.debug("Resolved version {} to {} for tool {}/{}", version, resolvedVersion, this.tool, edition);
×
451
    }
452
    settingsVariables.set(name, resolvedVersion.toString(), false);
7✔
453
    settingsVariables.save();
2✔
454
    this.context.info("{}={} has been set in {}", name, version, settingsVariables.getSource());
19✔
455
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
456
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
457
      this.context.warning(
×
458
          "The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.",
459
          name, declaringVariables.getSource());
×
460
    }
461
    if (hint) {
2✔
462
      this.context.info("To install that version call the following command:");
4✔
463
      this.context.info("ide install {}", this.tool);
11✔
464
    }
465
  }
1✔
466

467
  /**
468
   * Sets the tool edition in the environment variable configuration file.
469
   *
470
   * @param edition the edition to set.
471
   */
472
  public void setEdition(String edition) {
473

474
    setEdition(edition, true);
4✔
475
  }
1✔
476

477
  /**
478
   * Sets the tool edition in the environment variable configuration file.
479
   *
480
   * @param edition the edition to set
481
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
482
   */
483
  public void setEdition(String edition, boolean hint) {
484

485
    if ((edition == null) || edition.isBlank()) {
5!
486
      throw new IllegalStateException("Edition has to be specified!");
×
487
    }
488

489
    if (!Files.exists(this.context.getUrls().getEdition(getName(), edition).getPath())) {
12!
490
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
491

492
    }
493
    EnvironmentVariables variables = this.context.getVariables();
4✔
494
    EnvironmentVariables settingsVariables = variables.getByType(EnvironmentVariablesType.SETTINGS);
4✔
495
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
496
    settingsVariables.set(name, edition, false);
6✔
497
    settingsVariables.save();
2✔
498

499
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
500
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
501
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
502
      this.context.warning(
×
503
          "The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.",
504
          name, declaringVariables.getSource());
×
505
    }
506
    if (hint) {
2!
507
      this.context.info("To install that edition call the following command:");
4✔
508
      this.context.info("ide install {}", this.tool);
11✔
509
    }
510
  }
1✔
511

512
}
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