PowerShell getting logs from Jobs - powershell

I am using a PowerShell module called Logging. I use other Jobs to do some work, but unforunately the logs that I write in them are not received by the command Receive-Job.
function Get-Topic {
[CmdletBinding(DefaultParameterSetName = "Normal")]
[OutputType(ParameterSetName = "Normal")]
[OutputType(ParameterSetName = "AsJob")]
param (
[Parameter(Mandatory = $true, ParameterSetName = "Normal")]
[Parameter(Mandatory = $true, ParameterSetName = "AsJob")]
[string]
$ResourceGroupName,
[Parameter(Mandatory = $true, ParameterSetName = "Normal")]
[Parameter(Mandatory = $true, ParameterSetName = "AsJob")]
[string]
$Namespace,
[Parameter(Mandatory = $true, ParameterSetName = "Normal")]
[Parameter(Mandatory = $true, ParameterSetName = "AsJob")]
[string]
$Topic,
[Parameter(Mandatory = $true, ParameterSetName = "AsJob")]
[switch]
$AsJob
)
if ($AsJob) {
$PSBoundParameters.Remove('AsJob') | Out-Null
return Start-ThreadJob -ScriptBlock {
param(
[string]$myFunction,
[System.Collections.IDictionary]$argTable,
[System.Collections.Concurrent.ConcurrentDictionary[string, hashtable]] $loggingTargets
)
$loggingTargets.Keys | ForEach-Object { Add-LoggingTarget -Name $_ -Configuration $loggingTargets[$_] }
$cmd = [scriptblock]::Create($myFunction)
& $cmd #argTable
} -ArgumentList $MyInvocation.MyCommand.Definition, $PSBoundParameters, (Get-LoggingTarget)
}
$topicObj = Get-AzServiceBusTopic `
-ResourceGroupName $ResourceGroupName `
-Namespace $Namespace `
-Name $Topic
Write-Log -Message "Received topic $($topicObj.Name)" -Level INFO
return $topicObj
}
Is there any way to redirect the output to the parent powershell session? I saw that Write-Host works, but the logger with the Console target doesn't. Any workarounds for this?

The solution is the following:
return Start-ThreadJob -StreamingHost (Get-Host) -ScriptBlock {
param(
[string]$myFunction,
[System.Collections.IDictionary]$argTable,
[System.Collections.Concurrent.ConcurrentDictionary[string, hashtable]] $loggingTargets
)
$loggingTargets.Keys | ForEach-Object { Add-LoggingTarget -Name $_ -Configuration $loggingTargets[$_] }
$cmd = [scriptblock]::Create($myFunction)
& $cmd #argTable
} -ArgumentList $MyInvocation.MyCommand.Definition, $PSBoundParameters, (Get-LoggingTarget)
Thanks god the cmdlet Start-ThreadJob allows for such a functionality, basically the param -StreamingHost solved the deal

Related

Custom function, value from pipeline

This a function for changing local user password on a remote machine. I'd like to make it work with a value from pipeline. This works:
$x= #()
$x += Set-UserPassword -ComputerName smz0017d -User localadmin -NewPassword "1"
$x += Set-UserPassword -ComputerName smz0027d -User localadmin -NewPassword "2"
$x | Out-GridView
But with value from pipeline it doesn't. Any tips?
$x = #()
$x += [pscustomobject]#{
ComputerName = 'smzmi0027d'
User = 'localadmin'
NewPassword = 'djkufdjkuf1234'
}
$x += [pscustomobject]#{
ComputerName = 'smzmi0027d'
User = 'localadmin'
NewPassword = '1'
}
foreach ($y in $x)
{
$y | Set-UserPassword
}
The function to accept value from a pipeline. It invokes command on a remote machine and builds custom object with a result:
function Set-UserPasswordLocaly
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$User,
[Parameter(Mandatory = $true,
Position = 1)]
[string]$NewPassword
)
#TODO: Place script here
try
{
Set-LocalUser -Name $User -Password (ConvertTo-SecureString $NewPassword -AsPlainText -Force) -ErrorAction Stop
$pwdSetResult = "$user password has been changed"
$isSuccess = $true
}
catch
{
$pwdSetResult = ($_.Exception).Message
$isSuccess = $false
}
return [PSCustomObject]#{
'ComputerName' = $env:COMPUTERNAME
'isSuccess' = $isSuccess
'Message' = $pwdSetResult
}
}
function Set-UserPasswordRemotely
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$ComputerName,
[Parameter(Mandatory = $true,
Position = 1)]
[string]$User,
[Parameter(Mandatory = $true,
Position = 2)]
[string]$NewPassword
)
#TODO: Place script here
$param = #{
ComputerName = $ComputerName
ScriptBlock = ${function:Set-UserPasswordLocaly}
ArgumentList = $User, $NewPassword
ErrorAction = 'stop'
}
try
{
$invoke = Invoke-Command #param
$invokeResult = $invoke.Message
$isSuccess = $invoke.isSuccess
}
catch
{
$invokeResult = ($_.Exception).Message
$isSuccess = $false
}
return [PSCustomObject]#{
'ComputerName' = $ComputerName
'isSuccess' = $isSuccess
'Message' = $invokeResult
}
}
function Set-UserPassword
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$ComputerName,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 1)]
[string]$User,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 2)]
[string]$NewPassword,
[Parameter(Position = 3,ValueFromPipelineByPropertyName = $true)]
[string]$PasswordVersion
)
#TODO: Place script here
PROCESS
{
if ($env:COMPUTERNAME -eq $ComputerName)
{
Set-UserPasswordLocaly $User $NewPassword
}
else
{
Set-UserPasswordRemotely $ComputerName $User $NewPassword
}
}
}
The ComputerName parameter in Set-UserPassword is not configured to accept pipeline input. Change that and it'll work:
function Set-UserPassword
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 0)]
[string]$ComputerName,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 1)]
[string]$User,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 2)]
[string]$NewPassword,
[Parameter(Position = 3,ValueFromPipelineByPropertyName = $true)]
[string]$PasswordVersion
)
# I'm using Write-Host to demonstrate binding behavior, replace with your actual code
process { Write-Host $PSBoundParameters }
}
PS ~> [pscustomobject]#{
>> ComputerName = 'smzmi0027d'
>> User = 'localadmin'
>> NewPassword = '1'
>> } |Set-UserPassword
>>
[NewPassword, 1] [User, localadmin] [ComputerName, smzmi0027d]

Powershell functions, parameters and arg

I write own powershell func for debug like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName $OtherArg} catch {...} finally {...}
and use it everyway, but i need more arg after $FunctionName. is it realistic to pass many arguments in this case bec use from 0 to 10 arg. do I have to list all the arguments that can be in the parameters of the function? like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg,
[PARAMETER(Mandatory = $false)]
$OtherArg1,
[PARAMETER(Mandatory = $false)]
$OtherArg2,
[PARAMETER(Mandatory = $false)]
$OtherArg3
)
try {& $FunctionName $OtherArg OtherArg1 OtherArg2 OtherArg3 } catch {...} finally {...}
but i dont use positional parameters in code and too many named parameters in code (~100)
Interested in any ideas about this. tnx!
The magic word is Splatting. You can provide an array or a hashtable containing your arguments to a function. The splatter is written with an #VariableName instead of the $:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName #OtherArg # Watch out for the # in the OtherArg
} catch {$_} finally {}
}
$FunctionName = 'Get-ChildItem'
$Splatter = #{
Path = 'C:\'
Filter = 'Users'
Directory = $true
}
$Splatter2 = #('c:\')
StartDebug $FunctionName $Splatter
StartDebug $FunctionName $Splatter2
However if you want to use single items as $OtherArg you will have to provide them as single element array as can be seen with $Splatter2. Or extend your function to transform single arguments in arrays automatically, but thats up to you.
I think you better run it using scriptblock:
$result = Invoke-DeepDebug { Get-ChildItem -Path 'C:\' ; Get-Service -InformationAction Continue}
And in Invoke-DeepDebug you can work with $Command.AST as deep and detailed as you want.
Function Invoke-DeepDebug {
param(
[Parameter(Mandatory=$true, Position=0)]
[Scriptblock]$Command
)
Write-Host -f Cyan "Executing " -n
Write-Host -f Magenta $Command.Ast.Extent.Text -n
Write-Host -f Yellow " ... " -n
$result = $null
try {
$result = Invoke-Command $Command -ErrorAction Stop
Write-Host -f Green "OK!"
} catch {
Write-Host -f Red "Error"
Write-Host -f Red "`t$($_.Exception.Message)"
}
return $result
}

