2

My goal is to monitor notepad process. If existing instance is closed then new one should start. I have following code (this is simplified snippet from my larger code base where I use WinForms):

function ShowRunningPids($startedProcesses)
{
    foreach ($proc in $startedProcesses){
        Write-Host ("`$proc.Id: {0}" -f $proc.Id)
    }
}

function CheckPids($procPid, $startedProcesses)
{
    Write-Host "Already started PIDS:"
    ShowRunningPids $startedProcesses

    $proc = Get-Process -Id $procPid -ErrorAction Ignore
    # PROBLEM: Not updating this PID means that after closing first instance of notepad the new instance is always spawned.
    # Because if statement is always checking against PID that was created as first.
    if (!$proc){
        Write-Host ("Process with PID: {0} not running - starting new process" -f $procPid)
        $processStatus = Start-Process -passthru -FilePath notepad
        Write-Host ("Process with PID: {0} started" -f $processStatus.Id)
        # PROBLEM: PID of closed notepad are not deleted from $startedProcesses
        [System.Collections.ArrayList]$startedProcesses = $startedProcesses | Where-Object { $_.Id -ne $procPid }
        $startedProcesses.add($processStatus)
        Write-Host ("Removed PID {0} and added PID {}" -f $procPid, $processStatus.Id)
    }
}


[System.Collections.ArrayList]$startedProcesses = @()

$processStatus = Start-Process -passthru -FilePath notepad
$startedProcesses.add($processStatus)

# start timer
$timer = New-Object System.Windows.Forms.Timer
    $timer.Interval = 1000
    # PROBLEM: How to update PID to instance of newly created notepad?
    $timer.add_tick({CheckPids $processStatus.id $startedProcesses})
    $timer.Start()


# Form
$objForm = New-Object System.Windows.Forms.Form 
    $objForm.Text = "Auto restart of process"
    $objForm.Size = New-Object System.Drawing.Size(330,380) 
    $objForm.StartPosition = "CenterScreen"
    $objForm.Add_Shown({$objForm.Activate()})
    $objForm.Add_Closing({$timer.Stop(); })
    [void] $objForm.ShowDialog()

After I close existing instance of notepad the new one is started every second until I stop script. I can see several problems here which are described in comments in above code. Also I get following error message on console:

Cannot convert the "System.Diagnostics.Process (notepad)" value of type "System.Diagnostics.Process" to type "System.Collections.ArrayList".
At C:\Users\wakatana\PS\ps.ps1:19 char:9
+         [System.Collections.ArrayList]$script:startedProcesses = $scr ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [], ArgumentTransformationMetadataException
    + FullyQualifiedErrorId : RuntimeException

Error formatting a string: Input string was not in a correct format..
At C:\Users\wakatana\ps.ps1:21 char:9
+         Write-Host ("Removed PID {0} and added PID {}" -f $procPid, $ ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Removed PID {0} and added PID {}:String) [], RuntimeException
    + FullyQualifiedErrorId : FormatError

How to fix this code?

