1

I was reading this post about getting functions passed into a scriptblock for use with jobs:

Powershell start-job -scriptblock cannot recognize the function defined in the same file?

I get how that works by passing the function in as variable and it works for the simple example. What about a real world solution though, is there a more elegant way of handling this?

I have script I'm using to deploy changes to vendor software. It reads an xml that tells it how to navigate the environment and performs the various tasks, ie: map drives, stop services, call a perl installation script. I would like to provide a parameter to the script to allow it to run concurrently, this way if the perl script takes 5 minutes (not uncommon) and you're rolling out to 11 servers you're not waiting for the script to run for an hour.

I'm just going to post some snippets since the full script is a little lengthy. A log function:

function Log
{
    Param(
    [parameter(ValueFromPipeline=$true)]
    $InputObject,
    [parameter()]
    [alias("v")]
    $verbosity = $debug    
    )       

    $messageIndex = [array]::IndexOf($verbosityArray, $verbosity)
    $verbosityIndex = [array]::IndexOf($verbosityArray, $loggingVerbosity)

    if($messageIndex -ge $VerbosityIndex)
    {
        switch($verbosity)
        {
            $debug {Write-Host $verbosity ": " $InputObject}
            $info  {Write-Host $verbosity ": " $InputObject}
            $warn  {Write-Host $verbosity ": " $InputObject -ForegroundColor yellow}
            $error {Write-Host $verbosity ": " $InputObject -ForegroundColor red}
        }        
    }
}

Here's another function that calls the log function:

function ExecuteRollout
{
    param(
    [parameter(Mandatory=$true)]
    [alias("ses")]
    $session,
    [parameter(Mandatory=$true)]
    $command
    )

    #invoke command    
    Invoke-Command -session $session -ScriptBlock {$res = cmd /v /k `"$args[0]`"} -args $command

    #get the return code from the remote session
    $res = Invoke-Command -session $session {$res}
    Log ("Command Output: "+$res)    
    $res = [string] $res        
    $exitCode = $res.substring($res.IndexOf("ExitCode:"), 10)
    $exitCode = $exitCode.substring(9,1)     
    Log ("Exit code: "+$exitCode)

    return $exitCode
}

And lastly a snippet from my main so you can get an idea of what's going on. $target.Destinations.Destination will contain all the servers and relevant information about them that the deployment will go to. I removed some variable setup and logging to make this more compact so yes you'll see variables referenced that are never defined:

#Execute concurrently
$target.Destinations.Destination | %{
    $ScriptBlock = {

        $destination = $args[0]

        Log -v $info ("Starting remote session on: "+$destination.Server)
        $session = New-PSSession -computerName $destination.Server               

        $InitializeRemote -session $session -destination $destination

        #Gets a little tricky here, we need to keep the cmd session so it doesn't lose the sys vars set by env.bat
        #String everything together with &'s    
        $cmdString = $destDrive + ": & call "+$lesDestDir+"data\env.bat & cd "+$rolloutDir+" & perl ..\JDH-rollout-2010.pl "+$rollout+" NC,r:\les & echo ExitCode:!errorlevel!"
        Log ("cmdString: "+$cmdString)
        Log -v $info ("Please wait, executing the rollout now...")
        $exitCode = $ExecuteRollout -session $session -command $cmdString    
        Log ("ExitCode: "+$exitCode)

        #respond to return code from rollout script
        $HandleExitCode -session $session -destination $destination -exitCode $exitCode

        $CleanUpRemote -session $session -destination $destination            
    }
    Start-Job $ScriptBlock -Args $_
}

So if i go with the approach in the link I'd be converting all my functions to variables and passing them in to the script block. Currently, my log function will by default log in DEBUG unless the verbosity parameter is explicitly passed as a different verbosity. If I convert my functins to variables however powershell doesn't seem to like this syntax:

$Log ("Print this to the log")

So I think I'd need to use the parameter all the time now:

$Log ("Print this to the log" -v $debug

So bottom line it looks like I just need to pass all my functions as variables to the script block and change some formatting when I call them. It's not a huge effort, but I'd like to know if there's a better way before I start hacking my script up. Thanks for the input and for looking, I know this is quite a long post.

Community
  • 1
  • 1
JDH
  • 2,618
  • 5
  • 38
  • 48

1 Answers1

0

I started another post about passing parameters to functions stored as variables, the answer to that also resolves this issue. That post can be found here:

Powershell: passing parameters to functions stored in variables

The short answer is you can use the initializationscript parameter of Start-Job to feed all your functions in if you wrap them in a block and store that in a variable.

Example:

# concurrency
$func = {
    function Logx 
    {
        param(
        [parameter(ValueFromPipeline=$true)]
        $msg
        )    
        Write-Host ("OUT:"+$msg)
    }
}

# Execution starts here
cls
$colors = @("red","blue","green")
$colors | %{
    $scriptBlock = 
    {   
        Logx $args[0]
        Start-Sleep 9        
    } 

    Write-Host "Processing: " $_
    Start-Job -InitializationScript $func -scriptblock $scriptBlock -args $_
}

Get-Job

while(Get-Job -State "Running")
{
    write-host "Running..."
    Start-Sleep 2
}

# Output
Get-Job | Receive-Job

# Cleanup jobs
Remove-Job * 
Community
  • 1
  • 1
JDH
  • 2,618
  • 5
  • 38
  • 48