Variable Assignment Not Working With Switch Parameter

I'm doing something that seems pretty basic to me but is not working as expected.
If the script is run with the -WhatIf switch then $liveTest should be "Test".
If the script is run with the -Live switch then $liveTest should be "Live".
However both switches are causing $liveTest to be "Test"
param (
[CmdletBinding()]
[Parameter(Mandatory = $true, ParameterSetName = 'UsersOnlyLive')]
[Parameter(Mandatory = $true, ParameterSetName = 'UsersOnlyTest')]
[Switch]
$users,
[Parameter(Mandatory = $true, ParameterSetName = 'ComputersOnlyLive')]
[Parameter(Mandatory = $true, ParameterSetName = 'ComputersOnlyTest')]
[Switch]
$computers,
[Parameter(Mandatory = $true, ParameterSetName = 'AllLive')]
[Parameter(Mandatory = $true, ParameterSetName = 'AllTest')]
[Switch]
$all,
[Parameter(Mandatory=$true)]
[string]
$days,
[switch]
$console,
[Parameter(Mandatory = $true, ParameterSetName = 'AllTest')]
[Parameter(Mandatory = $true, ParameterSetName = 'UsersOnlyTest')]
[Parameter(Mandatory = $true, ParameterSetName = 'ComputersOnlyTest')]
[switch]
$WhatIf,
[Parameter(Mandatory = $true, ParameterSetName = 'AllLive')]
[Parameter(Mandatory = $true, ParameterSetName = 'UsersOnlyLive')]
[Parameter(Mandatory = $true, ParameterSetName = 'ComputersOnlyLive')]
[switch]
$live
)
Process {
# If -WhatIf or -Live switch is passed, creates a hashtable for the -WhatIf parameter.
If($WhatIf) {
$whatIf = #{ WhatIf = $true }
$liveTest = "Test"
}
ElseIf($live) {
$whatIf = #{ WhatIf = $false }
$liveTest = "Live"
}
If($liveTest = "Test"){Write-Output $liveTest}
elseif($liveTest = "Live"){Write-Output $liveTest}
}
Your if and elseif conditions are using the assignment operator = rather than comparison operator -eq. As a result, $liveTest is getting set to Test on each run. Update your code to the following:
if ($liveTest -eq "Test") {
Write-Output $liveTest
}
elseif ($liveTest -eq "Live") {
Write-Output $liveTest
}
Since you are using if and elseif conditions to do variable assignment, $liveTest = "Test" always happens and $liveTest = "Live" never happens.
See About_Comparison_Operators for more information.

