• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

devonfw / IDEasy / 21174441650

20 Jan 2026 02:06PM UTC coverage: 70.447% (-0.02%) from 70.471%
21174441650

push

github

web-flow
#1679: fix determine version if npm list has exit code 1 (#1681)

4031 of 6308 branches covered (63.9%)

Branch coverage included in aggregate %.

10479 of 14289 relevant lines covered (73.34%)

3.18 hits per line

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

80.71
cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java
1
package com.devonfw.tools.ide.process;
2

3
import java.io.BufferedReader;
4
import java.io.IOException;
5
import java.io.InputStream;
6
import java.io.InputStreamReader;
7
import java.lang.ProcessBuilder.Redirect;
8
import java.nio.charset.StandardCharsets;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.util.ArrayList;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.Objects;
15
import java.util.concurrent.CompletableFuture;
16
import java.util.concurrent.ConcurrentLinkedQueue;
17
import java.util.function.Predicate;
18
import java.util.stream.Collectors;
19

20
import com.devonfw.tools.ide.cli.CliProcessException;
21
import com.devonfw.tools.ide.common.SystemPath;
22
import com.devonfw.tools.ide.context.IdeContext;
23
import com.devonfw.tools.ide.environment.VariableLine;
24
import com.devonfw.tools.ide.log.IdeLogLevel;
25
import com.devonfw.tools.ide.os.SystemInfoImpl;
26
import com.devonfw.tools.ide.os.WindowsPathSyntax;
27
import com.devonfw.tools.ide.util.FilenameUtil;
28
import com.devonfw.tools.ide.variable.IdeVariables;
29

30
/**
31
 * Implementation of {@link ProcessContext}.
32
 */
