Skip to content

Commit

Permalink
[Exec] Properly split command & pipe support (openhab#6819)
Browse files Browse the repository at this point in the history
* [Exec] Properly split command

Signed-off-by: Constantin Piber <cp.piber@gmail.com>
Signed-off-by: leluna <hengrui.jiang@googlemail.com>
  • Loading branch information
cpiber authored and leluna committed Mar 21, 2020
1 parent 0ee694f commit 2287bf0
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 38 deletions.
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();
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) {
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

0 comments on commit 2287bf0

Please sign in to comment.