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

devonfw / IDEasy / 12125578396

02 Dec 2024 06:25PM UTC coverage: 67.265% (+0.3%) from 67.008%
12125578396

push

github

web-flow
#824: fix git url with branch (#828)

2509 of 4080 branches covered (61.5%)

Branch coverage included in aggregate %.

6565 of 9410 relevant lines covered (69.77%)

3.08 hits per line

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

80.68
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.stream.Collectors;
17

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

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

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

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

38
  private final ProcessBuilder processBuilder;
39

40
  private final List<String> arguments;
41

42
  private Path executable;
43

44
  private String overriddenPath;
45

46
  private final List<Path> extraPathEntries;
47

48
  private ProcessErrorHandling errorHandling;
49

50
  /**
51
   * The constructor.
52
   *
53
   * @param context the owning {@link IdeContext}.
54
   */
55
  public ProcessContextImpl(IdeContext context) {
56

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

71
  @Override
72
  public ProcessContext errorHandling(ProcessErrorHandling handling) {
73

74
    Objects.requireNonNull(handling);
3✔
75
    this.errorHandling = handling;
3✔
76
    return this;
2✔
77
  }
78

79
  @Override
80
  public ProcessContext directory(Path directory) {
81

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

89
    return this;
×
90
  }
91

92
  @Override
93
  public ProcessContext executable(Path command) {
94

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

99
    this.executable = command;
3✔
100
    return this;
2✔
101
  }
102

103
  @Override
104
  public ProcessContext addArg(String arg) {
105

106
    this.arguments.add(arg);
5✔
107
    return this;
2✔
108
  }
109

110
  @Override
111
  public ProcessContext withEnvVar(String key, String value) {
112

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

122
  @Override
123
  public ProcessContext withPathEntry(Path path) {
124

125
    this.extraPathEntries.add(path);
5✔
126
    return this;
2✔
127
  }
128

129
  @Override
130
  public ProcessResult run(ProcessMode processMode) {
131

132
    if (processMode == ProcessMode.DEFAULT) {
3✔
133
      this.processBuilder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT);
7✔
134
    }
135

136
    if (processMode == ProcessMode.DEFAULT_SILENT) {
3!
137
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
138
    }
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(this.executable.toString(), args);
7✔
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

163
      if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
164
        this.processBuilder.redirectOutput(Redirect.PIPE).redirectError(Redirect.PIPE);
8✔
165
      } else if (processMode.isBackground()) {
3✔
166
        modifyArgumentsOnBackgroundProcess(processMode);
3✔
167
      }
168

169
      this.processBuilder.command(args);
5✔
170

171
      List<String> out = null;
2✔
172
      List<String> err = null;
2✔
173

174
      Process process = this.processBuilder.start();
4✔
175

176
      try {
177
        if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
178
          CompletableFuture<List<String>> outFut = readInputStream(process.getInputStream());
4✔
179
          CompletableFuture<List<String>> errFut = readInputStream(process.getErrorStream());
4✔
180
          out = outFut.get();
4✔
181
          err = errFut.get();
4✔
182
        }
183

184
        int exitCode;
185

186
        if (processMode.isBackground()) {
3✔
187
          exitCode = ProcessResult.SUCCESS;
3✔
188
        } else {
189
          exitCode = process.waitFor();
3✔
190
        }
191

192
        ProcessResult result = new ProcessResultImpl(this.executable.getFileName().toString(), command, exitCode, out, err);
12✔
193

194
        performLogging(result, exitCode, interpreter);
5✔
195

196
        return result;
4✔
197
      } finally {
198
        process.destroy();
2✔
199
      }
200
    } catch (CliProcessException | IllegalStateException e) {
1✔
201
      // these exceptions are thrown from performLogOnError and we do not want to wrap them (see #593)
202
      throw e;
2✔
203
    } catch (Exception e) {
1✔
204
      String msg = e.getMessage();
3✔
205
      if ((msg == null) || msg.isEmpty()) {
5!
206
        msg = e.getClass().getSimpleName();
×
207
      }
208
      throw new IllegalStateException(createCommandMessage(interpreter, " failed: " + msg), e);
10✔
209
    } finally {
210
      this.arguments.clear();
3✔
211
    }
