mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[exec] Prevent deadlock (#19152)
* Add TextOutputConsumer * Add stdout and stderr channels * Add charset configuration * Add upgrade instructions Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
This commit is contained in:
+2
@@ -30,6 +30,8 @@ public class ExecBindingConstants {
|
|||||||
|
|
||||||
// List of all Channel ids
|
// List of all Channel ids
|
||||||
public static final String OUTPUT = "output";
|
public static final String OUTPUT = "output";
|
||||||
|
public static final String STDOUT = "stdout";
|
||||||
|
public static final String STDERR = "stderr";
|
||||||
public static final String INPUT = "input";
|
public static final String INPUT = "input";
|
||||||
public static final String EXIT = "exit";
|
public static final String EXIT = "exit";
|
||||||
public static final String RUN = "run";
|
public static final String RUN = "run";
|
||||||
|
|||||||
+63
-24
@@ -18,16 +18,22 @@ import java.io.BufferedReader;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.IllegalCharsetNameException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.charset.UnsupportedCharsetException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.IllegalFormatException;
|
import java.util.IllegalFormatException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.regex.PatternSyntaxException;
|
import java.util.regex.PatternSyntaxException;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
@@ -82,6 +88,7 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
public static final String COMMAND = "command";
|
public static final String COMMAND = "command";
|
||||||
public static final String TRANSFORM = "transform";
|
public static final String TRANSFORM = "transform";
|
||||||
public static final String AUTORUN = "autorun";
|
public static final String AUTORUN = "autorun";
|
||||||
|
public static final String CHARSET = "charset";
|
||||||
|
|
||||||
private ExecutorService executor;
|
private ExecutorService executor;
|
||||||
private @Nullable ScheduledFuture<?> scheduledTask;
|
private @Nullable ScheduledFuture<?> scheduledTask;
|
||||||
@@ -172,18 +179,22 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
timeOut = ((BigDecimal) getConfig().get(TIME_OUT)).intValue() * 1000;
|
timeOut = ((BigDecimal) getConfig().get(TIME_OUT)).intValue() * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Charset charset = null;
|
||||||
|
String charsetValue = (String) getConfig().get(CHARSET);
|
||||||
|
if (charsetValue != null && !charsetValue.isBlank()) {
|
||||||
|
try {
|
||||||
|
charset = Charset.forName(charsetValue);
|
||||||
|
} catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
|
||||||
|
logger.warn("Invalid or unsupported character encoding '{}', falling back to UTF-8", charsetValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (charset == null) {
|
||||||
|
charset = StandardCharsets.UTF_8;
|
||||||
|
}
|
||||||
|
|
||||||
if (commandLine != null && !commandLine.isEmpty()) {
|
if (commandLine != null && !commandLine.isEmpty()) {
|
||||||
updateState(RUN, OnOffType.ON);
|
updateState(RUN, OnOffType.ON);
|
||||||
|
|
||||||
// For some obscure reason, when using Apache Common Exec, or using a straight implementation of
|
|
||||||
// Runtime.Exec(), on Mac OS X (Yosemite and El Capitan), there seems to be a lock race condition
|
|
||||||
// randomly appearing (on UNIXProcess) *when* one tries to gobble up the stdout and sterr output of the
|
|
||||||
// subprocess in separate threads. It seems to be common "wisdom" to do that in separate threads, but
|
|
||||||
// only when keeping everything between .exec() and .waitfor() in the same thread, this lock race
|
|
||||||
// condition seems to go away. This approach of not reading the outputs in separate threads *might* be a
|
|
||||||
// problem for external commands that generate a lot of output, but this will be dependent on the limits
|
|
||||||
// of the underlying operating system.
|
|
||||||
|
|
||||||
Date date = Calendar.getInstance().getTime();
|
Date date = Calendar.getInstance().getTime();
|
||||||
try {
|
try {
|
||||||
if (lastInput != null) {
|
if (lastInput != null) {
|
||||||
@@ -197,6 +208,8 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
commandLine, date, lastInput, e.getMessage());
|
commandLine, date, lastInput, e.getMessage());
|
||||||
updateState(RUN, OnOffType.OFF);
|
updateState(RUN, OnOffType.OFF);
|
||||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||||
|
updateState(STDOUT, new StringType());
|
||||||
|
updateState(STDERR, new StringType(e.getMessage()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +223,8 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
|
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
|
||||||
updateState(RUN, OnOffType.OFF);
|
updateState(RUN, OnOffType.OFF);
|
||||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||||
|
updateState(STDOUT, new StringType());
|
||||||
|
updateState(STDERR, new StringType(e.getMessage()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -235,6 +250,8 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
logger.warn("OS {} not supported, please manually split commands!", getOperatingSystemName());
|
logger.warn("OS {} not supported, please manually split commands!", getOperatingSystemName());
|
||||||
updateState(RUN, OnOffType.OFF);
|
updateState(RUN, OnOffType.OFF);
|
||||||
updateState(OUTPUT, new StringType("OS not supported, please manually split commands!"));
|
updateState(OUTPUT, new StringType("OS not supported, please manually split commands!"));
|
||||||
|
updateState(STDOUT, new StringType());
|
||||||
|
updateState(STDERR, new StringType("OS not supported, please manually split commands!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,13 +271,19 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
e.getMessage());
|
e.getMessage());
|
||||||
updateState(RUN, OnOffType.OFF);
|
updateState(RUN, OnOffType.OFF);
|
||||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||||
|
updateState(STDOUT, new StringType());
|
||||||
|
updateState(STDERR, new StringType(e.getMessage()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder outputBuilder = new StringBuilder();
|
StringBuilder outputBuilder = new StringBuilder();
|
||||||
StringBuilder errorBuilder = new StringBuilder();
|
StringBuilder errorBuilder = new StringBuilder();
|
||||||
|
|
||||||
try (InputStreamReader isr = new InputStreamReader(proc.getInputStream());
|
TextOutputConsumer errConsumer = new TextOutputConsumer();
|
||||||
|
Future<List<String>> stdErrFuture = errConsumer.consume(proc.getErrorStream(), charset,
|
||||||
|
Thread.currentThread().getName() + "-stderr-consumer");
|
||||||
|
|
||||||
|
try (InputStreamReader isr = new InputStreamReader(proc.getInputStream(), charset);
|
||||||
BufferedReader br = new BufferedReader(isr)) {
|
BufferedReader br = new BufferedReader(isr)) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = br.readLine()) != null) {
|
while ((line = br.readLine()) != null) {
|
||||||
@@ -272,18 +295,6 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
e.getMessage());
|
e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
try (InputStreamReader isr = new InputStreamReader(proc.getErrorStream());
|
|
||||||
BufferedReader br = new BufferedReader(isr)) {
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
errorBuilder.append(line).append("\n");
|
|
||||||
logger.debug("Exec [{}]: '{}'", "ERROR", line);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("An exception occurred while reading the stderr when executing '{}' : '{}'", commandLine,
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean exitVal = false;
|
boolean exitVal = false;
|
||||||
try {
|
try {
|
||||||
exitVal = proc.waitFor(timeOut, TimeUnit.MILLISECONDS);
|
exitVal = proc.waitFor(timeOut, TimeUnit.MILLISECONDS);
|
||||||
@@ -297,17 +308,43 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
proc.destroyForcibly();
|
proc.destroyForcibly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<String> stdErr = stdErrFuture.get(timeOut, TimeUnit.MILLISECONDS);
|
||||||
|
for (String line : stdErr) {
|
||||||
|
errorBuilder.append(line).append("\n");
|
||||||
|
logger.debug("Exec [{}]: '{}'", "ERROR", line);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.debug("Interrupted while waiting for process ('{}') stderr content", commandLine);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
logger.warn("Timed out while waiting for process ('{}') stderr content", commandLine);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
logger.warn("An exception occurred while reading the stderr when executing '{}' : '{}'", commandLine,
|
||||||
|
cause != null ? cause.getMessage() : e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
updateState(RUN, OnOffType.OFF);
|
updateState(RUN, OnOffType.OFF);
|
||||||
updateState(EXIT, new DecimalType(proc.exitValue()));
|
updateState(EXIT, new DecimalType(proc.exitValue()));
|
||||||
|
|
||||||
|
ChannelTransformation transformation = channelTransformation;
|
||||||
|
String transformedStdout = Objects.requireNonNull(StringUtils.chomp(outputBuilder.toString()));
|
||||||
|
String transformedStderr = Objects.requireNonNull(StringUtils.chomp(errorBuilder.toString()));
|
||||||
|
if (transformation != null) {
|
||||||
|
transformedStdout = transformation.apply(transformedStdout).orElse(transformedStdout);
|
||||||
|
transformedStderr = transformation.apply(transformedStderr).orElse(transformedStderr);
|
||||||
|
}
|
||||||
|
updateState(STDOUT, new StringType(transformedStdout));
|
||||||
|
updateState(STDERR, new StringType(transformedStderr));
|
||||||
|
|
||||||
outputBuilder.append(errorBuilder.toString());
|
outputBuilder.append(errorBuilder.toString());
|
||||||
|
|
||||||
outputBuilder.append(errorBuilder.toString());
|
outputBuilder.append(errorBuilder.toString());
|
||||||
|
|
||||||
String transformedResponse = Objects.requireNonNull(StringUtils.chomp(outputBuilder.toString()));
|
String transformedResponse = Objects.requireNonNull(StringUtils.chomp(outputBuilder.toString()));
|
||||||
|
|
||||||
if (channelTransformation != null) {
|
if (transformation != null) {
|
||||||
transformedResponse = channelTransformation.apply(transformedResponse).orElse(transformedResponse);
|
transformedResponse = transformation.apply(transformedResponse).orElse(transformedResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState(OUTPUT, new StringType(transformedResponse));
|
updateState(OUTPUT, new StringType(transformedResponse));
|
||||||
@@ -344,6 +381,8 @@ public class ExecHandler extends BaseThingHandler {
|
|||||||
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
|
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
|
||||||
updateState(RUN, OnOffType.OFF);
|
updateState(RUN, OnOffType.OFF);
|
||||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||||
|
updateState(STDOUT, new StringType());
|
||||||
|
updateState(STDERR, new StringType(e.getMessage()));
|
||||||
return new String[] {};
|
return new String[] {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.exec.internal.handler;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.FutureTask;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawns a new thread that consumes all bytes from the specified {@link InputStream}, converts
|
||||||
|
* the stream to characters using the specified {@link Charset} and then returns the content
|
||||||
|
* as a {@link List} of {@link String}s, one {@link String} per "line" of text.
|
||||||
|
*
|
||||||
|
* @author Ravi Nadahar - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class TextOutputConsumer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using UTF-8. The
|
||||||
|
* result is returned as a {@link FutureTask}.
|
||||||
|
*
|
||||||
|
* @param inputStream the {@link InputStream} to consume.
|
||||||
|
* @return The {@link FutureTask} where the task can be cancelled and the results retrieved.
|
||||||
|
*/
|
||||||
|
public FutureTask<List<String>> consume(InputStream inputStream) {
|
||||||
|
return consume(inputStream, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using the
|
||||||
|
* specified {@link Charset}. The result is returned as a {@link FutureTask}.
|
||||||
|
*
|
||||||
|
* @param inputStream the {@link InputStream} to consume.
|
||||||
|
* @param charset the {@link Charset} to use for byte to character conversion.
|
||||||
|
* will be generated.
|
||||||
|
* @return The {@link FutureTask} where the task can be cancelled and the results retrieved.
|
||||||
|
*/
|
||||||
|
public FutureTask<List<String>> consume(InputStream inputStream, @Nullable Charset charset) {
|
||||||
|
return consume(inputStream, charset, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using UTF-8. The
|
||||||
|
* result is returned as a {@link FutureTask}.
|
||||||
|
*
|
||||||
|
* @param inputStream the {@link InputStream} to consume.
|
||||||
|
* @param threadName the name of the worker thread that will do the consumption. If {@code null}, a thread name
|
||||||
|
* will be generated.
|
||||||
|
* @return The {@link FutureTask} where the task can be cancelled and the results retrieved.
|
||||||
|
*/
|
||||||
|
public FutureTask<List<String>> consume(InputStream inputStream, @Nullable String threadName) {
|
||||||
|
return consume(inputStream, null, threadName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using the
|
||||||
|
* specified {@link Charset}. The result is returned as a {@link FutureTask}.
|
||||||
|
*
|
||||||
|
* @param inputStream the {@link InputStream} to consume.
|
||||||
|
* @param charset the {@link Charset} to use for byte to character conversion.
|
||||||
|
* @param threadName the name of the worker thread that will do the consumption. If {@code null}, a thread name
|
||||||
|
* will be generated.
|
||||||
|
* @return The {@link FutureTask} where the task can be cancelled and the results retrieved.
|
||||||
|
*/
|
||||||
|
public FutureTask<List<String>> consume(final InputStream inputStream, @Nullable Charset charset,
|
||||||
|
@Nullable String threadName) {
|
||||||
|
FutureTask<List<String>> result = new FutureTask<List<String>>(() -> {
|
||||||
|
List<String> output = new ArrayList<>();
|
||||||
|
try (InputStreamReader isr = new InputStreamReader(inputStream,
|
||||||
|
charset == null ? StandardCharsets.UTF_8 : charset); BufferedReader br = new BufferedReader(isr)) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
output.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread runner;
|
||||||
|
if (threadName == null || threadName.isBlank()) {
|
||||||
|
runner = new Thread(result);
|
||||||
|
} else {
|
||||||
|
runner = new Thread(result, threadName);
|
||||||
|
}
|
||||||
|
runner.start();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ thing-type.config.exec.command.timeout.label = Timeout
|
|||||||
thing-type.config.exec.command.timeout.description = Time out, in seconds, the execution of the command will time out
|
thing-type.config.exec.command.timeout.description = Time out, in seconds, the execution of the command will time out
|
||||||
thing-type.config.exec.command.transform.label = Transform
|
thing-type.config.exec.command.transform.label = Transform
|
||||||
thing-type.config.exec.command.transform.description = The transformation to apply on the execution result, e.g. REGEX((.*)). You can chain transformations by listing each transformation on a separate line, or by separating them with the intersection character ∩.
|
thing-type.config.exec.command.transform.description = The transformation to apply on the execution result, e.g. REGEX((.*)). You can chain transformations by listing each transformation on a separate line, or by separating them with the intersection character ∩.
|
||||||
|
thing-type.config.exec.command.charset.label = Character Encoding
|
||||||
|
thing-type.config.exec.command.charset.description = The character encoding to use for process output. Any string valid for Java's Charset.forName() can be used. If blank, UTF-8 will be used.
|
||||||
|
|
||||||
# channel types
|
# channel types
|
||||||
|
|
||||||
@@ -31,5 +33,9 @@ channel-type.exec.lastexecution.label = Last Execution
|
|||||||
channel-type.exec.lastexecution.description = Time/Date the command was last executed, in yyyy-MM-dd'T'HH:mm:ss.SSSZ format
|
channel-type.exec.lastexecution.description = Time/Date the command was last executed, in yyyy-MM-dd'T'HH:mm:ss.SSSZ format
|
||||||
channel-type.exec.output.label = Output
|
channel-type.exec.output.label = Output
|
||||||
channel-type.exec.output.description = Output of the last execution of the command
|
channel-type.exec.output.description = Output of the last execution of the command
|
||||||
|
channel-type.exec.stdout.label = Standard Out
|
||||||
|
channel-type.exec.stdout.description = The stdout output of the last execution of the command
|
||||||
|
channel-type.exec.stderr.label = Standard Error
|
||||||
|
channel-type.exec.stderr.description = The stderr output of the last execution of the command
|
||||||
channel-type.exec.run.label = Running
|
channel-type.exec.run.label = Running
|
||||||
channel-type.exec.run.description = Send ON to execute the command and the current state tells whether it is running or not
|
channel-type.exec.run.description = Send ON to execute the command and the current state tells whether it is running or not
|
||||||
|
|||||||
@@ -10,12 +10,18 @@
|
|||||||
|
|
||||||
<channels>
|
<channels>
|
||||||
<channel id="output" typeId="output"/>
|
<channel id="output" typeId="output"/>
|
||||||
|
<channel id="stdout" typeId="stdout"/>
|
||||||
|
<channel id="stderr" typeId="stderr"/>
|
||||||
<channel id="input" typeId="input"/>
|
<channel id="input" typeId="input"/>
|
||||||
<channel id="exit" typeId="exit"/>
|
<channel id="exit" typeId="exit"/>
|
||||||
<channel id="run" typeId="run"/>
|
<channel id="run" typeId="run"/>
|
||||||
<channel id="lastexecution" typeId="lastexecution"/>
|
<channel id="lastexecution" typeId="lastexecution"/>
|
||||||
</channels>
|
</channels>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<property name="thingTypeVersion">1</property>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<config-description>
|
<config-description>
|
||||||
<parameter name="command" type="text" required="true">
|
<parameter name="command" type="text" required="true">
|
||||||
<label>Command</label>
|
<label>Command</label>
|
||||||
@@ -43,6 +49,12 @@
|
|||||||
<description>When true, the command will execute each time the state of the input channel changes</description>
|
<description>When true, the command will execute each time the state of the input channel changes</description>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</parameter>
|
</parameter>
|
||||||
|
<parameter name="charset" type="text" required="false">
|
||||||
|
<label>Character Encoding</label>
|
||||||
|
<description>The character encoding to use for process output. Any string valid for Java's Charset.forName() can be
|
||||||
|
used. If blank, UTF-8 will be used.</description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
</config-description>
|
</config-description>
|
||||||
|
|
||||||
</thing-type>
|
</thing-type>
|
||||||
@@ -53,6 +65,18 @@
|
|||||||
<description>Output of the last execution of the command</description>
|
<description>Output of the last execution of the command</description>
|
||||||
<state readOnly="true"></state>
|
<state readOnly="true"></state>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
<channel-type id="stdout">
|
||||||
|
<item-type>String</item-type>
|
||||||
|
<label>Standard Out</label>
|
||||||
|
<description>The stdout output of the last execution of the command</description>
|
||||||
|
<state readOnly="true"></state>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="stderr">
|
||||||
|
<item-type>String</item-type>
|
||||||
|
<label>Standard Error</label>
|
||||||
|
<description>The stderr output of the last execution of the command</description>
|
||||||
|
<state readOnly="true"></state>
|
||||||
|
</channel-type>
|
||||||
<channel-type id="input">
|
<channel-type id="input">
|
||||||
<item-type>String</item-type>
|
<item-type>String</item-type>
|
||||||
<label>Input</label>
|
<label>Input</label>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
|
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||||
|
|
||||||
|
<thing-type uid="exec:command">
|
||||||
|
<instruction-set targetVersion="1">
|
||||||
|
<add-channel id="stdout">
|
||||||
|
<type>exec:stdout</type>
|
||||||
|
<label>Standard Out</label>
|
||||||
|
</add-channel>
|
||||||
|
<add-channel id="stderr">
|
||||||
|
<type>exec:stderr</type>
|
||||||
|
<label>Standard Error</label>
|
||||||
|
</add-channel>
|
||||||
|
</instruction-set>
|
||||||
|
</thing-type>
|
||||||
|
</update:update-descriptions>
|
||||||
Reference in New Issue
Block a user