lunes, 21 de julio de 2014

VBscript: ejecutar comandos limitando su duración máxima

Cuando utilizamos VBscript para ejecutar comandos de MS-DOS estos se ejecutan en un proceso aparte diferente del proceso del script original. Si el comando ejecutado no termina correctamente es muy probable que su proceso permanezca abierto indefinidamente.

En el caso de scripts de monitorización, que se ejecutan a intervalos regulares, es necesario controlar estos procesos secundarios para así evitar que multiples procesos secundarios no terminen y queden abiertos en el sistema. Este control podemos hacerlo desde el propio script.

El código siguiente es un ejemplo de como realizarlo para el caso de un script que testea una conexión a una base de datos. El comando que se ejecuta es "sqlplus.exe" y queda parado sin terminar porque la linea "exit" del fichero que ejecuta esta comentada. De esta forma simulamos un problema que tendrá que resolver el script:

'-----------------------------------------------------------------------
' Constantes 
'-----------------------------------------------------------------------
Const ORA_SID="XE"
Const ORA_PORT="1521"
Const operuser="sys"
Const operpwd="manager"
'-----------------------------------------------------------------------

On Error Resume Next

IdProceso= ScriptProcessID()

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set WshShell = CreateObject("WScript.Shell")

scriptPath=WshShell.CurrentDirectory

PathFileConexSQL=scriptPath & "\conexion.sql"
PathFileConexLog=scriptPath & "\conexion.log"

If objFSO.FileExists(PathFileConexLog) Then objFSO.DeleteFile(PathFileConexLog)
If objFSO.FileExists(PathFileConexSQL) Then objFSO.DeleteFile(PathFileConexSQL)

Set objPathFileConexSQL = objFSO.CreateTextFile(PathFileConexSQL)
objPathFileConexSQL.WriteLine("spool " & PathFileConexLog & ";")
objPathFileConexSQL.WriteLine("column host_name format A20;")
objPathFileConexSQL.WriteLine("select host_name, instance_name, status from v$instance;")
objPathFileConexSQL.WriteLine("spool off;")
'objPathFileConexSQL.WriteLine("exit;")
objPathFileConexSQL.Close

comando="sqlplus -L """ & operuser & "/" & operpwd & "@" & ORA_SID & " as sysdba"" @" & PathFileConexSQL
WshShell.Run(comando),2,false

wscript.sleep 10000

Set colProcessList=Nothing
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
Set colProcessList = objWMIService.ExecQuery("SELECT * FROM Win32_Process WHERE Name = 'sqlplus.exe' And ParentProcessId = " & IdProceso)

If colProcessList.count>0 Then
  For Each objProcess in colProcessList
    MsgBox("Terminando proceso bloqueado ...")
    objProcess.Terminate()
  Next
End If

'If objFSO.FileExists(PathFileConexLog) Then objFSO.DeleteFile(PathFileConexLog)
'If objFSO.FileExists(PathFileConexSQL) Then objFSO.DeleteFile(PathFileConexSQL)

WScript.Quit

'-----------------------------------------------------------------------
' Función obtener el processID de este script
'-----------------------------------------------------------------------
Function ScriptProcessID()

  Set objSWbemServices = GetObject ("WinMgmts:Root\Cimv2")
  Set colProcess = objSWbemServices.ExecQuery("Select * From Win32_Process Where Name='wscript.exe' Or Name='cscript.exe'")

  For Each objProcess In colProcess
    if instr(objProcess.CommandLine,WScript.ScriptFullName)<>0 Then
      ScriptProcessID=objProcess.ProcessID
    End If
  Next

End Function
'-----------------------------------------------------------------------

Lo primero es identificar el ID del proceso de sistema del propio script; esto se hace en la linea:

IdProceso= ScriptProcessID()

La función ScriptProcessID utiliza el ScriptFullName y una consulta WMI para encontrar dicho ID.

Generamos el fichero PathFileConexSQL con la consulta SQL que lanzaremos a la base de datos una vez conectados; dejamos la sentencia "exit" comentada para simular un proceso que no termina:

spool C:\Oracle\conexion.log;
column host_name format A20;
select host_name, instance_name, status from v$instance;
spool off;

