decodes.polling.DigiConnectPortPool Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opendcs Show documentation
Show all versions of opendcs Show documentation
A collection of software for aggregatting and processing environmental data such as from NOAA GOES satellites.
The newest version!
/*
* $Id$
*
* This software was written by Cove Software, LLC ("COVE") under contract
* to Alberta Environment and Sustainable Resource Development (Alberta ESRD).
* No warranty is provided or implied other than specific contractual terms
* between COVE and Alberta ESRD.
*
* Copyright 2014 Alberta Environment and Sustainable Resource Development.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package decodes.polling;
import ilex.net.BasicClient;
import ilex.util.Logger;
import ilex.util.PropertiesUtil;
import java.io.IOException;
import java.net.InetAddress;
import java.rmi.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Properties;
import java.util.StringTokenizer;
import opendcs.dai.DeviceStatusDAI;
import decodes.db.Database;
import decodes.db.DatabaseIO;
import decodes.db.TransportMedium;
import decodes.sql.SqlDatabaseIO;
import decodes.tsdb.DbIoException;
public class DigiConnectPortPool
extends PortPool
{
public static final String module = "DigiConnectPortPool";
private String digiIpAddr = null;
public int digiPortBase = 2100;
private String digiUserName = null;
private String digiPassword = null;
private String processName = null;
private String hostname = null;
private DigiConnectPortManager portManager = null;
// A port with last activity time more than 5 min is considered idle even if it's flagged as in use.
public static final long PORT_STALE_MS = 300000L;
/**
* the names match PORT_NAME column in the SERIAL_PORT_STATUS table.
* Format is :portNum.
*/
private ArrayList portNames = new ArrayList();
ArrayList allocatedPorts = new ArrayList();
/** The index of the next port to try, so we can use them round-robin. */
private int nextPortIdx = 0;
private SqlDatabaseIO sqldbio = null;
class PortStats
{
long dontUseUntil = 0L;
int consecutiveErrors = 0;
};
HashMap portStats = new HashMap();
// Once a BC is created it is reused rather than recreating.
// This ensures that there are no orphan sockets due to abnormal termination.
private HashMap name2bc = new HashMap();
public DigiConnectPortPool()
{
super(module);
portManager = new DigiConnectPortManager(this);
}
@Override
public void configPool(Properties dataSourceProps)
throws ConfigException
{
// Parse property digiIpAddr
digiIpAddr = PropertiesUtil.getIgnoreCase(dataSourceProps, "digiIpAddr");
if (digiIpAddr == null || digiIpAddr.trim().length() == 0)
throw new ConfigException("Missing required property 'digiIpAddr'. Should be "
+ "set to hostname or IP Address of the Digi ConnectPort device.");
// Parse property digiPortBase
String s = PropertiesUtil.getIgnoreCase(dataSourceProps, "digiPortBase");
if (s != null && s.trim().length() > 0)
{
try { digiPortBase = Integer.parseInt(s); }
catch(NumberFormatException ex)
{
throw new ConfigException("Invalid digiPortBase property '" + s + "' -- expected integer.");
}
}
// Parse the comma-separated list of port numbers and ranges
// e.g. "1-4,7,9-13". Fill in the portNames array.
s = PropertiesUtil.getIgnoreCase(dataSourceProps, "availablePorts");
portNames.clear();
if (s != null && s.trim().length() > 0)
{
try
{
StringTokenizer st = new StringTokenizer(s, ", ");
while(st.hasMoreTokens())
{
String t = st.nextToken();
int hyphen = t.indexOf('-');
Logger.instance().debug1(module + " token '" + t + "' hyphen=" + hyphen);
if (hyphen == -1)
{
int portnum = Integer.parseInt(t);
String n = digiIpAddr + ":" + (portnum<10?"0":"") + portnum;
Logger.instance().debug1(module + " adding " + n);
portNames.add(n);
}
else
{
int start = Integer.parseInt(t.substring(0, hyphen));
int end = Integer.parseInt(t.substring(hyphen+1));
if (start > end)
throw new ConfigException("Invalid range '" + t + "' in availablePorts property '"
+ s + "'");
while(start <= end)
{
int portnum = start++;
String n = digiIpAddr + ":" + (portnum<10?"0":"") + portnum;
Logger.instance().debug1(module + " adding " + n);
portNames.add(n);
}
}
}
}
catch(ConfigException ex)
{
throw ex;
}
catch(Exception ex)
{
throw new ConfigException("Invalid availablePorts property '" + s
+ "'. Must be comma separated list of ports, or port ranges. Example: "
+ "1-5,7,9-12");
}
}
if (portNames.size() == 0)
throw new ConfigException("No availablePorts specified");
Logger.instance().info(module + " initialized with " + portNames.size() + " ports. Ports are: "
+ PropertiesUtil.getIgnoreCase(dataSourceProps, "availablePorts"));
// Parse properties for digiUserName and digiPassword
digiUserName = PropertiesUtil.getIgnoreCase(dataSourceProps, "digiUserName");
if (digiUserName == null || digiUserName.trim().length() == 0)
throw new ConfigException("Missing required property 'digiUserName'. Should be "
+ "the username for telnetting into the control port 23 of the Digi ConnectPort device.");
digiPassword = PropertiesUtil.getIgnoreCase(dataSourceProps, "digiPassword");
if (digiPassword == null || digiPassword.trim().length() == 0)
throw new ConfigException("Missing required property 'digiPassword'. Should be "
+ "the password for telnetting into the control port 23 of the Digi ConnectPort device.");
processName = PropertiesUtil.getIgnoreCase(dataSourceProps, "RoutingSpecName");
if (processName == null)
{
Logger.instance().warning(module + " no 'RoutingSpecName' property available, will use process name '"
+ module + "'");
processName = module;
}
try { hostname = InetAddress.getLocalHost().getHostName(); }
catch(Exception ex)
{
Logger.instance().warning(module + " Cannot resolve hostname. Will use 'localhost'");
hostname = "localhost";
}
// Get access to the database for DeviceStatus records.
DatabaseIO dbio = Database.getDb().getDbIo();
if (!(dbio instanceof SqlDatabaseIO))
throw new ConfigException("The DECODES Database is not a SQL Database. Cannot use Digi!");
sqldbio = (SqlDatabaseIO)dbio;
portManager.start();
}
@Override
public synchronized IOPort allocatePort()
{
Logger.instance().debug3(module + " allocatePort starting.");
// Use SERIAL_PORT_STATUS table to find a free port
DeviceStatusDAI deviceStatusDAO = sqldbio.makeDeviceStatusDAO();
IOPort ret = null;
try
{
if (nextPortIdx >= portNames.size())
nextPortIdx = 0;
int startPortIdx = nextPortIdx;
while(ret == null)
{
String portName = portNames.get(nextPortIdx);
PortStats ps = portStats.get(portName);
Logger.instance().debug3(module + " trying port " + portName);
DeviceStatus devStat = deviceStatusDAO.getDeviceStatus(portName);
if (devStat == null) // first time this device used?
devStat = new DeviceStatus(portName);
long now = System.currentTimeMillis();
if ((!devStat.isInUse()
|| devStat.getLastActivityTime() == null
|| now - devStat.getLastActivityTime().getTime() > PORT_STALE_MS)
&& (ps == null || now > ps.dontUseUntil))
{
Logger.instance().debug2(module + " Port " + portName
+ " is free. Will attempt to allocate.");
// Set its IN_USE, LAST_USED_BY_PROC, and LAST_USED_BY_HOST
devStat.setInUse(true);
devStat.setLastUsedByProc(processName);
devStat.setLastUsedByHost(hostname);
devStat.setLastActivityTime(new Date());
devStat.setPortStatus("Allocated");
deviceStatusDAO.writeDeviceStatus(devStat);
// Wait 2 seconds and then check to see that last used proc/host are still me.
try { Thread.sleep(2000L); } catch(InterruptedException ex) {}
devStat = deviceStatusDAO.getDeviceStatus(portName);
Logger.instance().debug3(module + " After 2 sec delay, " + portName
+ " is still mine. Will use.");
if (!devStat.getLastUsedByHost().equals(hostname)
|| !devStat.getLastUsedByProc().equals(processName))
{
Logger.instance().info(module + " Port '" + portName + " stolen by process '"
+ devStat.getLastUsedByProc() + "' host '" + devStat.getLastUsedByHost()
+ "' -- will keep trying.");
}
else // If so, I have the port! Open socket, create IOPort and set ret.
{
int colon = portName.indexOf(':');
int portnum = Integer.parseInt(portName.substring(colon+1));
BasicClient bc = getBasicClient(digiIpAddr, digiPortBase + portnum);
// MJM Note: I determined experimentally that configuring the digi baudrate, stopbits, etc.,
// will cause DTR to the modem to drop. This tells the modem to hangup and become unusable.
// Therefore, we defer opening the socket until the configPort() method is called below.
// We open the socket to the port AFTER all the settings are done.
ret = new IOPort(this, portnum, new ModemDialer());
ret.setPortName(portName);
allocatedPorts.add(new AllocatedSerialPort(ret, devStat, bc));
}
}
else // Else this is already in use
{
Logger.instance().debug3(module + " Port " + portName
+ " was already in use, last activity time=" + devStat.getLastActivityTimeStr());
}
if (++nextPortIdx >= portNames.size())
nextPortIdx = 0;
if (ret == null && nextPortIdx == startPortIdx)
{
// Already tried all the ports
Logger.instance().debug3(module + " No ports currently available.");
break; // stop trying, will return null.
}
}
}
catch (DbIoException ex)
{
Logger.instance().failure(module + " Error reading/writing device statuses: " + ex);
}
finally
{
deviceStatusDAO.close();
}
if (ret == null)
Logger.instance().debug3(module + " failed. No ports available.");
else
Logger.instance().debug3(module + " success. returning port " + ret.getPortNum());
return ret;
}
@Override
public synchronized void releasePort(IOPort ioPort, PollingThreadState finalState,
boolean wasConnectException)
{
Logger.instance().debug1(module + " releasePort starting for port " + ioPort.getPortName());
// Close the socket and remove it from my allocatedPorts
AllocatedSerialPort allocatedPort = null;
for (AllocatedSerialPort ap : allocatedPorts)
if (ap.ioPort == ioPort)
{
allocatedPort = ap;
break;
}
if (allocatedPort == null)
return;
allocatedPorts.remove(allocatedPort);
PortStats ps = portStats.get(ioPort.getPortName());
if (ps == null)
portStats.put(ioPort.getPortName(), ps = new PortStats());
Logger.instance().debug1(module + " disconnecting basic client from: "
+ allocatedPort.basicClient.getName());
allocatedPort.basicClient.disconnect();
ioPort.setIn(null);
ioPort.setOut(null);
if (wasConnectException)
ps.consecutiveErrors++;
else
ps.consecutiveErrors = 0;
if (ps.consecutiveErrors >= 3)
{
Logger.instance().warning(module + " Port " + ioPort.getPortNum()
+ " has had 3 consecutive connect errors. It will be disabled for two minutes.");
ps.dontUseUntil = System.currentTimeMillis() + 120000L;
ps.consecutiveErrors = 0;
}
else
{
// Give it a 5 second rest to ensure that modem notices DTR low.
// This also ensures that an idle modem will be used, if one is available.
ps.dontUseUntil = System.currentTimeMillis() + 5000L;
}
// Free the port in SERIAL_PORT_STATUS
DeviceStatusDAI deviceStatusDAO = sqldbio.makeDeviceStatusDAO();
// Finalize and set the deviceStatus to no In Use
try
{
Date now = new Date();
allocatedPort.deviceStatus.setLastActivityTime(now);
if (finalState == PollingThreadState.Success)
{
allocatedPort.deviceStatus.setLastReceiveTime(now);
allocatedPort.deviceStatus.setLastMediumId(allocatedPort.transportMedium.getMediumId());
}
else
allocatedPort.deviceStatus.setLastErrorTime(now);
allocatedPort.deviceStatus.setInUse(false);
allocatedPort.deviceStatus.setPortStatus("");
deviceStatusDAO.writeDeviceStatus(allocatedPort.deviceStatus);
}
catch (DbIoException ex)
{
Logger.instance().failure(module + " Cannot write deviceStatus: " + ex);
}
finally
{
deviceStatusDAO.close();
}
Logger.instance().debug3(module + " releasePort returning.");
}
@Override
public int getNumPorts()
{
return portNames.size();
}
@Override
public int getNumFreePorts()
{
DeviceStatusDAI deviceStatusDAO = sqldbio.makeDeviceStatusDAO();
try
{
int n = 0;
for(DeviceStatus devstat : deviceStatusDAO.listDeviceStatuses())
{
if (portNames.contains(devstat.getPortName()))
n++;
}
return n;
}
catch (DbIoException ex)
{
Logger.instance().failure(module + " Cannot list deviceStatus: " + ex);
}
finally
{
deviceStatusDAO.close();
}
return 0;
}
@Override
public void configPort(IOPort ioPort, TransportMedium tm) throws DialException
{
// Find the matching allocatedPort object in my list
AllocatedSerialPort allocatedPort = null;
for (AllocatedSerialPort ap : allocatedPorts)
if (ap.ioPort == ioPort)
{
allocatedPort = ap;
break;
}
if (allocatedPort == null)
{
Logger.instance().warning(module + " asked to config port for " + tm.toString()
+ ", but port was not allocated by this object!");
return;
}
allocatedPort.transportMedium = tm;
// Port Manager will use info in tm to set serial port parameters.
portManager.configPort(allocatedPort);
if (allocatedPort.ioPort.getConfigureState() != PollingThreadState.Success)
throw new DialException("Failed to configure serial port.", false);
// I'm seeing broken pipe on sending init string. Try waiting two seconds after
// configuring before actually opening the socket to the port.
try { Thread.sleep(2000L); } catch(InterruptedException ex) {}
Logger.instance().debug1(module + " Connecting to " + allocatedPort.basicClient.getHost()
+ ":" + allocatedPort.basicClient.getPort());
try
{
allocatedPort.basicClient.connect();
ioPort.setIn(allocatedPort.basicClient.getInputStream());
ioPort.setOut(allocatedPort.basicClient.getOutputStream());
}
catch (IOException ex)
{
allocatedPort.basicClient.disconnect();
throw new DialException("Cannot connect to port " + ioPort.getPortName()
+ ", host:tcpPort=" + allocatedPort.basicClient.getName()
+ ": " + ex, true);
}
}
/**
* Manage the basic clients & return the requested one. Create the object
* if one has not already been created for this port.
* @param host the host name of the digi device
* @param port The actual TCP listening socket port number (e.g. 2101)
* @return
*/
private BasicClient getBasicClient(String host, int port)
{
String name = host + ":" + port;
BasicClient bc = name2bc.get(name);
if (bc == null)
name2bc.put(name, bc = new BasicClient(digiIpAddr, port));
return bc;
}
@Override
public void close()
{
Logger.instance().info(module + " there are " + allocatedPorts.size() + " still allocated.");
// It's up to each PollingThread to close it's own port.
// So if anything is left, it means the controller is shutting down prematurely.
// So close anything still open.
try
{
while(allocatedPorts.size() > 0)
releasePort(allocatedPorts.get(0).ioPort, PollingThreadState.Failed, false);
}
catch(Exception ex)
{
Logger.instance().warning(module + ".close() - unexpected erro releasing ports: " + ex);
}
if (portManager != null)
portManager.shutdown();
portManager = null;
int closed = 0;
for(BasicClient bc : name2bc.values())
if (bc.isConnected())
{
bc.disconnect();
closed++;
}
name2bc.clear();
if (closed > 0)
Logger.instance().warning(module + " " + closed
+ " ports were left open when close() was called.");
}
@Override
public void finalize()
{
close();
}
public String getDigiIpAddr()
{
return digiIpAddr;
}
public String getDigiUserName()
{
return digiUserName;
}
public String getDigiPassword()
{
return digiPassword;
}
}