Combining parameter sets

This is somewhat related to a question I asked earlier; Group parameter (set?) requiring one of the parameters
I have a set of 5 parameters; Years, Months, Days, Path, Input. I'm looking to combine parameter sets so that I can have the following combinations;
Years & (Path or Input)
Months & (Path or Input)
Days & (Path or Input)
Path is a typed path and Input is a pointer to a file, either way, one of them is required.
I've tried
[Parameter(Mandatory = $true, ParameterSetName = 'Path')]
[Parameter(Mandatory = $true, ParameterSetName = 'Input')
[int]$Years = '7'
But Path & Input are required if using Years
Param (
[parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[string[]]$Filter = '*.*',
[Parameter(Mandatory = $True, ParameterSetName = 'ByYears')]
[int]$Years = '7',
[Parameter(Mandatory = $True, ParameterSetName = 'ByMonths')]
[int]$Months,
[Parameter(Mandatory = $True, ParameterSetName = 'ByDay')]
[int]$Days,
[Parameter(Mandatory = $true, ParameterSetName = 'Path')]
[string[]]$Path,
[Parameter(Mandatory = $true, ParameterSetName = 'Input')]
[string]$Input,
[switch]$Recurse,
[Parameter(Mandatory = $true)]
[string]$ReportPath = $PWD
)
In the end I'm looking to have the possibility of only one of the combinations below;
-Years 2 -Path C:\Temp
-Years 2 -Input C:\Temp\file.txt
-Months 2 -Path C:\Temp
-Months 2 -Input C:\Temp\file.txt
-Days 2 -Path C:\Temp
-Days 2 -Input C:\Temp\file.txt
There's no way of directly expressing ParameterA and (ParameterB or ParameterC) in ParameterSets but you can create two ParameterSets: ParameterA and ParameterB and ParameterA and ParameterC which does the same thing.
If you extend that to your scenario, you'll need 6 ParameterSets:
YearsAndPath
YearsAndInput
MonthsAndPath
MonthsAndInput
DaysAndPath
DaysAndInput
And then you just tag each parameter with the ParameterSets it needs to be used in, and your param block becomes something like:
function Invoke-Params
{
param(
[parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[string[]]$Filter = '*.*',
[Parameter(Mandatory = $True, ParameterSetName = 'YearsAndPath')]
[Parameter(Mandatory = $True, ParameterSetName = 'YearsAndInput')]
[int]$Years = '7',
[Parameter(Mandatory = $True, ParameterSetName = 'MonthsAndPath')]
[Parameter(Mandatory = $True, ParameterSetName = 'MonthsAndInput')]
[int]$Months,
[Parameter(Mandatory = $True, ParameterSetName = 'DaysAndPath')]
[Parameter(Mandatory = $True, ParameterSetName = 'DaysAndInput')]
[int]$Days,
[Parameter(Mandatory = $true, ParameterSetName = 'YearsAndPath')]
[Parameter(Mandatory = $true, ParameterSetName = 'MonthsAndPath')]
[Parameter(Mandatory = $true, ParameterSetName = 'DaysAndPath')]
[string[]]$Path,
[Parameter(Mandatory = $true, ParameterSetName = 'YearsAndInput')]
[Parameter(Mandatory = $true, ParameterSetName = 'MonthsAndInput')]
[Parameter(Mandatory = $true, ParameterSetName = 'DaysAndInput')]
[string]$Input,
[switch]$Recurse,
[Parameter(Mandatory = $true)]
[string]$ReportPath = $PWD
)
}
The following then work fine:
PS> Invoke-Params -Years 5 -Path "xxx"
PS> Invoke-Params -Years 5 -Input "xxx"
and these throw an exception:
PS> Invoke-Params -Years 5
Invoke-Params : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ Invoke-Params -Years 5
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Params], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Invoke-Params
PS> Invoke-Params -Years 5 -Path "aaa" -Input "bbb"
Invoke-Params : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ Invoke-Params -Years 5 -Path "aaa" -Input "bbb"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Params], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Invoke-Params
The answer by mclayton is correct in that you would have to use 6 parameter set names.
There are three 'time' parameters that should rule each other out and must be used together with either one of two 'path' parameters.
However, you should not use $input as parameter, since this is an Automatic variable as Mathias R. Jessen already commented, and because you are setting up the $Years parameter with a default value, Mandatory should be $false on that one.
Because using combinations of parameter sets is always a puzzle, I would put the parameters in a more logical order (at least to me..) by first defining the two 'path' parameters and setting the DefaultParameterSetName to be the first of those.
By also adding Position = 0 on that first parameter, you can call the function with just the path without having to name it.
Just play around with the function below to see what combinations are possible and what the function will use:
function Show-Parameters {
[CmdletBinding(DefaultParameterSetName = 'ByFolderYears')]
param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ByFolderYears')]
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ByFolderMonths')]
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ByFolderDays')]
[Alias('Path')]
[string[]]$FolderPath,
[Parameter(Mandatory = $true, ParameterSetName = 'ByFileYears')]
[Parameter(Mandatory = $true, ParameterSetName = 'ByFileMonths')]
[Parameter(Mandatory = $true, ParameterSetName = 'ByFileDays')]
[Alias('File')]
[string]$FilePath,
[Parameter(Mandatory = $false, ParameterSetName = 'ByFolderYears')]
[Parameter(Mandatory = $false, ParameterSetName = 'ByFileYears')]
[int]$Years = 7,
[Parameter(Mandatory = $true, ParameterSetName = 'ByFolderMonths')]
[Parameter(Mandatory = $true, ParameterSetName = 'ByFileMonths')]
[int]$Months,
[Parameter(Mandatory = $true, ParameterSetName = 'ByFolderDays')]
[Parameter(Mandatory = $true, ParameterSetName = 'ByFileDays')]
[int]$Days,
[string[]]$Filter = '*.*',
[string]$ReportPath = $PWD,
[switch]$Recurse
)
# just to show what parameter the function will use
$paramsUsed = [ordered]#{}
$paramSet = $PSCmdlet.ParameterSetName
if ($paramSet.StartsWith('ByFolder')) { $paramsUsed['FolderPath'] = $FolderPath -join '; ' }
else {$paramsUsed['FilePath'] = $FilePath}
if ($paramSet.EndsWith('Years')) { $paramsUsed['Years'] = $Years }
elseif ($paramSet.EndsWith('Months')) { $paramsUsed['Months'] = $Months }
else { $paramsUsed['Days'] = $Days }
if ($Filter) { $paramsUsed['Filter'] = $Filter -join '; ' }
if ($ReportPath) { $paramsUsed['ReportPath'] = $ReportPath }
if ($Recurse) { $paramsUsed['Recurse'] = $Recurse }
Write-Host "Using ParameterSet '$paramSet'" -ForegroundColor Cyan
$paramsUsed
}
For instance
Show-Parameters 'D:\some\path', 'Z:\some\folder'
outputs
Using ParameterSet 'ByFolderYears'
Name Value
---- -----
FolderPath D:\some\path; Z:\some\folder
Years 7
Filter *.*
ReportPath C:\Users\YourName
etc.

