1

Тема: AHK: Tiny Master Script

Ищу мастер скрипт для управления запущенными скриптами. Перебрал найденные на форуме варианты - выходят ошибки о недостающих элементах и некорректно отображаются. Вероятно, многие здесь имеют у себя отлаженные образцы менеджеров скриптов. Прошу поделиться. В идеале, это трей-меню с отображением всех запущенных скриптов с возможностью их завершения/заморозки без дополнительных окон с вкладками и подробной информацией. Ну или какой есть.

2

Re: AHK: Tiny Master Script

Наверное ещё стоит уточнить, требуется управление ahk-скриптами или скомпилированным exe-скриптами.

3

Re: AHK: Tiny Master Script

Вот, слепил из того, что было:

#NoEnv
SetBatchLines, -1
Menu, Tray, NoStandard
DetectHiddenWindows, On
OnMessage(0x404, "AHK_NOTIFYICON")
Return

AHK_NOTIFYICON(wp, lp) {
   static WM_MOUSEMOVE := 0x200, WM_RBUTTONUP := 0x205, info
        , IsData := [], timer := Func("IsDataClear").Bind(IsData)
   if (lp = WM_MOUSEMOVE) {
      Critical
      if !IsData[1] {
         info := GetAhkScriptsInfo()
         IsData[1] := true
         SetTimer, % timer, -1000
      }
      Critical Off
   }
   if (lp = WM_RBUTTONUP) {
      names := {}
      for k, v in info {
         if names.HasKey(v.name) {
            i := 0
            Loop
               name := v.name . " (" . ++i . ")"
            until !names.HasKey(name)
            v.name := name
         }
         names[v.name] := ""
      }
      for name in names {
         for k, v in info {
            if (v.name != name)
               continue
            CreateSubMenu(k, v)
            item := v.name
            if v.suspended
               item .= A_Tab . "Suspended"
            else if v.paused
               item .= A_Tab . "Paused"
            Menu, MyMenu, Add, % item, :SubMenu%k%
         }
      }
      Menu, MyMenu, Add
      Menu, MyMenu, Add, Exit, ExitScript
      Menu, MyMenu, Show
      Loop % info.MaxIndex()
         Menu, SubMenu%A_Index%, DeleteAll
      Menu, MyMenu, DeleteAll
      IsData[1] := false
   }
}

IsDataClear(IsData) {
   IsData[1] := false
}

GetAhkScriptsInfo() {
   static Waiting := 5, Suspended := 5
        , MyPID := DllCall("GetCurrentProcessId")
   info := [], PIDs := {}
   WinGet, List, List, ahk_class AutoHotkey
   Loop % List {
      WinExist("ahk_id" List%A_Index%)
      threadID := DllCall("GetWindowThreadProcessId", "Ptr", List%A_Index%, "UIntP", PID)
      if (PID = MyPID)
         continue
      PIDs[PID] := threadID
      WinGetTitle, title
      scriptPath := A_IsCompiled ? title : RegExReplace(title, " - AutoHotkey v[\d\.]+")
      SplitPath, scriptPath, fileName
      info.Push({ scriptPath: scriptPath, name: fileName, PID: PID, hwnd: List%A_Index%, suspended: false, bitness: GetProcessBitness(PID) })
   }
   ThreadsInfo(PIDs)
   for k, v in info {
      v.threads := PIDs[ v.PID ]
      for k, j in PIDs[ v.PID ] {
         if !(j.ThreadState = Waiting && j.WaitReason = Suspended)
            continue 2
         v.suspended := true
      }
   }
   for k, v in info
      if !v.suspended
         v.paused := IsPaused(v.hwnd)
   Return info
}

GetProcessBitness(PID) {
   if !A_Is64bitOS
      Return 32
   hProc := DllCall("OpenProcess", "UInt", PROCESS_QUERY_INFORMATION := 0x400, "UInt", 0, "UInt", PID, "Ptr")
   DllCall("IsWow64Process", "Ptr", hProc, "IntP", is32)
   DllCall("CloseHandle", "Ptr", hProc)
   Return 32 << !is32
}

ThreadsInfo(PIDs) {
   static SPI := SystemProcessInformation := 0x5, STATUS_SUCCESS := 0
        , szSPI := A_PtrSize = 4 ? 0xB8 : 0x100  ; size of SYSTEM_PROCESS_INFORMATION
        , szSTI := A_PtrSize = 4 ? 0x40 : 0x050  ; size of SYSTEM_THREAD_INFORMATION
        
   DllCall("Ntdll\NtQuerySystemInformation", "Int", SPI, "Ptr", 0, "UInt", 0, "UIntP", size, "UInt")
   VarSetCapacity(buff, size, 0)
   addr := &buff
   
   res := DllCall("Ntdll\NtQuerySystemInformation", "Int", SPI, "Ptr", addr, "UInt", size, "UIntP", 0, "UInt")
   if (res != STATUS_SUCCESS)
      throw Exception( "Could not get data from NtQuerySystemInformation. Error: " . Format("{:#x}", res) )
   offset := 0
   Loop {
      addr += offset
      ProcessID := NumGet(addr + 4*14 + A_PtrSize*3)
      if !PIDs.HasKey(ProcessID)
         continue
      threadsCount := NumGet(addr + 4, "UInt")
      mainThreadID := PIDs[ProcessID]
      PIDs[ProcessID] := []
      Loop % threadsCount {
         ThreadID := NumGet(addr + szSPI + 24 + A_PtrSize*3 + (A_Index - 1)*szSTI)
         if (ThreadID != mainThreadID)
            continue
         ThreadState := NumGet(addr + szSPI + 36 + A_PtrSize*4 + (A_Index - 1)*szSTI, "UInt")
         WaitReason  := NumGet(addr + szSPI + 40 + A_PtrSize*4 + (A_Index - 1)*szSTI, "UInt")
         PIDs[ProcessID].Push( {ThreadID: ThreadID, ThreadState: ThreadState, WaitReason: WaitReason} )
      }
   } until !offset := NumGet(addr + 0, "UInt")
}

CreateSubMenu(k, info) {
   static MyBitness := GetProcessBitness( DllCall("GetCurrentProcessId") )
   if !(MyBitness = 32 && info.bitness = 64) && !(info.name ~= "i)\.exe( \(\d+\))?$") {
      handler := Func("SuspendResumeThreads").Bind(info, k)
      Menu, SubMenu%k%, Add, Suspend Script, % handler
   }
   if info.suspended
      Menu, SubMenu%k%, Check, Suspend Script
   else {
      handler := Func("SelectMenuItem").Bind(info.hwnd, k, 5)
      Menu, SubMenu%k%, Add, Pause Script, % handler
      if info.paused
         Menu, SubMenu%k%, Check, Pause Script
      handler := Func("SelectMenuItem").Bind(info.hwnd, k, 8)
      Menu, SubMenu%k%, Add, Exit (Terminate Script), % handler
   }
   handler := Func("KillScript").Bind(info.PID)
   Menu, SubMenu%k%, Add, Kill Script, % handler
}

SelectMenuItem(hwnd, k, nMenu) {
   WinMenuSelectItem, ahk_id %hWnd%,, 1&, % nMenu . "&"
}

SuspendResumeThreads(info, k) {
   static MyBitness := GetProcessBitness( DllCall("GetCurrentProcessId") )
        , THREAD_SUSPEND_RESUME := 0x2
   for k, v in info.threads {
      hThread := DllCall("OpenThread", "UInt", THREAD_SUSPEND_RESUME, "UInt", false, "UInt", v.ThreadID, "Ptr")
      if info.suspended
         DllCall("ResumeThread", "Ptr", hThread)
      else {
         if (MyBitness = info.bitness)
            DllCall("SuspendThread", "Ptr", hThread)
         else
            DllCall("Wow64SuspendThread", "Ptr", hThread)
      }
      DllCall("CloseHandle", "Ptr", hThread)
   }
}

KillScript(PID) {
   Process, Close, % PID
   Process, WaitClose, % PID
   DeleteDummyIcons()
}

IsPaused(hWnd) {
   static WM_ENTERMENULOOP := 0x211, WM_EXITMENULOOP := 0x212, MF_BYPOSITION := 0x400, MF_CHECKED := 8
        , Params := ["Ptr", 0, "Ptr", 0, "UInt", SMTO_ABORTIFHUNG := 0x2|SMTO_ERRORONEXIT := 0x20, "UInt", 500, "Ptr", 0]
   Loop 2
      if !DllCall("SendMessageTimeout", Ptr, hWnd, UInt, A_Index = 1 ? WM_ENTERMENULOOP : WM_EXITMENULOOP, Params*)
         Return
   hWinMenu := DllCall("GetMenu", Ptr, hWnd, Ptr)
   hFileMenu := DllCall("GetSubMenu", Ptr, hWinMenu, Int, 0, Ptr)
   Return DllCall("GetMenuState", Ptr, hFileMenu, UInt, 4, UInt, MF_BYPOSITION) = MF_CHECKED   ; Pause state
}

ExitScript() {
   ExitApp
}

DeleteDummyIcons()
{
   static PtrSize := A_Is64bitOS ? 8 : 4, WM_USER := 0x400
        , TB_BUTTONCOUNT := WM_USER + 24, TB_GETBUTTON := WM_USER + 23
        , szTBBUTTON := 8 + PtrSize*3, szTRAYDATA := 16 + PtrSize*2
   
   DHW_Prev := A_DetectHiddenWindows
   DetectHiddenWindows, On
   ControlGet, hTray, hwnd,, ToolbarWindow321, ahk_class Shell_TrayWnd
   ControlGet, hOverflow, hwnd,, ToolbarWindow321, ahk_class NotifyIconOverflowWindow
   WinGet, PID, PID, ahk_id %hTray%

   RemoteBuff := New RemoteBuffer(PID, szTRAYDATA)
   Dummy := []
   Loop 2
   {
      hwnd := A_Index = 1 ? hTray : hOverflow
      SendMessage, TB_BUTTONCOUNT,,,, ahk_id %hwnd%
      Loop % ErrorLevel
      {
         SendMessage, TB_GETBUTTON, A_Index - 1, RemoteBuff.ptr,, ahk_id %hwnd%
         RemoteBuff.Read(TBBUTTON, szTBBUTTON)
         
         RemoteBuff.Read(TRAYDATA, szTRAYDATA, NumGet(&TBBUTTON + 8 + PtrSize) - RemoteBuff.ptr)
         hWndIcon := NumGet(TRAYDATA), uID := NumGet(&TRAYDATA + PtrSize, "UInt")
         
         if !WinExist("ahk_id" hWndIcon)
            Dummy.Insert({hWnd: hWndIcon, uID: uID})
      }
   }
   DetectHiddenWindows, %DHW_Prev%
   for k, v in Dummy
      RemoveTrayIcon(v.hWnd, v.uID)
}