212
  }
213

214
  /**
215
   * Asynchronously and parallel reads {@link InputStream input stream} and stores it in {@link CompletableFuture}. Inspired by: <a href=
216
   * "https://stackoverflow.com/questions/14165517/processbuilder-forwarding-stdout-and-stderr-of-started-processes-without-blocki/57483714#57483714">StackOverflow</a>
217
   *
218
   * @param is {@link InputStream}.
219
   * @return {@link CompletableFuture}.
220
   */
221
  private static CompletableFuture<List<String>> readInputStream(InputStream is) {
222

223
    return CompletableFuture.supplyAsync(() -> {
4✔
224

225
      try (InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) {
10✔
226
        return br.lines().toList();
6✔
227
      } catch (Throwable e) {
1✔
228
        throw new RuntimeException("There was a problem while executing the program", e);
6✔
229
      }
230
    });
231
  }
232

233
  private String createCommand() {
234
    String cmd = this.executable.toString();
4✔
235
    StringBuilder sb = new StringBuilder(cmd.length() + this.arguments.size() * 4);
12✔
236
    sb.append(cmd);
4✔
237
    for (String arg : this.arguments) {
11✔
238
      sb.append(' ');
4✔
239
      sb.append(arg);
4✔
240
    }
1✔
241
    return sb.toString();
3✔
242
  }
243

244
  private String createCommandMessage(String interpreter, String suffix) {
245

246
    StringBuilder sb = new StringBuilder();
4✔
247
    sb.append("Running command '");
4✔
248
    sb.append(this.executable);
5✔
249
    sb.append("'");
4✔
250
    if (interpreter != null) {
2✔
251
      sb.append(" using ");
4✔
252
      sb.append(interpreter);
4✔
253
    }
254
    int size = this.arguments.size();
4✔
255
    if (size > 0) {
2✔
256
      sb.append(" with arguments");
4✔
257
      for (int i = 0; i < size; i++) {
7✔
258
        String arg = this.arguments.get(i);
6✔
259
        sb.append(" '");
4✔
260
        sb.append(arg);
4✔
261
        sb.append("'");
4✔
262
      }
263
    }
264
    sb.append(suffix);
4✔
265
    return sb.toString();
3✔
266
  }
267

268
  private String getSheBang(Path file) {
269

270
    try (InputStream in = Files.newInputStream(file)) {
5✔
271
      // "#!/usr/bin/env bash".length() = 19
272
      byte[] buffer = new byte[32];
3✔
273
      int read = in.read(buffer);
4✔
274
      if ((read > 2) && (buffer[0] == '#') && (buffer[1] == '!')) {
13!
275
        int start = 2;
2✔
276
        int end = 2;
2✔
277
        while (end < read) {
3!
278
          byte c = buffer[end];
4✔
279
          if ((c == '\n') || (c == '\r') || (c > 127)) {
9!
280
            break;
×
281
          } else if ((end == start) && (c == ' ')) {
6!
282
            start++;
×
283
          }
284
          end++;
1✔
285
        }
1✔
286
        String sheBang = new String(buffer, start, end - start, StandardCharsets.US_ASCII).trim();
11✔
287
        if (sheBang.startsWith(PREFIX_USR_BIN_ENV)) {
4✔
288
          sheBang = sheBang.substring(PREFIX_USR_BIN_ENV.length());
5✔
289
        }
290
        return sheBang;
4✔
291
      }
292
    } catch (IOException e) {
5!
293
      // ignore...
294
    }
×
295
    return null;
2✔
296
  }
297

