1

I am working on a custom powershell statusline, and implemented a couple cool features: battery monitoring, wifi monitoring, etc. Unfortunately, these tend to be slow to update, as they require calls to Get-NetAdapterStatistics, getting wmi objects, and other very slow calls. I am trying to spawn child processes via start-job to allow for background polling and updating a shared variable, but can't figure out how to do this. The best possible substitute I have found is a sort of IPC (as described here: Pipelining between two SEPARATE Powershell processes), but I'd prefer to stick with a traditional shared variable if possible. Is there a way to do this? The best alternative I've found is passing variables as arguments, but this won't allow for polling.

To disclaim, I am aware this is not really what powershell is designed for, but I am still wondering if this is possible. Or is the best option to write a c/c++ binary that will return the statusline? Write to a file (this seems like it might be slow)? Let me know what might work, if you are able, or if you need more information. Thank you.

More information:

Start-Job -Name testJob -Script {
        $testVar = "asdf"
}

Write-Host $testVar
# should output asdf

Is there any way to do this? I am trying to do some work and return a variable. How is this possible? The only possible ways I have found are:

  1. Write a file to the disk, which is a bit slow

  2. Use an IPC pipe:

$pipe = New-Object System.IO.Pipes.NamedPipeClientStream '.',"testPipe",'In'
Start-Job -Name testJob -Script {
        $pipe = New-Object System.IO.Pipes.NamedPipeServerStream "testPipe",'Out'
        $pipe.WaitForConnection()
        $sw = New-Object System.IO.StreamWriter $pipe
        $sw.AutoFlush = $true
        $sw.writeLine("foo")
        While ($true) {
                # do looping and polling, then print stuff
                $sw.writeLine($pollResult)
        }
        $sw.Dispose()
        $pipe.Dispose()
}

$pipe.Connect()
$sr = New-Object System.IO.StreamReader $pipe
#when necessary, read info
$output = $sr.ReadLine()
$sr.Dispose()
$pipe.Dispose()

The biggest downside to this is that it's a bit glitchy. Most importantly, I don't know how to shut down the IPC pipe when I close the powershell window (as this is a statusline) and I end up with a "pipe-leak" that leads to high cpu usage and powershell processes running in the background. This is at least not the case with the straight background job writing to a file. Apparently the pipe is supposed to close when the last reference is deleted, but the background job keeps running with it open. This is because a powershell session will hang to the point where it cannot be ctrl-c'd when it's waiting for something pipe-related (when waiting for pipe connect, to finish writing a line, etc.).

Thank you, and please let me know if I can add any more information.

Update: I've tried using empty files basically as control flags (I think it would be faster than parsing one file for each setting), but am trying to use a job to return VCS information (which would be easier to just return, and faster). Any ideas on how to solve this issue? I'm stumped.

Note: I tagged this C# because powershell uses C#'s pipe functions, and I'm hoping someone with knowledge thereof might be able to help.

Ace
  • 33
  • 4
  • Please show a example of what you are looking for even if its just pseudo code. – ArcSet Apr 20 '19 at 14:33
  • I'm trying to figure out if I can share a variable between a powershell Job. I added a little more deail above; let me know if that's helpful. – Ace Apr 20 '19 at 18:41
  • Are you trying to pass a varible to the Job or get a response after a job is finished? – ArcSet Apr 20 '19 at 18:55
  • Get a response. It needs to be a string that contains utf-8 characters; I'm polling for git status changes as part of my statusline. I've got the existing functions to do it, I just need to figure out how to pass it back. I'd like to have it run every number of seconds and just update a variable each time, because running it when the git info is needed and waiting for it to finish would defeat the purpose (lowering draw latency). – Ace Apr 20 '19 at 19:02
  • To be clear, I don't actually want it to end. I would like to spawn it and have it poll in the background and update a variable accessible to the parent, send via a pipe, etc. until the parent is closed. – Ace Apr 20 '19 at 19:05

1 Answers1

0

Boe Prox did an excellent writeup on this years ago. He's also expanded this with the PoshRSJob module. Using this module makes this fairly easy to handle.

#Create a synchronized hashtable
$sync = [hashtable]::Synchronized(@{
    Time = ''
    Stop = $false
    Updater = ''
})
#create 5 RSJobs
1..5 | Start-RSJob -ScriptBlock {
    param($sync) #accept $sync as a param
    $updater = [Guid]::NewGuid() #unique id per job
    while(-not $sync.Stop) { #run until told not to
        $sync.Time = Get-Date
        $sync.Updater = $updater
        start-sleep -Seconds 1
    }
} -ArgumentList $sync #pass $sync as a param

Running this spawns 5 jobs:

Id       Name                 State           HasMoreData  HasErrors    Command
--       ----                 -----           -----------  ---------    -------
1        Job1                 Running         False        False        ...
2        Job2                 Running         False        False        ...
3        Job3                 Running         False        False        ...
4        Job4                 Running         False        False        ...
5        Job5                 Running         False        False        ...

You can then check $sync in the parent process and see that it is constantly being updated from these Jobs. Note that these are not just string representations but full fledged objects.

PS C:\> $sync

Name     Value
----     -----
Time     4/22/2019 11:58:35 AM
Stop     False
Updater  9ab28c51-2941-4866-a064-165b1ceca673

PS C:\> $sync

Name     Value
----     -----
Time     4/22/2019 11:58:37 AM
Stop     False
Updater  113e78a8-1774-4cdf-9638-7235109f0a0d

To terminate the jobs we set $sync.Stop = $true

PS C:\> Get-RSJob

Id       Name                 State           HasMoreData  HasErrors    Command
--       ----                 -----           -----------  ---------    -------
1        Job1                 Completed       False        False        ...
2        Job2                 Completed       False        False        ...
3        Job3                 Completed       False        False        ...
4        Job4                 Completed       False        False        ...
5        Job5                 Completed       False        False        ...
StephenP
  • 3,895
  • 18
  • 18
  • This is exactly what I was looking for; thanks so much. – Ace Apr 22 '19 at 13:05
  • Is there any way I can do this such that it allows already imported functions (imported into the main script) to be used in the job? – Ace Apr 22 '19 at 21:01
  • Start-RSJob has -FunctionsToImport and -VariablesToImport parameters specifically for this – StephenP Apr 22 '19 at 22:26