RemoveTrayIcon(hWnd, uID)
{
   VarSetCapacity(NOTIFYICONDATA, size := A_PtrSize = 8 ? 848 : A_IsUnicode? 828 : 444, 0)
   NumPut(size, NOTIFYICONDATA, "UInt")
   NumPut(hWnd, NOTIFYICONDATA, A_PtrSize)
   NumPut(uID, NOTIFYICONDATA, 2*A_PtrSize)
   DllCall("shell32\Shell_NotifyIcon", "UInt", NIM_DELETE := 2, Ptr, &NOTIFYICONDATA)
   Return
}

class RemoteBuffer
{
   __New(PID, size) {
      static flags := (PROCESS_VM_OPERATION := 0x8) | (PROCESS_VM_WRITE := 0x20) | (PROCESS_VM_READ := 0x10)
           , Params := ["UInt", MEM_COMMIT := 0x1000, "UInt", PAGE_READWRITE := 0x4, "Ptr"]
         
      if !this.hProc := DllCall("OpenProcess", "UInt", flags, "Int", 0, "UInt", PID, "Ptr")
         throw Exception("Can't open remote process PID = " . PID . "`nA_LastError: " . A_LastError, "RemoteBuffer.__New")
      
      if !this.ptr := DllCall("VirtualAllocEx", "Ptr", this.hProc, "Ptr", 0, "Ptr", size, Params*) {
         DllCall("CloseHandle", "Ptr", this.hProc)
         throw Exception("Can't allocate memory in remote process PID = " . PID . "`nA_LastError: " . A_LastError, "RemoteBuffer.__New")
      }
   }
   
   __Delete() {
      DllCall("VirtualFreeEx", "Ptr", this.hProc, "Ptr", this.ptr, "UInt", 0, "UInt", MEM_RELEASE := 0x8000)
      DllCall("CloseHandle", "Ptr", this.hProc)
   }
   
   Read(ByRef localBuff, size, offset = 0) {
      VarSetCapacity(localBuff, size, 0)
      if !DllCall("ReadProcessMemory", "Ptr", this.hProc, "Ptr", this.ptr + offset, "Ptr", &localBuff, "Ptr", size, "PtrP", bytesRead)
         throw Exception("Can't read data from remote buffer`nA_LastError: " . A_LastError, "RemoteBuffer.Read")
      Return bytesRead
   }
   
   Write(pData, size, offset = 0) {
      if !res := DllCall("WriteProcessMemory", "Ptr", this.hProc, "Ptr", this.ptr + offset, "Ptr", pData, "Ptr", size, "PtrP", bytesWritten)
         throw Exception("Can't write data to remote buffer`nA_LastError: " . A_LastError, "RemoteBuffer.Write")
      Return bytesWritten
   }
}

И для скомпилированных, и для обычных.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

4

Re: AHK: Tiny Master Script

teadrinker, круто! То, что нужно! Только не понял, в чем отличие kill от exit.

Понял, что не хватает пункта Edit и чекбокса для StartUp внутри управления на ряду с Suspend, Pause.., а так же некого watch folder, т.е. чтобы скрипты из директории рядом с мастер-скриптом автоматически отображались в списке трей-меню. А чтобы отличить, какие из списка запущены, а какие просто отображаются - добавить чекбоксы, т.е. его наличие отражает активность скрипта. Иными словами, получить возможность еще и быстро запускать скрипты из watch folder.

Другой момент - не знаю, насколько это возможно - вкладывать иконки запущенных скриптов из трея, т.е. не отображать их отдельно, а только в списке мастер-скрипта. Понимаю, что это можно решить c помощью #NoTrayIcon для каждого скрипта, но часто в списке могут оказаться тестовые одноразовые скрипты, да и скрипты иногда можно запускать отдельно от мастер скрипта, и тогда ему уже нужна иконка в трее.

5

Re: AHK: Tiny Master Script

Kill — это примерно как компьютер выключить выдёргиванием шнура из розетки.
Там много чего не хватает, это на данный момент скорее заготовка, чем рабочий скрипт.
Иконки можно, если знать пути к файлам, где они содержатся.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

6

Re: AHK: Tiny Master Script

teadrinker пишет:

Там много чего не хватает

думаю, для мини-менеджера этот набор функций был бы исчерпывающим.
Для остальных случаев можно скорректировать работу (отображение) MasterScript.ahk

7 (изменено: teadrinker, 2020-01-21 02:35:29)

Re: AHK: Tiny Master Script

Подобавлял всяких команд:

#NoEnv
SetBatchLines, -1
Menu, Tray, NoStandard
DetectHiddenWindows, On

scriptDirectory := "D:\AHK\My Scripts"
recurse := true
enumerateEXE := true
enumerateLNK := true

