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

devonfw / IDEasy / 9907372175

12 Jul 2024 11:49AM UTC coverage: 61.142% (-0.02%) from 61.162%
9907372175

push

github

hohwille
fixed tests

1997 of 3595 branches covered (55.55%)

Branch coverage included in aggregate %.

5296 of 8333 relevant lines covered (63.55%)

2.8 hits per line

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

77.04
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.CliException;
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.IdeSubLogger;
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

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

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

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

37
  private final ProcessBuilder processBuilder;
38

39
  private final List<String> arguments;
40

41
  private Path executable;
42

43
  private ProcessErrorHandling errorHandling;
44

45
  /**
46
   * The constructor.
47
   *
48
   * @param context the owning {@link IdeContext}.
49
   */
50
  public ProcessContextImpl(IdeContext context) {
51

52
    super();
2✔
53
    this.context = context;
3✔
54
    this.processBuilder = new ProcessBuilder();
7✔
55
    this.errorHandling = ProcessErrorHandling.THROW;
3✔
56
    Map<String, String> environment = this.processBuilder.environment();
4✔
57
    for (VariableLine var : this.context.getVariables().collectExportedVariables()) {
13✔
58
      if (var.isExport()) {
3!
59
        environment.put(var.getName(), var.getValue());
7✔
60
      }
61
    }
1✔
62
    this.arguments = new ArrayList<>();
5✔
63
  }
1✔
64

65
  @Override
66
  public ProcessContext errorHandling(ProcessErrorHandling handling) {
67

68
    Objects.requireNonNull(handling);
3✔
69
    this.errorHandling = handling;
3✔
70
    return this;
2✔
71
  }
72

73
  @Override
74
  public ProcessContext directory(Path directory) {
75

76
    if (directory != null) {
×
77
      this.processBuilder.directory(directory.toFile());
×
78
    } else {
79
      this.context.debug(
×
80
          "Could not set the process builder's working directory! Directory of the current java process is used.");
81
    }
82

83
    return this;
×
84
  }
85

86
  @Override
87
  public ProcessContext executable(Path command) {
88

89
    if (!this.arguments.isEmpty()) {
4!
90
      throw new IllegalStateException("Arguments already present - did you forget to call run for previous call?");
×
91
    }
92

93
    this.executable = this.context.getPath().findBinary(command);
7✔
94
    return this;
2✔
95
  }
96

97
  @Override
98
  public ProcessContext addArg(String arg) {
99

100
    this.arguments.add(arg);
5✔
101
    return this;
2✔
102
  }
103

104
  @Override
105
  public ProcessContext withEnvVar(String key, String value) {
106

107
    this.processBuilder.environment().put(key, value);
7✔
108
    return this;
2✔
109
  }
110

111
  @Override
112
  public ProcessResult run(ProcessMode processMode) {
113

114
    // TODO ProcessMode needs to be configurable for GUI
115
    if (processMode == ProcessMode.DEFAULT) {
3✔
116
      this.processBuilder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT);
7✔
117
    }
118

119
    if (processMode == ProcessMode.DEFAULT_SILENT) {
3!
120
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
121
    }
122

123
    if (this.executable == null) {
3✔
124
      throw new IllegalStateException("Missing executable to run process!");
5✔
125
    }
126
    List<String> args = new ArrayList<>(this.arguments.size() + 4);
9✔
127
    String interpreter = addExecutable(this.executable.toString(), args);
7✔
128
    args.addAll(this.arguments);
5✔
129
    if (this.context.debug().isEnabled()) {
5!
130
      String message = createCommandMessage(interpreter, " ...");
5✔
131
      this.context.debug(message);
4✔
132
    }
133

134
    try {
135

136
      if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
137
        this.processBuilder.redirectOutput(Redirect.PIPE).redirectError(Redirect.PIPE);
8✔
138
      } else if (processMode.isBackground()) {
3✔
139
        modifyArgumentsOnBackgroundProcess(processMode);
3✔
140
      }
141

142
      this.processBuilder.command(args);
5✔
143

144
      List<String> out = null;
2✔
145
      List<String> err = null;
2✔
146

147
      Process process = this.processBuilder.start();
