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

oracle / opengrok / #3721

30 Nov 2023 04:03PM UTC coverage: 66.155% (-9.8%) from 75.915%
#3721

push

vladak
1.12.25

38762 of 58593 relevant lines covered (66.15%)

0.66 hits per line

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

87.28
/opengrok-indexer/src/main/java/org/opengrok/indexer/util/Executor.java
1
/*
2
 * CDDL HEADER START
3
 *
4
 * The contents of this file are subject to the terms of the
5
 * Common Development and Distribution License (the "License").
6
 * You may not use this file except in compliance with the License.
7
 *
8
 * See LICENSE.txt included in this distribution for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing Covered Code, include this CDDL HEADER in each
12
 * file and include the License file at LICENSE.txt.
13
 * If applicable, add the following below this CDDL HEADER, with the
14
 * fields enclosed by brackets "[]" replaced with your own identifying
15
 * information: Portions Copyright [yyyy] [name of copyright owner]
16
 *
17
 * CDDL HEADER END
18
 */
19

20
/*
21
 * Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved.
22
 * Portions Copyright (c) 2019, Chris Fraire <cfraire@me.com>.
23
 */
24
package org.opengrok.indexer.util;
25

26
import java.io.BufferedInputStream;
27
import java.io.ByteArrayInputStream;
28
import java.io.ByteArrayOutputStream;
29
import java.io.File;
30
import java.io.IOException;
31
import java.io.InputStream;
32
import java.io.InputStreamReader;
33
import java.io.Reader;
34
import java.lang.Thread.UncaughtExceptionHandler;
35
import java.util.Arrays;
36
import java.util.List;
37
import java.util.Map;
38
import java.util.Optional;
39
import java.util.Timer;
40
import java.util.TimerTask;
41
import java.util.logging.Level;
42
import java.util.logging.Logger;
43
import java.util.regex.Matcher;
44
import java.util.regex.Pattern;
45
import java.util.stream.Collectors;
46

47
import org.apache.commons.lang3.SystemUtils;
48
import org.opengrok.indexer.configuration.RuntimeEnvironment;
49
import org.opengrok.indexer.logger.LoggerFactory;
50

51
/**
52
 * Wrapper to Java Process API.
53
 *
54
 * @author Emilio Monti - emilmont@gmail.com
55
 */