Scripts := EnumScriptsInDirectory(scriptDirectory, recurse, enumerateEXE, enumerateLNK)
OnMessage( 0x404, Func("AHK_NOTIFYICON").Bind(Scripts, scriptDirectory) )
Return

EnumScriptsInDirectory(scriptDirectory, recurse, exe, lnk) {
   if !InStr( FileExist(scriptDirectory), "D" ) {
      MsgBox, Folder "%scriptDirectory%" not found
      ExitApp
   }
   Scripts := [], Names := {}
   Loop, Files, % scriptDirectory . "\*.*", % (recurse ? "R" : "")
   {
      if (A_LoopFileExt ~= "i)ahk" . (exe ? "|exe" : "") . (lnk ? "|lnk" : "")) {
         if (A_LoopFileExt = "lnk") {
            FileGetShortcut, % A_LoopFilePath, scriptPath
            if !(scriptPath ~= "i)(ahk" . (exe ? "|exe" : "") . ")$")
               continue
            SplitPath, scriptPath, name
         }
         else {
            name := A_LoopFileName
            scriptPath := A_LoopFilePath
         }
         if Names.HasKey(name) {
            if (scriptPath = Names[name].scriptPath)
               continue
            i := 0
            Loop
               newName := name . " (" . ++i . ")"
            until !Names.HasKey(newName)
            name := newName
         }
         Names[name] := {name: name, scriptPath: scriptPath}
      }
   }
   for k, v in Names
      Scripts.Push(v)
   Return Scripts
}

AHK_NOTIFYICON(Scripts, scriptDirectory, wp, lp) {
   static WM_MOUSEMOVE := 0x200, WM_RBUTTONUP := 0x205, info
        , IsData := [], timer := Func("IsDataClear").Bind(IsData)
   if (lp = WM_MOUSEMOVE) {
      if !IsData[1] {
         Critical
         Loop 3 {
            try info := GetAhkScriptsInfo()
            catch {
               Sleep, 100
               continue
            }
            break
         }
         IsData[1] := true
         SetTimer, % timer, -1000
         Critical Off
      }
   }
   if (lp = WM_RBUTTONUP) {
      handler := Func("RunFile").Bind("taskmgr")
      Menu, MyMenu, Add, Running Scripts, % handler
      Menu, MyMenu, Default, Running Scripts
      Menu, MyMenu, Add
      Names := {}
      for k, v in info {
         if Names.HasKey(v.name) {
            i := 0
            Loop
               name := v.name . " (" . ++i . ")"
            until !Names.HasKey(name)
            v.name := name
         }
         Names[v.name] := ""
      }
      for name in Names {
         for k, v in info {
            if (v.name = name) {
               CreateActionsSubMenu(k, v)
               item := v.name
               if v.suspended
                  item .= A_Tab . "Suspended"
               else if v.paused
                  item .= A_Tab . "Paused"
               Menu, MyMenu, Add, % item, :Actions%k%
               break
            }
         }
      }
      Menu, MyMenu, Add
      CreateScriptsSubmenu(Scripts, scriptDirectory, info)
      Menu, MyMenu, Add, My Scripts, :Scripts
      Menu, MyMenu, Add
      Menu, MyMenu, Add, Close this Script, ExitScript
      Menu, MyMenu, Show
      Loop % info.MaxIndex()
         Menu, Actions%A_Index%, DeleteAll
      Loop % Scripts.MaxIndex()
         Menu, ScriptFile%A_Index%, DeleteAll
      Menu, Scripts, DeleteAll
      Menu, MyMenu, DeleteAll
      IsData[1] := false
   }
}

IsDataClear(IsData) {
   IsData[1] := false
}

GetAhkScriptsInfo() {
   static Waiting := 5, Suspended := 5
        , MyPID := DllCall("GetCurrentProcessId")
   info := [], PIDs := {}
   WinGet, List, List, ahk_class AutoHotkey
   Loop % List {
      WinExist("ahk_id" List%A_Index%)
      threadID := DllCall("GetWindowThreadProcessId", "Ptr", List%A_Index%, "UIntP", PID)
      if (PID = MyPID)
         continue
      PIDs[PID] := threadID
      WinGetTitle, title
      scriptPath := RegExReplace(title, " - AutoHotkey v[\d\.]+")
      SplitPath, scriptPath, fileName
      
      info.Push({ scriptPath: scriptPath, name: fileName, PID: PID
                , hwnd: List%A_Index%, suspended: false, bitness: GetProcessBitness(PID) })
   }
   ThreadsInfo(PIDs)
   for k, v in info {
      v.threads := PIDs[ v.PID ]
      for k, j in PIDs[ v.PID ] {
         if !(j.ThreadState = Waiting && j.WaitReason = Suspended)
            continue 2
         v.suspended := true
      }
   }
   for k, v in info
      if !v.suspended
         v.paused := IsPaused(v.hwnd)
   Return info
}

GetProcessBitness(PID) {
   if !A_Is64bitOS
      Return 32
   hProc := DllCall("OpenProcess", "UInt", PROCESS_QUERY_INFORMATION := 0x400, "UInt", 0, "UInt", PID, "Ptr")
   DllCall("IsWow64Process", "Ptr", hProc, "IntP", is32)
   DllCall("CloseHandle", "Ptr", hProc)
   Return 32 << !is32
}