4✔
148

149
      if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
150
        CompletableFuture<List<String>> outFut = readInputStream(process.getInputStream());
4✔
151
        CompletableFuture<List<String>> errFut = readInputStream(process.getErrorStream());
4✔
152
        out = outFut.get();
4✔
153
        err = errFut.get();
4✔
154
      }
155

156
      int exitCode;
157
      if (processMode.isBackground()) {
3✔
158
        exitCode = ProcessResult.SUCCESS;
3✔
159
      } else {
160
        exitCode = process.waitFor();
3✔
161
      }
162

163
      ProcessResult result = new ProcessResultImpl(exitCode, out, err);
7✔
164
      performLogOnError(result, exitCode, interpreter);
5✔
165

166
      return result;
4✔
167

168
    } catch (Exception e) {
1✔
169
      String msg = e.getMessage();
3✔
170
      if ((msg == null) || msg.isEmpty()) {
5!
171
        msg = e.getClass().getSimpleName();
×
172
      }
173
      throw new IllegalStateException(createCommandMessage(interpreter, " failed: " + msg), e);
10✔
174
    } finally {
175
      this.arguments.clear();
3✔
176
    }
177
  }
178

179
  /**
180
   * Asynchronously and parallel reads {@link InputStream input stream} and stores it in {@link CompletableFuture}. Inspired by: <a href=
181
   * "https://stackoverflow.com/questions/14165517/processbuilder-forwarding-stdout-and-stderr-of-started-processes-without-blocki/57483714#57483714">StackOverflow</a>
182
   *
183
   * @param is {@link InputStream}.
184
   * @return {@link CompletableFuture}.
185
   */
186
  private static CompletableFuture<List<String>> readInputStream(InputStream is) {
187

188
    return CompletableFuture.supplyAsync(() -> {
4✔
189

190
      try (InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) {
10✔
191
        return br.lines().toList();
6✔
192
      } catch (Throwable e) {
×
193
        throw new RuntimeException("There was a problem while executing the program", e);
×
194
      }
195
    });
196
  }
197

198
  private String createCommandMessage(String interpreter, String suffix) {
199

200
    StringBuilder sb = new StringBuilder();
4✔
201
    sb.append("Running command '");
4✔
202
    sb.append(this.executable);
5✔
203
    sb.append("'");
4✔
204
    if (interpreter != null) {
2✔
205
      sb.append(" using ");
4✔
206
      sb.append(interpreter);
4✔
207
    }
208
    int size = this.arguments.size();
4✔
209
    if (size > 0) {
2✔
210
      sb.append(" with arguments");
4✔
211
      for (int i = 0; i < size; i++) {
7✔
212
        String arg = this.arguments.get(i);
6✔
213
        sb.append(" '");
4✔
214
        sb.append(arg);
4✔
215
        sb.append("'");
4✔
216
      }
217
    }
218
    sb.append(suffix);
4✔
219
    return sb.toString();
3✔
220
  }
221

222
  private String getSheBang(Path file) {
223

224
    try (InputStream in = Files.newInputStream(file)) {
5✔
225
      // "#!/usr/bin/env bash".length() = 19
226
      byte[] buffer = new byte[32];
3✔
227
      int read = in.read(buffer);
4✔
228
      if ((read > 2) && (buffer[0] == '#') && (buffer[1] == '!')) {
13!
229
        int start = 2;
2✔
230
        int end = 2;
2✔
231
        while (end < read) {
3!
232
          byte c = buffer[end];
4✔
233
          if ((c == '\n') || (c == '\r') || (c > 127)) {
9!
234
            break;
×
235
          } else if ((end == start) && (c == ' ')) {
6!
236
            start++;
×
237
          }
238
          end++;
1✔
239
        }
1✔
240
        String sheBang = new String(buffer, start, end - start, StandardCharsets.US_ASCII).trim();
11✔
241
        if (sheBang.startsWith(PREFIX_USR_BIN_ENV)) {
4!
242
          sheBang = sheBang.substring(PREFIX_USR_BIN_ENV.length());
×
243
        }
244
        return sheBang;
4✔
245
      }
246
    } catch (IOException e) {
5!
247
      // ignore...
248
    }
×
249
    return null;
2✔
250
  }
