/****************************************************************************** * Product: Adempiere ERP & CRM Smart Business Solution * * Copyright (C) 1999-2007 ComPiere, Inc. All Rights Reserved. * * This program is free software, you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. 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. * * For the text or an alternative of this public license, you may reach us * * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * * or via info@compiere.org or http://www.compiere.org/license.html * *****************************************************************************/ package ar.com.ergio.print.fiscal.comm; import java.io.IOException; import java.net.ConnectException; import java.net.Socket; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.logging.Level; import org.compiere.util.CLogger; import ar.com.ergio.print.fiscal.FiscalPacket; import ar.com.ergio.print.fiscal.msg.MsgRepository; import ar.com.ergio.print.fiscal.util.ArrayUtils; /** * Interfaz de comunicación mediante un Spooler conectado por TCP. * <br> * NOTAS Modificaciones Ader Javier: <br> * <ul> * <li>Version original 10.03: Se tenia 5 timeous de 200 mls para considerar que la respuesta * del spooler habia terminado. Esto da un overhead de 1 segundo por comando enviado independientemente * del cuanto tarde realmente la ejecución comando y la recepción de la respuesta desde el Spooler. En * general esa espera es innecesaria, ya que la respuesta (o un DC2) llega rapidamente. * <br> * <li>Optimización I (nov 2010): Se modifico el bucle de espera para que en caso de baber recibido una secuencia * de bytes de longitud minima 9 (todas las respuestas del Spooler, salvo DC2, y DC4 tiene al menos * 9 bytes) se pase a una espera de 2 timeouts de 200mls (ver variable readTimeOutCountAfterFirstRealByte). * Con esto se logra una mejora de 600mls, sin sacrificar visiblemente la correctitud: es muy poco probable, pero puede * darse el caso de la respuesta completa llegue en dos partes, y la segunda a mas de 400mls que la primera * y por lo tanto se "corte antes". Es poco probable las respuestas son pequeñas (menos de 512 bytes siempre; * en genral menores a 30 bytes) y el spooler hasar "flushea" el buffer de escritura cada vez * que emite una respuesta; bajo estas condiciones,para que la respuesta llegue en partes, * la capa TCP o IP deberia fragmentar la respuesta y ninguna capa de TPC/IP con minimo sentido común * hace esta fragmentacion. * Basicamente, una vez que llega el primer byte de un respuesta (sin contar a DC2) es porque * llego completa desde la red, el proximo timeout de lectura casi necesariamente marca el final * real de la respuesta (de todas maneras se espera que esto ocurra 2 veces). * <br> De cualquier manera, esta optimizacion estuvo ampliamente testeado, incluso bajo producción * durante varios meses. * <br> * Junto con esta optimización se ha hecho otra modificación que hace que la implementacion actual * sea incluso más conservativa que la anterior: por el primer byte de un respuesta real (no DC2) * se espera dos rondas de 5 timeouts de 200mls (esto es, 2 segundos, frente a 1 segundo en la version * anterior). Esto es para tener en cuenta posibles roundtrips de red largos o un red con mucha carga (ver * variable retriesWaitFirstByte). Esta mejora (variando el valor de la variable retriesWaitFirstByte) * permiría lograr accesos realmente remotos o lentos, de manera confiable. * <br> * A manera de conclusión: siempre tratando de manera especial los casos de recepcion de DC2 * ("espere, impresor procesando comando") se espera de más si el primer byte tarda en arribar; * pero una vez que arriba se espera menos. En la práctica, el primer byte arriba rapido, y una * vez que llego, es porque llego la respuesta completa; esto es, salvo ciertos comandos que * son lentos a nivel de controlador fiscal (cierre X por ej) basicamente todos los respuestas * se esperan 400 mls (vs 1 segundo anteior).<br> * <b>NOTA Adicional </b>: ademas de esta modificación, se agrego el "flush" sobre el socket * despues escribir el comando; esto para evitar que Java retarde la emision real del comando * y que potencialmente el bucle de espera de respuesta posterior falle (simplemente, se va * a esperar por el simple hecho de que la respuesta no llego al spooler, porque Java retardo * su emisión....). * De todas maneras esta no es la solución completamente real a este escenario, porque el caching * de la emisión se puede dar por culpa de la stack TCP/IP del SO (hay formas de obligar * a que esto tampoco ocurra, y realmente asegurarse que se ha trasmitido el comando * a nivel de TCP, pero es un hack medio dudoso ya que afecta a todo el entorno de Java, no solo * a esta conexión en particular; este "evitar" de caching puede ralentar otras partes * del sistema, en particular el acceso a la base de datos...). * * * * <br> * <li>Optimizacion II (feb 2011): Saleo completo de Timeouts para la respuesta de ciertos comandos. * <br> * La razón de esta optimización es que las respuestas de muchos comandos son necesariamente * de longitud fija EN TODOS los modelos; una vez leida esta longitud en bytes, es seguro suponer * que la respuesta llego completamente. Bajo estas circunstancias, se corta el bucle de lectura * incluso sin esperar por timeouts (esto es, incluso se evitan los 400mls que logra la optimizacion I). * <br> * Actualmente solo se trata el caso de PrintLineItem, el cual es el más usado en la impresión * de facturas y NC(una vez por cada item de la misma). Hay otros comandos con respuesta fija, * que serian facilmente chequeables de la misma manera (por ej PrintFiscalText, LastItemDiscount, * SetCustomerData), pero bajo la versió 10.03 estos se usan poco (no son una función de la cantidad * de items) o directente no se usan (OBS: bajo 10.09, LastItemDiscount creo que se usa, y hay * pontencialmente uno por item de factura; si este es el caso, debería hacerse lo mismo * que con PrintLineItem; creo que ademas se utilizan documentos no fiscales, cuyas lineas * se manejan usando PrintNonFiscalText, el cual como los anteriores, tiene respueta fija). * <br> * Ver SpoolerManagerResponse. * </ul> * TODO: manejar respuesta DC4 (en la actualidad no es muy necesario... ningún modelo nuevo * muestra este aviso de "tapa abierta" simplemente porque no tienen tapa; ademas esto * es responsabilidad del usuario y antes esta situcaion Liberyta simplemente aborta * porque no puede interpretar la respuesta). * * * * @author Franco Bonafine * @date 04/02/2008 */ public class SpoolerTCPComm extends AbstractFiscalComm { /** Puerto TCP donde se encuentra escuchando el Spooler */ private int tcpPort; /** HOST en donde se encuentra el Spooler */ private String host; /** Socket de conexión al Spooler */ private Socket spoolerSocket; /** Cantidad de timeOuts a esperar para leer un byte del stream */ private int readTimeOutCount = 5; /** * @param host Host donde se encuentra el Spooler. * @param tcpPort Puerto TCP donde se encuentra abierto el Spooler. */ public SpoolerTCPComm(String host, int tcpPort) { super(); this.tcpPort = tcpPort; this.host = host; } public void connect() throws IOException { try { Socket soc = new Socket(getHost(), getTcpPort()); soc.setSoTimeout(200); setSpoolerSocket(soc); setInputStream(soc.getInputStream()); setOutputStream(soc.getOutputStream()); setConnected(true); } catch (UnknownHostException e) { setConnected(false); throw new IOException(MsgRepository.get("UnknownHostError") + " (" + getHost() + ")"); } catch (ConnectException e) { throw new ConnectException(MsgRepository.get("SpoolerConnectError") + " (Host: " + getHost() + ":" + getTcpPort() + ")."); } } public void close() throws IOException { super.close(); if(isConnected()) { getSpoolerSocket().close(); } } public synchronized void execute(FiscalPacket request, FiscalPacket response) throws IOException { if(request == response) throw new IllegalArgumentException(); if(request == null) throw new NullPointerException(MsgRepository.get("NullRequestError")); if(response == null) throw new NullPointerException(MsgRepository.get("NullResponseError")); // Se valida el estado de la conexión con el spooler. validateConnection(); // Se obtiene la representación en bytes del comando y se escribe // sobre stream de salida. byte[] cmdBytes = request.encodeBytes(); getOutputStream().write(cmdBytes); //Ader Javier: 10 oct 2010 se debe poner el flush luego de //escribir; si no puede que despues fallen los timeouts de lectura getOutputStream().flush(); debug("REQ: " + request.toString()); // Se obtiene la respuesta de la impresora a partir del stream de entrada // del socket y se decodifican los bytes para crear el paquete de respuesta. byte[] resBytes = new byte[512]; resBytes = getResponse(request); response.decode(request.getCommandCode(), resBytes); debug("RES: " + response.toString()); } //Ader Javier, feb 2011; se agrego el parametro request para que se puedan aplicar //optimizaciones de salteo completos de timeouts para ciertos comandos y ante ciertas //respuesta. private byte[] getResponse(FiscalPacket request) throws IOException { // Se obtienen los bytes a partir del stream de entrada. // NOTA: El WSpooler tiene una particularidad muy desagradable // de realizar respuestas de comandos sin indicar el fin de la // respuesta. Por lo tanto no existe ningún caracter de control // para determinar el fin de una respuesta, ni tampoco es posible // determinar la cantidad de bytes que componen a la misma, por // lo tanto, se optó por implementar un contador de timeOuts para // determinar este fin. byte[] rspBytes = new byte[0]; byte b; // boolean end = false; SE USA while(true) y breaks int timeOutCount = 0; int DC2Counter = 0; boolean lastReadWasDC2 = false; // TODO: que sea parametrizable y documetado // si expiran todos los timeous y aun no se recibio ni un solo byte // se reintenta "volver a esperar" esta cantidad de veces int retriesWaitFirstByte = 1; // TODO: que sea parametrizable y documentado // Depues de una succion de DC2DC2DC2...DC2 puede que expiren todos los timeouts // en la espera del primer byte de la respueta o del primer byte de otro DC2... // Bajo esta circunstancia se vuelve a esperar una vez la siguiente cantidad de // veces int retriesWaitFirstRealByteAfterTimeoutInDC2s = 5; //serian boolean alwaysWaitFirstRealByteAfterDC2 = false; //si esto es true SIEMPRE // se espera luego de haber consumido un almeno un DC2, y el contadora anterior no tiene // efecto int readTimeOutCountAfterFirstRealByte = 2; //despues de que ya se lleyo parte // de la respuesta real, cuanto tiempo hay que esperar? Cuantos timeouts pueden expirar // sin haber recibido nuevamente nada? (por defecto 2 de 200mls; lo cual da 400mls). // Esto es para acelerar las cosas; una vez que uno recibe parte de la repuseta real (esto es NO "DC2"), // es muy poco probable que el timeout "corte" la respuesta. Esta optimizacion // solo se usa cuando la repusta actual tieen una longitud mayor o igaul a 9 (el spooler // retonrna en respuesta reales al menos 9 bytes (dos conjuntos de 4 bytes de status // y un byte seperardor) // El proceso de lectura termina cuando... documentar: acutalmente no es tan simple // como simplemente terminar hasta que cuando todos los timeoust expiran... while(true) { try { int res = getInputStream().read(); if (res == -1) { // endOfStream! No necesariamene un error aunque es muy probable... debug("WARNING: sokcet cerrado a la espera de respuesta. Long. Resp. parcial: " + rspBytes.length); break; } b = (byte)res; rspBytes = ArrayUtils.append(rspBytes, b); timeOutCount = 0; // Ader Javier : 10 oct 2010 , manejo de DC2 if (startWithDC2(rspBytes)) { //DC2 : se debe consumir y setear lastReadWasDC2 rspBytes = ArrayUtils.removePrefix(rspBytes, 3); DC2Counter++; debug("DC2 : " + DC2Counter); lastReadWasDC2 = true; continue; // Optimizacion II: solo se agrego este "else if" } else if (isResponseCompleted(request,rspBytes)) { debug("Dejando de esperar respuesta por optimización II: Salteo completo de timeots"); break; // Se sale pero no porque por que haya ocurrido un timeout } // Si se llega a aca se tiene en rspBytes secuncioa "D", "DC", // o otra secuencia de mas de un byte la cual no se puede saber por isResponseCompleted // que represente una respuesta completa; se tiene que volver para leer los siguientes // bytes o para que ImputStream.read() corte por timeout de 200 mls (si esto // ultimo ocurre la logica sigue en el catch) lastReadWasDC2 = false; continue; } catch(SocketTimeoutException e) { timeOutCount++; // No se recibio ni un solo byte de la respueta, ni siquiera un DC2 if (rspBytes.length == 0 && !lastReadWasDC2) { // la unica forma de que esto pase es que se haya enviado la solicitud // y no se haya respondido ni un solo byte if (timeOutCount < getReadTimeOutCount()) continue; //caso comun a la espera del primer byte // else; expiraron todos los timeouts y no se recibio ni un solo byte! if (retriesWaitFirstByte >0) { // se hace todo una ronda nueva retriesWaitFirstByte--; debug("Reitentando al espera del primer byte"); timeOutCount =0; continue; } else { // NECESARIAMENTE UN ERROR! debug("ERROR: expiraron todos los timeouts y no se recibio ni un solo byte de respeusta"); break; //se sale y rspBytes necesariamente vacio.... } } else if (rspBytes.length == 0 ) { // En este punto lastReadWasDC2 = true; esto es se han leido // una sucesión de DC2DC2DC2...DC2 y se expiro el timeout if (timeOutCount < getReadTimeOutCount()) { continue; // Se sigue esperando } // Si se llega aca todos los timeouts expiraron pero lo ulmito // que se recibio fue un DC2; conceptualmente uno debería esperar // idefinidamente bajo esta situacion; igual por las dudas // se pone esta condicion if (retriesWaitFirstRealByteAfterTimeoutInDC2s > 0 || alwaysWaitFirstRealByteAfterDC2) { retriesWaitFirstRealByteAfterTimeoutInDC2s--; debug("WARNING: volviendo a esperar primer byte real de repuesta luego de DC2's"); timeOutCount =0; continue; } // Si se llega aca: se recibieron muchos DC2's pero se espero // mas retriesWaitFirstRealByteAfterTimeoutInDC2s tandas de timeouts... debug("ERROR: cancelando espera de repuesta en DC2; demasiada espera"); break; } // OK Si se llega a ca rspBytes.length > 0; y casi necesariamete contiene // toda la repuesta. Es poco probable que "se corte la repuesta" si // se hace en este punto una optimizacion.... if (rspBytes.length >= 9 && timeOutCount >= readTimeOutCountAfterFirstRealByte) { // OTPIMIZACION I en tiempo de procesamiento. En realidad este es el caso // común. ESTE ES EL PUNTO DE SALIDA MAS COMUN para todos los comandos // no manejados por la Optimizacion II debug("Dejando de esperar respueta por optimización"); break; } if (timeOutCount >= getReadTimeOutCount()) { debug("WARNING: Dejando de esperar repuesta antes de recibir al menos 9 bytes"); break; } // else {continue} // end = timeOutCount == getReadTimeOutCount(); debug("Timeout! = " + timeOutCount + " partial. length:" + rspBytes.length + " CT(mls):" + System.currentTimeMillis()); } } // Este metodo no tenia sentido y hacia una conversion // a string muy dudusa. Los bytes DC2 se eleminan en el bucle de lectura; SIEMPRE (ver // codido del Spooler Hasar) en sucesión antes de que la respuesta. // rspBytes = cleanResponseBytes(rspBytes); return rspBytes; } private static final byte byteD = 0x44; private static final byte byteC = 0x43; private static final byte byte2 = 0x32; // private static final byte byte4 = 0x34; //NO usado actulemtne; serviria para chequear respuesta "DC4" (cajon abierto) // Ader Javier: 10 Oct 2010 : esta es la forma correcta de // chequear los bytes DC2 private boolean startWithDC2(byte[] res) { if (res.length < 3) return false; if (res[0]== byteD && res[1] == byteC && res[2]==byte2) return true; return false; } private boolean isResponseCompleted(FiscalPacket request, byte[] rspBytes) { // se delega a SpoolerManagerResponse return SpoolerManagerResponse.getDefInst().isResponseCompleted(request, rspBytes); //return false; descomentar esta linea, y comaentar la anterior para comparar //la performance con y sin Optimizacion II. } //cambiar acordemente; este es solo para debugguear protected static boolean debugInStdOut = false; protected static boolean debugInLogger = true; protected static CLogger log = CLogger.getCLogger(SpoolerTCPComm.class); private static void debug(String text) { String textDebug = "==> DEBUG = " + text; if (debugInStdOut) System.out.println(textDebug); if (debugInLogger) log.log(Level.INFO,textDebug); } /** * @return Returns the host. */ public String getHost() { return host; } /** * @param host The host to set. */ public void setHost(String host) { this.host = host; } /** * @return Returns the tcpPort. */ public int getTcpPort() { return tcpPort; } /** * @param tcpPort The tcpPort to set. */ public void setTcpPort(int tcpPort) { this.tcpPort = tcpPort; } /** * @return Returns the spoolerSocket. */ protected Socket getSpoolerSocket() { return spoolerSocket; } /** * @param spoolerSocket The spoolerSocket to set. */ protected void setSpoolerSocket(Socket spoolerSocket) { this.spoolerSocket = spoolerSocket; } /** * @return Returns the readTimeOutCount. */ public int getReadTimeOutCount() { return readTimeOutCount; } /** * @param readTimeOutCount The readTimeOutCount to set. */ public void setReadTimeOutCount(int readTimeOutCount) { this.readTimeOutCount = readTimeOutCount; } }