/* ================================================================== * HttpRequesterJob.java - Jul 20, 2013 5:58:39 PM * * Copyright 2007-2013 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== */ package net.solarnetwork.node.control.ping; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Scanner; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import org.quartz.DisallowConcurrentExecution; import org.quartz.JobExecutionContext; import org.quartz.PersistJobDataAfterExecution; import org.springframework.context.MessageSource; import org.springframework.context.support.ResourceBundleMessageSource; import net.solarnetwork.node.SSLService; import net.solarnetwork.node.job.AbstractJob; import net.solarnetwork.node.reactor.InstructionHandler; import net.solarnetwork.node.reactor.InstructionStatus.InstructionState; import net.solarnetwork.node.reactor.support.InstructionUtils; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; import net.solarnetwork.node.settings.support.BasicToggleSettingSpecifier; import net.solarnetwork.util.OptionalService; /** * Make a HTTP request to test for network connectivity, and toggle a control * value when connectivity lost. * * <p> * The idea behind this class is to test for network reachability of a * configured HTTP URL. If the URL cannot be reached, the configured control * will be set to {@code failedToggleValue}, followed by a pause, followed by * setting the control back to the opposite of {@code failedToggleValue}. The * control might cycle the power of a mobile modem, for example. * </p> * * <p> * Alternatively, or in addition to to, toggling a control two OS-specific * commands can be executed if the URL cannot be reached. The * {@code osCommandToggleOff} command will be executed when the URL fails, * followed by the configured pause, followed by the {@code osCommandToggleOn} * command. * </p> * * <p> * The configurable properties of this class are: * </p> * * <dl class="class-properties"> * <dt>controlId</dt> * <dd>The ID of the boolean control to toggle.</dd> * * <dt>failedToggleValue</dt> * <dd>The value to set the configured control to if the ping fails. The * opposite value will then be used to toggle the control back again.</dd> * * <dt>osCommandToggleOff</dt> * <dd>If configured, an OS-specific command to run after the URL cannot be * reached.</dd> * * <dt>osCommandToggleOn</dt> * <dd>If configured, an OS-specific command to run after the URL was not * reached and the configured pause time has elapsed.</dd> * * <dt>osCommandSleepSeconds</dt> * <dd>The number of seconds to sleep after successfully executing either the * {@code osCommandToggleOn} or {@code osCommandToggleOff} commands. Defaults to * <b>5</b></dd> * * <dt>sleepSeconds</dt> * <dd>The number of seconds to wait after toggling the control to * <em>false</em> before toggling the control back to <em>true</em>. Defaults to * <b>5</b>.</dd> * * <dt>connectionTimeoutSeconds</dt> * <dd>The number of seconds to wait for the network connection request to * return a result. Defaults to <b>15</b>.</dd> * * <dt>url</dt> * <dd>The URL to "ping". This must be a HTTP URL that accepts {@code HEAD} * requests. When this job executes, it will make a HTTP HEAD request to this * URL, and will be considered successful only if the HTTP response status code * is between <b>200 - 399</b>.</dd> * </dl> * * @author matt * @version 2.0 */ @PersistJobDataAfterExecution @DisallowConcurrentExecution public class HttpRequesterJob extends AbstractJob implements SettingSpecifierProvider { private static MessageSource MESSAGE_SOURCE; private String controlId; private String osCommandToggleOff; private String osCommandToggleOn; private int osCommandSleepSeconds = 5; private boolean failedToggleValue = true; private int sleepSeconds = 5; private int connectionTimeoutSeconds = 15; private String url = "http://www.google.com/"; private Collection<InstructionHandler> handlers = Collections.emptyList(); private OptionalService<SSLService> sslService; @Override protected void executeInternal(JobExecutionContext jobContext) throws Exception { if ( handlers == null ) { log.warn("No configured InstructionHandler collection"); return; } if ( controlId == null && osCommandToggleOff == null && osCommandToggleOn == null ) { log.debug("No control ID or OS commands configured."); return; } if ( ping() ) { log.info("Ping {} successful", url); } else { handleOSCommand(osCommandToggleOff); if ( controlId != null && toggleControl(failedToggleValue) == InstructionState.Completed ) { handleSleep(); toggleControl(!failedToggleValue); } else if ( osCommandToggleOn != null ) { handleSleep(); } handleOSCommand(osCommandToggleOn); } } private void handleOSCommand(String command) { if ( command == null ) { return; } ProcessBuilder pb = new ProcessBuilder(command.split("\\s+")); try { Process pr = pb.start(); logInputStream(pr.getInputStream(), false); logInputStream(pr.getErrorStream(), true); pr.waitFor(); if ( pr.exitValue() == 0 ) { log.debug("Command [{}] executed", command); handleCommandSleep(); } else { log.error("Error executing [{}], exit status: {}", command, pr.exitValue()); } } catch ( IOException e ) { throw new RuntimeException(e); } catch ( InterruptedException e ) { throw new RuntimeException(e); } } private void logInputStream(final InputStream src, final boolean errorStream) { new Thread(new Runnable() { @Override public void run() { Scanner sc = new Scanner(src); try { while ( sc.hasNextLine() ) { if ( errorStream ) { log.error(sc.nextLine()); } else { log.info(sc.nextLine()); } } } finally { sc.close(); } } }).start(); } private void handleSleep() { if ( sleepSeconds > 0 ) { log.info("Sleeping for {} seconds before toggling {} to true", sleepSeconds, controlId); try { Thread.sleep(sleepSeconds * 1000L); } catch ( InterruptedException e ) { log.warn("Interrupted while sleeping"); } } } private void handleCommandSleep() { if ( osCommandSleepSeconds > 0 ) { log.info("Sleeping for {} seconds before continuing", osCommandSleepSeconds, controlId); try { Thread.sleep(osCommandSleepSeconds * 1000L); } catch ( InterruptedException e ) { log.warn("Interrupted while sleeping"); } } } private boolean ping() { log.debug("Attempting to ping {}", url); try { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setConnectTimeout(connectionTimeoutSeconds * 1000); connection.setReadTimeout(connectionTimeoutSeconds * 1000); connection.setRequestMethod("HEAD"); connection.setInstanceFollowRedirects(false); if ( sslService != null && connection instanceof HttpsURLConnection ) { SSLService service = sslService.service(); if ( service != null ) { SSLSocketFactory factory = service.getSolarInSocketFactory(); if ( factory != null ) { HttpsURLConnection sslConnection = (HttpsURLConnection) connection; sslConnection.setSSLSocketFactory(factory); } } } int responseCode = connection.getResponseCode(); return (responseCode >= 200 && responseCode < 400); } catch ( IOException e ) { log.info("Error pinging {}: {}", url, e.getMessage()); return false; } } private InstructionState toggleControl(final boolean value) { InstructionState result = null; try { result = InstructionUtils.setControlParameter(handlers, controlId, String.valueOf(value)); } catch ( RuntimeException e ) { log.error("Exception setting control parameter {} to {}", controlId, value, e); } if ( result == null ) { // nobody handled it! result = InstructionState.Declined; log.warn("No InstructionHandler found for control {}", controlId); } else if ( result == InstructionState.Completed ) { log.info("Set {} value to {}", controlId, value); } else { log.warn("Unable to set {} to {}; result is {}", controlId, value, result); } return result; } // SettingSpecifierProvider @Override public String getSettingUID() { return "net.solarnetwork.node.control.ping.http"; } @Override public String getDisplayName() { return "HTTP Ping"; } @Override public List<SettingSpecifier> getSettingSpecifiers() { HttpRequesterJob defaults = new HttpRequesterJob(); List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(4); results.add(new BasicTextFieldSettingSpecifier("url", defaults.url)); results.add(new BasicTextFieldSettingSpecifier("controlId", defaults.controlId)); results.add(new BasicToggleSettingSpecifier("failedToggleValue", defaults.failedToggleValue)); results.add(new BasicTextFieldSettingSpecifier("connectionTimeoutSeconds", String.valueOf(defaults.connectionTimeoutSeconds))); results.add(new BasicTextFieldSettingSpecifier("sleepSeconds", String.valueOf(defaults.sleepSeconds))); results.add( new BasicTextFieldSettingSpecifier("osCommandToggleOff", defaults.osCommandToggleOff)); results.add(new BasicTextFieldSettingSpecifier("osCommandToggleOn", defaults.osCommandToggleOn)); results.add(new BasicTextFieldSettingSpecifier("osCommandSleepSeconds", String.valueOf(defaults.osCommandSleepSeconds))); return results; } @Override public MessageSource getMessageSource() { if ( MESSAGE_SOURCE == null ) { ResourceBundleMessageSource source = new ResourceBundleMessageSource(); source.setBundleClassLoader(getClass().getClassLoader()); source.setBasename(getClass().getName()); MESSAGE_SOURCE = source; } return MESSAGE_SOURCE; } public void setControlId(String value) { if ( value != null && value.length() < 1 ) { value = null; } this.controlId = value; } public void setSleepSeconds(int sleepSeconds) { this.sleepSeconds = sleepSeconds; } public void setHandlers(Collection<InstructionHandler> handlers) { this.handlers = handlers; } public void setUrl(String url) { this.url = url; } public void setConnectionTimeoutSeconds(int connectionTimeout) { this.connectionTimeoutSeconds = connectionTimeout; } public void setSslService(OptionalService<SSLService> sslService) { this.sslService = sslService; } public void setOsCommandToggleOff(String value) { if ( value != null && value.length() < 1 ) { value = null; } this.osCommandToggleOff = value; } public void setOsCommandToggleOn(String value) { if ( value != null && value.length() < 1 ) { value = null; } this.osCommandToggleOn = value; } public void setFailedToggleValue(boolean failedToggleValue) { this.failedToggleValue = failedToggleValue; } public void setOsCommandSleepSeconds(int osCommandSleepSeconds) { this.osCommandSleepSeconds = osCommandSleepSeconds; } }