Trying to code a function which takes a scriptblock with params and argument list, edits that scriptblock and then runs invoke-command

I am editing a function which it will invoke a command directly on the VM. The issue I keep running into is if someone passes a function declaration as a scriptblock, I get and error when calling create, because params() is not at the top of the scriptblock.
Trying to figure out how I can still set-fulllanguage first then execute a function with params.
function Invoke-DirectOnVM
{
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true)]
[CloudEngine.Configurations.EceInterfaceParameters]
$Parameters,
[Parameter(Mandatory = $true)]
[String[]]$VMNames,
[Parameter(Mandatory = $true)]
[Object]$VMCredential,
[Parameter(Mandatory = $true)]
[ScriptBlock]$ScriptBlock,
[Object[]]$ArgumentList = $null
)
{
Invoke-Command -VMName $localVMs -Credential $using:VMCredential -ScriptBlock ([ScriptBlock]::Create($("Import-Module OpenUpSession; Set-FullLanguage; `r`n" + $using:ScriptBlock)))
}
Remove the $using: from the scriptblock and it should work properly. I took the liberty of cleaning up the code a bit. The result looks like:
function Invoke-DirectOnVM
{
[CmdletBinding()]
Param (
[Parameter(Mandatory)]
[CloudEngine.Configurations.EceInterfaceParameters]
$Parameters,
[Parameter(Mandatory)]
[String[]]
$VMNames,
[Parameter(Mandatory)]
$VMCredential,
[Parameter(Mandatory)]
[ScriptBlock]
$ScriptBlock,
[Parameter()]
[Object[]]
$ArgumentList = $null
)
$PSBoundParameters.Remove("ScriptBlock")
Invoke-Command #PSBoundParameters -ScriptBlock ([ScriptBlock]::Create( "Import-Module OpenUpSession; Set-FullLanguage; `r`n" + $ScriptBlock ))
}