Skip to content

Commit

Permalink
Fix blocking initialization (#17057)
Browse files Browse the repository at this point in the history
Resolves #16806

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
  • Loading branch information
jlaur committed Jul 15, 2024
1 parent 24eef7e commit 2f35a79
Showing 1 changed file with 103 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public class DenonMarantzHandler extends BaseThingHandler implements DenonMarant
private DenonMarantzConnectorFactory connectorFactory = new DenonMarantzConnectorFactory();
private DenonMarantzState denonMarantzState;
private @Nullable ScheduledFuture<?> retryJob;
private @Nullable ScheduledFuture<?> initJob;

public DenonMarantzHandler(Thing thing, HttpClient httpClient) {
super(thing);
Expand Down Expand Up @@ -211,120 +212,123 @@ private void autoConfigure() throws InterruptedException {
* When not set we will try to auto-detect the correct values
* for isTelnet and zoneCount and update the Thing accordingly.
*/
if (config.isTelnet() == null) {
logger.debug("Trying to auto-detect the connection.");
ContentResponse response;
boolean telnetEnable = true;
int httpPort = 80;
boolean httpApiUsable = false;

// try to reach the HTTP API at port 80 (most models, except Denon ...H should respond.
String host = config.getHost();
if (config.isTelnet() != null) {
return;
}
logger.debug("Trying to auto-detect the connection.");
ContentResponse response;
boolean telnetEnable = true;
int httpPort = 80;
boolean httpApiUsable = false;

// try to reach the HTTP API at port 80 (most models, except Denon ...H should respond.
String host = config.getHost();
try {
response = httpClient.newRequest("http://" + host + "/goform/Deviceinfo.xml").timeout(3, TimeUnit.SECONDS)
.send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
logger.debug("We can access the HTTP API, disabling the Telnet mode by default.");
telnetEnable = false;
httpApiUsable = true;
}
} catch (TimeoutException | ExecutionException e) {
logger.debug("Error when trying to access AVR using HTTP on port 80.", e);
}

if (telnetEnable) {
// the above attempt failed. Let's try on port 8080, as for some models a subset of the HTTP API is
// available
try {
response = httpClient.newRequest("http://" + host + "/goform/Deviceinfo.xml")
response = httpClient.newRequest("http://" + host + ":8080/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
logger.debug("We can access the HTTP API, disabling the Telnet mode by default.");
logger.debug("This model responds to HTTP port 8080, disabling the Telnet mode by default.");
telnetEnable = false;
httpPort = 8080;
httpApiUsable = true;
}
} catch (TimeoutException | ExecutionException e) {
logger.debug("Error when trying to access AVR using HTTP on port 80.", e);
logger.debug("Additionally tried to connect to port 8080, this also failed. Reverting to Telnet mode.",
e);
}
}

if (telnetEnable) {
// the above attempt failed. Let's try on port 8080, as for some models a subset of the HTTP API is
// available
try {
response = httpClient.newRequest("http://" + host + ":8080/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
logger.debug("This model responds to HTTP port 8080, disabling the Telnet mode by default.");
telnetEnable = false;
httpPort = 8080;
httpApiUsable = true;
}
} catch (TimeoutException | ExecutionException e) {
logger.debug(
"Additionally tried to connect to port 8080, this also failed. Reverting to Telnet mode.",
e);
}
}
// default zone count
int zoneCount = 2;

// default zone count
int zoneCount = 2;
// try to determine the zone count by checking the Deviceinfo.xml file
if (httpApiUsable) {
int status = 0;
response = null;
try {
response = httpClient.newRequest("http://" + host + ":" + httpPort + "/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send();
status = response.getStatus();
} catch (TimeoutException | ExecutionException e) {
logger.debug("Failed in fetching the Deviceinfo.xml to determine zone count", e);
}

// try to determine the zone count by checking the Deviceinfo.xml file
if (httpApiUsable) {
int status = 0;
response = null;
if (status == HttpURLConnection.HTTP_OK && response != null) {
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
try {
response = httpClient.newRequest("http://" + host + ":" + httpPort + "/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send();
status = response.getStatus();
} catch (TimeoutException | ExecutionException e) {
logger.debug("Failed in fetching the Deviceinfo.xml to determine zone count", e);
}

if (status == HttpURLConnection.HTTP_OK && response != null) {
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
try {
// see
// https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
domFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
domFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
domFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
domFactory.setXIncludeAware(false);
domFactory.setExpandEntityReferences(false);
DocumentBuilder builder;
builder = domFactory.newDocumentBuilder();
Document dDoc = builder.parse(new InputSource(new StringReader(response.getContentAsString())));
XPath xPath = XPathFactory.newInstance().newXPath();
Node node = (Node) xPath.evaluate("/Device_Info/DeviceZones/text()", dDoc, XPathConstants.NODE);
if (node != null) {
String nodeValue = node.getNodeValue();
logger.trace("/Device_Info/DeviceZones/text() = {}", nodeValue);
zoneCount = Integer.parseInt(nodeValue);
logger.debug("Discovered number of zones: {}", zoneCount);
}
} catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException
| NumberFormatException e) {
logger.debug("Something went wrong with looking up the zone count in Deviceinfo.xml: {}",
e.getMessage());
// see
// https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
domFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
domFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
domFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
domFactory.setXIncludeAware(false);
domFactory.setExpandEntityReferences(false);
DocumentBuilder builder;
builder = domFactory.newDocumentBuilder();
Document dDoc = builder.parse(new InputSource(new StringReader(response.getContentAsString())));
XPath xPath = XPathFactory.newInstance().newXPath();
Node node = (Node) xPath.evaluate("/Device_Info/DeviceZones/text()", dDoc, XPathConstants.NODE);
if (node != null) {
String nodeValue = node.getNodeValue();
logger.trace("/Device_Info/DeviceZones/text() = {}", nodeValue);
zoneCount = Integer.parseInt(nodeValue);
logger.debug("Discovered number of zones: {}", zoneCount);
}
} catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException
| NumberFormatException e) {
logger.debug("Something went wrong with looking up the zone count in Deviceinfo.xml: {}",
e.getMessage());
}
}
config.setTelnet(telnetEnable);
config.setZoneCount(zoneCount);
Configuration configuration = editConfiguration();
configuration.put(PARAMETER_TELNET_ENABLED, telnetEnable);
configuration.put(PARAMETER_ZONE_COUNT, zoneCount);
updateConfiguration(configuration);
}
config.setTelnet(telnetEnable);
config.setZoneCount(zoneCount);
Configuration configuration = editConfiguration();
configuration.put(PARAMETER_TELNET_ENABLED, telnetEnable);
configuration.put(PARAMETER_ZONE_COUNT, zoneCount);
updateConfiguration(configuration);
}

@Override
public void initialize() {
config = getConfigAs(DenonMarantzConfiguration.class);

// Configure Connection type (Telnet/HTTP) and number of zones
// Note: this only happens for discovered Things
try {
autoConfigure();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
updateStatus(ThingStatus.UNKNOWN);

if (!checkConfiguration()) {
return;
}
initJob = scheduler.schedule(() -> {
// Configure Connection type (Telnet/HTTP) and number of zones
// Note: this only happens for discovered Things
try {
autoConfigure();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}

configureZoneChannels();
updateStatus(ThingStatus.UNKNOWN);
// create connection (either Telnet or HTTP)
// ThingStatus ONLINE/OFFLINE is set when AVR status is known.
createConnection();
if (!checkConfiguration()) {
return;
}

configureZoneChannels();
// create connection (either Telnet or HTTP)
// ThingStatus ONLINE/OFFLINE is set when AVR status is known.
createConnection();
}, 0, TimeUnit.SECONDS);
}

private void createConnection() {
Expand All @@ -337,6 +341,14 @@ private void createConnection() {
connector.connect();
}

private void cancelInitJob() {
ScheduledFuture<?> initJob = this.initJob;
if (initJob != null) {
initJob.cancel(true);
}
this.initJob = null;
}

private void cancelRetry() {
ScheduledFuture<?> retryJob = this.retryJob;
if (retryJob != null) {
Expand Down Expand Up @@ -430,6 +442,7 @@ public void dispose() {
}
this.connector = null;
cancelRetry();
cancelInitJob();
super.dispose();
}

Expand Down

0 comments on commit 2f35a79

Please sign in to comment.