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

oracle / opengrok / #3663

26 Oct 2023 04:47PM UTC coverage: 66.029% (-9.1%) from 75.156%
#3663

push

web-flow
opengrok web project code smell fixes (#4457)

---------

Signed-off-by: Gino Augustine <ginoaugustine@gmail.com>

19 of 19 new or added lines in 4 files covered. (100.0%)

38701 of 58612 relevant lines covered (66.03%)

0.66 hits per line

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

87.85
/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.Timer;
39
import java.util.TimerTask;
40
import java.util.logging.Level;
41
import java.util.logging.Logger;
42
import java.util.regex.Matcher;
43
import java.util.regex.Pattern;
44

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

174
        File cwd = processBuilder.directory();
1✔
175
        if (cwd == null) {
1✔
176
            dir_str = System.getProperty("user.dir");
1✔
177
        } else {
178
            dir_str = cwd.toString();
1✔
179
        }
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
            int timeout = this.timeout;
1✔
213
            /*
214
             * Setup timer so if the process get stuck we can terminate it and
215
             * make progress instead of hanging the whole operation.
216
             */
217
            if (timeout != 0) {
1✔
218
                // invoking the constructor starts the background thread
219
                timer = new Timer();
1✔
220
                timer.schedule(new TimerTask() {
1✔
221
                    @Override public void run() {
222
                        LOGGER.log(Level.WARNING,
×
223
                            String.format("Terminating process of command [%s] in directory '%s' " +
×
224
                            "due to timeout %d seconds", cmd_str, dir_str, timeout / 1000));
×
225
                        proc.destroy();
×
226
                    }
×
227
                }, timeout);
228
            }
229

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

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

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

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

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

289
        return ret;
1✔
290
    }
291

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

303
        return ret;
1✔
304
    }
305

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

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

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

335
        return ret;
1✔
336
    }
337

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

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

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

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

373
    private static class SpoolHandler implements StreamHandler {
1✔
374

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

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

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

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

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

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

406
    /**
407
     * Build a string from the specified argv list with optional tab-indenting
408
     * and line-continuations if {@code multiline} is {@code true}.
409
     * @param isWindows a value indicating if the platform is Windows so that
410
     *                  PowerShell escaping is done; else Bourne shell escaping
411
     *                  is done.
412
     * @return a defined instance
413
     */
414
    public static String escapeForShell(List<String> argv, boolean multiline, boolean isWindows) {
415
        StringBuilder result = new StringBuilder();
1✔
416
        for (int i = 0; i < argv.size(); ++i) {
1✔
417
            if (multiline && i > 0) {
1✔
418
                result.append("\t");
1✔
419
            }
420
            String arg = argv.get(i);
1✔
421
            result.append(isWindows ? maybeEscapeForPowerShell(arg) : maybeEscapeForSh(arg));
1✔
422
            if (i + 1 < argv.size()) {
1✔
423
                if (!multiline) {
1✔
424
                    result.append(" ");
1✔
425
                } else {
426
                    result.append(isWindows ? " `" : " \\");
1✔
427
                    result.append(System.lineSeparator());
1✔
428
                }
429
            }
430
        }
431
        return result.toString();
1✔
432
    }
433

434
    private static String maybeEscapeForSh(String value) {
435
        Matcher m = ARG_UNIX_QUOTING.matcher(value);
1✔
436
        if (!m.find()) {
1✔
437
            return value;
1✔
438
        }
439
        m = ARG_GNU_STYLE_EQ.matcher(value);
1✔
440
        if (!m.find()) {
1✔
441
            return "$'" + escapeForSh(value) + "'";
1✔
442
        }
443
        String following = value.substring(m.end());
1✔
444
        return m.group() + "$'" + escapeForSh(following) + "'";
1✔
445
    }
446

447
    private static String escapeForSh(String value) {
448
        return value.replace("\\", "\\\\").
1✔
449
                replace("'", "\\'").
1✔
450
                replace("\n", "\\n").
1✔
451
                replace("\r", "\\r").
1✔
452
                replace("\f", "\\f").
1✔
453
                replace("\u0011", "\\v").
1✔
454
                replace("\t", "\\t");
1✔
455
    }
456

457
    private static String maybeEscapeForPowerShell(String value) {
458
        Matcher m = ARG_WIN_QUOTING.matcher(value);
1✔
459
        if (!m.find()) {
1✔
460
            return value;
1✔
461
        }
462
        m = ARG_GNU_STYLE_EQ.matcher(value);
1✔
463
        if (!m.find()) {
1✔
464
            return "\"" + escapeForPowerShell(value) + "\"";
1✔
465
        }
466
        String following = value.substring(m.end());
1✔
467
        return m.group() + "\"" + escapeForPowerShell(following) + "\"";
1✔
468
    }
469

470
    private static String escapeForPowerShell(String value) {
471
        return value.replace("`", "``").
1✔
472
                replace("\"", "`\"").
1✔
473
                replace("$", "`$").
1✔
474
                replace("\n", "`n").
1✔
475
                replace("\r", "`r").
1✔
476
                replace("\f", "`f").
1✔
477
                replace("\u0011", "`v").
1✔
478
                replace("\t", "`t");
1✔
479
    }
480
}
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