AHKv2: una clase para abrir nuevos procesos de AHK y comunicarse con él [14/11/2016]

Compartir enlaces, herramientas y otros recursos de Internet que puedan ser útiles para programar en AutoHotkey

Moderator: Flipeador

User avatar
Flipeador
Posts: 1204
Joined: 15 Nov 2014, 21:31
Location: Argentina
Contact:

AHKv2: una clase para abrir nuevos procesos de AHK y comunicarse con él [14/11/2016]

Post by Flipeador » 14 Nov 2016, 03:07

descargar AutoHotKey v2 y Archivos .bin (usar el compilador de la v1)

Nota importante: si tu Sistema Operativo es de 64-bit ejecutar la versión de 64-bit de AutoHotKey o añadir SetRegView 64 a el script y en cada nuevo proceso (parámetro Script al crear el objeto)
ThreadInstance Class
¿Por qué publico esto aquí y no en Scripts y Funciones?.
Porque la verdad no considero muy útil esta clase, solo la he echo por diversión (y de lo muy al p2) :lol: .

¿Y entonces, cual es su función?
Lo que hace es ejecutar tantos procesos nuevos como queramos de AutoHotKey.exe con un script inicial.

¿cuales son los Pros de usar esta clase y no directamente un comando Run?
--> Esta clase permite comunicarse con el nuevo proceso creado y acceder a sus variables y funciones, como establecer valores y capacidad a variables.

¿y los contras?
--> no la consideraría fiable, además de ser muy lenta.
--> al parecer __Delete() no es llamada cuando asignamos un nuevo valor al objeto, no se por qué... por eso la creación del método Terminate().
--> se debe tener AutoHotKey.exe para poder ejecutar nuevos procesos. Esto quiere decir que por más que compilemos nuestro script nunca va funcionar la clase en otra computadora si no se encuentra AutoHotKey instalado. Aunque la clase dispone de un parámetro para especificar su ruta, en caso de que nuestro script esté compilado.
También se podría incluir AutoHotKey.exe dentro de nuestro script compilado (en el recurso RC_DATA), luego al usarlo, extraerlo en una carpeta temporal, y especificar esa ruta al crear el objeto (clase).

Code: Select all

/*
/*
/*
Permite crear nuevas instancias de AutoHotKey y ejecutar un Script espesificado
Notas:
	• cuando el proceso principal (el que creo todas las instanacias) es terminado, todas sus instancias son automáticamente terminadas con él
	• cuando el objeto es destruido, tambien lo és la instancia asociada a él
	• no podrá utilizar ninguno de estos nombres para funciones: Script_OnExit()
	• no podrá utilizar ninguno de estos nombres para clases: Class_RegisterActiveObject
	• no deverá sobreescribir por ningún motivo las siguientes variables: Object_Class_RegisterActiveObject
	• no deverá utilizar ninguna de estas funciones: OnExit()
*/
class ThreadInstance {
	static Instances := []			;en esta variable almacenamos todas las instancias (processid) creadas
	static nCLSID := 1000		;esta variable la utilizamos para evitar repetir el CLSID al registrar el objeto activo (siempre debe tener 4 dígitos)

