LoadFile - Load script file as a separate process

Post your working scripts, libraries and tools for AHK v1.1 and older
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

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

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).
    
    Version: 1.1
*/

#Requires AutoHotkey v1.1.35+

LoadFile(path, exe:="", exception_level:=-1) {
    exe := """" (exe="" ? A_AhkPath : exe) """"
    exec := ComObjCreate("WScript.Shell")
        .Exec(exe " /ErrorStdOut /include """ A_LineFile """ """ path """")
    exec.StdIn.Close()
    err := exec.StdErr.ReadAll()
    if SubStr(err, 1, 8) = "LRESULT=" {
        hr := DllCall("oleacc\ObjectFromLresult", "ptr", SubStr(err, 9), "ptr", LoadFile.IID, "ptr", 0, "ptr*", pobj:=0)
        if hr >= 0
            return ComObj(9, pobj, 1)
        err := Format("ObjectFromLresult returned failure (0x{:x})", hr & 0xffffffff)
    }
    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]
    else
        ex.Message .= "`n`nReason:`t" err
    throw ex
}

class LoadFile {
    Init() {
        static IID, _ := LoadFile.Init()
        VarSetCapacity(IID, 16), this.IID := &IID
        NumPut(0x46000000000000c0, NumPut(0x20400, IID, "int64"), "int64") ; IID_IDispatch
        if InStr(DllCall("GetCommandLine", "str"), " /include """ A_LineFile """ ")
            this.Serve()
    }
    Serve() {
        stderr := FileOpen("**", "w")
        try {
            proxy := new this.Proxy
            lResult := DllCall("oleacc\LresultFromObject", "ptr", this.IID, "ptr", 0, "ptr", &proxy, "ptr")
            if lResult < 0
                throw Exception(Format("LresultFromObject returned failure (0x{:x})", lResult))
            stderr.Write("LRESULT=" lResult)
            DllCall("CloseHandle", "ptr", stderr := stderr.__Handle)  ; Flush buffer and end ReadAll().
        }
        catch ex {
            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, LoadFile:%A_ScriptHwnd%
        Hotkey vk07, #Persistent, Off
        Hotkey IfWinActive
        #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: See ObjRegisterActive for some limitations which apply to all scripts using COM for IPC.


Version 1.0 of the script was mostly intended as an example for ObjRegisterActive, but might be useful in its own right. It can be used on AutoHotkey v1.1.17+.
Version 1.0


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"]
Last edited by lexikos on 23 Dec 2022, 02:45, edited 1 time in total.
Reason: Update to v1.1; requires AutoHotkey v1.1.35+
User avatar
Learning one
Posts: 173
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: 265
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: 9560
Joined: 30 Sep 2013, 04:07
Contact:

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: 9560
Joined: 30 Sep 2013, 04:07
Contact:

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: 9560
Joined: 30 Sep 2013, 04:07
Contact:

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: 62
Joined: 19 Sep 2015, 19:33
Contact:

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: 9560
Joined: 30 Sep 2013, 04:07
Contact:

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.
Vaggeto
Posts: 24
Joined: 14 Dec 2020, 23:52

Re: LoadFile - Load script file as a separate process

18 Dec 2020, 00:51

Hello! This is great but I was wondering if it can kick of the child process, but then leave it orphaned without waiting for anything to be returned?

Essentially I want to trigger multiple files concurrently, but this will wait for each file to be completed before continuing to launch the next file.
User avatar
Delta Pythagorean
Posts: 627
Joined: 13 Feb 2017, 13:44
Location: Somewhere in the US
Contact:

Re: LoadFile - Load script file as a separate process

20 Dec 2020, 21:37

Is there a possible way to dynamically call a script's method? I've tried using Func and ObjBindMethod and neither seem to work.
Edit: Nevermind, I made a mistake and added too many parameters to the function call when the function itself doesn't allow for that many parameters. ObjBindMethod worked just fine.

[AHK]......: v2.0.12 | 64-bit
[OS].......: Windows 11 | 23H2 (OS Build: 22621.3296)
[GITHUB]...: github.com/DelPyth
[PAYPAL]...: paypal.me/DelPyth
[DISCORD]..: tophatcat

pv007
Posts: 93
Joined: 20 Jul 2020, 23:50

Re: LoadFile - Load script file as a separate process

21 Feb 2021, 12:08

Does it did not work with compiled script?

Testing case:

test2.ahk

Code: Select all

Hi() {
	MsgBox, "hi!"
}
I compiled test3.ahk to test3.exe with the following code:

Code: Select all

#SingleInstance force
#NoEnv

Say := LoadFile("C:\Users\test2.ahk")
return

F1::
Say.Hi()
When i run test3.exe it throw this messagebox:
image.png
image.png (7.73 KiB) Viewed 14332 times
I have the Lib at C:\Users\Documents\AutoHotkey\Lib

Also i confirmed using RH that the code at least is installed in the assembly:
image.png
image.png (125.61 KiB) Viewed 14332 times
Obs: I test running with/without admin privileges.
User avatar
Delta Pythagorean
Posts: 627
Joined: 13 Feb 2017, 13:44
Location: Somewhere in the US
Contact:

Re: LoadFile - Load script file as a separate process

23 Feb 2021, 12:09

Well, you can do it but you have to mess with the LoadFile function to use an external AHK executable for the code to be parsed and ran.

[AHK]......: v2.0.12 | 64-bit
[OS].......: Windows 11 | 23H2 (OS Build: 22621.3296)
[GITHUB]...: github.com/DelPyth
[PAYPAL]...: paypal.me/DelPyth
[DISCORD]..: tophatcat

jsong55
Posts: 227
Joined: 30 Mar 2021, 22:02

Re: LoadFile - Load script file as a separate process

21 May 2021, 02:26

Does this sort of create "multi threading?"

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 180 guests