blob: 7e53883440c553e492982354dacbe80146aac2f6 [file] [log] [blame]
/**
* 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.
*/
package org.apache.hadoop.util;
import com.google.common.base.Supplier;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.security.alias.AbstractJavaKeyStoreProvider;
import org.junit.Assert;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.test.GenericTestUtils;
import static org.apache.hadoop.util.Shell.*;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.rules.Timeout;
public class TestShell extends Assert {
/**
* Set the timeout for every test
*/
@Rule
public Timeout testTimeout = new Timeout(30000);
@Rule
public TestName methodName = new TestName();
private File rootTestDir = GenericTestUtils.getTestDir();
/**
* A filename generated uniquely for each test method. The file
* itself is neither created nor deleted during test setup/teardown.
*/
private File methodDir;
private static class Command extends Shell {
private int runCount = 0;
private Command(long interval) {
super(interval);
}
@Override
protected String[] getExecString() {
// There is no /bin/echo equivalent on Windows so just launch it as a
// shell built-in.
//
return WINDOWS ?
(new String[] {"cmd.exe", "/c", "echo", "hello"}) :
(new String[] {"echo", "hello"});
}
@Override
protected void parseExecResult(BufferedReader lines) throws IOException {
++runCount;
}
public int getRunCount() {
return runCount;
}
}
@Before
public void setup() {
rootTestDir.mkdirs();
assertTrue("Not a directory " + rootTestDir, rootTestDir.isDirectory());
methodDir = new File(rootTestDir, methodName.getMethodName());
}
@Test
public void testInterval() throws IOException {
testInterval(Long.MIN_VALUE / 60000); // test a negative interval
testInterval(0L); // test a zero interval
testInterval(10L); // interval equal to 10mins
testInterval(Time.now() / 60000 + 60); // test a very big interval
}
/**
* Assert that a string has a substring in it
* @param string string to search
* @param search what to search for it
*/
private void assertInString(String string, String search) {
assertNotNull("Empty String", string);
if (!string.contains(search)) {
fail("Did not find \"" + search + "\" in " + string);
}
}
@Test
public void testShellCommandExecutorToString() throws Throwable {
Shell.ShellCommandExecutor sce=new Shell.ShellCommandExecutor(
new String[] { "ls", "..","arg 2"});
String command = sce.toString();
assertInString(command,"ls");
assertInString(command, " .. ");
assertInString(command, "\"arg 2\"");
}
@Test
public void testShellCommandTimeout() throws Throwable {
Assume.assumeFalse(WINDOWS);
String rootDir = rootTestDir.getAbsolutePath();
File shellFile = new File(rootDir, "timeout.sh");
String timeoutCommand = "sleep 4; echo \"hello\"";
Shell.ShellCommandExecutor shexc;
try (PrintWriter writer = new PrintWriter(new FileOutputStream(shellFile))) {
writer.println(timeoutCommand);
writer.close();
}
FileUtil.setExecutable(shellFile, true);
shexc = new Shell.ShellCommandExecutor(new String[]{shellFile.getAbsolutePath()},
null, null, 100);
try {
shexc.execute();
} catch (Exception e) {
//When timing out exception is thrown.
}
shellFile.delete();
assertTrue("Script did not timeout" , shexc.isTimedOut());
}
@Test
public void testEnvVarsWithInheritance() throws Exception {
Assume.assumeFalse(WINDOWS);
testEnvHelper(true);
}
@Test
public void testEnvVarsWithoutInheritance() throws Exception {
Assume.assumeFalse(WINDOWS);
testEnvHelper(false);
}
private void testEnvHelper(boolean inheritParentEnv) throws Exception {
Map<String, String> customEnv = Collections.singletonMap(
AbstractJavaKeyStoreProvider.CREDENTIAL_PASSWORD_ENV_VAR, "foo");
Shell.ShellCommandExecutor command = new ShellCommandExecutor(
new String[]{"env"}, null, customEnv, 0L,
inheritParentEnv);
command.execute();
String[] varsArr = command.getOutput().split("\n");
Map<String, String> vars = new HashMap<>();
for (String var : varsArr) {
int eqIndex = var.indexOf('=');
vars.put(var.substring(0, eqIndex), var.substring(eqIndex + 1));
}
Map<String, String> expectedEnv = new HashMap<>();
expectedEnv.putAll(System.getenv());
if (inheritParentEnv) {
expectedEnv.putAll(customEnv);
} else {
assertFalse("child process environment should not have contained "
+ AbstractJavaKeyStoreProvider.CREDENTIAL_PASSWORD_ENV_VAR,
vars.containsKey(
AbstractJavaKeyStoreProvider.CREDENTIAL_PASSWORD_ENV_VAR));
}
assertEquals(expectedEnv, vars);
}
private static int countTimerThreads() {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
int count = 0;
ThreadInfo[] infos = threadBean.getThreadInfo(threadBean.getAllThreadIds(), 20);
for (ThreadInfo info : infos) {
if (info == null) continue;
for (StackTraceElement elem : info.getStackTrace()) {
if (elem.getClassName().contains("Timer")) {
count++;
break;
}
}
}
return count;
}
@Test
public void testShellCommandTimerLeak() throws Exception {
String quickCommand[] = new String[] {"/bin/sleep", "100"};
int timersBefore = countTimerThreads();
System.err.println("before: " + timersBefore);
for (int i = 0; i < 10; i++) {
Shell.ShellCommandExecutor shexec = new Shell.ShellCommandExecutor(
quickCommand, null, null, 1);
try {
shexec.execute();
fail("Bad command should throw exception");
} catch (Exception e) {
// expected
}
}
Thread.sleep(1000);
int timersAfter = countTimerThreads();
System.err.println("after: " + timersAfter);
assertEquals(timersBefore, timersAfter);
}
@Test
public void testGetCheckProcessIsAliveCommand() throws Exception {
String anyPid = "9999";
String[] checkProcessAliveCommand = getCheckProcessIsAliveCommand(
anyPid);
String[] expectedCommand;
if (Shell.WINDOWS) {
expectedCommand =
new String[]{getWinUtilsPath(), "task", "isAlive", anyPid };
} else if (Shell.isSetsidAvailable) {
expectedCommand = new String[] { "bash", "-c", "kill -0 -- -'" +
anyPid + "'"};
} else {
expectedCommand = new String[] {"bash", "-c", "kill -0 '" + anyPid +
"'" };
}
Assert.assertArrayEquals(expectedCommand, checkProcessAliveCommand);
}
@Test
public void testGetSignalKillCommand() throws Exception {
String anyPid = "9999";
int anySignal = 9;
String[] checkProcessAliveCommand = getSignalKillCommand(anySignal,
anyPid);
String[] expectedCommand;
if (Shell.WINDOWS) {
expectedCommand =
new String[]{getWinUtilsPath(), "task", "kill", anyPid };
} else if (Shell.isSetsidAvailable) {
expectedCommand = new String[] { "bash", "-c", "kill -9 -- -'" + anyPid +
"'"};
} else {
expectedCommand = new String[]{ "bash", "-c", "kill -9 '" + anyPid +
"'"};
}
Assert.assertArrayEquals(expectedCommand, checkProcessAliveCommand);
}
private void testInterval(long interval) throws IOException {
Command command = new Command(interval);
command.run();
assertEquals(1, command.getRunCount());
command.run();
if (interval > 0) {
assertEquals(1, command.getRunCount());
} else {
assertEquals(2, command.getRunCount());
}
}
@Test
public void testHadoopHomeUnset() throws Throwable {
assertHomeResolveFailed(null, "unset");
}
@Test
public void testHadoopHomeEmpty() throws Throwable {
assertHomeResolveFailed("", E_HADOOP_PROPS_EMPTY);
}
@Test
public void testHadoopHomeEmptyDoubleQuotes() throws Throwable {
assertHomeResolveFailed("\"\"", E_HADOOP_PROPS_EMPTY);
}
@Test
public void testHadoopHomeEmptySingleQuote() throws Throwable {
assertHomeResolveFailed("\"", E_HADOOP_PROPS_EMPTY);
}
@Test
public void testHadoopHomeValid() throws Throwable {
File f = checkHadoopHomeInner(rootTestDir.getCanonicalPath());
assertEquals(rootTestDir, f);
}
@Test
public void testHadoopHomeValidQuoted() throws Throwable {
File f = checkHadoopHomeInner('"'+ rootTestDir.getCanonicalPath() + '"');
assertEquals(rootTestDir, f);
}
@Test
public void testHadoopHomeNoDir() throws Throwable {
assertHomeResolveFailed(methodDir.getCanonicalPath(), E_DOES_NOT_EXIST);
}
@Test
public void testHadoopHomeNotADir() throws Throwable {
File touched = touch(methodDir);
try {
assertHomeResolveFailed(touched.getCanonicalPath(), E_NOT_DIRECTORY);
} finally {
FileUtils.deleteQuietly(touched);
}
}
@Test
public void testHadoopHomeRelative() throws Throwable {
assertHomeResolveFailed("./target", E_IS_RELATIVE);
}
@Test
public void testBinDirMissing() throws Throwable {
FileNotFoundException ex = assertWinutilsResolveFailed(methodDir,
E_DOES_NOT_EXIST);
assertInString(ex.toString(), "Hadoop bin directory");
}
@Test
public void testHadoopBinNotADir() throws Throwable {
File bin = new File(methodDir, "bin");
touch(bin);
try {
assertWinutilsResolveFailed(methodDir, E_NOT_DIRECTORY);
} finally {
FileUtils.deleteQuietly(methodDir);
}
}
@Test
public void testBinWinUtilsFound() throws Throwable {
try {
File bin = new File(methodDir, "bin");
File winutils = new File(bin, WINUTILS_EXE);
touch(winutils);
assertEquals(winutils.getCanonicalPath(),
getQualifiedBinInner(methodDir, WINUTILS_EXE).getCanonicalPath());
} finally {
FileUtils.deleteQuietly(methodDir);
}
}
@Test
public void testBinWinUtilsNotAFile() throws Throwable {
try {
File bin = new File(methodDir, "bin");
File winutils = new File(bin, WINUTILS_EXE);
winutils.mkdirs();
assertWinutilsResolveFailed(methodDir, E_NOT_EXECUTABLE_FILE);
} finally {
FileUtils.deleteDirectory(methodDir);
}
}
/**
* This test takes advantage of the invariant winutils path is valid
* or access to it will raise an exception holds on Linux, and without
* any winutils binary even if HADOOP_HOME points to a real hadoop
* directory, the exception reporting can be validated
*/
@Test
public void testNoWinutilsOnUnix() throws Throwable {
Assume.assumeFalse(WINDOWS);
try {
getWinUtilsFile();
} catch (FileNotFoundException ex) {
assertExContains(ex, E_NOT_A_WINDOWS_SYSTEM);
}
try {
getWinUtilsPath();
} catch (RuntimeException ex) {
assertExContains(ex, E_NOT_A_WINDOWS_SYSTEM);
if ( ex.getCause() == null
|| !(ex.getCause() instanceof FileNotFoundException)) {
throw ex;
}
}
}
/**
* Touch a file; creating parent dirs on demand.
* @param path path of file
* @return the file created
* @throws IOException on any failure to write
*/
private File touch(File path) throws IOException {
path.getParentFile().mkdirs();
FileUtils.writeByteArrayToFile(path, new byte[]{});
return path;
}
/**
* Assert that an attept to resolve the hadoop home dir failed with
* an expected text in the exception string value.
* @param path input
* @param expectedText expected exception text
* @return the caught exception
* @throws FileNotFoundException any FileNotFoundException that was thrown
* but which did not contain the expected text
*/
private FileNotFoundException assertHomeResolveFailed(String path,
String expectedText) throws Exception {
try {
File f = checkHadoopHomeInner(path);
fail("Expected an exception with the text `" + expectedText + "`"
+ " -but got the path " + f);
// unreachable
return null;
} catch (FileNotFoundException ex) {
assertExContains(ex, expectedText);
return ex;
}
}
/**
* Assert that an attept to resolve the {@code bin/winutils.exe} failed with
* an expected text in the exception string value.
* @param hadoopHome hadoop home directory
* @param expectedText expected exception text
* @return the caught exception
* @throws Exception any Exception that was thrown
* but which did not contain the expected text
*/
private FileNotFoundException assertWinutilsResolveFailed(File hadoopHome,
String expectedText) throws Exception {
try {
File f = getQualifiedBinInner(hadoopHome, WINUTILS_EXE);
fail("Expected an exception with the text `" + expectedText + "`"
+ " -but got the path " + f);
// unreachable
return null;
} catch (FileNotFoundException ex) {
assertExContains(ex, expectedText);
return ex;
}
}
private void assertExContains(Exception ex, String expectedText)
throws Exception {
if (!ex.toString().contains(expectedText)) {
throw ex;
}
}
@Test
public void testBashQuote() {
assertEquals("'foobar'", Shell.bashQuote("foobar"));
assertEquals("'foo'\\''bar'", Shell.bashQuote("foo'bar"));
assertEquals("''\\''foo'\\''bar'\\'''", Shell.bashQuote("'foo'bar'"));
}
@Test(timeout=120000)
public void testDestroyAllShellProcesses() throws Throwable {
Assume.assumeFalse(WINDOWS);
StringBuffer sleepCommand = new StringBuffer();
sleepCommand.append("sleep 200");
String[] shellCmd = {"bash", "-c", sleepCommand.toString()};
final ShellCommandExecutor shexc1 = new ShellCommandExecutor(shellCmd);
final ShellCommandExecutor shexc2 = new ShellCommandExecutor(shellCmd);
Thread shellThread1 = new Thread() {
@Override
public void run() {
try {
shexc1.execute();
} catch(IOException ioe) {
//ignore IOException from thread interrupt
}
}
};
Thread shellThread2 = new Thread() {
@Override
public void run() {
try {
shexc2.execute();
} catch(IOException ioe) {
//ignore IOException from thread interrupt
}
}
};
shellThread1.start();
shellThread2.start();
GenericTestUtils.waitFor(new Supplier<Boolean>() {
@Override
public Boolean get() {
return shexc1.getProcess() != null;
}
}, 10, 10000);
GenericTestUtils.waitFor(new Supplier<Boolean>() {
@Override
public Boolean get() {
return shexc2.getProcess() != null;
}
}, 10, 10000);
Shell.destroyAllShellProcesses();
shexc1.getProcess().waitFor();
shexc2.getProcess().waitFor();
}
}