Can I use $? variable inside worflow?

Hello,

I’m confused what I’m doing wrong here. I expected $? variable to have success of previously run command. It does not work inside workflow for some reason. Code is below.

workflow GetActiveComputers 
{
    Test-WSMan blablah 
   $?
}
GetActiveComputers 
Test-WSMan blablah
$?

Short answer, no. Multiple reasons, starting with the fact that Workflows aren’t run as scripts, and they aren’t run in PowerShell. They’re translated to XAML and run by WWF. If WWF doesn’t know what something is (e.g., there’s no native WWF activity equivalent), it wraps it in an InlineScript and launches a new instance of PowerShell.

So in your case, Test-WSMan got run as one script. That instance of PowerShell finished, and then a new instance was launched to run $?. Obviously that second instance had no idea the first ever existed.

This is one of the big reasons Workflow is a PITA.

So how do I accomplish what I need to accomplish?
Test-WsMan does not return $bool value for me check and do some logic based on that, so I have to rely on $? to find out if Test-Wsman completed successfully or not.

First, I’m not sure $? is a really reliable check. But, if that’s what you want, manually control the InlineScript wrapping.

$worked = $false
InlineScript {
Test-WSMan
if ($_) { whatever }
}

You should have access to global variables - $worked in this case - inside the InlineScript. You’ll need to do a little testing, but the idea is that the InlineScript needs to contain everything that’s atomic to the task you’re doing. Writing Workflows are pretty drastically different from writing scripts.

Am I correct in assuming you’re trying to utilize the foreach parallel feature of workflows? I’m guessing from your workflow name you’re trying to find ‘active’ computers and you want to process the function in parallel rather than one by one. Personally I’d suggest looking at runspaces. PSRunspaceJob was a module i came across before that should help you get what you need done.

Oh, +1 to that, Peter. Yeah, if the parallelization was the whole reason for workflow, let’s find you a better way!

Yes, I’m doing it to find out if WinRM is running on computer, I need to do it in parallel since I have to process 100+ computers. I really want to use workflow and not to use any third party code to do that. Can this be done with straight up workflow parallelism?

Oh, you CAN. It’s just going to be harder. You’ll have to try the bit I showed you and see what breaks next. Just remember - it may LOOK like a PowerShell script, but it ISN’T a PowerShell script, so it behaves very differently and has its own unique rules.

I’d probably try to parallelize it with jobs, myself, but to each his own. If you’re mad for workflow, dive in!

I tried to do it with jobs but Jobs don’t have any throttling mechanism and I end up creating 100+ jobs, so it’s not solution without additional overhead of maintaining pool of jobs.
Workflow does work but I’m having issue with return values and keeping track of common variables accross workflow runs.

Here is what I come up with minimum third party code overhead, it seems to be working

$comps = "localhost", "SJVINTAPP540", "127.0.0.1", "dda"
workflow GetActiveComputers 
{
param ([string[]] $comps)
$returnValues = @()
    Foreach -parallel -ThrottleLimit 50 ($computer in $comps) {
    $workflow:returnValues += InlineScript{
    Test-WSMan $using:computer -ErrorAction SilentlyContinue | Out-Null
    If ($?) {return [pscustomobject]@{'ComputerName' =  $using:computer};} 
    }
   }
return $returnValues
}
$returnValues = GetActiveComputers $comps
$returnValues

Runspaces are not 3rd party, per-se. PSRunspaceJob module is third party in the way that it’s a PowerShell module developed by an individual, but it is simply a module that leverages the existing runspace pool class from the .NET framework with easily understandable PowerShell-format cmdlets. You CAN throttle runspaces and you can set timeouts as well (with a bit of extra work). Although I’ve not had any personal experience using the PSRunspaceJob module I’m sure you won’t regret looking it up! When I’m in front of a computer I’ll try to share a simple example of what you’re trying to accomplish using runspaces.

What would be advantage of using runspaces instead of workflow?