298
  private String addExecutable(String exec, List<String> args) {
299

300
    String interpreter = null;
2✔
301
    String fileExtension = FilenameUtil.getExtension(exec);
3✔
302
    boolean isBashScript = "sh".equals(fileExtension);
4✔
303
    if (!isBashScript) {
2✔
304
      String sheBang = getSheBang(this.executable);
5✔
305
      if (sheBang != null) {
2✔
306
        String cmd = sheBang;
2✔
307
        int lastSlash = cmd.lastIndexOf('/');
4✔
308
        if (lastSlash >= 0) {
2✔
309
          cmd = cmd.substring(lastSlash + 1);
6✔
310
        }
311
        if (cmd.equals("bash")) {
4!
312
          isBashScript = true;
2✔
313
        } else {
314
          // currently we do not support other interpreters...
315
        }
316
      }
317
    }
318
    if (isBashScript) {
2✔
319
      interpreter = "bash";
2✔
320
      args.add(this.context.findBashRequired());
6✔
321
    }
322
    if ("msi".equalsIgnoreCase(fileExtension)) {
4!
323
      args.add(0, "/i");
×
324
      args.add(0, "msiexec");
×
325
    }
326
    args.add(exec);
4✔
327
    return interpreter;
2✔
328
  }
329

330
  private void performLogging(ProcessResult result, int exitCode, String interpreter) {
331

332
    if (!result.isSuccessful() && (this.errorHandling != ProcessErrorHandling.NONE)) {
7!
333
      IdeLogLevel ideLogLevel;
334
      String message = createCommandMessage(interpreter, "\nfailed with exit code " + exitCode + "!");
6✔
335

336
      if (this.errorHandling == ProcessErrorHandling.LOG_ERROR) {
4✔
337
        ideLogLevel = IdeLogLevel.ERROR;
3✔
338
      } else if (this.errorHandling == ProcessErrorHandling.LOG_WARNING) {
4✔
339
        ideLogLevel = IdeLogLevel.WARNING;
3✔
340
      } else {
341
        ideLogLevel = IdeLogLevel.ERROR;
2✔
342
        this.context.error("Internal error: Undefined error handling {}", this.errorHandling);
11✔
343
      }
344

345
      context.level(ideLogLevel).log(message);
6✔
346
      result.log(ideLogLevel, context);
5✔
347

348
      if (this.errorHandling == ProcessErrorHandling.THROW_CLI) {
4!
349
        throw new CliProcessException(message, result);
×
350
      } else if (this.errorHandling == ProcessErrorHandling.THROW_ERR) {
4✔
351
        throw new IllegalStateException(message);
5✔
352
      }
353
    }
354
  }
1✔
355

356
  private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) {
357

358
    if (processMode == ProcessMode.BACKGROUND) {
3✔
359
      this.processBuilder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT);
8✔
360
    } else if (processMode == ProcessMode.BACKGROUND_SILENT) {
3!
361
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
8✔
362
    } else {
363
      throw new IllegalStateException("Cannot handle non background process mode!");
×
364
    }
365

366
    String bash = this.context.findBash();
4✔
367
    if (bash == null) {
2!
368
      this.context.warning(
×
369
          "Cannot start background process via bash because no bash installation was found. Hence, output will be discarded.");
370
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
371
      return;
×
372
    }
373

374
    String commandToRunInBackground = buildCommandToRunInBackground();
3✔
375

376
    this.arguments.clear();
3✔
377
    this.arguments.add(bash);
5✔
378
    this.arguments.add("-c");
5✔
379
    commandToRunInBackground += " & disown";
3✔
380
    this.arguments.add(commandToRunInBackground);
5✔
381

382
  }
1✔
383

384
  private String buildCommandToRunInBackground() {
385

386
    if (this.context.getSystemInfo().isWindows()) {
5!
387

388
      StringBuilder stringBuilder = new StringBuilder();
×
389

390
      for (String argument : this.arguments) {
×
391

392
        if (SystemInfoImpl.INSTANCE.isWindows() && SystemPath.isValidWindowsPath(argument)) {
×
393
          argument = WindowsPathSyntax.MSYS.normalize(argument);
×
394
        }
395

396
        stringBuilder.append(argument);
×
397
        stringBuilder.append(" ");
×
398
      }
×
399
      return stringBuilder.toString().trim();
×
400
    } else {
401
      return this.arguments.stream().map(Object::toString).collect(Collectors.joining(" "));
10✔
402
    }
403
  }
404
}
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