#NoTrayIcon
#RequireAdmin
#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_Icon=ProcRespawn.ico
#AutoIt3Wrapper_Compression=4
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Res_Fileversion=1.0.0.2
#AutoIt3Wrapper_Res_Fileversion_AutoIncrement=y
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#include <Constants.au3>
#include <GUIConstantsEx.au3>
#include <WindowsConstants.au3>
#include <GUIListView.au3>
#include <Array.au3>
#include <StaticConstants.au3>
#include <GUIMenu.au3>
#include <Misc.au3>

#include <MySingleton.au3>

Opt('GUIOnEventMode', 1)
Opt('MustDeclareVars', 1)
Opt('TrayMenuMode', 1+2)
Opt('TrayOnEventMode', 1)

_MySingleton('ProcRespawn')

Global $sAppTitle = 'Process Respawn', $bHideNotify = 0, $bHideUnreadable = 1, _
    $hMainWindow, $lv_Monitor, $bt_Add, $bt_Remove, $bt_Params, $in_Interval, $bt_Shortcut, $bt_Exit, _
    $cm_Shortcut, $me_Startup, $mi_StartHide, $mi_StartShow, $me_Desktop, $mi_DeskHide, $mi_DeskShow, _
    $sConfigPath = @AppDataDir & '\therksoft-' & $sAppTitle & '.ini', $iCheckTime = Int(IniRead($sConfigPath, 'Config', 'Time', 5))
If $iCheckTime < 1 Then $iCheckTime = 1

If $CmdLine[0] And $CmdLine[1] = '/hide' Then $bHideNotify = 1

Main()

Func Main()
    TraySetState()
    TraySetToolTip($sAppTitle)
    TraySetClick(16)
    TraySetOnEvent($TRAY_EVENT_PRIMARYDOUBLE, '_TrayShow')
    TrayCreateItem('&Settings')
        TrayItemSetState(-1, 512)
        TrayItemSetOnEvent(-1, '_TrayShow')
    TrayCreateItem('E&xit')
        TrayItemSetOnEvent(-1, '_ProgramExit')

    $hMainWindow = GUICreate($sAppTitle, 400, 250, Default, Default, $WS_OVERLAPPEDWINDOW)
        GUISetOnEvent($GUI_EVENT_CLOSE, '_GenHandler')
    $lv_Monitor = GUICtrlCreateListView('Name|PID|Path|Parameters', 5, 5, 290, 240, BitOR($GUI_SS_DEFAULT_LISTVIEW, $LVS_NOSORTHEADER))
        GUICtrlSetResizing(-1, $GUI_DOCKBORDERS)

    Opt('GUIResizeMode', $GUI_DOCKSIZE+$GUI_DOCKTOP+$GUI_DOCKRIGHT)
    $bt_Add = GUICtrlCreateButton('&Add Process', 300, 5, 95, 25)
        GUICtrlSetOnEvent(-1, '_AddProcess')
    $bt_Remove = GUICtrlCreateButton('&Remove Process', 300, 35, 95, 25)
        GUICtrlSetOnEvent(-1, '_GenHandler')
    $bt_Params = GUICtrlCreateButton('Set &Parameters', 300, 65, 95, 25)
        GUICtrlSetOnEvent(-1, '_GenHandler')
    GUICtrlCreateLabel('&Frequency:', 300, 95, 55, 20, $SS_CENTERIMAGE)
    $in_Interval = GUICtrlCreateInput($iCheckTime, 355, 95, 20, 20)
        GUICtrlSetTip(-1, 'How frequently to check if processes are running.')
        GUICtrlSetOnEvent(-1, '_GenHandler')
    GUICtrlCreateLabel(' sec', 375, 95, 40, 20, $SS_CENTERIMAGE)

    Opt('GUIResizeMode', $GUI_DOCKSIZE+$GUI_DOCKBOTTOM+$GUI_DOCKRIGHT)
    $bt_Shortcut = GUICtrlCreateButton('Create Shortcut', 300, 190, 95, 25)
        GUICtrlSetOnEvent(-1, '_GenHandler')
    $bt_Exit = GUICtrlCreateButton('E&xit', 300, 220, 95, 25)
        GUICtrlSetOnEvent(-1, '_ProgramExit')

    $cm_Shortcut = GUICtrlCreateContextMenu(GUICtrlCreateDummy())
        $me_Startup = GUICtrlCreateMenu('Create shortcut in &Startup', $cm_Shortcut)
            $mi_StartShow = GUICtrlCreateMenuItem('&Show program window when launched', $me_Startup)
                GUICtrlSetOnEvent(-1, '_MakeShortcut')
            $mi_StartHide = GUICtrlCreateMenuItem('&Hide program window when launched', $me_Startup)
                GUICtrlSetOnEvent(-1, '_MakeShortcut')
        $me_Desktop = GUICtrlCreateMenu('Create shortcut on &Desktop', $cm_Shortcut)
            $mi_DeskShow = GUICtrlCreateMenuItem('&Show program window when launched', $me_Desktop)
                GUICtrlSetOnEvent(-1, '_MakeShortcut')
            $mi_DeskHide = GUICtrlCreateMenuItem('&Hide program window when launched', $me_Desktop)
                GUICtrlSetOnEvent(-1, '_MakeShortcut')

    _RefreshMonitorList()
    AdlibRegister('_RefreshMonitorList', $iCheckTime * 1000)
    If Not $bHideNotify Then GUISetState()

    ProcessWaitClose(@AutoItPID)