Después generamos el comando que vamos a ejecutar, un sqlplus con su cadena de conexión generada a partir de las variables definidas al inicio del script y que una vez conectado a la base de datos ejecutara el fichero anterior:

comando="sqlplus -L """ & operuser & "/" & operpwd & "@" & ORA_SID & " as sysdba"" @" & PathFileConexSQL

Lanzamos el comando y damos un tiempo de espera (10 segundos):

WshShell.Run(comando),2,false
wscript.sleep 10000

Es el momento de confirmar que nuestro comando ha terminado. Lo hacemos mediante una consulta WMI que utiliza el nombre del comando ejecutado, sqlplus.exe, y el ID del proceso padre que lo lanzo, que es el ID de nuestro script obtenido al principio:

Set colProcessList=Nothing
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
Set colProcessList = objWMIService.ExecQuery("SELECT * FROM Win32_Process WHERE Name = 'sqlplus.exe' And ParentProcessId = " & IdProceso)

En caso de que la consulta obtenga algún resultado tenemos que matar dicho proceso, en este ejemplo hemos añadido un mensaje que en un entorno real no tendría sentido incluir:

If colProcessList.count>0 Then
  For Each objProcess in colProcessList
    MsgBox("Terminando proceso bloqueado ...")
    objProcess.Terminate()
  Next
End If

La ejecución del script da este resultado:














Si quitamos el comentario a la linea que hacia el "exit" en la conexión a la base de datos el script funciona perfectamente sin ningún problema:

objPathFileConexSQL.WriteLine("exit;")

Un saludo

miércoles, 2 de julio de 2014

Como trabajar con parámetros tipo Array entre Java y Oracle.

Imaginemos el caso en que para crear un objeto Java necesitamos leer varios registros de una tabla de una base de datos Oracle y queremos hacerlo con una sola llamada a un procedimiento almacenado PL/SQL. Necesitamos poder trabajar con parámetros tipo Array entre Java y Oracle.

Por ejemplo, una clase usuario que aparte de atributos normales incluye dos atributos múltiples de tipo lista de objetos de otra clase servicio:

ArrayList <servicio> servicios = new ArrayList<servicio>();
ArrayList <servicio> intereses = new ArrayList<servicio>();

Tendremos que leer de la base de datos la tabla usuarios y las tablas necesarias para obtener los servicios e intereses de dicho usuario.

Creación de objetos y procedimiento almacenado en Oracle

Necesitamos un objeto tipo que tenga la misma estructura que el objeto servicio:

create or replace TYPE TYP_SERVICIO AS
    OBJECT (SRV_ID NUMBER, SRV_NOMBRE VARCHAR2(40))

Ahora necesitamos un objeto tipo lista que sea una lista del anterior y que utilizaremos como parámetro de salida del procedimiento almacenado:

create or replace TYPE TYP_LST_SERVICIOS_2 AS TABLE OF TYP_SERVICIO;

Los objetos anteriores los utilizamos tanto para los servicios de usuario como para sus intereses porque tienen la misma estructura y, en realidad, los datos vienen de la misma tabla, pero eso no viene al caso. Si no fuese así tendríamos que haber creado otro objeto tipo y lista adicional.

Creamos el procedimiento almacenado en el que realizamos las tres consultas que necesitamos y que requiere de un solo parámetro de entrada, el ID del usuario:

PROCEDURE PRC_USR_GET_DATA(
    P_USR_ID IN USUARIOS.USR_ID%TYPE,
    P_USR_NICK OUT USUARIOS.USR_NICK%TYPE,
    P_USR_NOMBRE OUT USUARIOS.USR_NOMBRE%TYPE,
    P_USR_APELLIDOS OUT USUARIOS.USR_APELLIDOS%TYPE,
    P_USR_EMAIL OUT USUARIOS.USR_EMAIL%TYPE,
    P_USR_LOC_LAT OUT USUARIOS.USR_LOC_LAT%TYPE,
    P_USR_LOC_LONG OUT USUARIOS.USR_LOC_LONG%TYPE,
    P_USR_SALDO OUT USUARIOS.USR_SALDO%TYPE,
    P_USR_VALORACION OUT USUARIOS.USR_VALORACION%TYPE,
    P_USR_FECALTA OUT VARCHAR2,
    P_USR_LASTCONEX OUT VARCHAR2,
    P_SRV_LIST OUT TYP_LST_SERVICIOS_2,
    P_INT_LIST OUT TYP_LST_SERVICIOS_2,
    P_ERROR OUT VARCHAR2
) AS
V_PRC_NOMBRE BT_LOG.LOG_PROCESO%TYPE := 'PKG_USUARIOS.PRC_USR_GET_DATA';
BEGIN
    SELECT USR_NICK, USR_NOMBRE, USR_APELLIDOS, USR_EMAIL,
        USR_LOC_LAT, USR_LOC_LONG, USR_SALDO, USR_VALORACION,
        TO_CHAR(USR_FEC_ALTA, 'DD Month YYYY'),
        TO_CHAR(USR_LASTCONEX, 'DD Month YYYY HH24:MI')
    INTO P_USR_NICK, P_USR_NOMBRE, P_USR_APELLIDOS, P_USR_EMAIL,    
        P_USR_LOC_LAT, P_USR_LOC_LONG, P_USR_SALDO,
        P_USR_VALORACION, P_USR_FECALTA, P_USR_LASTCONEX
    FROM USUARIOS WHERE USR_ID=P_USR_ID;
    SELECT TYP_SERVICIO(S.SRV_ID, initcap(S.SRV_NOMBRE)) bulk collect
        INTO P_INT_LIST
        FROM USR_INT UI LEFT JOIN SERVICIOS S ON S.SRV_ID=UI.SRV_ID
        WHERE UI.USR_ID=P_USR_ID;
    SELECT TYP_SERVICIO(S.SRV_ID, initcap(S.SRV_NOMBRE)) bulk collect
        INTO P_SRV_LIST
        FROM USR_SRV US LEFT JOIN SERVICIOS S ON S.SRV_ID=US.SRV_ID
        WHERE US.USR_ID=P_USR_ID;
    RETURN;
    EXCEPTION WHEN OTHERS THEN
        PKG_LOG.PRC_LOG_WRITE(P_USR_ID, V_PRC_NOMBRE, SQLCODE,
            SUBSTR(SQLERRM,0,200), P_ERROR);
    RETURN;
END PRC_USR_GET_DATA;

Hay que destacar los dos puntos; primero los parámetros de salida que son del mismo tipo lista que creamos anteriormente:

    P_SRV_LIST OUT TYP_LST_SERVICIOS_2,
    P_INT_LIST OUT TYP_LST_SERVICIOS_2,

Y segundo, las consultas para rellenar dichos parámetros de salida:

    SELECT TYP_SERVICIO(S.SRV_ID, initcap(S.SRV_NOMBRE)) bulk collect
        INTO P_SRV_LIST...

Creación de objetos Java

La clase servicio es muy simple, dos atributos y sus métodos getter y setter:

package comun;
public class servicio {
    private int id;
    private String nombre;
    public void setId(int a){ id=a; }
    public int getId(){ return id; }
    public void setNombre(String a){ nombre=a; }
    public String getNombre(){ return nombre; }
}

Es el objeto en el que nos hemos basado para crear el objeto de Oracle TYP_SERVICIO.

La clase usuario tiene un constructor desde el que llama al procedimiento almacenado. Necesita el ID del usuario que queremos crear:

package comun;
import java.sql.*;
import java.util.ArrayList;
import java.sql.Types;
import oracle.sql.STRUCT;
import java.math.BigDecimal;
import comun.BBDD;
import comun.servicio;
public class usuario {
    private int id ;
    private String nick;
    private String nombre;
    private String apellidos;
    private String email;
    private int saldo;
    private double loc_lat;
    private double loc_long;
    private String fec_alta;
    private int valoracion;
    private String ultima_conexion;
    ArrayList <servicio> servicios = new ArrayList<servicio>();
    ArrayList <servicio> intereses = new ArrayList<servicio>();
    private BBDD bd = new BBDD();
    public usuario(int user_id) {
        try{
            Connection conn = bd.conectar();
            bd.conectar();
            CallableStatement cs = conn.prepareCall("{call
                PKG_USUARIOS.PRC_USR_GET_DATA(?,?,?,?,?,?,?,?,?,?,?,?,?,?)}");
            cs.setInt(1, user_id);
            cs.registerOutParameter(2, Types.VARCHAR);
            cs.registerOutParameter(3, Types.VARCHAR);
            cs.registerOutParameter(4, Types.VARCHAR);
            cs.registerOutParameter(5, Types.VARCHAR);
            cs.registerOutParameter(6, Types.DOUBLE);
            cs.registerOutParameter(7, Types.DOUBLE);
            cs.registerOutParameter(8, Types.INTEGER);
            cs.registerOutParameter(9, Types.INTEGER);
            cs.registerOutParameter(10, Types.VARCHAR);
            cs.registerOutParameter(11, Types.VARCHAR);
            cs.registerOutParameter(12, Types.ARRAY, "TYP_LST_SERVICIOS_2");
            cs.registerOutParameter(13, Types.ARRAY, "TYP_LST_SERVICIOS_2");
            cs.registerOutParameter(14, Types.VARCHAR);
            cs.executeUpdate();
            if(cs.getString(14)!=null){
                System.out.println("ERROR: " + cs.getString(14));
            }
            else{
                id=user_id;
                nick=cs.getString(2);
                nombre=cs.getString(3);
                apellidos=cs.getString(4);
                email=cs.getString(5);
                loc_lat=cs.getDouble(6);
                loc_long=cs.getDouble(7);
                saldo=cs.getInt(8);
                valoracion=cs.getInt(9);
                fec_alta=cs.getString(10);
                ultima_conexion=cs.getString(11);
                Array arrS = cs.getArray(12);
                Object[] objArrS = (Object[]) arrS.getArray();
                for(int i=0; i<objArrS.length;i++){
                    STRUCT st = (STRUCT)objArrS[i];
                    servicio service = new servicio();
                    Object[] obj = st.getAttributes();
                    service.setId(((BigDecimal)obj[0]).intValueExact());
                    service.setNombre(String.valueOf(obj[1]));                  
                    servicios.add(service);
                }              
                Array arrI = cs.getArray(13);
                Object[] objArrI = (Object[]) arrI.getArray();
                for(int i=0; i<objArrI.length;i++){
                    STRUCT st = (STRUCT)objArrI[i];
                    servicio service = new servicio();
                    Object[] obj = st.getAttributes();
                    service.setId(((BigDecimal)obj[0]).intValueExact());
                    service.setNombre(String.valueOf(obj[1]));                  
                    intereses.add(service);
                }              
            }          
        }
        catch (Exception ex) {
            System.out.println("ERROR: " + ex.toString());
        }
    }
    public String getNick(){ return nick; }
    public String getNombre(){ return nombre; }
    public String getApellidos(){ return apellidos; }
    public String getEmail(){ return email; }
    public int getSaldo(){ return saldo; }
    public String getFec_alta(){ return fec_alta; }
    public int getValoracion(){ return valoracion; }
    public double getLoc_lat(){ return loc_lat; }
    public double getLoc_long(){ return loc_long; }
    public String getUltima_conexion(){ return ultima_conexion; }  
    public ArrayList <servicio> getServicios(){ return servicios; }
    public ArrayList <servicio> getIntereses(){ return intereses; }
}

Hay que destacar la definición de los parámetros de salida para los servicios e intereses, en los que hay que especificar el tipo de lista definido en Oracle:

    cs.registerOutParameter(12, Types.ARRAY, "TYP_LST_SERVICIOS_2");
    cs.registerOutParameter(13, Types.ARRAY, "TYP_LST_SERVICIOS_2");

y la conversión del Array que se obtiene como salida del procedimiento almacenado en una ArrayList:

    Array arrS = cs.getArray(12);
    Object[] objArrS = (Object[]) arrS.getArray();
    for(int i=0; i<objArrS.length;i++){
        STRUCT st = (STRUCT)objArrS[i];
        servicio service = new servicio();
        Object[] obj = st.getAttributes();
        service.setId(((BigDecimal)obj[0]).intValueExact());
        service.setNombre(String.valueOf(obj[1]));                  
        servicios.add(service);
    }  

Con esto hemos terminado, espero que le sea de utilidad a alguien.

Hasta otra.