├── .gitattributes ├── .gitignore ├── Build.ps1 ├── Commands ├── DisableLogFileCommand.cs ├── DisableOutputSubscriberCommand.cs ├── EnableLogFileCommand.cs ├── EnableOutputSubscriberCommand.cs ├── GetLogFileCommand.cs ├── GetOutputSubscriberCommand.cs ├── ResumeLoggingCommand.cs └── SuspendLoggingCommand.cs ├── HostIOInterceptor.cs ├── HostIOSubscriberBase.cs ├── IHostIOSubscriber.cs ├── LICENSE ├── LogFile.cs ├── Module └── PowerShellLogging │ ├── PowerShellLogging.psd1 │ ├── PowerShellLogging.psm1 │ └── en-US │ ├── PowerShellLoggingModule.dll-help.xml │ └── about_PowerShellLogging.help.txt ├── PowerShellLoggingModule.csproj ├── PowerShellLoggingModule.sln ├── README.md ├── ScriptBlockOutputSubscriber.cs ├── StreamType.cs ├── Test.ps1 └── Tests ├── Invoke.Tests.ps1 ├── ParallelRemote.Tests.ps1 ├── ParallelRunspace.Tests.ps1 ├── Remote.Tests.ps1 ├── Runspace.Tests.ps1 └── Simple.Tests.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | 24 | *.ps1 text 25 | *.psm1 text 26 | *.psd1 text 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | .vs/ 53 | 54 | # MSTest test Results 55 | [Tt]est[Rr]esult*/ 56 | [Bb]uild[Ll]og.* 57 | 58 | *_i.c 59 | *_p.c 60 | *.ilk 61 | *.meta 62 | *.obj 63 | *.pch 64 | *.pdb 65 | *.pgc 66 | *.pgd 67 | *.rsp 68 | *.sbr 69 | *.tlb 70 | *.tli 71 | *.tlh 72 | *.tmp 73 | *.tmp_proj 74 | *.log 75 | *.vspscc 76 | *.vssscc 77 | .builds 78 | *.pidb 79 | *.log 80 | *.scc 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | 102 | # TeamCity is a build add-in 103 | _TeamCity* 104 | 105 | # DotCover is a Code Coverage Tool 106 | *.dotCover 107 | 108 | # NCrunch 109 | *.ncrunch* 110 | .*crunch*.local.xml 111 | 112 | # Installshield output folder 113 | [Ee]xpress/ 114 | 115 | # DocProject is a documentation generator add-in 116 | DocProject/buildhelp/ 117 | DocProject/Help/*.HxT 118 | DocProject/Help/*.HxC 119 | DocProject/Help/*.hhc 120 | DocProject/Help/*.hhk 121 | DocProject/Help/*.hhp 122 | DocProject/Help/Html2 123 | DocProject/Help/html 124 | 125 | # Click-Once directory 126 | publish/ 127 | 128 | # Publish Web Output 129 | *.Publish.xml 130 | *.pubxml 131 | 132 | # NuGet Packages Directory 133 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 134 | #packages/ 135 | 136 | # Windows Azure Build Output 137 | csx 138 | *.build.csdef 139 | 140 | # Windows Store app package directory 141 | AppPackages/ 142 | 143 | # Others 144 | sql/ 145 | *.Cache 146 | ClientBin/ 147 | [Ss]tyle[Cc]op.* 148 | ~$* 149 | *~ 150 | *.dbmdl 151 | *.[Pp]ublish.xml 152 | *.pfx 153 | *.publishsettings 154 | 155 | # RIA/Silverlight projects 156 | Generated_Code/ 157 | 158 | # Backup & report files from converting an old project file to a newer 159 | # Visual Studio version. Backup files are not needed, because we have git ;-) 160 | _UpgradeReport_Files/ 161 | Backup*/ 162 | UpgradeLog*.XML 163 | UpgradeLog*.htm 164 | 165 | # SQL Server files 166 | App_Data/*.mdf 167 | App_Data/*.ldf 168 | 169 | ############# 170 | ## Windows detritus 171 | ############# 172 | 173 | # Windows image file caches 174 | Thumbs.db 175 | ehthumbs.db 176 | 177 | # Folder config file 178 | Desktop.ini 179 | 180 | # Recycle Bin used on file shares 181 | $RECYCLE.BIN/ 182 | 183 | # Mac crap 184 | .DS_Store 185 | 186 | 187 | ############# 188 | ## Python 189 | ############# 190 | 191 | *.py[co] 192 | 193 | # Packages 194 | *.egg 195 | *.egg-info 196 | dist/ 197 | build/ 198 | eggs/ 199 | parts/ 200 | var/ 201 | sdist/ 202 | develop-eggs/ 203 | .installed.cfg 204 | 205 | # Installer logs 206 | pip-log.txt 207 | 208 | # Unit test / coverage reports 209 | .coverage 210 | .tox 211 | 212 | #Translations 213 | *.mo 214 | 215 | #Mr Developer 216 | .mr.developer.cfg 217 | 218 | # The default build.ps1 output folders 219 | [0-9]*\.[0-9]*\.[0-9]*/ 220 | -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | # A version number to update the output with (uses gitversion by default) 4 | $Version = $(gitversion -showvariable nugetversion), 5 | 6 | # The output folder (defaults to the version number) 7 | $OutputDirectory = $("$PSScriptRoot\$(($Version -split '[-+]',2)[0])"), 8 | 9 | # If set, removes the output folder without prompting! 10 | [switch]$Force 11 | ) 12 | 13 | $VersionPrefix, $VersionSuffix = $Version -split '[-+]', 2 14 | 15 | if (Test-Path $OutputDirectory) { 16 | Remove-Item $OutputDirectory -Recurse -Confirm:(!$Force) 17 | } 18 | 19 | Copy-Item $PSScriptRoot\Module\PowerShellLogging -Destination $OutputDirectory -recurse 20 | Set-Content $OutputDirectory\PowerShellLogging.psd1 ( 21 | (Get-Content $OutputDirectory\PowerShellLogging.psd1) -replace 22 | "(ModuleVersion\s+=\s+)'.*'", "`$1'$VersionPrefix'" -replace 23 | "(Prerelease\s+=\s+)'.*'", "`$1'$VersionSuffix'" 24 | ) 25 | 26 | dotnet build -c Release -p:VersionPrefix=$VersionPrefix -p:VersionSuffix=$VersionSuffix 27 | 28 | Get-ChildItem bin/Release -recurse -filter *.dll | 29 | Move-Item -Destination $OutputDirectory\ -------------------------------------------------------------------------------- /Commands/DisableLogFileCommand.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable MemberCanBePrivate.Global 2 | // ReSharper disable UnusedAutoPropertyAccessor.Global 3 | // ReSharper disable UnusedMember.Global 4 | 5 | namespace PSLogging.Commands 6 | { 7 | using System.Management.Automation; 8 | 9 | [Cmdlet(VerbsLifecycle.Disable, "LogFile")] 10 | public class DisableLogFileCommand : PSCmdlet 11 | { 12 | [Parameter(Mandatory = true, 13 | ValueFromPipeline = true, 14 | Position = 0)] 15 | public LogFile InputObject { get; set; } 16 | 17 | protected override void EndProcessing() 18 | { 19 | HostIOInterceptor.Instance.RemoveSubscriber(InputObject); 20 | } 21 | } // End DisableLogFileCommand class 22 | } 23 | 24 | // ReSharper restore MemberCanBePrivate.Global 25 | // ReSharper restore UnusedAutoPropertyAccessor.Global 26 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Commands/DisableOutputSubscriberCommand.cs: -------------------------------------------------------------------------------- 1 |  2 | // ReSharper disable MemberCanBePrivate.Global 3 | // ReSharper disable UnusedAutoPropertyAccessor.Global 4 | // ReSharper disable UnusedMember.Global 5 | 6 | namespace PSLogging.Commands 7 | { 8 | using System.Management.Automation; 9 | [Cmdlet(VerbsLifecycle.Disable, "OutputSubscriber")] 10 | public class DisableOutputSubscriberCommand : PSCmdlet 11 | { 12 | [Parameter(Mandatory = true, 13 | ValueFromPipeline = true, 14 | Position = 0)] 15 | public ScriptBlockOutputSubscriber InputObject { get; set; } 16 | 17 | protected override void EndProcessing() 18 | { 19 | HostIOInterceptor.Instance.RemoveSubscriber(InputObject); 20 | } 21 | } // End DisableOutputSubscriberCommand class 22 | } 23 | 24 | // ReSharper restore MemberCanBePrivate.Global 25 | // ReSharper restore UnusedAutoPropertyAccessor.Global 26 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Commands/EnableLogFileCommand.cs: -------------------------------------------------------------------------------- 1 |  2 | // ReSharper disable MemberCanBePrivate.Global 3 | // ReSharper disable UnusedAutoPropertyAccessor.Global 4 | // ReSharper disable UnusedMember.Global 5 | 6 | namespace PSLogging.Commands 7 | { 8 | using System.Management.Automation; 9 | 10 | [Cmdlet(VerbsLifecycle.Enable, "LogFile")] 11 | public class EnableLogFileCommand : PSCmdlet 12 | { 13 | private ScriptBlock errorCallback; 14 | private LogFile inputObject; 15 | private string path; 16 | private StreamType streams = StreamType.All; 17 | private string dateTimeFormat = "r"; 18 | 19 | #region Parameters 20 | 21 | [Parameter(ParameterSetName = "AttachExisting", 22 | Mandatory = true, 23 | Position = 0, 24 | ValueFromPipeline = true)] 25 | public LogFile InputObject 26 | { 27 | get { return inputObject; } 28 | set { inputObject = value; } 29 | } 30 | 31 | [Parameter(ParameterSetName = "New")] 32 | public ScriptBlock OnError 33 | { 34 | get { return errorCallback; } 35 | set { errorCallback = value; } 36 | } 37 | 38 | [Parameter(Mandatory = true, 39 | Position = 0, 40 | ParameterSetName = "New")] 41 | public string Path 42 | { 43 | get { return path; } 44 | set 45 | { 46 | path = GetUnresolvedProviderPathFromPSPath(value); 47 | } 48 | } 49 | 50 | [Parameter(ParameterSetName = "New")] 51 | public StreamType StreamType 52 | { 53 | get { return streams; } 54 | set { streams = value; } 55 | } 56 | 57 | [Parameter(ParameterSetName = "New")] 58 | public string DateTimeFormat 59 | { 60 | get { return dateTimeFormat; } 61 | set { dateTimeFormat = value; } 62 | } 63 | 64 | #endregion 65 | 66 | protected override void EndProcessing() 67 | { 68 | LogFile logFile; 69 | 70 | if (ParameterSetName == "New") 71 | { 72 | logFile = new LogFile(path, streams, errorCallback, dateTimeFormat); 73 | WriteObject(logFile); 74 | } 75 | else 76 | { 77 | logFile = inputObject; 78 | } 79 | 80 | HostIOInterceptor.Instance.AttachToHost(Host); 81 | HostIOInterceptor.Instance.AddSubscriber(logFile); 82 | } 83 | } // End AddLogFileCommand class 84 | } 85 | 86 | // ReSharper restore MemberCanBePrivate.Global 87 | // ReSharper restore UnusedAutoPropertyAccessor.Global 88 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Commands/EnableOutputSubscriberCommand.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable MemberCanBePrivate.Global 2 | // ReSharper disable UnusedAutoPropertyAccessor.Global 3 | // ReSharper disable UnusedMember.Global 4 | 5 | namespace PSLogging.Commands 6 | { 7 | using System.Management.Automation; 8 | 9 | [Cmdlet(VerbsLifecycle.Enable, "OutputSubscriber")] 10 | public class EnableOutputSubscriberCommand : PSCmdlet 11 | { 12 | private ScriptBlockOutputSubscriber inputObject; 13 | private ScriptBlock onWriteDebug; 14 | private ScriptBlock onWriteError; 15 | private ScriptBlock onWriteOutput; 16 | private ScriptBlock onWriteVerbose; 17 | private ScriptBlock onWriteWarning; 18 | 19 | #region Parameters 20 | 21 | [Parameter(ParameterSetName = "AttachExisting", 22 | Mandatory = true, 23 | ValueFromPipeline = true, 24 | Position = 0)] 25 | public ScriptBlockOutputSubscriber InputObject 26 | { 27 | get { return inputObject; } 28 | set { inputObject = value; } 29 | } 30 | 31 | [Parameter(ParameterSetName = "New")] 32 | public ScriptBlock OnWriteDebug 33 | { 34 | get { return onWriteDebug; } 35 | set { onWriteDebug = value; } 36 | } 37 | 38 | [Parameter(ParameterSetName = "New")] 39 | public ScriptBlock OnWriteError 40 | { 41 | get { return onWriteError; } 42 | set { onWriteError = value; } 43 | } 44 | 45 | [Parameter(ParameterSetName = "New")] 46 | public ScriptBlock OnWriteOutput 47 | { 48 | get { return onWriteOutput; } 49 | set { onWriteOutput = value; } 50 | } 51 | 52 | [Parameter(ParameterSetName = "New")] 53 | public ScriptBlock OnWriteVerbose 54 | { 55 | get { return onWriteVerbose; } 56 | set { onWriteVerbose = value; } 57 | } 58 | 59 | [Parameter(ParameterSetName = "New")] 60 | public ScriptBlock OnWriteWarning 61 | { 62 | get { return onWriteWarning; } 63 | set { onWriteWarning = value; } 64 | } 65 | 66 | #endregion 67 | 68 | protected override void EndProcessing() 69 | { 70 | ScriptBlockOutputSubscriber subscriber; 71 | 72 | if (ParameterSetName == "New") 73 | { 74 | subscriber = new ScriptBlockOutputSubscriber(onWriteOutput, 75 | onWriteDebug, 76 | onWriteVerbose, 77 | onWriteError, 78 | onWriteWarning); 79 | WriteObject(subscriber); 80 | } 81 | else 82 | { 83 | subscriber = inputObject; 84 | } 85 | 86 | HostIOInterceptor.Instance.AttachToHost(Host); 87 | HostIOInterceptor.Instance.AddSubscriber(subscriber); 88 | } 89 | } // End AddOutputSubscriberCommand class 90 | } 91 | 92 | // ReSharper restore MemberCanBePrivate.Global 93 | // ReSharper restore UnusedAutoPropertyAccessor.Global 94 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Commands/GetLogFileCommand.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable MemberCanBePrivate.Global 2 | // ReSharper disable UnusedAutoPropertyAccessor.Global 3 | // ReSharper disable UnusedMember.Global 4 | 5 | namespace PSLogging.Commands 6 | { 7 | using System.Management.Automation; 8 | 9 | [Cmdlet(VerbsCommon.Get, "LogFile")] 10 | public class GetLogFileCommand : PSCmdlet 11 | { 12 | private string path; 13 | 14 | [Parameter(Mandatory = false, 15 | Position = 0, 16 | ValueFromPipeline = true, 17 | ValueFromPipelineByPropertyName = true)] 18 | [ValidateNotNullOrEmpty] 19 | public string Path 20 | { 21 | get { return path; } 22 | set 23 | { 24 | path = GetUnresolvedProviderPathFromPSPath(value); 25 | } 26 | } 27 | 28 | protected override void EndProcessing() 29 | { 30 | foreach (IHostIOSubscriber subscriber in HostIOInterceptor.Instance.Subscribers) 31 | { 32 | var logFile = subscriber as LogFile; 33 | 34 | if (logFile != null && 35 | (path == null || System.IO.Path.GetFullPath(logFile.Path) == System.IO.Path.GetFullPath(path))) 36 | { 37 | WriteObject(logFile); 38 | } 39 | } 40 | } 41 | } // End GetLogFileCommand class 42 | } 43 | 44 | // ReSharper restore MemberCanBePrivate.Global 45 | // ReSharper restore UnusedAutoPropertyAccessor.Global 46 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Commands/GetOutputSubscriberCommand.cs: -------------------------------------------------------------------------------- 1 |  2 | // ReSharper disable MemberCanBePrivate.Global 3 | // ReSharper disable UnusedAutoPropertyAccessor.Global 4 | // ReSharper disable UnusedMember.Global 5 | 6 | namespace PSLogging.Commands 7 | { 8 | using System.Management.Automation; 9 | 10 | [Cmdlet(VerbsCommon.Get, "OutputSubscriber")] 11 | public class GetOutputSubscriberCommand : PSCmdlet 12 | { 13 | protected override void EndProcessing() 14 | { 15 | foreach (IHostIOSubscriber subscriber in HostIOInterceptor.Instance.Subscribers) 16 | { 17 | var scriptBlockSubscriber = subscriber as ScriptBlockOutputSubscriber; 18 | if (scriptBlockSubscriber != null) 19 | { 20 | WriteObject(scriptBlockSubscriber); 21 | } 22 | } 23 | } 24 | } // End GetOutputSubscriberCommand class 25 | } 26 | 27 | // ReSharper restore MemberCanBePrivate.Global 28 | // ReSharper restore UnusedAutoPropertyAccessor.Global 29 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Commands/ResumeLoggingCommand.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable MemberCanBePrivate.Global 2 | // ReSharper disable UnusedAutoPropertyAccessor.Global 3 | // ReSharper disable UnusedMember.Global 4 | 5 | namespace PSLogging.Commands 6 | { 7 | using System.Management.Automation; 8 | 9 | [Cmdlet(VerbsLifecycle.Resume, "Logging")] 10 | public class ResumeLoggingCommand : PSCmdlet 11 | { 12 | protected override void EndProcessing() 13 | { 14 | HostIOInterceptor.Instance.Paused = false; 15 | } 16 | } // End ResumeLoggingCommand class 17 | } 18 | 19 | // ReSharper restore MemberCanBePrivate.Global 20 | // ReSharper restore UnusedAutoPropertyAccessor.Global 21 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Commands/SuspendLoggingCommand.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable MemberCanBePrivate.Global 2 | // ReSharper disable UnusedAutoPropertyAccessor.Global 3 | // ReSharper disable UnusedMember.Global 4 | 5 | namespace PSLogging.Commands 6 | { 7 | using System.Management.Automation; 8 | 9 | [Cmdlet(VerbsLifecycle.Suspend, "Logging")] 10 | public class SuspendLoggingCommand : PSCmdlet 11 | { 12 | protected override void EndProcessing() 13 | { 14 | HostIOInterceptor.Instance.Paused = true; 15 | } 16 | } 17 | } 18 | 19 | // ReSharper restore MemberCanBePrivate.Global 20 | // ReSharper restore UnusedAutoPropertyAccessor.Global 21 | // ReSharper restore UnusedMember.Global 22 | -------------------------------------------------------------------------------- /HostIOInterceptor.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedMember.Global 2 | // ReSharper disable UnusedMethodReturnValue.Global 3 | 4 | namespace PSLogging 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.ObjectModel; 9 | using System.Management.Automation; 10 | using System.Management.Automation.Host; 11 | using System.Reflection; 12 | using System.Security; 13 | using System.Text; 14 | 15 | public class HostIOInterceptor : PSHostUserInterface 16 | { 17 | #region Fields 18 | 19 | private PSHostUserInterface externalUI; 20 | private PSHost host; 21 | public static readonly HostIOInterceptor Instance = new HostIOInterceptor(); 22 | private bool paused; 23 | private readonly List subscribers; 24 | private readonly StringBuilder writeCache; 25 | 26 | #endregion 27 | 28 | #region Constructors and Destructors 29 | 30 | private HostIOInterceptor() 31 | { 32 | externalUI = null; 33 | subscribers = new List(); 34 | writeCache = new StringBuilder(); 35 | paused = false; 36 | host = null; 37 | } 38 | 39 | #endregion 40 | 41 | #region Properties 42 | 43 | public bool Paused 44 | { 45 | get { return paused; } 46 | set { paused = value; } 47 | } 48 | 49 | public override PSHostRawUserInterface RawUI 50 | { 51 | get 52 | { 53 | return externalUI == null ? null : externalUI.RawUI; 54 | } 55 | } 56 | 57 | public IEnumerable Subscribers 58 | { 59 | get 60 | { 61 | foreach (WeakReference reference in subscribers) 62 | { 63 | var subscriber = (IHostIOSubscriber) reference.Target; 64 | if (subscriber != null) 65 | { 66 | yield return subscriber; 67 | } 68 | } 69 | } 70 | } 71 | 72 | #endregion 73 | 74 | #region Public Methods and Operators 75 | 76 | public void AddSubscriber(IHostIOSubscriber subscriber) 77 | { 78 | foreach (WeakReference reference in subscribers) 79 | { 80 | if (reference.Target == subscriber) 81 | { 82 | return; 83 | } 84 | } 85 | 86 | subscribers.Add(new WeakReference(subscriber)); 87 | } 88 | 89 | public void AttachToHost(PSHost host) 90 | { 91 | if (this.host != null) { return; } 92 | if (host == null) { return; } 93 | 94 | const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; 95 | 96 | // When PowerShell went open source, they renamed the private variables to have _underbarPrefixes 97 | if (host.Version >= new Version(6, 0)) 98 | { 99 | object uiRef = host.GetType().GetField("_internalUIRef", flags)?.GetValue(host); 100 | object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); 101 | FieldInfo externalUIField = ui.GetType().GetField("_externalUI", flags); 102 | externalUI = (PSHostUserInterface)externalUIField.GetValue(ui); 103 | externalUIField.SetValue(ui, this); 104 | } 105 | else 106 | { 107 | // Try the WindowsPowerShell version: 108 | object uiRef = host.GetType().GetField("internalUIRef", flags).GetValue(host); 109 | object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); 110 | FieldInfo externalUIField = ui.GetType().GetField("externalUI", flags); 111 | externalUI = (PSHostUserInterface)externalUIField.GetValue(ui); 112 | externalUIField.SetValue(ui, this); 113 | } 114 | 115 | this.host = host; 116 | } 117 | 118 | public void DetachFromHost() 119 | { 120 | if (host == null) { return; } 121 | 122 | const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; 123 | 124 | // When PowerShell went open source, they renamed the private variables to have _underbarPrefixes 125 | if (host.Version >= new Version(6, 0)) 126 | { 127 | object uiRef = host.GetType().GetField("_internalUIRef", flags)?.GetValue(host); 128 | object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); 129 | FieldInfo externalUIField = ui.GetType().GetField("_externalUI", flags); 130 | if (externalUIField.GetValue(ui) == this) 131 | { 132 | externalUIField.SetValue(ui, externalUI); 133 | } 134 | } 135 | else 136 | { 137 | // Try the WindowsPowerShell version: 138 | object uiRef = host.GetType().GetField("internalUIRef", flags).GetValue(host); 139 | object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); 140 | FieldInfo externalUIField = ui.GetType().GetField("externalUI", flags); 141 | if (externalUIField.GetValue(ui) == this) 142 | { 143 | externalUIField.SetValue(ui, externalUI); 144 | } 145 | } 146 | 147 | externalUI = null; 148 | host = null; 149 | } 150 | 151 | public override Dictionary Prompt(string caption, 152 | string message, 153 | Collection descriptions) 154 | { 155 | if (externalUI == null) 156 | { 157 | throw new InvalidOperationException("Unable to prompt user in headless session"); 158 | } 159 | 160 | Dictionary result = externalUI.Prompt(caption, message, descriptions); 161 | 162 | SendToSubscribers(s => s.Prompt(result)); 163 | 164 | return result; 165 | } 166 | 167 | public override int PromptForChoice(string caption, 168 | string message, 169 | Collection choices, 170 | int defaultChoice) 171 | { 172 | if (externalUI == null) 173 | { 174 | throw new InvalidOperationException("Unable to prompt user for choice in headless session"); 175 | } 176 | 177 | int result = externalUI.PromptForChoice(caption, message, choices, defaultChoice); 178 | 179 | SendToSubscribers(s => s.ChoicePrompt(choices[result])); 180 | 181 | return result; 182 | } 183 | 184 | public override PSCredential PromptForCredential(string caption, 185 | string message, 186 | string userName, 187 | string targetName) 188 | { 189 | if (externalUI == null) 190 | { 191 | throw new InvalidOperationException("Unable to prompt user for credential in headless session"); 192 | } 193 | 194 | PSCredential result = externalUI.PromptForCredential(caption, message, userName, targetName); 195 | 196 | SendToSubscribers(s => s.CredentialPrompt(result)); 197 | 198 | return result; 199 | } 200 | 201 | public override PSCredential PromptForCredential(string caption, 202 | string message, 203 | string userName, 204 | string targetName, 205 | PSCredentialTypes allowedCredentialTypes, 206 | PSCredentialUIOptions options) 207 | { 208 | if (externalUI == null) 209 | { 210 | throw new InvalidOperationException("Unable to prompt user for credential in headless session"); 211 | } 212 | 213 | PSCredential result = externalUI.PromptForCredential(caption, 214 | message, 215 | userName, 216 | targetName, 217 | allowedCredentialTypes, 218 | options); 219 | 220 | SendToSubscribers(s => s.CredentialPrompt(result)); 221 | 222 | return result; 223 | } 224 | 225 | public override string ReadLine() 226 | { 227 | if (externalUI == null) 228 | { 229 | throw new InvalidOperationException("Unable to ReadLine from host in headless session"); 230 | } 231 | 232 | string result = externalUI.ReadLine(); 233 | 234 | SendToSubscribers(s => s.ReadFromHost(result)); 235 | 236 | return result; 237 | } 238 | 239 | public override SecureString ReadLineAsSecureString() 240 | { 241 | if (externalUI == null) 242 | { 243 | throw new InvalidOperationException("Unable to ReadLineAsSecureString from host in headless session"); 244 | } 245 | 246 | return externalUI.ReadLineAsSecureString(); 247 | } 248 | 249 | public void RemoveAllSubscribers() 250 | { 251 | subscribers.Clear(); 252 | } 253 | 254 | public void RemoveSubscriber(IHostIOSubscriber subscriber) 255 | { 256 | var matches = new List(); 257 | 258 | foreach (WeakReference reference in subscribers) 259 | { 260 | if (reference.Target == subscriber) 261 | { 262 | matches.Add(reference); 263 | } 264 | } 265 | 266 | foreach (WeakReference reference in matches) 267 | { 268 | subscribers.Remove(reference); 269 | } 270 | } 271 | 272 | public override void Write(string value) 273 | { 274 | if (externalUI != null) 275 | { 276 | externalUI.Write(value); 277 | } 278 | 279 | if (!paused) 280 | { 281 | writeCache.Append(value); 282 | } 283 | } 284 | 285 | public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) 286 | { 287 | if (externalUI != null) 288 | { 289 | externalUI.Write(foregroundColor, backgroundColor, value); 290 | } 291 | 292 | if (!paused) 293 | { 294 | writeCache.Append(value); 295 | } 296 | } 297 | 298 | public override void WriteDebugLine(string message) 299 | { 300 | string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 301 | foreach (string line in lines) 302 | { 303 | string temp = line; 304 | SendToSubscribers(s => s.WriteDebug(temp.TrimEnd() + "\r\n")); 305 | } 306 | 307 | if (externalUI != null) 308 | { 309 | externalUI.WriteDebugLine(message); 310 | } 311 | } 312 | 313 | public override void WriteErrorLine(string message) 314 | { 315 | string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 316 | foreach (string line in lines) 317 | { 318 | string temp = line; 319 | SendToSubscribers(s => s.WriteError(temp.TrimEnd() + "\r\n")); 320 | } 321 | 322 | if (externalUI != null) 323 | { 324 | externalUI.WriteErrorLine(message); 325 | } 326 | } 327 | 328 | public override void WriteLine() 329 | { 330 | string[] lines = writeCache.ToString().Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 331 | foreach (string line in lines) 332 | { 333 | string temp = line; 334 | SendToSubscribers(s => s.WriteOutput(temp.TrimEnd() + "\r\n")); 335 | } 336 | 337 | writeCache.Length = 0; 338 | if (externalUI != null) { 339 | externalUI.WriteLine(); 340 | } 341 | } 342 | 343 | public override void WriteLine(string value) 344 | { 345 | string[] lines = (writeCache + value).Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 346 | foreach (string line in lines) 347 | { 348 | string temp = line; 349 | SendToSubscribers(s => s.WriteOutput(temp.TrimEnd() + "\r\n")); 350 | } 351 | 352 | writeCache.Length = 0; 353 | if (externalUI != null) { 354 | externalUI.WriteLine(value); 355 | } 356 | } 357 | 358 | public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) 359 | { 360 | string[] lines = (writeCache + value).Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 361 | foreach (string line in lines) 362 | { 363 | string temp = line; 364 | SendToSubscribers(s => s.WriteOutput(temp.TrimEnd() + "\r\n")); 365 | } 366 | 367 | writeCache.Length = 0; 368 | if (externalUI != null){ 369 | externalUI.WriteLine(foregroundColor, backgroundColor, value); 370 | } 371 | } 372 | 373 | public override void WriteProgress(long sourceId, ProgressRecord record) 374 | { 375 | SendToSubscribers(s => s.WriteProgress(sourceId, record)); 376 | 377 | if (externalUI != null) 378 | { 379 | externalUI.WriteProgress(sourceId, record); 380 | } 381 | } 382 | 383 | public override void WriteVerboseLine(string message) 384 | { 385 | string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 386 | foreach (string line in lines) 387 | { 388 | string temp = line; 389 | SendToSubscribers(s => s.WriteVerbose(temp.TrimEnd() + "\r\n")); 390 | } 391 | 392 | if (externalUI != null) 393 | { 394 | externalUI.WriteVerboseLine(message); 395 | } 396 | } 397 | 398 | public override void WriteWarningLine(string message) 399 | { 400 | string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 401 | foreach (string line in lines) 402 | { 403 | string temp = line; 404 | SendToSubscribers(s => s.WriteWarning(temp.TrimEnd() + "\r\n")); 405 | } 406 | 407 | if (externalUI != null) 408 | { 409 | externalUI.WriteWarningLine(message); 410 | } 411 | } 412 | 413 | #endregion 414 | 415 | #region Private Methods 416 | 417 | public void SendToSubscribers(Action action) 418 | { 419 | if (paused) { return; } 420 | 421 | var deadReferences = new List(); 422 | 423 | foreach (WeakReference reference in subscribers) 424 | { 425 | var subscriber = (IHostIOSubscriber) reference.Target; 426 | if (subscriber == null) 427 | { 428 | deadReferences.Add(reference); 429 | } 430 | else 431 | { 432 | action(subscriber); 433 | } 434 | } 435 | 436 | foreach (WeakReference reference in deadReferences) 437 | { 438 | subscribers.Remove(reference); 439 | } 440 | } 441 | 442 | #endregion 443 | } 444 | } 445 | 446 | // ReSharper restore UnusedMember.Global 447 | // ReSharper restore UnusedMethodReturnValue.Global -------------------------------------------------------------------------------- /HostIOSubscriberBase.cs: -------------------------------------------------------------------------------- 1 | namespace PSLogging 2 | { 3 | using System.Collections.Generic; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Host; 6 | 7 | public class HostIOSubscriberBase : IHostIOSubscriber 8 | { 9 | public virtual void ChoicePrompt(ChoiceDescription choice) {} 10 | public virtual void CredentialPrompt(PSCredential credential) {} 11 | public virtual void Prompt(Dictionary returnValue) {} 12 | public virtual void ReadFromHost(string inputText) {} 13 | public virtual void WriteDebug(string message) {} 14 | public virtual void WriteError(string message) {} 15 | public virtual void WriteOutput(string message) {} 16 | public virtual void WriteProgress(long sourceId, ProgressRecord record) { } 17 | public virtual void WriteVerbose(string message) { } 18 | public virtual void WriteWarning(string message) {} 19 | } 20 | } -------------------------------------------------------------------------------- /IHostIOSubscriber.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedParameter.Global 2 | // ReSharper disable UnusedMember.Global 3 | 4 | namespace PSLogging 5 | { 6 | using System.Collections.Generic; 7 | using System.Management.Automation; 8 | using System.Management.Automation.Host; 9 | 10 | /// 11 | /// The Logger interface. 12 | /// 13 | public interface IHostIOSubscriber 14 | { 15 | #region Public Methods and Operators 16 | 17 | #region From MWalker Solution (unused by this module) 18 | 19 | // These methods intercept input, which is not really useful for the type of logging I intend this module to perform. 20 | // The script that called these methods will already have access to the results, and the script author can choose 21 | // to display it or not (at which point it will be caught by the logging module). 22 | // 23 | // WriteProgress is also included in this unused category, because this doesn't seem to make much sense in a log file. 24 | 25 | void ChoicePrompt(ChoiceDescription choice); 26 | void CredentialPrompt(PSCredential credential); 27 | void Prompt(Dictionary returnValue); 28 | void ReadFromHost(string inputText); 29 | void WriteProgress(long sourceId, ProgressRecord record); 30 | 31 | #endregion 32 | 33 | void WriteDebug(string message); 34 | void WriteError(string message); 35 | void WriteOutput(string message); 36 | void WriteVerbose(string message); 37 | void WriteWarning(string message); 38 | 39 | #endregion 40 | } 41 | } 42 | 43 | // ReSharper restore UnusedMember.Global 44 | // ReSharper restore UnusedParameter.Global -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LogFile.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable MemberCanBePrivate.Global 2 | // ReSharper disable UnusedMember.Global 3 | 4 | namespace PSLogging 5 | { 6 | using System; 7 | using System.IO; 8 | using System.Management.Automation; 9 | 10 | public class LogFile : HostIOSubscriberBase 11 | { 12 | #region Fields 13 | 14 | private readonly string fileName; 15 | private readonly string path; 16 | 17 | #endregion 18 | 19 | #region Constructors and Destructors 20 | 21 | public LogFile(string filename, 22 | StreamType streams = StreamType.All, 23 | ScriptBlock errorCallback = null, 24 | string dateTimeFormat = "r") 25 | { 26 | fileName = System.IO.Path.GetFileName(filename); 27 | path = System.IO.Path.GetDirectoryName(filename); 28 | 29 | Streams = streams; 30 | ErrorCallback = errorCallback; 31 | DateTimeFormat = dateTimeFormat; 32 | } 33 | 34 | #endregion 35 | 36 | #region Properties 37 | 38 | public ScriptBlock ErrorCallback { get; set; } 39 | 40 | public string Path 41 | { 42 | get { return System.IO.Path.Combine(path, fileName); } 43 | } 44 | 45 | public StreamType Streams { get; set; } 46 | 47 | public string DateTimeFormat { get; set; } 48 | 49 | #endregion 50 | 51 | #region Public Methods and Operators 52 | 53 | // 54 | // IPSOutputSubscriber 55 | // 56 | 57 | public override void WriteDebug(string message) 58 | { 59 | if ((Streams & StreamType.Debug) != StreamType.Debug) 60 | { 61 | return; 62 | } 63 | if (message == null) 64 | { 65 | message = String.Empty; 66 | } 67 | 68 | try 69 | { 70 | CheckDirectory(); 71 | if (message != String.Empty) 72 | { 73 | message = String.Format("{0,-29} - [D] {1}", DateTime.UtcNow.ToString(DateTimeFormat), message); 74 | } 75 | 76 | File.AppendAllText(System.IO.Path.Combine(path, fileName), message); 77 | } 78 | catch (Exception e) 79 | { 80 | ReportError(e); 81 | } 82 | } 83 | 84 | public override void WriteError(string message) 85 | { 86 | if ((Streams & StreamType.Error) != StreamType.Error) 87 | { 88 | return; 89 | } 90 | if (message == null) 91 | { 92 | message = String.Empty; 93 | } 94 | 95 | try 96 | { 97 | CheckDirectory(); 98 | if (message.Trim() != String.Empty) 99 | { 100 | message = String.Format("{0,-29} - [E] {1}", DateTime.UtcNow.ToString(DateTimeFormat), message); 101 | } 102 | 103 | File.AppendAllText(System.IO.Path.Combine(path, fileName), message); 104 | } 105 | catch (Exception e) 106 | { 107 | ReportError(e); 108 | } 109 | } 110 | 111 | public override void WriteOutput(string message) 112 | { 113 | if ((Streams & StreamType.Output) != StreamType.Output) 114 | { 115 | return; 116 | } 117 | if (message == null) 118 | { 119 | message = String.Empty; 120 | } 121 | 122 | try 123 | { 124 | CheckDirectory(); 125 | if (message.Trim() != String.Empty) 126 | { 127 | message = String.Format("{0,-29} - {1}", DateTime.UtcNow.ToString(DateTimeFormat), message); 128 | } 129 | 130 | File.AppendAllText(System.IO.Path.Combine(path, fileName), message); 131 | } 132 | catch (Exception e) 133 | { 134 | ReportError(e); 135 | } 136 | } 137 | 138 | public override void WriteVerbose(string message) 139 | { 140 | if ((Streams & StreamType.Verbose) != StreamType.Verbose) 141 | { 142 | return; 143 | } 144 | if (message == null) 145 | { 146 | message = String.Empty; 147 | } 148 | 149 | try 150 | { 151 | CheckDirectory(); 152 | if (message.Trim() != String.Empty) 153 | { 154 | message = String.Format("{0,-29} - [V] {1}", DateTime.UtcNow.ToString(DateTimeFormat), message); 155 | } 156 | 157 | File.AppendAllText(System.IO.Path.Combine(path, fileName), message); 158 | } 159 | catch (Exception e) 160 | { 161 | ReportError(e); 162 | } 163 | } 164 | 165 | public override void WriteWarning(string message) 166 | { 167 | if ((Streams & StreamType.Warning) != StreamType.Warning) 168 | { 169 | return; 170 | } 171 | if (message == null) 172 | { 173 | message = String.Empty; 174 | } 175 | 176 | try 177 | { 178 | CheckDirectory(); 179 | if (message.Trim() != String.Empty) 180 | { 181 | message = String.Format("{0,-29} - [W] {1}", DateTime.UtcNow.ToString(DateTimeFormat), message); 182 | } 183 | 184 | File.AppendAllText(System.IO.Path.Combine(path, fileName), message); 185 | } 186 | catch (Exception e) 187 | { 188 | ReportError(e); 189 | } 190 | } 191 | 192 | #endregion 193 | 194 | #region Private Methods 195 | 196 | private void CheckDirectory() 197 | { 198 | if (!String.IsNullOrEmpty(path) && !Directory.Exists(path)) 199 | { 200 | Directory.CreateDirectory(path); 201 | } 202 | } 203 | 204 | private void ReportError(Exception e) 205 | { 206 | if (ErrorCallback == null) 207 | { 208 | return; 209 | } 210 | 211 | // ReSharper disable once EmptyGeneralCatchClause 212 | try 213 | { 214 | HostIOInterceptor.Instance.Paused = true; 215 | ErrorCallback.Invoke(this, e); 216 | } 217 | catch { } 218 | finally 219 | { 220 | HostIOInterceptor.Instance.Paused = false; 221 | } 222 | } 223 | 224 | #endregion 225 | } 226 | } 227 | 228 | // ReSharper restore MemberCanBePrivate.Global 229 | // ReSharper restore UnusedMember.Global 230 | -------------------------------------------------------------------------------- /Module/PowerShellLogging/PowerShellLogging.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleToProcess = 'PowerShellLogging.psm1' 3 | NestedModules = @( "PowerShellLoggingModule.dll" ) 4 | ModuleVersion = '1.4.0' 5 | GUID = '345320b5-bdc3-4ab6-a13e-fcb019362fe6' 6 | CompanyName = 'Home' 7 | CopyRight = 'Copyright 2017 David Wyatt' 8 | Author = 'Dave Wyatt' 9 | Description = 'Captures PowerShell console output to a log file.' 10 | PowerShellVersion = '2.0' 11 | DotNetFrameworkVersion = '2.0' 12 | FunctionsToExport = '*' 13 | CmdletsToExport = '*' 14 | VariablesToExport = '*' 15 | AliasesToExport = '*' 16 | 17 | FileList = @('PowerShellLogging.psm1','PowerShellLogging.psd1','PowerShellLoggingModule.dll','en-US\about_PowerShellLogging.help.txt','en-US\PowerShellLoggingModule.dll-help.xml') 18 | 19 | PrivateData = @{ 20 | PSData = @{ 21 | Prerelease = '' 22 | LicenseUri = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 23 | ProjectUri = 'https://github.com/dlwyatt/PowerShellLoggingModule' 24 | ReleaseNotes = 'Added support for PowerShell 6 & 7 25 | Added support for logging from runspaces in pools (i.e. without an actual host)' 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Module/PowerShellLogging/PowerShellLogging.psm1: -------------------------------------------------------------------------------- 1 | # Cmdlets and associated data types are defined in PowerShellLoggingModule.dll. 2 | # This script file just handles detaching the HostIOInterceptor object when the module unloads. 3 | 4 | $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { 5 | [PSLogging.HostIOInterceptor]::Instance.RemoveAllSubscribers() 6 | [PSLogging.HostIOInterceptor]::Instance.DetachFromHost() 7 | } 8 | -------------------------------------------------------------------------------- /Module/PowerShellLogging/en-US/PowerShellLoggingModule.dll-help.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Enable-LogFile 6 | 7 | Creates and attaches a new LogFile object, or attaches an existing LogFile object to the host interceptor. 8 | 9 | 10 | 11 | 12 | Enable 13 | LogFile 14 | 15 | 16 | 17 | 18 | This cmdlet creates a new LogFile object and attaches it to the current session's host interceptor, or attaches an existing LogFile object to the interceptor. When creating a new LogFile, the calling script MUST maintain a reference to the LogFile object in a variable to prevent it from being garbage collected and detached from the interceptor. 19 | 20 | All host output in streams specified by the -StreamType parameter is sent to the log file in addition to the console. By default, non-blank lines are prepended with the current date and time in a culture-invariant format (Get-Date -Format 'r'). Log file output from the Error, Warning, Verbose and Debug streams are also prepended with [E], [W], [V] and [D], respectively. 21 | 22 | 23 | 24 | Enable-LogFile 25 | 26 | Path 27 | 28 | Specifies the path to the desired log file. 29 | 30 | String 31 | 32 | 33 | StreamType 34 | 35 | Specifies which streams should be logged to this file. Valid options are 'Output', 'Verbose', 'Warning' 'Error', 'Debug', and 'All'. You can specify multiple streams in a single comma-separated string, such as 'Output, Error, Warning'. 36 | 37 | StreamType 38 | 39 | 40 | DateTimeFormat 41 | 42 | Specifies the format of timestamps that should be prepended to lines in the log file. Default is 'r'. 43 | 44 | String 45 | 46 | 47 | OnError 48 | 49 | Specifies a script block to be executed if the PSLogFile object encounters any exceptions when attempting to append to the specified log file. Arguments to this script block are a reference to the PSLogFile object ($args[0]) and the exception that was thrown ($args[1]). The Host Interceptor temporarily suspends redirection while this Script Block is executing, so you don't have to worry about console output produced by the OnError script block causing infinite recursion. 50 | 51 | While not required, it is recommended that the OnError script block disable this log file by calling $args[0] | Disable-LogFile , so the OnError block doesn't have to be called repeatedly for every line of output. 52 | 53 | ScriptBlock 54 | 55 | 56 | 57 | Enable-LogFile 58 | 59 | InputObject 60 | 61 | The existing LogFile object to be attached to the host interceptor. 62 | 63 | LogFile 64 | 65 | 66 | 67 | 68 | 69 | Path 70 | 71 | Specifies the path to the desired log file. 72 | 73 | String 74 | 75 | String 76 | 77 | 78 | 79 | 80 | 81 | StreamType 82 | 83 | Specifies which streams should be logged to this file. Valid options are 'Output', 'Verbose', 'Warning' 'Error', 'Debug', and 'All'. You can specify multiple streams in a single comma-separated string, such as 'Output, Error, Warning'. 84 | 85 | StreamType 86 | 87 | StreamType 88 | 89 | 90 | All 91 | 92 | 93 | DateTimeFormat 94 | 95 | Specifies the format of timestamps that should be prepended to lines in the log file. Default is 'r'. 96 | 97 | String 98 | 99 | String 100 | 101 | 102 | r 103 | 104 | 105 | OnError 106 | 107 | Specifies a script block to be executed if the PSLogFile object encounters any exceptions when attempting to append to the specified log file. Arguments to this script block are a reference to the PSLogFile object ($args[0]) and the exception that was thrown ($args[1]). The Host Interceptor temporarily suspends redirection while this Script Block is executing, so you don't have to worry about console output produced by the OnError script block causing infinite recursion. 108 | 109 | While not required, it is recommended that the OnError script block disable this log file by calling $args[0] | Disable-LogFile , so the OnError block doesn't have to be called repeatedly for every line of output. 110 | 111 | ScriptBlock 112 | 113 | ScriptBlock 114 | 115 | 116 | null 117 | 118 | 119 | InputObject 120 | 121 | The existing LogFile object to be attached to the host interceptor. 122 | 123 | LogFile 124 | 125 | LogFile 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | PowerShellLogging.LogFile (if attaching an existing object) 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | PowerShellLogging.LogFile (If creating a new object) 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | None (If attaching an existing object) 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | Disable-LogFile 178 | 179 | 180 | 181 | Get-LogFile 182 | 183 | 184 | 185 | Suspend-Logging 186 | 187 | 188 | 189 | Resume-Logging 190 | 191 | 192 | 193 | about_PowerShellLogging 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | Enable-OutputSubscriber 202 | 203 | Attaches a ScriptBlockOutputSubscriber to the current host interceptor, or attaches an existing ScriptBlockOutputSubscriber object to the interceptor. 204 | 205 | 206 | 207 | 208 | Add 209 | OutputSubscriber 210 | 211 | 212 | 213 | 214 | This cmdlet creates a new ScriptBlockOutputSubscriber object and attaches it to the current session's host interceptor, or attaches an existing ScriptBlockOutputSubscriber object to the interceptor. When creating a new ScriptBlockOutputSubscriber, the calling script MUST maintain a reference to the resulting object in a variable to prevent it from being garbage collected and detached from the interceptor. 215 | 216 | Every time a line is written to the console via the Output, Error, Warning, Verbose or Debug streams, the corresponding script block is called. The script block will be sent a single string argument containing the line to be written (including the trailing "`r`n" characters). 217 | 218 | This is a generic implementation of the HostIoInterceptor / HostIoSubscriber classes that allows script writers to develop their own logging logic rather than using the included PSLogFile class. 219 | 220 | 221 | 222 | Enable-OutputSubscriber 223 | 224 | OnWriteOutput 225 | 226 | The script block to be executed when a line of normal host output is displayed. 227 | 228 | ScriptBlock 229 | 230 | 231 | OnWriteDebug 232 | 233 | The script block to be executed when a line of debug output is displayed. 234 | 235 | ScriptBlock 236 | 237 | 238 | OnWriteVerbose 239 | 240 | The script block to be executed when a line of verbose output is displayed. 241 | 242 | ScriptBlock 243 | 244 | 245 | OnWriteError 246 | 247 | The script block to be executed when a line of error output is displayed. 248 | 249 | ScriptBlock 250 | 251 | 252 | OnWriteWarning 253 | 254 | The script block to be executed when a line of warning output is displayed. 255 | 256 | ScriptBlock 257 | 258 | 259 | 260 | Enable-OutputSubscriber 261 | 262 | InputObject 263 | 264 | The existing ScriptBlockOutputSubscriber object to be attached to the host interceptor. 265 | 266 | ScriptBlockOutputSubscriber 267 | 268 | 269 | 270 | 271 | 272 | OnWriteOutput 273 | 274 | The script block to be executed when a line of normal host output is displayed. 275 | 276 | ScriptBlock 277 | 278 | ScriptBlock 279 | 280 | 281 | null 282 | 283 | 284 | OnWriteDebug 285 | 286 | The script block to be executed when a line of debug output is displayed. 287 | 288 | ScriptBlock 289 | 290 | ScriptBlock 291 | 292 | 293 | null 294 | 295 | 296 | OnWriteVerbose 297 | 298 | The script block to be executed when a line of verbose output is displayed. 299 | 300 | ScriptBlock 301 | 302 | ScriptBlock 303 | 304 | 305 | null 306 | 307 | 308 | OnWriteError 309 | 310 | The script block to be executed when a line of error output is displayed. 311 | 312 | ScriptBlock 313 | 314 | ScriptBlock 315 | 316 | 317 | null 318 | 319 | 320 | OnWriteWarning 321 | 322 | The script block to be executed when a line of warning output is displayed. 323 | 324 | ScriptBlock 325 | 326 | ScriptBlock 327 | 328 | 329 | null 330 | 331 | 332 | InputObject 333 | 334 | The existing ScriptBlockOutputSubscriber object to be attached to the host interceptor. 335 | 336 | ScriptBlockOutputSubscriber 337 | 338 | ScriptBlockOutputSubscriber 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | PowerShellLogging.ScriptBlockOutputSubscriber (if attaching an existing object) 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | PowerShellLogging.ScriptBlockOutputSubscriber (if creating a new object) 360 | 361 | 362 | 363 | 364 | 365 | 366 | None (if attaching an existing object) 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | Get-OutputSubscriber 388 | 389 | 390 | 391 | Disable-OutputSubscriber 392 | 393 | 394 | 395 | about_PowerShellLogging 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | Get-LogFile 404 | 405 | Retrieves active LogFile objects. 406 | 407 | 408 | 409 | 410 | Get 411 | LogFile 412 | 413 | 414 | 415 | 416 | Retrieves LogFile objects which are currently attached to the session's host interceptor. 417 | 418 | 419 | 420 | Get-LogFile 421 | 422 | Path 423 | 424 | Path to a file for which you want to retrieve the associated PSLogFile object. If not specified, all active PSLogFile objects are returned. 425 | 426 | String 427 | 428 | 429 | 430 | 431 | 432 | Path 433 | 434 | Path to a file for which you want to retrieve the associated PSLogFile object. If not specified, all active PSLogFile objects are returned. 435 | 436 | String 437 | 438 | String 439 | 440 | 441 | null 442 | 443 | 444 | 445 | 446 | 447 | String 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | PowerShellLogging.LogFile 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | -------------- Example 1 -------------- 479 | 480 | 481 | 482 | PS C:\> Get-LogFile 483 | 484 | Returns all PSLogFile objects currently attached to the host interceptor. 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | -------------- Example 2 -------------- 494 | 495 | 496 | 497 | PS C:\> Get-LogFile -Path '.\log.txt' 498 | 499 | Retrieves PSLogFiles matching the specified path. 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | Enable-LogFile 511 | 512 | 513 | 514 | Disable-LogFile 515 | 516 | 517 | 518 | about_PowerShellLogging 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | Get-OutputSubscriber 527 | 528 | Retrieves active ScriptBlockOutputSubscriber objects. 529 | 530 | 531 | 532 | 533 | Get 534 | OutputSubscriber 535 | 536 | 537 | 538 | 539 | Retrieves ScriptBlockOutputSubscriber objects which are currently attached to the session's host interceptor. 540 | 541 | 542 | 543 | Get-OutputSubscriber 544 | 545 | 546 | 547 | 548 | 549 | 550 | None 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | PowerShellLogging.ScriptBlockOutputSubscriber 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | Enable-OutputSubscriber 584 | 585 | 586 | 587 | Disable-OutputSubscriber 588 | 589 | 590 | 591 | about_PowerShellLogging 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | Disable-LogFile 600 | 601 | Detaches a LogFile object from the session's host interceptor. 602 | 603 | 604 | 605 | 606 | Remove 607 | LogFile 608 | 609 | 610 | 611 | 612 | If the specified LogFile object is currently attached to the session's host interceptor, it will be removed. 613 | 614 | 615 | 616 | Disable-LogFile 617 | 618 | InputObject 619 | 620 | The LogFile object to be detached. 621 | 622 | LogFile 623 | 624 | 625 | 626 | 627 | 628 | InputObject 629 | 630 | The LogFile object to be detached. 631 | 632 | LogFile 633 | 634 | LogFile 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | PowerShellLogging.LogFile 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | None. This command does not write output to the pipeline. 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | Enable-LogFile 677 | 678 | 679 | 680 | Get-LogFile 681 | 682 | 683 | 684 | about_PowerShellLogging 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | Disable-OutputSubscriber 693 | 694 | Detaches a ScriptBlockOutputSubscriber object from the session's host interceptor. 695 | 696 | 697 | 698 | 699 | Remove 700 | OutputSubscriber 701 | 702 | 703 | 704 | 705 | If the specified ScriptBlockOutputSubscriber object is currently attached to the session's host interceptor, it will be removed. 706 | 707 | 708 | 709 | Disable-OutputSubscriber 710 | 711 | InputObject 712 | 713 | The ScriptBlockOutputSubscriber object to be detached. 714 | 715 | ScriptBlockOutputSubscriber 716 | 717 | 718 | 719 | 720 | 721 | InputObject 722 | 723 | The ScriptBlockOutputSubscriber object to be detached. 724 | 725 | ScriptBlockOutputSubscriber 726 | 727 | ScriptBlockOutputSubscriber 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | PowerShellLogging.ScriptBlockOutputSubscriber 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | None 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | Enable-OutputSubscriber 770 | 771 | 772 | 773 | Get-OutputSubscriber 774 | 775 | 776 | 777 | about_PowerShellLogging 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | Resume-Logging 786 | 787 | Signals the host interceptor to resume sending events to subscribers. 788 | 789 | 790 | 791 | 792 | Resume 793 | Logging 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | Resume-Logging 803 | 804 | 805 | 806 | 807 | 808 | 809 | None 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | None 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | Suspend-Logging 843 | 844 | 845 | 846 | about_PowerShellLogging 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | Suspend-Logging 855 | 856 | Prevent the current session's host interceptor from sending events to subscribers. 857 | 858 | 859 | 860 | 861 | Suspend 862 | Logging 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | Suspend-Logging 872 | 873 | 874 | 875 | 876 | 877 | 878 | None 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | None 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | Resume-Logging 912 | 913 | 914 | 915 | about_PowerShellLogging 916 | 917 | 918 | 919 | Unknown 920 | 921 | 922 | 923 | 924 | -------------------------------------------------------------------------------- /Module/PowerShellLogging/en-US/about_PowerShellLogging.help.txt: -------------------------------------------------------------------------------- 1 | TOPIC 2 | about_PowerShellLogging 3 | 4 | SHORT DESCRIPTION 5 | Provides background information about the PowerShellLogging module. 6 | 7 | LONG DESCRIPTION 8 | This topic provides information about the PowerShellLogging.IHostIoSubscriber 9 | interface, the PowerShellLogging.LogFile and PowerShellLogging.ScriptBlockOutputSubscriber 10 | classes that implement it, and the Windows PowerShell cmdlets used to 11 | manage objects of these types. 12 | 13 | About PowerShellLogging 14 | The PowerShellLogging module helps to solve the problem of how to capture 15 | all output host, and tee it to a log file (including Error, Warning, 16 | Verbose, and Debug streams), without relying on the user who called 17 | a script to redirect those streams to a log file, and without relying 18 | on calls to specific cmdlets or functions. An existing PowerShell 19 | script can achieve full logging by importing this module and executing 20 | the Enable-LogFile cmdlet. 21 | 22 | It accomplishes this by using .NET Reflection to access private members 23 | of the PowerShell classes. This is a double-edged sword, as any time 24 | Microsoft releases a new version of PowerShell (or even a patch to the 25 | existing version), this module's functionality may break and require an 26 | update. If you prefer not to use this approach, there is a script-based 27 | module with similar (though less complete) functionality available at 28 | http://gallery.technet.microsoft.com/scriptcenter/Write-timestamped-output-4ff1565f 29 | 30 | PowerShellLogging.HostIoInterceptor and PowerShellLogging.IHostIoSubscriber 31 | The heart of this module is the HostIoInterceptor class, which is a subclass 32 | of Microsoft's System.Management.Automation.Host.PSHostUserInterface class 33 | and acts as a middle man between the InternalHostUserInterface class and its 34 | externalUI field. Every time the PowerShell host sends text output to the 35 | console or ISE output window, it does so via calls to the externalUI field. 36 | 37 | This module replaces the externalUI field with an instace of the 38 | HostIoInterceptor object. HostIoInterceptor passes calls on to the original 39 | externalUI object, and also to any number of objects implementing the 40 | PowerShellLogging.IHostIoSubscriber interface, with one minor difference: when passing 41 | output to the subscribers, it is broken up into lines, and the strings sent 42 | to the subscriber include the trailing carriage return/newline characters. 43 | 44 | The module provides two classes that implement IHostIoSubscriber out of the 45 | box: PowerShellLogging.LogFile and PowerShellLogging.ScriptBlockOutputSubscriber. Both of 46 | these classes focus only on host Output (Output, Error, Warning, Verbose and 47 | Debug streams). The LogFile class has certain hard-coded behavior that I 48 | desire on my own scripts and log files: it prepends each non-blank line of 49 | output with a date/time string (in a culture-invariant format to avoid confusion 50 | over time zones or date formats). The ScriptBlockOutputSubscriber class exists 51 | to provide a dynamic alternative for people who might not want the same logging 52 | behavior that I use; it simply executes script blocks for each type of output, 53 | and allows the script writer to decide what to do with the text. 54 | 55 | Please note that this is very low-level interception of text, after PowerShell's 56 | formatting cmdlets have already processed the objects in the pipeline. You cannot 57 | intercept an ErrorRecord object via the HostInterceptor, for example; you can only 58 | intercept the several lines of text that are displayed when an ErrorRecord object 59 | is displayed in the console. This works very well when creating text-based log files, 60 | but will be of little use if you want to add an Event Log entry or database record 61 | for an entire error record. 62 | 63 | PowerShellLogging Cmdlets 64 | 65 | When the PowerShellLogging module is loaded, the following Cmdlets are available: 66 | 67 | Cmdlet Descriptions 68 | --------- -------------------------------- 69 | Enable-LogFile Creates or attaches a LogFile to the interceptor. 70 | Get-LogFile Retrieves references to attached LogFile objects. 71 | Disable-LogFile Detaches a LogFile object from the interceptor. 72 | Enable-OutputSubscriber Creates or attaches a ScriptBlockOutputSubscriber. 73 | Get-OutputSubscriber Retrieves references to attached ScriptBlockOutputSubscribers. 74 | Disable-OutputSubscriber Detaches a ScriptBlockOutputSubscriber. 75 | Suspend-Logging Temporarily stops the interceptor from making calls to subscribers. 76 | Resume-Logging Resumes interceptor-to-subscriber calls. 77 | 78 | Notes 79 | When creating new references to LogFile or OutputSubscriber objects, the calling script 80 | must maintain a reference to the resulting objects for as long as they intend the subscriber 81 | to remain active. As soon as the script releases the reference (either intentionally, or 82 | due to the variable falling out of scope), the subscriber object becomes eligible for garbage 83 | collection, and will be detached within a short time. 84 | 85 | This is to make sure that subscribers don't remain active for an entire PowerShell session 86 | by mistake, if a script author forgets to call Disable-LogFile / Disable-OutputSubscriber, 87 | or if the script crashes before that cleanup code can be called. It is still possible for 88 | such objects to remain active after a script ends, if the script author chooses to place 89 | those variables in the Global scope. 90 | 91 | This logging technique creates certain "circular reference" style situations which may 92 | cause infinite output to either the console, or worse, to a log file. For example, 93 | creating a LogFile object and then executing Get-Content on its file (allowing the output 94 | of Get-Content to be displayed to the screen) will run forever until either cancelled, 95 | or until it errors out for some reason (probably because you've just filled your hard disk). 96 | 97 | To be on the safe side, I'd recommend not attempting to read the log file at all until 98 | after you've called Disable-LogFile. 99 | 100 | If, for some reason, you need to perform an action like this, that's what the Suspend-Logging 101 | and Resume-Logging Cmdlets are for. 102 | 103 | SEE ALSO 104 | Enable-LogFile 105 | Get-LogFile 106 | Disable-LogFile 107 | Enable-OutputSubscriber 108 | Get-OutputSubscriber 109 | Disable-OutputSubscriber 110 | Suspend-Logging 111 | Resume-Logging -------------------------------------------------------------------------------- /PowerShellLoggingModule.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 13 | 14 | netstandard2.0 15 | 16 | 1.4.0 17 | Library 18 | embedded 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 31 | 32 | 33 | 34 | 35 | Designer 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /PowerShellLoggingModule.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.352 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellLoggingModule", "PowerShellLoggingModule.csproj", "{9793E825-3711-4C3E-8F12-A06DA73BBEB3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9793E825-3711-4C3E-8F12-A06DA73BBEB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {9793E825-3711-4C3E-8F12-A06DA73BBEB3}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {9793E825-3711-4C3E-8F12-A06DA73BBEB3}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {9793E825-3711-4C3E-8F12-A06DA73BBEB3}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {72D99336-7067-4FE7-901A-601CAC37CFF3} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PowerShellLoggingModule 2 | ======================= 3 | Uses Reflection to intercept text output headed for the PowerShell console. 4 | All lines of output are sent to any number of subscriber objects 5 | producing complete log files of script output (adding date/timestamps) 6 | without needing extra code by the script author. 7 | 8 | The upside is that any text which would have shown up in the console is logged, 9 | and the downside is that only that text is logged. 10 | For example, verbose output is only logged if VerbosePreference is Continue... 11 | 12 | 13 | Supports PowerShell 2, 3, 4, 5, 6 and 7 14 | 15 | 16 | Install from the PowerShell Gallery 17 | ================================== 18 | 19 | ```posh 20 | Install-Module PowerShellLogging 21 | ``` 22 | 23 | Compile your own copy 24 | ==================== 25 | 26 | You can compile the assembly with: 27 | 28 | ```posh 29 | dotnet build -c Release 30 | ``` 31 | 32 | To generate the full module, you can run the build script: 33 | 34 | ```posh 35 | .\build -Version $(gitversion -showvariable nugetversion) 36 | ``` 37 | 38 | **Note:** this script builds into a version numbered folder in the module root. 39 | The expectation is that you have the source in a folder like `~\Projects\Modules\PowerShellLogging` 40 | where the parent folder can be added to your PSModulePath for testing purposes, 41 | so the build will end up in, e.g.: `~\Projects\Modules\PowerShellLogging\1.4.0` 42 | and the build script will update the metadata to make it all versioned properly! 43 | 44 | Testing 45 | ======= 46 | 47 | The test cases are very minimal (basically just covering the fact that it logs, and testing a couple of edge cases where it used to fail to log). Despite that, weand have some problems due to the way that WindowsPowerShell breaks when they fail. 48 | 49 | As a result, _to be sure that the tests are actually working_ (reporting the correct results), you should run each test case in a new session. There is a wrapper script `test.ps1` which you can use to do that, it basically just runs each test case in `PowerShell` and `pwsh` to ensure everything is working in both WindowsPowerShell and PowerShell core. E.g.: 50 | 51 | ``` 52 | foreach ($testcase in Get-ChildItem Tests\*.Tests.ps1) { 53 | powershell -NoProfile -Command Invoke-Pester $testcase.FullName 54 | } 55 | ``` 56 | 57 | Alternative Download 58 | =================== 59 | 60 | Note, the original version is also available from: 61 | 62 | http://gallery.technet.microsoft.com/scriptcenter/Enhanced-Script-Logging-27615f85 63 | -------------------------------------------------------------------------------- /ScriptBlockOutputSubscriber.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable MemberCanBePrivate.Global 2 | // ReSharper disable UnusedMember.Global 3 | 4 | namespace PSLogging 5 | { 6 | using System.Management.Automation; 7 | 8 | public class ScriptBlockOutputSubscriber : HostIOSubscriberBase 9 | { 10 | public ScriptBlockOutputSubscriber(ScriptBlock onWriteOutput, 11 | ScriptBlock onWriteDebug, 12 | ScriptBlock onWriteVerbose, 13 | ScriptBlock onWriteError, 14 | ScriptBlock onWriteWarning) 15 | { 16 | OnWriteOutput = onWriteOutput; 17 | OnWriteDebug = onWriteDebug; 18 | OnWriteVerbose = onWriteVerbose; 19 | OnWriteError = onWriteError; 20 | OnWriteWarning = onWriteWarning; 21 | } 22 | 23 | public ScriptBlockOutputSubscriber() : this(null, null, null, null, null) {} 24 | 25 | public ScriptBlock OnWriteDebug { get; set; } 26 | public ScriptBlock OnWriteOutput { get; set; } 27 | public ScriptBlock OnWriteError { get; set; } 28 | public ScriptBlock OnWriteVerbose { get; set; } 29 | public ScriptBlock OnWriteWarning { get; set; } 30 | 31 | public override void WriteDebug(string message) 32 | { 33 | if (OnWriteDebug != null) 34 | { 35 | OnWriteDebug.Invoke(message); 36 | } 37 | } 38 | 39 | public override void WriteError(string message) 40 | { 41 | if (OnWriteError != null) 42 | { 43 | OnWriteError.Invoke(message); 44 | } 45 | } 46 | 47 | public override void WriteOutput(string message) 48 | { 49 | if (OnWriteOutput != null) 50 | { 51 | OnWriteOutput.Invoke(message); 52 | } 53 | } 54 | 55 | public override void WriteVerbose(string message) 56 | { 57 | if (OnWriteVerbose != null) 58 | { 59 | OnWriteVerbose.Invoke(message); 60 | } 61 | } 62 | 63 | public override void WriteWarning(string message) 64 | { 65 | if (OnWriteWarning != null) 66 | { 67 | OnWriteWarning.Invoke(message); 68 | } 69 | } 70 | } 71 | } 72 | 73 | // ReSharper restore MemberCanBePrivate.Global 74 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /StreamType.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedMember.Global 2 | 3 | namespace PSLogging 4 | { 5 | using System; 6 | 7 | [Flags] 8 | public enum StreamType 9 | { 10 | None = 0, 11 | Output = 1, 12 | Verbose = 2, 13 | Warning = 4, 14 | Error = 8, 15 | Debug = 16, 16 | All = Output | Verbose | Warning | Error | Debug 17 | } 18 | } 19 | 20 | // ReSharper restore UnusedMember.Global -------------------------------------------------------------------------------- /Test.ps1: -------------------------------------------------------------------------------- 1 | # The tests in here do not work properly in PowerShell 5.x 2 | # If you run them all at once, you will get a lot of FALSE PASSES using the old code 3 | # Running one test at a time in a new powershell session solves this problem: 4 | if (Get-Command powershell.exe -ErrorAction SilentlyContinue) { 5 | foreach ($testcase in ls $PSScriptRoot\Tests\*.Tests.ps1) { 6 | powershell -NoProfile -Command Invoke-Pester $testcase.FullName 7 | } 8 | } else { 9 | Write-Warning "Skipping Windows PowerShell tests" 10 | } 11 | 12 | if (Get-Command pwsh -ErrorAction SilentlyContinue) { 13 | foreach ($testcase in ls $PSScriptRoot\Tests\*.Tests.ps1) { 14 | pwsh -NoProfile -Command Invoke-Pester $testcase.FullName 15 | } 16 | } else { 17 | Write-Warning "Skipping PowerShell Core tests" 18 | } 19 | -------------------------------------------------------------------------------- /Tests/Invoke.Tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PowerShellLogging 2 | Describe "Working when called inside PowerShell.Invoke (as long as we Remove-Module)" { 3 | 4 | $Path = "TestDrive:\log.txt" 5 | $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) 6 | 7 | BeforeAll { 8 | $script:PowerShell = [PowerShell]::Create() 9 | } 10 | 11 | AfterAll { 12 | if ($script:PowerShell) { 13 | $script:PowerShell.Dispose() 14 | } 15 | } 16 | 17 | $TestScript = " 18 | Import-Module PowerShellLogging 19 | `$Logging = Enable-LogFile -Path '$Path' 20 | Write-Host 'This is a host test' 21 | 'Returned OK' 22 | Write-Verbose 'This is a verbose test' -Verbose 23 | Disable-LogFile `$Logging 24 | Remove-Module PowerShellLogging 25 | " 26 | 27 | It "Should not crash when used" { 28 | $script:Result = $script:PowerShell.AddScript($TestScript).Invoke() 29 | } 30 | 31 | It "Should not interfere with output" { 32 | $script:Result | Should -Be "Returned OK" 33 | } 34 | 35 | It "Should not cause Enable-LogFile to fail" { 36 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" 37 | } 38 | 39 | It "Should not cause Write-Host to fail" { 40 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" 41 | } 42 | 43 | It "Should not cause Write-Verbose to fail" { 44 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" 45 | } 46 | 47 | It "Should not cause Disable-LogFile to fail" { 48 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" 49 | } 50 | 51 | It "Should not cause any errors" { 52 | $script:PowerShell.Streams.Error 53 | $script:PowerShell.Streams.Error.Count | Should -Be 0 54 | } 55 | 56 | It "Should create the log file" { 57 | # this proves the logging works 58 | $Path | Should -Exist 59 | } 60 | 61 | It "Should log host output" -Skip:(!(Test-Path $Path)) { 62 | (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty 63 | } 64 | 65 | It "Should log verbose output" -Skip:(!(Test-Path $Path)) { 66 | (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty 67 | } 68 | 69 | 70 | } -------------------------------------------------------------------------------- /Tests/ParallelRemote.Tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PowerShellLogging, ThreadJob 2 | param($Count = 4) 3 | 4 | Describe "Working when called in parallel in remote runspaces" -Tag "Remoting" { 5 | 6 | $Path = "TestDrive:\log{0}.txt" 7 | $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) 8 | 9 | BeforeAll { 10 | $script:PowerShell = $( 11 | foreach($index in 1..$Count) { 12 | $LocalHost = [System.Management.Automation.Runspaces.WSManConnectionInfo]@{ComputerName = "."; EnableNetworkAccess = $true } 13 | $Runspace = [runspacefactory]::CreateRunspace($LocalHost) 14 | $Runspace.Open() 15 | $PowerShell = [PowerShell]::Create() 16 | $PowerShell.Runspace = $Runspace 17 | $PowerShell 18 | } 19 | ) 20 | } 21 | 22 | AfterAll { 23 | foreach($PS in $script:PowerShell) { 24 | $PS.Runspace.Dispose() 25 | $PS.Dispose() 26 | } 27 | } 28 | 29 | $TestScript = " 30 | Import-Module PowerShellLogging 31 | `$Logging = Enable-LogFile -Path '${Path}' 32 | Write-Host 'This is a host test from attempt {0}' 33 | '${Path}' 34 | Start-Sleep 2 35 | Write-Verbose 'This is a verbose test from attempt {0}' -Verbose 36 | Disable-LogFile `$Logging 37 | " 38 | 39 | It "Should not crash when used" { 40 | $script:Results = & { 41 | $i = 0 42 | foreach($PS in $script:PowerShell) { 43 | $i += 1 44 | Start-ThreadJob { param($PS, $Script) $PS.AddScript($Script).Invoke() } -ArgumentList $PS, ($TestScript -f $i) 45 | } 46 | } | Wait-Job | Receive-Job 47 | } 48 | 49 | It "Should not interfere with output" { 50 | $script:Results.Count | Should -Be $script:PowerShell.Count 51 | $i = 0 52 | foreach ($resultPath in $script:Results) { 53 | $i += 1 54 | $resultPath | Should -Be ($Path -f $i) 55 | } 56 | } 57 | 58 | It "Should not cause Enable-LogFile to fail" { 59 | foreach($PS in $script:PowerShell) { 60 | $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" 61 | } 62 | } 63 | 64 | It "Should not cause Write-Host to fail" { 65 | foreach ($PS in $script:PowerShell) { 66 | $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" 67 | } 68 | } 69 | 70 | It "Should not cause Write-Verbose to fail" { 71 | foreach ($PS in $script:PowerShell) { 72 | $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" 73 | } 74 | } 75 | 76 | It "Should not cause Disable-LogFile to fail" { 77 | foreach ($PS in $script:PowerShell) { 78 | $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" 79 | } 80 | } 81 | 82 | It "Should not cause any errors" { 83 | foreach ($PS in $script:PowerShell) { 84 | $PS.Streams.Error.Count | Should -Be 0 85 | } 86 | } 87 | 88 | Write-Warning "Expecting $($script:Results.Count) log files!" 89 | 90 | It "Should create the log file" { 91 | # this is enough to prove the logging works 92 | $i = 0 93 | foreach($PS in $script:PowerShell) { 94 | $i += 1 95 | ($Path -f $i) | Should -Exist 96 | } 97 | } 98 | 99 | $i = 0 100 | foreach ($PS in $script:PowerShell) { 101 | $i += 1 102 | It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { 103 | (Get-Content ($Path -f $i)) -match "This is a host test from attempt $i$" | Should -Not -BeNullOrEmpty 104 | } 105 | } 106 | 107 | $i = 0 108 | foreach ($PS in $script:PowerShell) { 109 | $i += 1 110 | It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { 111 | (Get-Content ($Path -f $i)) -match "This is a verbose test from attempt $i$" | Should -Not -BeNullOrEmpty 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /Tests/ParallelRunspace.Tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PowerShellLogging, ThreadJob 2 | param($Count = 2) 3 | 4 | Describe "Working when called simultaneously in parallel runspaces" -Tag "ThreadJob", "WIP" { 5 | 6 | $Path = "TestDrive:\log{0}.txt" 7 | $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) 8 | 9 | $TestScript = " 10 | Import-Module PowerShellLogging 11 | `$Logging = Enable-LogFile -Path '${Path}' 12 | Write-Host 'This is a host test from attempt {0}' 13 | '${Path}' 14 | Start-Sleep 2 15 | Write-Verbose 'This is a verbose test from attempt {0}' -Verbose 16 | Disable-LogFile `$Logging 17 | " 18 | 19 | $TestScript = { 20 | param($Path, $Index) 21 | Import-Module PowerShellLogging 22 | $Logging = Enable-LogFile -Path $Path 23 | Microsoft.PowerShell.Utility\Write-Host "This is a host test from attempt $index" 24 | $Path 25 | Microsoft.PowerShell.Utility\Write-Verbose 'This is a verbose test' -Verbose 26 | # Does not output, because Verbose is suppressed 27 | Microsoft.PowerShell.Utility\Write-Verbose "This is a verbose test from attempt $index" -Verbose 28 | Disable-LogFile $Logging 29 | } 30 | 31 | 32 | It "Should not crash when used" { 33 | $script:Results = & { 34 | foreach ($i in 1..$Count) { 35 | Start-ThreadJob $TestScript -ArgumentList ($Path -f $i), $i 36 | } 37 | } | Wait-Job | Receive-Job -ErrorVariable Ev 38 | $Script:Ev = $Ev 39 | } 40 | 41 | It "Should not interfere with output" { 42 | $script:Results.Count | Should -Be $Count 43 | $i = 0 44 | foreach ($resultPath in $script:Results) { 45 | $i += 1 46 | $resultPath | Should -Be ($Path -f $i) 47 | } 48 | } 49 | 50 | It "Should not cause Enable-LogFile to fail" { 51 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" 52 | } 53 | 54 | It "Should not cause Write-Host to fail" { 55 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" 56 | } 57 | 58 | It "Should not cause Write-Verbose to fail" { 59 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" 60 | } 61 | 62 | It "Should not cause Disable-LogFile to fail" { 63 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" 64 | } 65 | 66 | It "Should not cause any errors" { 67 | $script:Ev.Count | Should -Be 0 68 | } 69 | 70 | 71 | Write-Warning "Expecting $($script:Results.Count) log files!" 72 | 73 | It "Should create the log file" { 74 | # this is enough to prove the logging works 75 | foreach ($i in 1..$Count) { 76 | ($Path -f $i) | Should -Exist 77 | } 78 | } 79 | 80 | 81 | foreach ($i in 1..$Count) { 82 | It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { 83 | (Get-Content ($Path -f $i)) -match "This is a host test from attempt $i$" | Should -Not -BeNullOrEmpty 84 | } 85 | } 86 | 87 | 88 | foreach ($i in 1..$Count) { 89 | It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { 90 | (Get-Content ($Path -f $i)) -match "This is a verbose test from attempt $i$" | Should -Not -BeNullOrEmpty 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Tests/Remote.Tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PowerShellLogging 2 | Describe "Working when called in a remote runspace" -Tag "Remoting" { 3 | 4 | $Path = "TestDrive:\log.txt" 5 | $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) 6 | 7 | BeforeAll { 8 | $LocalHost = [System.Management.Automation.Runspaces.WSManConnectionInfo]@{ComputerName = "."; EnableNetworkAccess = $true } 9 | $Runspace = [runspacefactory]::CreateRunspace($LocalHost) 10 | $Runspace.Open() 11 | 12 | $script:PowerShell = [PowerShell]::Create() 13 | $script:PowerShell.Runspace = $Runspace 14 | } 15 | 16 | AfterAll { 17 | if ($script:PowerShell) { 18 | $script:PowerShell.Runspace.Dispose() 19 | $script:PowerShell.Dispose() 20 | } 21 | } 22 | 23 | $TestScript = " 24 | Import-Module PowerShellLogging 25 | `$Logging = Enable-LogFile -Path '$Path' 26 | Write-Host 'This is a host test' 27 | 'Returned OK' 28 | Write-Verbose 'This is a verbose test' -Verbose 29 | Disable-LogFile `$Logging 30 | " 31 | 32 | It "Should not crash when used" { 33 | $script:Result = $script:PowerShell.AddScript($TestScript).Invoke() 34 | } 35 | 36 | It "Should not interfere with output" { 37 | $script:Result | Should -Be "Returned OK" 38 | } 39 | 40 | It "Should not cause Enable-LogFile to fail" { 41 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" 42 | } 43 | 44 | It "Should not cause Write-Host to fail" { 45 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" 46 | } 47 | 48 | It "Should not cause Write-Verbose to fail" { 49 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" 50 | } 51 | 52 | It "Should not cause Disable-LogFile to fail" { 53 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" 54 | } 55 | 56 | It "Should not cause any errors" { 57 | $script:PowerShell.Streams.Error.Count | Should -Be 0 58 | } 59 | 60 | It "Should create the log file" { 61 | # this proves the logging works 62 | $Path | Should -Exist 63 | } 64 | 65 | It "Should log host output" -Skip:(!(Test-Path $Path)) { 66 | (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty 67 | } 68 | 69 | It "Should log verbose output" -Skip:(!(Test-Path $Path)) { 70 | (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty 71 | } 72 | 73 | 74 | } -------------------------------------------------------------------------------- /Tests/Runspace.Tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PowerShellLogging 2 | Describe "Working when called synchronously in a parallel runspace (as long as we Remove-Module)" { 3 | 4 | $Path = "TestDrive:\log.txt" 5 | $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) 6 | 7 | BeforeAll { 8 | $script:PowerShell = [PowerShell]::Create() 9 | $PowerShell.Runspace = [runspacefactory]::CreateRunspace() 10 | $PowerShell.Runspace.Open() 11 | } 12 | 13 | AfterAll { 14 | if ($script:PowerShell) { 15 | $script:PowerShell.Runspace.Dispose() 16 | $script:PowerShell.Dispose() 17 | } 18 | } 19 | 20 | $TestScript = " 21 | Import-Module PowerShellLogging 22 | `$Logging = Enable-LogFile -Path '$Path' 23 | Write-Host 'This is a host test' 24 | 'Returned OK' 25 | Write-Verbose 'This is a verbose test' -Verbose 26 | Disable-LogFile `$Logging 27 | Remove-Module PowerShellLogging 28 | " 29 | 30 | It "Should not crash when used" { 31 | $script:Result = $script:PowerShell.AddScript($TestScript).Invoke() 32 | } 33 | 34 | It "Should not interfere with output" { 35 | $script:Result | Should -Be "Returned OK" 36 | } 37 | 38 | It "Should not cause Enable-LogFile to fail" { 39 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" 40 | } 41 | 42 | It "Should not cause Write-Host to fail" { 43 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" 44 | } 45 | 46 | It "Should not cause Write-Verbose to fail" { 47 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" 48 | } 49 | 50 | It "Should not cause Disable-LogFile to fail" { 51 | $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" 52 | } 53 | 54 | It "Should not cause any errors" { 55 | $script:PowerShell.Streams.Error.Count | Should -Be 0 56 | } 57 | 58 | It "Should create the log file" { 59 | # this proves the logging works 60 | $Path | Should -Exist 61 | } 62 | 63 | It "Should log host output" -Skip:(!(Test-Path $Path)) { 64 | (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty 65 | } 66 | 67 | It "Should log verbose output" -Skip:(!(Test-Path $Path)) { 68 | (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty 69 | } 70 | 71 | 72 | } -------------------------------------------------------------------------------- /Tests/Simple.Tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PowerShellLogging 2 | Describe "Working in scripts run locally in the host (as long as we Remove-Module)" { 3 | 4 | $Path = "TestDrive:\log.txt" 5 | $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) 6 | 7 | $TestScript = { 8 | [CmdletBinding()]param() 9 | 10 | Import-Module PowerShellLogging 11 | $Logging = Enable-LogFile -Path $Path 12 | Write-Host 'This is a host test' 13 | 'Returned OK' 14 | Write-Verbose 'This is a verbose test' -verbose 15 | Write-Verbose 'This is a another verbose test' 16 | Disable-LogFile $Logging 17 | Remove-Module PowerShellLogging 18 | } 19 | 20 | It "Should not crash when used" { 21 | $script:Result = & $TestScript -ErrorVariable Ev 22 | $script:Ev = $Ev 23 | } 24 | 25 | It "Should not interfere with output" { 26 | $script:Result | Should -Be "Returned OK" 27 | } 28 | 29 | It "Should not cause Enable-LogFile to fail" { 30 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" 31 | } 32 | 33 | It "Should not cause Write-Host to fail" { 34 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" 35 | } 36 | 37 | It "Should not cause Write-Verbose to fail" { 38 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" 39 | } 40 | 41 | It "Should not cause Disable-LogFile to fail" { 42 | $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" 43 | } 44 | 45 | It "Should not cause any errors" { 46 | $script:Ev.Count | Should -Be 0 47 | } 48 | 49 | It "Should create the log file" { 50 | # this proves the logging works 51 | $Path | Should -Exist 52 | } 53 | 54 | It "Should log host output" -Skip:(!(Test-Path $Path)) { 55 | (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty 56 | } 57 | 58 | It "Should log verbose output" -Skip:(!(Test-Path $Path)) { 59 | (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty 60 | } 61 | 62 | It "Should not log verbose that's not output" -Skip:(!(Test-Path $Path)) { 63 | (Get-Content $Path) -match ".*This is another verbose test$" | Should -BeNullOrEmpty 64 | } 65 | 66 | } --------------------------------------------------------------------------------