One, you could work in actual PowerShell, instead of PowerShell-but-not-really. The runspaces are simply PowerShell instances, running PowerShell code. Workflow isn’t PowerShell code, and it isn’t run by PowerShell - which is why you run into confusing bits. Runspaces would likely run more efficiently; WWF has a lot more overhead and is designed for long-running tasks that need to survive interruption, which is why that overhead exists. If your goal is to simply run a lot of things in parallel, Workflow isn’t actually the first tool that would come to mind. Yes, it can do that, but it isn’t its main strength, and it isn’t its headline feature.

The Runspace module is, as Peter said, just a way to make using runspaces easier. What you essentially do in .NET is spin up new threads of execution (and that’s where the throttling happens), tell each thread to spin up a PowerShell runspace, and then execute whatever code you have. You get however many threads running in parallel, and you aggregate whatever results you want. It might take some learning on your part, but it’s going to be substantially less learning than Workflow. Those threads all run a real PowerShell script, just as if you were running it in the console yourself. They use the same credential you’d use. They don’t have nearly the complexity of Workflow, and you wouldn’t need a whole separate set of documentation. It’s basically just multi-threading PowerShell; it’s not even all that unlike jobs, although jobs apply some overhead and partitioning that you wouldn’t have if you just worked directly with runspaces.

For me, the correct question is, “why would I move to Workflow instead of runspaces?” Workflow is like a 50mm cannon. Before I go for the cannon, I want to see if a 9mm handgun wild the job instead. I wouldn’t ask, “what are the advantages of a 9mm over a 50mm,” I’d ask “what about a 9mm won’t get the job done, thereby forcing me to a 50mm?” For parallelization, it’s jobs first (because they’re easy), runspaces next (a bit harder, but not much), then workflow (much harder and more complex).

Why you consider workflows hard? I already have script which is couple of lines of code which supports parallelism and throttling etc.
I would wait to see how much more complex code will be which Peter will provide and the compare performance of two.
I just like convenience of everything being built in into workflow instead of going outside of Powershell and learn .NET native threading. So for me if I already have 50mm and it works fine, why do I need to go and purchase 9mm and learn how to clean it etc if 50mm provides me this out of the box.

I consider them overly complex and poorly designed. That’s my opinion, based on my experience. You’re certainly welcome to disagree ;).

Don’s right, this recommendation is purely our opinion based on our experience. I agree with Don that I find WorkFlow overly complex and I can’t think of a single instance where it’s been necessary for me to use WorkFlow through my 5+ years using PowerShell.

Here’s an example of what you wish to accomplish by utilizing runspaces:

$Computers = 'PC001','PC002','PC003'
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,50)
$RunspacePool.Open()
$RunspaceCollection = @()
$Script = [ScriptBlock]::Create(
@'
PARAM($comp)
If(Test-WSMan -ComputerName $comp){
    [PSCustomObject]@{
        ComputerName=$comp
        Active=$True
    }
}Else{
    [PSCustomObject]@{
        ComputerName=$comp
        Active=$False
    }
}
'@
)
ForEach($computer in $Computers){
    $Powershell = [PowerShell]::Create().AddScript($Script).AddArgument($computer)
    $Powershell.RunspacePool = $RunspacePool
    [Collections.Arraylist]$RunspaceCollection += [PSCustomObject]@{
        Runspace = $Powershell.BeginInvoke()
        Powershell = $Powershell
        Computer = $computer
    }
}

While($RunspaceCollection){
    ForEach($Runspace in $RunspaceCollection.ToArray()){
        If($Runspace.Runspace.IsCompleted){
            $Runspace.PowerShell.EndInvoke($Runspace.Runspace)
            $Runspace.PowerShell.Dispose()
            $RunspaceCollection.Remove($Runspace)
        }
    }
}

$RunspacePool.Close()

I ran the above code against 388 computers in my organization (all located in the same geographical location) and ran your sample WorkFlow code against the same 388 computers, using measure-command to measure the execution time. Using runspaces the job finished in ~0:35 whereas using WorkFlow took ~2:50. This certainly showcases the additional overhead in WorkFlow as Don pointed out.