ThreadsInfo(PIDs) {
   static SPI := SystemProcessInformation := 0x5, STATUS_SUCCESS := 0
        , szSPI := A_PtrSize = 4 ? 0xB8 : 0x100  ; size of SYSTEM_PROCESS_INFORMATION
        , szSTI := A_PtrSize = 4 ? 0x40 : 0x050  ; size of SYSTEM_THREAD_INFORMATION
        
   DllCall("Ntdll\NtQuerySystemInformation", "Int", SPI, "Ptr", 0, "UInt", 0, "UIntP", size, "UInt")
   VarSetCapacity(buff, size, 0)
   addr := &buff
   
   res := DllCall("Ntdll\NtQuerySystemInformation", "Int", SPI, "Ptr", addr, "UInt", size, "UIntP", 0, "UInt")
   if (res != STATUS_SUCCESS)
      throw Exception( "Could not get data from NtQuerySystemInformation. Error: " . Format("{:#x}", res) )
   offset := 0
   Loop {
      addr += offset
      ProcessID := NumGet(addr + 4*14 + A_PtrSize*3)
      if !PIDs.HasKey(ProcessID)
         continue
      threadsCount := NumGet(addr + 4, "UInt")
      mainThreadID := PIDs[ProcessID]
      PIDs[ProcessID] := []
      Loop % threadsCount {
         ThreadID := NumGet(addr + szSPI + 24 + A_PtrSize*3 + (A_Index - 1)*szSTI)
         if (ThreadID != mainThreadID)
            continue
         ThreadState := NumGet(addr + szSPI + 36 + A_PtrSize*4 + (A_Index - 1)*szSTI, "UInt")
         WaitReason  := NumGet(addr + szSPI + 40 + A_PtrSize*4 + (A_Index - 1)*szSTI, "UInt")
         PIDs[ProcessID].Push( {ThreadID: ThreadID, ThreadState: ThreadState, WaitReason: WaitReason} )
      }
   } until !offset := NumGet(addr + 0, "UInt")
}

CreateScriptsSubmenu(Scripts, folder, info) {
   handler := Func("RunFile").Bind(folder)
   Menu, Scripts, Add, Files, % handler
   Menu, Scripts, Default, Files
   Menu, Scripts, Add
   for k, v in Scripts {
      isRunning := false
      for i, j in info {
         if (j.scriptPath = v.scriptPath && isRunning := true)
            break
      }
      CreateScriptFileSubmenu(v.scriptPath, isRunning, k)
      Menu, Scripts, Add, % v.name, :ScriptFile%k%
      if isRunning
         Menu, Scripts, Check, % v.name
   }
}