56
public class Executor {
57

58
    private static final Logger LOGGER = LoggerFactory.getLogger(Executor.class);
1✔
59

60
    private static final Pattern ARG_WIN_QUOTING = Pattern.compile("[^-:.+=%a-zA-Z0-9_/\\\\]");
1✔
61
    private static final Pattern ARG_UNIX_QUOTING = Pattern.compile("[^-:.+=%a-zA-Z0-9_/]");
1✔
62
    private static final Pattern ARG_GNU_STYLE_EQ = Pattern.compile("^--[-.a-zA-Z0-9_]+=");
1✔
63

64
    private final List<String> cmdList;
65
    private final File workingDirectory;
66
    private byte[] stdout;
67
    private byte[] stderr;
68
    private int timeout; // in milliseconds, 0 means no timeout
69

70
    /**
71
     * Create a new instance of the Executor.
72
     * @param cmd An array containing the command to execute
73
     */
74
    public Executor(String[] cmd) {
75
        this(Arrays.asList(cmd));
1✔
76
    }
1✔
77

78
    /**
79
     * Create a new instance of the Executor.
80
     * @param cmdList A list containing the command to execute
81
     */
82
    public Executor(List<String> cmdList) {
83
        this(cmdList, null);
1✔
84
    }
1✔
85

86
    /**
87
     * Create a new instance of the Executor with default command timeout value.
88
     * The timeout value will be based on the running context (indexer or web application).
89
     * @param cmdList A list containing the command to execute
90
     * @param workingDirectory The directory the process should have as the
91
     *                         working directory
92
     */
93
    public Executor(List<String> cmdList, File workingDirectory) {
1✔
94
        RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1✔
95
        int timeoutSec = env.isIndexer() ? env.getIndexerCommandTimeout() : env.getInteractiveCommandTimeout();
1✔
96

97
        this.cmdList = cmdList;
1✔
98
        this.workingDirectory = workingDirectory;
1✔
99
        this.timeout = timeoutSec * 1000;
1✔
100
    }
1✔
101

102
    /**
103
     * Create a new instance of the Executor with specific timeout value.
104
     * @param cmdList A list containing the command to execute
105
     * @param workingDirectory The directory the process should have as the
106
     *                         working directory
107
     * @param timeout If the command runs longer than the timeout (seconds),
108
     *                it will be terminated. If the value is 0, no timer
109
     *                will be set up.
110
     */
111
    public Executor(List<String> cmdList, File workingDirectory, int timeout) {
1✔
112
        this.cmdList = cmdList;
1✔
113
        this.workingDirectory = workingDirectory;
1✔
114
        this.timeout = timeout * 1000;
1✔
115
    }
1✔
116

117
    /**
118
     * Create a new instance of the Executor with or without timeout.
119
     * @param cmdList A list containing the command to execute
120
     * @param workingDirectory The directory the process should have as the
121
     *                         working directory
122
     * @param useTimeout terminate the process after default timeout or not
123
     */
124
    public Executor(List<String> cmdList, File workingDirectory, boolean useTimeout) {
125
        this(cmdList, workingDirectory);
1✔
126
        if (!useTimeout) {
1✔
127
            this.timeout = 0;
1✔
128
        }
129
    }
1✔
130

131
    /**
132
     * Execute the command and collect the output. All exceptions will be
133
     * logged.
134
     *
135
     * @return The exit code of the process
136
     */
137
    public int exec() {
138
        return exec(true);
1✔
139
    }
140

141
    /**
142
     * Execute the command and collect the output.
143
     *
144
     * @param reportExceptions Should exceptions be added to the log or not
145
     * @return The exit code of the process
146
     */
147
    public int exec(boolean reportExceptions) {
148
        SpoolHandler spoolOut = new SpoolHandler();
1✔
149
        int ret = exec(reportExceptions, spoolOut);
1✔
150
        stdout = spoolOut.getBytes();
1✔
151
        return ret;
1✔
152
    }
153

154
    /**
155
     * Execute the command and collect the output.
156
     *
157
     * @param reportExceptions Should exceptions be added to the log or not
158
     * @param handler The handler to handle data from standard output
159
     * @return The exit code of the process
160
     */
161
    public int exec(final boolean reportExceptions, StreamHandler handler) {
162
        int ret = -1;
1✔
163
        ProcessBuilder processBuilder = new ProcessBuilder(cmdList);
1✔
164
        final String cmd_str = escapeForShell(processBuilder.command(), false, SystemUtils.IS_OS_WINDOWS);
1✔
165
        final String dir_str;
166
        Timer timer = null; // timer for timing out the process
1✔
167

168
        if (workingDirectory != null) {
1✔
169
            processBuilder.directory(workingDirectory);
1✔
170
            if (processBuilder.environment().containsKey("PWD")) {
1✔
171
                processBuilder.environment().put("PWD",
1✔
172
                    workingDirectory.getAbsolutePath());
1✔
173
            }
174
        }
175

176
        File cwd = processBuilder.directory();
1✔
177
        dir_str = Optional.ofNullable(cwd)
1✔
178
                .map(File::toString)
1✔
179
                .orElseGet(() -> System.getProperty("user.dir"));
1✔
180

181
        String envStr = "";
1✔
182
        if (LOGGER.isLoggable(Level.FINER)) {
1✔
183
            Map<String, String> envMap = processBuilder.environment();
×
184
            envStr = " with environment: " + envMap.toString();
×
185
        }
186
        LOGGER.log(Level.FINE,
1✔
187
                "Executing command [{0}] in directory ''{1}''{2}",
188
                new Object[] {cmd_str, dir_str, envStr});
189

190
        Process process = null;
1✔
191
        try {
192
            Statistics stat = new Statistics();
1✔
193
            process = processBuilder.start();
1✔
194
            final Process proc = process;
1✔
195

196
            final InputStream errorStream = process.getErrorStream();
1✔
197
            final SpoolHandler err = new SpoolHandler();
1✔
198
            Thread thread = new Thread(() -> {
1✔
199
                try {
200
                    err.processStream(errorStream);
1✔
201
                } catch (IOException ex) {
×
202
                    if (reportExceptions) {
×
203
                        LOGGER.log(Level.SEVERE,
×
204
                                "Error while executing command [{0}] in directory ''{1}''",
205
                                new Object[] {cmd_str, dir_str});
206
                        LOGGER.log(Level.SEVERE, "Error during process pipe listening", ex);
×
207
                    }
208
                }
1✔
209
            });
1✔
210
            thread.start();
1✔
211

212
            /*
213
             * Setup timer so if the process get stuck we can terminate it and
214
             * make progress instead of hanging the whole operation.
215
             */
216
            if (timeout != 0) {
1✔
217
                // invoking the constructor starts the background thread
218
                timer = new Timer();
1✔
219
                timer.schedule(new TimerTask() {
1✔
220
                    @Override public void run() {
221
                        LOGGER.log(Level.WARNING,
×
222
                            String.format("Terminating process of command [%s] in directory '%s' " +
×
223
                            "due to timeout %d seconds", cmd_str, dir_str, timeout / 1000));
×
224
                        proc.destroy();
×
225
                    }
×
226
                }, timeout);
227
            }
228

229
            handler.processStream(process.getInputStream());
1✔
230

231
            ret = process.waitFor();
1✔
232

233
            stat.report(LOGGER, Level.FINE,
1✔
234
                    String.format("Finished command [%s] in directory '%s' with exit code %d", cmd_str, dir_str, ret),
1✔
235
                    "executor.latency");
236
            LOGGER.log(Level.FINE,
1✔
237
                "Finished command [{0}] in directory ''{1}'' with exit code {2}",
238
                new Object[] {cmd_str, dir_str, ret});
1✔
239

240
            // Wait for the stderr read-out thread to finish the processing and
241
            // only after that read the data.
242
            thread.join();
1✔
243
            stderr = err.getBytes();
1✔
244
        } catch (IOException e) {
1✔
245
            if (reportExceptions) {
1✔
246
                LOGGER.log(Level.SEVERE, String.format("Failed to read from process: %s", cmdList.get(0)), e);
1✔
247
            }
248
        } catch (InterruptedException e) {
×
249
            if (reportExceptions) {
×
250
                LOGGER.log(Level.SEVERE, String.format("Waiting for process interrupted: %s", cmdList.get(0)), e);
×
251
            }
252
        } finally {
253
            // Stop timer thread if the instance exists.
254
            if (timer != null) {
1✔
255
                timer.cancel();
1✔
256
            }
257
            try {
258
                if (process != null) {
1✔
259
                    IOUtils.close(process.getOutputStream());
1✔
260
                    IOUtils.close(process.getInputStream());
1✔
261
                    IOUtils.close(process.getErrorStream());
1✔
262
                    ret = process.exitValue();
1✔
263
                }
264
            } catch (IllegalThreadStateException e) {
×
265
                process.destroy();
×
266
            }
1✔
267
        }
268

269
        if (ret != 0 && reportExceptions) {
1✔
270
            int maxMsgSize = 512; /* limit to avoid flooding the logs */
1✔
271
            StringBuilder msg = new StringBuilder("Non-zero exit status ")
1✔
272
                    .append(ret).append(" from command [")
1✔
273
                    .append(cmd_str)
1✔
274
                    .append("] in directory '")
1✔
275
                    .append(dir_str).
1✔
276
                    append("'");
1✔
277
            if (stderr != null && stderr.length > 0) {
1✔
278
                    msg.append(": ");
×
279
                    if (stderr.length > maxMsgSize) {
×
280
                            msg.append(new String(stderr, 0, maxMsgSize)).append("...");
×
281
                    } else {
282
                            msg.append(new String(stderr));
×
283
                    }
284
            }
285
            LOGGER.log(Level.WARNING, msg::toString);
1✔
286
        }
287

288
        return ret;
1✔
289
    }
290

291
    /**
292
     * Get the output from the process as a string.
293
     *
294
     * @return The output from the process
295
     */
296
    public String getOutputString() {
297
        String ret = null;
1✔
298
        if (stdout != null) {
1✔
299
            ret = new String(stdout);
1✔
300
        }
301

302
        return ret;
1✔
303
    }
304

305
    /**
306
     * Get a reader to read the output from the process.
307
     *
308
     * @return A reader reading the process output
309
     */
310
    public Reader getOutputReader() {
311
        return new InputStreamReader(getOutputStream());
1✔
312
    }
313

314
    /**
315
     * Get an input stream read the output from the process.
316
     *
317
     * @return A reader reading the process output
318
     */
319
    public InputStream getOutputStream() {
320
        return new ByteArrayInputStream(stdout);
1✔
321
    }
322

323
    /**
324
     * Get the output from the process written to the error stream as a string.
325
     *
326
     * @return The error output from the process
327
     */
328
    public String getErrorString() {
329
        String ret = null;
1✔
330
        if (stderr != null) {
1✔
331
            ret = new String(stderr);
1✔
332
        }
333

334
        return ret;
1✔
335
    }
336

337
    /**
338
     * Get a reader to read the output the process wrote to the error stream.
339
     *
340
     * @return A reader reading the process error stream
341
     */
342
    public Reader getErrorReader() {
343
        return new InputStreamReader(getErrorStream());
1✔
344
    }
345

346
    /**
347
     * Get an input stream to read the output the process wrote to the error stream.
348
     *
349
     * @return An input stream for reading the process error stream
350
     */
351
    public InputStream getErrorStream() {
352
        return new ByteArrayInputStream(stderr);
1✔
353
    }
354

355
    /**
356
     * You should use the StreamHandler interface if you would like to process
357
     * the output from a process while it is running.
358
     */
359
    public interface StreamHandler {
360

361
        /**
362
         * Process the data in the stream. The processStream function is
363
         * called _once_ during the lifetime of the process, and you should
364
         * process all of the input you want before returning from the function.
365
         *
366
         * @param in The InputStream containing the data
367
         * @throws java.io.IOException if any read error
368
         */
369
        void processStream(InputStream in) throws IOException;
370
    }
371

372
    private static class SpoolHandler implements StreamHandler {
1✔
373

374
        private final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
1✔
375

376
        public byte[] getBytes() {
377
            return bytes.toByteArray();
1✔
378
        }
379

380
        @Override
381
        public void processStream(InputStream input) throws IOException {
382
            BufferedInputStream  in = new BufferedInputStream(input);
1✔
383

384
            byte[] buffer = new byte[8092];
1✔
385
            int len;
386

387
            while ((len = in.read(buffer)) != -1) {
1✔
388
                if (len > 0) {
1✔
389
                    bytes.write(buffer, 0, len);
1✔
390
                }
391
            }
392
        }
1✔
393
    }
394

395
    public static void registerErrorHandler() {
396
        UncaughtExceptionHandler exceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
1✔
397
        if (exceptionHandler == null) {
1✔
398
            LOGGER.log(Level.FINE, "Installing default uncaught exception handler");
1✔
399
            Thread.setDefaultUncaughtExceptionHandler((t, e) ->
1✔
400
                    LOGGER.log(Level.SEVERE, String.format("Uncaught exception in thread %s with ID %d: %s",
×
401
                            t.getName(), t.getId(), e.getMessage()), e));
×
402
        }
403
    }
1✔
404

405
    /**
406
     * Build a string from the specified argv list with optional tab-indenting
407
     * and line-continuations if {@code multiline} is {@code true}.
408
     * @param isWindows a value indicating if the platform is Windows so that
409
     *                  PowerShell escaping is done; else Bourne shell escaping
410
     *                  is done.
411
     * @return a defined instance
412
     */
413
    public static String escapeForShell(List<String> argv, boolean multiline, boolean isWindows) {
414
        return argv.stream()
1✔
415
                .map(arg -> isWindows ? maybeEscapeForPowerShell(arg) : maybeEscapeForSh(arg))
1✔
416
                .collect(Collectors.joining(multiline ? multiLineDelimiter(isWindows) : " "));
1✔
417
    }
418

419
    private static String multiLineDelimiter(boolean isWindows) {
420
        return (isWindows ? " `" : " \\") +
1✔
421
                System.lineSeparator() +
1✔
422
                "\t";
423
    }
424

425
    private static String maybeEscapeForSh(String value) {
426
        Matcher m = ARG_UNIX_QUOTING.matcher(value);
1✔
427
        if (!m.find()) {
1✔
428
            return value;
1✔
429
        }
430
        m = ARG_GNU_STYLE_EQ.matcher(value);
1✔
431
        if (!m.find()) {
1✔
432
            return "$'" + escapeForSh(value) + "'";
1✔
433
        }
434
        String following = value.substring(m.end());
1✔
435
        return m.group() + "$'" + escapeForSh(following) + "'";
1✔
436
    }
437

438
    private static String escapeForSh(String value) {
439
        return value.replace("\\", "\\\\").
1✔
440
                replace("'", "\\'").
1✔
441
                replace("\n", "\\n").
1✔
442
                replace("\r", "\\r").
1✔
443
                replace("\f", "\\f").
1✔
444
                replace("\u0011", "\\v").
1✔
445
                replace("\t", "\\t");
1✔
446
    }
447

448
    private static String maybeEscapeForPowerShell(String value) {
449
        Matcher m = ARG_WIN_QUOTING.matcher(value);
1✔
450
        if (!m.find()) {
1✔
451
            return value;
1✔
452
        }
453
        m = ARG_GNU_STYLE_EQ.matcher(value);
1✔
454
        if (!m.find()) {
1✔
455
            return "\"" + escapeForPowerShell(value) + "\"";
1✔
456
        }
457
        String following = value.substring(m.end());
1✔
458
        return m.group() + "\"" + escapeForPowerShell(following) + "\"";
1✔
459
    }
460

461
    private static String escapeForPowerShell(String value) {
462
        return value.replace("`", "``").
1✔
463
                replace("\"", "`\"").
1✔
464
                replace("$", "`$").
1✔
465
                replace("\n", "`n").
1✔
466
                replace("\r", "`r").
1✔
467
                replace("\f", "`f").
1✔
468
                replace("\u0011", "`v").
1✔
469
                replace("\t", "`t");
1✔
470
    }
471
}
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