At work, we have a frequently recurring pattern involving a pair of scripts like a Controller and work that function as a pair to invoke code onto a bunch of remote servers.
Unlike these sample scripts, the controllers usually look up the set of servers based on a role or something, but never mind that. The point of this paste is to show the motivation for a module. When we run these scripts in a scheduled task, if there's an error on the remote server, we don't generally have enough information:
<# C:\PS: #> .\Controller-Original.ps1 .\Work.ps1 -Args @('NoSuchFile') -Cn Server1
Start Remote Test
Copy File To Remote: \\Server1\C$\Temp\5b5e9981-8fac-4541-a436-f411cec7e162\ChildItem.ps1
Invoke the script C:\Temp\5b5e9981-8fac-4541-a436-f411cec7e162\ChildItem.ps1 remotely on Entertainer
Enter ChildItem
Cannot find path 'C:\Temp\5b5e9981-8fac-4541-a436-f411cec7e162\NoSuchFile' because it does not exist.
+ CategoryInfo : ObjectNotFound: (C:\Temp\5b5e9981-8fac-4541-a436-f411cec7e162\NoSuchFile:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
+ PSComputerName : Server1
Exit ChildItem
Clean up remote files
End Remote Test
In a few cases, someone has added an error handler that logs the error message, which might output something like this:
writeErrorStream : True
OriginInfo : Entertainer
Exception : System.Management.Automation.RemoteException: Cannot find path 'C:\Temp\39796a26-624d-46a8-a917-d1c538c4f3b6\NoSuchFile' because it
does not exist.
TargetObject : C:\Temp\39796a26-624d-46a8-a917-d1c538c4f3b6\NoSuchFile
CategoryInfo : ObjectNotFound: (C:\Temp\39796a2...f3b6\NoSuchFile:String) [Get-ChildItem], ItemNotFoundException
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
ErrorDetails :
InvocationInfo :
ScriptStackTrace : at <ScriptBlock>, C:\Temp\39796a26-624d-46a8-a917-d1c538c4f3b6\TestScript.ps1: line 28
at <ScriptBlock>, <No file>: line 4
PipelineIterationInfo : {}
PSMessageDetails :
However. this does not tell us a whole lot, and there's absolutely nothing more available. In many cases, even the stack trace isn't logged properly, and many of the developers who're writing these tasks aren't comfortable enough with PowerShell error handling to figure out what's going wrong, or to use remote debugging, etc. This has led to our teams adding Write-Host messages (as you saw in this example) as an attempt to keep track of what has been executed so far in a script just from the logs that the build or deploy tools capture.
The Information module now includes a Trace-Info command which you can wrap around another command to automatically convert Write-Host to Write-Info and also capture all the Error information at the end. In this extreme case where we're making a remote call, we need to make a change at two levels. First, we need to wrap the code that runs remotely, and then we can tweak with the invocation.
We'll add a line to copy the module to the remote server:
Copy-Item (Get-Module Information).ModuleBase -Destination $RemotePath\Information -Recurse
And then inside the Invoke-Command, we need to import the module, and possibly, pass the InfoTemplate
to make sure the remote is logging with the same template as the local server. So we add a InfoTemplate
parameter, and then Import-Module
and Set-InfoTemplate
before wrapping everything that was there before into Trace-Info
:
Invoke-Command -ComputerName $Server -Credential $Credential -ArgumentList $FileName, $ArgumentList, (Get-InfoTemplate) {
param($FileName, $ArgumentList, $InfoTemplate)
Import-Module (Join-Path (Split-Path $FileName) Information\Information.psd1)
Set-InfoTemplate $InfoTemplate
Trace-Info {
Push-Location (Split-Path $FileName)
&$FileName @ArgumentList
}
}
The finished script is below as Controller.ps1 -- with that in place, if we set a simple template, we can call our script as before and get the same output. But if we add -InformationAction Continue
, we suddenly get a full dump of the error:
<# C:\PS: #> Set-InfoTemplate '${Message}'
<# C:\PS: #> .\Controller.ps1 .\TestScript.ps1 -Args @('NoSuchFile') -Cn Server1 -InformationAction Continue
Start Remote Test
Copy File To Remote: \\Entertainer\C$\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Work.ps1
Copy Module To Remotes: \\Entertainer\C$\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Information
Invoke the script C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Work.ps1 remotely on Entertainer
Enter TestScript
Enter TestScript
Cannot find path 'C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile' because it does not exist.
+ CategoryInfo : ObjectNotFound: (C:\Temp\a065e87...22b8\NoSuchFile:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
+ PSComputerName : Entertainer
Exit TestScript
Exit TestScript
ERROR LOG: Cannot find path 'C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile' because it does not exist.
[System.Management.Automation.ErrorRecord]
writeErrorStream : True
Exception : System.Management.Automation.ItemNotFoundException: Cannot find path
'C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile' because it does not exist.
at System.Management.Automation.SessionStateInternal.GetChildItems(String path,
Boolean recurse, UInt32 depth, CmdletProviderContext context)
at Microsoft.PowerShell.Commands.GetChildItemCommand.ProcessRecord()
TargetObject : C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
InvocationInfo : System.Management.Automation.InvocationInfo
ErrorCategory_Category : 13
ErrorCategory_Activity : Get-ChildItem
ErrorCategory_Reason : ItemNotFoundException
ErrorCategory_TargetName : C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile
ErrorCategory_TargetType : String
ErrorCategory_Message : ObjectNotFound: (C:\Temp\a065e87...22b8\NoSuchFile:String) [Get-ChildItem],
ItemNotFoundException
SerializeExtendedInfo : False
ErrorDetails_ScriptStackTrace : at <ScriptBlock>, C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Work.ps1: line 27
at <ScriptBlock>, <No file>: line 7
at <ScriptBlock>,
C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Information\Information.psm1: line 359
at Trace-Info<End>,
C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Information\Information.psm1: line 357
at <ScriptBlock>, <No file>: line 5
PSMessageDetails :
CategoryInfo : ObjectNotFound: (C:\Temp\a065e87...22b8\NoSuchFile:String) [Get-ChildItem],
ItemNotFoundException
ErrorDetails :
ScriptStackTrace : at <ScriptBlock>, C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Work.ps1: line 27
at <ScriptBlock>, <No file>: line 7
at <ScriptBlock>,
C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Information\Information.psm1: line 359
at Trace-Info<End>,
C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\Information\Information.psm1: line 357
at <ScriptBlock>, <No file>: line 5
PipelineIterationInfo : {0, 1}
[System.Management.Automation.ItemNotFoundException]
ErrorRecord : Cannot find path 'C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile' because it
does not exist.
ItemName : C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile
SessionStateCategory : Drive
WasThrownFromThrowStatement : False
Message : Cannot find path 'C:\Temp\a065e873-bb6a-48d4-b224-a0b70c0b22b8\NoSuchFile' because it
does not exist.
Data : {}
InnerException :
TargetSite : Void GetChildItems(System.String, Boolean, UInt32,
System.Management.Automation.CmdletProviderContext)
StackTrace : at System.Management.Automation.SessionStateInternal.GetChildItems(String path,
Boolean recurse, UInt32 depth, CmdletProviderContext context)
at Microsoft.PowerShell.Commands.GetChildItemCommand.ProcessRecord()
HelpLink :
Source : System.Management.Automation
HResult : -2146233087
Clean up remote files
End Remote Test
And if we use a more informative InfoTemplate
and wrap the initial call in Trace-Info
, we suddenly get much more information:
<# C:\PS: #> Set-InfoTemplate '${Env:UserName}@${Env:ComputerName} $e[38;5;1m$("{0:hh\:mm\:ss\.fff}" -f ${Time}) $(" " * ${CallStackDepth})$e[38;5;6m${Message} $e[38;5;5m<${Command}> ${ScriptName}:${LineNumber}$e[39m'
<# C:\PS: #> Trace-Info { .\Controller.ps1 .\Work.ps1 -Args @('NoSuchFile') -Cn Server1 -InformationAction Continue }
Joel@DUO 01:35:27.224 Start Remote Test <<ScriptBlock>> Remote.ps1:21
Joel@DUO 01:35:27.273 Copy File To Remote: \\Entertainer\C$\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\TestScript.ps1 <<ScriptBlock>> Remote.ps1:30
Joel@DUO 01:35:27.325 Copy Module To Remotes: \\Entertainer\C$\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\Information <<ScriptBlock>> Remote.ps1:33
Joel@DUO 01:35:27.465 Invoke the script C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\TestScript.ps1 remotely on Entertainer <<ScriptBlock>> Remote.ps1:38
Joel@ENTERTAINER 01:34:25.417 Enter TestScript <<ScriptBlock>> TestScript.ps1:6
Cannot find path 'C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\NoSuchFile' because it does not exist.
+ CategoryInfo : ObjectNotFound: (C:\Temp\1390b88...851c\NoSuchFile:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
+ PSComputerName : Entertainer
Joel@ENTERTAINER 01:34:25.586 Exit TestScript <<ScriptBlock>> TestScript.ps1:29
Joel@ENTERTAINER 01:34:25.648 ERROR LOG: Cannot find path 'C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\NoSuchFile' because it does not exist.
[System.Management.Automation.ErrorRecord]
writeErrorStream : True
Exception : System.Management.Automation.ItemNotFoundException: Cannot find path
'C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\NoSuchFile' because it does not exist.
at System.Management.Automation.SessionStateInternal.GetChildItems(String path,
Boolean recurse, UInt32 depth, CmdletProviderContext context)
at Microsoft.PowerShell.Commands.GetChildItemCommand.ProcessRecord()
TargetObject : C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\NoSuchFile
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
InvocationInfo : System.Management.Automation.InvocationInfo
ErrorCategory_Category : 13
ErrorCategory_Activity : Get-ChildItem
ErrorCategory_Reason : ItemNotFoundException
ErrorCategory_TargetName : C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\NoSuchFile
ErrorCategory_TargetType : String
ErrorCategory_Message : ObjectNotFound: (C:\Temp\1390b88...851c\NoSuchFile:String) [Get-ChildItem],
ItemNotFoundException
SerializeExtendedInfo : False
ErrorDetails_ScriptStackTrace : at <ScriptBlock>, C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\TestScript.ps1: line 27
at <ScriptBlock>, <No file>: line 7
at <ScriptBlock>,
C:\Temp\1390b885-79c4-4067-9a3d-fcda4236851c\Information\Information.psm1: line 359
Joel@DUO 02:31:55.770 Start Remote Test <<ScriptBlock>> Controller.ps1:21
Joel@DUO 02:31:55.818 Copy File To Remote: \\Entertainer\C$\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Work.ps1 <<ScriptBlock>> Controller.ps1:30
Joel@DUO 02:31:55.879 Copy Module To Remotes: \\Entertainer\C$\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Information <<ScriptBlock>> Controller.ps1:33
Joel@DUO 02:31:56.027 Invoke the script C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Work.ps1 remotely on Entertainer <<ScriptBlock>> Controller.ps1:38
Joel@ENTERTAINER 02:30:54.035 Enter TestScript <<ScriptBlock>> Work.ps1:6
Joel@ENTERTAINER 02:30:54.035 Enter TestScript <<ScriptBlock>> Work.ps1:6
Cannot find path 'C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile' because it does not exist.
+ CategoryInfo : ObjectNotFound: (C:\Temp\fdfc78f...6d27\NoSuchFile:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
+ PSComputerName : Entertainer
Joel@ENTERTAINER 02:30:54.202 Exit TestScript <<ScriptBlock>> Work.ps1:29
Joel@ENTERTAINER 02:30:54.202 Exit TestScript <<ScriptBlock>> Work.ps1:29
Joel@ENTERTAINER 02:30:54.264 ERROR LOG: Cannot find path 'C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile' because it does not exist.
[System.Management.Automation.ErrorRecord]
writeErrorStream : True
Exception : System.Management.Automation.ItemNotFoundException: Cannot find path
'C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile' because it does not exist.
at System.Management.Automation.SessionStateInternal.GetChildItems(String path,
Boolean recurse, UInt32 depth, CmdletProviderContext context)
at Microsoft.PowerShell.Commands.GetChildItemCommand.ProcessRecord()
TargetObject : C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
InvocationInfo : System.Management.Automation.InvocationInfo
ErrorCategory_Category : 13
ErrorCategory_Activity : Get-ChildItem
ErrorCategory_Reason : ItemNotFoundException
ErrorCategory_TargetName : C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile
ErrorCategory_TargetType : String
ErrorCategory_Message : ObjectNotFound: (C:\Temp\fdfc78f...6d27\NoSuchFile:String) [Get-ChildItem],
ItemNotFoundException
SerializeExtendedInfo : False
ErrorDetails_ScriptStackTrace : at <ScriptBlock>, C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Work.ps1: line 27
at <ScriptBlock>, <No file>: line 7
at <ScriptBlock>,
C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Information\Information.psm1: line 359
at Trace-Info<End>,
C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Information\Information.psm1: line 357
at <ScriptBlock>, <No file>: line 5
PSMessageDetails :
CategoryInfo : ObjectNotFound: (C:\Temp\fdfc78f...6d27\NoSuchFile:String) [Get-ChildItem],
ItemNotFoundException
ErrorDetails :
ScriptStackTrace : at <ScriptBlock>, C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Work.ps1: line 27
at <ScriptBlock>, <No file>: line 7
at <ScriptBlock>,
C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Information\Information.psm1: line 359
at Trace-Info<End>,
C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\Information\Information.psm1: line 357
at <ScriptBlock>, <No file>: line 5
PipelineIterationInfo : {0, 1}
[System.Management.Automation.ItemNotFoundException]
ErrorRecord : Cannot find path 'C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile' because it
does not exist.
ItemName : C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile
SessionStateCategory : Drive
WasThrownFromThrowStatement : False
Message : Cannot find path 'C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile' because it
does not exist.
Data : {}
InnerException :
TargetSite : Void GetChildItems(System.String, Boolean, UInt32,
System.Management.Automation.CmdletProviderContext)
StackTrace : at System.Management.Automation.SessionStateInternal.GetChildItems(String path,
Boolean recurse, UInt32 depth, CmdletProviderContext context)
at Microsoft.PowerShell.Commands.GetChildItemCommand.ProcessRecord()
HelpLink :
Source : System.Management.Automation
HResult : -2146233087
<<ScriptBlock>> Work.ps1:27
Joel@DUO 02:31:57.213 Clean up remote files <<ScriptBlock>> Controller.ps1:48
Joel@DUO 02:31:57.438 End Remote Test <<ScriptBlock>> Controller.ps1:52
Cannot find path 'C:\Temp\fdfc78f2-aba5-4619-aafa-6eb622f06d27\NoSuchFile' because it does not exist.
At C:\Users\Joel\Projects\Modules\Information\0.4.1\Information.psm1:393 char:13
+ throw $ErrorRecord
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Temp\fdfc78f...6d27\NoSuchFile:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
+ PSComputerName : Entertainer
Of course, in the console, it's colored to make things stand out ...
However, the most important thing is that it logs this easily: just add -LogPath .\Remote.clixml
and you get a full serialized copy of everything to disk. Later on, you can come back and Import-CliXml
the file and you can even examine exceptions in detail:
<# C:\PS: #> (import-clixml .\Remote.clixml | Where Tags -contains Error).MessageData.MessageData | Format-List -f
writeErrorStream : True
Exception : System.Management.Automation.RemoteException: Cannot find path 'C:\Temp\392b3818-fa63-48a3-ada2-ee735b38f0bd\NoSuchFile' because it
does not exist.
TargetObject : C:\Temp\392b3818-fa63-48a3-ada2-ee735b38f0bd\NoSuchFile
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
InvocationInfo :
ErrorCategory_Category : 13
ErrorCategory_Activity : Get-ChildItem
ErrorCategory_Reason : ItemNotFoundException
ErrorCategory_TargetName : C:\Temp\392b3818-fa63-48a3-ada2-ee735b38f0bd\NoSuchFile
ErrorCategory_TargetType : String
ErrorCategory_Message : ObjectNotFound: (C:\Temp\392b381...f0bd\NoSuchFile:String) [Get-ChildItem], ItemNotFoundException
SerializeExtendedInfo : False
ErrorDetails_ScriptStackTrace : at <ScriptBlock>, C:\Temp\392b3818-fa63-48a3-ada2-ee735b38f0bd\TestScript.ps1: line 28
at <ScriptBlock>, <No file>: line 6
at <ScriptBlock>, C:\Temp\392b3818-fa63-48a3-ada2-ee735b38f0bd\Information\Information.psm1: line 191
at Protect-Trace<End>, C:\Temp\392b3818-fa63-48a3-ada2-ee735b38f0bd\Information\Information.psm1: line 188
at <ScriptBlock>, <No file>: line 4
PSMessageDetails :