Skip to content

Instantly share code, notes, and snippets.

@qwerty12
Last active August 15, 2023 00:20
Show Gist options
  • Save qwerty12/5537d1b2153259a8f48a6c8dd60d4c02 to your computer and use it in GitHub Desktop.
Save qwerty12/5537d1b2153259a8f48a6c8dd60d4c02 to your computer and use it in GitHub Desktop.
#NoEnv
#NoTrayIcon
AutoTrim Off
SetBatchLines, -1
ListLines, Off
#SingleInstance ignore
SetWorkingDir %A_ScriptDir%
/*
RegisterSyncCallback (by Lexikos)
A replacement for RegisterCallback for use with APIs that will call
the callback on the wrong thread. Synchronizes with the script's main
thread via a window message.
This version tries to emulate RegisterCallback as much as possible
without using RegisterCallback, so shares most of its limitations,
and some enhancements that could be made are not.
Other differences from v1 RegisterCallback:
- Variadic mode can't be emulated exactly, so is not supported.
- A_EventInfo can't be set in v1, so is not supported.
- Fast mode is not supported (the option is ignored).
- ByRef parameters are allowed (but ByRef is ignored).
- Throws instead of returning "" on failure.
*/
RegisterSyncCallback(FunctionName, Options:="", ParamCount:="") {
if !(fn := Func(FunctionName)) || fn.IsBuiltIn
throw Exception("Bad function", -1, FunctionName)
if (ParamCount == "")
ParamCount := fn.MinParams
if (ParamCount > fn.MaxParams && !fn.IsVariadic || ParamCount+0 < fn.MinParams)
throw Exception("Bad param count", -1, ParamCount)
static sHwnd := 0, sMsg, sSendMessageW
if !sHwnd
{
Gui RegisterSyncCallback: +Parent%A_ScriptHwnd% +hwndsHwnd
OnMessage(sMsg := 0x8000, Func("RegisterSyncCallback_Msg"))
sSendMessageW := DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "user32.dll", "ptr"), "astr", "SendMessageW", "ptr")
}
if !(pcb := DllCall("GlobalAlloc", "uint", 0, "ptr", 96, "ptr"))
throw
DllCall("VirtualProtect", "ptr", pcb, "ptr", 96, "uint", 0x40, "uint*", 0)
p := pcb
if (A_PtrSize = 8)
{
/*
48 89 4c 24 08 ; mov [rsp+8], rcx
48 89 54'24 10 ; mov [rsp+16], rdx
4c 89 44 24 18 ; mov [rsp+24], r8
4c'89 4c 24 20 ; mov [rsp+32], r9
48 83 ec 28' ; sub rsp, 40
4c 8d 44 24 30 ; lea r8, [rsp+48] (arg 3, &params)
49 b9 .. ; mov r9, .. (arg 4, operand to follow)
*/
p := NumPut(0x54894808244c8948, p+0)
p := NumPut(0x4c182444894c1024, p+0)
p := NumPut(0x28ec834820244c89, p+0)
p := NumPut( 0xb9493024448d4c, p+0) - 1
lParamPtr := p, p += 8
p := NumPut(0xba, p+0, "char") ; mov edx, nmsg
p := NumPut(sMsg, p+0, "int")
p := NumPut(0xb9, p+0, "char") ; mov ecx, hwnd
p := NumPut(sHwnd, p+0, "int")
p := NumPut(0xb848, p+0, "short") ; mov rax, SendMessageW
p := NumPut(sSendMessageW, p+0)
/*
ff d0 ; call rax
48 83 c4 28 ; add rsp, 40
c3 ; ret
*/
p := NumPut(0x00c328c48348d0ff, p+0)
}
else ;(A_PtrSize = 4)
{
p := NumPut(0x68, p+0, "char") ; push ... (lParam data)
lParamPtr := p, p += 4
p := NumPut(0x0824448d, p+0, "int") ; lea eax, [esp+8]
p := NumPut(0x50, p+0, "char") ; push eax
p := NumPut(0x68, p+0, "char") ; push nmsg
p := NumPut(sMsg, p+0, "int")
p := NumPut(0x68, p+0, "char") ; push hwnd
p := NumPut(sHwnd, p+0, "int")
p := NumPut(0xb8, p+0, "char") ; mov eax, &SendMessageW
p := NumPut(sSendMessageW, p+0, "int")
p := NumPut(0xd0ff, p+0, "short") ; call eax
p := NumPut(0xc2, p+0, "char") ; ret argsize
p := NumPut((InStr(Options, "C") ? 0 : ParamCount*4), p+0, "short")
}
NumPut(p, lParamPtr+0) ; To be passed as lParam.
p := NumPut(&fn, p+0)
p := NumPut(ParamCount, p+0, "int")
return pcb
}
RegisterSyncCallback_Msg(wParam, lParam) {
if (A_Gui != "RegisterSyncCallback")
return
fn := Object(NumGet(lParam + 0))
paramCount := NumGet(lParam + A_PtrSize, "int")
params := []
Loop % paramCount
params.Push(NumGet(wParam + A_PtrSize * (A_Index-1)))
return %fn%(params*)
}
; ----------------------------------------------------------------------------------------------------------------------
; Name .........: TermWait library
; Description ..: Implement the RegisterWaitForSingleObject Windows API.
; AHK Version ..: AHK_L 1.1.13.01 x32/64 Unicode
; Author .......: Cyruz (http://ciroprincipe.info) & SKAN (http://goo.gl/EpCq0Z)
; License ......: WTFPL - http://www.wtfpl.net/txt/copying/
; Changelog ....: Sep. 15, 2012 - v0.1 - First revision.
; ..............: Jan. 02, 2014 - v0.2 - AHK_L Unicode and x64 version.
; ----------------------------------------------------------------------------------------------------------------------
; ----------------------------------------------------------------------------------------------------------------------
; Function .....: TermWait_WaitForProcTerm
; Description ..: This function initializes a global structure and start an asynchrounous thread to wait for program
; ..............: termination. The global structure is used to pass arbitrary data at offset 24/36. Offsets are:
; ..............: < +0 = hWnd | +4/+8 = nMsgId | +8/+12 = nPid | +12/+16 = hProc | +16/+24 = hWait | +20/+32 = sDataIn >
; Parameters ...: hWnd - Handle of the window that will receive the notification.
; ..............: nMsgId - Generic message ID (msg).
; ..............: nPid - PID of the process that needs to be waited for.
; ..............: sDataIn - Arbitrary data (use this to pass any data in string form).
; ..............: getExitCode - allow your notification function to get the exit code of the process (if nPidIsHandle is true, however, then the handle must have the required rights)
; ..............: nPidIsHandle - treats the nPid parameter as if it is a valid process handle instead (WARNING: TermWait_StopWaiting will always close the handle. You should duplicate it yourself beforehand if needed.)
; Return .......: On success, address of global allocated structure. 0 on failure. If you passed a handle, it will not be closed on failure.
; ----------------------------------------------------------------------------------------------------------------------
TermWait_WaitForProcTerm(hWnd, nMsgId, nPid, ByRef sDataIn:="", getExitCode := False, nPidIsHandle := False) {
static addrCallback := 0
if (!addrCallback)
{
fnRegisterSyncCallback := Func("RegisterSyncCallback")
if (fnRegisterSyncCallback)
addrCallback := fnRegisterSyncCallback.Call("__TermWait_TermNotifier")
else
addrCallback := RegisterCallback("__TermWait_TermNotifier")
}
if (nPid < 1)
return 0
if (!nPidIsHandle) {
procOpenFlags := 0x00100000 ; SYNCHRONIZE
if (getExitCode) {
if A_OSVersion in WIN_2003,WIN_XP,WIN_2000
procOpenFlags |= 0x0400 ; PROCESS_QUERY_INFORMATION
else
procOpenFlags |= 0x1000 ; PROCESS_QUERY_LIMITED_INFORMATION
}
hProc := DllCall("OpenProcess", "UInt", procOpenFlags, "Int", False, "UInt", nPid, "Ptr")
if (!hProc)
return 0
} else {
hProc := nPid
}
szDataIn := VarSetCapacity(sDataIn)
pGlobal := DllCall("GlobalAlloc", "UInt", 0x0040, "UInt", (A_PtrSize == 8 ? 32 : 20) + szDataIn, "Ptr")
NumPut(hWnd, pGlobal+0,, "Ptr")
,NumPut(nMsgId, pGlobal+0, A_PtrSize, "UInt")
if (!nPidIsHandle)
NumPut(nPid, pGlobal+0, A_PtrSize == 8 ? 12 : 8, "UInt")
NumPut(hProc, pGlobal+0, A_PtrSize == 8 ? 16 : 12, "Ptr")
DllCall("RtlMoveMemory", "Ptr", pGlobal+(A_PtrSize == 8 ? 32 : 20), "Ptr", &sDataIn, "Ptr", szDataIn)
if (!DllCall("RegisterWaitForSingleObject", "Ptr", pGlobal+(A_PtrSize == 8 ? 24 : 16), "Ptr", hProc, "Ptr", addrCallback
, "Ptr", pGlobal, "UInt", 0xFFFFFFFF, "UInt", 0x00000004 | 0x00000008)) { ; INFINITE, WT_EXECUTEINWAITTHREAD | WT_EXECUTEONLYONCE
TermWait_StopWaiting(pGlobal, True)
return 0
}
return pGlobal
}
; ----------------------------------------------------------------------------------------------------------------------
; Function .....: TermWait_StopWaiting
; Description ..: This function cleans all handles and frees global allocated memory.
; Parameters ...: pGlobal - Global structure address.
; ----------------------------------------------------------------------------------------------------------------------
TermWait_StopWaiting(pGlobal, justFreepGlobal := False) {
if (pGlobal) {
if (!justFreepGlobal) {
DllCall("UnregisterWait", "Ptr", NumGet(pGlobal+0, A_PtrSize == 8 ? 24 : 16, "Ptr"))
,DllCall("CloseHandle", "Ptr", NumGet(pGlobal+0, A_PtrSize == 8 ? 16 : 12))
}
DllCall("GlobalFree", "Ptr", pGlobal, "Ptr")
}
}
; ----------------------------------------------------------------------------------------------------------------------
; Function .....: __TermWait_TermNotifier
; Description ..: This callback is called when a monitored process signal its closure. It gets executed on a different
; ..............: thread because of the RegisterWaitForSingleObject, so it could interferee with the normal AutoHotkey
; ..............: behaviour (eg. it's not bug free).
; Parameters ...: pGlobal - Global structure.
; ----------------------------------------------------------------------------------------------------------------------
__TermWait_TermNotifier(pGlobal) {
Critical 1000
DllCall("SendNotifyMessage",Ptr,NumGet(pGlobal+0,"Ptr"),UInt,NumGet((A_PtrSize==4)?pGlobal+4:pGlobal+8,"UInt"),UInt,0,Ptr,pGlobal)
Critical Off
}
; By cocobelgica
Jxon_Load(ByRef src, args*) {
static q := Chr(34)
key := "", is_key := false
stack := [ tree := [] ]
is_arr := { (tree): 1 }
next := q . "{[01234567890-tfn"
pos := 0
while ( (ch := SubStr(src, ++pos, 1)) != "" )
{
if InStr(" `t`n`r", ch)
continue
if !InStr(next, ch, true)
{
ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
msg := Format("{}: line {} col {} (char {})"
, (next == "") ? ["Extra data", ch := SubStr(src, pos)][1]
: (next == "'") ? "Unterminated string starting at"
: (next == "\") ? "Invalid \escape"
: (next == ":") ? "Expecting ':' delimiter"
: (next == q) ? "Expecting object key enclosed in double quotes"
: (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
: (next == ",}") ? "Expecting ',' delimiter or object closing '}'"
: (next == ",]") ? "Expecting ',' delimiter or array closing ']'"
: [ "Expecting JSON value(string, number, [true, false, null], object or array)"
, ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
, ln, col, pos)
throw Exception(msg, -1, ch)
}
is_array := is_arr[obj := stack[1]]
if i := InStr("{[", ch)
{
val := (proto := args[i]) ? new proto : {}
is_array? ObjPush(obj, val) : obj[key] := val
ObjInsertAt(stack, 1, val)
is_arr[val] := !(is_key := ch == "{")
next := q . (is_key ? "}" : "{[]0123456789-tfn")
}
else if InStr("}]", ch)
{
ObjRemoveAt(stack, 1)
next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
}
else if InStr(",:", ch)
{
is_key := (!is_array && ch == ",")
next := is_key ? q : q . "{[0123456789-tfn"
}
else ; string | number | true | false | null
{
if (ch == q) ; string
{
i := pos
while i := InStr(src, q,, i+1)
{
val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
static end := A_AhkVersion<"2" ? 0 : -1
if (SubStr(val, end) != "\")
break
}
if !i ? (pos--, next := "'") : 0
continue
pos := i ; update pos
val := StrReplace(val, "\/", "/")
, val := StrReplace(val, "\" . q, q)
, val := StrReplace(val, "\b", "`b")
, val := StrReplace(val, "\f", "`f")
, val := StrReplace(val, "\n", "`n")
, val := StrReplace(val, "\r", "`r")
, val := StrReplace(val, "\t", "`t")
i := 0
while i := InStr(val, "\",, i+1)
{
if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
continue 2
; \uXXXX - JSON unicode escape sequence
xxxx := Abs("0x" . SubStr(val, i+2, 4))
if (A_IsUnicode || xxxx < 0x100)
val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
}
if is_key
{
key := val, next := ":"
continue
}
}
else ; number | true | false | null
{
val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
; For numerical values, numerify integers and keep floats as is.
; I'm not yet sure if I should numerify floats in v2.0-a ...
static number := "number", integer := "integer"
if val is %number%
{
if val is %integer%
val += 0
}
; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
; SOMETIMES return strings due to certain optimizations. Since it
; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
else if (val == "true" || val == "false")
val := %val% + 0
; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
; as it would raise an exception in AHK_H(overriding built-in var)
else if (val == "null")
val := ""
; any other values are invalid, continue to trigger error
else if (pos--, next := "#")
continue
pos += i-1
}
is_array? ObjPush(obj, val) : obj[key] := val
next := obj==tree ? "" : is_array ? ",]" : ",}"
}
}
return tree[1]
}
_PROCESS_INFORMATION(ByRef pi) {
static piCb := A_PtrSize == 8 ? 24 : 16
if (IsByRef(pi))
VarSetCapacity(pi, piCb, 0)
}
_PROCESS_INFORMATION_hProcess(ByRef pi) {
return NumGet(pi,, "Ptr")
}
_PROCESS_INFORMATION_hThread(ByRef pi) {
return NumGet(pi, A_PtrSize, "Ptr")
}
cbStartupInfoEx := A_PtrSize == 8 ? 112 : 72
_STARTUPINFOEX(ByRef si) {
global cbStartupInfoEx
if (IsByRef(si))
VarSetCapacity(si, cbStartupInfoEx, 0), NumPut(cbStartupInfoEx, si,, "UInt")
}
CloseHandle(hObject) {
static INVALID_HANDLE_VALUE := -1
return (hObject && hObject != INVALID_HANDLE_VALUE) ? DllCall("kernel32.dll\CloseHandle", "Ptr", hObject) : False
}
GetCurrentProcess() {
static hProc := DllCall("GetCurrentProcess", "Ptr") ; always -1
return hProc
}
GetParentProcessID() {
; Undocumented but far easier than CreateToolhelp32Snapshot
VarSetCapacity(PROCESS_BASIC_INFORMATION, pbiSz := A_PtrSize == 8 ? 48 : 24)
if (DllCall("ntdll.dll\NtQueryInformationProcess", "Ptr", GetCurrentProcess(), "UInt", 0, "Ptr", &PROCESS_BASIC_INFORMATION, "UInt", pbiSz, "Ptr", 0) >= 0)
return NumGet(PROCESS_BASIC_INFORMATION, pbiSz - A_PtrSize, "UInt")
return 0
}
GetOverseerVersion() {
FileRead, package, package.json
package := Jxon_Load(package)
return package["version"]
}
DeleteFolder(target) {
RunWait, byenow -y -n -x "%target%",, Hide UseErrorLevel
RunWait, byenow -y -x "%target%",, Hide UseErrorLevel
FileRemoveDir, % target, 1
}
; VersionCompare function by boiler at ahkscript.org
; Compares versions where simple string comparison can fail, such as 9.1.3.2 and 10.1.3.5
; Both version numbers are in format n1[.n2.n3.n4...] where each n can be any number of digits.
; Fills in zeros for missing sections for purposes of comparison (e.g., comparing 9 to 8.1).
; Not limited to 4 sections. Can handle 5.3.2.1.6.19.6 (and so on) if needed.
; Returns 1 if version1 is more recent, 2 if version 2 is more recent, 0 if they are the same.
VersionCompare(version1, version2) {
StringSplit, verA, version1, .
StringSplit, verB, version2, .
Loop, % (verA0> verB0 ? verA0 : verB0)
{
if (verA0 < A_Index)
verA%A_Index% := "0"
if (verB0 < A_Index)
verB%A_Index% := "0"
if (verA%A_Index% > verB%A_Index%)
return 1
if (verB%A_Index% > verA%A_Index%)
return 2
}
return 0
}
SearchPathW(FileName, Extension)
{
static buf
if (!VarSetCapacity(buf))
VarSetCapacity(buf, (260 + 1) * 2)
written := DllCall("kernel32.dll\SearchPathW", "Ptr", 0, "WStr", FileName, "WStr", Extension, "UInt", 260, "Ptr", &buf, "Ptr", 0)
if (written == 0 || written > 260)
return 0
return StrGet(&buf, written, "UTF-16")
}
Try Menu, Tray, Icon, %A_ScriptDir%\public\favicon.ico
hProcess := 0
ExitFunc()
{
global hProcess
OnExit("ExitFunc", 0)
if (hProcess) {
shouldKill := True
dwProcessId := DllCall("kernel32.dll\GetProcessId", "Ptr", hProcess)
DllCall("SetConsoleCtrlHandler", "Ptr", 0, "Int", True)
if (DllCall("AttachConsole", "UInt", dwProcessId)) {
generated := DllCall("GenerateConsoleCtrlEvent", "UInt", 0, "UInt", 0)
,DllCall("FreeConsole")
if (generated)
shouldKill := DllCall("WaitForSingleObject", "Ptr", hProcess, "UInt", 3000, "UInt") != 0
}
DllCall("SetConsoleCtrlHandler", "Ptr", 0, "Int", False)
if (shouldKill)
DllCall("TerminateProcess", "Ptr", hProcess, "UInt", 0)
}
}
MSGID := 0x8500 ; msg
AHK_TERMNOTIFY(wParam, lParam) {
global hProcess
OnExit("ExitFunc", 0)
TermWait_StopWaiting(lParam)
;CloseHandle(hProcess)
hProcess := 0
ExitApp
}
EnvSet, CLINK_NOAUTORUN, 1
EnvSet, NEXT_TELEMETRY_DISABLED, 1
EnvSet, CYPRESS_INSTALL_BINARY, 0
DllCall("GetProcessAffinityMask", "Ptr", GetCurrentProcess(), "UPtr*", ProcessAffinityMask, "UPtr*", SystemAffinityMask)
,VarSetCapacity(SYSTEM_INFO, 24 + A_PtrSize*3)
,DllCall("GetSystemInfo", "Ptr", &SYSTEM_INFO)
,coresNumber := NumGet(SYSTEM_INFO, 8 + A_PtrSize*3, "UInt")
,lastTwoCoresAffinity := (1 << (coresNumber - 2)) | (1 << (coresNumber - 1))
,DllCall("SetProcessAffinityMask", "Ptr", GetCurrentProcess(), "UPtr", lastTwoCoresAffinity)
,DllCall("SetPriorityClass", "Ptr", GetCurrentProcess(), "UInt", PROCESS_MODE_BACKGROUND_BEGIN := 0x00100000)
EnvGet, NVM_SYMLINK, NVM_SYMLINK
if (!FileExist(NVM_SYMLINK . "\node_modules\win-node-env")) {
npm := SearchPathW("npm", ".cmd")
RunWait, %npm% i -g win-node-env,, Hide
}
previousVersion := GetOverseerVersion()
RunWait, git pull,, Hide
if (ErrorLevel == 0) {
newVersion := GetOverseerVersion()
if (previousVersion && newVersion && VersionCompare(previousVersion, newVersion) == 2) {
DeleteFolder(A_ScriptDir . "\dist")
;,DeleteFolder(A_ScriptDir . "\node_modules")
}
if (!FileExist("dist")) {
yarn := SearchPathW("yarn", ".cmd")
RunWait, %yarn% install --frozen-lockfile --prefer-offline,, Hide
RunWait, %yarn% build,, Hide
}
}
DllCall("SetPriorityClass", "Ptr", GetCurrentProcess(), "UInt", PROCESS_MODE_BACKGROUND_END := 0x00200000)
,DllCall("SetProcessAffinityMask", "Ptr", GetCurrentProcess(), "UPtr", ProcessAffinityMask)
_PROCESS_INFORMATION(pi)
,_STARTUPINFOEX(si)
,dwCreationFlags := CREATE_NO_WINDOW := 0x08000000
/*
if ((parentPid := GetParentProcessID())) {
if ((hParentProcess := DllCall("OpenProcess", "UInt", PROCESS_CREATE_PROCESS := 0x0080, "Int", False, "UInt", parentPid, "Ptr"))) {
DllCall("InitializeProcThreadAttributeList", "Ptr", 0, "UInt", 1, "UInt", 0, "Ptr*", size)
if (size) {
VarSetCapacity(AttributeList, size + A_PtrSize)
if (DllCall("InitializeProcThreadAttributeList", "Ptr", &AttributeList, "UInt", 1, "UInt", 0, "Ptr*", size)) {
NumPut(hParentProcess, AttributeList, size, "Ptr")
if (DllCall("UpdateProcThreadAttribute", "Ptr", &AttributeList, "UInt", 0, "UPtr", PROC_THREAD_ATTRIBUTE_PARENT_PROCESS := 0x00020000, "Ptr", &AttributeList+size, "Ptr", A_PtrSize, "Ptr", 0, "Ptr", 0)) {
NumPut(&AttributeList, si, cbStartupInfoEx - A_PtrSize, "Ptr")
,dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT := 0x00080000
}
} else {
VarSetCapacity(AttributeList, 0)
}
}
}
}
*/
EnvSet, NODE_ENV, production
if (!(exe := SearchPathW("node", ".exe")))
ExitApp 1
cmd := """" . exe . """" . " dist\index.js"
if (DllCall("CreateProcessW", "WStr", exe, "WStr", cmd, "Ptr", 0, "Ptr", 0, "Int", False, "UInt", dwCreationFlags, "Ptr", 0, "Ptr", 0, "Ptr", &si, "Ptr", &pi)) {
hProcess := _PROCESS_INFORMATION_hProcess(pi)
,DllCall("SetProcessAffinityMask", "Ptr", hProcess, "UPtr", lastTwoCoresAffinity)
,DllCall("SetPriorityClass", "Ptr", hProcess, "UInt", BELOW_NORMAL_PRIORITY_CLASS := 0x00004000)
,DllCall("ntdll.dll\NtSetInformationProcess", "Ptr", hProcess, "UInt", ProcessIoPriority := 33, "UInt*", 1, "UInt", 4)
CloseHandle(_PROCESS_INFORMATION_hThread(pi))
;,CloseHandle(hProcess)
OnExit("ExitFunc")
OnMessage(MSGID, "AHK_TERMNOTIFY")
TermWait_WaitForProcTerm(A_ScriptHwnd, MSGID, hProcess,,, True)
if (VarSetCapacity(AttributeList))
DllCall("DeleteProcThreadAttributeList", "Ptr", &AttributeList)
CloseHandle(hParentProcess)
} else {
ExitApp, 1
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment