Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Exec] Properly split command & pipe support #6819

Merged
merged 8 commits into from
Jan 25, 2020
Merged
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: 3 additions & 1 deletion bundles/org.openhab.binding.exec/pom.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

Expand Down
13 changes: 7 additions & 6 deletions bundles/org.openhab.binding.exec/src/main/feature/feature.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.exec-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<features name="org.openhab.binding.exec-${project.version}"
xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>

<feature name="openhab-binding-exec" description="Exec Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.exec/${project.version}</bundle>
</feature>
<feature name="openhab-binding-exec" description="Exec Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.exec/${project.version}</bundle>
</feature>
</features>
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Calendar;
import java.util.IllegalFormatException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
Expand Down Expand Up @@ -52,9 +54,20 @@
* sent to one of the channels.
*
* @author Karel Goderis - Initial contribution
* @author Constantin Piber - Added better argument support (delimiter and pass to shell)
*/
@NonNullByDefault
public class ExecHandler extends BaseThingHandler {
/**
* Use this to separate between command and parameter, and also between parameters.
*/
public static final String CMD_LINE_DELIMITER = "@@";

/**
* Shell executables
*/
public static final String[] SHELL_WINDOWS = new String[] { "cmd" };
public static final String[] SHELL_NIX = new String[] { "sh", "bash", "zsh", "csh" };

private Logger logger = LoggerFactory.getLogger(ExecHandler.class);

Expand Down Expand Up @@ -97,9 +110,8 @@ public void handleCommand(ChannelUID channelUID, Command command) {
lastInput = command.toString();
if (lastInput != null && !lastInput.equals(previousInput)) {
if (getConfig().get(AUTORUN) != null && ((Boolean) getConfig().get(AUTORUN)).booleanValue()) {
lastInput = command.toString();
9037568 marked this conversation as resolved.
Show resolved Hide resolved
logger.trace("Executing command '{}' after a change of the input channel to '{}'",
getConfig().get(COMMAND), command.toString());
getConfig().get(COMMAND), lastInput);
scheduler.schedule(periodicExecutionRunnable, 0, TimeUnit.SECONDS);
}
}
Expand Down Expand Up @@ -160,21 +172,66 @@ public void run() {
commandLine = String.format(commandLine, Calendar.getInstance().getTime());
}
} catch (IllegalFormatException e) {
logger.error(
logger.warn(
"An exception occurred while formatting the command line with the current time and input values : '{}'",
e.getMessage());
updateState(RUN, OnOffType.OFF);
return;
}

logger.trace("The command to be executed will be '{}'", commandLine);
String[] cmdArray;
String[] shell;
if (commandLine.contains(CMD_LINE_DELIMITER)) {
logger.debug("Splitting by '{}'", CMD_LINE_DELIMITER);
try {
cmdArray = commandLine.split(CMD_LINE_DELIMITER);
} catch (PatternSyntaxException e) {
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
updateState(RUN, OnOffType.OFF);
updateState(OUTPUT, new StringType(e.getMessage()));
return;
}
} else {
// Invoke shell with 'c' option and pass string
logger.debug("Passing to shell for parsing command.");
switch (getOperatingSystemType()) {
case WINDOWS:
shell = SHELL_WINDOWS;
logger.debug("OS: WINDOWS ({})", getOperatingSystemName());
cmdArray = createCmdArray(shell, "/c", commandLine);
break;

case LINUX:
case MAC:
case SOLARIS:
// assume sh is present, should all be POSIX-compliant
shell = SHELL_NIX;
logger.debug("OS: *NIX ({})", getOperatingSystemName());
cmdArray = createCmdArray(shell, "-c", commandLine);

default:
logger.debug("OS: Unknown ({})", getOperatingSystemName());
logger.warn("OS {} not supported, please manually split commands!",
getOperatingSystemName());
updateState(RUN, OnOffType.OFF);
updateState(OUTPUT, new StringType("OS not supported, please manually split commands!"));
return;
}
}

if (cmdArray.length == 0) {
logger.trace("Empty command received, not executing");
return;
}

logger.trace("The command to be executed will be '{}'", Arrays.asList(cmdArray));

Process proc = null;
try {
proc = rt.exec(commandLine.toString());
proc = rt.exec(cmdArray);
} catch (Exception e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to catch Exception here? According to the docs there is a limited number of exceptions, I think we can omit the NPE if we make sure that none of the commands is null (which should be done anyway).

logger.error("An exception occurred while executing '{}' : '{}'",
new Object[] { commandLine.toString(), e.getMessage() });
logger.warn("An exception occurred while executing '{}' : '{}'", Arrays.asList(cmdArray),
e.getMessage());
updateState(RUN, OnOffType.OFF);
updateState(OUTPUT, new StringType(e.getMessage()));
return;
Expand All @@ -192,8 +249,8 @@ public void run() {
}
isr.close();
} catch (IOException e) {
logger.error("An exception occurred while reading the stdout when executing '{}' : '{}'",
new Object[] { commandLine.toString(), e.getMessage() });
logger.warn("An exception occurred while reading the stdout when executing '{}' : '{}'",
commandLine, e.getMessage());
}

try (InputStreamReader isr = new InputStreamReader(proc.getErrorStream());
Expand All @@ -205,21 +262,21 @@ public void run() {
}
isr.close();
} catch (IOException e) {
logger.error("An exception occurred while reading the stderr when executing '{}' : '{}'",
new Object[] { commandLine.toString(), e.getMessage() });
logger.warn("An exception occurred while reading the stderr when executing '{}' : '{}'",
commandLine, e.getMessage());
}

boolean exitVal = false;
try {
exitVal = proc.waitFor(timeOut, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
logger.error("An exception occurred while waiting for the process ('{}') to finish : '{}'",
new Object[] { commandLine.toString(), e.getMessage() });
logger.warn("An exception occurred while waiting for the process ('{}') to finish : '{}'",
commandLine, e.getMessage());
}

if (!exitVal) {
logger.warn("Forcibly termininating the process ('{}') after a timeout of {} ms",
new Object[] { commandLine.toString(), timeOut });
logger.warn("Forcibly termininating the process ('{}') after a timeout of {} ms", commandLine,
timeOut);
proc.destroyForcibly();
}

Expand Down Expand Up @@ -264,8 +321,8 @@ public void run() {
transformationType);
}
} catch (TransformationException te) {
logger.error("An exception occurred while transforming '{}' with '{}' : '{}'",
new Object[] { response, transformation, te.getMessage() });
logger.warn("An exception occurred while transforming '{}' with '{}' : '{}'", response, transformation,
te.getMessage());

// in case of an error we return the response without any transformation
transformedResponse = response;
Expand Down Expand Up @@ -298,4 +355,77 @@ protected String[] splitTransformationConfig(String transformation) {
return new String[] { type, pattern };
}

/**
* Transforms the command string into an array.
* Either invokes the shell and passes using the "c" option
* or (if command already starts with one of the shells) splits by space.
*
* @param shell (path), picks to first one to execute the command
* @param "c"-option string
* @param command to execute
* @return command array
*/
protected String[] createCmdArray(String[] shell, String cOption, String commandLine) {
boolean startsWithShell = false;
for (String sh : shell) {
if (commandLine.startsWith(sh + " ")) {
startsWithShell = true;
break;
}
}

if (!startsWithShell) {
return new String[] { shell[0], cOption, commandLine };
} else {
logger.debug("Splitting by spaces");
try {
return commandLine.split(" ");
} catch (PatternSyntaxException e) {
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
updateState(RUN, OnOffType.OFF);
updateState(OUTPUT, new StringType(e.getMessage()));
return new String[] {};
}
}
}

/**
* Contains information about which operating system openHAB is running on.
* Found on https://stackoverflow.com/a/31547504/7508309, slightly modified
*
* @author Constantin Piber (for Memin) - Initial contribution
*/
public enum OS {
WINDOWS,
LINUX,
MAC,
SOLARIS,
UNKNOWN,
NOT_SET
};

private static OS os = OS.NOT_SET;

public static OS getOperatingSystemType() {
if (os == OS.NOT_SET) {
String operSys = System.getProperty("os.name").toLowerCase();
if (operSys.contains("win")) {
os = OS.WINDOWS;
} else if (operSys.contains("nix") || operSys.contains("nux") || operSys.contains("aix")) {
os = OS.LINUX;
} else if (operSys.contains("mac")) {
os = OS.MAC;
} else if (operSys.contains("sunos")) {
os = OS.SOLARIS;
} else {
os = OS.UNKNOWN;
}
}
return os;
}

public static String getOperatingSystemName() {
return System.getProperty("os.name");
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="exec"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<binding:binding id="exec" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">

<name>Exec Binding</name>
<description>This is the binding to execute arbitrary shell commands</description>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="exec"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">

<thing-type id="command">
<label>Command</label>
<description>The Command encapsulates a shell command to be executed</description>

<channels>
<channel id="output" typeId="output"/>
<channel id="input" typeId="input"/>
<channel id="exit" typeId="exit"/>
<channel id="run" typeId="run"/>
<channel id="output" typeId="output" />
<channel id="input" typeId="input" />
<channel id="exit" typeId="exit" />
<channel id="run" typeId="run" />
<channel id="lastexecution" typeId="lastexecution" />
</channels>

Expand All @@ -26,17 +26,17 @@
<description>The transformation to apply on the execution result, e.g. REGEX((.*))</description>
<default>REGEX((.*))</default>
</parameter>
<parameter name="interval" type="integer" required="false">
<parameter name="interval" type="integer" required="false">
<label>Interval</label>
<description>Interval, in seconds, the command will be repeatedly executed</description>
<default>0</default>
</parameter>
<parameter name="timeout" type="integer" required="false">
<parameter name="timeout" type="integer" required="false">
<label>Timeout</label>
<description>Time out, in seconds, the execution of the command will time out</description>
<default>15</default>
</parameter>
<parameter name="autorun" type="boolean" required="false">
<parameter name="autorun" type="boolean" required="false">
<label>Autorun</label>
<description>When true, the command will execute each time the state of the input channel changes</description>
<default>false</default>
Expand Down