LoadFile - Load script file as a separate process

Post your working scripts, libraries and tools
lexikos
Posts: 6653
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

LoadFile - Load script file as a separate process

31 Jan 2015, 20:57

Script := LoadFile(Path [, EXE])

Loads a script file as a child process and returns an object which can be used as follows:

Code: Select all

; Call a function named 'Function' in the target script:
Script.Function()
; Retrieve a global variable:
value := Script.G["VarName"]
; Set a global variable:
Script.G["VarName"] := value
This is mostly intended as an example for ObjRegisterActive, but might be useful in its own right.

All of the required functions are expected to be installed in a function library.

Code: Select all

/*
    LoadFile(Path [, EXE])
    
        Loads a script file as a child process and returns an object
        which can be used to call functions or get/set global vars.
    
    Path:
          The path of the script.
    EXE:
          The path of the AutoHotkey executable (defaults to A_AhkPath).
    
    Requirements:
      - AutoHotkey v1.1.17+    http://ahkscript.org/download/
      - ObjRegisterActive      http://goo.gl/wZsFLP
      - CreateGUID             http://goo.gl/obfmDc

    Version: 1.0
*/

LoadFile(path, exe:="", exception_level:=-1) {
    ObjRegisterActive(client := {}, guid := CreateGUID())
    code =
    (LTrim
    LoadFile.Serve("%guid%")
    #include %A_LineFile%
    #include %path%
    )
    try {
        exe := """" (exe="" ? A_AhkPath : exe) """"
        exec := ComObjCreate("WScript.Shell").Exec(exe " /ErrorStdOut *")
        exec.StdIn.Write(code)
        exec.StdIn.Close()
        while exec.Status = 0 && !client._proxy
            Sleep 10
        if exec.Status != 0 {
            err := exec.StdErr.ReadAll()
            ex := Exception("Failed to load file", exception_level)
            if RegExMatch(err, "Os)(.*?) \((\d+)\) : ==> (.*?)(?:\s*Specifically: (.*?))?\R?$", m)
                ex.Message .= "`n`nReason:`t" m[3] "`nLine text:`t" m[4] "`nFile:`t" m[1] "`nLine:`t" m[2]
            throw ex
        }
    }
    finally
        ObjRegisterActive(client, "")
    return client._proxy
}

class LoadFile {
    Serve(guid) {
        try {
            client := ComObjActive(guid)
            client._proxy := new this.Proxy
            client := ""
        }
        catch ex {
            stderr := FileOpen("**", "w")
            stderr.Write(format("{} ({}) : ==> {}`n     Specifically: {}"
                , ex.File, ex.Line, ex.Message, ex.Extra))
            stderr.Close()  ; Flush write buffer.
            ExitApp
        }
        ; Rather than sleeping in a loop, make the script persistent
        ; and then return so that the #included file is auto-executed.
        Hotkey IfWinActive, %guid%
        Hotkey vk07, #Persistent, Off
        #Persistent:
    }
    class Proxy {
        __call(name, args*) {
            if (name != "G")
                return %name%(args*)
        }
        G[name] { ; x.G[name] because x[name] via COM invokes __call.
            get {
                global
                return ( %name% )
            }
            set {
                global
                return ( %name% := value )
            }
        }
        __delete() {
            ExitApp
        }
    }
}
Known issues:
  • A_ScriptFullPath in the loaded file = %A_WorkingDir%\* and A_ScriptName = *. You can use A_LineFile instead.
  • See ObjRegisterActive.
Script := LoadLib(LibName [, EXE])

Like LoadFile, but loads a script from a function library.

Code: Select all

LoadLib(name, exe:="") {
    if (exe = "")
        exe := A_AhkPath
    libs := [A_ScriptDir "\Lib\", A_MyDocuments "\AutoHotkey\Lib\", exe "\..\Lib\"]
    for i, lib in libs {
        if FileExist(lib name ".ahk")
            return LoadFile(lib name ".ahk", exe, -2)
    }
}
Example usage:

Code: Select all

lib := LoadLib("CreateGUID")
MsgBox % lib.CreateGUID()
MsgBox % lib.G["A_WorkingDir"]
User avatar
joedf
Posts: 7289
Joined: 29 Sep 2013, 17:08
Facebook: J0EDF
Google: +joedf
GitHub: joedf
Location: Canada
Contact:

Re: LoadFile - Load script file as a separate process

01 Feb 2015, 01:09

Very Very Nice!
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500 @ 4.00 GHz, 2x8GB G.Skill RipJaws V - DDR4 3280 MHz, NVIDIA GTX 1060 6GB | [About Me] | [ASPDM - StdLib Distribution]
[Populate the AHK MiniCity!] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library] | [About the AHK Foundation]
User avatar
Learning one
Posts: 137
Joined: 04 Oct 2013, 13:59
Location: Croatia
Contact:

Re: LoadFile - Load script file as a separate process

01 Feb 2015, 12:35

Thank you Lexikos! :)
carno
Posts: 205
Joined: 20 Jun 2014, 16:48

Re: LoadFile - Load script file as a separate process

01 Feb 2015, 13:55

Any example using this script for a relative noob in some relatively simple application?
A_User
Posts: 36
Joined: 21 Aug 2017, 01:15

Re: LoadFile - Load script file as a separate process

30 Oct 2017, 08:48

This is something I've been looking for. Thank you for the great work, lexikos.

I'm wondering if there is a way to pass script arguments to the called script file, or preferably a way to set global variables before the auto-execute section starts. You showed an example to set a global variable but it is done after the auto-execute section gets loaded. Can it be done before the script starts? I tried modifying the code and passing a variable to the constructor of the Proxy child class and setting properties but did not work.

[Edit]
A workaround I found is to inject code that calls another ObjRegisterActive() but I'm not sure if there is a better way.

Code: Select all

LoadFileWithArguments(path, exe:="", exception_level:=-1, aArguments="" ) {
    ObjRegisterActive(client := {}, guid := CreateGUID())    
    ObjRegisterActive( aArguments, sArgumentGUID := CreateGUID() )
    code =
    (LTrim
    LoadFile.Serve("%guid%")    
    _aArguments := ComObjActive("%sArgumentGUID%")    
    #include %A_LineFile%
    #include %path%
    )
...
Also as pointed out in another forum thread, AutoHotkey objects cannot be iterated with the For loop in the child (remote) script so I had to serialize object values in the parent script and unserialize it in the child.

Another problem, when #SingleInstance, Force is present in the child script and runs a second instance, it throws an error, "Error: Failed to load file" without showing the reason. It would be nice if #SingleInstance, Force is properly handled as non-remote scripts do.
lexikos
Posts: 6653
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: LoadFile - Load script file as a separate process

07 Nov 2017, 03:52

A_User wrote:A workaround I found is to inject code that calls another ObjRegisterActive() but I'm not sure if there is a better way.
You could put the parameters in client instead and pull them out in Serve(). There's really no need to register two objects.

You could inject normal variable assignments, even emulate command line args.

Code: Select all

    code =
    (LTrim
    0 = 2
    1 = First arg
    2 = Second arg
    LoadFile.Serve("%guid%")
    #include %A_LineFile%
    #include %path%
    )
You could abandon the auto-execute section and use an initialisation function which accepts parameters.
Another problem, when #SingleInstance, Force is present in the child script and runs a second instance, it throws an error, "Error: Failed to load file" without showing the reason.
Please demonstrate. I have tried and failed to reproduce the error.
It would be nice if #SingleInstance, Force is properly handled as non-remote scripts do.
I think what you're asking for doesn't make sense. Are all scripts loaded from the same file to be counted as instances of the same script? Whether they should be counted as instances of a script at all depends on what you're using it for. It's up to you to detect other "instances" and do whatever you want with them.

By default, all "loaded" scripts have the title %A_WorkingDir%\* - AutoHotkey v%A_AhkVersion%. A single-instance script will look for another instance with this title, so every script loaded from that working directory will appear to be the same script. You should therefore avoid using #SingleInstance, and implement your own checks if needed. LoadFile should do this:

Code: Select all

    code =
    (LTrim
    LoadFile.Serve("%guid%")
    #include %A_LineFile%
    #include %path%
    #SingleInstance Off   ; <=== this
    )
#SingleInstance defaults to Off for all scripts which are read from stdin (*), but the script can override it.

If you used #SingleInstance with a "loaded" script, what would you expect to happen to the clients who were connected to the old instance? They would be disconnected.
A_User
Posts: 36
Joined: 21 Aug 2017, 01:15

Re: LoadFile - Load script file as a separate process

07 Nov 2017, 07:43

Thanks for the reply, lexikos.

I could do something like this and it works.

Code: Select all

LoadFileMod(path, exe:="", exception_level:=-1, aArguments="testing") {
    ObjRegisterActive(client := { arguments: aArguments }, guid := CreateGUID())
    
...
    
    Serve(guid) {
        try {            
            client := ComObjActive(guid)
            client._proxy := new this.Proxy
            global _aArguments := client.arguments
            client := ""
Please demonstrate. I have tried and failed to reproduce the error.
Terribly sorry. I cannot reproduce the error now. I surely got the error though. At that time, I was trying to implement a mechanism to kill all the threads when the main process gets unexpectedly terminated. So it must be my code that caused the error.

As you already explained, the problem could be caused by different scripts with #SingleInstance, Force.
Say, script A is loaded with LoadFile() and script B (a different file) with #SingleInstance, Force is loaded next, Script A gets forcedly terminated. This is what I did not know until you explained. So thank you.

By the way, when the main process is closed normally, like from the script tray menu, its threads get closed gracefully. However, when it gets closed with other means such as Task Manager's process list, the thread processes remain. What's the proper way to close them automatically? I tried to have child scripts run periodical checks to see if the parent process still exists. But this does not work well when the child script (thread) is paused.
lexikos
Posts: 6653
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: LoadFile - Load script file as a separate process

15 Nov 2017, 01:56

A_User wrote:What's the proper way to close them automatically?
Always close processes gracefully.
A_User
Posts: 36
Joined: 21 Aug 2017, 01:15

Re: LoadFile - Load script file as a separate process

19 Nov 2017, 12:55

lexikos wrote:Always close processes gracefully.
If the script user is only me, it is fine. But sometimes it could be an unknown person and he/she may kill the main process from Task Manager, Process Explorer or any other means. For such cases, I'd like to make sure that the threads (child/remote processes) created with LoadFile() automatically close when the main process gets closed.
lexikos
Posts: 6653
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: LoadFile - Load script file as a separate process

23 Nov 2017, 23:58

You asked for the proper way...

You already know what the workaround is. If the parent process is terminated abruptly and the child process is inactive, you can't expect to do anything. You must avoid any conditions which prevent the child process from running its periodic check.

If unknown person terminates the parent process, unknown person can terminate the child processes too.
A_User
Posts: 36
Joined: 21 Aug 2017, 01:15

Re: LoadFile - Load script file as a separate process

24 Nov 2017, 10:30

lexikos wrote:You asked for the proper way...
I asked for the proper way done programmatically. I asked in the hope that you have a good solution for it. Otherwise, this presents an unsolved problem so that somebody else may tackle.
lexikos wrote:If unknown person terminates the parent process, unknown person can terminate the child processes too.
If the script creates nested child processes, it will be difficult. The person can hardly figure which ones are Script A's nested child processes (loaded with LoadFile()) and which ones are Script B's and so on. Process Explorer shows them in a tree view structure but Task Manager doesn't. So I thought it would be useful to have functionality to handle process closing automatically. I suppose real threads can end themselves even if the process unexpectedly gets terminated.
User avatar
GollyJer
Posts: 59
Joined: 19 Sep 2015, 19:33
GitHub: GollyJer

Re: LoadFile - Load script file as a separate process

24 Feb 2018, 11:08

I stumbled on this the other day and would love to use this concept in my scripts.
But, I'm am having a hard time getting a simple example working.

Here's the file I'm loading with LoadFile.

Code: Select all

Hi() {
	MsgBox, "hi!"
}
And here's the file doing the loading.

Code: Select all

#SingleInstance force
#NoEnv

Say := LoadFile(A_ScriptDir . "\say.ahk")


^h:: Say.Hi()


/* Lexikos library stuff */
	/*
	LoadFile(Path [, EXE])

	Loads a script file as a child process and returns an object
	which can be used to call functions or get/set global vars.

	Path:
	The path of the script.
	EXE:
	The path of the AutoHotkey executable (defaults to A_AhkPath).

	Requirements:
	- AutoHotkey v1.1.17+    http://ahkscript.org/download/
	- ObjRegisterActive      http://goo.gl/wZsFLP
	- CreateGUID             http://goo.gl/obfmDc

	Version: 1.0
*/

LoadFile(path, exe:="", exception_level:=-1) {
	ObjRegisterActive(client := {}, guid := CreateGUID())
	code =
    (LTrim
    LoadFile.Serve("%guid%")
    #include %A_LineFile%
    #include %path%
    )
	try {
		exe := """" (exe="" ? A_AhkPath : exe) """"
		exec := ComObjCreate("WScript.Shell").Exec(exe " /ErrorStdOut *")
		exec.StdIn.Write(code)
		exec.StdIn.Close()
		while exec.Status = 0 && !client._proxy
			Sleep 10
		if exec.Status != 0 {
			err := exec.StdErr.ReadAll()
			ex := Exception("Failed to load file", exception_level)
			if RegExMatch(err, "Os)(.*?) \((\d+)\) : ==> (.*?)(?:\s*Specifically: (.*?))?\R?$", m)
				ex.Message .= "`n`nReason:`t" m[3] "`nLine text:`t" m[4] "`nFile:`t" m[1] "`nLine:`t" m[2]
			throw ex
		}
	}
	finally
		ObjRegisterActive(client, "")
	return client._proxy
}

class LoadFile {
	Serve(guid) {
		try {
			client := ComObjActive(guid)
			client._proxy := new this.Proxy
			client := ""
		}
		catch ex {
			stderr := FileOpen("**", "w")
			stderr.Write(format("{} ({}) : ==> {}`n     Specifically: {}"
                , ex.File, ex.Line, ex.Message, ex.Extra))
			stderr.Close()  ; Flush write buffer.
			ExitApp
		}
		; Rather than sleeping in a loop, make the script persistent
		; and then return so that the #included file is auto-executed.
		Hotkey IfWinActive, %guid%
		Hotkey vk07, #Persistent, Off
		#Persistent:
	}
	class Proxy {
		__call(name, args*) {
			if (name != "G")
				return %name%(args*)
		}
		G[name] { ; x.G[name] because x[name] via COM invokes __call.
			get {
				global
				return ( %name% )
			}
			set {
				global
				return ( %name% := value )
			}
		}
		__delete() {
			ExitApp
		}
	}
}

/*
	ObjRegisterActive(Object, CLSID, Flags:=0)

	Registers an object as the active object for a given class ID.
	Requires AutoHotkey v1.1.17+; may crash earlier versions.

	Object:
	Any AutoHotkey object.
	CLSID:
	A GUID or ProgID of your own making.
	Pass an empty string to revoke (unregister) the object.
	Flags:
	One of the following values:
	0 (ACTIVEOBJECT_STRONG)
	1 (ACTIVEOBJECT_WEAK)
	Defaults to 0.

	Related:
	http://goo.gl/KJS4Dp - RegisterActiveObject
	http://goo.gl/no6XAS - ProgID
	http://goo.gl/obfmDc - CreateGUID()
*/
ObjRegisterActive(Object, CLSID, Flags:=0) {
	static cookieJar := {}
	if (!CLSID) {
		if (cookie := cookieJar.Remove(Object)) != ""
			DllCall("oleaut32\RevokeActiveObject", "uint", cookie, "ptr", 0)
		return
	}
	if cookieJar[Object]
		throw Exception("Object is already registered", -1)
	VarSetCapacity(_clsid, 16, 0)
	if (hr := DllCall("ole32\CLSIDFromString", "wstr", CLSID, "ptr", &_clsid)) < 0
		throw Exception("Invalid CLSID", -1, CLSID)
	hr := DllCall("oleaut32\RegisterActiveObject"
        , "ptr", &Object, "ptr", &_clsid, "uint", Flags, "uint*", cookie
        , "uint")
	if hr < 0
		throw Exception(format("Error 0x{:x}", hr), -1)
	cookieJar[Object] := cookie
}

CreateGUID()
{
	VarSetCapacity(pguid, 16, 0)
	if !(DllCall("ole32.dll\CoCreateGuid", "ptr", &pguid)) {
		size := VarSetCapacity(sguid, (38 << !!A_IsUnicode) + 1, 0)
		if (DllCall("ole32.dll\StringFromGUID2", "ptr", &pguid, "ptr", &sguid, "int", size))
			return StrGet(&sguid)
	}
	return ""
}
The script loads without erroring but pressing Ctrl+h throws the error Error: 0x800706BA - The RPC server is unavailable.

I checked A_AhkPath is correctly pointing to my ahk exe file... it is.
I'm not sure where else to look.

Any help getting this simple example working is greatly appreciated! Thanks!
lexikos
Posts: 6653
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: LoadFile - Load script file as a separate process

25 Feb 2018, 03:38

Note:
I wrote:All of the required functions are expected to be installed in a function library.
Do not copy and paste the code into your script.

#include %A_LineFile% is supposed to include LoadFile into the loaded script (say.ahk) in order to turn it into a COM server, but in your case it includes "the file doing the loading" instead.
User avatar
GollyJer
Posts: 59
Joined: 19 Sep 2015, 19:33
GitHub: GollyJer

Re: LoadFile - Load script file as a separate process

26 Feb 2018, 10:17

Got it working. Thanks!

Return to “Scripts and Functions”

Who is online

Users browsing this forum: ahgfaa11, Gully and 22 guests