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

devonfw / IDEasy / 18680241255

21 Oct 2025 10:03AM UTC coverage: 68.407% (-0.1%) from 68.522%
18680241255

Pull #1529

github

web-flow
Merge 6c92cf30d into 03c8a307b
Pull Request #1529: #1521: Use wiremock for npm repository.

3457 of 5541 branches covered (62.39%)

Branch coverage included in aggregate %.

9045 of 12735 relevant lines covered (71.02%)

3.12 hits per line

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

81.06
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.stream.Collectors;
18

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

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

34
  private static final String PREFIX_USR_BIN_ENV = "/usr/bin/env ";
35

36
  /** The owning {@link IdeContext}. */
37
  protected final IdeContext context;
38

39
  private final ProcessBuilder processBuilder;
40

41
  private final List<String> arguments;
42

43
  private Path executable;
44

45
  private String overriddenPath;
46

47
  private final List<Path> extraPathEntries;
48

49
  private ProcessErrorHandling errorHandling;
50

51
  private OutputListener outputListener;
52

53
  /**
54
   * The constructor.
55
   *
56
   * @param context the owning {@link IdeContext}.
57
   */
58
  public ProcessContextImpl(IdeContext context) {
59

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

74
  @Override
75
  public ProcessContext errorHandling(ProcessErrorHandling handling) {
76

77
    Objects.requireNonNull(handling);
3✔
78
    this.errorHandling = handling;
3✔
79
    return this;
2✔
80
  }
81

82
  @Override
83
  public ProcessContext directory(Path directory) {
84

85
    if (directory != null) {
×
86
      this.processBuilder.directory(directory.toFile());
×
87
    } else {
88
      this.context.debug(
×
89
          "Could not set the process builder's working directory! Directory of the current java process is used.");
90
    }
91

92
    return this;
×
93
  }
94

95
  @Override
96
  public ProcessContext executable(Path command) {
97

98
    if (!this.arguments.isEmpty()) {
4!
99
      throw new IllegalStateException("Arguments already present - did you forget to call run for previous call?");
×
100
    }
101

102
    this.executable = command;
3✔
103
    return this;
2✔
104
  }
105

106
  @Override
107
  public ProcessContext addArg(String arg) {
108

109
    this.arguments.add(arg);
5✔
110
    return this;
2✔
111
  }
112

113
  @Override
114
  public ProcessContext withEnvVar(String key, String value) {
115

116
    if (IdeVariables.PATH.getName().equals(key)) {
5!
117
      this.overriddenPath = value;
×
118
    } else {
119
      this.context.trace("Setting process environment variable {}={}", key, value);
14✔
120
      this.processBuilder.environment().put(key, value);
7✔
121
    }
122
    return this;
2✔
123
  }
124

125
  @Override
126
  public ProcessContext withPathEntry(Path path) {
127

128
    this.extraPathEntries.add(path);
5✔
129
    return this;
2✔
130
  }
131

132
  @Override
133
  public void setOutputListener(OutputListener listener) {
134
    this.outputListener = listener;
3✔
135
  }
1✔
136

137
  @Override
138
  public ProcessResult run(ProcessMode processMode) {
139

140
    if (this.executable == null) {
3✔
141
      throw new IllegalStateException("Missing executable to run process!");
5✔
142
    }
143

144
    SystemPath systemPath = this.context.getPath();
4✔
145
    if ((this.overriddenPath != null) || !this.extraPathEntries.isEmpty()) {
7!
146
      systemPath = systemPath.withPath(this.overriddenPath, this.extraPathEntries);
7✔
147
    }
148
    String path = systemPath.toString();
3✔
149
    this.context.trace("Setting PATH for process execution of {} to {}", this.executable.getFileName(), path);
16✔
150
    this.executable = systemPath.findBinary(this.executable);
6✔
151
    this.processBuilder.environment().put(IdeVariables.PATH.getName(), path);
8✔
152
    List<String> args = new ArrayList<>(this.arguments.size() + 4);
9✔
153
    String interpreter = addExecutable(args);
4✔
154
    args.addAll(this.arguments);
5✔
155
    String command = createCommand();
3✔
156
    if (this.context.debug().isEnabled()) {
5!
157
      String message = createCommandMessage(interpreter, " ...");
5✔
158
      this.context.debug(message);
4✔
159
    }
160

161
    try {
162
      applyRedirects(processMode);
3✔
163
      if (processMode.isBackground()) {
3✔
164
        modifyArgumentsOnBackgroundProcess(processMode);
3✔
165
      }
166

167
      this.processBuilder.command(args);
5✔
168

169
      ConcurrentLinkedQueue<OutputMessage> output = new ConcurrentLinkedQueue<>();
4✔
170

171
      Process process = this.processBuilder.start();
4✔
172

173
      try {
174
        if (Redirect.PIPE == processMode.getRedirectOutput() || Redirect.PIPE == processMode.getRedirectError()) {
8!
175
          CompletableFuture<Void> outFut = readInputStream(process.getInputStream(), false, output);
6✔
176
          CompletableFuture<Void> errFut = readInputStream(process.getErrorStream(), true, output);
6✔
177
          if (Redirect.PIPE == processMode.getRedirectOutput()) {
4!
178
            outFut.get();
3✔
179
            if (this.outputListener != null) {
3✔
180
              for (OutputMessage msg : output) {
10✔
181
                this.outputListener.onOutput(msg.message(), msg.error());
7✔
182
              }
1✔
183
            }
184
          }
185

186
          if (Redirect.PIPE == processMode.getRedirectError()) {
4!
187
            errFut.get();
3✔
188
            if (this.outputListener != null) {
3✔
189
              for (OutputMessage msg : output) {
10✔
190
                this.outputListener.onOutput(msg.message(), msg.error());
7✔
191
              }
1✔
192
            }
193
          }
194
        }
195

196
        int exitCode;
197

198
        if (processMode.isBackground()) {
3✔
199
          exitCode = ProcessResult.SUCCESS;
3✔
200
        } else {
201
          exitCode = process.waitFor();
3✔
202
        }
203

204
        List<OutputMessage> finalOutput = new ArrayList<>(output);
5✔
205
        ProcessResult result = new ProcessResultImpl(this.executable.getFileName().toString(), command, exitCode, finalOutput);
11✔
206

207
        performLogging(result, exitCode, interpreter);
5✔
208

209
        return result;
4✔
210
      } finally {
211
        if (!processMode.isBackground()) {
3✔
212
          process.destroy();
2✔
213
        }
214
      }
215
    } catch (CliProcessException | IllegalStateException e) {
1✔
216
      // these exceptions are thrown from performLogOnError and we do not want to wrap them (see #593)
217
      throw e;
2✔
218
    } catch (Exception e) {
1✔
219
      String msg = e.getMessage();
3✔
220
      if ((msg == null) || msg.isEmpty()) {
5!
221
        msg = e.getClass().getSimpleName();
×
222
      }
223
      throw new IllegalStateException(createCommandMessage(interpreter, " failed: " + msg), e);
10✔
224
    } finally {
225
      this.arguments.clear();
3✔
226
    }
227
  }
228

229
  /**
230
   * Asynchronously and parallel reads {@link InputStream input stream} and stores it in {@link CompletableFuture}. Inspired by: <a href=
231
   * "https://stackoverflow.com/questions/14165517/processbuilder-forwarding-stdout-and-stderr-of-started-processes-without-blocki/57483714#57483714">StackOverflow</a>
232
   *
233
   * @param is {@link InputStream}.
234
   * @param errorStream to identify if the output came from stdout or stderr
235
   * @return {@link CompletableFuture}.
236
   */
237
  private static CompletableFuture<Void> readInputStream(InputStream is, boolean errorStream, ConcurrentLinkedQueue<OutputMessage> outputMessages) {
238

239
    return CompletableFuture.supplyAsync(() -> {
6✔
240

241
      try (InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) {
10✔
242

243
        String line;
244
        while ((line = br.readLine()) != null) {
5✔
245
          OutputMessage outputMessage = new OutputMessage(errorStream, line);
6✔
246
          outputMessages.add(outputMessage);
4✔
247
        }
1✔
248

249
        return null;
4✔
250
      } catch (Throwable e) {
1✔
251
        throw new RuntimeException("There was a problem while executing the program", e);
6✔
252
      }
253
    });
254
  }
255

256
  private String createCommand() {
257
    String cmd = this.executable.toString();
4✔
258
    StringBuilder sb = new StringBuilder(cmd.length() + this.arguments.size() * 4);
12✔
259
    sb.append(cmd);
4✔
260
    for (String arg : this.arguments) {
11✔
261
      sb.append(' ');
4✔
262
      sb.append(arg);
4✔
263
    }
1✔
264
    return sb.toString();
3✔
265
  }
266

267
  private String createCommandMessage(String interpreter, String suffix) {
268

269
    StringBuilder sb = new StringBuilder();
4✔
270
    sb.append("Running command '");
4✔
271
    sb.append(this.executable);
5✔
272
    sb.append("'");
4✔
273
    if (interpreter != null) {
2✔
274
      sb.append(" using ");
4✔
275
      sb.append(interpreter);
4✔
276
    }
277
    int size = this.arguments.size();
4✔
278
    if (size > 0) {
2✔
279
      sb.append(" with arguments");
4✔
280
      for (int i = 0; i < size; i++) {
7✔
281
        String arg = this.arguments.get(i);
6✔
282
        sb.append(" '");
4✔
283
        sb.append(arg);
4✔
284
        sb.append("'");
4✔
285
      }
286
    }
287
    sb.append(suffix);
4✔
288
    return sb.toString();
3✔
289
  }
290

291
  private String getSheBang(Path file) {
292

293
    try (InputStream in = Files.newInputStream(file)) {
5✔
294
      // "#!/usr/bin/env bash".length() = 19
295
      byte[] buffer = new byte[32];
3✔
296
      int read = in.read(buffer);
4✔
297
      if ((read > 2) && (buffer[0] == '#') && (buffer[1] == '!')) {
13!
298
        int start = 2;
2✔
299
        int end = 2;
2✔
300
        while (end < read) {
3!
301
          byte c = buffer[end];
4✔
302
          if ((c == '\n') || (c == '\r') || (c > 127)) {
9!
303
            break;
×
304
          } else if ((end == start) && (c == ' ')) {
6!
305
            start++;
×
306
          }
307
          end++;
1✔
308
        }
1✔
309
        String sheBang = new String(buffer, start, end - start, StandardCharsets.US_ASCII).trim();
11✔
310
        if (sheBang.startsWith(PREFIX_USR_BIN_ENV)) {
4!
311
          sheBang = sheBang.substring(PREFIX_USR_BIN_ENV.length());
×
312
        }
313
        return sheBang;
4✔
314
      }
315
    } catch (IOException e) {
5!
316
      // ignore...
317
    }
1✔
318
    return null;
2✔
319
  }
320

321
  private String addExecutable(List<String> args) {
322

323
    String interpreter = null;
2✔
324
    String fileExtension = FilenameUtil.getExtension(this.executable.getFileName().toString());
6✔
325
    boolean isBashScript = "sh".equals(fileExtension);
4✔
326
    this.context.getFileAccess().makeExecutable(this.executable, true);
7✔
327
    if (!isBashScript) {
2✔
328
      String sheBang = getSheBang(this.executable);
5✔
329
      if (sheBang != null) {
2✔
330
        String cmd = sheBang;
2✔
331
        int lastSlash = cmd.lastIndexOf('/');
4✔
332
        if (lastSlash >= 0) {
2!
333
          cmd = cmd.substring(lastSlash + 1);
6✔
334
        }
335
        if (cmd.equals("bash")) {
4!
336
          isBashScript = true;
2✔
337
        } else {
338
          // currently we do not support other interpreters...
339
        }
340
      }
341
    }
342
    if (isBashScript) {
2✔
343
      interpreter = "bash";
2✔
344
      args.add(this.context.findBashRequired());
6✔
345
    }
346
    if ("msi".equalsIgnoreCase(fileExtension)) {
4!
347
      args.add(0, "/i");
×
348
      args.add(0, "msiexec");
×
349
    }
350
    args.add(this.executable.toString());
6✔
351
    return interpreter;
2✔
352
  }
353

354
  private void performLogging(ProcessResult result, int exitCode, String interpreter) {
355

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

360
      context.level(ideLogLevel).log(message);
6✔
361
      result.log(ideLogLevel, context);
5✔
362

363
      if (this.errorHandling == ProcessErrorHandling.THROW_CLI) {
4!
364
        throw new CliProcessException(message, result);
×
365
      } else if (this.errorHandling == ProcessErrorHandling.THROW_ERR) {
4✔
366
        throw new IllegalStateException(message);
5✔
367
      }
368
    }
369
  }