33
public class ProcessContextImpl implements ProcessContext {
34

35
  private static final String PREFIX_USR_BIN_ENV = "/usr/bin/env ";
36
  private static final Predicate<Integer> EXIT_CODE_ACCEPTOR = rc -> rc == ProcessResult.SUCCESS;
10✔
37

38
  /** The owning {@link IdeContext}. */
39
  protected final IdeContext context;
40

41
  private final ProcessBuilder processBuilder;
42

43
  protected final List<String> arguments;
44

45
  protected Path executable;
46

47
  private String overriddenPath;
48

49
  private final List<Path> extraPathEntries;
50

51
  private ProcessErrorHandling errorHandling;
52

53
  private OutputListener outputListener;
54

55
  private Predicate<Integer> exitCodeAcceptor;
56

57
  /**
58
   * The constructor.
59
   *
60
   * @param context the owning {@link IdeContext}.
61
   */
62
  public ProcessContextImpl(IdeContext context) {
63

64
    super();
2✔
65
    this.context = context;
3✔
66
    this.processBuilder = new ProcessBuilder();
7✔
67
    this.errorHandling = ProcessErrorHandling.THROW_ERR;
3✔
68
    Map<String, String> environment = this.processBuilder.environment();
4✔
69
    for (VariableLine var : this.context.getVariables().collectExportedVariables()) {
13✔
70
      if (var.isExport()) {
3!
71
        environment.put(var.getName(), var.getValue());
7✔
72
      }
73
    }
1✔
74
    this.arguments = new ArrayList<>();
5✔
75
    this.extraPathEntries = new ArrayList<>();
5✔
76
    this.exitCodeAcceptor = EXIT_CODE_ACCEPTOR;
3✔
77
  }
1✔
78

79
  private ProcessContextImpl(ProcessContextImpl parent) {
80

81
    super();
×
82
    this.context = parent.context;
×
83
    this.processBuilder = parent.processBuilder;
×
84
    this.errorHandling = ProcessErrorHandling.THROW_ERR;
×
85
    this.arguments = new ArrayList<>();
×
86
    this.extraPathEntries = parent.extraPathEntries;
×
87
    this.exitCodeAcceptor = EXIT_CODE_ACCEPTOR;
×
88
  }
×
89

90
  @Override
91
  public ProcessContext errorHandling(ProcessErrorHandling handling) {
92

93
    Objects.requireNonNull(handling);
3✔
94
    this.errorHandling = handling;
3✔
95
    return this;
2✔
96
  }
97

98
  @Override
99
  public ProcessContext directory(Path directory) {
100

101
    if (directory != null) {
2!
102
      this.processBuilder.directory(directory.toFile());
7✔
103
    } else {
104
      this.context.debug(
×
105
          "Could not set the process builder's working directory! Directory of the current java process is used.");
106
    }
107

108
    return this;
2✔
109
  }
110

111
  @Override
112
  public ProcessContext executable(Path command) {
113

114
    if (!this.arguments.isEmpty()) {
4!
115
      throw new IllegalStateException("Arguments already present - did you forget to call run for previous call?");
×
116
    }
117

118
    this.executable = command;
3✔
119
    return this;
2✔
120
  }
121

122
  @Override
123
  public ProcessContext addArg(String arg) {
124

125
    this.arguments.add(arg);
5✔
126
    return this;
2✔
127
  }
128

129
  @Override
130
  public ProcessContext withEnvVar(String key, String value) {
131

132
    if (IdeVariables.PATH.getName().equals(key)) {
5!
133
      this.overriddenPath = value;
×
134
    } else {
135
      this.context.trace("Setting process environment variable {}={}", key, value);
14✔
136
      this.processBuilder.environment().put(key, value);
7✔
137
    }
138
    return this;
2✔
139
  }
140

141
  @Override
142
  public ProcessContext withPathEntry(Path path) {
143

144
    this.extraPathEntries.add(path);
5✔
145
    return this;
2✔
146
  }
147

148
  @Override
149
  public ProcessContext withExitCodeAcceptor(Predicate<Integer> exitCodeAcceptor) {
150

151
    this.exitCodeAcceptor = exitCodeAcceptor;
3✔
152
    return this;
2✔
153
  }
154

155
  @Override
156
  public ProcessContext createChild() {
157

158
    return new ProcessContextImpl(this);
×
159
  }
160

161
  @Override
162
  public void setOutputListener(OutputListener listener) {
163
    this.outputListener = listener;
3✔
164
  }
1✔
165

166
  @Override
167
  public ProcessResult run(ProcessMode processMode) {
168

169
    if (this.executable == null) {
3✔
170
      throw new IllegalStateException("Missing executable to run process!");
5✔
171
    }
172

173
    SystemPath systemPath = this.context.getPath();
4✔
174
    if ((this.overriddenPath != null) || !this.extraPathEntries.isEmpty()) {
7!
175
      systemPath = systemPath.withPath(this.overriddenPath, this.extraPathEntries);
7✔
176
    }
177
    String path = systemPath.toString();
3✔
178
    this.context.trace("Setting PATH for process execution of {} to {}", this.executable.getFileName(), path);
16✔
179
    this.executable = systemPath.findBinary(this.executable);
6✔
180
    this.processBuilder.environment().put(IdeVariables.PATH.getName(), path);
8✔
181
    List<String> args = new ArrayList<>(this.arguments.size() + 4);
9✔
182
    String interpreter = addExecutable(args);
4✔
183
    args.addAll(this.arguments);
5✔
184
    String command = createCommand();
3✔
185
    if (this.context.debug().isEnabled()) {
5!
186
      String message = createCommandMessage(interpreter, " ...");
5✔
187
      this.context.debug(message);
4✔
188
    }
189

190
    try {
191
      applyRedirects(processMode);
3✔
192
      if (processMode.isBackground()) {
3✔
193
        modifyArgumentsOnBackgroundProcess(processMode);
3✔
194
      }
195

196
      this.processBuilder.command(args);
5✔
197

198
      ConcurrentLinkedQueue<OutputMessage> output = new ConcurrentLinkedQueue<>();
4✔
199

200
      Process process = this.processBuilder.start();
4✔
201

202
      try {
203
        if (Redirect.PIPE == processMode.getRedirectOutput() || Redirect.PIPE == processMode.getRedirectError()) {
8!
204
          CompletableFuture<Void> outFut = readInputStream(process.getInputStream(), false, output);
6✔
205
          CompletableFuture<Void> errFut = readInputStream(process.getErrorStream(), true, output);
6✔
206
          if (Redirect.PIPE == processMode.getRedirectOutput()) {
4!
207
            outFut.get();
3✔
208
            if (this.outputListener != null) {
3✔
209
              for (OutputMessage msg : output) {
10✔
210
                this.outputListener.onOutput(msg.message(), msg.error());
7✔
211
              }
1✔
212
            }
213
          }
214

215
          if (Redirect.PIPE == processMode.getRedirectError()) {
4!
216
            errFut.get();
3✔
217
            if (this.outputListener != null) {
3✔
218
              for (OutputMessage msg : output) {
10✔
219
                this.outputListener.onOutput(msg.message(), msg.error());
7✔
220
              }
1✔
221
            }
222
          }
223
        }
224

225
        int exitCode;
226

227
        if (processMode.isBackground()) {
3✔
228
          exitCode = ProcessResult.SUCCESS;
3✔
229
        } else {
230
          exitCode = process.waitFor();
3✔
231
        }
232

233
        List<OutputMessage> finalOutput = new ArrayList<>(output);
5✔
234
        boolean success = this.exitCodeAcceptor.test(exitCode);
6✔
235
        ProcessResult result = new ProcessResultImpl(this.executable.getFileName().toString(), command, exitCode, success, finalOutput);
12✔
236

237
        performLogging(result, exitCode, interpreter);
5✔
238

239
        return result;
4✔
240
      } finally {
241
        if (!processMode.isBackground()) {
3✔
242
          process.destroy();
2✔
243
        }
244
      }
245
    } catch (CliProcessException | IllegalStateException e) {
1✔
246
      // these exceptions are thrown from performLogOnError and we do not want to wrap them (see #593)
247
      throw e;
2✔
248
    } catch (Exception e) {
1✔
249
      String msg = e.getMessage();
3✔
250
      if ((msg == null) || msg.isEmpty()) {
5!
251
        msg = e.getClass().getSimpleName();
×
252
      }
253
      throw new IllegalStateException(createCommandMessage(interpreter, " failed: " + msg), e);
10✔
254
    } finally {
255
      this.arguments.clear();
3✔
256
    }
257
  }
258

259
  /**
260
   * Asynchronously and parallel reads {@link InputStream input stream} and stores it in {@link CompletableFuture}. Inspired by: <a href=
261
   * "https://stackoverflow.com/questions/14165517/processbuilder-forwarding-stdout-and-stderr-of-started-processes-without-blocki/57483714#57483714">StackOverflow</a>
262
   *
263
   * @param is {@link InputStream}.
264
   * @param errorStream to identify if the output came from stdout or stderr
265
   * @return {@link CompletableFuture}.
266
   */
267
  private static CompletableFuture<Void> readInputStream(InputStream is, boolean errorStream, ConcurrentLinkedQueue<OutputMessage> outputMessages) {
268

269
    return CompletableFuture.supplyAsync(() -> {
6✔
270

271
      try (InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) {
10✔
272

273
        String line;
274
        while ((line = br.readLine()) != null) {
5✔
275
          OutputMessage outputMessage = new OutputMessage(errorStream, line);
6✔
276
          outputMessages.add(outputMessage);
4✔
277
        }
1✔
278

279
        return null;
4✔
280
      } catch (Throwable e) {
1✔
281
        throw new RuntimeException("There was a problem while executing the program", e);
6✔
282
      }
283
    });
284
  }
285

286
  private String createCommand() {
287
    String cmd = this.executable.toString();
4✔
288
    StringBuilder sb = new StringBuilder(cmd.length() + this.arguments.size() * 4);
12✔
289
    sb.append(cmd);
4✔
290
    for (String arg : this.arguments) {
11✔
291
      sb.append(' ');
4✔
292
      sb.append(arg);
4✔
293
    }
1✔
294
    return sb.toString();
3✔
295
  }
296

297
  private String createCommandMessage(String interpreter, String suffix) {
298

299
    StringBuilder sb = new StringBuilder();
4✔
300
    sb.append("Running command '");
4✔
301
    sb.append(this.executable);
5✔
302
    sb.append("'");
4✔
303
    if (interpreter != null) {
2✔
304
      sb.append(" using ");
4✔
305
      sb.append(interpreter);
4✔
306
    }
307
    int size = this.arguments.size();
4✔
308
    if (size > 0) {
2✔
309
      sb.append(" with arguments");
4✔
310
      for (int i = 0; i < size; i++) {
7✔
311
        String arg = this.arguments.get(i);
6✔
312
        sb.append(" '");
4✔
313
        sb.append(arg);
4✔
314
        sb.append("'");
4✔
315
      }
316
    }
317
    sb.append(suffix);
4✔
318
    return sb.toString();
3✔
319
  }
320

321
  private String getSheBang(Path file) {
322

323
    try (InputStream in = Files.newInputStream(file)) {
5✔
324
      // "#!/usr/bin/env bash".length() = 19
325
      byte[] buffer = new byte[32];
3✔
326
      int read = in.read(buffer);
4✔
327
      if ((read > 2) && (buffer[0] == '#') && (buffer[1] == '!')) {
13!
328
        int start = 2;
2✔
329
        int end = 2;
2✔
330
        while (end < read) {
3!
331
          byte c = buffer[end];
4✔
332
          if ((c == '\n') || (c == '\r') || (c > 127)) {
9!
333
            break;
×
334
          } else if ((end == start) && (c == ' ')) {
6!
335
            start++;
×
336
          }
337
          end++;
1✔
338
        }
1✔
339
        String sheBang = new String(buffer, start, end - start, StandardCharsets.US_ASCII).trim();
11✔
340
        if (sheBang.startsWith(PREFIX_USR_BIN_ENV)) {
4!
341
          sheBang = sheBang.substring(PREFIX_USR_BIN_ENV.length());
×
342
        }
343
        return sheBang;
4✔
344
      }
345
    } catch (IOException e) {
5!
346
      // ignore...
347
    }
1✔
348
    return null;
2✔
349
  }
350

351
  private String addExecutable(List<String> args) {
352

353
    String interpreter = null;
2✔
354
    String fileExtension = FilenameUtil.getExtension(this.executable.getFileName().toString());
6✔
355
    boolean isBashScript = "sh".equals(fileExtension);
4✔
356
    this.context.getFileAccess().makeExecutable(this.executable, true);
7✔
357
    if (!isBashScript) {
2✔
358
      String sheBang = getSheBang(this.executable);
5✔
359
      if (sheBang != null) {
2✔
360
        String cmd = sheBang;
2✔
361
        int lastSlash = cmd.lastIndexOf('/');
4✔
362
        if (lastSlash >= 0) {
2!
363
          cmd = cmd.substring(lastSlash + 1);
6✔
364
        }
365
        if (cmd.equals("bash")) {
4!
366
          isBashScript = true;
2✔
367
        } else {
368
          // currently we do not support other interpreters...
369
        }
370
      }
371
    }
372
    if (isBashScript) {
2✔
373
      interpreter = "bash";
2✔
374
      args.add(this.context.findBashRequired().toString());
7✔
375
    }
376
    if ("msi".equalsIgnoreCase(fileExtension)) {
4!
377
      args.add(0, "/i");
×
378
      args.add(0, "msiexec");
×
379
    }
380
    args.add(this.executable.toString());
6✔
381
    return interpreter;
2✔
382
  }
383

384
  private void performLogging(ProcessResult result, int exitCode, String interpreter) {
385

386
    if (!result.isSuccessful() && (this.errorHandling != ProcessErrorHandling.NONE)) {
7!
387
      IdeLogLevel ideLogLevel = this.errorHandling.getLogLevel();
4✔
388
      String message = createCommandMessage(interpreter, "\nfailed with exit code " + exitCode + "!");
6✔
389

390
      context.level(ideLogLevel).log(message);
6✔
391
      result.log(ideLogLevel, context);
5✔
392

393
      if (this.errorHandling == ProcessErrorHandling.THROW_CLI) {
4!
394
        throw new CliProcessException(message, result);
×
395
      } else if (this.errorHandling == ProcessErrorHandling.THROW_ERR) {
4✔
396
        throw new IllegalStateException(message);
5✔
397
      }
398
    }
399
  }
1✔
400

401
  private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) {
402

403
    assert processMode.isBackground() : "Cannot handle non background process mode!";
4!
404

405
    Path bash = this.context.findBash();
4✔
406
    if (bash == null) {
2!
407
      this.context.warning(
×
408
          "Cannot start background process via bash because no bash installation was found. Hence, output will be discarded.");
409
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
410
      return;
×
411
    }
412

413
    String commandToRunInBackground = buildCommandToRunInBackground();
3✔
414

415
    this.arguments.clear();
3✔
416
    this.arguments.add(bash.toString());
6✔
417
    this.arguments.add("-c");
5✔
418
    commandToRunInBackground += " & disown";
3✔
419
    this.arguments.add(commandToRunInBackground);
5✔
420

421
  }
