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

devonfw / IDEasy / 8144002629

04 Mar 2024 04:56PM UTC coverage: 58.928% (+0.7%) from 58.254%
8144002629

push

github

web-flow
#208: improve test infrastructure # 219: fix wrong executable (#238)

* Add first implementation

* Add javadoc

* Add javadoc

* Using target path instead of ressource path to make sure one-to-one copy of set up folders is used

* Add JavaDoc and mac mock program

* Add support for multiple dependencies while testing

* Minor changes

* Modify mock programs for testing

* Refactored example JmcTest

* Add possibility to set execution path by using the context

* Reenable test after related issue 228 has been merged

* Replace ternary with regular if

* Add missing javadoc

* Add missing javadoc

* remove unnecessary semicolon

* Fix spelling typo

* Minor test changes

* Refactoring FileExtractor class for more modularity

* using const

* Add missing extensions

* Remove unnecessary execption declaration and minor rename

* Fix spelling

* Add javadoc

* Forget dot

* minor change

* Revert "minor change"

This reverts commit ec81c3ce6.

* Revert "Merge branch 'main' into feature/208-MockOutToolRepoRefactorTestInfra"

This reverts commit d58847230, reversing
changes made to f38b3105f.

* Revert "Revert "Merge branch 'main' into feature/208-MockOutToolRepoRefactorTestInfra""

This reverts commit 3e49a0b3d.

* Revert "Revert "minor change""

This reverts commit 2f7b94624.

* fix typo

* #208: review and complete rework

* #208: improved system path

* #208: found and fixed bug on windows

* #208: improve OS mocking

* #208: improve test logging

reveal logs/errors so the developer actually sees what is happening instead of leaving them in the dark

* #208: improve OS detection

* #208: fixed

found and fixed bug in MacOS app detection for JMC, fixed copy file to folder bug, moved !extract logic back to FileAccess, f... (continued)

1580 of 2930 branches covered (53.92%)

Branch coverage included in aggregate %.

4047 of 6619 relevant lines covered (61.14%)

2.65 hits per line

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

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

3
import com.devonfw.tools.ide.cli.CliException;
4
import com.devonfw.tools.ide.common.SystemPath;
5
import com.devonfw.tools.ide.context.IdeContext;
6
import com.devonfw.tools.ide.environment.VariableLine;
7
import com.devonfw.tools.ide.log.IdeSubLogger;
8
import com.devonfw.tools.ide.os.SystemInfoImpl;
9
import com.devonfw.tools.ide.util.FilenameUtil;
10

11
import java.io.BufferedReader;
12
import java.io.IOException;
13
import java.io.InputStream;
14
import java.io.InputStreamReader;
15
import java.lang.ProcessBuilder.Redirect;
16
import java.nio.charset.StandardCharsets;
17
import java.nio.file.Files;
18
import java.nio.file.Path;
19
import java.util.ArrayList;
20
import java.util.List;
21
import java.util.Map;
22
import java.util.Objects;
23
import java.util.stream.Collectors;
24

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

30
  private static final String PREFIX_USR_BIN_ENV = "/usr/bin/env ";
31

32
  protected final IdeContext context;
33

34
  private final ProcessBuilder processBuilder;
35

36
  private final List<String> arguments;
37

38
  private Path executable;
39

40
  private ProcessErrorHandling errorHandling;
41

42
  /**
43
   * The constructor.
44
   *
45
   * @param context the owning {@link IdeContext}.
46
   */
47
  public ProcessContextImpl(IdeContext context) {
48

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

62
  @Override
63
  public ProcessContext errorHandling(ProcessErrorHandling handling) {
64

65
    Objects.requireNonNull(handling);
3✔
66
    this.errorHandling = handling;
3✔
67
    return this;
2✔
68
  }
69

70
  @Override
71
  public ProcessContext directory(Path directory) {
72

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

80
    return this;
×
81
  }
82

83
  @Override
84
  public ProcessContext executable(Path command) {
85

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

90
    this.executable = this.context.getPath().findBinary(command);
7✔
91
    return this;
2✔
92
  }
93

94
  @Override
95
  public ProcessContext addArg(String arg) {
96

97
    this.arguments.add(arg);
5✔
98
    return this;
2✔
99
  }
100

101
  @Override
102
  public ProcessContext withEnvVar(String key, String value) {
103

104
    this.processBuilder.environment().put(key, value);
×
105
    return this;
×
106
  }
107

108
  @Override
109
  public ProcessResult run(ProcessMode processMode) {
110

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

116
    if (this.executable == null) {
3✔
117
      throw new IllegalStateException("Missing executable to run process!");
5✔
118
    }
119
    List<String> args = new ArrayList<>(this.arguments.size() + 2);
9✔
120
    String interpreter = addExecutable(this.executable.toString(), args);
7✔
121
    args.addAll(this.arguments);
5✔
122
    if (this.context.debug().isEnabled()) {
5!
123
      String message = createCommandMessage(interpreter, " ...");
5✔
124
      this.context.debug(message);
4✔
125
    }
126

127
    try {
128

129
      if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
130
        this.processBuilder.redirectOutput(Redirect.PIPE).redirectError(Redirect.PIPE);
8✔
131
      } else if (processMode.isBackground()) {
3✔
132
        modifyArgumentsOnBackgroundProcess(processMode);
3✔
133
      }
134

135
      this.processBuilder.command(args);
5✔
136

137
      Process process = this.processBuilder.start();
4✔
138

139
      List<String> out = null;
2✔
140
      List<String> err = null;
2✔
141

142
      if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
143
        try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()));) {
9✔
144
          out = outReader.lines().collect(Collectors.toList());
6✔
145
        }