1✔
370

371
  private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) {
372

373
    if (!processMode.isBackground()) {
3!
374
      throw new IllegalStateException("Cannot handle non background process mode!");
×
375
    }
376

377
    String bash = this.context.findBash();
4✔
378
    if (bash == null) {
2!
379
      this.context.warning(
×
380
          "Cannot start background process via bash because no bash installation was found. Hence, output will be discarded.");
381
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
382
      return;
×
383
    }
384

385
    String commandToRunInBackground = buildCommandToRunInBackground();
3✔
386

387
    this.arguments.clear();
3✔
388
    this.arguments.add(bash);
5✔
389
    this.arguments.add("-c");
5✔
390
    commandToRunInBackground += " & disown";
3✔
391
    this.arguments.add(commandToRunInBackground);
5✔
392

393
  }
1✔
394

395
  private void applyRedirects(ProcessMode processMode) {
396

397
    Redirect output = processMode.getRedirectOutput();
3✔
398
    Redirect error = processMode.getRedirectError();
3✔
399
    Redirect input = processMode.getRedirectInput();
3✔
400

401
    if (output != null) {
2!
402
      this.processBuilder.redirectOutput(output);
5✔
403
    }
404
    if (error != null) {
2!
405
      this.processBuilder.redirectError(error);
5✔
406
    }
407
    if (input != null) {
2✔
408
      this.processBuilder.redirectInput(input);
5✔
409
    }
410
  }
1✔
411

412
  private String buildCommandToRunInBackground() {
413

414
    if (this.context.getSystemInfo().isWindows()) {
5!
415

416
      StringBuilder stringBuilder = new StringBuilder();
×
417

418
      for (String argument : this.arguments) {
×
419

420
        if (SystemInfoImpl.INSTANCE.isWindows() && SystemPath.isValidWindowsPath(argument)) {
×
421
          argument = WindowsPathSyntax.MSYS.normalize(argument);
×
422
        }
423

424
        stringBuilder.append(argument);
×
425
        stringBuilder.append(" ");
×
426
      }
×
427
      return stringBuilder.toString().trim();
×
428
    } else {
429
      return this.arguments.stream().map(Object::toString).collect(Collectors.joining(" "));
10✔
430
    }
431
  }
432
}
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

© 2025 Coveralls, Inc