	/*
	Constructor: crear nueva instancia de AutoHotKey.exe que ejecutará el script espesificado
	Parámetros:
		• Script: script a ejecutar
		• OnExitScript: aquí debera poner el script q se ejecutará al encontrar un ExitApp en la nueva instancia, ya que al crea la nueva instancia, se utiliza la función incorporada de AHK OnExit() para eliminar el objeto al salir.
		• Wait: determina si se debe esperar hasta que el nuevo proceso termine
		• AhkPath: si el script está compilado, deberá espesificar la ruta a AutoHotKey.exe (si no se espesifica comprueba AutoHotKey.exe en el directorio en el que se está ejecutando el script o A_AhkPath)
	Variables de cada instancia o proceso:
		• ProcessId: PID del proceso nuevo
	Notas:
		• si Wait=TRUE, ErrorLevel se establece en el código de salida del proceso y la llamada a __New() devuelve 0
	*/
	__New(Script := "", OnExitScript := "", Wait := false, AhkPath := "") {
		if (A_IsCompiled) {
			
			;sin terminar. para funcionar, se necesita AutoHotKey.exe
			AhkPath := FileExist(A_ScriptDir . "\AutoHotKey.exe") ? A_ScriptDir . "\AutoHotKey.exe" : A_AhkPath
			
		} else AhkPath := A_AhkPath
		
		if !FileExist(AhkPath)
			return false
		
		;creamos un objeto activo para la instancia actual, para poder recibir valores del nuevo proceso
		this.AppId := "AHK-" . ThreadInstance.nCLSID
		thisCLSID := this.CLSID := "{B0FA566D-126C-" . ThreadInstance.nCLSID . "-BAF5-D74046AC1F50}"
		VarSetCapacity(GUID, 16, 0)
		if (DllCall("Ole32.dll\CLSIDFromString", "WStr", this.CLSID, "Ptr", &GUID) != 0), return false
		if (DllCall("OleAut32.dll\RegisterActiveObject", "Ptr", &this, "Ptr", &GUID, "UInt", 1, "UIntP", hRegister, "UInt") != 0), return false
		this.hRegister := hRegister
		RegWrite, REG_SZ, HKCU\Software\Classes\%this.AppId%,, % this.AppId
		RegWrite, REG_SZ, HKCU\Software\Classes\%this.AppId%\CLSID,, % this.CLSID
		RegWrite, REG_SZ, HKCU\Software\Classes\CLSID\%this.CLSID%,, % this.AppId
		
		;1) establecemos la ventana propietaria de la ventana del nuevo script a la del script actual
			;esto es para que cuando el proceso de éste script (o sea, el principal) termine, automáticamente terminen todos los proceso que éste inició (todo proceso tiene por lo menos una ventana)
		;2) registramos un objeto activo en el nuevo script para obtener información de éste, desde el script actual (o sea, el principal). Este objeto es manejable desde cualquier proceso sabiendo el CLSID
		;3) creamos una función para OnExit() y en ella añadimos funciones a DllCall para eliminar el Objeto Activo actual y restaurar la ventana propietaria (el escritorio) para poder terminar el proceso correctamente
		;Notas: si alguna función fallta, automáticamente el nuevo script termina
		AppId := "AHK" . ThreadInstance.nCLSID
		CLSID := "{B0FA566D-" . ThreadInstance.nCLSID . "-4ED7-BAF5-D74046AC1F50}"

		Script := "
		(
			DllCall('User32.dll\SetParent', 'Ptr', A_ScriptHwnd, 'Ptr', %A_ScriptHwnd%, 'Ptr')
			
			Object_Class_RegisterActiveObject := new Class_RegisterActiveObject('%CLSID%', '%AppId%')
			
			#Persistent
			#NoTrayIcon
			OnExit, Script_OnExit, 1
			VarSetCapacity(t, 4, 0), DllCall('Ntdll.dll\RtlAdjustPrivilege', 'UInt', 20, 'UChar', true, 'UChar', false, 'Ptr', &t)
			DllCall('Kernel32.dll\SetProcessWorkingSetSize', 'Ptr', DllCall('Kernel32.dll\GetCurrentProcess', 'Ptr'), 'UPtr', -1, 'UPtr', -1)
			%Script%
			
			Script_OnExit(ExitReason, ExitCode) {
				global Object_Class_RegisterActiveObject
				%OnExitScript%
				DllCall('OleAut32.dll\RevokeActiveObject', 'UInt', Object_Class_RegisterActiveObject.hRegister, 'Ptr', 0)
				RegDeleteKey, HKCU\Software\Classes\%AppId%
				RegDeleteKey, HKCU\Software\Classes\CLSID\%CLSID%
				DllCall('User32.dll\SetParent', 'Ptr', A_ScriptHwnd, 'Ptr', 0, 'Ptr')
				return 0
			}
			
			class Class_RegisterActiveObject {
				static Owner
				__New(CLSID, AppId) {
					try Class_RegisterActiveObject.Owner := ComObjActive('%thisCLSID%')
					catch
						ExitApp
					VarSetCapacity(GUID, 16, 0)
					if (DllCall('Ole32.dll\CLSIDFromString', 'WStr', CLSID, 'Ptr', &GUID) != 0), ExitApp
					if (DllCall('OleAut32.dll\RegisterActiveObject', 'Ptr', &this, 'Ptr', &GUID, 'UInt', 1, 'UIntP', hRegister, 'UInt') != 0), ExitApp
					this.hRegister := hRegister
					RegWrite, REG_SZ, HKCU\Software\Classes\`%AppId`%,, `% AppId
					RegWrite, REG_SZ, HKCU\Software\Classes\`%AppId`%\CLSID,, `% CLSID
					RegWrite, REG_SZ, HKCU\Software\Classes\CLSID\`%CLSID`%,, `% AppId
				}
				
				CallFunc(FuncName, Params*) {
					try if !Params.MaxIndex(), Result := `%FuncName`%()
					else Result := `%FuncName`%(Params*)
					this.Owner.Pipe(Result, ErrorLevel)
				}
					
				SetVar(VarName, Value) {
					global
					`%VarName`% := Value
					this.Owner.Pipe()
				}
				
				SetVarCapacity(VarName, Capacity, FillByte) {
					global
					if (Capacity = ""), Result := VarSetCapacity(`%VarName`%)
					else Result := VarSetCapacity(`%VarName`%, Capacity, FillByte)
					this.Owner.Pipe(Result)
				}
				
				GetVar(VarName) {
					global
					this.Owner.Pipe(`%VarName`%)
				}
			}
		)"
		ThreadInstance.nCLSID++ ;sumamos uno para evitar repetir el CLSID

		;creamos un archivo .ahk el cual ejecutará AutoHotKey.exe
		;en este proceso intentamos abrir un archivo .ahk en la carpeta temporal del sistema y escribimos el script en él
		RegDeleteKey, HKCU\Software\Classes\%AppId%
		Loop
			f := FileOpen(AhkScriptFile := A_Temp "\" A_Index ".ahk", "w-rwd")
		until IsObject(f)
		f.Write(Script), f.Close()
		
		;ejecutamos AutoHotKey.exe con la ruta del nuevo script.ahk
		;Run no modifica ErrorLevel, pero RunWait si, lo establece en el código de salida del proceso ejecutado
		if (Wait), RunWait, "%AhkPath%" "%AhkScriptFile%",,, PID
		else Run, "%AhkPath%" "%AhkScriptFile%",,, PID

		;comprobamos que se haya ejecutado correctamente y guardamos el PID
		if !(this.ProcessId := PID) || (Wait), return false
		ThreadInstance.Instances.Push(this.ProcessId)
		
		;esperamos hasta que la nueva instancia cree el objeto activo y lo guardamos para comunicarnos con la nueva instancia
		Loop
			RegRead("HKCU\Software\Classes\" AppId), Sleep(1)
		until !ErrorLevel || (A_Index = 400)
		try this.ActiveObject := ComObjActive(CLSID)
		catch
			return false
	}

	__Delete() {
		this.Terminate()
	}
	
	/*
	Terminar proceso y eliminar objeto activo
	*/
	Terminate() {
		try this.Call("ExitApp")
		DllCall("OleAut32.dll\RevokeActiveObject", "UInt", this.hRegister, "Ptr", 0)
		RegDeleteKey, HKCU\Software\Classes\%this.AppId%
		RegDeleteKey, HKCU\Software\Classes\CLSID\%this.CLSID%
		ThreadInstance.GetInstances()
	}
	
	/*
	obtiene un Array con todos los PIDs de procesos existentes actuales iniciados por esta clase
	*/
	GetInstances() {
		Instances := [] ;primero remueve procesos inexistentes del Array
		for Index, ProcessId in ThreadInstance.Instances
			if ProcessExist(ProcessId), Instances.Push(ProcessId)
		return ThreadInstance.Instances := Instances
	}
	
	/*
	Llama a la función espesificada en la nueva instancia
	Parámetros:
		• FuncName: nombre de la función a llamar
		• Params: parámetros a pasar
	*/
	Call(FuncName, Params*) {
		this.PipeOk := false
		if !Params.MaxIndex(), this.ActiveObject.CallFunc(FuncName)
		else this.ActiveObject.CallFunc(FuncName, Params*)
		while !this.PipeOk
			Sleep 0
		ErrorLevel := this.Error
		return this.OutputVar
	}

	/*
	Cambiar valor a una variable
	*/
	SetVar(VarName, Value := "") {
		this.PipeOk := false
		this.ActiveObject.SetVar(VarName, Value)
		while !this.PipeOk
			Sleep 0
	}
	
	/*
	Establecer capacidad de una variable
	*/
	SetVarCapacity(VarName, Capacity := "", FillByte := "") {
		this.PipeOk := false
		this.ActiveObject.SetVarCapacity(VarName, Capacity, FillByte)
		while !this.PipeOk
			Sleep 0
		ErrorLevel := this.Error
		return this.OutputVar
	}
	
	/*
	Obtener valor de una variable
	*/
	GetVar(VarName) {
		this.PipeOk := false
		this.ActiveObject.GetVar(VarName)
		while !this.PipeOk
			Sleep 0
		ErrorLevel := this.Error
		return this.OutputVar
	}
	
	;######################################################################################################################################################
	
	/*
	Esta función es utilizada por el nuevo proceso para mandar valores a éste proceso
	*/
	Pipe(OutputVar := "", Error := 0) {
		this.OutputVar := OutputVar
		this.Error := Error
		this.PipeOk := true
	}
}

Ejemplo

Code: Select all

;script a ejecutar en la nueva instancia
Script := "
(
	MiVar1 := 125
	a::MsgBox Hola! soy la nueva instancia.``nMi ruta es: `%A_ScriptFullPath`%.
	My_Function(Param1, Param2) {
		MsgBox Nueva Instancia``n``nParam1 es `%Param1`%``nParam2 es `%Param2`%
		ErrorLevel := 247
		return 10066
	}
)"

;creamos la instancia
T1 := new ThreadInstance(Script)
if !IsObject(T1) {
	MsgBox ERROR!
	ExitApp
}

;creamos otra instancia
T2 := new ThreadInstance()
if !IsObject(T2) {
	MsgBox ERROR!
	ExitApp
}

MsgBox Se ha creado la nueva instancia con éxito. Precione la letra 'A' para ejecutar un MsgBox en ella.

;llamamos a la función My_Function() en la nueva instancia pasando sus dos parámetros y mostramos el resultado que tiene q se 10066 y ErrorLevel 247 (como espesificamos arriba)
MsgBox % "Call(My_Function) Return: " . T1.Call("My_Function", 954, "Value") "`nErrorLevel: " ErrorLevel

;mostrar valor de MiVar1
MsgBox % "GetVar(MiVar1): " . T1.GetVar("MiVar1")

;establecer valor a variable y mostrar
T1.SetVar("MiVar2", 666)
MsgBox % "GetVar(MiVar2): " . T1.GetVar("MiVar2")

;terminamos la instancia (o proceso)
T1.Terminate()

;obtenemos una lista con todas las instancias activas
for k, v in ThreadInstance.GetInstances()
	Instances .= v "`n"
MsgBox % "Instancias actuales:`n`n" . Instances

;establecer valor a variable y mostrar en la instancia 2
T2.SetVar("MiVar", 999999999099909)
MsgBox % "T2.GetVar(MiVar): " . T2.GetVar("MiVar")

;terminar
MsgBox <<END.>>
ExitApp

Return to “Otras Utilidades y Recursos”