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

devonfw / IDEasy / 8111080112

01 Mar 2024 12:07PM UTC coverage: 58.254% (+1.1%) from 57.131%
8111080112

push

github

web-flow
#9: background process (#200)

1519 of 2867 branches covered (52.98%)

Branch coverage included in aggregate %.

3954 of 6528 relevant lines covered (60.57%)

2.62 hits per line

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

47.58
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.file.Files;
9
import java.nio.file.Path;
10
import java.util.ArrayList;
11
import java.util.List;
12
import java.util.Map;
13
import java.util.Objects;
14
import java.util.stream.Collectors;
15

16
import com.devonfw.tools.ide.cli.CliException;
17
import com.devonfw.tools.ide.common.SystemPath;
18
import com.devonfw.tools.ide.context.IdeContext;
19
import com.devonfw.tools.ide.environment.VariableLine;
20
import com.devonfw.tools.ide.log.IdeSubLogger;
21
import com.devonfw.tools.ide.util.FilenameUtil;
22

23
/**
24
 * Implementation of {@link ProcessContext}.
25
 */
26
public final class ProcessContextImpl implements ProcessContext {
27

28
  private final IdeContext context;
29

30
  private final ProcessBuilder processBuilder;
31

32
  private final List<String> arguments;
33

34
  private Path executable;
35

36
  private ProcessErrorHandling errorHandling;
37

38
  /**
39
   * The constructor.
40
   *
41
   * @param context the owning {@link IdeContext}.
42
   */
43
  public ProcessContextImpl(IdeContext context) {
44

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

58
  @Override
59
  public ProcessContext errorHandling(ProcessErrorHandling handling) {
60

61
    Objects.requireNonNull(handling);
3✔
62
    this.errorHandling = handling;
3✔
63
    return this;
2✔
64
  }
65

66
  @Override
67
  public ProcessContext directory(Path directory) {
68

69
    this.processBuilder.directory(directory.toFile());
×
70
    return this;
×
71
  }
72

73
  @Override
74
  public ProcessContext executable(Path command) {
75

76
    if (!this.arguments.isEmpty()) {
×
77
      throw new IllegalStateException("Arguments already present - did you forget to call run for previous call?");
×
78
    }
79

80
    this.executable = this.context.getPath().findBinary(command);
×
81
    return this;
×
82
  }
83

84
  @Override
85
  public ProcessContext addArg(String arg) {
86

87
    this.arguments.add(arg);
×
88
    return this;
×
89
  }
90

91
  @Override
92
  public ProcessContext withEnvVar(String key, String value) {
93

94
    this.processBuilder.environment().put(key, value);
×
95
    return this;
×
96
  }
97

98
  @Override
99
  public ProcessResult run(ProcessMode processMode) {
100

101
    // TODO ProcessMode needs to be configurable for GUI
102
    if (processMode == ProcessMode.DEFAULT) {
3✔
103
      this.processBuilder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT);
7✔
104
    }
105

106
    if (this.executable == null) {
3✔
107
      throw new IllegalStateException("Missing executable to run process!");
5✔
108
    }
109
    String executableName = this.executable.toString();
4✔
110
    // pragmatic solution to avoid copying lists/arrays
111
    this.arguments.add(0, executableName);
5✔
112

113
    checkAndHandlePossibleBashScript(executableName);
3✔
114

115
    if (this.context.debug().isEnabled()) {
5!
116
      String message = createCommandMessage(" ...");
4✔
117
      this.context.debug(message);
4✔
118
    }
119

120
    try {
121

122
      if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
123
        this.processBuilder.redirectOutput(Redirect.PIPE).redirectError(Redirect.PIPE);
8✔
124
      } else if (processMode.isBackground()) {
3✔
125
        modifyArgumentsOnBackgroundProcess(processMode);
3✔
126
      }
127

128
      this.processBuilder.command(this.arguments);
6✔
129

130
      Process process = this.processBuilder.start();
4✔
131

132
      List<String> out = null;
2✔
133
      List<String> err = null;
2✔
134

135
      if (processMode == ProcessMode.DEFAULT_CAPTURE) {
3✔
136
        try (BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()));) {
9✔
137
          out = outReader.lines().collect(Collectors.toList());
6✔
138
        }
139
        try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
9✔
140
          err = errReader.lines().collect(Collectors.toList());
6✔
141
        }
142
      }
143

144
      int exitCode;
145
      if (processMode.isBackground()) {
3✔
146
        exitCode = ProcessResult.SUCCESS;
3✔
147
      } else {
148
        exitCode = process.waitFor();
3✔
149
      }
150

151
      ProcessResult result = new ProcessResultImpl(exitCode, out, err);
7✔
152
      performLogOnError(result, exitCode);
4✔
153

154
      return result;
4✔
155

156
    } catch (Exception e) {
1✔
157
      String msg = e.getMessage();
3✔
158
      if ((msg == null) || msg.isEmpty()) {
5!
159
        msg = e.getClass().getSimpleName();
×
160
      }
161
      throw new IllegalStateException(createCommandMessage(" failed: " + msg), e);
9✔
162
    } finally {
163
      this.arguments.clear();
3✔
164
    }
165
  }
166

167
  private String createCommandMessage(String suffix) {
168

169
    StringBuilder sb = new StringBuilder();
4✔
170
    sb.append("Running command '");
4✔
171
    sb.append(this.executable);
5✔
172
    sb.append("'");
4✔
173
    int size = this.arguments.size();
4✔
174
    if (size > 1) {
3!
175
      sb.append(" with arguments");
×
176
      for (int i = 1; i < size; i++) {
×
177
        String arg = this.arguments.get(i);
×
178
        sb.append(" '");
×
179
        sb.append(arg);
×
180
        sb.append("'");
×
181
      }
182
    }
183
    sb.append(suffix);
4✔
184
    String message = sb.toString();
3✔
185
    return message;
2✔
186
  }