1✔
422

423
  private void applyRedirects(ProcessMode processMode) {
424

425
    Redirect output = processMode.getRedirectOutput();
3✔
426
    Redirect error = processMode.getRedirectError();
3✔
427
    Redirect input = processMode.getRedirectInput();
3✔
428

429
    if (output != null) {
2!
430
      this.processBuilder.redirectOutput(output);
5✔
431
    }
432
    if (error != null) {
2!
433
      this.processBuilder.redirectError(error);
5✔
434
    }
435
    if (input != null) {
2✔
436
      this.processBuilder.redirectInput(input);
5✔
437
    }
438
  }
1✔
439

440
  private String buildCommandToRunInBackground() {
441

442
    if (this.context.getSystemInfo().isWindows()) {
5!
443

444
      StringBuilder stringBuilder = new StringBuilder();
×
445

446
      for (String argument : this.arguments) {
×
447

448
        if (SystemInfoImpl.INSTANCE.isWindows() && SystemPath.isValidWindowsPath(argument)) {
×
449
          argument = WindowsPathSyntax.MSYS.normalize(argument);
×
450
        }
451

452
        stringBuilder.append(argument);
×
453
        stringBuilder.append(" ");
×
454
      }
×
455
      return stringBuilder.toString().trim();
×
456
    } else {
457
      return this.arguments.stream().map(Object::toString).collect(Collectors.joining(" "));
10✔
458
    }
459
  }
460
}
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