PowerShell and global functions - function

Why is the following code not working? According to this article the usage of global should be correct: http://technet.microsoft.com/en-us/library/ff730957.aspx
Function global:writeLog {
param($logType, $logString, $logFile)
$fileStream = New-Object IO.FileStream $logFile ,'Append','Write','Read'
$streamWriter = New-Object System.IO.StreamWriter $fileStream
$time = get-date -Format "hh:mm:ss"
$streamWriter.writeLine("[${time}][$logType] ${logString}")
$streamWriter.close()
}
$temp = {
writeLog -logType "INFO" -logString "Test" -logFile "d:\scripts\powershell\logtest.txt"
}
Start-Job -ScriptBlock $temp
get-job | receive-job -AutoRemoveJob -Wait
This is the exception that powershell throws
The term 'writeLog' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included,
verify that the path is correct and try again.
+ CategoryInfo : ObjectNotFound: (writeLog:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
+ PSComputerName : localhost

From the documentation of Start-Job:
A Windows PowerShell background job runs a command "in the background" without interacting with the current session.
Therefor, the current session scope is ignored.
Trivial Solution: Define the function inside the scriptblock.
$JobScript = {
function write-log {
....
}
write-log <parameters>
}
Alternatively, check these related questions:
Powershell: passing parameters to a job
Variables in Start-Job

PowerShell jobs actually run in a separate PowerShell process. You can see this like so:
$pid
Start-Job {$pid} | Receive-Job -Wait
Where $pid is the current PowerShell's process id.
Anything that needs to be accessed from the script that runs in the job, must be either defined in the scriptblock passed to Start-Job i.e. function defined in the script block or as parameters passed into the script block using the -ArgumentList parameter on Start-Job or the script can dot source another script (or import a module) that contains the functions it needs. Personally, I would put shared functions in a module like Utils.psm1 and then import like so:
Start-Job {param($scriptdir) Import-Module $scriptdir\Utils.psm1; ...} -Arg $PSScriptRoot

Define the function in a script block, then use
Invoke-Command with NoNewScope to get it in the current scope
The InitializationScript parameter to get it into the job
#Create Shared Functions Script Block
[scriptblock] $func = {function getPID() {write-output "Function Running in process id: $pid!"}}
#Set up functions in normal script scope so they're accessible here
Invoke-Command -NoNewScope -ScriptBlock $func
write-output "Main script started"
#run the function from the parent script
getPID
#Create background job script
$jobScript = {getPID}
#Run background job
write-output "starting background job"
Start-Job $jobScript -name "Job1" -InitializationScript $func
get-job | Receive-Job
get-job | Stop-Job
get-job | Remove-Job

Related

Powershell startjob import-module call function with arguments

So I am trying to start-job from a module I wrote.
Copy-Modules.psm1
function startcopy([string] $ShowToCopy) {
if (-not($ShowToCopy)) { return "No name provided. Doing nothing." }
} else { return "Name Provided $ShowToCopy" }
}
in the main script I am calling it as follows:
$Copyname = "test"
Start-Job -Name "copy1" -InitializationScript { Import-Module -Name .\Copy-Modules.psm1 } -ScriptBlock {startcopy} -ArgumentList $Copyname
However the arguments never seems to go through. No matter how I format or pass the argument with switch or without I always get the result No name provided. Doing nothing.
The simplest solution - assuming you need no other functions from your Copy-Modules.psm1 module - is to pass your function's body as Start-Job's -ScriptBlock argument:
Start-Job -Name "copy1" -ScriptBlock $function:startcopy -ArgumentList $Copyname
$function:startcopy uses namespace variable notation to get the startcopy's body as a script block.
Note:
This obviates the need to define your startcopy function in the scope of the background job (which is an independent session in a child process that knows nothing about the caller's state), which is what your -InitializationScript script block does.
The only limitation of this approach is that the script block won't be named, i.e. the original function name is lost, and $MyInvocation.MyCommand.Name inside the function returns the empty string.
As for what you tried:
It is the script block as a whole that receives the (invariably positional arguments passed to -ArgumentList, which you'll have to pass on explicitly to any commands called inside the script block, using the automatic $args variable:
$Copyname = "test"
# Note the use of $args[0]
Start-Job -Name "copy1" `
-InitializationScript { Import-Module -Name .\Copy-Modules.psm1 } `
-ScriptBlock { startcopy $args[0] } -ArgumentList $Copyname

Powershell Start-job invoke function argument with parameter

I have a function mainFunction that gets 2 parameters - $name will be just a regular string, and $moveFunction will be some function.
I want to start a job of a ScriptBlock ($SB) that will invoke $moveFunction with $name as his argument.
function foo($a){
Write-Output "In function foo with the argument => $a"
}
$SB = {
param($C, $fooFunction)
$fooFunction.Invoke($C)
}
function mainFunction($name, $moveFunction){
Start-Job -Name "currentJob" -ArgumentList $name, ${Function:$moveFunction} -ScriptBlock $SB
}
$j1 = mainFunction -name "output!" -moveFunction $Function:foo
I checked that $moveFunction exists in mainFunction already ($moveFunction.invoke(5) at mainFunction)
I can't find the problem in passing the function as argument in the start-job.
and from Get-Job -Name "CurrentJob" | Receive-Job I get:
You cannot call a method on a null-valued expression.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
+ PSComputerName : localhost
Any help would be appreciated.
edit:
The problem is most likely the way I pass the function as an argument (${Function:$moveFunction}
Just a rehash of my previous comment plus code example. Similar issue here. Essentially, arguments passed to Jobs and Remote commands are serialized. During the de-serialization process, functions and script blocks come out as strings instead of their original type. Fortunately it's a simple process to transform these into invokable scriptblocks using [ScriptBlock]::Create("string").
function foo {
write-host "foo"
}
function bar {
# This argument comes in as a string
param($func)
write-host "bar"
# Create scriptblock from string
$func = [ScriptBlock]::Create($func)
$func.invoke()
}
Start-Job -ArgumentList $Function:Foo -ScriptBlock $Function:Bar
Get-Job | Wait-job
Get-Job | Receive-job
You passing the same function and invoking it. You can directly use the function in the job.
Start-Job -Name "currentJob" -ArgumentList $name - ScriptBlock ${function:foo}

How to call function outside a scriptblock

I have a function that I should be able to call from any place in my powershell script.
The problem is that it doesn't identify the function in a script block.
In the following example I have the function getNumberFive which should return the number 5.
I want to be able to use this function inside a scriptblock when I start a new job and also in the end of the script.
Expected result:
Write the number 15 to the file "C:\tmp\result.txt"
Write to the console: "I like the number 5"
In reality:
Write the number 10 to the file "C:\tmp\result.txt"
Write to the console: "I like the number 5"
I can workaround this issue by defining the same function inside the scriptblock but then I will duplicate the function and this is not a good programming.
Another way is to define:
$func = {
function getNumberFive(){
return 5
}
}
$scriptBlock = {
Function printSum(){
$number = getNumberFive
$newNumber = 10 + $number # => $newNumber should be 15
$newNumber >> "C:\tmp\result.txt"
}
}
Start-Job -ScriptBlock $scriptBlock -InitializationScript $func
But in this case I won't be able to call $five = getNumberFive.
I read number of methods but I didn't understand how exactly to use them:
CALLING A POWERSHELL FUNCTION IN A START-JOB SCRIPT BLOCK WHEN IT’S DEFINED IN THE SAME SCRIPT
How to pass a named function as a parameter (scriptblock)
https://social.technet.microsoft.com/Forums/ie/en-US/485df2df-1577-4770-9db9-a9c5627dd04a/how-to-pass-a-function-to-a-scriptblock?forum=winserverpowershell
PowerShell: Pass function as a parameter
Using Invoke-Command -ScriptBlock on a function with arguments
My script:
function getNumberFive(){
return 5
}
$scriptBlock = {
Function printSum(){
# $number = getNumberFive => DOESN'T WORK
# $number = Invoke-Expression ($(get-command getNumberFive) | Select -ExpandProperty Definition) => DOESN'T WORK AS EXPECTED
# $number = &(${function:getNumberFive}) => DOESN'T WORK AS EXPECTED
# $number = &(Get-Item function:getNumberFive) => DOESN'T WORK AS EXPECTED
$newNumber = 10 + $number # => $newNumber should be 15
$newNumber >> "C:\tmp\result.txt"
}
printSum
}
Start-Job -ScriptBlock $scriptBlock
$five = getNumberFive
Write-Host "I like the number"$five
Get-Job | Wait-Job
Get-Job | Stop-Job
Get-Job | Remove-Job
When you pass a scriptblock to start-job (or invoke-expression) the PowerShell instance that executes that scriptblock only has access to that scriptblock, anything that script block loads, and anything that already exists in the PowerShell instance.
Other parts of your script are not included. (For functions locally defined in your script to be available from other, possibly remote, instances of PowerShell the whole script – not just the scriptblock – and any dependencies would need to be accessible from the other instance.)
You could refactor the code you want in both places into a module which the script block loads as well as the job creating script.
When using jobs you are executing code in another process: like any remote operation the remote executing is a separate environment.
You can do like this:
function getNumberFive(){
return 5
}
$a={
Function printSum($number){
$newNumber = 10 + $number # => $newNumber should be 15
$newNumber >> "D:\result.txt"
}
}
$scriptBlock = {
param($number)
printSum -number $number
}
$five = getNumberFive
Write-Host "I like the number"$five
Start-Job -InitializationScript $a -ScriptBlock $scriptBlock -ArgumentList $five
There is a switch name -InitializationScript in Start-job where you can keep your initialization code like a function. Wrap it in a scriptblock and simply call it from the main scriptblock.
Hope it helps.

How to pass arguments to functions in PowerShell

I have the below PowerShell script
Function Publish
{
Param(
[parameter(Mandatory=$true)]
[String]
$RELEASEDIR,
[parameter(Mandatory=$true)]
[String]
$SERVICENAME,
[parameter(Mandatory=$true)]
[String]
$SERVER
)
Get-ChildItem "$RELEASEDIR\*"
$service = Get-Service -Name $SERVICENAME -Computername $SERVER -ErrorAction SilentlyContinue
$service.Status
}
Publish
How I am executing this:
PS C:\Release\RPCPS> .\RPCPublish.ps1 -RELEASEDIR "C:\Location" -SERVICENAME "value" -SERVER "server"
cmdlet Publish at command pipeline position 1
Supply values for the following parameters:
RELEASEDIR:
Even after passing arguments while executing, the script is expecting it again. What am I doing wrong here?
If you want to execute the script by calling the .ps1 as in your example, there is no need to use a function. Your script should look just like this:
Param(
[parameter(Mandatory=$true)]
[String]
$RELEASEDIR,
[parameter(Mandatory=$true)]
[String]
$SERVICENAME,
[parameter(Mandatory=$true)]
[String]
$SERVER
)
Get-ChildItem "$RELEASEDIR\*"
$service = Get-Service -Name $SERVICENAME -Computername $SERVER -ErrorAction SilentlyContinue
$service.Status
The parameters are passed directly to the script and can be used there.
If, on the other hand, you want to establish a (reusable) function, remove just the last line from your script, which calls the function without parameters (which is why it asks for the mandatory parameters every time).
If you remove the last line, you can call the script without parameters once. After that you have a new function Publish in your current session, which you can then call with
Publish -RELEASEDIR "C:\Release\Batchfile" -SERVICENAME "AmazonSSMAgent" -SERVER "10.0.1.91"
independent of the script file.
Your script is creating a function, "Publish", (lines 1-17) and then calling it with no parameters (line 18). Since you've defined parameters as mandatory (lines 4, 7, 10), failing to supply the parameters when you call the function (line 18) causes PowerShell to request values for the unsupplied parameters.
Supplying parameters to the script file itself does not help; there is no mechanism for "automagically" passing those parameters to anything within the script (you would have to explicitly code the script for that).
As Matt suggested in the comments, dot-source your script after deleting line 18, and then call your function explicitly, passing the parameters (publish -RELEASEDIR "C:\Release\Batchfile" -SERVICENAME "AmazonSSMAgent" -SERVER "10.0.1.91").
As per my understanding your requirement is to run the function, and you have to compile the scripts also in Jenkins.
You can do something like this:
Let's say your script name is RPCPublish.ps1 and the path is D:\Folder.
And I can see your function name is Publish.
So in your case,
powershell -command "& { D:\folder\RPCPublish.ps1; Publish }"
You can pass the parameters after this in the script block.
I used a PowerShell plugin (PowerShell) and executed the same.
. "C:\Release\RPCPS\RPCPublish.ps1"
FUunctionName -RELEASEDIR "C:\bin\Release" -SERVICENAME "Service" -SERVER "$env:SERVER" -DISPLAYNAME "Services Air" -BINPATH "D:\Build\Project.exe" -DESCRIPTION "This service hosts Air service" -DESTINATION "d$\Build\"

How do I dynamically create functions that are accessible in a parent scope?

Here is an example:
function ChildF()
{
#Creating new function dynamically
$DynFEx =
#"
function DynF()
{
"Hello DynF"
}
"#
Invoke-Expression $DynFEx
#Calling in ChildF scope Works
DynF
}
ChildF
#Calling in parent scope doesn't. It doesn't exist here
DynF
I was wondering whether you could define DynF in such a way that it is "visible" outside of ChildF.
Another option would be to use the Set-Item -Path function:global:ChildFunction -Value {...}
Using Set-Item, you can pass either a string or a script block to value for the function's definition.
The other solutions are better answers to the specific question. That said, it's good to learn the most general way to create global variables:
# inner scope
Set-Variable -name DynFEx -value 'function DynF() {"Hello DynF"}' -scope global
# somewhere other scope
Invoke-Expression $dynfex
DynF
Read 'help about_Scopes' for tons more info.
You can scope the function with the global keyword:
function global:DynF {...}
A more correct and functional way to do this would be to return the function body as a script block and then recompose it.
function ChildF() {
function DynF() {
"Hello DynF"
}
return ${function:DynF}
}
$DynFEx = ChildF
Invoke-Expression -Command "function DynF { $DynFEx }"
DynF
Thanks to Richard's post. Kept having issues doing this simple thing. I revised for passing a function from local to remote.
#Method 1 Load the function from disk
$getCert = gc 'C:\MyScripts\getCert.ps1'
Invoke-Command $RemoteSrv -ScriptBlock {Set-Variable -name DefFN -value ($Args -join "`n") -scope global ; Invoke-Expression $DefFn } -ArgumentList $getCert
#Method 2 Load the function from local definition of function
Invoke-Command $RemoteSrv -ScriptBlock {Set-Variable -name DefFN -value ($Args -join "`n") -scope global ; Invoke-Expression $DefFn } -ArgumentList ('Function GetCert {'+(Get-Command GetCert).Definition+'}')
#Remote server now has function
Invoke-Command $RemoteSrv -ScriptBlock {getcert stackoverflow.com}
URL : stackoverflow.com
Expires : 12/14/2021 8:07:08 AM
SAN : DNS Name=*.askubuntu.com, DNS Name=.....
Thumbprint : ec0055be478411bafe98d11d63a5c9279ff0e173
IP : 151.101.193.69
Handle : 2866249748176
Issuer : CN=R3, O=Let's Encrypt, C=US
Subject : CN=*.stackexchange.com