187

188
  private boolean hasSheBang(Path file) {
189

190
    try (InputStream in = Files.newInputStream(file)) {
×
191
      byte[] buffer = new byte[2];
×
192
      int read = in.read(buffer);
×
193
      if ((read == 2) && (buffer[0] == '#') && (buffer[1] == '!')) {
×
194
        return true;
×
195
      }
196
    } catch (IOException e) {
1!
197
      // ignore...
198
    }
×
199
    return false;
2✔
200
  }
201

202
  private String findBashOnWindows() {
203

204
    // Check if Git Bash exists in the default location
205
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
206
    if (Files.exists(defaultPath)) {
×
207
      return defaultPath.toString();
×
208
    }
209

210
    // If not found in the default location, try the registry query
211
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
212
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
213
    String regQueryResult;
214
    for (String bashVariant : bashVariants) {
×
215
      for (String registryKey : registryKeys) {
×
216
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
217
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
218

219
        try {
220
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
221
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
222
            StringBuilder output = new StringBuilder();
×
223
            String line;
224

225
            while ((line = reader.readLine()) != null) {
×
226
              output.append(line);
×
227
            }
228

229
            int exitCode = process.waitFor();
×
230
            if (exitCode != 0) {
×
231
              return null;
×
232
            }
233

234
            regQueryResult = output.toString();
×
235
            if (regQueryResult != null) {
×
236
              int index = regQueryResult.indexOf("REG_SZ");
×
237
              if (index != -1) {
×
238
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
239
                return path + "\\bin\\bash.exe";
×
240
              }
241
            }
242

243
          }
×
244
        } catch (Exception e) {
×
245
          return null;
×
246
        }
×
247
      }
248
    }
249
    // no bash found
250
    throw new IllegalStateException("Could not find Bash. Please install Git for Windows and rerun.");
×
251
  }
252

253
  private void checkAndHandlePossibleBashScript(String executableName) {
254

255
    String fileExtension = FilenameUtil.getExtension(executableName);
3✔
256
    boolean isBashScript = "sh".equals(fileExtension) || hasSheBang(this.executable);
11!
257
    if (isBashScript) {
2!
258
      String bash = "bash";
×
259
      if (this.context.getSystemInfo().isWindows()) {
×
260
        String findBashOnWindowsResult = findBashOnWindows();
×
261
        if (findBashOnWindowsResult != null) {
×
262
          bash = findBashOnWindowsResult;
×
263
        }
264
      }
265
      this.arguments.add(0, bash);
×
266
    }
267
  }
1✔
268

269
  private void performLogOnError(ProcessResult result, int exitCode) {
270

271
    if (!result.isSuccessful() && (this.errorHandling != ProcessErrorHandling.NONE)) {
7!
272
      String message = createCommandMessage(" failed with exit code " + exitCode + "!");
5✔
273
      if (this.errorHandling == ProcessErrorHandling.THROW) {
4✔
274
        throw new CliException(message, exitCode);
6✔
275
      }
276
      IdeSubLogger level;
277
      if (this.errorHandling == ProcessErrorHandling.ERROR) {
4✔
278
        level = this.context.error();
5✔
279
      } else if (this.errorHandling == ProcessErrorHandling.WARNING) {
4!
280
        level = this.context.warning();
5✔
281
      } else {
282
        level = this.context.error();
×
283
        level.log("Internal error: Undefined error handling {}", this.errorHandling);
×
284
      }
285
      level.log(message);
3✔
286
    }
287
  }
1✔
288

289
  private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) {
290

291
    if (processMode == ProcessMode.BACKGROUND) {
3✔
292
      this.processBuilder.redirectOutput(Redirect.INHERIT).redirectError(Redirect.INHERIT);
8✔
293
    } else if (processMode == ProcessMode.BACKGROUND_SILENT) {
3!
294
      this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
8✔
295
    } else {
296
      throw new IllegalStateException("Cannot handle non background process mode!");
×
297
    }
298

299
    String bash = "bash";
2✔
300

301
    // try to use bash in windows to start the process
302
    if (context.getSystemInfo().isWindows()) {
5!
303

304
      String findBashOnWindowsResult = findBashOnWindows();
×
305
      if (findBashOnWindowsResult != null) {
×
306

307
        bash = findBashOnWindowsResult;
×
308

309
      } else {
310
        context.warning(
×
311
            "Cannot start background process in windows! No bash installation found, output will be discarded.");
312
        this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
×
313
        return;
×
314
      }
315
    }
316

317
    String commandToRunInBackground = buildCommandToRunInBackground();
3✔
318

319
    this.arguments.clear();
3✔
320
    this.arguments.add(bash);
5✔
321
    this.arguments.add("-c");
5✔
322
    commandToRunInBackground += " & disown";
3✔
323
    this.arguments.add(commandToRunInBackground);
5✔
324

325
  }
1✔
326

327
  private String buildCommandToRunInBackground() {
328

329
    if (context.getSystemInfo().isWindows()) {
5!
330

331
      StringBuilder stringBuilder = new StringBuilder();
×
332

333
      for (String argument : this.arguments) {
×
334

335
        if (SystemPath.isValidWindowsPath(argument)) {
×
336
          argument = SystemPath.convertWindowsPathToUnixPath(argument);
×
337
        }
338

339
        stringBuilder.append(argument);
×
340
        stringBuilder.append(" ");
×
341
      }
×
342
      return stringBuilder.toString().trim();
×
343
    } else {
344
      return this.arguments.stream().map(Object::toString).collect(Collectors.joining(" "));
10✔
345
    }
346
  }
347
}
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