Again, these are our own opinions. I personally enjoyed taking the time to learn runspaces, even though I’m not fluent in them and still need to refer back to previous code. Feel free to disagree but I’m personally glad to give you an alternative option.

You sure your code works? I’m questioning Test-WSman usage in conditional statement (the whole reason I actually created a thread). It does not return anything ($false) all the time.

PS C:\Users\admin> ((Test-WSMan -ComputerName "dd") -eq $true)
Test-WSMan : The client cannot connect to the destination specified in the
request. Verify that the service on the destination is running and is accepting requests. Consult the logs and
documentation for the WS-Management service running on the destination, most commonly IIS or WinRM. If the destination
is the WinRM service, run the following command on the destination to analyze and configure the WinRM service: "winrm
quickconfig". 
At line:1 char:3
+ ((Test-WSMan -ComputerName "dd") -eq $true)
+   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (dd:String) [Test-WSMan], InvalidOperationException
    + FullyQualifiedErrorId : WsManError,Microsoft.WSMan.Management.TestWSManCommand

False
PS C:\Users\admim> ((Test-WSMan -ComputerName "localhost") -eq $true)
False

Test-WSMan does not return a boolean value therefore evaluating it against $True or $False will always return False. But, you don’t explicitly need a $True or $False condition in an If statement. When I run the test against computer names pulled from my Active Directory I get True and False results. I tested all of the False results and found that all of the False results did not respond to ping and/or did not have a record in DNS (we’ve got some cleanup to do in our AD for stale accounts). When I tested against True results all those results were pingable, and WinRM is enabled by GPO in our environment so I’m confident that they were not false positives.

Here I’ve turned it into a basic function so you can pass one or many computernames to it.

FUNCTION Get-WSManStatus ([string[]]$ComputerName){
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,50)
$RunspacePool.Open()
$RunspaceCollection = @()
$Script = [ScriptBlock]::Create(
@'
PARAM($comp)
If(Test-WSMan -ComputerName $comp){
    [PSCustomObject]@{
        ComputerName=$comp
        Active=$True
    }
}Else{
    [PSCustomObject]@{
        ComputerName=$comp
        Active=$False
    }
}
'@
)
ForEach($Computer in $ComputerName){
    $Powershell = [PowerShell]::Create().AddScript($Script).AddArgument($Computer)
    $Powershell.RunspacePool = $RunspacePool
    [Collections.Arraylist]$RunspaceCollection += [PSCustomObject]@{
        Runspace = $Powershell.BeginInvoke()
        Powershell = $Powershell
        Computer = $Computer
    }
}

While($RunspaceCollection){
    ForEach($Runspace in $RunspaceCollection.ToArray()){
        If($Runspace.Runspace.IsCompleted){
            $Runspace.PowerShell.EndInvoke($Runspace.Runspace)
            $Runspace.PowerShell.Dispose()
            $RunspaceCollection.Remove($Runspace)
        }
    }
}

$RunspacePool.Close()
}

So below returns $true to you? Or do you mean despite returning $false If() statement considers it successfull? Or what?

((Test-WSMan -ComputerName "localhost") -eq $true)

The following evaluation:

((Test-WSMan -ComputerName "localhost") -eq $true)

Will never return $True. This is because PowerShell cannot evaluate what is returned from Test-WSMan against $True, which is a boolean value. The following however:

If(Test-WSMan -ComputerName “localhost”){$True}Else{$False}

Will return True or False depending on whether Test-WSMan was successful or not. It’s essentially the same as what you’re doing with the $? variable, where you are testing if the last executed command was successful. I’d consider your way to be potentially unreliable however as $? is an automatic variable and it retains the value for the duration of your PowerShell host session. Evaluating your If statement against the condition of the Test-WSMan cmdlet execution directly is far more accurate and reliable.

Like I mentioned, you do not have to explicitly pass a $True or $False to an If condition for If statements to work the way you expect.