Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions common/src/main/java/com/highgo/cloud/util/ShellUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static int execShellSpaceParam(String[] shellCmd) {
log.debug("Begin to execute command: [{}]", Arrays.toString(shellCmd));

try {
Process ps = Runtime.getRuntime().exec(shellCmd);
Process ps = new ProcessBuilder(shellCmd).redirectErrorStream(true).start();
// -- catch the deploy log start
BufferedInputStream in = new BufferedInputStream(ps.getInputStream());
BufferedReader br = new BufferedReader(new InputStreamReader(in));
Expand Down Expand Up @@ -171,7 +171,7 @@ public static int execShell(String shellCmd) {
}

public static boolean execShellWithTimeout(String shellCmd, long time, TimeUnit unit) {
boolean result = true;
boolean result = false;
log.info("Begin to execute command: [{}], timeout: [{}], unit: [{}]", shellCmd, time, unit.name());
try {
Process ps = Runtime.getRuntime().exec(shellCmd);
Expand Down
80 changes: 80 additions & 0 deletions tests/regression/ShellArrayStderrRegression.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import com.highgo.cloud.util.ShellUtil;
import java.io.File;
import java.util.Arrays;

/** Exercises the complete executor with real, self-bounded JDK child processes. */
public final class ShellArrayStderrRegression {
private static int failures;

private static void check(String name, String mode, int exitCode, String argument) {
String executable = new File(
new File(System.getProperty("java.home"), "bin"),
System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java")
.getAbsolutePath();
String[] command = {
executable, "-Xmx32m", "-XX:ActiveProcessorCount=1",
"-cp", System.getProperty("java.class.path"),
Emitter.class.getName(), mode, Integer.toString(exitCode), argument
};
int actual = ShellUtil.execShellSpaceParam(command);
if (actual != exitCode) {
failures++;
System.err.println("FAIL " + name + ": expected exit " + exitCode + ", got " + actual);
} else {
System.out.println("PASS " + name);
}
}

public static void main(String[] args) {
check("large-stderr-zero-exit", "stderr", 0, "ordinary");
check("large-stderr-nonzero-exit", "stderr", 7, "ordinary");
check("stdout-only-control", "stdout", 0, "ordinary");
check("quiet-nonzero-control", "quiet", 7, "ordinary");
check("array-argument-with-spaces-control", "argument", 0, "literal argument with spaces");
if (failures != 0) {
throw new AssertionError(failures + " process completion assertions failed");
}
}

public static final class Emitter {
public static void main(String[] args) throws Exception {
// Bound THIS private child, including a blocked pipe write, without
// killing unrelated processes or depending on the production helper's timeout policy.
Thread watchdog = new Thread(() -> {
try {
Thread.sleep(8000);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return;
}
Runtime.getRuntime().halt(99);
}, "private-emitter-watchdog");
watchdog.setDaemon(true);
watchdog.start();

String mode = args[0];
int exitCode = Integer.parseInt(args[1]);
if ("stderr".equals(mode)) {
byte[] chunk = new byte[8192];
Arrays.fill(chunk, (byte) 'e');
chunk[chunk.length - 1] = (byte) '\n';
for (int i = 0; i < 64; i++) {
System.err.write(chunk);
System.err.flush();
}
System.out.println("stderr fully written");
} else if ("stdout".equals(mode)) {
System.out.println("ordinary stdout");
} else if ("argument".equals(mode)) {
if (args.length != 3 || !"literal argument with spaces".equals(args[2])) {
Runtime.getRuntime().halt(83);
}
System.out.println(args[2]);
} else if (!"quiet".equals(mode)) {
Runtime.getRuntime().halt(84);
}
System.exit(exitCode);
}
}
}

91 changes: 91 additions & 0 deletions tests/regression/ShellTimeoutRegression.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import com.highgo.cloud.util.ShellUtil;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/** Full production ShellUtil tests with actual local processes, no process mock. */
public final class ShellTimeoutRegression {
private static int passed;
private static int failed;

private interface Check {
void run() throws Exception;
}

private static void check(String name, Check check) {
try {
check.run();
passed++;
System.out.println("PASS " + name);
} catch (Throwable error) {
failed++;
System.err.println("FAIL " + name);
error.printStackTrace(System.err);
}
}

private static void require(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}

private static String exitCommand(int status) {
// Runtime.exec(String) tokenizes on whitespace. These fixed shell
// commands intentionally need no quoting or user-supplied interpolation.
return "cmd.exe /d /c exit " + status;
}

public static void main(String[] args) throws Exception {
if (args.length != 1) {
throw new IllegalArgumentException("Supply Main's existing private work directory");
}
Path root = Paths.get(args[0]).toRealPath();
require(Files.isDirectory(root), "Private work directory must exist");
require(root.getParent() != null, "Private work directory must not be a filesystem root");
Path missing = root.resolve("nonexistent-ivory-command-" + UUID.randomUUID() + ".exe");
require(Files.notExists(missing), "Missing-command fixture must not exist");

check("launch IOException returns false", () -> {
boolean completed = ShellUtil.execShellWithTimeout(
missing.toString(), 5, TimeUnit.SECONDS);
require(!completed, "A process that could not start cannot have completed");
});
check("successful local process still completes", () -> {
String command = System.getProperty("os.name").startsWith("Windows")
? exitCommand(0) : "/bin/true";
require(ShellUtil.execShellWithTimeout(command, 5, TimeUnit.SECONDS),
"An actual process exiting promptly must complete");
});
check("nonzero termination retains completion semantics", () -> {
String command = System.getProperty("os.name").startsWith("Windows")
? exitCommand(7) : "/bin/false";
require(ShellUtil.execShellWithTimeout(command, 5, TimeUnit.SECONDS),
"Existing waitFor semantics report termination, including a nonzero exit");
});
System.out.println("RESULT passed=" + passed + " failed=" + failed);
if (failed != 0) {
throw new AssertionError("Regression failures: " + failed);
}
}
}