Wakan Tanka
  • 7,542
  • 16
  • 69
  • 122
  • You can use WMI events to monitor for processes starting/stopping, and automatically take action when this happens, which avoids you needing to loop around and track stuff. Here is an example in a previous answer of mine: [start process and wait for parent process only](https://stackoverflow.com/questions/49436267/start-process-and-wait-for-parent-process-only/49438322#49438322) – boxdog Jan 12 '20 at 03:07

1 Answers1

1

So, based on your initial example, here is something that work as intended.

I actually have 2 examples to show.

My first thought was to do it without the use of forms timer since based on your sample, it seems pretty useless.

In the second example, we apply a similar logic but keep the form. In both case, there was significant changes that were needed in order for this to work..

In both exampless

  • Register-ObjectEvent was used to register a timer event of type System.Timers.Timer
  • The $ProcPid is passed down to the timer as a [ref] so we can update it's value (While you can update object through the timer, simple structure like integer and string won't update if you try to do it without specifically referencing the variable using [ref]. This is because the original reference get overriden when you assign a new value

Example #1 - System.Windows.timer only

I made this example first, as when looking at your code, there was no real use of the form other than its timer object. I initially thought maybe you weren't aware of System.Timers.Timer


function ShowRunningPids($startedProcesses) {
    foreach ($proc in $startedProcesses) {
        Write-Host ("`$proc.Id: {0}" -f $proc.Id)
    }
}

function CheckPids($procPid, $startedProcesses) {
    Write-Host "Already started PIDS:"
    ShowRunningPids $startedProcesses
    $proc = Get-Process -Id $procPid -ErrorAction Ignore
    if ($proc.Handles) {
        return $procPid
    }

    $DeadProc = $startedProcesses.where( { $_.id -eq $procPid }, 'first') | Select -first 1
    [void]$startedProcesses.Remove($DeadProc)
    Write-Host ("Process with PID: {0} not running - starting new process" -f $procPid)
    $processStatus = Start-Process -passthru -FilePath notepad

    Write-Host ("Process with PID: {0} started" -f $processStatus.Id)
    # PROBLEM: PID of closed notepad are not deleted from $startedProcesses

    $startedProcesses.add($processStatus)
    Write-Host ("Removed PID {0} and added PID {1}" -f $procPid, $processStatus.Id)
    return $processStatus.Id

}

# Array list is deprecated.
$startedProcesses = [System.Collections.Generic.List[PSObject]]::new()
$ProcPid = 0

$Timer = [System.Timers.Timer]::new()
$Timer.Interval = 1000
$Timer.Elapsed
$Timer.AutoReset = $true
$Timer.Start()
$proc = (Start-Process 'notepad.exe'  -PassThru)
$startedProcesses.Add($proc)
$procPid = $proc.Id
Unregister-Event -SourceIdentifier 'ProcessTimerEvent' -errorAction SilentlyContinue
Register-ObjectEvent $Timer elapsed -SourceIdentifier 'ProcessTimerEvent' -Action {
    try {
        $Data = $event.MessageData
        $Data.ProcessID.value = CheckPids -procPid $Data.ProcessID.value -startedProcesses $Data.startedProcesses
    }
    catch {
        Write-Warning $_
    }

} -MessageData @{
    startedProcesses = $startedProcesses
    ProcessID        = [ref]$procPid
}


while ($true) {
    start-sleep -Seconds 1
}


Unregister-Event -SourceIdentifier 'ProcessTimerEvent'

Then, I thought again about your questions and figured that maybe that form, in its original shape, contained more stuff than just an empty interface and you were actually using it.

So I came up with that second solution, which keep the original form in place.

This new version uses 2 different timers.

  • The original timer from my first example, which run as a job indefinitely on a separate thread.
  • A form timer, which do a get-job -name 'TimerThing' | Receive-Job so the data from our other timer is brought back to the main thread and that you actually see all those write-host that you put in your functions.

When you exist the form, the form timer is stopped and the job containing the process watch is stopped too.

Example #2 - running process watch on separate threads and using your original form

$InitScript = {
    function ShowRunningPids($startedProcesses) {
        foreach ($proc in $startedProcesses) {
            Write-Host ("`$proc.Id: {0}" -f $proc.Id)
        }
    }

    function CheckPids($procPid, $startedProcesses) {
        Write-Host "Already started PIDS:"
        ShowRunningPids $startedProcesses
        $proc = Get-Process -Id $procPid -ErrorAction Ignore
        if ($proc.Handles) {
            return $procPid
        }

        $DeadProc = $startedProcesses.where( { $_.id -eq $procPid }, 'first') | Select -first 1
       [void]$startedProcesses.Remove($DeadProc)
        Write-Host ("Process with PID: {0} not running - starting new process" -f $procPid)
        $processStatus = Start-Process -passthru -FilePath notepad

        Write-Host ("Process with PID: {0} started" -f $processStatus.Id)
        # PROBLEM: PID of closed notepad are not deleted from $startedProcesses

        $startedProcesses.add($processStatus)
        Write-Host ("Removed PID {0} and added PID {1}" -f $procPid, $processStatus.Id)
        return $processStatus.Id

    }
}


Start-Job -InitializationScript $InitScript  -Name 'TimerThing' -ScriptBlock {

    # Array list is deprecated.
    $startedProcesses = [System.Collections.Generic.List[PSObject]]::new()
    $ProcPid = 0

    $Timer = [System.Timers.Timer]::new()
    $Timer.Interval = 1000
    $Timer.Elapsed
    $Timer.AutoReset = $true
    $Timer.Start()
    $proc = (Start-Process 'notepad.exe'  -PassThru)
    $startedProcesses.Add($proc)
    $procPid = $proc.Id
    Unregister-Event -SourceIdentifier 'ProcessTimerEvent' -errorAction SilentlyContinue
    Register-ObjectEvent $Timer elapsed -SourceIdentifier 'ProcessTimerEvent' -Action {
        try {
            $Data = $event.MessageData
            $Data.ProcessID.value = CheckPids -procPid $Data.ProcessID.value -startedProcesses $Data.startedProcesses
        }
        catch {
            Write-Warning $_
        }

    } -MessageData @{
        startedProcesses = $startedProcesses
        ProcessID        = [ref]$procPid
    }

    while ($true) {
        Start-Sleep -Seconds 1
    }
}

# Form
add-type -AssemblyName 'System.Windows.Forms'
# start timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
# PROBLEM: How to update PID to instance of newly created notepad?
$timer.add_tick( { get-job -name 'TimerThing' | Receive-Job })
$timer.Start()

$objForm = New-Object System.Windows.Forms.Form
$objForm.Text = "Auto restart of process"
$objForm.Size = New-Object System.Drawing.Size(330, 380)
$objForm.StartPosition = "CenterScreen"
$objForm.Add_Shown( { $objForm.Activate() })
$objForm.Add_Closing( { $timer.Stop(); Stop-Job -Name 'TimerThing' })
[void] $objForm.ShowDialog()
Sage Pourpre
  • 9,932
  • 3
  • 27
  • 39
  • Thank you for reply. Would be possible to add 3rd version? I'm using `WinForms` and it's `add_tick` in larger code base. Also calling `startedProcesses.clean()` is probably not best option because I hold in this variable also another started processes. Thanks – Wakan Tanka Feb 06 '20 at 00:05
  • You are absolutely correct. I thought you were overriding variable assignment and deleting the old stuff from it in your initial sample. I totally misread that part. I also thought it didn't make too much sense... – Sage Pourpre Feb 06 '20 at 09:36
  • Both examples (instead of creating a third one) now remove the dead process instead of clearing the whole list. – Sage Pourpre Feb 06 '20 at 09:38