EndFunc

Func _TrayShow()
    GUISetState(@SW_SHOW, $hMainWindow)
    WinActivate($hMainWindow)
EndFunc

Func _ProgramExit()
    If MsgBox(0x2134, $sAppTitle, 'Are you sure you want to exit this program?' & @LF & '(Processes will not be monitored/restarted)') = 6 Then Exit
EndFunc

Func _GenHandler()
    Switch @GUI_CtrlId
        Case $in_Interval
            $iCheckTime = Int(GUICtrlRead($in_Interval))
            If $iCheckTime < 1 Then
                MsgBox(0x2030, $sAppTitle, 'Check time cannot be less than 1 second.')
                GUICtrlSetData($in_Interval, '1')
                $iCheckTime = 1
            EndIf
            IniWrite($sConfigPath, 'Config', 'Time', $iCheckTime)
            AdlibRegister('_RefreshMonitorList', $iCheckTime * 1000)

        Case $bt_Remove
            Local $iSel = _GUICtrlListView_GetNextItem($lv_Monitor)
            If $iSel = -1 Then Return

            Local $sProcName = _GUICtrlListView_GetItemText($lv_Monitor, $iSel, 0)
            If MsgBox(0x2124, $sAppTitle, 'Are you sure you want to stop monitoring this process? (' & $sProcName & ')', 0, $hMainWindow) = 6 Then
                IniDelete($sConfigPath, 'Processes', _IniEscape($sProcName))
                IniDelete($sConfigPath, 'ProcessParams', _IniEscape($sProcName))
                _RefreshMonitorList()
            EndIf

        Case $bt_Params
            Local $iSel = _GUICtrlListView_GetNextItem($lv_Monitor)
            If $iSel = -1 Then Return

            Local $sProcName = _GUICtrlListView_GetItemText($lv_Monitor, $iSel, 0)
            Local $sParams = _GUICtrlListView_GetItemText($lv_Monitor, $iSel, 3)
            $sParams = InputBox($sAppTitle, 'Enter parameters for process: ' & $sProcName, $sParams, '', 300, 120, Default, Default, 0, $hMainWindow)
            If $sParams Then
                IniWrite($sConfigPath, 'ProcessParams', _IniEscape($sProcName), $sParams)
                _RefreshMonitorList()
            EndIf

        Case $bt_Shortcut
            Local $aPos = WinGetPos(GUICtrlGetHandle($bt_Shortcut))
            If Not @error Then _GUICtrlMenu_TrackPopupMenu(GUICtrlGetHandle($cm_Shortcut), $hMainWindow, $aPos[0], $aPos[1] + $aPos[3])

        Case $GUI_EVENT_CLOSE
            GUISetState(@SW_HIDE, $hMainWindow)
            If Not $bHideNotify Then
                TrayTip($sAppTitle, 'The program window has been hidden. Double click this icon to see it again or right-click to choose Exit.', 5, 1)
                $bHideNotify = 1
            EndIf
    EndSwitch
EndFunc