251

252
  private String addExecutable(String exec, List<String> args) {
253

254
    String interpreter = null;
2✔
255
    String fileExtension = FilenameUtil.getExtension(exec);
3✔
256
    boolean isBashScript = "sh".equals(fileExtension);
4✔
257
    if (!isBashScript) {
2✔
258
      String sheBang = getSheBang(this.executable);
5✔
259
      if (sheBang != null) {
2✔
260
        String cmd = sheBang;
2✔
261
        int lastSlash = cmd.lastIndexOf('/');
4✔
262
        if (lastSlash >= 0) {
2!
263
          cmd = cmd.substring(lastSlash + 1);
6✔
264
        }
265
        if (cmd.equals("bash")) {
4✔
266
          isBashScript = true;
2✔
267
        } else {
268
          // currently we do not support other interpreters...
269
        }
270
      }
271
    }
272
    if (isBashScript) {
2✔
273
      interpreter = "bash";
2✔
274
      args.add(this.context.findBash());
6✔
275
    }
276
    if ("msi".equalsIgnoreCase(fileExtension)) {
4!
277
      args.add(0, "/i");
×
278
      args.add(0, "msiexec");
×
279
    }
280
    args.add(exec);
4✔
281
    return interpreter;
2✔
282
  }
283

284
  private void performLogOnError(ProcessResult result, int exitCode, String interpreter) {
285

286
    if (!result.isSuccessful() && (this.errorHandling != ProcessErrorHandling.NONE)) {
7!
287
      String message = createCommandMessage(interpreter, " failed with exit code " + exitCode + "!");
6✔
288
      if (this.errorHandling == ProcessErrorHandling.THROW) {
4✔
289
        throw new CliException(message, exitCode);
6✔
290
      }
291
      IdeSubLogger level;
292
      if (this.errorHandling == ProcessErrorHandling.ERROR) {
4✔
293
        level = this.context.error();
5✔
294
      } else if (this.errorHandling == ProcessErrorHandling.WARNING) {
4!
295
        level = this.context.warning();
5✔
296
      } else {
297
        level = this.context.error();
×
298
        this.context.error("Internal error: Undefined error handling {}", this.errorHandling);
×
299
      }
300
      level.log(message);
3✔
301
    }
302
  }
1✔
303

304
  private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) {
305

306
    if (processMode == ProcessMode.BACKGROUND) {
3✔
307
      this.processBuilder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT);
8✔
308
    } else if (processMode == ProcessMode.BACKGROUND_SILENT) {
3!
309
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
8✔
310
    } else {
311
      throw new IllegalStateException("Cannot handle non background process mode!");
×
312
    }
313

314
    String bash = this.context.findBash();
4✔
315
    if (bash == null) {
2!
316
      this.context.warning(
×
317
          "Cannot start background process via bash because no bash installation was found. Hence, output will be discarded.");
318
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
319
      return;
×
320
    }
321

322
    String commandToRunInBackground = buildCommandToRunInBackground();
3✔
323

324
    this.arguments.clear();
3✔
325
    this.arguments.add(bash);
5✔
326
    this.arguments.add("-c");
5✔
327
    commandToRunInBackground += " & disown";
3✔
328
    this.arguments.add(commandToRunInBackground);
5✔
329

330
  }
1✔
331

332
  private String buildCommandToRunInBackground() {
333

334
    if (this.context.getSystemInfo().isWindows()) {
5!
335

336
      StringBuilder stringBuilder = new StringBuilder();
×
337

338
      for (String argument : this.arguments) {
×
339

340
        if (SystemInfoImpl.INSTANCE.isWindows() && SystemPath.isValidWindowsPath(argument)) {
×
341
          argument = WindowsPathSyntax.MSYS.normalize(argument);
×
342
        }
343

344
        stringBuilder.append(argument);
×
345
        stringBuilder.append(" ");
×
346
      }
×
347
      return stringBuilder.toString().trim();
×
348
    } else {
349
      return this.arguments.stream().map(Object::toString).collect(Collectors.joining(" "));
10✔
350
    }
351
  }
352
}
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