blob: 895ed3ddcd2bc29ac0fc727be03b2bf47ec28875 [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.zeppelin.shell;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.lang3.StringUtils;
import org.apache.zeppelin.interpreter.ZeppelinContext;
import org.apache.zeppelin.util.ExecutorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.InterpreterException;
import org.apache.zeppelin.interpreter.InterpreterResult;
import org.apache.zeppelin.interpreter.InterpreterResult.Code;
import org.apache.zeppelin.interpreter.KerberosInterpreter;
import org.apache.zeppelin.scheduler.Scheduler;
import org.apache.zeppelin.scheduler.SchedulerFactory;
/**
* Shell interpreter for Zeppelin.
*/
public class ShellInterpreter extends KerberosInterpreter {
private static final Logger LOGGER = LoggerFactory.getLogger(ShellInterpreter.class);
private static final String TIMEOUT_PROPERTY = "shell.command.timeout.millisecs";
private static final String TIMEOUT_CHECK_INTERVAL_PROPERTY =
"shell.command.timeout.check.interval";
private static final String DEFAULT_TIMEOUT = "60000";
private static final String DEFAULT_CHECK_INTERVAL = "10000";
private static final String DIRECTORY_USER_HOME = "shell.working.directory.user.home";
private static final String MAX_CONCURRENCY = "shell.concurrency.max";
private final boolean isWindows = System.getProperty("os.name").startsWith("Windows");
private final String shell = isWindows ? "cmd /c" : "bash -c";
private ConcurrentHashMap<String, DefaultExecutor> executorMap;
private ConcurrentHashMap<String, InterpreterContext> contextMap;
private ScheduledExecutorService shellOutputCheckExecutor =
Executors.newSingleThreadScheduledExecutor();
public ShellInterpreter(Properties property) {
super(property);
}
@Override
public void open() {
super.open();
long timeoutThreshold = Long.parseLong(getProperty(TIMEOUT_PROPERTY, DEFAULT_TIMEOUT));
long timeoutCheckInterval = Long.parseLong(
getProperty(TIMEOUT_CHECK_INTERVAL_PROPERTY, DEFAULT_CHECK_INTERVAL));
LOGGER.info("Command timeout property: {}", timeoutThreshold);
executorMap = new ConcurrentHashMap<>();
contextMap = new ConcurrentHashMap<>();
shellOutputCheckExecutor.scheduleAtFixedRate(() -> {
try {
for (Map.Entry<String, DefaultExecutor> entry : executorMap.entrySet()) {
String paragraphId = entry.getKey();
DefaultExecutor executor = entry.getValue();
InterpreterContext context = contextMap.get(paragraphId);
if (context == null) {
LOGGER.warn("No InterpreterContext associated with paragraph: {}", paragraphId);
continue;
}
if ((System.currentTimeMillis() - context.out.getLastWriteTimestamp()) >
timeoutThreshold) {
LOGGER.info("No output for paragraph {} for the last {} milli-seconds, so kill it",
paragraphId, timeoutThreshold);
executor.getWatchdog().destroyProcess();
}
}
} catch (Exception e) {
LOGGER.error("Error when checking shell command timeout", e);
}
}, timeoutCheckInterval, timeoutCheckInterval, TimeUnit.MILLISECONDS);
}
@Override
public void close() {
super.close();
for (String executorKey : executorMap.keySet()) {
DefaultExecutor executor = executorMap.remove(executorKey);
if (executor != null) {
try {
executor.getWatchdog().destroyProcess();
} catch (Exception e){
LOGGER.error("error destroying executor for paragraphId: " + executorKey, e);
}
}
}
if (shellOutputCheckExecutor != null) {
ExecutorUtil.softShutdown("ShellOutputCheckExecutor", shellOutputCheckExecutor,
5, TimeUnit.SECONDS);
}
}
@Override
protected boolean isInterpolate() {
return Boolean.parseBoolean(getProperty("zeppelin.shell.interpolation", "false"));
}
@Override
public ZeppelinContext getZeppelinContext() {
return null;
}
@Override
public InterpreterResult internalInterpret(String cmd,
InterpreterContext context) {
LOGGER.debug("Run shell command '{}'", cmd);
CommandLine cmdLine = CommandLine.parse(shell);
// the Windows CMD shell doesn't handle multiline statements,
// they need to be delimited by '&&' instead
if (isWindows) {
String[] lines = StringUtils.split(cmd, "\n");
cmd = StringUtils.join(lines, " && ");
}
cmdLine.addArgument(cmd, false);
try {
contextMap.put(context.getParagraphId(), context);
DefaultExecutor executor = new DefaultExecutor();
executor.setStreamHandler(new PumpStreamHandler(
context.out, context.out));
executor.setWatchdog(new ExecuteWatchdog(Long.MAX_VALUE));
executorMap.put(context.getParagraphId(), executor);
if (Boolean.valueOf(getProperty(DIRECTORY_USER_HOME))) {
executor.setWorkingDirectory(new File(System.getProperty("user.home")));
}
int exitVal = executor.execute(cmdLine);
LOGGER.info("Paragraph {} return with exit value: {}", context.getParagraphId(), exitVal);
if (exitVal == 0) {
return new InterpreterResult(Code.SUCCESS);
} else {
return new InterpreterResult(Code.ERROR);
}
} catch (ExecuteException e) {
int exitValue = e.getExitValue();
LOGGER.error("Can not run command: " + cmd, e);
Code code = Code.ERROR;
StringBuilder messageBuilder = new StringBuilder();
if (exitValue == 143) {
code = Code.INCOMPLETE;
messageBuilder.append("Paragraph received a SIGTERM\n");
LOGGER.info("The paragraph {} stopped executing: {}",
context.getParagraphId(), messageBuilder.toString());
}
messageBuilder.append("ExitValue: " + exitValue);
return new InterpreterResult(code, messageBuilder.toString());
} catch (IOException e) {
LOGGER.error("Can not run command: " + cmd, e);
return new InterpreterResult(Code.ERROR, e.getMessage());
} finally {
executorMap.remove(context.getParagraphId());
contextMap.remove(context.getParagraphId());
}
}
@Override
public void cancel(InterpreterContext context) {
DefaultExecutor executor = executorMap.remove(context.getParagraphId());
if (executor != null) {
try {
executor.getWatchdog().destroyProcess();
} catch (Exception e){
LOGGER.error("error destroying executor for paragraphId: " + context.getParagraphId(), e);
}
}
}
@Override
public FormType getFormType() {
return FormType.NATIVE;
}
@Override
public int getProgress(InterpreterContext context) {
return 0;
}
@Override
public Scheduler getScheduler() {
return SchedulerFactory.singleton().createOrGetParallelScheduler(
ShellInterpreter.class.getName() + this.hashCode(), getMaxConcurrent());
}
private int getMaxConcurrent() {
String maxConcurrencyProperty = getProperty(MAX_CONCURRENCY, "10");
try {
return Integer.valueOf(maxConcurrencyProperty);
} catch (NumberFormatException e) {
LOGGER.error("Fail to parse {} with value: {}", MAX_CONCURRENCY,
maxConcurrencyProperty);
return 10;
}
}
@Override
protected boolean runKerberosLogin() {
try {
createSecureConfiguration();
return true;
} catch (Exception e) {
LOGGER.error("Unable to run kinit for zeppelin", e);
}
return false;
}
public ConcurrentHashMap<String, DefaultExecutor> getExecutorMap() {
return executorMap;
}
public void createSecureConfiguration() throws InterpreterException {
Properties properties = getProperties();
CommandLine cmdLine = CommandLine.parse(shell);
cmdLine.addArgument("-c", false);
String kinitCommand = String.format("kinit -k -t %s %s",
properties.getProperty("zeppelin.shell.keytab.location"),
properties.getProperty("zeppelin.shell.principal"));
cmdLine.addArgument(kinitCommand, false);
DefaultExecutor executor = new DefaultExecutor();
try {
executor.execute(cmdLine);
} catch (Exception e) {
LOGGER.error("Unable to run kinit for zeppelin user " + kinitCommand, e);
throw new InterpreterException(e);
}
}
@Override
protected boolean isKerboseEnabled() {
if (StringUtils.isNotBlank(getProperty("zeppelin.shell.auth.type")) && getProperty(
"zeppelin.shell.auth.type").equalsIgnoreCase("kerberos")) {
return true;
}
return false;
}
}