Func _MakeShortcut()
    Local $sHide, $sPath = @DesktopDir
    Switch @GUI_CtrlId
        Case $mi_DeskHide
            $sHide = '/hide'
        Case $mi_StartShow
            $sPath = @StartupDir
        Case $mi_StartHide
            $sHide = '/hide'
            $sPath = @StartupDir
    EndSwitch

    FileCreateShortcut(@ScriptFullPath, $sPath & '\' & $sAppTitle & '.lnk', @ScriptDir, $sHide, '', @ScriptFullPath)
EndFunc

Func _AddProcess()
    Opt('GUIOnEventMode', 0)
    Local $gm, $hListWindow, $bt_ListAdd, $ch_ListRefresh, $bt_AddByPath

    $hListWindow = GUICreate($sAppTitle, 300, 300, Default, Default, BitOR($WS_MAXIMIZEBOX, $WS_CAPTION, $WS_POPUP, $WS_SYSMENU, $WS_SIZEBOX), Default, $hMainWindow)
    Global $lv_ListProcess = GUICtrlCreateListView('Name|PID|Path', 5, 5, 290, 265, BitOR($GUI_SS_DEFAULT_LISTVIEW, $LVS_NOSORTHEADER))
        GUICtrlSetResizing(-1, $GUI_DOCKBORDERS)
    Opt('GUIResizeMode', $GUI_DOCKSIZE+$GUI_DOCKBOTTOM+$GUI_DOCKLEFT)
    $bt_ListAdd = GUICtrlCreateButton('Add to Monitor', 5, 270, 90, 25)
    $ch_ListRefresh = GUICtrlCreateCheckbox('Refresh', 100, 270, 110, 25)

    $bt_AddByPath = GUICtrlCreateButton('Add by Path', 215, 270, 80, 25)
        GUICtrlSetResizing(-1, $GUI_DOCKSIZE+$GUI_DOCKBOTTOM+$GUI_DOCKRIGHT)
    _RefreshProcList()
    GUISetState()

    WinMove($hListWindow, '', (@DesktopWidth - 640) / 2, (@DesktopHeight - 480) / 2, 640, 480)

    WinSetState($hMainWindow, '', @SW_DISABLE)
    While 1
        $gm = GUIGetMsg()
        Switch $gm
            Case $bt_AddByPath
                Local $sFilePath = FileOpenDialog('Select program', '', 'Applications (*.exe)|All files (*.*)', 1+2, '', $hListWindow)
                If Not @error Then
                    Local $sFileName = StringTrimLeft($sFilePath, StringInStr($sFilePath, '\', 0, -1))
                    IniWrite($sConfigPath, 'Processes', _IniEscape($sFileName), _IniEscape($sFilePath))
                    ExitLoop
                EndIf
            Case $bt_ListAdd
                Local $iSel = _GUICtrlListView_GetNextItem($lv_ListProcess)
                Local $sProcName = _GUICtrlListView_GetItemText($lv_ListProcess, $iSel, 0)
                Local $sProcPath = _GUICtrlListView_GetItemText($lv_ListProcess, $iSel, 2)
                IniWrite($sConfigPath, 'Processes', _IniEscape($sProcName), _IniEscape($sProcPath))
                ExitLoop
            Case $ch_ListRefresh
                If BitAND(GUICtrlRead($ch_ListRefresh), $GUI_CHECKED) Then
                    _RefreshProcList()
                    AdlibRegister('_RefreshProcList', 1000)
                Else
                    AdlibUnRegister('_RefreshProcList')
                EndIf
            Case $GUI_EVENT_CLOSE
                ExitLoop
        EndSwitch
    WEnd
    AdlibUnRegister('_RefreshProcList')
    _RefreshMonitorList()
    GUIDelete($hListWindow)
    WinSetState($hMainWindow, '', @SW_ENABLE)
    WinActivate($hMainWindow)
    Opt('GUIOnEventMode', 1)
EndFunc

Func _RefreshMonitorList()
    Local $iSel = _GUICtrlListView_GetNextItem($lv_Monitor)
    _GUICtrlListView_BeginUpdate($lv_Monitor)
    _GUICtrlListView_DeleteAllItems($lv_Monitor)
    Local $aMonList = IniReadSection($sConfigPath, 'Processes')
    If @error Then Return _GUICtrlListView_EndUpdate($lv_Monitor)

    _ArraySort($aMonList, 0, 1, 0, 0)
    For $forMonList = 1 To $aMonList[0][0]
        $aMonList[$forMonList][0] = _IniUnescape($aMonList[$forMonList][0])
        $aMonList[$forMonList][1] = _IniUnescape($aMonList[$forMonList][1])

        Local $sParams = _IniUnescape(IniReadRaw($sConfigPath, 'ProcessParams', $aMonList[$forMonList][0], @LF))
        Local $iPID = _CheckProcessByPath($aMonList[$forMonList][1])
        If Not $iPID Then
            If $sParams <> @LF Then
                $iPID = Run('"' & $aMonList[$forMonList][1] & '"' & ' ' & $sParams)
            Else
                $iPID = Run('"' & $aMonList[$forMonList][1] & '"')
            EndIf
        EndIf
        GUICtrlCreateListViewItem($aMonList[$forMonList][0] & '|' & $iPID & '|' & $aMonList[$forMonList][1] & '|' & StringStripWS($sParams, 3), $lv_Monitor)
    Next
    _GUICtrlListView_EndUpdate($lv_Monitor)
    _GUICtrlListView_SetColumnWidth($lv_Monitor, 0, $LVSCW_AUTOSIZE)
    _GUICtrlListView_SetColumnWidth($lv_Monitor, 1, $LVSCW_AUTOSIZE)
    _GUICtrlListView_SetColumnWidth($lv_Monitor, 2, $LVSCW_AUTOSIZE)
    If $iSel <> -1 Then _GUICtrlListView_SetItemSelected($lv_Monitor, $iSel)
EndFunc

Func _RefreshProcList()
    Local $iSel = _GUICtrlListView_GetNextItem($lv_ListProcess)
    Local $aProcList = _ProcessListProperties()
    _ArraySort($aProcList, 0, 1, 0, 0)
    _GUICtrlListView_BeginUpdate($lv_ListProcess)
    _GUICtrlListView_DeleteAllItems($lv_ListProcess)
    For $forProcs = 1 To $aProcList[0][0]
        GUICtrlCreateListViewItem($aProcList[$forProcs][0] & '|' & $aProcList[$forProcs][1] & '|' & $aProcList[$forProcs][2], $lv_ListProcess)
    Next
    _GUICtrlListView_EndUpdate($lv_ListProcess)
    _GUICtrlListView_SetColumnWidth($lv_ListProcess, 0, $LVSCW_AUTOSIZE)
    _GUICtrlListView_SetColumnWidth($lv_ListProcess, 1, $LVSCW_AUTOSIZE)
    _GUICtrlListView_SetColumnWidth($lv_ListProcess, 2, $LVSCW_AUTOSIZE)
    If $iSel <> -1 Then _GUICtrlListView_SetItemSelected($lv_ListProcess, $iSel)
EndFunc

Func _CheckProcessByPath($sPath)
    Local $iReturn, $oWMI, $oResult
    $sPath = StringReplace($sPath, "\", "\\")
    $sPath = StringReplace($sPath, "'", "\'")
    $sPath = StringReplace($sPath, '"', '\"')

    ; Connect to WMI and get process objects
    $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate,authenticationLevel=pktPrivacy, (Debug)}!\\.\root\cimv2")
    If IsObj($oWMI) Then
        $oResult = $oWMI.ExecQuery("select ProcessId from win32_process where ExecutablePath = '" & $sPath & "'")

        If IsObj($oResult) Then
            ; Return for no matches
            If $oResult.count = 0 Then Return SetError(3, 0, 0)

            ; For each process...
            For $oProc In $oResult
                $iReturn = $oProc.ProcessId
            Next
        Else
            SetError(2); Error getting process collection from WMI
        EndIf
        ; release the collection object
        $oResult = 0
    Else
        SetError(1); Error connecting to WMI
    EndIf

    ; Return array
    Return $iReturn
EndFunc

Func _ProcessListProperties()
    Local $sReturn, $oWMI, $oResult, $aProcList[1][1], $n = 1

    ; Connect to WMI and get process objects
    $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate,authenticationLevel=pktPrivacy, (Debug)}!\\.\root\cimv2")
    If IsObj($oWMI) Then
        $oResult = $oWMI.ExecQuery("select name,ProcessId,ExecutablePath from win32_process")

        If IsObj($oResult) Then
            ; Return for no matches
            If $oResult.count = 0 Then Return $aProcList

            ; Size the array
            ReDim $aProcList[$oResult.count + 1][3]
            $aProcList[0][0] = UBound($aProcList) - 1

            ; For each process...
            For $oProc In $oResult
                If Not $oProc.ExecutablePath Then ContinueLoop
                $aProcList[$n][0] = $oProc.name
                $aProcList[$n][1] = $oProc.ProcessId
                $aProcList[$n][2] = $oProc.ExecutablePath
                $n += 1
            Next
            ReDim $aProcList[$n][3]
            $aProcList[0][0] = $n-1
        Else
            SetError(2); Error getting process collection from WMI
        EndIf
        ; release the collection object
        $oResult = 0
    Else
        SetError(1); Error connecting to WMI
    EndIf

    ; Return array
    Return $aProcList
EndFunc

Func IniReadRaw($sFile, $sSection, $sKey, $sDefault)
    Local $aSection = IniReadSection($sFile, $sSection)
    If @error Then Return $sDefault
    For $i = 1 To $aSection[0][0]
        If $aSection[$i][0] = $sKey Then Return $aSection[$i][1]
    Next
    Return $sDefault
EndFunc


Func _IniEscape($sString)
    Local $aSplit = StringSplit($sString, ''), $sOutput
    For $i = 1 to $aSplit[0]
        Switch $aSplit[$i]
            Case '=', ']', '[', '"', "'", '%'
                $sOutput &= '%' & Hex(Asc($aSplit[$i]), 2)
            Case Else
                $sOutput &= $aSplit[$i]
        EndSwitch
    Next
    Return $sOutput
EndFunc

Func _IniUnescape($sString)
    Local $aSplit = StringSplit($sString, '%')
    For $i = 2 To $aSplit[0]
        $aSplit[1] &= Chr(Dec(StringLeft($aSplit[$i], 2))) & StringTrimLeft($aSplit[$i], 2)
    Next
    Return $aSplit[1]
EndFunc