CreateScriptFileSubmenu(scriptPath, isRunning, k) {
   if !isRunning {
      handler := Func("RunScript").Bind(scriptPath, k + 2)
      Menu, ScriptFile%k%, Add, Run, % handler
   }   
   if !(scriptPath ~= "i)\.exe$") {
      handler := Func("RunFileContextMenuItem").Bind(scriptPath, "Edit Script")
      Menu, ScriptFile%k%, Add, Edit Script, % handler
   }
   handler := Func("AddToStartup").Bind(scriptPath, k)
   Menu, ScriptFile%k%, Add, Add to Startup, % handler
   SplitPath, scriptPath, fileName
   if FileExist(A_Startup . "\" . fileName . ".lnk")
      Menu, ScriptFile%k%, Check, Add to Startup
   
   handler := Func("OpenScriptFolder").Bind(scriptPath)
   Menu, ScriptFile%k%, Add, Open File Location, % handler
}

RunFileContextMenuItem(filePath, contextMenuItemName) {
   SplitPath, filePath, fileName, dir
   try Folder := ComObjCreate("Shell.Application").Namespace(dir)
   catch
      Return
   for Item in Folder.Items {
      if (Item.Name = fileName) {
         Loop % Item.Verbs.Count {
            Verb := Item.Verbs.Item(A_Index - 1)
            if (Verb.Name = contextMenuItemName) {
               Verb.DoIt()
               break 2
            }
         }
      }
   }
}

RunFile(filePath) {
   Run, % filePath
}

RunScript(scriptPath, ItemPos) {
   hMenu := MenuGetHandle("Scripts")
   if !IsMenuChecked(hMenu, ItemPos)
      Run, % scriptPath
}

IsMenuChecked(hMenu, itemNumber)  {
   static MIIM_STATE := 1, MFS_CHECKED := 0x8
   VarSetCapacity(MENUITEMINFO, size := 4*4 + A_PtrSize*8, 0)
   NumPut(size, MENUITEMINFO)
   NumPut(MIIM_STATE, MENUITEMINFO, 4)
   DllCall("GetMenuItemInfo", Ptr, hMenu, UInt, itemNumber - 1, UInt, true, Ptr, &MENUITEMINFO)
   Return !!(NumGet(MENUITEMINFO, 4*3, "UInt") & MFS_CHECKED)
}

CreateActionsSubMenu(k, info) {
   static MyBitness := GetProcessBitness( DllCall("GetCurrentProcessId") )
   if !(MyBitness = 32 && info.bitness = 64) && !(info.name ~= "i)\.exe( \(\d+\))?$") {
      handler := Func("SuspendResumeThreads").Bind(info)
      Menu, Actions%k%, Add, Suspend Script, % handler
   }
   if info.suspended
      Menu, Actions%k%, Check, Suspend Script
   else {
      handler := Func("SelectMenuItem").Bind(info.hwnd, 5)
      Menu, Actions%k%, Add, Pause Script, % handler
      if info.paused
         Menu, Actions%k%, Check, Pause Script
   }
   Menu, Actions%k%, Add
   
   if !(info.name ~= "i)\.exe$") {
      handler := Func("RunFileContextMenuItem").Bind(info.scriptPath, "Edit Script")
      Menu, Actions%k%, Add, Edit Script, % handler
   }
   
   handler := Func("AddToStartup").Bind(info.scriptPath, k)
   Menu, Actions%k%, Add, Add to Startup, % handler
   if FileExist(A_Startup . "\" . info.name . ".lnk")
      Menu, Actions%k%, Check, Add to Startup
   
   handler := Func("OpenScriptFolder").Bind(info.scriptPath)
   Menu, Actions%k%, Add, Open File Location, % handler
   
   Menu, Actions%k%, Add
   handler := Func("KillAndReload").Bind(info.PID, info.scriptPath)
   Menu, Actions%k%, Add, Kill And Reload, % handler
   handler := Func("KillScript").Bind(info.PID)
   Menu, Actions%k%, Add, Kill Script, % handler
   if !info.suspended {
      handler := Func("SelectMenuItem").Bind(info.hwnd, 1)
      Menu, Actions%k%, Add, Reload, % handler
      handler := Func("SelectMenuItem").Bind(info.hwnd, 8)
      Menu, Actions%k%, Add, Exit, % handler
   }
}

OpenScriptFolder(filePath) {
   SplitPath, filePath,, dir
   for window in ComObjCreate("Shell.Application").Windows {
      try ShellFolderView := window.Document
      try if ( (Folder := ShellFolderView.Folder).Self.Path = dir ) {
         for item in Folder.Items {
            if (item.Path = filePath && found := true)
               break 2
         }
      }
   }
   if !found
      Run, explorer /select`, "%filePath%"
   else {
      WinActivate, % "ahk_id" window.hwnd
      ShellFolderView.SelectItem(item, 1|4|8|16)
    }
}

SelectMenuItem(hwnd, nMenu) {
   WinMenuSelectItem, ahk_id %hWnd%,, 1&, % nMenu . "&"
}

AddToStartup(scriptPath) {
   SplitPath, scriptPath, fileName
   lnk := A_Startup . "\" . fileName . ".lnk"
   if FileExist(lnk)
      FileDelete, % lnk
   else
      FileCreateShortcut, % scriptPath, % lnk
}

SuspendResumeThreads(info) {
   static MyBitness := GetProcessBitness( DllCall("GetCurrentProcessId") )
        , THREAD_SUSPEND_RESUME := 0x2
   for k, v in info.threads {
      hThread := DllCall("OpenThread", "UInt", THREAD_SUSPEND_RESUME, "UInt", false, "UInt", v.ThreadID, "Ptr")
      if info.suspended
         DllCall("ResumeThread", "Ptr", hThread)
      else {
         if (MyBitness = info.bitness)
            DllCall("SuspendThread", "Ptr", hThread)
         else
            DllCall("Wow64SuspendThread", "Ptr", hThread)
      }
      DllCall("CloseHandle", "Ptr", hThread)
   }
}

KillScript(PID) {
   Process, Close, % PID
   Process, WaitClose, % PID
   DeleteDummyIcons()
}

KillAndReload(PID, scriptPath) {
   KillScript(PID)
   Run, % scriptPath
}

IsPaused(hWnd) {
   static WM_ENTERMENULOOP := 0x211, WM_EXITMENULOOP := 0x212, MF_BYPOSITION := 0x400, MF_CHECKED := 8
        , Params := ["Ptr", 0, "Ptr", 0, "UInt", SMTO_ABORTIFHUNG := 0x2|SMTO_ERRORONEXIT := 0x20, "UInt", 500, "Ptr", 0]
   Loop 2
      if !DllCall("SendMessageTimeout", Ptr, hWnd, UInt, A_Index = 1 ? WM_ENTERMENULOOP : WM_EXITMENULOOP, Params*)
         Return
   hWinMenu := DllCall("GetMenu", Ptr, hWnd, Ptr)
   hFileMenu := DllCall("GetSubMenu", Ptr, hWinMenu, Int, 0, Ptr)
   Return DllCall("GetMenuState", Ptr, hFileMenu, UInt, 4, UInt, MF_BYPOSITION) = MF_CHECKED   ; Pause state
}

ExitScript() {
   ExitApp
}

DeleteDummyIcons()
{
   static TB_GETBUTTON   := 0x417
        , TB_BUTTONCOUNT := 0x418
        , PtrSize := 4 << A_Is64bitOS
        , szTBBUTTON := 8 + PtrSize*3
        , szTRAYDATA := 16 + PtrSize*2
   
   DHW_Prev := A_DetectHiddenWindows
   DetectHiddenWindows, On
   WinGet, PID, PID, ahk_exe explorer.exe
   RemoteBuff := new RemoteBuffer(PID, szTRAYDATA)
   Dummy := []
   
   Loop 2 {
      if (A_Index = 2)
         ControlGet, hToolBar, hwnd,, ToolbarWindow321, ahk_class NotifyIconOverflowWindow
      else {
         for k, v in ["TrayNotifyWnd", "SysPager", "ToolbarWindow32"]
            hToolBar := DllCall("FindWindowEx", "Ptr", k = 1 ? WinExist("ahk_class Shell_TrayWnd") : hToolBar, "Ptr", 0, "Str", v, "UInt", 0, "Ptr")
      }
      SendMessage, TB_BUTTONCOUNT,,,, % "ahk_id" . hToolBar
      Loop % ErrorLevel {
         SendMessage, TB_GETBUTTON, A_Index - 1, RemoteBuff.ptr,, % "ahk_id" . hToolBar
         RemoteBuff.Read(TBBUTTON, szTBBUTTON)
         RemoteBuff.Read(TRAYDATA, szTRAYDATA, NumGet(&TBBUTTON + 8 + PtrSize) - RemoteBuff.ptr)
         hWndIcon := NumGet(TRAYDATA), uID := NumGet(&TRAYDATA + PtrSize, "UInt")
         if !WinExist("ahk_id" hWndIcon)
            Dummy.Insert({hWnd: hWndIcon, uID: uID})
      }
   }
   DetectHiddenWindows, %DHW_Prev%
   for k, v in Dummy
      RemoveTrayIcon(v.hWnd, v.uID)
}

RemoveTrayIcon(hWnd, uID)
{
   VarSetCapacity(NOTIFYICONDATA, size := A_PtrSize = 8 ? 848 : A_IsUnicode? 828 : 444, 0)
   NumPut(size, NOTIFYICONDATA, "UInt")
   NumPut(hWnd, NOTIFYICONDATA, A_PtrSize)
   NumPut(uID, NOTIFYICONDATA, 2*A_PtrSize, "UInt")
   DllCall("shell32\Shell_NotifyIcon", "UInt", NIM_DELETE := 2, Ptr, &NOTIFYICONDATA)
   Return
}

class RemoteBuffer
{
   __New(PID, size) {
      static flags := (PROCESS_VM_OPERATION := 0x8) | (PROCESS_VM_WRITE := 0x20) | (PROCESS_VM_READ := 0x10)
           , Params := ["UInt", MEM_COMMIT := 0x1000, "UInt", PAGE_READWRITE := 0x4, "Ptr"]
         
      if !this.hProc := DllCall("OpenProcess", "UInt", flags, "Int", 0, "UInt", PID, "Ptr")
         throw Exception("Can't open remote process PID = " . PID . "`nA_LastError: " . A_LastError, "RemoteBuffer.__New")
      
      if !this.ptr := DllCall("VirtualAllocEx", "Ptr", this.hProc, "Ptr", 0, "Ptr", size, Params*) {
         DllCall("CloseHandle", "Ptr", this.hProc)
         throw Exception("Can't allocate memory in remote process PID = " . PID . "`nA_LastError: " . A_LastError, "RemoteBuffer.__New")
      }
   }
   
   __Delete() {
      DllCall("VirtualFreeEx", "Ptr", this.hProc, "Ptr", this.ptr, "UInt", 0, "UInt", MEM_RELEASE := 0x8000)
      DllCall("CloseHandle", "Ptr", this.hProc)
   }
   
   Read(ByRef localBuff, size, offset = 0) {
      VarSetCapacity(localBuff, size, 0)
      if !DllCall("ReadProcessMemory", "Ptr", this.hProc, "Ptr", this.ptr + offset, "Ptr", &localBuff, "Ptr", size, "PtrP", bytesRead)
         throw Exception("Can't read data from remote buffer`nA_LastError: " . A_LastError, "RemoteBuffer.Read")
      Return bytesRead
   }
   
   Write(pData, size, offset = 0) {
      if !res := DllCall("WriteProcessMemory", "Ptr", this.hProc, "Ptr", this.ptr + offset, "Ptr", pData, "Ptr", size, "PtrP", bytesWritten)
         throw Exception("Can't write data to remote buffer`nA_LastError: " . A_LastError, "RemoteBuffer.Write")
      Return bytesWritten
   }
}

Папку, из которой будут показаны скрипты в подменю "My Scripts" нужно прописать в начале в переменной scriptDirectory.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

8

Re: AHK: Tiny Master Script

teadrinker, круто! Спасибо! А чтобы "вложить" запущенные скрипты с исключением из трея, в любом случае необходимо прописывать в каждом #NoTrayIcon? По сути необходимость мини-мастера появилась в том, что различные сценарии у меня часто конфликтуют, т.к. прописаны и запускаются из одного файла. Была идея разделить один фактический скрипт на несколько процессов, но оставить единственную для всех иконку в трее.

9 (изменено: teadrinker, 2020-01-21 02:35:57)

Re: AHK: Tiny Master Script

Вот так можно удалить иконки AHK-скриптов из трея, но  только на один раз, до перезагрузки:

exclude = 
( ; здесь скрипты, иконки которых не надо удалять
D:\AHK\My Programs\ScreenCatcher\ScreenCatcher.ahk
D:\AHK\My Programs\LinksMenu\LinksMenu.ahk
)

DeleteAhkIcons(exclude)

DeleteAhkIcons(exclude)
{
   static TB_GETBUTTON   := 0x417
        , TB_BUTTONCOUNT := 0x418
        , PtrSize := 4 << A_Is64bitOS
        , szTBBUTTON := 8 + PtrSize*3
        , szTRAYDATA := 16 + PtrSize*2
   
   DHW_Prev := A_DetectHiddenWindows
   DetectHiddenWindows, On
   WinGet, PID, PID, ahk_exe explorer.exe
   RemoteBuff := new RemoteBuffer(PID, szTRAYDATA)
   DeleteIcon := []
   
   Loop 2 {
      if (A_Index = 2)
         ControlGet, hToolBar, hwnd,, ToolbarWindow321, ahk_class NotifyIconOverflowWindow
      else {
         for k, v in ["TrayNotifyWnd", "SysPager", "ToolbarWindow32"]
            hToolBar := DllCall("FindWindowEx", "Ptr", k = 1 ? WinExist("ahk_class Shell_TrayWnd") : hToolBar, "Ptr", 0, "Str", v, "UInt", 0, "Ptr")
      }
      SendMessage, TB_BUTTONCOUNT,,,, % "ahk_id" . hToolBar
      Loop % ErrorLevel {
         SendMessage, TB_GETBUTTON, A_Index - 1, RemoteBuff.ptr,, % "ahk_id" . hToolBar
         RemoteBuff.Read(TBBUTTON, szTBBUTTON)
         RemoteBuff.Read(TRAYDATA, szTRAYDATA, NumGet(&TBBUTTON + 8 + PtrSize) - RemoteBuff.ptr)
         hWnd := NumGet(TRAYDATA), uID := NumGet(&TRAYDATA + PtrSize, "UInt")
         WinGetClass, winClass, ahk_id %hWnd%
         if (winClass != "AutoHotkey")
            continue
         WinGetTitle, title, ahk_id %hWnd%
         scriptPath := RegExReplace(title, " - AutoHotkey v[\d\.]+")
         Loop, parse, % exclude, `n, `n
            if (scriptPath = A_LoopField)
               continue 2
         DeleteIcon.Insert({hWnd: hWnd, uID: uID})
      }
   }
   DetectHiddenWindows, %DHW_Prev%
   for k, v in DeleteIcon
      RemoveTrayIcon(v.hWnd, v.uID)
}

RemoveTrayIcon(hWnd, uID)
{
   VarSetCapacity(NOTIFYICONDATA, size := A_PtrSize = 8 ? 848 : A_IsUnicode? 828 : 444, 0)
   NumPut(size, NOTIFYICONDATA, "UInt")
   NumPut(hWnd, NOTIFYICONDATA, A_PtrSize)
   NumPut(uID, NOTIFYICONDATA, 2*A_PtrSize, "UInt")
   DllCall("shell32\Shell_NotifyIcon", "UInt", NIM_DELETE := 2, Ptr, &NOTIFYICONDATA)
   Return
}

class RemoteBuffer
{
   __New(PID, size) {
      static flags := (PROCESS_VM_OPERATION := 0x8) | (PROCESS_VM_WRITE := 0x20) | (PROCESS_VM_READ := 0x10)
           , Params := ["UInt", MEM_COMMIT := 0x1000, "UInt", PAGE_READWRITE := 0x4, "Ptr"]
         
      if !this.hProc := DllCall("OpenProcess", "UInt", flags, "Int", 0, "UInt", PID, "Ptr")
         throw Exception("Can't open remote process PID = " . PID . "`nA_LastError: " . A_LastError, "RemoteBuffer.__New")
      
      if !this.ptr := DllCall("VirtualAllocEx", "Ptr", this.hProc, "Ptr", 0, "Ptr", size, Params*) {
         DllCall("CloseHandle", "Ptr", this.hProc)
         throw Exception("Can't allocate memory in remote process PID = " . PID . "`nA_LastError: " . A_LastError, "RemoteBuffer.__New")
      }
   }
   
   __Delete() {
      DllCall("VirtualFreeEx", "Ptr", this.hProc, "Ptr", this.ptr, "UInt", 0, "UInt", MEM_RELEASE := 0x8000)
      DllCall("CloseHandle", "Ptr", this.hProc)
   }
   
   Read(ByRef localBuff, size, offset = 0) {
      VarSetCapacity(localBuff, size, 0)
      if !DllCall("ReadProcessMemory", "Ptr", this.hProc, "Ptr", this.ptr + offset, "Ptr", &localBuff, "Ptr", size, "PtrP", bytesRead)
         throw Exception("Can't read data from remote buffer`nA_LastError: " . A_LastError, "RemoteBuffer.Read")
      Return bytesRead
   }
   
   Write(pData, size, offset = 0) {
      if !res := DllCall("WriteProcessMemory", "Ptr", this.hProc, "Ptr", this.ptr + offset, "Ptr", pData, "Ptr", size, "PtrP", bytesWritten)
         throw Exception("Can't write data to remote buffer`nA_LastError: " . A_LastError, "RemoteBuffer.Write")
      Return bytesWritten
   }
}
Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

10

Re: AHK: Tiny Master Script

Подредактировал основной скрипт.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

11

Re: AHK: Tiny Master Script

teadrinker, спасибо! Потестирую.