146
        try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
9✔
147
          err = errReader.lines().collect(Collectors.toList());
6✔
148
        }
149
      }
150

151
      int exitCode;
152
      if (processMode.isBackground()) {
3✔
153
        exitCode = ProcessResult.SUCCESS;
3✔
154
      } else {
155
        exitCode = process.waitFor();
3✔
156
      }
157

158
      ProcessResult result = new ProcessResultImpl(exitCode, out, err);
7✔
159
      performLogOnError(result, exitCode, interpreter);
5✔
160

161
      return result;
4✔
162

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

174
  private String createCommandMessage(String interpreter, String suffix) {
175

176
    StringBuilder sb = new StringBuilder();
4✔
177
    sb.append("Running command '");
4✔
178
    sb.append(this.executable);
5✔
179
    sb.append("'");
4✔
180
    if (interpreter != null) {
2!
181
      sb.append(" using ");
×
182
      sb.append(interpreter);
×
183
    }
184
    int size = this.arguments.size();
4✔
185
    if (size > 1) {
3✔
186
      sb.append(" with arguments");
4✔
187
      for (int i = 1; i < size; i++) {
7✔
188
        String arg = this.arguments.get(i);
6✔
189
        sb.append(" '");
4✔
190
        sb.append(arg);
4✔
191
        sb.append("'");
4✔
192
      }
193
    }
194
    sb.append(suffix);
4✔
195
    String message = sb.toString();
3✔
196
    return message;
2✔
197
  }
198

199
  private String getSheBang(Path file) {
200

201
    try (InputStream in = Files.newInputStream(file)) {
×
202
      // "#!/usr/bin/env bash".length() = 19
203
      byte[] buffer = new byte[32];
×
204
      int read = in.read(buffer);
×
205
      if ((read > 2) && (buffer[0] == '#') && (buffer[1] == '!')) {
×
206
        int start = 2;
×
207
        int end = 2;
×
208
        while (end < read) {
×
209
          byte c = buffer[end];
×
210
          if ((c == '\n') || (c == '\r') || (c > 127)) {
×
211
            break;
×
212
          } else if ((end == start) && (c == ' ')) {
×
213
            start++;
×
214
          }
215
          end++;
×
216
        }
×
217
        String sheBang = new String(buffer, start, end - start, StandardCharsets.US_ASCII).trim();
×
218
        if (sheBang.startsWith(PREFIX_USR_BIN_ENV)) {
×
219
          sheBang = sheBang.substring(PREFIX_USR_BIN_ENV.length());
×
220
        }
221
        return sheBang;
×
222
      }
223
    } catch (IOException e) {
×
224
      // ignore...
225
    }
×
226
    return null;
×
227
  }
228

229
  private String findBashOnWindows() {
230

231
    // Check if Git Bash exists in the default location
232
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
233
    if (Files.exists(defaultPath)) {
×
234
      return defaultPath.toString();
×
235
    }
236

237
    // If not found in the default location, try the registry query
238
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
239
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
240
    String regQueryResult;
241
    for (String bashVariant : bashVariants) {
×
242
      for (String registryKey : registryKeys) {
×
243
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
244
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
245

246
        try {
247
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
248
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
249
            StringBuilder output = new StringBuilder();
×
250
            String line;
251

252
            while ((line = reader.readLine()) != null) {
×
253
              output.append(line);
×
254
            }
255

256
            int exitCode = process.waitFor();
×
257
            if (exitCode != 0) {
×
258
              return null;
×
259
            }
260

261
            regQueryResult = output.toString();
×
262
            if (regQueryResult != null) {
×
263
              int index = regQueryResult.indexOf("REG_SZ");
×
264
              if (index != -1) {
×
265
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
266
                return path + "\\bin\\bash.exe";
×
267
              }
268
            }
269

270
          }
×
271
        } catch (Exception e) {
×
272
          return null;
×
273
        }
×
274
      }
275
    }
276
    // no bash found
277
    throw new IllegalStateException("Could not find Bash. Please install Git for Windows and rerun.");
×
278
  }
279

280
  private String addExecutable(String executable, List<String> args) {
281

282
    if (!SystemInfoImpl.INSTANCE.isWindows()) {
3!
283
      args.add(executable);
4✔
284
      return null;
2✔
285
    }
286
    String interpreter = null;
×
287
    String fileExtension = FilenameUtil.getExtension(executable);
×
288
    boolean isBashScript = "sh".equals(fileExtension);
×
289
    if (!isBashScript) {
×
290
      String sheBang = getSheBang(this.executable);
×
291
      if (sheBang != null) {
×
292
        String cmd = sheBang;
×
293
        int lastSlash = cmd.lastIndexOf('/');
×
294
        if (lastSlash >= 0) {
×
295
          cmd = cmd.substring(lastSlash + 1);
×
296
        }
297
        if (cmd.equals("bash")) {
×
298
          isBashScript = true;
×
299
        } else {
300
          // currently we do not support other interpreters...
301
        }
302
      }
303
    }
304
    if (isBashScript) {
×
305
      String bash = "bash";
×
306
      interpreter = bash;
×
307
      // here we want to have native OS behavior even if OS is mocked during tests...
308
      // if (this.context.getSystemInfo().isWindows()) {
309
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
310
        String findBashOnWindowsResult = findBashOnWindows();
×
311
        if (findBashOnWindowsResult != null) {
×
312
          bash = findBashOnWindowsResult;
×
313
        }
314
      }
315
      args.add(bash);
×
316
    }
317
    args.add(executable);
×
318
    return interpreter;
×
319
  }
320

321
  private void performLogOnError(ProcessResult result, int exitCode, String interpreter) {
322

323
    if (!result.isSuccessful() && (this.errorHandling != ProcessErrorHandling.NONE)) {
7!
324
      String message = createCommandMessage(interpreter, " failed with exit code " + exitCode + "!");
6✔
325
      if (this.errorHandling == ProcessErrorHandling.THROW) {
4✔
326
        throw new CliException(message, exitCode);
6✔
327
      }
328
      IdeSubLogger level;
329
      if (this.errorHandling == ProcessErrorHandling.ERROR) {
4✔
330
        level = this.context.error();
5✔
331
      } else if (this.errorHandling == ProcessErrorHandling.WARNING) {
4!
332
        level = this.context.warning();
5✔
333
      } else {
334
        level = this.context.error();
×
335
        level.log("Internal error: Undefined error handling {}", this.errorHandling);
×
336
      }
337
      level.log(message);
3✔
338
    }
339
  }
1✔
340

341
  private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) {
342

343
    if (processMode == ProcessMode.BACKGROUND) {
3✔
344
      this.processBuilder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT);
8✔
345
    } else if (processMode == ProcessMode.BACKGROUND_SILENT) {
3!
346
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
8✔
347
    } else {
348
      throw new IllegalStateException("Cannot handle non background process mode!");
×
349
    }
350

351
    String bash = "bash";
2✔
352

353
    // try to use bash in windows to start the process
354
    if (context.getSystemInfo().isWindows()) {
5!
355

356
      String findBashOnWindowsResult = findBashOnWindows();
×
357
      if (findBashOnWindowsResult != null) {
×
358

359
        bash = findBashOnWindowsResult;
×
360

361
      } else {
362
        context.warning(
×
363
            "Cannot start background process in windows! No bash installation found, output will be discarded.");
364
        this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
365
        return;
×
366
      }
367
    }
368

369
    String commandToRunInBackground = buildCommandToRunInBackground();
3✔
370

371
    this.arguments.clear();
3✔
372
    this.arguments.add(bash);
5✔
373
    this.arguments.add("-c");
5✔
374
    commandToRunInBackground += " & disown";
3✔
375
    this.arguments.add(commandToRunInBackground);
5✔
376

377
  }
1✔
378

379
  private String buildCommandToRunInBackground() {
380

381
    if (context.getSystemInfo().isWindows()) {
5!
382

383
      StringBuilder stringBuilder = new StringBuilder();
×
384

385
      for (String argument : this.arguments) {
×
386

387
        if (SystemPath.isValidWindowsPath(argument)) {
×
388
          argument = SystemPath.convertWindowsPathToUnixPath(argument);
×
389
        }
390

391
        stringBuilder.append(argument);
×
392
        stringBuilder.append(" ");
×
393
      }
×
394
      return stringBuilder.toString().trim();
×
395
    } else {
396
      return this.arguments.stream().map(Object::toString).collect(Collectors.joining(" "));
10✔
397
    }
398
  }
399
}
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